caspian-utils 0.2.7 → 0.2.8

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.
@@ -320,7 +320,7 @@ async def chat(message: str):
320
320
  yield text
321
321
  ```
322
322
 
323
- Client side, append each chunk to reactive state so the UI grows token by token:
323
+ Client side, append each chunk to reactive state so the UI grows token by token:
324
324
 
325
325
  ```html
326
326
  <script>
@@ -333,10 +333,12 @@ Client side, append each chunk to reactive state so the UI grows token by token:
333
333
  onStreamComplete: () => console.log("done"),
334
334
  });
335
335
  }
336
- </script>
337
- ```
338
-
339
- In the current runtime, generator and async-generator RPC results are wrapped with `SSE(...)` inside `casp.rpc.py`, and route-level generator results are also wrapped by `main.py` before the response is returned.
336
+ </script>
337
+ ```
338
+
339
+ When a streaming call uses `abortPrevious: true`, it remains the active cancellable RPC until the stream's final chunk. A later RPC call with `abortPrevious: true` can therefore cancel a response even after its headers and early chunks have arrived. Cancellation resolves to `{ cancelled: true }`; use a request generation as well when the UI must state explicitly which response is authoritative.
340
+
341
+ In the current runtime, generator and async-generator RPC results are wrapped with `SSE(...)` inside `casp.rpc.py`, and route-level generator results are also wrapped by `main.py` before the response is returned.
340
342
 
341
343
  ## File Uploads
342
344
 
@@ -379,11 +381,11 @@ Client example:
379
381
  <script>
