@abide/abide 0.38.1 → 0.40.0

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.
Files changed (80) hide show
  1. package/AGENTS.md +5 -5
  2. package/CHANGELOG.md +63 -0
  3. package/package.json +9 -1
  4. package/src/build.ts +14 -0
  5. package/src/lib/bundle/exitWithParent.ts +4 -2
  6. package/src/lib/server/rpc/parseArgs.ts +10 -1
  7. package/src/lib/server/rpc/readBodyWithinLimit.ts +3 -0
  8. package/src/lib/server/rpc/runWithVerbTimeout.ts +7 -9
  9. package/src/lib/server/rpc/types/VerbHelper.ts +18 -21
  10. package/src/lib/server/runtime/SSR_SWAP_SCRIPT.ts +8 -7
  11. package/src/lib/server/runtime/createAppAssetServer.ts +37 -11
  12. package/src/lib/server/runtime/createPublicAssetServer.ts +14 -5
  13. package/src/lib/server/runtime/createUiPageRenderer.ts +53 -42
  14. package/src/lib/server/runtime/internalErrorResponse.ts +5 -2
  15. package/src/lib/server/runtime/snapshotEntryFromCache.ts +4 -1
  16. package/src/lib/server/sockets/createSocketDispatcher.ts +19 -3
  17. package/src/lib/server/sockets/defineSocket.ts +3 -1
  18. package/src/lib/shared/REF_JSON_HEADER.ts +9 -0
  19. package/src/lib/shared/REF_JSON_TAGS.ts +31 -0
  20. package/src/lib/shared/buildRpcRequest.ts +8 -1
  21. package/src/lib/shared/createRemoteFunction.ts +20 -11
  22. package/src/lib/shared/decodeRefJson.ts +110 -0
  23. package/src/lib/shared/encodeRefJson.ts +106 -0
  24. package/src/lib/shared/escapeHtml.ts +15 -0
  25. package/src/lib/shared/markFrameworkSourcesIgnored.ts +47 -0
  26. package/src/lib/shared/streamResponse.ts +8 -1
  27. package/src/lib/shared/types/RemoteCallable.ts +12 -3
  28. package/src/lib/shared/types/RpcOptions.ts +22 -0
  29. package/src/lib/shared/types/SourceMap.ts +14 -0
  30. package/src/lib/test/createTestSocketChannel.ts +6 -2
  31. package/src/lib/ui/compile/SSR_ESCAPE.ts +13 -3
  32. package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +5 -0
  33. package/src/lib/ui/compile/compileModule.ts +5 -2
  34. package/src/lib/ui/compile/compileSSR.ts +32 -9
  35. package/src/lib/ui/compile/compileShadow.ts +25 -10
  36. package/src/lib/ui/compile/composeProps.ts +53 -0
  37. package/src/lib/ui/compile/desugarSignals.ts +45 -17
  38. package/src/lib/ui/compile/generateBuild.ts +87 -76
  39. package/src/lib/ui/compile/generateSSR.ts +217 -71
  40. package/src/lib/ui/compile/isWhitespaceText.ts +11 -0
  41. package/src/lib/ui/compile/lowerDocAccess.ts +53 -16
  42. package/src/lib/ui/compile/parseTemplate.ts +118 -1
  43. package/src/lib/ui/compile/renameSignalRefs.ts +12 -0
  44. package/src/lib/ui/compile/resolveBranches.ts +21 -0
  45. package/src/lib/ui/compile/spreadExcludedNames.ts +27 -0
  46. package/src/lib/ui/compile/staticAttr.ts +1 -1
  47. package/src/lib/ui/compile/staticTextPart.ts +1 -1
  48. package/src/lib/ui/compile/types/TemplateAttr.ts +4 -1
  49. package/src/lib/ui/compile/types/TemplateNode.ts +15 -3
  50. package/src/lib/ui/createScope.ts +14 -0
  51. package/src/lib/ui/dom/appendText.ts +2 -3
  52. package/src/lib/ui/dom/appendTextAt.ts +2 -3
  53. package/src/lib/ui/dom/applyResolved.ts +5 -8
  54. package/src/lib/ui/dom/awaitBlock.ts +14 -1
  55. package/src/lib/ui/dom/mergeProps.ts +32 -0
  56. package/src/lib/ui/dom/on.ts +7 -0
  57. package/src/lib/ui/dom/parseRawNodes.ts +17 -0
  58. package/src/lib/ui/dom/readCall.ts +27 -0
  59. package/src/lib/ui/dom/restProps.ts +32 -0
  60. package/src/lib/ui/dom/spreadAttrs.ts +34 -0
  61. package/src/lib/ui/dom/spreadProps.ts +32 -0
  62. package/src/lib/ui/installHotBridge.ts +10 -0
  63. package/src/lib/ui/navigate.ts +28 -3
  64. package/src/lib/ui/remoteProxy.ts +68 -36
  65. package/src/lib/ui/renderChain.ts +39 -37
  66. package/src/lib/ui/renderToStream.ts +84 -68
  67. package/src/lib/ui/resumeSeedScript.ts +27 -0
  68. package/src/lib/ui/router.ts +81 -51
  69. package/src/lib/ui/runtime/RESUME.ts +13 -6
  70. package/src/lib/ui/runtime/createEffectNode.ts +5 -0
  71. package/src/lib/ui/runtime/localStoragePersistence.ts +21 -8
  72. package/src/lib/ui/runtime/toTeardown.ts +10 -5
  73. package/src/lib/ui/runtime/types/RenderContext.ts +8 -0
  74. package/src/lib/ui/runtime/types/SsrRender.ts +16 -11
  75. package/src/lib/ui/runtime/types/UiComponent.ts +7 -1
  76. package/src/lib/ui/socketChannel.ts +5 -2
  77. package/src/lib/ui/tryEncodeResume.ts +20 -0
  78. package/src/lib/ui/types/Scope.ts +9 -3
  79. package/src/lib/ui/compile/escapeHtml.ts +0 -15
  80. /package/src/lib/{server/runtime → shared}/safeJsonForScript.ts +0 -0
package/AGENTS.md CHANGED
@@ -69,7 +69,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
69
69
 
70
70
  ### RPC verbs — @documentation rpc
71
71
 
