caspian-utils 0.1.2 → 0.1.4

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.
@@ -64,6 +64,7 @@ Use prompts like these to check whether AI lands on the correct docs and files.
64
64
  | Decide whether a dashboard child route should use page caching. | [index.md](./index.md), [cache.md](./cache.md), [routing.md](./routing.md), [fetch-data.md](./fetch-data.md) | `main.py`, `.venv/Lib/site-packages/casp/cache_handler.py`, the route's `index.py` | route-level cache ownership, cacheable HTML scope, and invalidation points |
65
65
  | Add a PulsePoint context provider to a reusable component. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `ts/TemplateCompiler.ts`, `public/js/pp-reactive-v2.js`, component `.html`, `.venv/Lib/site-packages/casp/components_compiler.py` | lowercase HTML-first `*.provider` usage, logical ancestry, single-root template shape |
66
66
  | Add upload progress to an existing file manager. | [index.md](./index.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md) | `src/app/**/index.py`, `src/app/**/index.html`, `src/lib/**`, `public/js/pp-reactive-v2.js` | `pp.rpc` upload options, state replacement, persisted metadata, BrowserSync ignore rules |
67
+ | Add or debug a live WebSocket channel. | [index.md](./index.md), [websockets.md](./websockets.md), [core-runtime-map.md](./core-runtime-map.md), [pulsepoint.md](./pulsepoint.md), [auth.md](./auth.md) | `caspian.config.json`, `main.py`, `src/lib/websocket/**`, `src/app/**`, `settings/bs-config.json` | `websocket` feature gate, endpoint registration, origin checks, auth/session handling, native `WebSocket` client lifecycle, and BrowserSync URL selection |
67
68
  | Decide whether MCP files should be created. | [index.md](./index.md), [mcp.md](./mcp.md), [commands.md](./commands.md), [project-structure.md](./project-structure.md) | `caspian.config.json`, `settings/restart-mcp.ts`, `package.json`, `src/lib/mcp/**` when enabled | `mcp` feature gate, update workflow, nested FastMCP config ownership |
68
69
 
69
70
  Treat the table as a prompt pack for spot checks, not as a full validation matrix.
@@ -179,9 +179,12 @@ In skip-prompt mode, the default feature values are:
179
179
 
180
180
  - `backendOnly: false`
181
181
  - `tailwindcss: false`
182
- - `typescript: false`
183
- - `mcp: false`
184
- - `prisma: false`
182
+ - `typescript: false`
183
+ - `mcp: false`
184
+ - `prisma: false`
185
+ - `websocket: false`
186
+
187
+ If a project needs WebSockets after creation, set `websocket: true` in `caspian.config.json`, then run the project update workflow so framework-managed files and any scaffolded socket surfaces stay aligned before assuming `@app.websocket(...)` routes or `src/lib/websocket/**` exist.
185
188
 
186
189
  ### 4.2 Backend-only combinations
187
190
 
@@ -7,8 +7,9 @@ related:
7
7
  links:
8
8
  - /docs/project-structure
9
9
  - /docs/routing
10
- - /docs/auth
10
+ - /docs/auth
11
11
  - /docs/fetch-data
12
+ - /docs/websockets
12
13
  - /docs/pulsepoint
13
14
  - /docs/pulsepoint-runtime-map
14
15
  - /docs/index
@@ -59,6 +60,7 @@ Use this table when the task names a framework feature but the owning file is no
59
60
  | Baseline response headers and safe public-file serving | `main.py` imports from `casp.runtime_security` | `.venv/Lib/site-packages/casp/runtime_security.py` | [auth.md](./auth.md), [project-structure.md](./project-structure.md) |
60
61
  | RPC and server actions | route or component Python modules with `@rpc()` | `casp.rpc`, `main.py` middleware | [fetch-data.md](./fetch-data.md), [file-uploads.md](./file-uploads.md) |
61
62
  | Streaming | route `page()` generators, RPC generators | `casp.streaming`, `casp.rpc`, `main.py` | [fetch-data.md](./fetch-data.md) |
63
+ | WebSockets | `main.py` `@app.websocket(...)` endpoints when `caspian.config.json` has `websocket: true`, `src/lib/**` socket helpers, route-owned browser clients in `src/app/**` | FastAPI app-owned ASGI endpoints in `main.py` | [websockets.md](./websockets.md), [auth.md](./auth.md), [pulsepoint.md](./pulsepoint.md) |
62
64
  | Server state | request handlers and RPC actions | `casp.state_manager`, `main.py` middleware | [state.md](./state.md) |
63
65
  | Page caching | route-level `Cache(...)` declarations | `casp.cache_handler`, `main.py` cache check/save | [cache.md](./cache.md) |
64
66
  | Validation | route and RPC input boundaries | `casp.validate` | [validation.md](./validation.md) |
@@ -111,7 +113,8 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
111
113
  | Runtime area | Verify these behaviors |
112
114
  | --- | --- |
113
115
  | `main.py` routing and request flow | route registration, path and query injection, static asset handling, session-middleware wiring, response-header middleware, and exception rendering |
114
- | `public/js/pp-reactive-v2.js` browser runtime | SPA interception, history-vs-push scroll behavior, `pp-reset-scroll` semantics, and browser-side `pp.rpc()` behavior |
116
+ | `main.py` WebSockets | `cfg.websocket` route-registration gate, `@app.websocket(...)` paths, origin checks before `accept()`, auth/session extraction, message-size and idle-timeout handling, close codes, and connection cleanup |
117
+ | `public/js/pp-reactive-v2.js` browser runtime | SPA interception, history-vs-push scroll behavior, `pp-reset-scroll` semantics, and browser-side `pp.rpc()` behavior |
115
118
  | `casp.auth` | auth settings, signin and signout flow, provider wiring, and page protection behavior |
116
119
  | `casp.rpc` and streamed RPC responses | middleware interception, CSRF and session expectations, registry behavior, and helper-level RPC contracts |
117
120
  | `casp.layout` | layout discovery, metadata merge, root handling, and layout rendering rules |
@@ -23,6 +23,8 @@ This page explains how data fetching works in Caspian. Use route functions for i
23
23
 
24
24
  Treat RPC as the default way for browser code to talk to Python in Caspian. For CRUD operations and any browser-initiated backend reads after first render, default to `@rpc()` on the server and `pp.rpc()` in PulsePoint code. Do not reach for ad hoc fetch calls to custom JSON endpoints, alternate transport layers, or older helper names unless the task explicitly requires that shape.
25
25
 
26
+ WebSockets are the main exception for long-lived bidirectional live channels when `caspian.config.json` has `websocket: true`. If the task needs a native browser `WebSocket`, persistent broadcast channel, presence, live chat, or socket origin/auth handling, confirm that flag and read [websockets.md](./websockets.md) before adding endpoint or client code. Do not use WebSockets as a replacement for ordinary RPC form submits or CRUD work.
27
+
26
28
  When `caspian.config.json` has `prisma: true`, the Python side of those route and RPC actions must use the generated Prisma Python ORM from `src/lib/prisma/**` for database reads and writes. The browser asks Python through `pp.rpc(...)`; Python asks the database through Prisma. Do not replace either side with a custom browser fetch, direct database driver, hand-written SQL helper, JSON active store, or second app-owned data-fetch abstraction.
27
29
 
28
30
  Browser-triggered data work should still be PulsePoint-first at the event layer. Bind the initiating click, submit, input, upload, refresh, filter, or pagination control in authored HTML with `onclick`, `onsubmit`, `oninput`, `onchange`, or another native `on*` attribute handled by PulsePoint. For normal form submissions, read named controls with `Object.fromEntries(new FormData(event.currentTarget).entries())` in the submit handler and pass that object directly to `pp.rpc(...)`; the route's Python `@rpc()` action should validate, normalize, and decide what to persist. Do not set up first-party data actions by assigning ids and then wiring `querySelector(...)`, `addEventListener(...)`, manual `fetch(...)`, manual DOM repainting, or per-input `pp-ref` payload collection.
@@ -184,7 +186,8 @@ Important:
184
186
 
185
187
  - `pp.rpc()` posts to the current route RPC bridge.
186
188
  - Older docs may refer to `pp.fetchFunction()`. In this repo's current runtime, the supported helper is `pp.rpc()`.
187
- - Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
189
+ - Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when debugging browser-side RPC options such as streaming, upload progress, abort behavior, redirects, or SPA interaction.
190
+ - Use [websockets.md](./websockets.md) when `caspian.config.json` has `websocket: true` and the browser and server need a persistent bidirectional channel instead of request/response RPC or one-way SSE streaming.
188
191
 
189
192
  ## Streaming Responses
190
193
 
@@ -11,8 +11,9 @@ related:
11
11
  - /docs/core-runtime-map
12
12
  - /docs/pulsepoint-runtime-map
13
13
  - /docs/mcp
14
- - /docs/file-uploads
15
- - /docs/file-conventions
14
+ - /docs/file-uploads
15
+ - /docs/websockets
16
+ - /docs/file-conventions
16
17
  - /docs/project-structure
17
18
  - /docs/components
18
19
  ---