380
382
  async function uploadSelectedFiles(fileList, collection = "auto") {
381
383
  for (const file of Array.from(fileList ?? [])) {
382
- const response = await pp.rpc("upload_file", { file, collection }, {
383
- onUploadProgress: (progress) => {
384
- console.log(progress.percentage ?? 0);
385
- },
386
- });
384
+ const response = await pp.rpc("upload_file", { file, collection }, {
385
+ onUploadProgress: (progress) => {
386
+ console.log(progress.percent ?? 0);
387
+ },
388
+ });
387
389
 
388
390
  if (response?.data) {
389
391
  setFileManagerData(response.data);
@@ -391,9 +393,11 @@ Client example:
391
393
  }
392
394
  }
393
395
  </script>
394
- ```
395
-
396
- Use this pattern for real file managers:
396
+ ```
397
+
398
+ `onUploadProgress` receives `{ loaded, total, percent }`; there is no `percentage` field. A payload containing a `File` or non-empty `FileList` becomes multipart. PulsePoint emits all non-file fields before file parts so companion values are available to a server that starts handling a streamed file before the browser has finished sending the body. Object values are JSON-stringified, nullish values are omitted, and every file in a `FileList` is appended under the original field name.
399
+
400
+ Use this pattern for real file managers:
397
401
 
398
402
  - Keep upload and delete actions in the owning route `index.py`, not in `main.py`.
399
403
  - Use `page()` to render the initial manager payload.
@@ -129,11 +129,11 @@ Keep route-specific auth checks, input validation, and final response shape here
129
129
 
130
130
  async function uploadSelectedFiles(fileList) {
131
131
  for (const file of Array.from(fileList ?? [])) {
132
- const response = await pp.rpc("upload_asset", { file }, {
133
- onUploadProgress: (progress) => {
134
- console.log(progress.percentage ?? 0);
135
- },
136
- });
132
+ const response = await pp.rpc("upload_asset", { file }, {
133
+ onUploadProgress: (progress) => {
134
+ console.log(progress.percent ?? 0);
135
+ },
136
+ });
137
137
 
138
138
  if (response?.data) {
139
139
  setFileManagerData(response.data);
@@ -142,9 +142,11 @@ Keep route-specific auth checks, input validation, and final response shape here
142
142
  }
143
143
  </script>
144
144
  </section>
145
- ```
146
-
147
- Prefer this state-driven shape over manual `innerHTML` list painting. In Caspian pages, manual DOM writes are easy to lose when PulsePoint rerenders the owner template.
145
+ ```
146
+
147
+ `onUploadProgress` receives `{ loaded, total, percent }`; `total` and `percent` may be `null` when the browser cannot compute the upload length. A multipart RPC writes companion non-file values before all `File`/`FileList` parts, so route parameters are available even when the server begins processing a streamed upload before the full request body arrives. A `FileList` is sent as repeated parts under one field name.
148
+
149
+ Prefer this state-driven shape over manual `innerHTML` list painting. In Caspian pages, manual DOM writes are easy to lose when PulsePoint rerenders the owner template.
148
150
 
149
151
  ## Persistence Rules
150
152
 
@@ -50,10 +50,10 @@ If an inspected browser DOM disagrees with authored template source, remember th
50
50
  | 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" |
51
51
  | Lists | `<template pp-for="item in items">` | `public/js/pp-reactive-v2.js` | `pp-for` belongs on `<template>`, accepts arrays and synchronous iterables, captures rendered row values for events/props, and uses plain `key`, not `pp-key` |
52
52
  | Events | native `onclick`, `oninput`, `onchange`, `onsubmit` | `public/js/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 |
53
- | RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; protected actions use `@rpc(require_auth=True)` |
54
- | Upload progress | `pp.rpc(..., { onUploadProgress })` | `public/js/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 |
55
- | Streaming | `pp.rpc(..., { onStream })` | `public/js/pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses |
56
- | SPA navigation | `body[pp-spa="true"]`, links | `public/js/pp-reactive-v2.js`, `main.py` | same-origin eligible links intercept; root-layout mismatches hard reload |
53
+ | RPC | `pp.rpc(...)` in scripts, `@rpc()` in Python | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | use `pp.rpc`, not legacy `pp.fetchFunction`; options include abort, URL/CSRF URL, credentials, streaming, and upload callbacks; protected actions use `@rpc(require_auth=True)` |
54
+ | Upload progress | `pp.rpc(..., { onUploadProgress })` | `public/js/pp-reactive-v2.js`, `casp/rpc.py` | XHR path is used for progress callbacks; the callback receives `{ loaded, total, percent }`, never `percentage`; multipart scalar/object fields precede file parts, and a `FileList` repeats its field name; replace state from returned payload |
55
+ | Streaming | `pp.rpc(..., { onStream })` | `public/js/pp-reactive-v2.js`, `casp/rpc.py`, `casp/streaming.py` | server generators become SSE responses; an `abortPrevious` stream stays cancellable until its final chunk |
56
+ | SPA navigation | links after `pp.mount()`; `a[pp-spa="false"]` opts out | `public/js/pp-reactive-v2.js`, `main.py` | interception is enabled automatically without a body opt-in; same-origin eligible links intercept; root-layout mismatches hard reload |
57
57
  | Scroll restoration | `pp-reset-scroll="true"` | `public/js/pp-reactive-v2.js` | push navigation resets window; mark only content panes that should reset |
58
58
  | Tailwind merge | `{twMerge(...)}`, Python `merge_classes(...)` | `html_attrs.py`, `public/js/pp-reactive-v2.js` | Python emits frontend-ready expressions when Tailwind is enabled |
59
59
  | Markup deferral | `<template pp-component>` / `<template pp-owner>` wrappers | render pipeline (`main.py`), `public/js/pp-reactive-v2.js` (`materializeTemplateComponentBoundaries`) | top-level roots ship inside an inert `<template>`; runtime materializes them on mount and SPA navigation before scanning. Browser never validates `<template>` contents, so `{...}` is safe in any attribute/position (SVG geometry, `src`/`href`, form `value`/date, table/select text) — no per-tag workarounds. See [pulsepoint.md](./pulsepoint.md) "Component markup is deferred inside an inert `<template>`" |
@@ -93,9 +93,8 @@ Before writing a template, ask: *"Would this file be valid HTML if I deleted eve
93
93
  | `pp-ref="name"` / `pp-ref="{expr}"` | Native elements and `x-*` component tags | Imperative element access |
94
94
  | `pp-style="{cssText}"` | Any element | Dynamic inline style, as a **CSS string** |
95
95
  | `pp-spread="{...obj}"` | Any element | Spread an object into attributes |
96
- | `pp-ref-forward` | *Server-set* on component roots see note below | Ref forwarding through layout-neutral hosts |
97
- | `<token.provider value="{v}">` (lowercase) | Anywhere in markup | Context provider |
98
- | `pp-spa="true"` / `pp-spa="false"` | `<body>` enables SPA navigation; `pp-spa="false"` on an `<a>` opts that link out | SPA navigation interception |
96
+ | `<token.provider value="{v}">` (lowercase) | Anywhere in markup | Context provider |
97
+ | `pp-spa="false"` | An `<a>` that must use normal browser navigation | Opt one link out of the SPA interception enabled automatically by `pp.mount()` |
99
98
  | `pp-reset-scroll="true"` | A scroll container, or `<body>` | Reset that container's scroll on navigation |
100
99
  | `pp-scroll-key="stable-name"` | A scroll container | Stable scroll-restoration identity |
101
100
  | `pp-loading-content="true"` | The region swapped during navigation | Marks the navigation content region |
@@ -106,11 +105,11 @@ There is **no** `pp-if`, `pp-show`, `pp-else`, `pp-model`, `pp-bind`, `pp-class`
106
105
 
107
106
  ### Runtime-managed — never author these
108
107
 
109
- The render pipeline or the browser runtime writes these. Handwriting them corrupts instance tracking:
110
-
111
- `pp-component`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `pp-context-provider` (the generated tag), `pp-context-token`, `pp-context-value`, `pp-root-layout`, `pp-fragment-root`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-dynamic-script`, `pp-dynamic-meta`, `pp-dynamic-link`.
112
-
113
- `pp-ref-forward` appears in the author-facing table only because you will see it in rendered HTML and in composition components; the server component compiler sets it on a root whose own root is another `x-*` component. Do not write it yourself.
108
+ The render pipeline or the browser runtime writes these. Handwriting them corrupts instance tracking or reconciliation:
109
+
110
+ `pp-component`, `pp-owner`, `pp-event-owner`, `pp-ref-owner`, `pp-ref-forward`, `<pp-context-provider>` (the generated tag), `data-pp-context-token`, `data-pp-context-value`, `data-pp-fragment-root`, `data-pp-script-source`, `data-pp-ref`, `data-pp-input-value`, `data-pp-default-value`, `data-pp-checked-value`, `data-pp-default-checked`, `data-pp-select-value`, `pp-keep`, `pp-keep-run`, and `pp-keep-content`.
111
+
112
+ The server also emits `meta[name="pp-root-layout"]` for SPA compatibility checks. These names may appear in rendered or transient runtime HTML, but none belongs in authored route or component templates.
114
113
 
115
114
  ### Component-script hooks (the whole list)
116
115
 
@@ -444,18 +443,16 @@ Important:
444
443
  - `pp.props.children` contains the root's initial inner HTML before the owned script is removed from the render template.
445
444
  - Props are not auto-injected as standalone top-level template or handler variables. Read them through `pp.props` or explicitly destructure them in the component script.
446
445
 
447
- Bindings exported to the template:
448
-
449
- - Top-level function declarations.
450
- - Top-level identifier declarations such as `const title = "Pulse"`.
451
- - Top-level object destructuring identifiers such as `const { title, subtitle } = pp.props`.
452
- - Top-level array destructuring identifiers when the initializer is `pp.state(...)`, such as `const [count, setCount] = pp.state(0)`.
453
-
454
- Bindings that should not be assumed:
455
-
456
- - Generic top-level array destructuring is not auto-exported unless it comes from `pp.state(...)`.
457
- - Nested declarations inside functions, conditions, loops, and callbacks are not auto-exported.
458
- - There is no standalone `props` variable injected by the runtime. Use `pp.props`.
446
+ Bindings exported to the template:
447
+
448
+ - Top-level function declarations.
449
+ - Top-level identifier declarations such as `const title = "Pulse"`.
450
+ - Every identifier in a top-level destructuring pattern, regardless of initializer: arrays, objects, nested patterns, defaults, rest elements, and holes. This includes `const { title, subtitle } = pp.props`, `const [count, setCount] = pp.state(0)`, and tuples returned by any other hook or helper.
451
+
452
+ Bindings that should not be assumed:
453
+
454
+ - Nested declarations inside functions, conditions, loops, and callbacks are not auto-exported.
455
+ - There is no standalone `props` variable injected by the runtime. Use `pp.props`.
459
456
 
460
457
  Example:
461
458
 
@@ -1213,7 +1210,7 @@ The render pipeline wraps each top-level `pp-component` root in an inert `<templ
1213
1210
  - Plain `pp-ref` bindings are preserved across rerenders, including no-op rerenders that skip DOM diffing.
1214
1211
  - Ref callbacks may be called with `null` during cleanup.
1215
1212
  - The runtime generates `data-pp-ref` internally. Do not author it.
1216
- - Do not author `pp-event-owner`, `pp-owner`, or `pp-dynamic-*` attributes by hand.
1213
+ - Do not author `pp-event-owner`, `pp-owner`, or the other runtime-managed names listed above.
1217
1214
 
1218
1215
  Example:
1219
1216
 
@@ -1343,10 +1340,10 @@ Example:
1343
1340
  - 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(...)`.
1344
1341
  - 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.
1345
1342
 
1346
- ## SPA, loading, and navigation helpers
1347
-
1348
- - `body[pp-spa="true"]` enables client-side navigation interception.
1349
- - `a[pp-spa="false"]` disables interception for that link.
1343
+ ## SPA, loading, and navigation helpers
1344
+
1345
+ - `pp.mount()` enables client-side navigation interception automatically; no `<body pp-spa="true">` opt-in is required.
1346
+ - `a[pp-spa="false"]` disables interception for that link.
1350
1347
  - External links, downloads, `_blank`, and modifier-key clicks bypass SPA interception.
1351
1348
  - `pp-loading-content="true"` marks the page region that gets swapped or faded during navigation.
1352
1349
  - Route-specific loading states are looked up with `pp-loading-url`.
@@ -1362,18 +1359,22 @@ Example:
1362
1359
 
1363
1360
  RPC notes:
1364
1361
 
1365
- - `pp.rpc(name, data?, optionsOrAbort?)` posts to the current route.
1366
- - Passing `true` as the third argument means `abortPrevious: true`.
1367
- - The options object supports `abortPrevious`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, and `onUploadComplete`.
1368
- - File uploads switch to the XHR path when upload progress callbacks are needed.
1369
- - 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`.
1370
- - 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).
1371
- - 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.
1372
- - Redirect headers are honored through `pp.redirect()`.
1362
+ - `pp.rpc(name, data?, optionsOrAbort?)` posts to the current route.
1363
+ - Passing `true` as the third argument means `abortPrevious: true`.
1364
+ - The complete options object supports `abortPrevious`, `url`, `csrfUrl`, `credentials`, `onStream`, `onStreamError`, `onStreamComplete`, `onUploadProgress`, and `onUploadComplete`.
1365
+ - `url` overrides the current-route RPC URL. `csrfUrl` overrides the URL used for the preliminary CSRF-cookie bootstrap GET. `credentials` is a standard Fetch `RequestCredentials` value; same-origin calls default to `"same-origin"` and cross-origin overrides default to `"include"`.
1366
+ - When `abortPrevious` is enabled, a later aborting call cancels the active request and the cancelled promise resolves to `{ cancelled: true }`. A streamed response remains the active request until its final chunk, so it can still be cancelled after the response headers and early chunks have arrived.
1367
+ - File uploads switch to the XHR path when upload progress callbacks are needed.
1368
+ - A payload containing a `File` or non-empty `FileList` becomes multipart. The runtime writes every non-file field before the first file so a streaming server upload handler can read companion arguments even when the caller's object listed a file first. Objects are JSON-stringified, nullish fields are omitted, and a `FileList` becomes repeated parts under its original field name.
1369
+ - 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`.
1370
+ - `onUploadComplete` belongs to the same XHR progress path and runs only after a successful upload response (and any accepted redirect).
1371
+ - 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).
1372
+ - 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.
1373
+ - Same-origin `X-PP-Redirect` headers (and redirect-status `Location` headers) are honored through `pp.redirect()`. Invalid or cross-origin server redirect targets are ignored.
1373
1374
 
1374
1375
  Notes:
1375
1376
 
1376
- - `pp.redirect()` uses SPA navigation for same-origin URLs when SPA mode is enabled. Otherwise it falls back to normal navigation.
1377
+ - After the runtime mounts, `pp.redirect()` uses SPA navigation for same-origin URLs. Cross-origin targets use normal browser navigation.
1377
1378
  - Root-layout mismatches during SPA navigation trigger a hard reload.
1378
1379
  - `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.
1379
1380
 
@@ -1493,20 +1494,14 @@ JSX constructs in markup — these are the highest-frequency generation failure,
1493
1494
 
1494
1495
  Also avoid:
1495
1496
 
1496
- - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
1497
- - 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
1498
- - `pp-context`
1499
- - `pp-key`
1500
- - `data-pp-ref`
1501
- - `pp-context-provider`
1502
- - `pp-owner`
1503
- - `pp-event-owner`
1504
- - `pp-dynamic-script`
1505
- - `pp-dynamic-meta`
1506
- - `pp-dynamic-link`
1497
+ - React, Vue, Svelte, Alpine, HTMX, or JSX-first patterns as the default Caspian frontend approach
1498
+ - 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
1499
+ - `pp-context`
1500
+ - `pp-key`
1501
+ - any runtime-managed name listed in [Runtime-managed — never author these](#runtime-managed--never-author-these)
1507
1502
  - handwritten `pp-component="..."` in authored route, layout, or component templates
1508
- - `pp.fetchFunction()` as the current raw runtime helper name
1509
- - made-up hooks, directives, or globals not present in the current bundled runtime
1503
+ - `pp.fetchFunction()` as the current raw runtime helper name
1504
+ - made-up hooks, directives, or globals not present in the current bundled runtime
1510
1505
 
1511
1506
  ## Review notes
1512
1507
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "caspian-utils",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "Caspian tooling",
5
5
  "main": "index.js",
6
6
  "scripts": {