72
- - `abide/server/GET`, `abide/server/POST`, `abide/server/PUT`, `abide/server/PATCH`, `abide/server/DELETE`, `abide/server/HEAD` — declare a verb: `VERB(handler, opts?)`; the preload rewrites each to `defineVerb(method, url, …)` (server) or `remoteProxy` (client). `opts`: `inputSchema`, `outputSchema`, `filesSchema`, `clients`, `crossOrigin`, `maxBodySize`, `timeout`. The returned function is callable in-process and exposes `.raw(args)` (the `Response`), `.stream(args)` (frame iterable), and `.fetch(request)` (router entry).
72
+ - `abide/server/GET`, `abide/server/POST`, `abide/server/PUT`, `abide/server/PATCH`, `abide/server/DELETE`, `abide/server/HEAD` — declare a verb: `VERB(handler, opts?)`; the preload rewrites each to `defineVerb(method, url, …)` (server) or `remoteProxy` (client). `opts`: `inputSchema`, `outputSchema`, `filesSchema`, `clients`, `crossOrigin`, `maxBodySize`, `timeout`. The returned function is callable in-process as `fn(args, callOpts?)` and exposes `.raw(args, callOpts?)` (the `Response`), `.stream(args)` (frame iterable), and `.fetch(request)` (router entry). `callOpts` is a curated `Pick` of `RequestInit` for per-call transport — `signal`, `keepalive`, `priority`, `cache`, `headers` — that the server handler never sees, so the call stays isomorphic: `signal` merges with the scope abort + client timeout (`AbortSignal.any`, ignored under `cache()`), and `headers` merge onto the framework headers with `traceparent`/offline/`content-type` winning.
73
73
  - `abide/server/rpc/defineVerb` — the rewrite target the verb helpers expand to: validates args against `inputSchema` (422 on issues), enforces `timeout` (504 + abort), and registers the verb for MCP/CLI/OpenAPI/inspector discovery.
74
74
 
75
75
  ### Responses — @documentation response
@@ -142,7 +142,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
142
142
 
143
143
  ### Reactive state — @documentation reactive-state
144
144
 