@@ -23,7 +24,7 @@ Treat these docs as reusable Caspian feature guidance. Treat `./caspian.config.j
23
24
 
24
25
  The docs can mention optional features even when those features are disabled in a project. Their job is to explain how a feature works, when a doc applies, and which files to inspect next once the feature is confirmed as relevant.
25
26
 
26
- Before making feature, tooling, or file-placement decisions in a Caspian project, read `./caspian.config.json` almost immediately. That file tells you which optional capabilities are enabled, such as Prisma, MCP, TypeScript, Tailwind, backend-only mode, and component scan directories.
27
+ Before making feature, tooling, or file-placement decisions in a Caspian project, read `./caspian.config.json` almost immediately. That file tells you which optional capabilities are enabled, such as Prisma, MCP, WebSockets, TypeScript, Tailwind, backend-only mode, and component scan directories.
27
28
 
28
29
  ## Default Stack
29
30
 
@@ -34,6 +35,7 @@ When generating or editing a Caspian app, treat these as the default choices unl
34
35
  - Treat every authored route, layout, and component HTML file like a React component return value: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` kept inside that same root.
35
36
  - When `caspian.config.json` has `tailwindcss: true`, use Python `merge_classes(...)` plus browser `twMerge(...)` as the only supported Tailwind class-merging path.
36
37
  - Use `@rpc()` plus `pp.rpc()` for browser-triggered reads, writes, streaming, and uploads.
38
+ - When `caspian.config.json` has `websocket: true`, use app-owned FastAPI WebSocket endpoints only for long-lived bidirectional live channels; keep normal browser-triggered data work on RPC.
37
39
  - When `caspian.config.json` has `prisma: true`, use the generated Prisma Python ORM from `src/lib/prisma/**` for Python-side database reads and writes. Do not invent a second fetch layer, raw driver wrapper, JSON active store, or browser-side data path that bypasses Prisma.
38
40
  - Use `Validate` and `Rule` from `casp.validate` for server-side input validation and sanitization.
39
41
 
