caspian-utils 0.2.3 → 0.2.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.
- package/dist/docs/ai-validation-checklist.md +5 -3
- package/dist/docs/components.md +6 -2
- package/dist/docs/core-runtime-map.md +3 -3
- package/dist/docs/fetch-data.md +75 -3
- package/dist/docs/index.md +8 -7
- package/dist/docs/project-structure.md +26 -26
- package/dist/docs/pulsepoint-runtime-map.md +14 -6
- package/dist/docs/pulsepoint.md +176 -5
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: AI Validation Checklist
|
|
3
|
-
description: Use this page when testing whether AI can find the right Caspian docs, core files, and verification checkpoints for a task after documentation or runtime changes.
|
|
3
|
+
description: Use this page when testing whether AI can find the right Caspian docs, core files, performance owner, and verification checkpoints for a task after documentation or runtime changes.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Start with the manifest, use the runtime map when needed, then validate against the matching feature guide and owning runtime behavior.
|
|
@@ -55,7 +55,8 @@ Use prompts like these to check whether AI lands on the correct docs and files.
|
|
|
55
55
|
| Create a protected dashboard section with child routes and a shared shell. | [index.md](./index.md), [routing.md](./routing.md), [auth.md](./auth.md), [project-structure.md](./project-structure.md) | `src/app/**`, `src/lib/auth/auth_config.py`, `main.py`, `.venv/Lib/site-packages/casp/layout.py` | section layout ownership, route privacy mode, and child-route wrapping |
|
|
56
56
|
| Make a grouped shell keep sidebar scroll while resetting page content on child-route navigation. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/layout.html`, `public/js/pp-reactive-v2.js`, `main.py` | `pp-reset-scroll` placement, push-vs-history scroll behavior, and shared-shell ownership |
|
|
57
57
|
| Create a new contact page with interactive form behavior. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md) | `src/app/**/index.html`, `main.py`, `.venv/Lib/site-packages/casp/components_compiler.py`, `.venv/Lib/site-packages/casp/scripts_type.py` | single-root template shape, script-inside-root authoring, and authored-vs-runtime boundaries |
|
|
58
|
-
| Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
|
|
58
|
+
| Add a button, filter, or form interaction to a route template. | [index.md](./index.md), [routing.md](./routing.md), [pulsepoint.md](./pulsepoint.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | `src/app/**/index.html`, `public/js/pp-reactive-v2.js`, `main.py` | PulsePoint `on*` event attributes, `pp.state`, directives, `Object.fromEntries(new FormData(...).entries())` for simple form submits, and avoiding id-driven `querySelector` or `addEventListener` wiring |
|
|
59
|
+
| A debounced server search pauses the first key typed after the timer fires. Determine whether the component or runtime owns the problem. | [index.md](./index.md), [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [fetch-data.md](./fetch-data.md), [pulsepoint-runtime-map.md](./pulsepoint-runtime-map.md) | Owning `src/app/**` or `src/components/**`; `public/js/pp-reactive-v2.js` only when profiling points to runtime reconciliation | State versus ref ownership, debounce frequency versus render cost, stale-response generations, the smallest useful render owner, and `pp.enablePerf()` evidence before a runtime edit |
|
|
59
60
|
| Add a file manager page with upload progress and persisted metadata. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [fetch-data.md](./fetch-data.md), [database.md](./database.md) | `src/app/**/index.html`, `src/app/**/index.py`, `src/lib/**`, `prisma/schema.prisma` when Prisma applies | route-owned upload actions, persisted metadata flow, and mandatory Prisma Python ORM use for database reads and writes when Prisma is enabled |
|
|
60
61
|
| Add `public/icons/app.png` and make it browser-accessible without editing routes. | [index.md](./index.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md), [auth.md](./auth.md) | `public/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | root-relative URL mapping, no per-directory route or prefix registry, traversal and symlink containment, `GET`/`HEAD` only, and middleware order |
|
|
61
62
|
| Store untrusted uploads in a custom public directory. | [index.md](./index.md), [file-uploads.md](./file-uploads.md), [project-structure.md](./project-structure.md), [core-runtime-map.md](./core-runtime-map.md) | `src/app/**/index.py`, `src/lib/**`, `main.py`, `.venv/Lib/site-packages/casp/runtime_security.py` | top-level `inline_safe_subdirectories` registration, attachment mode for HTML/SVG/unknown types, `nosniff`, path containment, and BrowserSync ignore rules |
|
|
@@ -80,7 +81,8 @@ Treat the table as a prompt pack for spot checks, not as a full validation matri
|
|
|
80
81
|
- AI treats runtime HTML examples as authored template examples.
|
|
81
82
|
- AI reads the React comparison in [pulsepoint.md](./pulsepoint.md), [components.md](./components.md), [index.md](./index.md), or [routing.md](./routing.md) as permission to write JSX, and emits `{cond && (<div/>)}`, `{list.map(item => (<tr/>))}`, `className`, `onClick`, or unquoted `class={...}`. The React comparison covers the hook API and component decomposition only. An unquoted brace attribute is invalid HTML: the parser shreds it, the component root never compiles, and the page renders blank with **no console error**. Confirm every brace expression in an attribute is quoted, and that the file would still be valid HTML with all `{}` deleted.
|
|
82
83
|
- AI generates a route or component template with a valid-looking root element but leaves a sibling top-level `<script>` or second top-level element after it. This includes single-file `html("""...""")` components where the `<script>` is placed after the root's closing tag instead of nested inside it; the string handed to `html(...)` must resolve to one top-level element, script included.
|
|
83
|
-
- AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, manual `innerHTML`, or per-input `pp-ref` payload collection instead of PulsePoint `on*` attributes, form submit events, state, directives, and `pp.rpc()`.
|
|
84
|
+
- AI starts first-party interactivity with ids, `data-*` attributes, `querySelector`, `addEventListener`, manual `fetch`, manual `innerHTML`, or per-input `pp-ref` payload collection instead of PulsePoint `on*` attributes, form submit events, state, directives, and `pp.rpc()`.
|
|
85
|
+
- AI assumes every visible pause is a PulsePoint runtime defect and edits reconciliation before proving which setter requested the render. A debounced state setter can still synchronously render a large owner when its timer fires. Keep non-rendering query text, timers, request generations, and cursors in refs; keep rendered results in state; and profile before changing the runtime. The inverse is also wrong: a value the template must display belongs in state, not a ref.
|
|
84
86
|
- AI bypasses the generated Prisma Python ORM in a Prisma-enabled project with direct database drivers, custom fetch helpers, JSON active stores, browser-side database fetches, or a second app-owned database abstraction.
|
|
85
87
|
- AI extracts one-route backend logic into `src/lib/**` even though the logic is not shared; route-specific first-render queries, RPC actions, validation, and response shaping belong in the owning `src/app/**/index.py`.
|
|
86
88
|
- AI puts `pp-reset-scroll="true"` on the whole shell or `body` when only the page-content pane should reset, causing persistent sidebars or rails to lose their scroll position.
|
package/dist/docs/components.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Components
|
|
3
|
-
description: Use this page when the task mentions `@component`, reusable UI, HTML-first `x-*` component tags, component imports, same-name `.html` templates, forwarding Python component props to `pp.props`, `get_attributes(...)`, `merge_classes(...)`, `twMerge(...)`, or where shared components belong in a Caspian project.
|
|
3
|
+
description: Use this page when the task mentions `@component`, reusable UI, component granularity or render ownership, HTML-first `x-*` component tags, component imports, same-name `.html` templates, forwarding Python component props to `pp.props`, `get_attributes(...)`, `merge_classes(...)`, `twMerge(...)`, or where shared components belong in a Caspian project.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Use the structure guide for file placement, the routing guide for route templates, the PulsePoint guide for browser-side scripts, and the data guide for component-owned RPC flows.
|
|
@@ -583,7 +583,11 @@ Do not create a single `AccountPage.py` or `DashboardTabs.py` that contains ever
|
|
|
583
583
|
|
|
584
584
|
Related subcomponents may live in one Python file only when they are tiny and tightly coupled, such as `Tabs`, `TabsList`, `TabsTrigger`, and `TabsContent` primitives, or a component plus two very small private helpers. For app-specific page chunks, prefer one exported component per file with a name that explains its role.
|
|
585
585
|
|
|
586
|
-
Props are the boundary between components. Pass parent-owned data and configuration down through attributes or direct Python calls, keep local interactive state inside the component that owns the behavior, and use slot content when the parent needs to provide authored child markup. If two sibling chunks need the same server data, load it in the route's `page()` or a shared helper and pass the relevant pieces into each component.
|
|
586
|
+
Props are the boundary between components. Pass parent-owned data and configuration down through attributes or direct Python calls, keep local interactive state inside the component that owns the behavior, and use slot content when the parent needs to provide authored child markup. If two sibling chunks need the same server data, load it in the route's `page()` or a shared helper and pass the relevant pieces into each component.
|
|
587
|
+
|
|
588
|
+
Component boundaries are also performance ownership boundaries. A value typed into a search field should not normally rerender a page-sized component that also owns a large card grid, several dialogs, and unrelated forms. Put high-frequency state in the smallest component that actually renders from it, and pass only the committed result or callback across the boundary. Do not split every element into a component; split where update frequency and subtree cost differ. A small search/toolbar component beside a result-list component is a useful boundary, while a wrapper that still owns both states and rerenders both subtrees is not.
|
|
589
|
+
|
|
590
|
+
Before blaming the PulsePoint runtime for a slow interaction, check whether the component explicitly requested the expensive render. Debouncing `setQuery(...)` still rerenders the query's owner when the timer fires. If the query is used only to build an RPC payload, keep it in `pp.ref(...)`, debounce the RPC, and keep returned rows in `pp.state(...)`. See [pulsepoint.md](./pulsepoint.md#high-performance-authoring) and [fetch-data.md](./fetch-data.md#search-filters-and-request-races).
|
|
587
591
|
|
|
588
592
|
## Component Root Refs
|
|
589
593
|
|
|
@@ -39,10 +39,10 @@ Important current `main.py` behaviors AI should keep in mind:
|
|
|
39
39
|
- `register_single_route(...)` passes path params to `page()` as one positional dict, injects matching query params by name, and injects `request` by keyword when declared.
|
|
40
40
|
- The render pipeline is `transform_components(...)`, then `render_with_nested_layouts(...)`, then `transform_scripts(...)`.
|
|
41
41
|
- Route-level generators returned from `page()` are wrapped in `SSE(...)` before the response is sent.
|
|
42
|
-
- Safe public-file helpers live in `casp.runtime_security`. `PublicFilesMiddleware` maps every existing nested file under `public/**` to the same root-relative URL without per-directory routes, rejects traversal and symlink escape, handles only `GET`/`HEAD`, and falls through when no file exists so page and mounted-app routing still works. User-upload directories must retain their restricted inline-media policy.
|
|
42
|
+
- Safe public-file helpers live in `casp.runtime_security`. `PublicFilesMiddleware` maps every existing nested file under `public/**` to the same root-relative URL without per-directory routes, rejects traversal and symlink escape, handles only `GET`/`HEAD`, and falls through when no file exists so page and mounted-app routing still works. User-upload directories must retain their restricted inline-media policy.
|
|
43
43
|
- Session middleware secrets may be resolved through `casp.runtime_security` so production can fail fast when `AUTH_SECRET` is missing or still on a default placeholder.
|
|
44
44
|
- Baseline response headers are built by `casp.runtime_security` and attached through `SecurityHeadersMiddleware`. That set now includes a Content-Security-Policy from `build_content_security_policy()`, which the `CONTENT_SECURITY_POLICY` environment variable replaces wholesale. The policy must keep `'unsafe-eval'` and `'unsafe-inline'` in `script-src`, because the PulsePoint runtime compiles templates with `new Function`; read the current helper before tightening it. Outside production the helper also adds loopback origins to `connect-src` so the BrowserSync live-reload client, which polls a different port than the proxied page, is not blocked; an environment override replaces that too.
|
|
45
|
-
- Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `PublicFilesMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective production request order is reversed: security headers, public files, rate limit, body limit, session, CSRF, auth, then RPC and routing. Development diagnostics are outermost. Existing public-file `GET`/`HEAD` requests stop at `PublicFilesMiddleware`; missing paths fall through. Verify the current list in `main.py` rather than trusting an older order.
|
|
45
|
+
- Middleware is added in source order as `RPCMiddleware`, `AuthMiddleware`, `CSRFMiddleware`, `SessionMiddleware`, `BodySizeLimitMiddleware`, `RateLimitMiddleware`, `PublicFilesMiddleware`, `SecurityHeadersMiddleware`, plus `RequestDiagnosticsMiddleware` outside production. The effective production request order is reversed: security headers, public files, rate limit, body limit, session, CSRF, auth, then RPC and routing. Development diagnostics are outermost. Existing public-file `GET`/`HEAD` requests stop at `PublicFilesMiddleware`; missing paths fall through. Verify the current list in `main.py` rather than trusting an older order.
|
|
46
46
|
- `public/js/pp-reactive-v2.js` saves scroll positions per history entry, resets window scroll on push navigation, and uses `pp-reset-scroll="true"` to opt specific containers or the whole body into reset behavior.
|
|
47
47
|
|
|
48
48
|
## Caspian Core Feature Map
|
|
@@ -112,7 +112,7 @@ Use these behavior checkpoints when AI needs the fastest verification path for a
|
|
|
112
112
|
|
|
113
113
|
| Runtime area | Verify these behaviors |
|
|
114
114
|
| --- | --- |
|
|
115
|
-
| `main.py` routing and request flow | route registration, path and query injection, generic public-root file handling and middleware placement, session-middleware wiring, response-header middleware, and exception rendering |
|
|
115
|
+
| `main.py` routing and request flow | route registration, path and query injection, generic public-root file handling and middleware placement, session-middleware wiring, response-header middleware, and exception rendering |
|
|
116
116
|
| `main.py` WebSockets | `cfg.websocket` route-registration gate, `@app.websocket(...)` paths, and the shared transport loop (message-size, idle-timeout, close codes, connection cleanup). Authorization is a single guard in `src/lib/websocket/websocket_security.py` (`authorize_websocket`) that runs the origin check before `accept()` and delegates auth to `Auth`. The HTTP middleware stack skips `scope["type"] == "websocket"`, so socket auth is enforced by that guard, not by `AuthMiddleware` |
|
|
117
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 |
|
|
118
118
|
| `casp.auth` | auth settings, signin and signout flow, provider wiring, and page protection behavior |
|
package/dist/docs/fetch-data.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Fetch Data
|
|
3
|
-
description: Use this page when the task mentions `page()`, `layout()`, `@rpc()`, `pp.rpc()`, streaming, interactive fetches, uploads, or browser-triggered data loads in Caspian.
|
|
3
|
+
description: Use this page when the task mentions `page()`, `layout()`, `@rpc()`, `pp.rpc()`, debounced search, filters, stale request races, streaming, interactive fetches, uploads, or browser-triggered data loads in Caspian.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Use the routing guide to place route logic correctly, then use the auth guide for protected actions, the MCP guide for AI-facing tools, the state guide for transient request-scoped mutation state, the cache guide for reusable first-render HTML, and the PulsePoint runtime guide for client-side `pp.rpc()` details.
|
|
@@ -182,12 +182,84 @@ Validate incoming form and mutation payloads before persisting them. See [valida
|
|
|
182
182
|
|
|
183
183
|
If an RPC action needs transient request-scoped success or error state beyond its direct return payload, read [state.md](./state.md) and verify the current wire-request lifecycle before depending on persistence across navigations or full-page reloads.
|
|
184
184
|
|
|
185
|
-
Important:
|
|
185
|
+
Important:
|
|
186
186
|
|
|
187
187
|
- `pp.rpc()` posts to the current route RPC bridge.
|
|
188
188
|
- Older docs may refer to `pp.fetchFunction()`. In this repo's current runtime, the supported helper is `pp.rpc()`.
|
|
189
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.
|
|
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.
|
|
191
|
+
|
|
192
|
+
## Search, Filters, And Request Races
|
|
193
|
+
|
|
194
|
+
A debounced search has two separate costs:
|
|
195
|
+
|
|
196
|
+
1. how often it starts an RPC request
|
|
197
|
+
2. what PulsePoint renders when the debounce timer fires and when the response arrives
|
|
198
|
+
|
|
199
|
+
Debouncing a `pp.state` setter reduces frequency but does not make its owner cheaper to render. If query text is not displayed by the template and exists only to build the eventual RPC payload, keep it in a ref, debounce the request directly, and put only the returned rows in state.
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<section>
|
|
203
|
+
<input
|
|
204
|
+
type="search"
|
|
205
|
+
placeholder="Search products"
|
|
206
|
+
oninput="scheduleSearch(event.target.value)"
|
|
207
|
+
/>
|
|
208
|
+
|
|
209
|
+
<p hidden="{results.length !== 0}">No results.</p>
|
|
210
|
+
<template pp-for="result in results">
|
|
211
|
+
<article key="{result.id}">{result.label}</article>
|
|
212
|
+
</template>
|
|
213
|
+
|
|
214
|
+
<script>
|
|
215
|
+
const [results, setResults] = pp.state([]);
|
|
216
|
+
const searchText = pp.ref("");
|
|
217
|
+
const searchTimer = pp.ref(null);
|
|
218
|
+
const requestGeneration = pp.ref(0);
|
|
219
|
+
|
|
220
|
+
function scheduleSearch(value) {
|
|
221
|
+
searchText.current = value;
|
|
222
|
+
const generation = ++requestGeneration.current;
|
|
223
|
+
|
|
224
|
+
clearTimeout(searchTimer.current);
|
|
225
|
+
searchTimer.current = setTimeout(
|
|
226
|
+
() => search(generation),
|
|
227
|
+
300
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function search(generation) {
|
|
232
|
+
const response = await pp.rpc(
|
|
233
|
+
"search_products",
|
|
234
|
+
{ query: searchText.current },
|
|
235
|
+
{ abortPrevious: true }
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (generation !== requestGeneration.current) return;
|
|
239
|
+
setResults(response.items ?? []);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
pp.effect(
|
|
243
|
+
() => () => clearTimeout(searchTimer.current),
|
|
244
|
+
[]
|
|
245
|
+
);
|
|
246
|
+
</script>
|
|
247
|
+
</section>
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Why this shape is responsive:
|
|
251
|
+
|
|
252
|
+
- The native input paints each character immediately.
|
|
253
|
+
- Updating `searchText.current`, the timer, and the generation does not request a render.
|
|
254
|
+
- The large result subtree changes only when an authoritative response is accepted.
|
|
255
|
+
- A response for an older query cannot overwrite a newer query.
|
|
256
|
+
- The timer is cleaned up when the component is disposed.
|
|
257
|
+
|
|
258
|
+
Use state for the query when the template genuinely renders from it, such as a controlled input, a visible query label, or a local filtered view. Keep that state in the smallest useful component boundary. When a large derived consumer may lag one commit, pass `pp.deferredValue(query)` to that consumer. Do not use a ref when the UI is expected to react to the mutation.
|
|
259
|
+
|
|
260
|
+
Loading indicators are state only when they create useful visible feedback. Do not set `loading=true` before every background refresh if existing results stay visible and no indicator changes; that adds a render before the result render. If feedback is needed, keep the indicator near the control rather than making a page-sized owner rerender.
|
|
261
|
+
|
|
262
|
+
`abortPrevious` reduces wasted request work, but a generation check still documents and enforces which response is authoritative. Reset pagination cursors when the accepted query/filter changes, and keep cursors in refs when changing them should not itself render.
|
|
191
263
|
|
|
192
264
|
## Streaming Responses
|
|
193
265
|
|
package/dist/docs/index.md
CHANGED
|
@@ -31,8 +31,9 @@ Before making feature, tooling, or file-placement decisions in a Caspian project
|
|
|
31
31
|
|
|
32
32
|
When generating or editing a Caspian app, treat these as the default choices unless the task explicitly requires something else:
|
|
33
33
|
|
|
34
|
-
- Use PulsePoint for reactive frontend behavior.
|
|
35
|
-
- For first-party HTML events and reactivity, use PulsePoint `on*` attributes, state, refs, effects, directives, and `pp.rpc()` instead of ordinary DOM wiring with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML`.
|
|
34
|
+
- Use PulsePoint for reactive frontend behavior.
|
|
35
|
+
- For first-party HTML events and reactivity, use PulsePoint `on*` attributes, state, refs, effects, directives, and `pp.rpc()` instead of ordinary DOM wiring with ids, `data-*` state, `querySelector`, `addEventListener`, or manual `innerHTML`.
|
|
36
|
+
- For PulsePoint performance, treat state as "render required," keep non-rendering timers/request generations/cursors in refs, keep high-frequency state in the smallest useful component, and classify broad authored renders separately from runtime reconciliation overhead. Read [pulsepoint.md](./pulsepoint.md#high-performance-authoring) before changing the runtime for a slow input, search, filter, sidebar, or provider.
|
|
36
37
|
- Treat every authored route, layout, and component HTML file like a React component return value **in shape only**: exactly one top-level parent HTML element or one imported `x-*` root, with any owned plain `<script>` kept inside that same root.
|
|
37
38
|
- That single-root analogy is the *only* place React applies to markup. Template files are plain HTML — no JSX, no `{cond && <div/>}`, no `{list.map(...)}`, no unquoted `class={...}`. Conditionals are `hidden="{...}"` and lists are `<template pp-for="…">`. See [pulsepoint.md](./pulsepoint.md) "PulsePoint Is Not JSX" before writing any template.
|
|
38
39
|
- When `caspian.config.json` has `tailwindcss: true`, use Python `merge_classes(...)` plus browser `twMerge(...)` as the only supported Tailwind class-merging path.
|
|
@@ -81,14 +82,14 @@ The packaged Caspian docs referenced by this index live here:
|
|
|
81
82
|
- `installation.md` - First-time setup flow for creating a new Caspian application
|
|
82
83
|
- `commands.md` - Main Caspian CLI workflows for project creation, generation, updates, and config-aware maintenance
|
|
83
84
|
- `core-runtime-map.md` - map of `main.py` plus installed `casp` modules to the packaged docs that explain them and the behaviors AI should trace there
|
|
84
|
-
- `pulsepoint-runtime-map.md` - fast feature-to-runtime lookup for PulsePoint state, effects, refs, context, portals, lists, events, RPC, uploads, streaming, SPA navigation, scroll restoration, and Tailwind merge behavior
|
|
85
|
+
- `pulsepoint-runtime-map.md` - fast feature-to-runtime lookup for PulsePoint state, effects, refs, render-performance ownership, context, portals, lists, events, RPC, uploads, streaming, SPA navigation, scroll restoration, and Tailwind merge behavior
|
|
85
86
|
- `mcp.md` - MCP-specific layout, launch flow, and AI routing rules for projects where `caspian.config.json` enables MCP
|
|
86
87
|
- `database.md` - Prisma schema, migration, seed, and client-generation workflow for projects where `caspian.config.json` enables Prisma, plus Python-side helper caveats
|
|
87
88
|
- `auth.md` - Session-backed authentication with `casp.auth`, centralized `auth_config.py`, public-vs-private route mode guidance, RPC-first signout guidance, RBAC, and OAuth provider helpers
|
|
88
89
|
- `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
|
|
89
|
-
- `components.md` - Create reusable Python components, template-backed
|
|
90
|
-
- `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
|
|
91
|
-
- `fetch-data.md` - Initial server-side data loading and browser-triggered RPC flows with `pp.rpc()`, streaming, uploads, and auth-aware actions
|
|
90
|
+
- `components.md` - Create reusable Python components, choose component granularity and render ownership, use template-backed or single-file `html(...)` UI, resolve HTML-first `x-*` tags and slots, keep one authored root, and apply the Python-side `merge_classes(...)` contract when Tailwind CSS is enabled
|
|
91
|
+
- `pulsepoint.md` - Default reactive frontend runtime contract for component scripts, first-party `on*` events, high-performance state/ref ownership, effects, directives, SPA navigation scroll restoration, `pp-reset-scroll`, and direct browser `twMerge(...)` usage when Tailwind CSS is enabled
|
|
92
|
+
- `fetch-data.md` - Initial server-side data loading and browser-triggered RPC flows with `pp.rpc()`, including responsive debounced search, stale-response control, streaming, uploads, and auth-aware actions
|
|
92
93
|
- `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
|
|
93
94
|
- `file-uploads.md` - Route-local file uploads and file-manager flows with `@rpc()`, `pp.rpc()`, Prisma metadata, public asset storage, and BrowserSync ignore rules
|
|
94
95
|
- `state.md` - Request-scoped server state with `StateManager`, session-backed JSON persistence, and listener callbacks for transient flows
|
|
@@ -114,7 +115,7 @@ Preferred lookup order:
|
|
|
114
115
|
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.
|
|
115
116
|
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`.
|
|
116
117
|
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.
|
|
117
|
-
9. If the task names a PulsePoint feature or
|
|
118
|
+
9. If the task names a PulsePoint feature, directive, slow interaction, unnecessary rerender, or reconciliation concern, read `pulsepoint-runtime-map.md` for the fastest feature-to-owner lookup, then read `pulsepoint.md` for authoring and performance rules.
|
|
118
119
|
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.
|
|
119
120
|
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.
|
|
120
121
|
12. Prefer packaged Caspian docs before upstream documentation when generating code, commands, or migration guidance.
|
|
@@ -180,24 +180,24 @@ This folder contains your database model definitions in `schema.prisma` and any
|
|
|
180
180
|
|
|
181
181
|
### `public/`
|
|
182
182
|
|
|
183
|
-
Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
|
|
184
|
-
|
|
185
|
-
The public root maps directly to the URL root. Any existing nested file is
|
|
186
|
-
available at the same root-relative URL—for example,
|
|
187
|
-
`public/icons/app.png` is served as `/icons/app.png`. Adding a public
|
|
188
|
-
subdirectory does not require a matching route or mount in `main.py`.
|
|
189
|
-
|
|
190
|
-
Runtime-uploaded public blobs can also live here. Confirm the actual upload path in the project code, commonly `public/uploads/`, and keep that directory aligned with any BrowserSync ignore rules.
|
|
191
|
-
|
|
192
|
-
If the upload directory is created only at runtime, create it on demand in the owning upload helper instead of assuming it is already committed.
|
|
193
|
-
|
|
194
|
-
Public means browser-accessible, not automatically safe to render inline.
|
|
195
|
-
`PublicFilesMiddleware.inline_safe_subdirectories` must include every top-level
|
|
196
|
-
public directory that receives untrusted runtime uploads. Unsafe types in a
|
|
197
|
-
configured upload directory, notably HTML and SVG, are sent as attachments with
|
|
198
|
-
`nosniff`; trusted first-party assets are served inline. The standard
|
|
199
|
-
`public/uploads/**` location is protected only when `main.py` configures the
|
|
200
|
-
top-level `uploads` entry.
|
|
183
|
+
Store static assets here, including images, fonts, and generated frontend assets that should be served directly.
|
|
184
|
+
|
|
185
|
+
The public root maps directly to the URL root. Any existing nested file is
|
|
186
|
+
available at the same root-relative URL—for example,
|
|
187
|
+
`public/icons/app.png` is served as `/icons/app.png`. Adding a public
|
|
188
|
+
subdirectory does not require a matching route or mount in `main.py`.
|
|
189
|
+
|
|
190
|
+
Runtime-uploaded public blobs can also live here. Confirm the actual upload path in the project code, commonly `public/uploads/`, and keep that directory aligned with any BrowserSync ignore rules.
|
|
191
|
+
|
|
192
|
+
If the upload directory is created only at runtime, create it on demand in the owning upload helper instead of assuming it is already committed.
|
|
193
|
+
|
|
194
|
+
Public means browser-accessible, not automatically safe to render inline.
|
|
195
|
+
`PublicFilesMiddleware.inline_safe_subdirectories` must include every top-level
|
|
196
|
+
public directory that receives untrusted runtime uploads. Unsafe types in a
|
|
197
|
+
configured upload directory, notably HTML and SVG, are sent as attachments with
|
|
198
|
+
`nosniff`; trusted first-party assets are served inline. The standard
|
|
199
|
+
`public/uploads/**` location is protected only when `main.py` configures the
|
|
200
|
+
top-level `uploads` entry.
|
|
201
201
|
|
|
202
202
|
If the local BrowserSync stack is running, keep that upload directory in `settings/bs-config.ts` `PUBLIC_IGNORE_DIRS` so new uploads do not force a full browser reload.
|
|
203
203
|
|
|
@@ -231,14 +231,14 @@ Use this package module when the task is about:
|
|
|
231
231
|
- user-facing vs production-safe exception messaging helpers
|
|
232
232
|
- baseline response headers such as permissions, referrer, MIME sniffing, framing, or HSTS behavior
|
|
233
233
|
|
|
234
|
-
Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
|
|
235
|
-
|
|
236
|
-
`PublicFilesMiddleware` serves only existing `GET`/`HEAD` files that resolve
|
|
237
|
-
inside the configured public root and falls through for every other request.
|
|
238
|
-
Install it inside the security-header layer and outside rate limiting, sessions,
|
|
239
|
-
CSRF, auth, RPC, and page routing. Preserve a restricted inline-media policy for
|
|
240
|
-
user-upload directories so executable uploads such as HTML and SVG download
|
|
241
|
-
instead of rendering with the application's origin.
|
|
234
|
+
Because this file is framework-owned, edit it only for Caspian runtime work or when documentation must match the installed package. App-specific auth policy still belongs in `src/lib/auth/auth_config.py`, and app-specific upload or storage behavior should live in route-owned code or other `src/lib/**` helpers.
|
|
235
|
+
|
|
236
|
+
`PublicFilesMiddleware` serves only existing `GET`/`HEAD` files that resolve
|
|
237
|
+
inside the configured public root and falls through for every other request.
|
|
238
|
+
Install it inside the security-header layer and outside rate limiting, sessions,
|
|
239
|
+
CSRF, auth, RPC, and page routing. Preserve a restricted inline-media policy for
|
|
240
|
+
user-upload directories so executable uploads such as HTML and SVG download
|
|
241
|
+
instead of rendering with the application's origin.
|
|
242
242
|
|
|
243
243
|
### `caspian.config.json`
|
|
244
244
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: PulsePoint Runtime Map
|
|
3
|
-
description: Use this page when AI needs a fast feature-to-runtime lookup for PulsePoint behavior before editing `src/app/**`, component templates, or `public/js/pp-reactive-v2.js`.
|
|
3
|
+
description: Use this page when AI needs a fast feature-to-runtime lookup for PulsePoint behavior or must classify a performance problem as component ownership versus runtime reconciliation before editing `src/app/**`, component templates, or `public/js/pp-reactive-v2.js`.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Use the PulsePoint guide for authoring rules, the routing and component guides for template placement, and the core runtime map when Python-side transforms are involved.
|
|
@@ -44,7 +44,8 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
44
44
|
| External stores | `pp.syncExternalStore(subscribe, getSnapshot)` | `public/js/pp-reactive-v2.js` | `subscribe` must be stable — wrap in `pp.callback(..., [])`; runtime re-reads the snapshot right after subscribing to close the render-to-subscribe gap; cleanup runs on dispose |
|
|
45
45
|
| Imperative handles | `pp.imperativeHandle(ref, createHandle, deps?)` | `public/js/pp-reactive-v2.js` | parent passes its own `pp.ref(...)` down as a normal prop (object props pass by reference); do not author `pp-ref-forward`; handle publishes in the layout-effect phase and clears on unmount |
|
|
46
46
|
| Transitions | `pp.transition()` | `public/js/pp-reactive-v2.js` | returns `[isPending, startTransition]`; not concurrent — rendering is still synchronous; handles sync and promise scopes; overlapping scopes stay pending until the last settles |
|
|
47
|
-
| Deferred values | `pp.deferredValue(value, initialValue?)` | `public/js/pp-reactive-v2.js` | lags one commit behind the source |
|
|
47
|
+
| Deferred values | `pp.deferredValue(value, initialValue?)` | `public/js/pp-reactive-v2.js` | lags one commit behind the source |
|
|
48
|
+
| Render performance | state/ref ownership, component boundaries, `pp.enablePerf()` | authored component first; `public/js/pp-reactive-v2.js` when runtime phases are disproportionate | state means "render required"; refs hold non-rendering mutable bookkeeping; debounce does not reduce render cost; `pp.transition()` is not concurrent; diagnose redundant owner renders before changing reconciliation |
|
|
48
49
|
| Optimistic updates | `pp.optimistic(passthrough, reducer?)` | `public/js/pp-reactive-v2.js` | pending actions are dropped when `passthrough` changes, so a confirmed server value never double-counts the guess; without a reducer each action replaces the value |
|
|
49
50
|
| Error boundaries | `pp.errorBoundary()` | `public/js/pp-reactive-v2.js` | returns `[error, reset]`; render **and** effect/cleanup throws walk logical component ancestry to the nearest boundary; a boundary also catches its own throws; latches until `reset()`; caps at 5 captures per arming; event-handler errors are not routed — use `try`/`catch` |
|
|
50
51
|
| Conditional UI | `hidden="{!cond}"`, a ternary inside `{...}`, or `pp-for` over a 0/1-length array | `public/js/pp-reactive-v2.js` | **there is no `pp-if`, `pp-show`, or `pp-else`, and no JSX `{cond && <div/>}`**; `hidden` is a bound boolean attribute (Tailwind preflight makes `[hidden]` `display:none !important`, so it beats a `flex`/`grid` utility; without preflight a `display` rule wins); a hidden subtree still renders, so guard with `?.`. See [pulsepoint.md](./pulsepoint.md) "Conditional rendering" |
|
|
@@ -66,7 +67,9 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
66
67
|
- Read [fetch-data.md](./fetch-data.md) before adding browser-triggered backend work.
|
|
67
68
|
- Use this map when the task names a PulsePoint feature and you need the owning runtime file quickly.
|
|
68
69
|
- Verify implemented behavior in `public/js/pp-reactive-v2.js` before adding new PulsePoint API claims.
|
|
69
|
-
- If an interaction is normal first-party HTML behavior, route it through PulsePoint before considering standard DOM scripting.
|
|
70
|
+
- If an interaction is normal first-party HTML behavior, route it through PulsePoint before considering standard DOM scripting.
|
|
71
|
+
- For a slow input, search, filter, sidebar, or provider, classify the update before changing the runtime: unnecessary authored state/broad ownership is an app issue; byte-identical output that still traverses a large stable subtree may be a runtime issue.
|
|
72
|
+
- Use `pp.enablePerf()`, reproduce one interaction, inspect `pp.getPerfStats()`, and reset between comparisons. Correlate the expensive component and phase with the state setter that requested it.
|
|
70
73
|
|
|
71
74
|
## Copy-Safe Authoring Rules
|
|
72
75
|
|
|
@@ -83,8 +86,11 @@ If an inspected browser DOM disagrees with authored template source, remember th
|
|
|
83
86
|
- Use `pp.syncExternalStore(...)` for browser or global sources the component does not own (`matchMedia`, `localStorage`, `navigator.onLine`, a shared store) instead of `pp.state` plus `pp.effect`.
|
|
84
87
|
- Wrap a component subtree that can fail in a parent with `pp.errorBoundary()` rather than leaving a render throw to the console.
|
|
85
88
|
- In a Python component, list every prop the template's `{...}` expressions reference in `get_attributes({...}, props)`. If a binding "does nothing" — an icon toggle that never switches, a `hidden` that never applies — check that prop is on the rendered root before debugging the expression.
|
|
86
|
-
- Prefer PulsePoint state and directives over manual `innerHTML` repainting.
|
|
87
|
-
- Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
|
|
89
|
+
- Prefer PulsePoint state and directives over manual `innerHTML` repainting.
|
|
90
|
+
- Keep direct DOM APIs inside `pp.ref(...)` plus `pp.effect(...)` only when a third-party or browser API integration actually requires them.
|
|
91
|
+
- Use `pp.state(...)` only for values that must render. Use `pp.ref(...)` for debounce handles, request generations, pagination cursors, and transient RPC query text that should persist without rendering.
|
|
92
|
+
- Debounce the expensive action, not an otherwise unnecessary page-sized state update. Keep returned rows in state, discard stale responses, and avoid loading-state commits that change no visible UI.
|
|
93
|
+
- Keep keystroke-frequency state in the smallest useful component boundary. Use `pp.deferredValue(...)` only when the source truly must be state and an expensive consumer may lag by one commit.
|
|
88
94
|
|
|
89
95
|
## Compact Examples
|
|
90
96
|
|
|
@@ -179,5 +185,7 @@ Use these prompts after docs or runtime changes to confirm AI can route correctl
|
|
|
179
185
|
- "Explain why authored scripts are plain `<script>` but browser output shows `type=\"text/pp\"`."
|
|
180
186
|
- "Debug a dashboard sidebar losing scroll during child-route navigation."
|
|
181
187
|
- "Add an upload widget with progress and a reactive file list."
|
|
182
|
-
- "Use context to share theme state between parent and child components."
|
|
188
|
+
- "Use context to share theme state between parent and child components."
|
|
189
|
+
- "A debounced product search pauses the next keystroke. Decide whether this is component ownership or a PulsePoint runtime defect, then show the correct server-search pattern."
|
|
190
|
+
- "Profile a sidebar toggle whose rendered HTML is byte-identical but whose provider owns 1,000 descendants." (Correct analysis profiles runtime phases and treats stable-subtree traversal as a possible runtime issue.)
|
|
183
191
|
- "Render a list of users with edit and delete buttons, plus a confirmation modal." (Correct output uses `<template pp-for>` with `key` and `hidden="{…}"`; JSX `.map()` or `&&` in the template is a failure.)
|
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: PulsePoint Runtime Guide
|
|
3
|
-
description: Use this page when the task mentions PulsePoint, `pp.state`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read
|
|
3
|
+
description: Use this page when the task mentions PulsePoint, performance, rerenders, slow inputs or searches, `pp.state`, `pp.ref`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read "PulsePoint Is Not JSX" before writing templates and "High-performance authoring" before changing runtime reconciliation.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Read the components, routing, data-fetching, and project-structure docs alongside the PulsePoint runtime contract.
|
|
@@ -771,7 +771,172 @@ Behavior to rely on:
|
|
|
771
771
|
- A boundary absorbs at most five errors without an intervening `reset()`, then logs and stops catching. This stops a fallback that itself throws from spinning the render loop. `reset()` re-arms it.
|
|
772
772
|
- Event handler errors are not routed to boundaries. Handle those with `try`/`catch` inside the handler, which matches React.
|
|
773
773
|
|
|
774
|
-
##
|
|
774
|
+
## High-performance authoring
|
|
775
|
+
|
|
776
|
+
PulsePoint renders synchronously when a component requests an update. That makes state ownership and component boundaries part of the performance contract: the runtime can optimize reconciliation, but it cannot decide that an authored state change was unnecessary.
|
|
777
|
+
|
|
778
|
+
### State means "render required"
|
|
779
|
+
|
|
780
|
+
Use `pp.state(...)` when changing the value must affect at least one render-facing behavior:
|
|
781
|
+
|
|
782
|
+
- template text, attributes, lists, or conditional visibility
|
|
783
|
+
- a bound child prop or provided context value
|
|
784
|
+
- a memo, effect, or other render dependency that must follow the value
|
|
785
|
+
- authoritative data returned from an RPC call
|
|
786
|
+
|
|
787
|
+
Use `pp.ref(...)` when a mutable value must persist but changing it should not render:
|
|
788
|
+
|
|
789
|
+
- debounce and timeout handles
|
|
790
|
+
- request generations or stale-response tokens
|
|
791
|
+
- pagination cursors that are not displayed
|
|
792
|
+
- previous values and imperative integration handles
|
|
793
|
+
- transient search text used only to build a later RPC payload
|
|
794
|
+
|
|
795
|
+
A ref mutation is intentionally invisible to the template. Do not replace state with a ref when the UI is expected to update.
|
|
796
|
+
|
|
797
|
+
### Debounce frequency and render cost are different
|
|
798
|
+
|
|
799
|
+
A debounce answers "how often should this work start?" It does not answer "how much work happens when it starts?"
|
|
800
|
+
|
|
801
|
+
This shape can still stall a large owner:
|
|
802
|
+
|
|
803
|
+
```html
|
|
804
|
+
<script>
|
|
805
|
+
const [query, setQuery] = pp.state("");
|
|
806
|
+
|
|
807
|
+
function onSearch(value) {
|
|
808
|
+
clearTimeout(onSearch.timer);
|
|
809
|
+
onSearch.timer = setTimeout(() => setQuery(value), 300);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
pp.effect(() => {
|
|
813
|
+
loadRows(query);
|
|
814
|
+
}, [query]);
|
|
815
|
+
</script>
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
When the timer fires, `setQuery` asks the whole owning component to render even if `query` never appears in its markup. If that owner also contains a large list, dialogs, forms, providers, or many nested components, typing that resumes near the timer can compete with the render.
|
|
819
|
+
|
|
820
|
+
When query text exists only for an RPC request, keep the query and timer in refs, debounce the request itself, and render only the accepted result:
|
|
821
|
+
|
|
822
|
+
```html
|
|
823
|
+
<section>
|
|
824
|
+
<input
|
|
825
|
+
type="search"
|
|
826
|
+
placeholder="Search products"
|
|
827
|
+
oninput="scheduleSearch(event.target.value)"
|
|
828
|
+
/>
|
|
829
|
+
|
|
830
|
+
<template pp-for="row in rows">
|
|
831
|
+
<article key="{row.id}">{row.label}</article>
|
|
832
|
+
</template>
|
|
833
|
+
|
|
834
|
+
<script>
|
|
835
|
+
const [rows, setRows] = pp.state([]);
|
|
836
|
+
const query = pp.ref("");
|
|
837
|
+
const timer = pp.ref(null);
|
|
838
|
+
const generation = pp.ref(0);
|
|
839
|
+
|
|
840
|
+
function scheduleSearch(value) {
|
|
841
|
+
query.current = value;
|
|
842
|
+
const requestGeneration = ++generation.current;
|
|
843
|
+
|
|
844
|
+
clearTimeout(timer.current);
|
|
845
|
+
timer.current = setTimeout(
|
|
846
|
+
() => search(requestGeneration),
|
|
847
|
+
300
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function search(requestGeneration) {
|
|
852
|
+
const response = await pp.rpc(
|
|
853
|
+
"search_products",
|
|
854
|
+
{ query: query.current },
|
|
855
|
+
{ abortPrevious: true }
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
if (requestGeneration !== generation.current) return;
|
|
859
|
+
setRows(response.items ?? []);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
pp.effect(
|
|
863
|
+
() => () => clearTimeout(timer.current),
|
|
864
|
+
[]
|
|
865
|
+
);
|
|
866
|
+
</script>
|
|
867
|
+
</section>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The native input remains immediate, timer/query bookkeeping does not render, and only the authoritative rows update the list. The generation check prevents an older response from overwriting a newer search. See [fetch-data.md](./fetch-data.md#search-filters-and-request-races) for the RPC-side rules.
|
|
871
|
+
|
|
872
|
+
### Keep high-frequency state close to the control
|
|
873
|
+
|
|
874
|
+
Component boundaries define who owns a render. A keystroke-frequency value should live in the smallest component that needs to render from it.
|
|
875
|
+
|
|
876
|
+
- Split a search/toolbar from a large result grid when their update frequencies differ.
|
|
877
|
+
- Split frequently edited form state from unrelated dialogs, page shells, and providers.
|
|
878
|
+
- Keep shared state in a parent only when multiple children genuinely need the same render-driving value.
|
|
879
|
+
- Do not create wrapper components that still own all high-frequency state and therefore rerender every subtree.
|
|
880
|
+
|
|
881
|
+
For a controlled input that genuinely must write state on every keystroke, keep its owner small. If an expensive derived consumer may lag by one commit, use `pp.deferredValue(...)` for that consumer:
|
|
882
|
+
|
|
883
|
+
```html
|
|
884
|
+
<section>
|
|
885
|
+
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
886
|
+
<x-large-results query="{deferredQuery}" />
|
|
887
|
+
|
|
888
|
+
<script>
|
|
889
|
+
const [query, setQuery] = pp.state("");
|
|
890
|
+
const deferredQuery = pp.deferredValue(query, "");
|
|
891
|
+
</script>
|
|
892
|
+
</section>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
`pp.deferredValue` is not a substitute for correct ownership, and `pp.transition()` does not time-slice or deprioritize work. PulsePoint remains synchronous.
|
|
896
|
+
|
|
897
|
+
### Avoid duplicate and invisible commits
|
|
898
|
+
|
|
899
|
+
Each setter that changes a value requests a render, even when the eventual UI change is small.
|
|
900
|
+
|
|
901
|
+
- Do not set `loading=true` for a background refresh when existing rows remain visible and no loading indicator changes.
|
|
902
|
+
- Batch related state changes in the same synchronous turn when possible; PulsePoint coalesces scheduled updates.
|
|
903
|
+
- Do not mirror the same value in multiple state hooks.
|
|
904
|
+
- Keep pagination/request cursors in refs when moving the cursor should only start work, then put returned rows and visible metadata in state.
|
|
905
|
+
- Memoize only genuinely expensive derivations or values whose identity matters. `pp.memo` does not make an unnecessary owner render free.
|
|
906
|
+
- Use keyed `pp-for` rows so reconciliation can preserve row identity.
|
|
907
|
+
|
|
908
|
+
### Diagnose the owner before the runtime
|
|
909
|
+
|
|
910
|
+
Classify the symptom before editing `public/js/pp-reactive-v2.js`:
|
|
911
|
+
|
|
912
|
+
| Evidence | Likely classification | First action |
|
|
913
|
+
| --- | --- | --- |
|
|
914
|
+
| A state setter fires, but the changed value is not rendered | Component authoring issue | Move non-rendering bookkeeping to a ref or call the action directly |
|
|
915
|
+
| A debounce fires just before typing stalls | Component/request ownership issue | Debounce the RPC or expensive action, not an unnecessary broad-owner setter |
|
|
916
|
+
| Loading and result setters produce two large commits | Component authoring issue | Remove invisible loading commits or isolate the visible indicator |
|
|
917
|
+
| A high-frequency control owns a large list/provider/dialog subtree | Component-boundary issue | Move the state to a smaller focused component |
|
|
918
|
+
| Rendered HTML is byte-identical but a large stable subtree is still reconciled | Possible runtime issue | Profile runtime phases and verify the stable-DOM case |
|
|
919
|
+
| One small necessary binding change spends disproportionate time in DOM diffing or nested bootstrap | Possible runtime issue | Build a focused reproduction and benchmark before changing reconciliation |
|
|
920
|
+
|
|
921
|
+
Enable measurements only while profiling:
|
|
922
|
+
|
|
923
|
+
```html
|
|
924
|
+
<script>
|
|
925
|
+
pp.enablePerf();
|
|
926
|
+
pp.resetPerfStats();
|
|
927
|
+
|
|
928
|
+
// Reproduce one interaction, then inspect:
|
|
929
|
+
console.log(pp.getPerfStats());
|
|
930
|
+
|
|
931
|
+
pp.disablePerf();
|
|
932
|
+
</script>
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
Correlate the expensive component and phase with the setter or RPC completion that requested it. Compare the same interaction, DOM size, data, and number of repetitions before and after a change. Input paint timing and RPC latency are separate from render timing, so measure them separately.
|
|
936
|
+
|
|
937
|
+
Do not "fix" a PulsePoint performance problem with `querySelector`, manual listeners, `innerHTML`, or parallel DOM state. Those bypass ownership instead of correcting it.
|
|
938
|
+
|
|
939
|
+
## Context
|
|
775
940
|
|
|
776
941
|
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`.
|
|
777
942
|
|
|
@@ -1153,8 +1318,12 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
1153
1318
|
- In authored Caspian templates, do not handwrite `pp-component` or `type="text/pp"`; let the render pipeline inject them.
|
|
1154
1319
|
- 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`.
|
|
1155
1320
|
- 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.
|
|
1156
|
-
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
1157
|
-
-
|
|
1321
|
+
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
1322
|
+
- Treat `pp.state(...)` as a request to render. Keep timers, request generations, undisplayed pagination cursors, and transient RPC-only query text in `pp.ref(...)`; do not move a value to a ref when markup or render dependencies must react to it.
|
|
1323
|
+
- A debounce limits frequency but does not reduce the cost of the update it eventually runs. For server search, debounce the RPC, discard stale responses, and keep only accepted results in state instead of waking a page-sized owner through an undisplayed query state.
|
|
1324
|
+
- Keep high-frequency input state in the smallest useful component boundary. Use `pp.deferredValue(...)` only when an expensive consumer may safely lag one commit, and remember that `pp.transition()` is not concurrent scheduling.
|
|
1325
|
+
- Before changing runtime reconciliation, use `pp.enablePerf()` to decide whether the app requested a redundant broad render or stable byte-identical output still incurred disproportionate runtime work.
|
|
1326
|
+
- Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
|
|
1158
1327
|
- 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(...)`.
|
|
1159
1328
|
- If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
|
|
1160
1329
|
- In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
|
|
@@ -1217,7 +1386,9 @@ These are current runtime caveats that matter for authors and AI tools:
|
|
|
1217
1386
|
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
1218
1387
|
- `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
|
|
1219
1388
|
- Top-level array destructuring is exported to template scope for any initializer, not only `pp.state` and `pp.reducer`. A binding declared inside a block or function is still not exported.
|
|
1220
|
-
- `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
|
|
1389
|
+
- `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
|
|
1390
|
+
- `pp.state(...)` changes schedule a component render; `pp.ref(...)` mutations do not. This is an ownership contract, not merely a syntax choice.
|
|
1391
|
+
- Debouncing a state setter does not make the resulting render cheaper. A large owner should not hold RPC-only query text, timer handles, request generations, or undisplayed pagination cursors in state.
|
|
1221
1392
|
- `pp.syncExternalStore(...)` resubscribes whenever `subscribe` changes identity, so pass a `pp.callback(..., [])`-wrapped function.
|
|
1222
1393
|
- `pp.optimistic(...)` clears its pending actions on the render where `passthrough` changes, using a render-phase update rather than an effect, so there is no frame showing stale optimistic output.
|
|
1223
1394
|
- `pp.errorBoundary()` catches render, effect, and effect-cleanup throws, including the boundary component's own. It does not catch event-handler errors. It latches until `reset()` and stops catching after five captures without a reset.
|