145
- - `abide/ui/scope` — `scope()`: the sole reactive surface. `.state(initial?, transform?)` (writable cell), `.computed(fn)` (read-only), `.linked(seed, transform?)` (local draft reseeded from upstream). Bare `state`/`computed`/`linked` are a compile error; a writable computed is expressed at the binding (`bind:value={{ get, set }}`). Also carries doc/persistence/undo/broadcast methods.
145
+ - `abide/ui/scope` — `scope()`: the sole reactive surface. `.state(initial?, transform?)` (writable cell), `.computed(fn)` (read-only), `.linked(seed, transform?)` (local draft reseeded from upstream). Bare `state`/`computed`/`linked` are a compile error; a writable computed is expressed at the binding (`bind:value={{ get, set }}`). `.share(key, value)`/`.shared(key)` pass context down the tree (closest-ancestor lookup; the reactive doc itself doesn't inherit, so share a `cell` for reactive context). Also carries doc/persistence/undo/broadcast methods.
146
146
 
147
147
  ### Effect — @documentation effect
148
148
 
@@ -151,7 +151,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
151
151
  ### Tail & navigate — @documentation tail · navigate
152
152
 
153
153
  - `abide/ui/tail` — `tail(subscribable)`: latest frame of a `Socket`/stream (reactive); window form `tail(s, { last })` → array; `tail.error`/`tail.status` probes.
154
- - `abide/ui/navigate` — `navigate(path, replace?)`: client navigation through the router.
154
+ - `abide/ui/navigate` — `navigate(path, options?)`: client navigation through the router. `options` is `{ replace?, keepScroll? }` — `replace` swaps the current history entry, `keepScroll` preserves the live scroll offset across an in-page URL swap.
155
155
 
156
156
  ### Outbox — @documentation ui
157
157
 
@@ -162,7 +162,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
162
162
  - `abide/ui/enterScope`, `abide/ui/exitScope` — push/pop the lexical scope around an SSR render.
163
163
  - `abide/ui/router`, `abide/ui/startClient`, `abide/ui/renderToStream` — the client router, the official client entry (reads `window.__SSR__`, seeds cache, starts the router), and the out-of-order SSR streamer.
164
164
  - `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the bundler-emitted client substitutes that swap a server verb/socket import for a `fetch` proxy / ws-multiplexed `Socket`.
165
- - `abide/ui/dom/*` — the compiled-template DOM runtime (one node-builder per construct): `mount`, `mountChild`, `mountSlot`, `outlet`, `hydrate`, `skeleton`, `cloneStatic`, `anchorCursor`, `text`, `appendText`, `appendTextAt`, `appendSnippet`, `appendStatic`, `attr`, `on`, `attach`, `each`, `eachAsync`, `when`, `awaitBlock`, `tryBlock`, `switchBlock`, `applyResolved`. `outlet` is the marker-range a layout `<slot/>` compiles to (no wrapper element — the router fills it with the next chain layer as a direct child).
165
+ - `abide/ui/dom/*` — the compiled-template DOM runtime (one node-builder per construct): `mount`, `mountChild`, `mergeProps`, `spreadProps`, `restProps`, `spreadAttrs`, `readCall`, `mountSlot`, `outlet`, `hydrate`, `skeleton`, `cloneStatic`, `anchorCursor`, `text`, `appendText`, `appendTextAt`, `appendSnippet`, `appendStatic`, `attr`, `on`, `attach`, `each`, `eachAsync`, `when`, `awaitBlock`, `tryBlock`, `switchBlock`, `applyResolved`. `outlet` is the marker-range a layout `<slot/>` compiles to (no wrapper element — the router fills it with the next chain layer as a direct child). `readCall` guards a method call lowered onto a reactive-doc read (`model.draft.trim()` → `readCall(model.read("draft"), …)`) so a nullish read throws naming the scope path and member, not the engine's opaque `undefined is not an object`. A `{...expr}` spreads onto a component (its props, via `mergeProps`/`spreadProps`) or a native element (its attributes, via `spreadAttrs` — each key reactive, an `on<event>` function becomes a listener); keys resolve last-wins in source order. A component reads the props it doesn't name with `const { foo, ...rest } = props()` — `rest` (a `restProps` bag of the unconsumed props) can itself be spread onward (`<input {...rest}/>`).
166
166
  - `abide/ui/runtime/*` — render-pass helpers: `escapeKey` (JSON-Pointer key escaping), `nextBlockId`, `enterRenderPass`, `exitRenderPass`.
167
167
 
168
168
  ## Build / tooling — @documentation building
@@ -171,7 +171,7 @@ Test suites compile `.abide` and rewrite verbs under `bun test` via `preload = [
171
171
  - `abide/compile` — `compile({ cwd?, target?, outfile?, buildClient? })`: produce a standalone Bun server executable (bytecode, embedded compressed assets).
172
172
  - `abide/preload` — the Bun preload (`bunfig.toml`) that installs the `.abide`, resolver, and CSS-noop plugins and rewrites verb/socket imports.
173
173
  - `abide/resolver-plugin` — `abideResolverPlugin({ cwd?, embedAssets?, target? })`: virtualizes every generated module and rewrites verbs/sockets per side.
174
- - `abide/ui-plugin` — `abideUiPlugin`: the Bun loader compiling `.abide` SFCs to ES modules (scoped `<style>` via virtual imports; `layout.abide` `<abide-outlet>`).
174
+ - `abide/ui-plugin` — `abideUiPlugin`: the Bun loader compiling `.abide` SFCs to ES modules (scoped `<style>` via virtual imports; a `layout.abide`'s `<slot/>` lowers to the marker-range `outlet` the router fills as a direct child, no wrapper element).
175
175
  - `abide/tsconfig` — `tsconfig.app.json` for consumers to extend (`bundler` resolution, `strict`, `noEmit`, `bun` types).
176
176
 
177
177
  ## Desktop bundle — @documentation bundle
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # abide
2
2
 
3
+ ## 0.40.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`05ea7e2`](https://github.com/briancray/abide/commit/05ea7e235b1e7c035d3b4313e12fc83733fa814b) - <template elseif> conditional chains ([`c3237cc`](https://github.com/briancray/abide/commit/c3237ccc0d222c36e8b00ce68595eb92609ef856))
8
+
9
+ - [`15c0ad3`](https://github.com/briancray/abide/commit/15c0ad334f698dfb49a9dcd0e0361925afc4f6c8) - `navigate` takes an options object — `navigate(path, { replace?, keepScroll? })` — replacing the old `navigate(path, replace?)` boolean. **Breaking:** a caller passing a boolean (`navigate(path, true)`) must migrate to `navigate(path, { replace: true })`; a bare `navigate(path)` is unchanged. The new `keepScroll` flag carries the live scroll offset onto the destination entry so an in-page URL swap — e.g. selecting another episode on the same detail page — restores to the current position instead of jumping to top. A push re-buckets the offset under the freshly minted entry id; a `replace` keeps the entry's bucket instead of discarding it.
10
+
11
+ - [`ba56bb2`](https://github.com/briancray/abide/commit/ba56bb28be06fa0a11ab57fb10fa2d6b75ab8656) - Add `share`/`shared` context to `scope()`. The reactive doc does not inherit down the scope tree (each scope owns a separate document), so passing a value from an ancestor to a descendant previously meant threading it through every layer as props. `scope().share(key, value)` now puts a named value on a scope's own side-map, and `scope().shared(key)` reads the closest ancestor (self included) that has the key — an existence-checked, non-tracking upward walk, returning `undefined` when no scope provides it. The value is held by reference and the lookup never subscribes, so reactive context is expressed by sharing a `cell` (or a scope) rather than a plain object — reactivity rides what you share, not the share itself. A shared `undefined` shadows an ancestor (the walk stops on `has`, not truthiness). The shared map is released on `dispose`.
12
+
13
+ ### Patch Changes
14
+
15
+ - [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - extract resolveBranches for branch lookup ([`19a28a1`](https://github.com/briancray/abide/commit/19a28a1236e38a78c53a2adff0a981c8ad20b698))
16
+
17
+ - [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - sync AGENTS.md surface map for scope share/shared and navigate options ([`3e41255`](https://github.com/briancray/abide/commit/3e4125516164c94cfe9c9e6baa35f9ff9ec9c3e0))
18
+
19
+ - [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - use Promise.withResolvers in runWithVerbTimeout ([`504bdca`](https://github.com/briancray/abide/commit/504bdcac285cfd3762c8390fd1b6d640e62029bc))
20
+
21
+ - [`a5a37f9`](https://github.com/briancray/abide/commit/a5a37f94258a56c4f24eb2b5539a280ce877bdfc) - extract VerbBaseOpts for shared verb options ([`d7414ca`](https://github.com/briancray/abide/commit/d7414cab7a557b54395321026d818b9fba82ba34))
22
+
23
+ - [`ccc7f87`](https://github.com/briancray/abide/commit/ccc7f879d0506af084335785026200aeb38fdde4) - `on()` no longer attaches a non-function event handler. A component that omits an optional `on*` prop forwards it as `undefined`, yet the compiler still emits the `on()` call — so the listener fired `undefined(event)` and threw "handler is not a function" on every matching event (a keystroke or input on a search box, a click, …). `on()` now skips attaching when the handler is not a function, so an omitted optional handler is a no-op rather than a per-event crash.
24
+
25
+ - [`c29dcd2`](https://github.com/briancray/abide/commit/c29dcd2796b5caa3ee41857aa612322b1e4220fa) - Parse `html`-branded raw markup in the parent's namespace. The client-side raw-HTML
26
+ binding parsed markup by setting `innerHTML` on a throwaway `<div>`, so the fragment
27
+ parser always used the HTML namespace — markup bound inside an `<svg>` produced
28
+ HTML-namespaced `<path>`/`<circle>` elements that exist in the DOM but never render.
29
+ This only surfaced on client-created or re-rendered nodes; the initial SSR/hydration
30
+ path adopts the server markup verbatim and kept the correct namespace, so an icon
31
+ component (`{html(svgInnerMarkup)}` inside `<svg>`) rendered on first paint but
32
+ vanished the moment its subtree was rebuilt on the client (e.g. a live cache delta).
33
+ A new `parseRawNodes` helper picks an SVG holder when the parent is SVG-namespaced,
34
+ shared by both `appendText` and `appendTextAt`.
35
+
36
+ - [`9a74da6`](https://github.com/briancray/abide/commit/9a74da67c1f65cdb37e7a47af2e7b104cfe7620f) - Stop the signal-ref rewrite from descending into type space. `renameSignalRefs` rewrote every value-position identifier matching a signal name, but never skipped TypeScript type aliases, interfaces, or type annotations — so a component that destructures a prop (`const { option, ...rest } = props<Props>()`) and names that prop in its own type (`type Props = { option?: (value: T) => unknown; … }`) had the type member rewritten into a call (`option(): …`). This only surfaced once a `...rest` binding forced the named prop type to be emitted into the build (a no-rest `props<{…}>()` strips its type argument); with simple property types the mangle landed as accidentally-valid method-signature syntax, but optional/function/union members (`option?: (v) => unknown`, `value?: T | T[] | string`) became invalid TS and failed the page build. The visitor now leaves type aliases, interfaces, and any type node untouched — type space never holds a runtime read, and types erase at build regardless.
37
+
38
+ - [`92597f0`](https://github.com/briancray/abide/commit/92597f0cbabc1e44449c0f0f812048481c38c579) - RPC bodies and socket frames now ship as ref-json (cycles, shared references, and
39
+ the types JSON drops/coerces — `undefined`, `bigint`, `Date`, `Map`, `Set`, `NaN`,
40
+ `±Infinity`, `-0` — survive the wire), and the server still accepts ordinary JSON from
41
+ non-abide clients. abide's own client flags a ref-json body with the `abide-ref-json`
42
+ header so the server decodes it with the matching codec; a request without the header
43
+ (curl, an OpenAPI-generated SDK, a webhook) is read with plain `JSON.parse`, so the
44
+ documented HTTP/OpenAPI body contract keeps working. The header is the discriminator
45
+ rather than the payload shape because ref-json's `[root, slots]` envelope is ambiguous
46
+ with a legitimate plain-JSON two-element array body. Socket frames need no header — a
47
+ frame is always an object, never the envelope array, so a plain-JSON frame falls back
48
+ to `JSON.parse` unambiguously.
49
+
50
+ ## 0.39.0
51
+
52
+ ### Minor Changes
53
+
54
+ - [`ae15b27`](https://github.com/briancray/abide/commit/ae15b2733c3c569b9bde9e4d79f9b768836b3a46) - async SSR render with inline nested-blocking awaits and spread/rest props ([`514a796`](https://github.com/briancray/abide/commit/514a796833c5be4caa296b295d63fda6c8452a36))
55
+
56
+ - [`527795d`](https://github.com/briancray/abide/commit/527795d1035f7cb169723a03571603a9ce8556e1) - Remote functions now accept an optional trailing options bag — `fn(args, opts)` — for per-call transport control: `signal`, `keepalive`, `priority`, `cache`, and `headers`. It's a curated `Pick` of `RequestInit`, not a raw passthrough: the server handler never observes these, so the call stays isomorphic, and a caller can't clobber the method, body, or framework headers the RPC contract owns. `opts.signal` merges with the scope abort and client timeout (`AbortSignal.any`) rather than replacing them, and is ignored under `cache()` so one reader can't abort a coalesced flight the others share. `opts.headers` merge onto abide's headers with the framework winning — a caller adds transport metadata (idempotency-key, authorization) but can't overwrite `traceparent`, the offline marker, or `content-type`.
57
+
58
+ ### Patch Changes
59
+
60
+ - [`ae15b27`](https://github.com/briancray/abide/commit/ae15b2733c3c569b9bde9e4d79f9b768836b3a46) - harden client router, asset serving, sockets, and runtime edge cases ([`4735920`](https://github.com/briancray/abide/commit/4735920edea942dcd2954c522673b0c8af5a9a7f))
61
+
62
+ - [`ae15b27`](https://github.com/briancray/abide/commit/ae15b2733c3c569b9bde9e4d79f9b768836b3a46) - consolidate escapeHtml and safeJsonForScript into shared/ ([`ae2d602`](https://github.com/briancray/abide/commit/ae2d6022f6dc23994164d1764bf5b812b136e383))
63
+
64
+ - [`76a4a19`](https://github.com/briancray/abide/commit/76a4a19048fd5373b0d3ed75f2c6de017e33c7a3) - Make reactive errors and stack traces read in authored terms. A method call lowered onto a reactive-doc read now routes through a guard (`readCall`) that throws naming the scope path and member (`cannot call .close() — scope value "modal" is undefined`) instead of the engine's opaque `undefined is not an object`. The client build's source maps ignore-list abide's own framework sources, so a debugger collapses the mount-stack wall (`scope`/`mountRange`/`runNode`/…) and shows only authored `.abide`/`.ts` frames. Reactive bindings emit named thunks (`attr_title`/`text`/`bind_value`) so those surviving frames carry a name instead of `(anonymous)`.
65
+
3
66
  ## 0.38.1
4
67
 
5
68
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.38.1",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Isomorphic multimodal HTTP framework built for humans and machines in a single Bun runtime",
@@ -79,6 +79,11 @@
79
79
  "./ui/outbox": "./src/lib/ui/outbox.ts",
80
80
  "./ui/dom/mount": "./src/lib/ui/dom/mount.ts",
81
81
  "./ui/dom/mountChild": "./src/lib/ui/dom/mountChild.ts",
82
+ "./ui/dom/mergeProps": "./src/lib/ui/dom/mergeProps.ts",
83
+ "./ui/dom/spreadProps": "./src/lib/ui/dom/spreadProps.ts",
84
+ "./ui/dom/restProps": "./src/lib/ui/dom/restProps.ts",
85
+ "./ui/dom/spreadAttrs": "./src/lib/ui/dom/spreadAttrs.ts",
86
+ "./ui/dom/readCall": "./src/lib/ui/dom/readCall.ts",
82
87
  "./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
83
88
  "./ui/dom/text": "./src/lib/ui/dom/text.ts",
84
89
  "./ui/dom/appendText": "./src/lib/ui/dom/appendText.ts",
@@ -127,6 +132,9 @@
127
132
  "./ui/socketProxy": "./src/lib/ui/socketProxy.ts",
128
133
  "./server/prompts/renderPromptTemplate": "./src/lib/server/prompts/renderPromptTemplate.ts"
129
134
  },
135
+ "scripts": {
136
+ "test": "bun test ./tests"
137
+ },
130
138
  "bin": {
131
139
  "abide": "./bin/abide.ts"
132
140
  },
package/src/build.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { clientBuildPlugins } from './clientBuildPlugins.ts'
2
2
  import { abideLog } from './lib/shared/abideLog.ts'
3
3
  import { exitOnBuildFailure } from './lib/shared/exitOnBuildFailure.ts'
4
+ import { markFrameworkSourcesIgnored } from './lib/shared/markFrameworkSourcesIgnored.ts'
4
5
 
5
6
  const CLIENT_ENTRY = new URL('./clientEntry.ts', import.meta.url).pathname
6
7
 
@@ -97,6 +98,19 @@ export async function build({
97
98
  return false
98
99
  }
99
100
 
101
+ /* Ignore-list abide's own framework sources in every emitted map, so a debugger
102
+ collapses the mount-stack wall (scope/mountRange/runNode/…) and a stack trace
103
+ shows only authored `.abide`/`.ts` frames. Runs before the gzip step so the
104
+ `.gz` siblings compress the updated maps. */
105
+ await Promise.all(
106
+ result.outputs
107
+ .filter((output) => output.kind === 'sourcemap')
108
+ .map(async (output) => {
109
+ const map = await Bun.file(output.path).json()
110
+ await Bun.write(output.path, JSON.stringify(markFrameworkSourcesIgnored(map)))
111
+ }),
112
+ )
113
+
100
114
  // Dev skips the gzip siblings (paths still point into stagingDir here).
101
115
  const compressedBytes = compress
102
116
  ? (
@@ -10,10 +10,12 @@ the var is absent (standalone `abide start`).
10
10
  */
11
11
  export function exitWithParent(): void {
12
12
  const parent = process.env.ABIDE_PARENT_PID
13
- if (!parent) {
13
+ const parentPid = Number(parent)
14
+ /* A non-numeric value is truthy but coerces to NaN; without the integer guard
15
+ process.kill(NaN, 0) throws on the first tick and exits a healthy server. */
16
+ if (!parent || !Number.isInteger(parentPid)) {
14
17
  return
15
18
  }
16
- const parentPid = Number(parent)
17
19
  const timer = setInterval(() => {
18
20
  try {
19
21
  // Signal 0 sends nothing — it only probes existence, throwing when the
@@ -1,6 +1,8 @@
1
1
  import { carriesBodyArgs } from '../../shared/carriesBodyArgs.ts'
2
2
  import { contentTypeOf } from '../../shared/contentTypeOf.ts'
3
+ import { decodeRefJson } from '../../shared/decodeRefJson.ts'
3
4
  import { HttpError } from '../../shared/HttpError.ts'
5
+ import { REF_JSON_HEADER } from '../../shared/REF_JSON_HEADER.ts'
4
6
  import type { HttpVerb } from '../../shared/types/HttpVerb.ts'
5
7
  import { error } from '../error.ts'
6
8
  import { requestContext } from '../runtime/requestContext.ts'
@@ -92,7 +94,14 @@ export async function parseArgs(
92
94
  if (contentType.includes('application/json')) {
93
95
  const text = await bounded.text()
94
96
  if (text !== '') {
95
- body = JSON.parse(text)
97
+ /* abide's own client flags ref-json with REF_JSON_HEADER (restores
98
+ cycles/shared refs JSON can't carry); a non-abide client (curl, an
99
+ OpenAPI SDK) omits it and sends ordinary JSON — read with plain
100
+ JSON.parse, since the ref-json envelope is ambiguous with a 2-element
101
+ array body. */
102
+ body = bounded.headers.has(REF_JSON_HEADER)
103
+ ? decodeRefJson(text)
104
+ : JSON.parse(text)
96
105
  }
97
106
  } else if (
98
107
  contentType.includes('application/x-www-form-urlencoded') ||
@@ -40,5 +40,8 @@ export async function readBodyWithinLimit(request: Request, maxBytes: number): P
40
40
  method: request.method,
41
41
  headers: request.headers,
42
42
  body: buffered,
43
+ /* Carry the inbound signal forward so a handler reading `request().signal` still
44
+ aborts on client disconnect — a fresh Request would otherwise drop it. */
45
+ signal: request.signal,
43
46
  })
44
47
  }
@@ -19,18 +19,16 @@ export function runWithVerbTimeout(
19
19
  ms: number,
20
20
  onTimeout: () => void,
21
21
  ): Promise<Response> {
22
- let timer: ReturnType<typeof setTimeout> | undefined
23
22
  let timedOut = false
24
- const deadline = new Promise<Response>((resolve) => {
25
- timer = setTimeout(() => {
26
- timedOut = true
27
- onTimeout()
28
- resolve(error(504, 'handler timeout'))
29
- }, ms)
30
- })
23
+ const deadline = Promise.withResolvers<Response>()
24
+ const timer = setTimeout(() => {
25
+ timedOut = true
26
+ onTimeout()
27
+ deadline.resolve(error(504, 'handler timeout'))
28
+ }, ms)
31
29
  return (async () => {
32
30
  try {
33
- return await Promise.race([work, deadline])
31
+ return await Promise.race([work, deadline.promise])
34
32
  } finally {
35
33
  clearTimeout(timer)
36
34
  if (timedOut) {
@@ -3,6 +3,21 @@ import type { RemoteFunction } from '../../../shared/types/RemoteFunction.ts'
3
3
  import type { StandardSchemaV1 } from '../../../shared/types/StandardSchemaV1.ts'
4
4
  import type { RemoteHandler } from './RemoteHandler.ts'
5
5
 
6
+ /*
7
+ Options every verb overload accepts: the OpenAPI 200 `outputSchema`, the
8
+ `clients` surface flags, the same-origin CSRF exemption (`crossOrigin`), the
9
+ pre-parse body-byte ceiling (`maxBodySize`), and the per-surface handler
10
+ `timeout` (ms). The schema-bearing overloads intersect this with their own
11
+ `inputSchema`/`filesSchema` members.
12
+ */
13
+ type VerbBaseOpts = {
14
+ outputSchema?: StandardSchemaV1
15
+ clients?: Partial<ClientFlags>
16
+ crossOrigin?: boolean
17
+ maxBodySize?: number
18
+ timeout?: number
19
+ }
20
+
6
21
  /*
7
22
  Shared signature for every verb helper (GET / POST / …). Three overloads:
8
23
 
@@ -50,36 +65,18 @@ export type VerbHelper = {
50
65
  StandardSchemaV1.InferOutput<InputSchema> & StandardSchemaV1.InferOutput<FilesSchema>,
51
66
  Return
52
67
  >,
53
- opts: {
68
+ opts: VerbBaseOpts & {
54
69
  inputSchema: InputSchema
55
70
  filesSchema: FilesSchema
56
- outputSchema?: StandardSchemaV1
57
- clients?: Partial<ClientFlags>
58
- crossOrigin?: boolean
59
- maxBodySize?: number
60
- timeout?: number
61
71
  },
62
72
  ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
63
73
  <Return = unknown, InputSchema extends StandardSchemaV1 = StandardSchemaV1>(
64
74
  fn: RemoteHandler<StandardSchemaV1.InferOutput<InputSchema>, Return>,
65
- opts: {
66
- inputSchema: InputSchema
67
- outputSchema?: StandardSchemaV1
68
- clients?: Partial<ClientFlags>
69
- crossOrigin?: boolean
70
- maxBodySize?: number
71
- timeout?: number
72
- },
75
+ opts: VerbBaseOpts & { inputSchema: InputSchema },
73
76
  ): RemoteFunction<StandardSchemaV1.InferInput<InputSchema>, Return>
74
77
  <Args = undefined, Return = unknown>(
75
78
  fn: RemoteHandler<Args, Return>,
76
- opts: {
77
- outputSchema?: StandardSchemaV1
78
- clients?: Partial<ClientFlags>
79
- crossOrigin?: boolean
80
- maxBodySize?: number
81
- timeout?: number
82
- },
79
+ opts: VerbBaseOpts,
83
80
  ): RemoteFunction<Args, Return>
84
81
  <Args = undefined, Return = unknown>(
85
82
  fn: RemoteHandler<Args, Return>,
@@ -1,17 +1,18 @@
1
1
  /*
2
2
  The tiny inline script the abide-ui SSR stream ships in <head>. For each streamed
3
3
  `<abide-resolve data-id>` frame it reads the leading `<script type=application/json>`,
4
- registers the resolved value into
5
- `window.__abideResume` (the resume manifest hydration reads) and swaps the resolved
6
- markup into the matching `<!--abide:await:ID-->…<!--/abide:await:ID-->` boundary —
7
- so the pending shell paints instantly and each value lands as it arrives, before
8
- the client bundle even loads. Vanilla and self-contained (no framework runtime),
9
- minified to one line so it inlines cheaply ahead of the document body.
4
+ stores its raw ref-json text into `window.__abideResume` (the resume manifest hydration
5
+ reads, decoding each entry at read time) and swaps the resolved markup into the matching
6
+ `<!--abide:await:ID-->…<!--/abide:await:ID-->` boundary — so the pending shell paints
7
+ instantly and each value lands as it arrives, before the client bundle even loads. It
8
+ stores the text undecoded precisely because it's vanilla and self-contained no framework
9
+ runtime, hence no ref-json decoder; the bundle decodes on read. Minified to one line so it
10
+ inlines cheaply ahead of the document body.
10
11
  */
11
12
  export const SSR_SWAP_SCRIPT =
12
13
  "function __abideSwap(){var f=document.querySelector('abide-resolve');while(f){" +
13
14
  "var id=f.getAttribute('data-id'),p=f.firstChild,w=document.createTreeWalker(document.body,NodeFilter.SHOW_COMMENT),o=null,c;" +
14
- "if(p&&p.nodeName==='SCRIPT'){try{(window.__abideResume=window.__abideResume||{})[id]=JSON.parse(p.textContent||'null');}catch(e){}p.remove();}" +
15
+ "if(p&&p.nodeName==='SCRIPT'){(window.__abideResume=window.__abideResume||{})[id]=p.textContent||'';p.remove();}" +
15
16
  "while((c=w.nextNode())){if(c.data==='abide:await:'+id){o=c;break;}}" +
16
17
  "if(o){var n=o.nextSibling;while(n&&!(n.nodeType===8&&n.data==='/abide:await:'+id)){var x=n.nextSibling;n.remove();n=x;}" +
17
18
  "while(f.firstChild){o.parentNode.insertBefore(f.firstChild,n);}}f.remove();f=document.querySelector('abide-resolve');}}"
@@ -34,13 +34,19 @@ export async function createAppAssetServer({
34
34
  }): Promise<(req: Request, url: URL) => Promise<Response>> {
35
35
  // Per-pathname asset header bundles, hashed-chunk-aware Cache-Control.
36
36
  const headersForAsset = createAssetHeaderCache(cacheControlForAsset)
37
- const diskGzipPaths = assets
37
+ /* Boot snapshot of every disk `_app` path, mirroring createPublicAssetServer: the
38
+ header cache keys on (request-controlled) pathnames, so building bundles for junk
39
+ `/_app/*` probes would grow it without bound. A rebuild restarts the server (same
40
+ restart `bun --watch` triggers on a code change), re-snapshotting. */
41
+ const diskPaths = assets
38
42
  ? new Set<string>()
39
- : await globToPathSet(
40
- `${distDir}/_app`,
41
- '**/*.gz',
42
- (file) => `/_app/${file.replace(/\.gz$/, '')}`,
43
- )
43
+ : await globToPathSet(`${distDir}/_app`, '**/*', (file) => `/_app/${file}`)
44
+ /* Derive the precompressed `.gz` sibling set from the single tree scan (it already
45
+ includes the `.gz` files) — a gzip-capable client gets those bytes without
46
+ on-the-fly compression. Keyed by the base path (the `.gz` suffix stripped). */
47
+ const diskGzipPaths = new Set(
48
+ [...diskPaths].filter((path) => path.endsWith('.gz')).map((path) => path.slice(0, -3)),
49
+ )
44
50
 
45
51
  return async function serveAppAsset(req, url) {
46
52
  if (containsTraversal(req.url)) {
@@ -49,8 +55,20 @@ export async function createAppAssetServer({
49
55
  headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
50
56
  })
51
57
  }
58
+ /* Embed map and gzip Set hold decoded filesystem names; url.pathname stays
59
+ percent-encoded — decode so a chunk/asset with a non-ASCII name matches and
60
+ Bun.file opens the real path, not a literal `%xx` name. */
61
+ let assetPath: string
62
+ try {
63
+ assetPath = decodeURIComponent(url.pathname)
64
+ } catch {
65
+ return new Response('Not Found', {
66
+ status: 404,
67
+ headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
68
+ })
69
+ }
52
70
  if (assets) {
53
- const compressed = assets[url.pathname]
71
+ const compressed = assets[assetPath]
54
72
  /* Miss-check before header work: the header cache keys on
55
73
  (request-controlled) pathnames, so building bundles for junk
56
74
  `/_app/*` probes would grow it without bound. */
@@ -63,12 +81,20 @@ export async function createAppAssetServer({
63
81
  return respondWithEmbeddedAsset(
64
82
  compressed,
65
83
  acceptsGzip(req),
66
- headersForAsset(url.pathname),
84
+ headersForAsset(assetPath),
67
85
  )
68
86
  }
69
- const { base: baseHeaders, gzip: gzipHeaders } = headersForAsset(url.pathname)
70
- const diskPath = distDir + url.pathname
71
- if (acceptsGzip(req) && diskGzipPaths.has(url.pathname)) {
87
+ /* Miss-check before header work so probes for non-existent chunks can't grow the
88
+ header cache (the embed branch above guards the same way). */
89
+ if (!diskPaths.has(assetPath)) {
90
+ return new Response('Not Found', {
91
+ status: 404,
92
+ headers: { 'Content-Type': TEXT_PLAIN, 'Cache-Control': NO_STORE },
93
+ })
94
+ }
95
+ const { base: baseHeaders, gzip: gzipHeaders } = headersForAsset(assetPath)
96
+ const diskPath = distDir + assetPath
97
+ if (acceptsGzip(req) && diskGzipPaths.has(assetPath)) {
72
98
  return new Response(Bun.file(`${diskPath}.gz`), { headers: gzipHeaders })
73
99
  }
74
100
  return new Response(Bun.file(diskPath), { headers: baseHeaders })
@@ -50,18 +50,27 @@ export async function createPublicAssetServer({
50
50
  if (containsTraversal(req.url)) {
51
51
  return undefined
52
52
  }
53
+ /* Both the embed map and the disk Set hold decoded filesystem names, but
54
+ url.pathname stays percent-encoded — decode it so a file whose name has a
55
+ space or non-ASCII char matches. A malformed escape can't name a real file. */
56
+ let assetPath: string
57
+ try {
58
+ assetPath = decodeURIComponent(url.pathname)
59
+ } catch {
60
+ return undefined
61
+ }
53
62
  if (publicAssets) {
54
- const compressed = publicAssets[url.pathname]
63
+ const compressed = publicAssets[assetPath]
55
64
  if (!compressed) {
56
65
  return undefined
57
66
  }
58
- return respondWithEmbeddedAsset(compressed, acceptsGzip(req), headersFor(url.pathname))
67
+ return respondWithEmbeddedAsset(compressed, acceptsGzip(req), headersFor(assetPath))
59
68
  }
60
- if (!diskPaths.has(url.pathname)) {
69
+ if (!diskPaths.has(assetPath)) {
61
70
  return undefined
62
71
  }
63
- return new Response(Bun.file(publicDir + url.pathname), {
64
- headers: headersFor(url.pathname).base,
72
+ return new Response(Bun.file(publicDir + assetPath), {
73
+ headers: headersFor(assetPath).base,
65
74
  })
66
75
  }
67
76
  }
@@ -3,16 +3,17 @@ import { SSR_CACHE_CONTROL } from '../../shared/CACHE_CONTROL_VALUES.ts'
3
3
  import { formatTraceparent } from '../../shared/formatTraceparent.ts'
4
4
  import { isReplayableMethod } from '../../shared/isReplayableMethod.ts'
5
5
  import { layoutChainForRoute } from '../../shared/layoutChainForRoute.ts'
6
+ import { safeJsonForScript } from '../../shared/safeJsonForScript.ts'
6
7
  import type { CacheEntry } from '../../shared/types/CacheEntry.ts'
7
8
  import type { CacheSnapshotEntry } from '../../shared/types/CacheSnapshotEntry.ts'
8
9
  import type { StreamedResolution } from '../../shared/types/StreamedResolution.ts'
9
10
  import { renderChain } from '../../ui/renderChain.ts'
10
11
  import { renderToStream } from '../../ui/renderToStream.ts'
12
+ import { resumeSeedScript } from '../../ui/resumeSeedScript.ts'
11
13
  import type { UiComponent } from '../../ui/runtime/types/UiComponent.ts'
12
14
  import { pageUrlFromStore } from './pageUrlFromStore.ts'
13
15
  import { SSR_SWAP_SCRIPT } from './SSR_SWAP_SCRIPT.ts'
14
16
  import { STREAMED_HTML_HEADER } from './STREAMED_HTML_HEADER.ts'
15
- import { safeJsonForScript } from './safeJsonForScript.ts'
16
17
  import { serializeCacheSnapshot } from './serializeCacheSnapshot.ts'
17
18
  import { streamCacheResolutions } from './streamCacheResolutions.ts'
18
19
  import type { RequestStore } from './types/RequestStore.ts'
@@ -33,11 +34,11 @@ function wantsJson(request: Request): boolean {
33
34
  const CACHE_RESOLVE_SCRIPT =
34
35
  'window.__abideResolve=function(r){(window.__abideResumeCache=window.__abideResumeCache||[]).push(r)}'
35
36
 
36
- /* One streamed cache resolution as an inline `__abideResolve(...)` call. `<` is escaped
37
- (as in encodeResume) so a body value can't close the script early. */
37
+ /* One streamed cache resolution as an inline `__abideResolve(...)` call. Encoded via
38
+ safeJsonForScript so `<`, `-->`, and U+2028/U+2029 can't close the script early or
39
+ parse as line terminators — the same escaping the page's other inline scripts use. */
38
40
  function resolveChunk(resolution: StreamedResolution): string {
39
- const payload = JSON.stringify(resolution).replace(/</g, '\\u003c')
40
- return `<script>__abideResolve(${payload})</script>`
41
+ return `<script>__abideResolve(${safeJsonForScript(resolution)})</script>`
41
42
  }
42
43
 
43
44
  /*
@@ -168,19 +169,21 @@ export function createUiPageRenderer({
168
169
  ...chainKeys.map((key) => layouts[key]?.().then((module) => module.default)),
169
170
  loadPage().then((module) => module.default),
170
171
  ])
171
- const ssr = renderChain(
172
+ const ssr = await renderChain(
172
173
  views.filter((view): view is UiComponent => view !== undefined),
173
174
  params,
174
175
  )
175
176
 
176
- /* Snapshot the cache settled by render-return — top-level `await` reads (render
177
- blocked on them) — into __SSR__. A `{#await}` read is NOT here: its expression
178
- is a thunk `renderToStream` runs lazily, so its entry is created mid-stream and
179
- seeded after the drain (see below). */
177
+ /* Snapshot the cache settled by render-return — top-level `await` reads and blocking
178
+ `{#await then}` reads (the async render awaited them inline) — into __SSR__. A
179
+ STREAMING `{#await}` read is NOT here: its expression is a thunk `renderToStream`
180
+ runs lazily, so its entry is created mid-stream and seeded after the drain (below). */
180
181
  const inline = await serializeCacheSnapshot(store.cache)
181
182
  const inlinedKeys = new Set(inline.map((entry) => entry.key))
182
183
 
183
- /* No await blocks → render synchronously, ship buffered. */
184
+ /* No STREAMING await blocks → ship buffered. Blocking `{#await … then}` blocks
185
+ rendered inline (their markup is already in `ssr.html`); seed their resolved
186
+ values so hydration adopts them without a refetch. */
184
187
  if (ssr.awaits.length === 0) {
185
188
  const html = injectRoutePreloads(
186
189
  shell.replace(SSR_MARKER, (_match, key: string) =>
@@ -190,7 +193,7 @@ export function createUiPageRenderer({
190
193
  )
191
194
  const withState = html.replace(
192
195
  '</body>',
193
- `${await stateTag(routeUrl, params, store, inline)}</body>`,
196
+ `${resumeSeedScript(ssr.resume)}${await stateTag(routeUrl, params, store, inline)}</body>`,
194
197
  )
195
198
  return new Response(withState, {
196
199
  headers: {
@@ -219,38 +222,46 @@ export function createUiPageRenderer({
219
222
  return new Response(
220
223
  new ReadableStream({
221
224
  async start(controller) {
222
- controller.enqueue(encoder.encode(before ?? ''))
223
- let first = true
224
- for await (const chunk of renderToStream(() => ssr)) {
225
- controller.enqueue(
226
- encoder.encode(
227
- first ? chunk : `${chunk}<script>__abideSwap()</script>`,
228
- ),
225
+ /* The shell `before` already flushed, so a mid-stream render rejection
226
+ (a streaming `{#await}` with no `:catch`) can't become a 500 — surface it
227
+ on the stream (`controller.error`) so the response terminates legibly
228
+ instead of leaking an unhandledrejection (process-fatal under Bun). */
229
+ try {
230
+ controller.enqueue(encoder.encode(before ?? ''))
231
+ let first = true
232
+ for await (const chunk of renderToStream(() => ssr)) {
233
+ controller.enqueue(
234
+ encoder.encode(
235
+ first ? chunk : `${chunk}<script>__abideSwap()</script>`,
236
+ ),
237
+ )
238
+ first = false
239
+ }
240
+ /* The {#await} reads created (and settled) their cache entries DURING the
241
+ stream — the await expression is a thunk `renderToStream` ran lazily —
242
+ so they missed the render-return snapshot. Drain them now: each lands an
243
+ inline `__abideResolve(...)` chunk (a warm snapshot, or a `{ key, miss }`
244
+ marker for an unshippable body → live refetch) before the deferred
245
+ bundle, so startClient seeds the store and the block's subscription read
246
+ is warm. Skip keys already shipped inline in __SSR__. */
247
+ const streamedEntries = Array.from(store.cache.entries.values()).filter(
248
+ (entry: CacheEntry) =>
249
+ entry.settled === true &&
250
+ entry.request !== undefined &&
251
+ isReplayableMethod(entry.request.method.toUpperCase()) &&
252
+ !inlinedKeys.has(entry.key),
229
253
  )
230
- first = false
254
+ for await (const resolution of streamCacheResolutions(
255
+ store.cache,
256
+ streamedEntries,
257
+ )) {
258
+ controller.enqueue(encoder.encode(resolveChunk(resolution)))
259
+ }
260
+ controller.enqueue(encoder.encode(after ?? ''))
261
+ controller.close()
262
+ } catch (streamError) {
263
+ controller.error(streamError)
231
264
  }
232
- /* The {#await} reads created (and settled) their cache entries DURING the
233
- stream — the await expression is a thunk `renderToStream` ran lazily —
234
- so they missed the render-return snapshot. Drain them now: each lands an
235
- inline `__abideResolve(...)` chunk (a warm snapshot, or a `{ key, miss }`
236
- marker for an unshippable body → live refetch) before the deferred
237
- bundle, so startClient seeds the store and the block's subscription read
238
- is warm. Skip keys already shipped inline in __SSR__. */
239
- const streamedEntries = Array.from(store.cache.entries.values()).filter(
240
- (entry: CacheEntry) =>
241
- entry.settled === true &&
242
- entry.request !== undefined &&
243
- isReplayableMethod(entry.request.method.toUpperCase()) &&
244
- !inlinedKeys.has(entry.key),
245
- )
246
- for await (const resolution of streamCacheResolutions(
247
- store.cache,
248
- streamedEntries,
249
- )) {
250
- controller.enqueue(encoder.encode(resolveChunk(resolution)))
251
- }
252
- controller.enqueue(encoder.encode(after ?? ''))
253
- controller.close()
254
265
  },
255
266
  }),
256
267
  {