@@ -83,8 +85,9 @@ The packaged Caspian docs referenced by this index live here:
83
85
  - `file-conventions.md` - quick decision guide for `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, and `error.html`, plus the owning runtime files to verify
84
86
  - `components.md` - Create reusable Python components, template-backed UI, HTML-first `x-*` component tags, the single-parent authored-root rule for component HTML files, and the Python-side `merge_classes(...)` contract when Tailwind CSS is enabled
85
87
  - `pulsepoint.md` - Default reactive frontend runtime contract for component scripts, first-party `on*` events, state, effects, directives, SPA navigation scroll restoration, `pp-reset-scroll`, and direct browser `twMerge(...)` usage when Tailwind CSS is enabled
86
- - `fetch-data.md` - Initial server-side data loading and browser-triggered RPC flows with `pp.rpc()`, streaming, uploads, and auth-aware actions
87
- - `file-uploads.md` - Route-local file uploads and file-manager flows with `@rpc()`, `pp.rpc()`, Prisma metadata, public asset storage, and BrowserSync ignore rules
88
+ - `fetch-data.md` - Initial server-side data loading and browser-triggered RPC flows with `pp.rpc()`, streaming, uploads, and auth-aware actions
89
+ - `websockets.md` - WebSocket feature-gate guidance for projects where `caspian.config.json` has `websocket: true`, app-owned FastAPI endpoint placement, browser `WebSocket` clients inside PulsePoint templates, origin checks, auth/session handling, and when to choose sockets over RPC or SSE
90
+ - `file-uploads.md` - Route-local file uploads and file-manager flows with `@rpc()`, `pp.rpc()`, Prisma metadata, public asset storage, and BrowserSync ignore rules
88
91
  - `state.md` - Request-scoped server state with `StateManager`, session-backed JSON persistence, and listener callbacks for transient flows
89
92
  - `cache.md` - Route-level HTML caching with `Cache`, `CacheHandler`, TTL behavior, file-system storage, and invalidation patterns
90
93
  - `validation.md` - Input validation and sanitization with `Validate`, `Rule`, direct field checks, and multi-rule workflows for routes and RPC actions
@@ -103,14 +106,15 @@ Preferred lookup order:
103
106
  3. Before authoring or editing any `src/app/**` or component HTML template, apply the single-root invariant: one authored root only, any owned `<script>` inside that root, and no handwritten `pp-component` or `type="text/pp"`.
104
107
  4. Before inventing browser JavaScript, check whether the interaction is first-party UI behavior. If it is, use PulsePoint `on*` attributes, `pp.state`, refs, effects, directives, and `pp.rpc()` rather than id-driven DOM scripting.
105
108
  5. When Prisma is enabled, route all Python database work through the generated Prisma Python ORM. Use `database.md` before schema, migration, seed, or ORM decisions, and use `fetch-data.md` before wiring route data or RPC flows.
106
- 6. Treat `caspian.config.json` as the single source of truth for optional features. Use feature-specific docs only when the matching flag is enabled. If a feature is disabled and the user wants it, ask first, then update `caspian.config.json` and follow the update workflow in `commands.md`.
107
- 7. If the task touches `main.py` or `.venv/Lib/site-packages/casp/**`, read `core-runtime-map.md` to jump to the controlling runtime file and the matching feature doc.
108
- 8. If the task names a PulsePoint feature or directive, read `pulsepoint-runtime-map.md` for the fastest feature-to-runtime lookup, then read `pulsepoint.md` for authoring rules.
109
- 9. After the feature is confirmed, inspect the actual project files that decide behavior, such as `package.json`, `main.py`, `src/app/**`, `src/lib/**`, `settings/**`, `prisma/**`, and the installed `casp` runtime.
110
- 10. Use `file-conventions.md` for quick decisions about `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, and `error.html`; use `commands.md` for scaffold and update workflows, `project-structure.md` for placement decisions, and the feature docs such as `mcp.md`, `database.md`, `auth.md`, `fetch-data.md`, and `file-uploads.md` for task-specific guidance.
111
- 11. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
112
- 12. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
113
- 13. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
109
+ 6. If the task asks for WebSockets, live bidirectional channels, socket origin checks, or native browser `WebSocket` clients, confirm `caspian.config.json` has `websocket: true`, read `websockets.md`, then inspect `main.py`, `src/lib/**`, the owning `src/app/**` route, and `settings/bs-config.json`. If `websocket` is false and the user wants it, ask first, then enable the config flag and follow the update workflow.
110
+ 7. Treat `caspian.config.json` as the single source of truth for optional features. Use feature-specific docs only when the matching flag is enabled. If a feature is disabled and the user wants it, ask first, then update `caspian.config.json` and follow the update workflow in `commands.md`.
111
+ 8. If the task touches `main.py` or `.venv/Lib/site-packages/casp/**`, read `core-runtime-map.md` to jump to the controlling runtime file and the matching feature doc.
112
+ 9. If the task names a PulsePoint feature or directive, read `pulsepoint-runtime-map.md` for the fastest feature-to-runtime lookup, then read `pulsepoint.md` for authoring rules.
113
+ 10. After the feature is confirmed, inspect the actual project files that decide behavior, such as `package.json`, `main.py`, `src/app/**`, `src/lib/**`, `settings/**`, `prisma/**`, and the installed `casp` runtime.
114
+ 11. Use `file-conventions.md` for quick decisions about `index.html`, `index.py`, `layout.html`, `layout.py`, `loading.html`, `not-found.html`, and `error.html`; use `commands.md` for scaffold and update workflows, `project-structure.md` for placement decisions, and the feature docs such as `mcp.md`, `database.md`, `auth.md`, `fetch-data.md`, `websockets.md`, and `file-uploads.md` for task-specific guidance.
115
+ 12. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
116
+ 13. Use `ai-validation-checklist.md` when you want to verify that the docs lead AI to the correct files and behavior checkpoints.
117
+ 14. Keep `index.md` and cross-links aligned so AI can quickly discover the right doc.
114
118
 
115
119
  ## Maintenance
116
120
 
@@ -31,7 +31,7 @@ In that layout, the default stack is Python components for reusable UI, PulsePoi
31
31
 
32
32
  For public pages that can safely reuse rendered HTML, Caspian also supports route-level page caching through `casp.cache_handler`.
33
33
 
34
- Before an AI agent decides which Caspian features are available in a workspace, it should read `./caspian.config.json` almost immediately. That file is the feature gate for project capabilities such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, and `componentScanDirs`.
34
+ Before an AI agent decides which Caspian features are available in a workspace, it should read `./caspian.config.json` almost immediately. That file is the feature gate for project capabilities such as `backendOnly`, `tailwindcss`, `mcp`, `prisma`, `typescript`, `websocket`, and `componentScanDirs`.
35
35
 
36
36
  Treat `caspian.config.json` as the single source of truth for optional feature enablement. Use feature-specific files and docs only after the matching flag is confirmed as enabled. If a feature is disabled and the user wants it, ask first, then update `caspian.config.json` and follow the update workflow in `commands.md`.
37
37
 
@@ -40,9 +40,10 @@ Treat `caspian.config.json` as the single source of truth for optional feature e
40
40
  - `src/` contains routes, page templates, styles, reusable components, and shared libraries.
41
41
  - `src/components/` contains reusable application UI components and optional same-name HTML templates.
42
42
  - `src/lib/` contains reusable non-UI code such as helpers, services, validators, adapters, and shared support modules.
43
- - `src/lib/auth/auth_config.py` contains auth-specific configuration for the app.
44
- - `src/lib/mcp/` contains the app-owned FastMCP server and nested FastMCP config when MCP is enabled.
45
- - `prisma/` contains the Prisma schema and seed scripts.
43
+ - `src/lib/auth/auth_config.py` contains auth-specific configuration for the app.
44
+ - `src/lib/mcp/` contains the app-owned FastMCP server and nested FastMCP config when MCP is enabled.
45
+ - `src/lib/websocket/` contains reusable socket helpers when WebSockets are enabled and the project includes shared session, auth, connection, or broadcast utilities.
46
+ - `prisma/` contains the Prisma schema and seed scripts.
46
47
  - `public/` contains static assets served directly.
47
48
  - `settings/` contains BrowserSync, build, restart, and generated-project helper files.
48
49
  - `main.py` is the application entry point.
@@ -77,9 +78,11 @@ my-app/
77
78
  lib/
78
79
  auth/
79
80
  auth_config.py
80
- mcp/
81
- fastmcp.json
82
- mcp_server.py
81
+ mcp/
82
+ fastmcp.json
83
+ mcp_server.py
84
+ websocket/
85
+ websocket_security.py
83
86
  prisma/
84
87
  __init__.py
85
88
  db.py
@@ -94,7 +97,7 @@ my-app/
94
97
  docs/
95
98
  ```
96
99
 
97
- Optional directories such as `src/lib/mcp/` appear only when the relevant feature flag is enabled in `caspian.config.json`.
100
+ Optional directories such as `src/lib/mcp/` and `src/lib/websocket/` appear only when the relevant feature flag is enabled in `caspian.config.json` and the project needs that app-owned surface.
98
101
 
99
102
  ## Directory Breakdown
100
103
 
@@ -37,12 +37,12 @@ If an inspected browser DOM disagrees with authored template source, remember th
37
37
  | State | `pp.state(initial)` | `pp-reactive-v2.js` | setters accept values or updater functions, state belongs to the component instance |
38
38
  | Effects | `pp.effect(...)`, `pp.layoutEffect(...)` | `pp-reactive-v2.js` | callbacks may return cleanup functions, promises are not awaited |
39
39
  | Refs | `pp.ref(...)`, `pp-ref` | `pp-reactive-v2.js` | generated ref internals are runtime-managed; do not author `data-pp-ref` |
40
- | Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
40
+ | Context | `pp.createContext(...)`, lowercase `<themecontext.provider>`, `pp.context(token)` | `TemplateCompiler.ts`, `NestedBoundaryManager.ts`, `pp-reactive-v2.js` | authored provider tags are HTML-first and lowercase; `TemplateCompiler.transformContextProviderTags(...)` rewrites `*.provider` to runtime-owned `pp-context-provider`; ancestry is logical component ancestry; do not invent `pp-context` or `pp.provideContext` |
41
41
  | Portals | `pp.portal(ref, target?)` | `pp-reactive-v2.js` | context should preserve logical ancestry through the registry |
42
42
  | Lists | `<template pp-for="item in items">` | `pp-reactive-v2.js` | `pp-for` belongs on `<template>`, use plain `key`, not `pp-key` |
43
- | Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
43
+ | Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `pp-reactive-v2.js` | first-party events belong in `on*` attributes; event scope exposes `event`, `e`, `$event`, `target`, `currentTarget`, and `el`; normal form submits should use `Object.fromEntries(new FormData(event.currentTarget).entries())` instead of per-input refs; avoid id-driven `querySelector`/`addEventListener` for normal UI |
44
44
  | RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
45
- | Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; replace state from returned payload |
45
+ | Upload progress | `pp.rpc(..., { onUploadProgress })` | `pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; the callback receives `{ loaded, total, percent }` and `percent` can be `null` when length is not computable; replace state from returned payload |
46
46
  | Streaming | `pp.rpc(..., { onStream })` | `pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
47
47
  | SPA navigation | `body[pp-spa="true"]`, links | `pp-reactive-v2.js`, `main.py` | same-origin eligible links intercept; root-layout mismatches hard reload |
48
48
  | Scroll restoration | `pp-reset-scroll="true"` | `pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
@@ -63,32 +63,32 @@ If an inspected browser DOM disagrees with authored template source, remember th
63
63
  - Author one root element or one imported `x-*` root per route, layout, or component template.
64
64
  - Keep any owned plain `<script>` inside that same root.
65
65
  - Do not handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, `pp-owner`, `pp-event-owner`, or other runtime-managed attributes.
66
- - Use `pp.rpc(...)` for current browser-to-server calls.
67
- - Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
68
- - For ordinary form submits, bind `onsubmit` on the form and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; reserve `pp-ref` for imperative access rather than routine payload collection.
69
- - Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
66
+ - Use `pp.rpc(...)` for current browser-to-server calls.
67
+ - Use native `on*` attributes for button clicks, form submits, input changes, filters, toggles, and menus instead of adding ids and manual listeners.
68
+ - For ordinary form submits, bind `onsubmit` on the form and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; reserve `pp-ref` for imperative access rather than routine payload collection.
69
+ - Use lowercase provider tags such as `<themecontext.provider>` and `pp.context(...)` for context.
70
70
  - Use `pp-for` only on `<template>` and plain `key` for keyed lists.
71
71
  - Prefer PulsePoint state and directives over manual `innerHTML` repainting.
72
72
  - Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
73
73
 
74
74
  ## Compact Examples
75
75
 
76
- Context provider:
77
-
78
- ```html
79
- <section>
80
- <script>
81
- const ThemeContext = pp.createContext("light");
76
+ Context provider:
77
+
78
+ ```html
79
+ <section>
80
+ <script>
81
+ const ThemeContext = pp.createContext("light");
82
82
  const [theme, setTheme] = pp.state("dark");
83
83
  </script>
84
84
 
85
- <themecontext.provider value="{theme}">
86
- <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
87
- Theme: {theme}
88
- </button>
89
- </themecontext.provider>
90
- </section>
91
- ```
85
+ <themecontext.provider value="{theme}">
86
+ <button onclick="setTheme(theme === 'dark' ? 'light' : 'dark')">
87
+ Theme: {theme}
88
+ </button>
89
+ </themecontext.provider>
90
+ </section>
91
+ ```
92
92
 
93
93
  Upload progress:
94
94
 
@@ -104,7 +104,7 @@ Upload progress:
104
104
  if (!file) return;
105
105
 
106
106
  await pp.rpc("upload_asset", { file }, {
107
- onUploadProgress: (event) => setProgress(event.percentage ?? 0),
107
+ onUploadProgress: (event) => setProgress(Math.round(event.percent ?? 0)),
108
108
  });
109
109
  }
110
110
  </script>
@@ -115,11 +115,11 @@ Grouped shell scroll reset:
115
115
 
116
116
  ```html
117
117
  <section class="dashboard-shell">
118
- <aside class="dashboard-sidebar">...</aside>
119
- <main class="dashboard-content" pp-reset-scroll="true">
120
- <slot />
121
- </main>
122
- </section>
118
+ <aside class="dashboard-sidebar">...</aside>
119
+ <main class="dashboard-content" pp-reset-scroll="true">
120
+ <slot />
121
+ </main>
122
+ </section>
123
123
  ```
124
124
 
125
125
  ## Verification Prompts
@@ -26,6 +26,19 @@ PulsePoint is the default reactive frontend layer for Caspian. In the current ru
26
26
 
27
27
  Do not assume React, Vue, Svelte, JSX, Alpine, HTMX, or older PulsePoint docs unless the task explicitly asks for a different frontend contract.
28
28
 
29
+ ## Hard Invariants
30
+
31
+ Apply these before generating any template, even without reading the rest of this page:
32
+
33
+ 1. One authored top-level root per route, layout, or component template; the owned plain `<script>` lives inside that root, never as a sibling.
34
+ 2. Never handwrite `pp-component`, `type="text/pp"`, `data-pp-ref`, or other runtime-managed attributes; the render pipeline injects them.
35
+ 3. Bind first-party events with native `on*` attributes in the HTML; never wire normal UI with ids, `querySelector`, `addEventListener`, or manual `innerHTML`.
36
+ 4. For ordinary form submits, use `onsubmit="{handler(event)}"` plus `Object.fromEntries(new FormData(event.currentTarget).entries())`; refs are for imperative access only.
37
+ 5. Keep reactive values in `pp.state(...)`; keep template-facing bindings at the top level of the script.
38
+ 6. Call the backend with `pp.rpc(...)` backed by Python `@rpc()` actions; do not invent fetch wrappers or `pp.fetchFunction()`.
39
+ 7. `pp-for` goes only on `<template>` with plain `key`; context uses `pp.createContext(...)`, a lowercase `<token.provider>` tag, and `pp.context(token)`.
40
+ 8. If an API is not in `public/js/pp-reactive-v2.js`, it does not exist; do not invent hooks, directives, or globals.
41
+
29
42
  ## Source Of Truth
30
43
 
31
44
  When documenting or generating PulsePoint code, follow this order:
@@ -48,245 +61,209 @@ Use [core-runtime-map.md](./core-runtime-map.md) when the controlling runtime fi
48
61
 
49
62
  Use [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) when the task names a specific PulsePoint feature or directive and you need a quick feature-to-runtime lookup before reading the full guide.
50
63
 
51
- ## Default Frontend Rule
52
-
53
- When a Caspian page needs reactive browser behavior, use PulsePoint.
54
-
55
- - Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
56
- - Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
57
- - Bind first-party events in the HTML with PulsePoint-handled native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`.
58
- - For ordinary forms, bind the submit event in the `<form>` and collect named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys, then validate and normalize those values in Python. Do not use `pp.ref(...)` on every input or an effect-managed listener just to read submitted values.
59
- - When the browser needs CRUD operations or follow-up reads from the backend, call `pp.rpc()` from PulsePoint code and back it with route or backend `@rpc()` actions.
60
- - Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
61
- - For dashboards, admin areas, account sections, docs sections, and other grouped subtrees, keep shared shell markup and shared PulsePoint behavior in the parent folder's `layout.html`, then keep child-route PulsePoint state local to each `index.html`. Follow the same mental model as the Next.js App Router.
62
- - Only introduce another frontend runtime when the user explicitly asks for it or the project already depends on one.
63
-
64
- ## PulsePoint-First Events And Reactivity
65
-
66
- For first-party Caspian HTML, PulsePoint is not a later enhancement after ordinary JavaScript. It is the event, state, and reactivity model AI should use from the start.
67
-
68
- Default to this workflow:
69
-
70
- - Put the button, form, input, toggle, menu, filter, upload control, or list markup directly in the route, layout, or component HTML template.
71
- - Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
72
- - This is the PulsePoint `onClick`/native event-attribute model. Authored examples use lowercase HTML spellings such as `onclick` because browser HTML normalizes attribute names, but the important rule is to bind the event in the template instead of wiring it later with DOM selectors.
73
- - For simple form submissions, let HTML own the form shape and let the submit event carry the values: use `const data = Object.fromEntries(new FormData(event.currentTarget).entries())` inside the handler, then pass that object directly to `pp.rpc(...)`. In a form-level submit handler, `event.target` is usually the form too, but `event.currentTarget` is the copy-safe default because it always means the element that owns the handler.
74
- - Keep reactive values in `pp.state(...)`.
75
- - Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
76
- - Use `pp.ref(...)` and `pp-ref` when a real imperative element reference is needed, such as focus, measurement, media, canvas, third-party widgets, or resetting a specific file/password input after a successful action.
77
- - Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
78
- - Use `pp.rpc(...)` for browser-triggered backend reads and writes.
79
-
80
- Avoid building a parallel JavaScript layer for normal UI behavior:
81
-
82
- - Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
83
- - Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
84
- - Do not create click-in buttons by placing intent in `data-*` attributes and then scanning for those attributes. Put the action directly on the element with `onclick`, `oninput`, `onchange`, `onsubmit`, or another native `on*` event attribute.
85
- - Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
86
- - Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
87
- - Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
88
- - Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
89
-
90
- Use direct DOM APIs only as a narrow escape hatch: third-party widgets, browser APIs that require imperative access, measurements, focus, media, canvas, or behavior the current PulsePoint runtime cannot express declaratively. Even then, keep the imperative code inside the owning PulsePoint component script, usually through `pp.ref(...)` plus `pp.effect(...)`, so PulsePoint still owns the component's state, cleanup, and event flow.
91
-
92
- Preferred authored pattern:
93
-
94
- ```html
95
- <section>
96
- <input value="{query}" oninput="setQuery(event.target.value)" />
97
- <button onclick="clearSearch()" disabled="{query.length === 0}">Clear</button>
98
-
99
- <ul>
100
- <template pp-for="item in filteredItems">
101
- <li key="{item.id}">{item.label}</li>
102
- </template>
103
- </ul>
104
-
105
- <script>
106
- const [query, setQuery] = pp.state("");
107
- const items = pp.props.items ?? [];
108
- const filteredItems = items.filter((item) =>
109
- item.label.toLowerCase().includes(query.toLowerCase())
110
- );
111
-
112
- function clearSearch() {
113
- setQuery("");
114
- }
115
- </script>
116
- </section>
117
- ```
118
-
119
- Avoid this first-party pattern:
120
-
121
- ```html
122
- <section>
123
- <input id="search" />
124
- <button id="clear-search">Clear</button>
125
- <ul id="results"></ul>
126
-
127
- <script>
128
- const input = document.querySelector("#search");
129
- const button = document.querySelector("#clear-search");
130
- const results = document.querySelector("#results");
131
-
132
- button.addEventListener("click", () => {
133
- input.value = "";
134
- results.innerHTML = "";
135
- });
136
- </script>
137
- </section>
138
- ```
139
-
140
- That second shape recreates a separate event and rendering system inside a Caspian component. It is harder to maintain because it bypasses PulsePoint's rerender, event rebinding, refs, cleanup, and backend RPC conventions.
141
-
142
- Preferred form submit pattern:
143
-
144
- ```html
145
- <section>
146
- <form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
147
- <input name="name" value="{{ user_name }}" autocomplete="name" required />
148
- <input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
149
- <input name="password" type="password" autocomplete="new-password" />
150
-
151
- <button type="submit" disabled="{isSubmitting}">
152
- {isSubmitting ? "Saving..." : "Save changes"}
153
- </button>
154
- </form>
155
-
156
- <script>
157
- const [isSubmitting, setIsSubmitting] = pp.state(false);
158
-
159
- async function saveProfile(event) {
160
- event.preventDefault();
161
- if (isSubmitting) return;
162
-
163
- const data = Object.fromEntries(new FormData(event.currentTarget).entries());
164
-
165
- setIsSubmitting(true);
166
- try {
167
- await pp.rpc("save_profile", data, { abortPrevious: true });
168
- } finally {
169
- setIsSubmitting(false);
170
- }
171
- }
172
- </script>
173
- </section>
174
- ```
175
-
176
- Avoid this for a normal form:
177
-
178
- ```html
179
- <section>
180
- <form pp-ref="{formRef}">
181
- <input pp-ref="{nameRef}" name="name" />
182
- <input pp-ref="{emailRef}" name="email" />
183
- </form>
184
-
185
- <script>
186
- const formRef = pp.ref(null);
187
- const nameRef = pp.ref(null);
188
- const emailRef = pp.ref(null);
189
-
190
- pp.effect(() => {
191
- const form = formRef.current;
192
- if (!form) return;
193
-
194
- const submit = (event) => {
195
- event.preventDefault();
196
- return pp.rpc("save_profile", {
197
- name: nameRef.current?.value ?? "",
198
- email: emailRef.current?.value ?? "",
199
- });
200
- };
201
-
202
- form.addEventListener("submit", submit);
203
- return () => form.removeEventListener("submit", submit);
204
- }, []);
205
- </script>
206
- </section>
207
- ```
208
-
209
- Refs are still useful when the feature actually requires imperative access. They are not the first choice for reading standard form fields that the browser already exposes through the submit event. Keep client code minimal and put reviewable data normalization in the route's Python `@rpc()` action unless the client must transform values for UX before submitting.
210
-
211
- Basic two-way state pattern:
212
-
213
- ```html
214
- <section>
215
- <label>
216
- Name
217
- <input name="name" value="{name}" oninput="setName(event.target.value)" />
218
- </label>
219
-
220
- <p>Hello, {name || "friend"}.</p>
221
- <button onclick="setName('')">Clear</button>
222
-
223
- <script>
224
- const [name, setName] = pp.state("");
225
- </script>
226
- </section>
227
- ```
228
-
229
- Basic RPC-backed form pattern:
230
-
231
- ```html
232
- <section>
233
- <form onsubmit="{saveContact(event)}" novalidate>
234
- <input name="name" value="{name}" oninput="setName(event.target.value)" required />
235
- <input name="email" type="email" required />
236
-
237
- <button type="submit" disabled="{isSaving}">
238
- {isSaving ? "Saving..." : "Save"}
239
- </button>
240
- </form>
241
-
242
- <script>
243
- const [name, setName] = pp.state("");
244
- const [isSaving, setIsSaving] = pp.state(false);
245
-
246
- async function saveContact(event) {
247
- event.preventDefault();
248
- if (isSaving) return;
249
-
250
- const data = Object.fromEntries(new FormData(event.currentTarget).entries());
251
-
252
- setIsSaving(true);
253
- try {
254
- await pp.rpc("save_contact", data, { abortPrevious: true });
255
- } finally {
256
- setIsSaving(false);
257
- }
258
- }
259
- </script>
260
- </section>
261
- ```
262
-
263
- Back that form with a route-owned `@rpc()` action in the sibling `index.py`. If Prisma is enabled, the action should use the generated Prisma Python ORM for persistence.
264
-
265
- ```python
266
- from casp.rpc import rpc
267
- from casp.validate import Validate
268
- from src.lib.prisma import prisma
269
-
270
-
271
- @rpc()
272
- async def save_contact(name: str, email: str):
273
- name = name.strip()
274
- email = email.strip().lower()
275
-
276
- if not name:
277
- raise ValueError("Name is required.")
278
- if Validate.with_rules(email, "required|email") is not True:
279
- raise ValueError("A valid email address is required.")
280
-
281
- contact = await prisma.contact.create(
282
- data={
283
- "name": name,
284
- "email": email,
285
- }
286
- )
287
-
288
- return contact.to_dict()
289
- ```
64
+ ## Default Frontend Rule
65
+
66
+ When a Caspian page needs reactive browser behavior, use PulsePoint.
67
+
68
+ - Use PulsePoint component roots, scripts, directives, and runtime helpers for interactive UI.
69
+ - Use PulsePoint state, effects, refs, and template directives as the default reactivity model in authored Caspian templates.
70
+ - Bind first-party events in the HTML with PulsePoint-handled native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit`.
71
+ - For ordinary forms, bind the submit event in the `<form>` and collect named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`. Let input `name` attributes define the payload keys, then validate and normalize those values in Python. Do not use `pp.ref(...)` on every input or an effect-managed listener just to read submitted values.
72
+ - When the browser needs CRUD operations or follow-up reads from the backend, call `pp.rpc()` from PulsePoint code and back it with route or backend `@rpc()` actions.
73
+ - Keep server-rendered HTML plus PulsePoint enhancement as the baseline architecture.
74
+ - For dashboards, admin areas, account sections, docs sections, and other grouped subtrees, keep shared shell markup and shared PulsePoint behavior in the parent folder's `layout.html`, then keep child-route PulsePoint state local to each `index.html`. Follow the same mental model as the Next.js App Router.
75
+ - Only introduce another frontend runtime when the user explicitly asks for it or the project already depends on one.
76
+
77
+ ## PulsePoint-First Events And Reactivity
78
+
79
+ For first-party Caspian HTML, PulsePoint is not a later enhancement after ordinary JavaScript. It is the event, state, and reactivity model AI should use from the start.
80
+
81
+ Default to this workflow:
82
+
83
+ - Put the button, form, input, toggle, menu, filter, upload control, or list markup directly in the route, layout, or component HTML template.
84
+ - Bind events with native `on*` attributes handled by PulsePoint, for example `onclick="save()"`, `oninput="setQuery(event.target.value)"`, or `onsubmit="{submitForm(event)}"`.
85
+ - This is the PulsePoint `onClick`/native event-attribute model. Authored examples use lowercase HTML spellings such as `onclick` because browser HTML normalizes attribute names, but the important rule is to bind the event in the template instead of wiring it later with DOM selectors.
86
+ - For simple form submissions, let HTML own the form shape and let the submit event carry the values: use `const data = Object.fromEntries(new FormData(event.currentTarget).entries())` inside the handler, then pass that object directly to `pp.rpc(...)`. In a form-level submit handler, `event.target` is usually the form too, but `event.currentTarget` is the copy-safe default because it always means the element that owns the handler.
87
+ - Keep reactive values in `pp.state(...)`.
88
+ - Render conditional text, classes, attributes, lists, and styles with template expressions, `pp-for`, `pp-style`, `pp-spread`, and other PulsePoint-supported template features.
89
+ - Use `pp.ref(...)` and `pp-ref` when a real imperative element reference is needed, such as focus, measurement, media, canvas, third-party widgets, or resetting a specific file/password input after a successful action.
90
+ - Use `pp.effect(...)` or `pp.layoutEffect(...)` for lifecycle work that must happen after render.
91
+ - Use `pp.rpc(...)` for browser-triggered backend reads and writes.
92
+
93
+ Avoid building a parallel JavaScript layer for normal UI behavior:
94
+
95
+ - Do not add ids only so a script can find elements with `document.querySelector(...)` or `document.getElementById(...)`.
96
+ - Do not use `data-*` attributes as a private client state system when PulsePoint state or props should own the data.
97
+ - Do not create click-in buttons by placing intent in `data-*` attributes and then scanning for those attributes. Put the action directly on the element with `onclick`, `oninput`, `onchange`, `onsubmit`, or another native `on*` event attribute.
98
+ - Do not bind normal first-party clicks, input changes, submits, filters, menus, or toggles with `addEventListener(...)`.
99
+ - Do not use form refs plus input refs plus `pp.effect(...)` solely to construct an RPC payload from normal submitted fields.
100
+ - Do not repaint first-party lists or panels with manual `innerHTML` writes when `pp.state(...)` plus `pp-for` can express the same UI.
101
+ - Do not create a custom client-side store, event bus, or hydration routine for behavior that belongs in a PulsePoint component script.
102
+
103
+ Use direct DOM APIs only as a narrow escape hatch: third-party widgets, browser APIs that require imperative access, measurements, focus, media, canvas, or behavior the current PulsePoint runtime cannot express declaratively. Even then, keep the imperative code inside the owning PulsePoint component script, usually through `pp.ref(...)` plus `pp.effect(...)`, so PulsePoint still owns the component's state, cleanup, and event flow.
104
+
105
+ Preferred authored pattern:
106
+
107
+ ```html
108
+ <section>
109
+ <input value="{query}" oninput="setQuery(event.target.value)" />
110
+ <button onclick="clearSearch()" disabled="{query.length === 0}">Clear</button>
111
+
112
+ <ul>
113
+ <template pp-for="item in filteredItems">
114
+ <li key="{item.id}">{item.label}</li>
115
+ </template>
116
+ </ul>
117
+
118
+ <script>
119
+ const [query, setQuery] = pp.state("");
120
+ const items = pp.props.items ?? [];
121
+ const filteredItems = items.filter((item) =>
122
+ item.label.toLowerCase().includes(query.toLowerCase())
123
+ );
124
+
125
+ function clearSearch() {
126
+ setQuery("");
127
+ }
128
+ </script>
129
+ </section>
130
+ ```
131
+
132
+ Avoid this first-party pattern:
133
+
134
+ ```html
135
+ <section>
136
+ <input id="search" />
137
+ <button id="clear-search">Clear</button>
138
+ <ul id="results"></ul>
139
+
140
+ <script>
141
+ const input = document.querySelector("#search");
142
+ const button = document.querySelector("#clear-search");
143
+ const results = document.querySelector("#results");
144
+
145
+ button.addEventListener("click", () => {
146
+ input.value = "";
147
+ results.innerHTML = "";
148
+ });
149
+ </script>
150
+ </section>
151
+ ```
152
+
153
+ That second shape recreates a separate event and rendering system inside a Caspian component. It is harder to maintain because it bypasses PulsePoint's rerender, event rebinding, refs, cleanup, and backend RPC conventions.
154
+
155
+ Preferred form submit pattern:
156
+
157
+ ```html
158
+ <section>
159
+ <form onsubmit="{saveProfile(event)}" class="grid gap-4" novalidate>
160
+ <input name="name" value="{{ user_name }}" autocomplete="name" required />
161
+ <input name="email" type="email" value="{{ user_email }}" autocomplete="email" required />
162
+ <input name="password" type="password" autocomplete="new-password" />
163
+
164
+ <button type="submit" disabled="{isSubmitting}">
165
+ {isSubmitting ? "Saving..." : "Save changes"}
166
+ </button>
167
+ </form>
168
+
169
+ <script>
170
+ const [isSubmitting, setIsSubmitting] = pp.state(false);
171
+
172
+ async function saveProfile(event) {
173
+ event.preventDefault();
174
+ if (isSubmitting) return;
175
+
176
+ const data = Object.fromEntries(new FormData(event.currentTarget).entries());
177
+
178
+ setIsSubmitting(true);
179
+ try {
180
+ await pp.rpc("save_profile", data, { abortPrevious: true });
181
+ } finally {
182
+ setIsSubmitting(false);
183
+ }
184
+ }
185
+ </script>
186
+ </section>
187
+ ```
188
+
189
+ Avoid this for a normal form:
190
+
191
+ ```html
192
+ <section>
193
+ <form pp-ref="{formRef}">
194
+ <input pp-ref="{nameRef}" name="name" />
195
+ <input pp-ref="{emailRef}" name="email" />
196
+ </form>
197
+
198
+ <script>
199
+ const formRef = pp.ref(null);
200
+ const nameRef = pp.ref(null);
201
+ const emailRef = pp.ref(null);
202
+
203
+ pp.effect(() => {
204
+ const form = formRef.current;
205
+ if (!form) return;
206
+
207
+ const submit = (event) => {
208
+ event.preventDefault();
209
+ return pp.rpc("save_profile", {
210
+ name: nameRef.current?.value ?? "",
211
+ email: emailRef.current?.value ?? "",
212
+ });
213
+ };
214
+
215
+ form.addEventListener("submit", submit);
216
+ return () => form.removeEventListener("submit", submit);
217
+ }, []);
218
+ </script>
219
+ </section>
220
+ ```
221
+
222
+ Refs are still useful when the feature actually requires imperative access. They are not the first choice for reading standard form fields that the browser already exposes through the submit event. Keep client code minimal and put reviewable data normalization in the route's Python `@rpc()` action unless the client must transform values for UX before submitting.
223
+
224
+ Back the form with a route-owned `@rpc()` action in the sibling `index.py`. If Prisma is enabled, the action should use the generated Prisma Python ORM for persistence.
225
+
226
+ ```python
227
+ from casp.rpc import rpc
228
+ from casp.validate import Validate
229
+ from src.lib.prisma import prisma
230
+
231
+
232
+ @rpc()
233
+ async def save_profile(name: str, email: str, password: str = ""):
234
+ name = name.strip()
235
+ email = email.strip().lower()
236
+
237
+ if not name:
238
+ raise ValueError("Name is required.")
239
+ if Validate.with_rules(email, "required|email") is not True:
240
+ raise ValueError("A valid email address is required.")
241
+
242
+ profile = await prisma.user.update(
243
+ where={"email": email},
244
+ data={"name": name},
245
+ )
246
+
247
+ return profile.to_dict()
248
+ ```
249
+
250
+ Basic two-way state pattern:
251
+
252
+ ```html
253
+ <section>
254
+ <label>
255
+ Name
256
+ <input name="name" value="{name}" oninput="setName(event.target.value)" />
257
+ </label>
258
+
259
+ <p>Hello, {name || "friend"}.</p>
260
+ <button onclick="setName('')">Clear</button>
261
+
262
+ <script>
263
+ const [name, setName] = pp.state("");
264
+ </script>
265
+ </section>
266
+ ```
290
267
 
291
268
  ## Authoring Model
292
269
 
@@ -447,14 +424,101 @@ Notes:
447
424
  - Keep template-facing bindings at the top level so the AST-based exporter can see them.
448
425
  - For predictable code generation, prefer passing an explicit dependency array to `pp.effect`, `pp.layoutEffect`, `pp.memo`, and `pp.callback`.
449
426
 
427
+ Effect with cleanup and dependencies:
428
+
429
+ ```html
430
+ <section>
431
+ <p>Elapsed: {seconds}s</p>
432
+ <button onclick="setRunning(!running)">{running ? "Pause" : "Resume"}</button>
433
+
434
+ <script>
435
+ const [seconds, setSeconds] = pp.state(0);
436
+ const [running, setRunning] = pp.state(true);
437
+
438
+ pp.effect(() => {
439
+ if (!running) return;
440
+ const id = setInterval(() => setSeconds((s) => s + 1), 1000);
441
+ return () => clearInterval(id);
442
+ }, [running]);
443
+ </script>
444
+ </section>
445
+ ```
446
+
447
+ Use the same shape for any subscription: attach in the effect body, detach in the returned cleanup. The cleanup also runs on component disposal, which is why route-owned `WebSocket` clients close their socket from a cleanup effect.
448
+
449
+ Memoized derived value:
450
+
451
+ ```html
452
+ <section>
453
+ <input value="{query}" oninput="setQuery(event.target.value)" />
454
+ <p>{visible.length} of {items.length} items</p>
455
+
456
+ <script>
457
+ const [query, setQuery] = pp.state("");
458
+ const items = pp.props.items ?? [];
459
+
460
+ const visible = pp.memo(
461
+ () => items.filter((item) => item.label.toLowerCase().includes(query.toLowerCase())),
462
+ [query]
463
+ );
464
+ </script>
465
+ </section>
466
+ ```
467
+
468
+ For cheap derivations, a plain top-level `const` recomputed each render is fine; reach for `pp.memo` when the computation is heavy or the result identity matters for dependencies.
469
+
470
+ Reducer for multi-field update logic:
471
+
472
+ ```html
473
+ <section>
474
+ <p>{cart.count} items, total {cart.total}</p>
475
+ <button onclick="dispatch({ type: 'add', price: 10 })">Add</button>
476
+ <button onclick="dispatch({ type: 'clear' })">Clear</button>
477
+
478
+ <script>
479
+ const [cart, dispatch] = pp.reducer((state, action) => {
480
+ if (action.type === "add") {
481
+ return { count: state.count + 1, total: state.total + action.price };
482
+ }
483
+ if (action.type === "clear") {
484
+ return { count: 0, total: 0 };
485
+ }
486
+ return state;
487
+ }, { count: 0, total: 0 });
488
+ </script>
489
+ </section>
490
+ ```
491
+
492
+ Portal pattern for overlays that must escape clipping ancestors:
493
+
494
+ ```html
495
+ <div>
496
+ <button onclick="setOpen(true)">Open dialog</button>
497
+
498
+ <div pp-ref="dialogRef" hidden="{!open}" class="modal">
499
+ <p>Rendered under document.body.</p>
500
+ <button onclick="setOpen(false)">Close</button>
501
+ </div>
502
+
503
+ <script>
504
+ const [open, setOpen] = pp.state(false);
505
+ const dialogRef = pp.ref(null);
506
+
507
+ pp.portal(dialogRef);
508
+ </script>
509
+ </div>
510
+ ```
511
+
512
+ `pp.portal(ref)` moves the ref-managed element to `document.body` by default, or to the element passed as the second argument. Portaled content keeps its logical component ancestry, so context, props, and events keep working.
513
+
450
514
  ## Context
451
515
 
452
- Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
516
+ Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
453
517
 
454
518
  How it works:
455
519
 
456
520
  - Create a token with `pp.createContext(defaultValue)`.
457
- - Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
521
+ - Provide a value with a lowercase provider tag such as `<themecontext.provider value="{theme}">`.
458
522
  - Read it in a descendant component with `pp.context(token)`.
459
523
  - Resolution walks the logical component parent chain stored in the registry, not the live DOM.
460
524
  - Portaled descendants still resolve providers through component ancestry.
@@ -468,24 +532,24 @@ Important:
468
532
  - Context is component-level, not directive-based. There is no `pp-context` attribute.
469
533
  - `pp.context(token)` resolves from ancestors. A component does not consume the value it provides in the same render.
470
534
  - If provider and consumer live in different component script scopes, pass the token through props or store it in shared outer or global state.
471
- - The preferred authoring style is a lowercase provider tag in the template. The TypeScript runtime's `TemplateCompiler.transformContextProviderTags(...)` recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The runtime context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
472
- - The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but that form provides from the component boundary rather than documenting the HTML subtree shape. Use it only when component-wide provision is intended.
473
- - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
535
+ - The preferred authoring style is a lowercase provider tag in the template. The TypeScript runtime's `TemplateCompiler.transformContextProviderTags(...)` recognizes `*.provider` tags case-insensitively and rewrites them to runtime-owned `<pp-context-provider>` boundaries. The runtime context lookup is also case-insensitive, so `<themecontext.provider>` can resolve a script binding named `ThemeContext`.
536
+ - The runtime also supports imperative `ThemeContext.Provider({ value })` calls during render, but that form provides from the component boundary rather than documenting the HTML subtree shape. Use it only when component-wide provision is intended.
537
+ - Do not invent or document `pp.provideContext`. The current runtime explicitly does not expose it.
474
538
 
475
539
  Provider example:
476
540
 
477
541
  ```html
478
542
  <section>
479
- <script>
480
- const ThemeContext = pp.createContext("light");
481
- const [theme] = pp.state("dark");
482
- </script>
483
-
484
- <themecontext.provider value="{theme}">
485
- <p>This subtree receives the provided theme.</p>
486
- </themecontext.provider>
487
- </section>
488
- ```
543
+ <script>
544
+ const ThemeContext = pp.createContext("light");
545
+ const [theme] = pp.state("dark");
546
+ </script>
547
+
548
+ <themecontext.provider value="{theme}">
549
+ <p>This subtree receives the provided theme.</p>
550
+ </themecontext.provider>
551
+ </section>
552
+ ```
489
553
 
490
554
  Consumer example for a child component that receives the token through props:
491
555
 
@@ -555,10 +619,10 @@ Example:
555
619
  </div>
556
620
  ```
557
621
 
558
- ## Refs
559
-
560
- - Refs are for imperative element access. Do not use `pp-ref` as the default way to read normal form input values on submit; prefer the form's `onsubmit` event plus `Object.fromEntries(new FormData(event.currentTarget).entries())`.
561
- - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
622
+ ## Refs
623
+
624
+ - Refs are for imperative element access. Do not use `pp-ref` as the default way to read normal form input values on submit; prefer the form's `onsubmit` event plus `Object.fromEntries(new FormData(event.currentTarget).entries())`.
625
+ - Use `pp-ref="nameInput"` when the ref object or callback is already available in scope.
562
626
  - Use `pp-ref="{registerRef(id)}"` when you want the compiler to capture a dynamic ref expression.
563
627
  - `pp.ref(null)` is the normal way to create a ref object.
564
628
  - Callback refs and `{ current }` refs are both supported.
@@ -618,19 +682,19 @@ Example:
618
682
  </div>
619
683
  ```
620
684
 
621
- ## Events
622
-
623
- - Use native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit` for first-party events.
624
- - Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
625
- - Event values may be raw code or wrapped in `{...}`.
626
- - The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
627
- - For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
628
- - `event.target` can also work for form-level submit handlers, but examples use `event.currentTarget` so nested event origins do not change which element becomes the `FormData` source.
629
- - Do not use hyphenated event attrs like `on-click`.
630
- - Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
631
- - Owned template/event-owner internals are runtime-managed. Do not author them directly.
632
- - Do not replace normal PulsePoint event attributes with id-driven `querySelector(...)` plus `addEventListener(...)` wiring. If an imperative listener is unavoidable for an integration, attach and clean it up from `pp.effect(...)`.
633
- - Do not replace a normal `onclick` with a `data-action`, `data-target`, or similar attribute plus a script that scans the DOM. PulsePoint event attributes are the event contract for first-party Caspian UI.
685
+ ## Events
686
+
687
+ - Use native `on*` attributes such as `onclick`, `oninput`, `onchange`, and `onsubmit` for first-party events.
688
+ - Treat this as the HTML form of `onClick`-style PulsePoint event binding. Prefer lowercase examples in authored HTML because the browser normalizes attribute names.
689
+ - Event values may be raw code or wrapped in `{...}`.
690
+ - The runtime injects `event`, `e`, `$event`, `target`, `currentTarget`, and `el`.
691
+ - For form submit handlers, prefer `onsubmit="{submitForm(event)}"` and `Object.fromEntries(new FormData(event.currentTarget).entries())` when collecting named fields for `pp.rpc(...)`.
692
+ - `event.target` can also work for form-level submit handlers, but examples use `event.currentTarget` so nested event origins do not change which element becomes the `FormData` source.
693
+ - Do not use hyphenated event attrs like `on-click`.
694
+ - Event attributes are removed from the live DOM after binding and rebound after DOM morphing.
695
+ - Owned template/event-owner internals are runtime-managed. Do not author them directly.
696
+ - Do not replace normal PulsePoint event attributes with id-driven `querySelector(...)` plus `addEventListener(...)` wiring. If an imperative listener is unavoidable for an integration, attach and clean it up from `pp.effect(...)`.
697
+ - Do not replace a normal `onclick` with a `data-action`, `data-target`, or similar attribute plus a script that scans the DOM. PulsePoint event attributes are the event contract for first-party Caspian UI.
634
698
 
635
699
  ## SPA, loading, and navigation helpers
636
700
 
@@ -654,8 +718,9 @@ RPC notes:
654
718
  - Passing `true` as the third argument means `abortPrevious: true`.
655
719
  - The options object supports `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, and `onUploadComplete`.
656
720
  - File uploads switch to the XHR path when upload progress callbacks are needed.
721
+ - The `onUploadProgress` callback receives `{ loaded, total, percent }`. `percent` is a 0-100 number, and both `total` and `percent` are `null` when the browser cannot compute the upload length. Do not document or generate `event.percentage`.
657
722
  - For file managers, use upload callbacks for progress UI but replace the asset list from returned RPC state with `pp.state(...)` and `pp-for` instead of manual DOM repainting. See [file-uploads.md](./file-uploads.md).
658
- - Streamed `text/event-stream` responses are supported when a stream handler is provided.
723
+ - Streamed `text/event-stream` responses are supported when a stream handler is provided. Each `onStream` chunk is JSON-parsed when the event data looks like JSON; otherwise the raw string is passed through. If the response streams but no `onStream` handler was given, the runtime warns and discards the stream.
659
724
  - Redirect headers are honored through `pp.redirect()`.
660
725
 
661
726
  Notes:
@@ -664,6 +729,53 @@ Notes:
664
729
  - Root-layout mismatches during SPA navigation trigger a hard reload.
665
730
  - `pp.mount()` bootstraps every `[pp-component]` it finds, so generated code should call it only through the global runtime if you are manually mounting at all.
666
731
 
732
+ Upload with progress pattern:
733
+
734
+ ```html
735
+ <section>
736
+ <input type="file" onchange="{uploadFile(event.target.files?.[0])}" />
737
+ <progress max="100" value="{percent ?? 0}"></progress>
738
+
739
+ <script>
740
+ const [percent, setPercent] = pp.state(null);
741
+
742
+ async function uploadFile(file) {
743
+ if (!file) return;
744
+
745
+ await pp.rpc("upload_asset", { file }, {
746
+ onUploadProgress: ({ percent }) => setPercent(percent),
747
+ onUploadComplete: () => setPercent(100),
748
+ });
749
+ }
750
+ </script>
751
+ </section>
752
+ ```
753
+
754
+ Streaming pattern for server generators that return `text/event-stream`:
755
+
756
+ ```html
757
+ <section>
758
+ <button onclick="ask()" disabled="{isStreaming}">Ask</button>
759
+ <pre>{answer}</pre>
760
+
761
+ <script>
762
+ const [answer, setAnswer] = pp.state("");
763
+ const [isStreaming, setIsStreaming] = pp.state(false);
764
+
765
+ function ask() {
766
+ setAnswer("");
767
+ setIsStreaming(true);
768
+
769
+ pp.rpc("ask_question", { topic: "caspian" }, {
770
+ onStream: (chunk) => setAnswer((current) => current + chunk),
771
+ onStreamError: () => setIsStreaming(false),
772
+ onStreamComplete: () => setIsStreaming(false),
773
+ });
774
+ }
775
+ </script>
776
+ </section>
777
+ ```
778
+
667
779
  ## Runtime Output And Debugging
668
780
 
669
781
  When you inspect rendered HTML in the browser, you should expect to see runtime-managed attributes and elements that are not part of authored source.
@@ -684,23 +796,23 @@ These are runtime details.
684
796
 
685
797
  Use these rules when generating or editing PulsePoint runtime code:
686
798
 
687
- - Treat PulsePoint as the default reactive frontend for Caspian app code.
688
- - For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
689
- - For simple forms, bind `onsubmit` in the authored HTML and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; do not generate per-input refs or effect-managed submit listeners just to gather values.
690
- - Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
799
+ - Treat PulsePoint as the default reactive frontend for Caspian app code.
800
+ - For first-party HTML interactions, use PulsePoint `on*` event attributes, state, refs, effects, directives, and `pp.rpc()` before reaching for DOM APIs.
801
+ - For simple forms, bind `onsubmit` in the authored HTML and read named fields with `Object.fromEntries(new FormData(event.currentTarget).entries())`; do not generate per-input refs or effect-managed submit listeners just to gather values.
802
+ - Treat `pp.rpc()` as the default browser-to-server path for CRUD operations and interactive backend reads.
691
803
  - Use `public/js/pp-reactive-v2.js` as the shipped runtime contract AI should follow.
692
804
  - Keep `main.py` in view because it injects the runtime-facing attributes and rewrites authored scripts before the browser sees them.
693
805
  - If a development-only source tree exists behind the shipped runtime, treat it as optional implementation detail rather than something generated apps are guaranteed to contain.
694
806
  - In authored Caspian templates, do not handwrite `pp-component` or `type="text/pp"`; let the render pipeline inject them.
695
807
  - For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md), keep the shared interactive shell in the parent folder's `layout.html`, and keep route-specific PulsePoint code in each child `index.html`.
696
808
  - For grouped shells with independent shell and content scrolling, put `pp-reset-scroll="true"` on the content pane rather than the whole shell when only the page content should reset between child-route navigations.
697
- - Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
698
- - Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
699
- - Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
809
+ - Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
810
+ - Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
811
+ - Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
700
812
  - If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
701
813
  - In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
702
814
  - Keep template-facing variables at top level.
703
- - Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
815
+ - Follow the HTML-first context pattern: `pp.createContext(...)`, a lowercase provider tag such as `<themecontext.provider value="{...}">`, and `pp.context(token)`.
704
816
  - Do not invent `pp.provideContext`, `pp-context`, or other legacy context helpers.
705
817
  - Keep `pp-for` on `<template>` and use plain `key`.
706
818
  - Use native `on*` attributes, not framework-specific event syntax.
@@ -715,9 +827,9 @@ Use these rules when generating or editing PulsePoint runtime code:
715
827
 
716
828
  Do not generate these unless the current source explicitly adds support:
717
829
 
718
- - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
719
- - standard DOM scripting as the default first-party interaction model, including id/data-attribute driven `querySelector(...)`, `addEventListener(...)`, or manual `innerHTML` rendering for normal buttons, forms, filters, toggles, uploads, and reactive lists
720
- - `pp-context`
830
+ - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
831
+ - standard DOM scripting as the default first-party interaction model, including id/data-attribute driven `querySelector(...)`, `addEventListener(...)`, or manual `innerHTML` rendering for normal buttons, forms, filters, toggles, uploads, and reactive lists
832
+ - `pp-context`
721
833
  - `pp-key`
722
834
  - `data-pp-ref`
723
835
  - `pp-context-provider`
@@ -741,7 +853,7 @@ These are current runtime caveats that matter for authors and AI tools:
741
853
  - The global `pp` singleton auto-mounts on DOM ready, and `pp.mount()` is idempotent.
742
854
  - `pp.effect` and `pp.layoutEffect` are cleanup-style hooks. Their callbacks are not promise-aware.
743
855
  - `pp.context()` resolves through ancestor components, not the current component's own pending providers.
744
- - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
856
+ - `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
745
857
  - `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
746
858
 
747
859
  ## Final reminder
@@ -0,0 +1,165 @@
1
+ ---
2
+ title: WebSockets
3
+ description: Use this page when the task mentions FastAPI WebSocket endpoints, live channels, broadcast managers, browser `WebSocket`, origin checks, or authenticated socket sessions in Caspian.
4
+ related:
5
+ title: Related docs
6
+ description: Use routing and PulsePoint for client placement, auth for session policy, and the core runtime map for endpoint ownership in `main.py`.
7
+ links:
8
+ - /docs/routing
9
+ - /docs/pulsepoint
10
+ - /docs/auth
11
+ - /docs/fetch-data
12
+ - /docs/core-runtime-map
13
+ - /docs/index
14
+ ---
15
+
16
+ WebSockets in Caspian are gated by `caspian.config.json` and implemented as app-owned FastAPI endpoints, not the default RPC data path.
17
+
18
+ Use `@rpc()` plus `pp.rpc()` for ordinary browser-triggered reads, writes, uploads, and Server-Sent Event streams. Use WebSockets only when the task needs a bidirectional, long-lived channel such as live chat, collaboration, multiplayer state, server push that is not a good SSE fit, or presence.
19
+
20
+ Treat `caspian.config.json` as the single source of truth for whether the WebSocket feature is enabled. If `websocket` is false, this page is reference material only; do not assume `@app.websocket(...)` endpoints, `src/lib/websocket/**`, or socket demo routes exist until the user chooses to enable the feature and the update workflow has run.
21
+
22
+ ## Source Of Truth
23
+
24
+ Before adding, debugging, or documenting a WebSocket feature:
25
+
26
+ - Read `caspian.config.json` first and confirm `websocket: true`.
27
+ - If `websocket` is false and the user wants live channels, ask for confirmation before enabling it, then update the config and follow the project update workflow.
28
+ - Inspect `main.py` for `@app.websocket(...)` routes, origin checks, close codes, message limits, idle timeout, and middleware interactions.
29
+ - Inspect `src/lib/**` for app-owned socket helpers such as session extraction, auth payload checks, connection managers, or broadcast utilities.
30
+ - Inspect the owning route under `src/app/**` for the browser client, first-render URL selection, and PulsePoint state around the native `WebSocket`.
31
+ - Check `settings/bs-config.json` before testing development URLs through BrowserSync or a proxy.
32
+
33
+ Packaged docs should describe this workflow generally. Keep endpoint names, route names, ports, and demo inventory project-owned; do not treat one app's route tree as the framework convention.
34
+
35
+ ## Recommended Shape
36
+
37
+ When `caspian.config.json` has `websocket: true`, put socket endpoints in `main.py` when they are application-level ASGI routes:
38
+
39
+ ```python
40
+ from fastapi import WebSocket, WebSocketDisconnect, status
41
+
42
+ WEBSOCKET_PATH = "/ws/channel"
43
+
44
+ @app.websocket(WEBSOCKET_PATH)
45
+ async def websocket_live_endpoint(websocket: WebSocket):
46
+ if not is_origin_allowed(websocket):
47
+ await websocket.close(code=status.WS_1008_POLICY_VIOLATION)
48
+ return
49
+
50
+ await manager.connect(websocket)
51
+ try:
52
+ while True:
53
+ raw_message = await websocket.receive_text()
54
+ ...
55
+ except WebSocketDisconnect:
56
+ return
57
+ finally:
58
+ manager.disconnect(websocket)
59
+ ```
60
+
61
+ Keep reusable socket helpers in `src/lib/**` when they are shared by more than one endpoint or route. Good candidates include connection managers, session helpers, auth-payload extraction, payload normalization, and shared broadcast code.
62
+
63
+ Keep route-specific browser UI in `src/app/**/index.html` and route-specific first-render values in the matching `index.py`. For example, an `index.py` can pass `websocket_path` and a development-only BrowserSync-aware `websocket_url` into the template with `render_page(__file__, {...})`.
64
+
65
+ ## Browser Client Pattern
66
+
67
+ Use PulsePoint for the component state and lifecycle, while using the native browser `WebSocket` object for the socket itself.
68
+
69
+ ```html
70
+ <main>
71
+ <button onclick="{connectSocket()}" disabled="{isConnected}">Connect</button>
72
+ <button onclick="{disconnectSocket()}" disabled="{!isConnected}">Disconnect</button>
73
+
74
+ <script>
75
+ const websocketPath = "{{ websocket_path }}";
76
+ const socketRef = pp.ref(null);
77
+ const [status, setStatus] = pp.state("idle");
78
+ const isConnected = status === "connected";
79
+
80
+ function buildWebSocketUrl(path) {
81
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
82
+ return new URL(`${protocol}//${window.location.host}${path}`, window.location.href).toString();
83
+ }
84
+
85
+ function connectSocket() {
86
+ const socket = new WebSocket(buildWebSocketUrl(websocketPath));
87
+ socketRef.current = socket;
88
+
89
+ socket.addEventListener("message", (event) => {
90
+ const payload = JSON.parse(event.data);
91
+ ...
92
+ });
93
+ }
94
+
95
+ function disconnectSocket() {
96
+ socketRef.current?.close(1000, "Client disconnect");
97
+ socketRef.current = null;
98
+ }
99
+
100
+ pp.effect(() => {
101
+ return () => socketRef.current?.close(1000, "Page disposed");
102
+ }, []);
103
+ </script>
104
+ </main>
105
+ ```
106
+
107
+ This is one of the narrow cases where direct browser APIs and event listeners are expected. Keep them inside the owning PulsePoint component script, store the socket in `pp.ref(...)`, and keep rendered UI state in `pp.state(...)`.
108
+
109
+ ## Auth And Sessions
110
+
111
+ HTTP route privacy does not automatically make a WebSocket endpoint private. If a socket is authenticated, the endpoint must explicitly inspect the session or auth payload available on the WebSocket scope and close unauthenticated clients.
112
+
113
+ Use close code `1008` for policy violations such as failed origin checks or missing authentication. If the endpoint accepts before rejecting, send a small JSON error payload first so the browser UI can show a useful message.
114
+
115
+ If the app refreshes auth sessions, verify whether the WebSocket helper refreshes session expiry and whether that session mutation is persisted in the current middleware stack. Do not assume HTTP `AuthMiddleware` behavior applies to `scope["type"] == "websocket"`.
116
+
117
+ ## Origin And Proxy Checks
118
+
119
+ WebSocket endpoints should validate the `Origin` header before accepting connections, especially in production.
120
+
121
+ Common allowed origins:
122
+
123
+ - same-origin derived from the socket URL
124
+ - explicit values from environment variables such as `WEBSOCKET_ALLOWED_ORIGINS`, `CORS_ALLOWED_ORIGINS`, `APP_BASE_URL`, `PUBLIC_BASE_URL`, or `SITE_URL`
125
+ - localhost HTTP origins in development when the app intentionally permits local BrowserSync or proxy testing
126
+
127
+ When testing locally, read `settings/bs-config.json` first and build `ws://` or `wss://` URLs from its active `local` value. Do not assume the BrowserSync port is the default.
128
+
129
+ ## Message Contract
130
+
131
+ Document the JSON message contract near the endpoint and client that own it.
132
+
133
+ Useful baseline conventions:
134
+
135
+ - Clients send JSON objects, usually `{ "type": "message", "text": "..." }` or `{ "type": "ping" }`.
136
+ - Servers send `ready`, `message`, `error`, and `pong` payloads as JSON.
137
+ - Enforce a maximum incoming message size before parsing or broadcasting.
138
+ - Close idle sockets after a configured timeout when the app does not need long silent connections.
139
+ - Sanitize or trim outbound text so one client cannot broadcast unbounded data.
140
+ - Disconnect stale sockets when sends fail.
141
+
142
+ ## WebSockets Vs RPC/SSE
143
+
144
+ Choose the transport deliberately:
145
+
146
+ | Need | Preferred transport |
147
+ | --- | --- |
148
+ | CRUD, form submits, button actions, upload progress | `@rpc()` plus `pp.rpc()` |
149
+ | Server sends one-way progress chunks for a request | RPC streaming / SSE |
150
+ | Browser and server both send messages over a persistent channel | WebSocket |
151
+ | Presence, collaboration, live chat, multiplayer state | WebSocket |
152
+
153
+ Do not replace normal Caspian RPC with WebSockets just because a page is interactive.
154
+
155
+ ## AI Retrieval Notes
156
+
157
+ If an AI agent is working on WebSockets in a Caspian app:
158
+
159
+ - Start with `caspian.config.json`. Continue only after confirming `websocket: true`, unless the task is specifically to enable the feature.
160
+ - Use [core-runtime-map.md](./core-runtime-map.md) to connect `main.py` behavior back to this doc.
161
+ - Use [routing.md](./routing.md) before changing the route files that render the client.
162
+ - Use [pulsepoint.md](./pulsepoint.md) for authored template rules and component lifecycle cleanup.
163
+ - Use [auth.md](./auth.md) before changing authenticated socket behavior.
164
+ - Keep reusable connection/session helpers in `src/lib/websocket/**` when they are shared; keep route UI in whichever `src/app/**` route owns the live experience; keep app-level ASGI endpoints in `main.py` unless the runtime intentionally provides another registration layer.
165
+ - Verify local test URLs from `settings/bs-config.json`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {