create-caspian-app 1.8.2 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/AGENTS.md
CHANGED
|
@@ -71,11 +71,11 @@ Authoring is **Python-only and single-file**. There are no `.html` sidecars in t
|
|
|
71
71
|
|
|
72
72
|
None of these is required; add one only when the app actually wants that behavior. What the table settles is _how_ — when a task does call for one, the answer is this file, never a hand-built equivalent:
|
|
73
73
|
|
|
74
|
-
| Behavior, when wanted
|
|
75
|
-
|
|
|
76
|
-
| Shared shell for a subtree
|
|
77
|
-
| Loading UI during route navigation
|
|
78
|
-
| Global 404
|
|
74
|
+
| Behavior, when wanted | File | Export |
|
|
75
|
+
| --------------------------------------- | ------------------------------------- | ------------------------ |
|
|
76
|
+
| Shared shell for a subtree | `layout.py` | `layout()` |
|
|
77
|
+
| Loading UI during route navigation | `loading.py` | `loading()` |
|
|
78
|
+
| Global 404 | `src/app/not_found.py` | `page()` |
|
|
79
79
|
| Error page for a subtree (nearest wins) | `error.py` (root: `src/app/error.py`) | `page(error: ErrorInfo)` |
|
|
80
80
|
|
|
81
81
|
**Failing on purpose is `raise HttpError(status, message)` from `casp.errors`** — in a page, layout, or `@rpc()` — never hand-built error markup or JSON. 4xx messages are shown to users; 5xx messages and unexpected exceptions are replaced by a generic sentence in production (use `HttpError.public(...)` or `public_message=` to choose visible text). A page failure renders the nearest ancestor `error.py`, wrapped by the layouts at and above that folder; a 404 renders `not_found.py`; RPCs keep `{"error", "requestId", ...details}`. Every response carries `X-Request-Id` (`casp.observability.request_id()`), and error responses are `no-store`. Deep dive: `node_modules/caspian-utils/dist/docs/error-handling.md`.
|
|
@@ -340,10 +340,10 @@ Use `.github/copilot-instructions.md` for the repo-wide implementation rules. Th
|
|
|
340
340
|
- **A child whose props did not change is not re-walked.** `refreshPropsFromParent` only re-runs `bootstrapNestedComponents()` when the child produced nested runtime structure in its own last render (`hadNestedRuntimeStructures`, the same condition `render()` uses). For a leaf component — every card in a shell — the pass traversed nothing, rebuilt an empty provider set and collected an always-empty descendant list, once per child per parent render.
|
|
341
341
|
- **Every per-render capture store mints ids from a sequence that restarts at zero each render** (the `ppref_`, `ppinput_`, `ppselect_`, `ppchecked_`, `ppcontext*_`, `ppdefault*_` and `ppv_` families), so unchanged markup re-renders to an identical string. Do not give any of them a globally increasing counter: the loop capture store used to, which made every row carrying a per-row handler byte-different on every render and defeated both the byte-identical render skip and per-row reuse. These ids are also lifted out of event-handler source so all rows of one loop share a single compiled handler function — if an id format changes, the matching extraction must change with it, or every row compiles and caches its own handler.
|
|
342
342
|
- When `caspian.config.json` has `websocket: true`, socket behavior is app-owned: the single named-socket endpoint is wired in `main.py` and the layer lives in `src/lib/websocket/**`. Routes do not pass `websocket_path`/`websocket_url` into templates — `pp.socket(...)` already knows the shared endpoint; a route only names its `@socket()` function.
|
|
343
|
-
- **Named sockets are this workspace's preferred live-channel layer.** `src/lib/websocket/sockets.py` is the server half of `pp.socket(...)`: `@socket()` registers an async function by its own name (application-wide, duplicate names refused at registration), every connection lands on the single `SOCKET_PATH` endpoint (`/__pulsepoint/ws` — named for the PulsePoint runtime so every backend serving `pp.socket` uses the same path; wired in `main.py`, gated on `websocket: true`), the arguments arrive as the first frame (one JSON object, filtered against the handler signature like rpc payloads), and failure travels as an `{"error": "..."}` frame followed by a close. The handler declares a `socket` parameter (`Socket`: `recv`/`recv_text`/`send`/`sender`/`close`); `socket.sender()` + `SocketPool` is the broadcast pattern (see `src/app/chat/`). `@socket(require_auth=True, allowed_roles=[...])` delegates to `Auth`; the endpoint keeps the origin check, connection cap, message-size limit, per-connection rate, and idle timeout (
|
|
343
|
+
- **Named sockets are this workspace's preferred live-channel layer.** `src/lib/websocket/sockets.py` is the server half of `pp.socket(...)`: `@socket()` registers an async function by its own name (application-wide, duplicate names refused at registration), every connection lands on the single `SOCKET_PATH` endpoint (`/__pulsepoint/ws` — named for the PulsePoint runtime so every backend serving `pp.socket` uses the same path; wired in `main.py`, gated on `websocket: true`), the arguments arrive as the first frame (one JSON object, filtered against the handler signature like rpc payloads), and failure travels as an `{"error": "..."}` frame followed by a close. The handler declares a `socket` parameter (`Socket`: `recv`/`recv_text`/`send`/`sender`/`close`); `socket.sender()` + `SocketPool` is the broadcast pattern (see `src/app/chat/`). `@socket(require_auth=True, allowed_roles=[...])` delegates to `Auth`; the endpoint keeps the origin check, connection cap, message-size limit, per-connection rate, and idle timeout (traffic in either direction counts as liveness). **Quiet is not dead:** `pp.socket` heartbeats with `{"__pp": "ping"}` every 25 s, a per-connection reader task in `Socket` answers `{"__pp": "pong"}` even when the handler never calls `recv()`, the idle timeout closes an unresponsive peer with code 4000 (never 1000), and the client reconnects with backoff after every close except `close()`, an error frame, 1000 (handler returned), or a policy code — resending the argument frame, so a handler must be safe to re-enter. A send-only handler (a notification feed) parks its sender in a pool and `await socket.wait_closed()`. A socket in a route's `index.py` registers when the route first renders; shared sockets live in `src/lib/**`. The shipped browser runtime (`public/js/pp-reactive-v2.min.js`) connects `pp.socket(...)` to that same default path, so treat `SOCKET_PATH` as fixed unless the served runtime's default changes with it. This is the only socket layer: hand-written `@app.websocket(...)` + native `WebSocket` is reserved for wires the JSON-frame contract cannot carry (binary, non-JSON protocols) and must run the same origin check and `Auth` delegation itself. Read `node_modules/caspian-utils/dist/docs/websockets.md` "Named Sockets"; tests in `tests/test_socket.py`.
|
|
344
344
|
- Socket auth policy is per socket, not per endpoint: `@socket()` is public, `@socket(require_auth=True)` needs a session, `@socket(allowed_roles=[...])` adds RBAC — all delegating to Caspian's `Auth` (`Auth.set_request(websocket)` plus `is_authenticated`/`get_payload`/`check_role`) inside `sockets.py`. **The old public/private channel layer is gone**: there are no `/ws/live` / `/ws/public` endpoints, no `authorize_websocket(...)` guard, and no `WebSocketConnectionManager` pools — do not reintroduce them or write per-endpoint session parsing. Keep authenticated and guest traffic in separate `SocketPool`s, and treat the socket session as read-only (mutations are not persisted to the cookie over a WebSocket).
|
|
345
345
|
- For socket clients, use `pp.socket(name, args, handlers)` inside the owning component script: open it in `pp.effect(..., [])`, keep the handle in `pp.ref(...)`, close it in the effect cleanup. Reach for a native `new WebSocket(...)` only for a wire the named-socket contract cannot carry (binary frames, non-JSON protocols).
|
|
346
|
-
- Before changing socket security, verify the running code in `src/lib/websocket/sockets.py` — it owns the whole surface: origin allow-list, `MAX_WEBSOCKET_CONNECTIONS`, auth delegation, idle timeout
|
|
346
|
+
- Before changing socket security, verify the running code in `src/lib/websocket/sockets.py` — it owns the whole surface: origin allow-list, `MAX_WEBSOCKET_CONNECTIONS`, auth delegation, idle timeout (close code 4000) and heartbeat answering, message-size limit, per-connection message rate, and the error-frame-then-close behavior. HTTP route privacy and `AuthMiddleware` do not by themselves protect WebSocket scopes: the HTTP middleware stack early-returns on `scope["type"] == "websocket"`, so only `SessionMiddleware` runs and the socket endpoint authorizes each connection itself.
|
|
347
347
|
- Keep route-specific backend logic in that route's `src/app/**/index.py`, including first-render data loading, route-owned `@rpc()` actions, auth checks, redirects, and validation. Move logic to `src/lib/**` only when it is shared by more than one route, feature, component, or integration.
|
|
348
348
|
- The `pp` component-script API mirrors React hooks **inside the `<script>` only** — the surrounding markup is never JSX: `state`, `effect`, `layoutEffect`, `ref`, `memo`, `callback`, `reducer`, `context`, `portal`, `id`, `syncExternalStore`, `imperativeHandle`, `transition`, `deferredValue`, `optimistic`, `errorBoundary`, plus `props`. Use `pp.id()` for generated `id`/`for`/`aria-*` values, `pp.syncExternalStore(...)` for sources the component does not own, and wrap failure-prone subtrees in a parent with `pp.errorBoundary()` instead of letting a render throw reach the console. Verify against `public/js/pp-reactive-v2.min.js` and see `pulsepoint.md` "Hooks and runtime API".
|
|
349
349
|
- For grouped-subtree SPA navigation UX, the current browser runtime keeps unmarked shell scrollers stable and uses `pp-reset-scroll="true"` on the content pane that should reset. Check `pulsepoint.md`, `routing.md`, and `public/js/pp-reactive-v2.min.js` before changing that behavior.
|