@abide/abide 0.32.1 → 0.33.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.
- package/AGENTS.md +3 -3
- package/CHANGELOG.md +93 -63
- package/package.json +6 -2
- package/src/lib/server/runtime/buildCacheSnapshot.ts +5 -4
- package/src/lib/server/runtime/types/InspectorCacheEntry.ts +1 -1
- package/src/lib/shared/cache.ts +43 -29
- package/src/lib/shared/types/CacheEntry.ts +12 -12
- package/src/lib/shared/types/CacheOptions.ts +17 -13
- package/src/lib/ui/README.md +3 -3
- package/src/lib/ui/compile/HTML_TAGS.ts +132 -0
- package/src/lib/ui/compile/UI_RUNTIME_IMPORTS.ts +4 -1
- package/src/lib/ui/compile/componentWrapperTag.ts +13 -10
- package/src/lib/ui/compile/generateBuild.ts +265 -121
- package/src/lib/ui/compile/generateSSR.ts +78 -37
- package/src/lib/ui/compile/parseTemplate.ts +52 -0
- package/src/lib/ui/compile/skeletonable.ts +80 -0
- package/src/lib/ui/dom/MATHML_NAMESPACE.ts +6 -0
- package/src/lib/ui/dom/SVG_NAMESPACE.ts +7 -0
- package/src/lib/ui/dom/anchorCursor.ts +24 -0
- package/src/lib/ui/dom/appendSnippet.ts +1 -1
- package/src/lib/ui/dom/appendTextAt.ts +70 -0
- package/src/lib/ui/dom/awaitBlock.ts +27 -7
- package/src/lib/ui/dom/cloneStatic.ts +15 -24
- package/src/lib/ui/dom/each.ts +44 -25
- package/src/lib/ui/dom/eachAsync.ts +6 -2
- package/src/lib/ui/dom/effectiveChildNamespace.ts +13 -0
- package/src/lib/ui/dom/enterNamespace.ts +20 -0
- package/src/lib/ui/dom/fillBefore.ts +20 -3
- package/src/lib/ui/dom/foreignWrapperTag.ts +22 -0
- package/src/lib/ui/dom/hydrate.ts +1 -1
- package/src/lib/ui/dom/inheritedNamespace.ts +19 -0
- package/src/lib/ui/dom/mountSlot.ts +32 -0
- package/src/lib/ui/dom/openMarker.ts +4 -2
- package/src/lib/ui/dom/skeleton.ts +202 -0
- package/src/lib/ui/dom/switchBlock.ts +10 -3
- package/src/lib/ui/dom/templateFor.ts +28 -0
- package/src/lib/ui/dom/tryBlock.ts +7 -5
- package/src/lib/ui/dom/types/SkeletonHoles.ts +8 -0
- package/src/lib/ui/dom/when.ts +6 -2
- package/src/lib/ui/installHotBridge.ts +8 -2
- package/src/lib/ui/runtime/HOLE_ATTRIBUTE.ts +9 -0
- package/src/lib/ui/runtime/RENDER.ts +7 -0
- package/src/lib/ui/runtime/createDoc.ts +11 -8
- package/src/lib/ui/runtime/types/PathWalk.ts +10 -0
- package/src/lib/ui/runtime/types/UiProps.ts +3 -4
- package/src/lib/ui/runtime/walkPath.ts +27 -0
- package/template/src/ui/pages/about/page.abide +4 -6
- package/template/src/ui/pages/layout.abide +21 -0
- package/template/src/ui/pages/page.abide +5 -8
- package/src/lib/ui/compile/partitionSlots.ts +0 -36
- package/src/lib/ui/dom/openChild.ts +0 -22
- package/src/lib/ui/runtime/pathExists.ts +0 -23
- package/src/lib/ui/runtime/valueAtPath.ts +0 -18
- package/template/src/ui/Layout.abide +0 -19
package/AGENTS.md
CHANGED
|
@@ -134,7 +134,7 @@ Resolve only during an in-flight SSR render or RPC handler (throw outside one):
|
|
|
134
134
|
## Isomorphic surface — `abide/shared/*`
|
|
135
135
|
|
|
136
136
|
### Cache — `@readme cache`
|
|
137
|
-
- `abide/shared/cache(fnOrProducer)` → a memoised callable. Key auto-derives (method+url+args for a remote fn; producer-reference+args for a producer). Options `{ ttl?, scope?, global?,
|
|
137
|
+
- `abide/shared/cache(fnOrProducer)` → a memoised callable. Key auto-derives (method+url+args for a remote fn; producer-reference+args for a producer). Options `{ ttl?, scope?, global?, swr? }`: `ttl` ms-past-resolve (omit = forever, `0` = dedupe-only / the **mutation idiom**); `scope` tags for group invalidation; `global` = process-level store (server); `swr` = stale-while-revalidate — on invalidate, keep the stale value (reader sees `refreshing()`, not `pending()`) and refetch in the background. `swr: true` refetches immediately; `swr: { throttle | debounce }` adds a coalescing window. GET-only, throws on a write.
|
|
138
138
|
- `cache.invalidate(selector?, args?)` — end retention early (by fn, fn+args, `{ scope }`, or all).
|
|
139
139
|
- `cache.patch(selector, updater, args?)` — optimistic local fold over matching entries.
|
|
140
140
|
- `cache.on(subscribable, (frame, ctx) => …)` — event-driven cache maintenance: run a handler per socket/stream frame; `ctx.invalidate` / `ctx.patch` are coverage-tracked and resync on reconnect. Client-only (no-op on server).
|
|
@@ -174,13 +174,13 @@ Same selector grammar as `cache.invalidate`; also accept a `Subscribable`. See C
|
|
|
174
174
|
- `abide/ui/router(...)`, `abide/ui/startClient(...)`, `abide/ui/renderToStream(render)` — bootstrap/render runtime (compiler/launcher uses these).
|
|
175
175
|
|
|
176
176
|
### DOM + render runtime — `@readme plumbing` (compiler-emitted; you don't hand-write these)
|
|
177
|
-
`abide/ui/dom/{mount,mountChild,hydrate,text,
|
|
177
|
+
`abide/ui/dom/{mount,mountChild,hydrate,text,appendText,appendTextAt,appendSnippet,appendStatic,cloneStatic,skeleton,anchorCursor,mountSlot,attr,on,attach,each,eachAsync,when,awaitBlock,tryBlock,switchBlock,applyResolved}` and `abide/ui/runtime/{nextBlockId,enterRenderPass,exitRenderPass}`. These are what `analyzeComponent → generateBuild/generateSSR` lower a `.abide` file into — every bound element builds through the parser-backed `skeleton` (one clone + located holes / `<!--a-->` anchors); there is no imperative element builder. Read them only to understand compiler output.
|
|
178
178
|
- `abide/ui/remoteProxy`, `abide/ui/socketProxy` — the browser-side implementations the bundler swaps in for `GET(...)` / `socket(...)`.
|
|
179
179
|
|
|
180
180
|
### `.abide` component format (see `src/lib/ui/README.md`)
|
|
181
181
|
Valid HTML with `<script>` + native `<template>` control flow + scoped `<style>`.
|
|
182
182
|
- **Bindings:** `{expr}` text, `name={expr}` attr, `onclick={fn}`, `bind:value={…}` / `bind:checked` / `bind:group`, `attach={fn}` (node-lifetime attachment — the dual of `on`; the `use:`-action / `{@attach}` equivalent, lowered to `ui/dom/attach`).
|
|
183
|
-
- **Control flow (native `<template>`):** `if
|
|
183
|
+
- **Control flow (native `<template>`):** `if` with a nested `else` (the `<template else>` is a CHILD of its `<template if>`, not a sibling), `each={list} as="x" key="x.id"`, `await={p}`/`then`/`catch`, `switch`/`case`/`default`.
|
|
184
184
|
- **A branch is a lexical scope:** any control-flow branch may host its own nested `<script>` and `<style>`. The nested `<script>` declares branch-local **plain** signals (`state`/`derived`/`linked`/`prop`) — owned by the branch's render scope, re-seeded each mount (not the serializable top-level `doc`) — with the branch's binding in scope (`then`/`catch`'s `as` value, the `each` `as`/`key` row), so it can derive from the awaited/iterated value; its bindings cover the branch subtree and later siblings auto-deref them. The nested `<style>` is scoped to that branch alone (its own `data-a-<hash>`), not the whole component.
|
|
185
185
|
- **Components:** capitalised tags (`<Layout title=…>`); children fill `<slot/>`; props are reactive (passed as thunks). A component has no directives — every attribute is a prop under its written name (so `onclick=`/`bind:open=`/`attach=` pass through as props, e.g. callbacks, not the DOM-element directives those are on a lowercase tag) and is type-checked against the child's declared props. `prop('name')` reads a typed component prop (the parent-supplied thunk, reactive + read-only); route params come from the `page` proxy (`page.params.name`), not `prop()`.
|
|
186
186
|
- **Snippets / named slots:** `<template name="x" args={…}>` declares a reusable named builder (the `snippet()` form), rendered like a function — covers named slots / `{@render}`.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# abide
|
|
2
2
|
|
|
3
|
+
## 0.33.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`8af031e`](https://github.com/briancray/abide/commit/8af031e74ff3f3ab9cef05df75c0bbc22bd370d6) - feat(cache): rename the cache invalidation-policy option from `invalidate` to `swr` (stale-while-revalidate). `swr: true` keeps the entry and refetches in the background on a `cache.invalidate` hit — the stale value stays visible and `refreshing()` reports the in-flight reload — instead of dropping the entry to `pending()`. An optional window coalesces a burst of invalidations: `swr: { throttle: N }` refetches on the leading edge then at most once per N ms, `swr: { debounce: N }` refetches only after N ms of quiet. `cache()` still throws at wrap time on throttle+debounce set together, on `ttl: 0`, and on a non-replayable remote method. Replaces the former `invalidate: { throttle, debounce }` option (same coalescing semantics, clearer name).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [`9d4d899`](https://github.com/briancray/abide/commit/9d4d8992499bc7ef6e9cfc963dafc819725adabe) - fix: keyed `each`, `when`, and `switch` no longer crash when a synchronous write rebuilds them mid-hydrate. A page seeding shared reactive state during the hydrate pass (e.g. `breadcrumbs.crumbs = [...]`, or flipping an `if`/`switch` condition) re-ran the block's effect while `RENDER.hydration` was still active, so its fresh build claimed SSR nodes that were never adopted — surfacing as `null is not an object (element.setAttribute)` in `attr`/`openChild`. The reactive rebuild now runs with the global claim cursor cleared (restored after, mirroring `awaitBlock`/`tryBlock`): `each` clears it around its reconcile body, and `when`/`switch` get it for free via `fillBefore`, their single fresh-build path — adopt happens in place, so `fillBefore` is only ever create mode.
|
|
12
|
+
|
|
13
|
+
- [`9d4d899`](https://github.com/briancray/abide/commit/9d4d8992499bc7ef6e9cfc963dafc819725adabe) - fix: SVG and MathML now render correctly on the client. The imperative element builder used `document.createElement`, so a foreign element built on the client (an `<svg>` with a binding, a bound `<path>` inside an svg, foreign content with reactive children, or foreign elements generated by `if`/`each`/`await`) landed in the HTML namespace and rendered as nothing — SSR was unaffected, the divergence appeared only on the client-create path. Three changes close it: a static foreign subtree carrying bindings builds through a new parser-backed `skeleton` (one clone of a `<template>`-parsed skeleton, holes located and wired by index — the browser parser namespaces foreign content for free); the imperative path (`openChild`/`cloneStatic`) now namespaces elements off their parent via `createElementNS`/a foreign parse wrapper; and control-flow blocks (`when`/`switch`/`each`/`eachAsync`/`await`/`try`) carry an ambient foreign-namespace context (`RENDER.namespace`) across the detached fragment they build into, which `skeleton`/`openChild`/`cloneStatic` consult. Hydration was already correct (it adopts server-namespaced nodes).
|
|
14
|
+
|
|
15
|
+
- [`9d4d899`](https://github.com/briancray/abide/commit/9d4d8992499bc7ef6e9cfc963dafc819725adabe) - refactor(ui): unify the client render backend on the parser-backed `skeleton` and delete the imperative `openChild` element builder. Every bound element now builds through one clone-with-located-holes path — element holes by element-only path, and reactive text / control-flow blocks / `<slot>` outlets positioned by `<!--a-->` anchors (`anchorCursor`), so a block or slot can sit anywhere among static siblings (not just in a `[prefix][contiguous blocks][suffix]` shape). Nested `<script>`/snippet children build through the skeleton too. Foreign content (SVG/MathML) keeps its namespace in every case (the browser parser is the sole tree-builder). New plumbing: `abide/ui/dom/anchorCursor`, `abide/ui/dom/mountSlot`. Removed: `abide/ui/dom/openChild`, `abide/ui/dom/cursorAfterElements` (and the internal `childNamespace`). SSR emits the matching `<!--a-->` anchors, so the SSR == client DOM invariant holds. No public `.abide` behaviour change.
|
|
16
|
+
|
|
17
|
+
Also fixes `<template await>` hydration when the block is genuinely pending (no resume value, not warm-sync): it discards the SSR boundary and rebuilds, and now (a) inserts at the node _after_ the discarded boundary rather than the captured insertion ref — which for a skeleton-anchored block was the await's own open boundary, removed by the discard, so reusing it threw `NotFoundError` in a strict DOM (WebKit) — and (b) clears the claim cursor while rebuilding the pending branch, so its static content actually builds instead of trying to claim discarded nodes. The test mini-DOM's `insertBefore` is now strict (throws on a non-child reference) so this class of bug surfaces in tests, not only the browser.
|
|
18
|
+
|
|
19
|
+
## 0.32.2
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- [`786f07a`](https://github.com/briancray/abide/commit/786f07a3c23986e988b2f0c0b3326e050129c959) - sync examples and ui README to current API ([`923bb86`](https://github.com/briancray/abide/commit/923bb8674814e8efcacc954beac0f1c7982b0e2c))
|
|
24
|
+
|
|
25
|
+
- [`786f07a`](https://github.com/briancray/abide/commit/786f07a3c23986e988b2f0c0b3326e050129c959) - reject a static import in a nested <template> script ([`b9e7c76`](https://github.com/briancray/abide/commit/b9e7c76509d2401484d3f997b0cb904af4b5080c))
|
|
26
|
+
|
|
27
|
+
- [`786f07a`](https://github.com/briancray/abide/commit/786f07a3c23986e988b2f0c0b3326e050129c959) - merge valueAtPath + pathExists into a single-pass walkPath ([`bad087b`](https://github.com/briancray/abide/commit/bad087b39c4b5427bf44fec6c3f2eb1fdbccf9c2))
|
|
28
|
+
|
|
29
|
+
- [`786f07a`](https://github.com/briancray/abide/commit/786f07a3c23986e988b2f0c0b3326e050129c959) - remap any HTML-element-named component wrapper, not just void ([`bbbbe35`](https://github.com/briancray/abide/commit/bbbbe353e22b5d016cd47e81eb8bc9a786fbe336))
|
|
30
|
+
|
|
31
|
+
- [`786f07a`](https://github.com/briancray/abide/commit/786f07a3c23986e988b2f0c0b3326e050129c959) - extract the static-clone template cache, keyed per document ([`d35beaa`](https://github.com/briancray/abide/commit/d35beaa956a449b79a5be63034473262226ad44d))
|
|
32
|
+
|
|
3
33
|
## 0.32.1
|
|
4
34
|
|
|
5
35
|
### Patch Changes
|
|
@@ -90,16 +120,16 @@
|
|
|
90
120
|
|
|
91
121
|
- [`0cf889b`](https://github.com/briancray/abide/commit/0cf889b859b9169be2abd35c2624ff762bd08e91) - Add `@abide/inspector` — an opt-in inspector activated by `ABIDE_ENABLE_INSPECTOR=true`. Install it (`bun add -d @abide/inspector`) and the flag mounts a UI at `/__abide/inspector`.
|
|
92
122
|
|
|
93
|
-
|
|
123
|
+
Three tabs over one event stream:
|
|
94
124
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
- **Logs** (default) — a live tail of request traffic, app/diagnostic channels, cache tallies, and published socket frames, with filters by channel, trace id, and free text. Click a trace id to pivot the feed to that trace.
|
|
126
|
+
- **Traces** — records grouped by trace id. Because abide propagates W3C trace context (a page session is one trace — the SSR render plus every RPC its interactions fire), a trace holds many requests; the tab splits each trace into nested per-request lanes (each on its own time axis, children indented under the request they descend from) with a span waterfall per request. Records now carry `requestSpan` / `parentSpan` (from the request's `TraceContext`) so this split is possible.
|
|
127
|
+
- **Cache** — the persistent (`global: true`) cache store: each entry's key, lifecycle state (settled / in-flight / refreshing), kind, ttl, time-to-expiry, scope tags, invalidate policy, and a value preview. Refreshes on open and on demand. (Request-scoped caches are ephemeral, so they surface as per-request tallies in Logs/Traces instead.)
|
|
128
|
+
- **Surface** — the static machine catalog: RPC verbs (with their declared `timeout` / `maxBodySize` / `crossOrigin` / `files` options as columns, and input/output JSON Schemas on expand) and sockets.
|
|
99
129
|
|
|
100
|
-
|
|
130
|
+
The framework now self-instruments the request lifecycle with `log.trace` spans on DEBUG-gated diagnostic channels — `abide:render` (SSR render), `abide:view` (view/module resolution), `abide:rpc` (verb dispatch + validation; reveals in-process RPC→RPC calls as nested spans in the same trace), `abide:cache` (producer run on a miss + coalesced wait), `abide:mcp` (tool dispatch), and `abide:sockets` (REST tail/publish). They're zero-cost when their channel is off and fill the Traces waterfall when enabled (`DEBUG=abide:*`). The verb registry now records `timeout` / `maxBodySize` / `crossOrigin` so introspection can report them.
|
|
101
131
|
|
|
102
|
-
|
|
132
|
+
Core stays clean: a guarded, non-literal dynamic import keeps the package out of the compiled binary, and core injects an `InspectorContext` so the package imports no abide internals. Two passive observation seams feed the inspector — the existing log tap plus a new socket-frame tap in `defineSocket`'s `publish()` — both no-ops when no inspector is mounted. The inspector is privileged operator tooling: it answers ahead of `app.handle` and warns loudly on mount, so enable it only in trusted/dev environments.
|
|
103
133
|
|
|
104
134
|
### Patch Changes
|
|
105
135
|
|
|
@@ -171,13 +201,13 @@
|
|
|
171
201
|
|
|
172
202
|
- [`f9cd269`](https://github.com/briancray/abide/commit/f9cd269b44b065702275422ac710019117f69a2c) - fix(abide): `json(undefined)` returns 204 No Content instead of throwing
|
|
173
203
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
204
|
+
`Response.json(undefined)` throws TypeError because JSON has no encoding for
|
|
205
|
+
`undefined`, so any handler returning `json(undefined)` — e.g. a
|
|
206
|
+
`Shape | undefined` route signalling "not found" — 500ed instead of degrading.
|
|
207
|
+
`json()` now emits 204 No Content for `undefined`, which `decodeResponse`
|
|
208
|
+
already maps back to `undefined` on both the fetch and in-process paths, so
|
|
209
|
+
the `Shape | undefined` RPC contract round-trips the wire. The helper owns
|
|
210
|
+
the 204; it wins over any `init.status`.
|
|
181
211
|
|
|
182
212
|
## 0.25.1
|
|
183
213
|
|
|
@@ -249,7 +279,7 @@
|
|
|
249
279
|
|
|
250
280
|
- [`d8fe8fc`](https://github.com/briancray/abide/commit/d8fe8fcae7863407d7b55774e19762715d311450) - **Fixed**
|
|
251
281
|
|
|
252
|
-
|
|
282
|
+
- The lifecycle channel defers its notify to a microtask, coalescing marks within a tick. With a live `pending()`/`refreshing()` probe armed, a cold cache read inside a `$derived` — the documented `$derived(await cache(fn)())` idiom — registered its entry mid-derived and wrote the subscriber's version source synchronously, throwing `state_unsafe_mutation` and killing the flush (seen as an unhandled rejection plus a follow-on `active_reaction` TypeError, with UI updates in that batch silently dropped). Probes scan the registry at re-derive time, so the deferred ping reads state that is already current.
|
|
253
283
|
|
|
254
284
|
## 0.23.0
|
|
255
285
|
|
|
@@ -257,52 +287,52 @@
|
|
|
257
287
|
|
|
258
288
|
- [`13f841c`](https://github.com/briancray/abide/commit/13f841c4f3ec92727414715c1bb5eeabc12b60a7) - **Breaking**
|
|
259
289
|
|
|
260
|
-
|
|
290
|
+
- `bundled()` moved to the bundle namespace alongside `onMenu`: `import { bundled } from '@abide/abide/bundle/bundled'` (was `abide/shared/bundled`).
|
|
261
291
|
|
|
262
292
|
- [`bbc82a1`](https://github.com/briancray/abide/commit/bbc82a12c948acbb2f82fe1dfc46165678fafb04) - Registries act, probes report: standalone `pending`/`refreshing` spanning cache and streams, the `ttl: 0` mutation idiom made sound, and tail reconnect-with-retained-value.
|
|
263
293
|
|
|
264
|
-
|
|
294
|
+
**Breaking**
|
|
265
295
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
296
|
+
- `cache.pending` / `cache.refreshing` moved to their own modules: `import { pending } from '@abide/abide/shared/pending'`, `import { refreshing } from '@abide/abide/shared/refreshing'`. `cache.invalidate` stays on `cache`.
|
|
297
|
+
- SSR snapshots ship GET entries only (`REPLAYABLE_METHODS`): DELETE is idempotent but still a write and no longer replays from hydration.
|
|
298
|
+
- `cache()` now throws at wrap time on invalid policy combinations: `invalidate` policy on a non-GET remote, policy with `ttl: 0`, or `throttle` and `debounce` together.
|
|
269
299
|
|
|
270
|
-
|
|
300
|
+
**Fixed**
|
|
271
301
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
302
|
+
- Invalidate policies attach on read to entries that lack one (like scope tags), so a snapshot-hydrated entry revalidates stale-in-place from its first invalidate instead of hard-dropping to a pending flash, and a policy-less first read no longer permanently wins.
|
|
303
|
+
- Hydrated snapshot entries adopt the first reading call site's `ttl` (the snapshot ships no wrap options): omitted keeps the entry as before, `ttl > 0` starts the expiry clock at that read, and `ttl: 0` serves the hydration pass only, evicting a macrotask later — previously any ttl was ignored, so a `ttl: 0` key warm-hit forever and never refetched.
|
|
304
|
+
- Eviction clears armed policy timers — a TTL-expired or rejected key can no longer ghost-refetch.
|
|
275
305
|
|
|
276
|
-
|
|
306
|
+
**Added**
|
|
277
307
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
- `pending(x)` / `refreshing(x)` probe both registries: cache selectors (bare / fn / `{ scope }`) plus Subscribables — `pending(chat)` is "awaiting first frame", `refreshing(chat)` is "reconnecting with last value retained" (never merely open). Bare forms span registries. Probes report, never act: they open no fetch and no stream.
|
|
309
|
+
- `tail()` (né `subscribe()` — renamed in this release) self-heals transport loss: on the typed `SocketDisconnectedError` it keeps its value, flags `refreshing`, and reopens under the channel's backoff (retained-tail replay converges the value — correct for a latest-wins/window consumer). Server `err` frames stay terminal; raw `for await` consumers keep the explicit-disconnect contract.
|
|
310
|
+
- `cache()` warns once per call site when handed an anonymous producer (fresh identity per call — it can never coalesce and probes can never match it).
|
|
281
311
|
|
|
282
312
|
- [`c77fc89`](https://github.com/briancray/abide/commit/c77fc89a12f07b9356b5ad68cd4a92f4764e52b8) - Replay is demarcated on the wire: a sub's seed now arrives as one per-sub `{ type: 'replay', sub, messages }` frame (sent even when empty) instead of socket-keyed `msg` frames.
|
|
283
313
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
314
|
+
- `tail()` windows commit their seed atomically at the boundary — no more shrink-regrow rebuild on reconnect or first paint — and an **empty** replay keeps the held window across a gap (nothing replayed = nothing to duplicate; live frames append). Previously the first post-gap frame wiped the window even on non-retaining sockets.
|
|
315
|
+
- Per-sub addressing fixes cross-sub replay leakage: two subs on the same socket no longer receive each other's replay.
|
|
316
|
+
- `Subscribable.tail(count, hooks?)` gains optional `TailHooks`: retaining sources must signal `hooks.replayed` in-band once the seed is delivered (sockets do; a source that ends without signalling commits at `done`).
|
|
317
|
+
- Internal: `createPushIterator` gains `control(run)` — in-band signal slots, strictly ordered against pushed values, invisible to consumers. Raw `for await` iteration is unchanged.
|
|
288
318
|
|
|
289
319
|
- [`c77fc89`](https://github.com/briancray/abide/commit/c77fc89a12f07b9356b5ad68cd4a92f4764e52b8) - `subscribe()` is now `tail()`, and "history" is dead: one word for reading the retained end of a stream at every altitude — declaration (`socket<T>({ tail: n })`), raw iteration (`chat.tail(count)`), and the reactive consumer (`tail(x)` / `tail(x, { last: n })`).
|
|
290
320
|
|
|
291
|
-
|
|
321
|
+
**Breaking**
|
|
292
322
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
323
|
+
- `import { subscribe } from '@abide/abide/browser/subscribe'` → `import { tail } from '@abide/abide/browser/tail'`; `subscribe.status`/`subscribe.error` → `tail.status`/`tail.error` (both accept the same `{ last }` options to address a window entry).
|
|
324
|
+
- Socket declaration option `history` → `tail` (`socket<T>({ tail: 100 })`). Retention is opt-in: an undeclared socket is a pure live pipe and storage is the consumer's concern.
|
|
325
|
+
- Bare socket iteration (`for await (const m of chat)`) is now live-only — replay is exclusively `.tail`'s job. `chat.tail()` no-arg replays the whole retained tail (the old bare behavior); `chat.tail(n)` the last n. The ws wire is unchanged; only the local defaults flipped.
|
|
326
|
+
- The reactive bare form seeds a socket via `tail(1)` instead of full replay — retained frames no longer churn through `latest` on open.
|
|
297
327
|
|
|
298
|
-
|
|
328
|
+
**Added**
|
|
299
329
|
|
|
300
|
-
|
|
301
|
-
|
|
330
|
+
- `tail(x, { last: n })` → `T[]`: a live rolling window of the last ≤n frames, however they arrived ([] while pending and on SSR, never undefined). Retaining sockets seed it by replaying up to `last` (clamped to the declared `tail`); rpc streams and undeclared sockets fill it from live frames. The bare form and each window size are independent subscriptions (`last` is registry-keyed). On reconnect the replay replaces the window, so a gap can't duplicate frames.
|
|
331
|
+
- `Subscribable.tail(count)` is the optional retention capability: sources that keep recent frames implement it (sockets do verbatim) and the consumer bounds replay to what the reader keeps — no consumer special-casing per source type.
|
|
302
332
|
|
|
303
|
-
|
|
333
|
+
**Fixed**
|
|
304
334
|
|
|
305
|
-
|
|
335
|
+
- An untracked `tail.status()`/`tail.error()` read (outside `$derived`/`$effect`) no longer leaks a permanently-pending registry entry that held the bare `pending()` probe true forever.
|
|
306
336
|
|
|
307
337
|
## 0.22.0
|
|
308
338
|
|
|
@@ -336,7 +366,7 @@
|
|
|
336
366
|
|
|
337
367
|
- [`9cbca7d`](https://github.com/briancray/abide/commit/9cbca7d28c258d4574ac450811628f107e502711) - Bundle apps auto-start the local assistant. When a bundled abide app connects (embedded or remote) and the app ships `@abide/claude-code` (its UI uses `browser/assistant`) with `claude` on PATH, the bundle launcher runs the loopback bridge for you and hands the page its port+token via the URL fragment — no copy-paste command. The bridge is loopback-only and dies with the connection. abide takes **no dependency** on `@abide/claude-code`: it's a guarded optional import that no-ops (and compiles fine) when the app doesn't ship it.
|
|
338
368
|
|
|
339
|
-
|
|
369
|
+
`assistant()` gains a `status` — `'ready' | 'starting' | 'manual' | 'unavailable'` — so the same UI works in a browser (`manual` → show `command`) and a bundle (`starting`/`ready` auto-managed, or `unavailable` when `claude` isn't installed → show an install hint). `command` is now `string | undefined` (undefined whenever a host manages the bridge).
|
|
340
370
|
|
|
341
371
|
## 0.19.4
|
|
342
372
|
|
|
@@ -460,15 +490,15 @@
|
|
|
460
490
|
|
|
461
491
|
- [#40](https://github.com/briancray/abide/pull/40) [`26ba6fe`](https://github.com/briancray/abide/commit/26ba6fe710002f84b99947f7b198fa3b3f235d53) Thanks [@briancray](https://github.com/briancray)! - Namespace the CLI's baked env under `ABIDE_` and add data-dir controls.
|
|
462
492
|
|
|
463
|
-
|
|
493
|
+
**Breaking:** `APP_URL` → `ABIDE_APP_URL` and `APP_TOKEN` → `ABIDE_APP_TOKEN`. These are the values baked into a downloaded CLI's `.env` (the hosted server URL, derived from the request origin, plus the bearer token when the download was authenticated) and read by the thin client to resolve its connection target. `ABIDE_APP_URL` is now public, documented surface — app code can read it to refer to the app's hosted location. Existing baked binaries and any shell `APP_URL`/`APP_TOKEN` overrides must switch to the prefixed names.
|
|
464
494
|
|
|
465
|
-
|
|
495
|
+
**Added:** `abide/server/appDataDir` — a zero-arg accessor returning the running bundle's per-user data dir, keyed to the same program name abide uses for the user's `.env`/`last-connection.json`, so an app's DB/cache lands beside abide's own config rather than a drifted sibling directory.
|
|
466
496
|
|
|
467
|
-
|
|
497
|
+
**Added:** `ABIDE_DATA_DIR` — overrides the data dir on every platform, used as-is. A cross-platform `XDG_DATA_HOME` (which the helper otherwise honours on Linux only), letting dev point at a throwaway dir without touching app code. Must come from a layer above the data-dir `.env` (shell, CWD `.env`, or binary-dir `.env`), since it decides where that file lives.
|
|
468
498
|
|
|
469
499
|
- [#40](https://github.com/briancray/abide/pull/40) [`310eceb`](https://github.com/briancray/abide/commit/310ecebebd018df62155d18ac01a376f5a0f42ba) Thanks [@briancray](https://github.com/briancray)! - Remove the `key` option from `cache()`.
|
|
470
500
|
|
|
471
|
-
|
|
501
|
+
**Breaking:** `cache(fn, { key })` and the `{ key }` selector form of `cache.invalidate` / `cache.pending` are gone. Cache keys are always auto-derived — method+url+args for a remote function, producer-reference+args for a plain producer. To share an entry across calls, hoist the producer to a stable reference (an inline arrow gets a fresh identity every call and never dedupes). To target a set of unrelated calls with one `cache.invalidate`, tag them with a `scope`; a unique tag (e.g. a uuid) gives a set of calls their own private invalidation group.
|
|
472
502
|
|
|
473
503
|
## 0.11.1
|
|
474
504
|
|
|
@@ -482,7 +512,7 @@
|
|
|
482
512
|
|
|
483
513
|
- [`a1d1d56`](https://github.com/briancray/abide/commit/a1d1d56efe4887bebb74dd6707cf7cb38d8b4771) - `cache` and `HttpError` move from the `browser`/`server` namespaces to `shared`, which now denotes the isomorphic surface — names that are the same callable with the same behaviour on both sides. `cache()` runs in SSR and MCP request scope just as it does on the client, so importing it as a "browser" module misrepresented it; its client-only streaming/hydration helpers stay in `browser/` and its server-only snapshot helpers stay in `server/runtime/`. Update imports: `abide/browser/cache` → `abide/shared/cache`, and `abide/browser/HttpError` (or `abide/server/HttpError`) → `abide/shared/HttpError`.
|
|
484
514
|
|
|
485
|
-
|
|
515
|
+
The package `exports` map is now an explicit allowlist of the public API instead of per-directory `*` globs, so internal modules (machinery under `shared/`, runtime/registry internals under `server/`, launcher internals under `bundle/`, and all `types/` subtrees) are no longer reachable via the package specifier. Only documented names — the verb/response/context helpers, `cache`, `HttpError`, `page`/`navigate`/`subscribe`, the `bundle` window config, the test client, and the build/plugin entries — resolve. Importing an unlisted internal path now fails; use the public name instead.
|
|
486
516
|
|
|
487
517
|
- [`a1d1d56`](https://github.com/briancray/abide/commit/a1d1d56efe4887bebb74dd6707cf7cb38d8b4771) - `cache()` now memoises plain producers, not just rpc verb helpers — pass any `() => Promise<T>` to dedupe and retain external calls (e.g. a third-party `fetch` the server makes). Producers key on the function's reference plus args (so hoist the function, or pass an explicit `key`; an inline arrow is a fresh reference every call and never dedupes), and the value promise is stored as-is — no Response decode and no SSR snapshot. A new `global: true` option puts the entry in a process-level store instead of the request-scoped one, so a value computed in one request is reused by later ones; omit it to keep per-request data from leaking across requests, and note it is a no-op on the client (one tab store). `cache.invalidate` / `cache.pending` accept a producer reference and span both stores.
|
|
488
518
|
|
|
@@ -492,13 +522,13 @@
|
|
|
492
522
|
|
|
493
523
|
- [`a1d1d56`](https://github.com/briancray/abide/commit/a1d1d56efe4887bebb74dd6707cf7cb38d8b4771) - Run in-process rpc dispatch inside the request scope for the MCP tool dispatcher and the in-process CLI client. Previously both invoked handlers without a per-request scope, so `cache()` silently shared one process-wide store across calls (leaking state between unrelated tool/CLI invocations) and `cookies()`/`request()` threw. Both now cross the same `runWithRequestScope` seam the HTTP router uses, giving per-call cache isolation and resolving the scope-bound helpers.
|
|
494
524
|
|
|
495
|
-
|
|
525
|
+
Behavior change for MCP: a tool handler that throws is now caught by the scope and returned as a tool result with `isError: true` (framed from the 500 response), instead of surfacing as a JSON-RPC `-32603` error on the envelope. The JSON-RPC call itself succeeds; the failure is reported at the tool-result level, which is the correct MCP shape.
|
|
496
526
|
|
|
497
527
|
- [`a1d1d56`](https://github.com/briancray/abide/commit/a1d1d56efe4887bebb74dd6707cf7cb38d8b4771) - Project JSON Schema from a schema's own `toJSONSchema()` everywhere it's needed (OpenAPI, MCP tools, CLI flags, the bundle setup form). Drop the `inputJsonSchema` / `outputJsonSchema` / `filesJsonSchema` verb opts and the socket `jsonSchema` opt — a schema whose library doesn't expose a method wraps once with the new `abide/shared/withJsonSchema` helper. Multipart file parts are now advertised generically as binary in OpenAPI rather than named per field.
|
|
498
528
|
|
|
499
|
-
|
|
529
|
+
Add `src/server/config.ts` as the home for typed env: `export const config = env(schema)`, imported as `$server/config` and eager-imported at boot so validation fails fast. The file is optional and scaffolded — when absent you read `Bun.env` directly.
|
|
500
530
|
|
|
501
|
-
|
|
531
|
+
The bundle's first-run setup form is now derived from that same env schema by default, so one declaration drives boot validation and the form. `BundleWindow.config` still works but now _replaces_ the derived schema (for a form that should differ from the env schema) rather than being the only source.
|
|
502
532
|
|
|
503
533
|
- [`a1d1d56`](https://github.com/briancray/abide/commit/a1d1d56efe4887bebb74dd6707cf7cb38d8b4771) - Add two server primitives. `abide/server/env` validates the process environment against a Standard Schema at module load, returning typed config and failing the boot with every issue listed when a variable is missing or malformed. `abide/server/cookies` exposes the request's cookie jar — Bun's native `CookieMap` parsed from the inbound `Cookie` header, with `set`/`delete` writes flushed to `Set-Cookie` on the outgoing response when the handler returns. `cookies` resolves from the request scope like `request()`, materialized lazily so a request that never touches them parses and emits nothing; `env` reads `Bun.env` once at module load, independent of any request.
|
|
504
534
|
|
|
@@ -596,12 +626,12 @@
|
|
|
596
626
|
|
|
597
627
|
- [#10](https://github.com/briancray/abide/pull/10) [`6ceb71b`](https://github.com/briancray/abide/commit/6ceb71b28e3b1a4c9726483d2c7dd3f40be3be59) Thanks [@briancray](https://github.com/briancray)! - Bundles now resolve config from a cwd-independent source instead of relying on Bun's cwd-based `.env` autoload (which a launched `.app`, whose cwd is `/`, silently misses). Config flows entirely through `process.env`, so app code keeps reading `Bun.env.*` and never learns where a value came from.
|
|
598
628
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
629
|
+
- The compiled server loads two `.env` layers into `process.env` at boot, before anything reads it: the per-user data dir first (user config), then the binary dir (shipped default). Both back-fill only what a shell export or Bun's CWD `.env` didn't already set, so the precedence is `shell > CWD .env > data-dir .env > binary-dir .env > code default`.
|
|
630
|
+
- Add `abide/shared/appDataDir` — the platform-standard per-user data directory keyed by program name, where the data-dir `.env` lives.
|
|
631
|
+
- `abide bundle` ships an optional project `.env.bundle` as the binary-dir `.env` (the shipped default layer). Skipped when absent; use a dedicated file, never the working `.env`, since a compiled bundle is extractable.
|
|
632
|
+
- Start now races server readiness against the child's exit, so a misconfigured bundle reports the crash immediately instead of stalling for the full readiness timeout.
|
|
633
|
+
- A bundle resolves its last connection before the window opens: the launcher records the choice (embedded, or a remote URL) in the data dir, and on relaunch boots/probes it first, opening the window straight at the live server — so the connect screen never flashes. A boot that fails or exceeds a short ceiling, an unconfigured embedded resume, a dead saved server, or no saved choice falls back to opening the connect screen.
|
|
634
|
+
- A bundle can declare `config` on its `BundleWindow` as a Standard Schema (the same kind abide accepts for RPC/MCP). Its JSON Schema drives a first-run settings modal on the connect screen — `title` → label, `description` → hint, `format: 'password'` → masked input, `default` → prefill — and answers persist to the data-dir `.env`. An explicit Start (button or File-menu click) always opens the modal prefilled with the last-used values, so re-running Start after a disconnect is how you reconfigure; an auto-start on relaunch never opens the modal — it boots a fully-configured app, or stays on the connect screen when a required key is still unset. Apps with no schema always boot straight through.
|
|
605
635
|
|
|
606
636
|
### Patch Changes
|
|
607
637
|
|
|
@@ -667,12 +697,12 @@
|
|
|
667
697
|
|
|
668
698
|
- [`cf136c7`](https://github.com/briancray/abide/commit/cf136c7b763283570ef431b3aad269626bea7824) - Add a `abide bundle` desktop app and make the CLI a thin remote-only client.
|
|
669
699
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
700
|
+
- `abide bundle` assembles a movable, self-contained desktop app (a `.app` on macOS, a flat dir elsewhere) that boots into a connect screen — start the embedded server or connect to a remote one by URL.
|
|
701
|
+
- **Breaking:** the CLI binary is now always a thin remote client (talks to a running server over HTTP, `APP_URL` required). Dropped the `--thin`/full split and in-process fallback — use `abide bundle` for the embedded-backend case.
|
|
702
|
+
- **Breaking:** MCP prompts are now markdown files (`src/mcp/prompts/**.md`) with YAML frontmatter, replacing the `.ts` prompt modules.
|
|
703
|
+
- **Breaking:** handlers read the inbound request via `request()` and the live server via `server()` rather than `RequestStore` fields.
|
|
704
|
+
- `json` / `jsonl` / `sse` / `error` / `redirect` accept a trailing `ResponseInit`.
|
|
705
|
+
- Static-asset header caching is shared across asset servers, and zstd decompression moved to the async API.
|
|
676
706
|
|
|
677
707
|
## 0.1.0
|
|
678
708
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.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",
|
|
@@ -81,11 +81,14 @@
|
|
|
81
81
|
"./ui/dom/mountChild": "./src/lib/ui/dom/mountChild.ts",
|
|
82
82
|
"./ui/dom/hydrate": "./src/lib/ui/dom/hydrate.ts",
|
|
83
83
|
"./ui/dom/text": "./src/lib/ui/dom/text.ts",
|
|
84
|
-
"./ui/dom/openChild": "./src/lib/ui/dom/openChild.ts",
|
|
85
84
|
"./ui/dom/appendText": "./src/lib/ui/dom/appendText.ts",
|
|
85
|
+
"./ui/dom/appendTextAt": "./src/lib/ui/dom/appendTextAt.ts",
|
|
86
86
|
"./ui/dom/appendSnippet": "./src/lib/ui/dom/appendSnippet.ts",
|
|
87
87
|
"./ui/dom/appendStatic": "./src/lib/ui/dom/appendStatic.ts",
|
|
88
88
|
"./ui/dom/cloneStatic": "./src/lib/ui/dom/cloneStatic.ts",
|
|
89
|
+
"./ui/dom/skeleton": "./src/lib/ui/dom/skeleton.ts",
|
|
90
|
+
"./ui/dom/anchorCursor": "./src/lib/ui/dom/anchorCursor.ts",
|
|
91
|
+
"./ui/dom/mountSlot": "./src/lib/ui/dom/mountSlot.ts",
|
|
89
92
|
"./ui/dom/attr": "./src/lib/ui/dom/attr.ts",
|
|
90
93
|
"./ui/dom/on": "./src/lib/ui/dom/on.ts",
|
|
91
94
|
"./ui/dom/attach": "./src/lib/ui/dom/attach.ts",
|
|
@@ -151,6 +154,7 @@
|
|
|
151
154
|
"devDependencies": {
|
|
152
155
|
"@abide/inspector": "workspace:*",
|
|
153
156
|
"bun-plugin-tailwind": "^0.1.2",
|
|
157
|
+
"happy-dom": "^20.10.6",
|
|
154
158
|
"tailwindcss": "^4.0.0"
|
|
155
159
|
}
|
|
156
160
|
}
|
|
@@ -18,19 +18,20 @@ function preview(value: unknown): string | undefined {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* The entry's armed
|
|
21
|
+
/* The entry's armed swr policy as a label, if it declared one. A bare `swr: true`
|
|
22
|
+
has no window, so it labels as plain `swr`. */
|
|
22
23
|
function policyLabel(entry: CacheEntry): string | undefined {
|
|
23
24
|
const policy = entry.invalidation
|
|
24
25
|
if (!policy) {
|
|
25
26
|
return undefined
|
|
26
27
|
}
|
|
27
28
|
if (policy.debounce !== undefined) {
|
|
28
|
-
return `debounce ${policy.debounce}ms`
|
|
29
|
+
return `swr debounce ${policy.debounce}ms`
|
|
29
30
|
}
|
|
30
31
|
if (policy.throttle !== undefined) {
|
|
31
|
-
return `throttle ${policy.throttle}ms`
|
|
32
|
+
return `swr throttle ${policy.throttle}ms`
|
|
32
33
|
}
|
|
33
|
-
return
|
|
34
|
+
return 'swr'
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function projectEntry(entry: CacheEntry, now: number): InspectorCacheEntry {
|
|
@@ -19,6 +19,6 @@ export type InspectorCacheEntry = {
|
|
|
19
19
|
scope: string[]
|
|
20
20
|
/* A short JSON preview of the decoded warm value, when the entry holds one. */
|
|
21
21
|
value: string | undefined
|
|
22
|
-
/* An armed
|
|
22
|
+
/* An armed swr policy (`swr`, optionally + throttle/debounce + ms), if declared. */
|
|
23
23
|
policy: string | undefined
|
|
24
24
|
}
|
package/src/lib/shared/cache.ts
CHANGED
|
@@ -215,31 +215,48 @@ export function cache<Args, Return>(
|
|
|
215
215
|
return read
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
/*
|
|
219
|
+
Normalises the `swr` option to its window, or undefined when off. `true` (or
|
|
220
|
+
`{}`) is stale-while-revalidate with no window — refetch immediately on every
|
|
221
|
+
invalidate; an object carries the throttle/debounce window. `false`/omitted is
|
|
222
|
+
off. Collapsing the boolean here lets every downstream site treat "is SWR on"
|
|
223
|
+
as a single defined/undefined check.
|
|
224
|
+
*/
|
|
225
|
+
function swrWindow(
|
|
226
|
+
options: CacheOptions | undefined,
|
|
227
|
+
): { throttle?: number; debounce?: number } | undefined {
|
|
228
|
+
const swr = options?.swr
|
|
229
|
+
if (swr === undefined || swr === false) {
|
|
230
|
+
return undefined
|
|
231
|
+
}
|
|
232
|
+
return swr === true ? {} : swr
|
|
233
|
+
}
|
|
234
|
+
|
|
218
235
|
/*
|
|
219
236
|
Guards impossible option combinations at wrap time, where the call site is on
|
|
220
|
-
the stack.
|
|
221
|
-
non-replayable remote method (a write) must never carry
|
|
237
|
+
the stack. `swr` declares "this call is safe to re-run unprompted", so a
|
|
238
|
+
non-replayable remote method (a write) must never carry it — replaying a write
|
|
222
239
|
through the invalidation grammar would be a state change disguised as a
|
|
223
240
|
refresh. Producers are opaque (no method to check); the same contract is on
|
|
224
|
-
the caller there. ttl: 0 retains nothing, so there is nothing
|
|
225
|
-
|
|
241
|
+
the caller there. ttl: 0 retains nothing, so there is nothing to revalidate;
|
|
242
|
+
and the two coalescing windows are exclusive by construction.
|
|
226
243
|
*/
|
|
227
244
|
function validatePolicy(options: CacheOptions | undefined, method: string | undefined): void {
|
|
228
|
-
const policy = options
|
|
229
|
-
if (!policy
|
|
245
|
+
const policy = swrWindow(options)
|
|
246
|
+
if (!policy) {
|
|
230
247
|
return
|
|
231
248
|
}
|
|
232
249
|
if (policy.throttle !== undefined && policy.debounce !== undefined) {
|
|
233
|
-
throw new Error('[abide] cache(): set
|
|
250
|
+
throw new Error('[abide] cache(): set swr.throttle or swr.debounce, not both')
|
|
234
251
|
}
|
|
235
252
|
if (options?.ttl === 0) {
|
|
236
253
|
throw new Error(
|
|
237
|
-
'[abide] cache():
|
|
254
|
+
'[abide] cache(): swr requires retention — ttl: 0 keeps nothing to revalidate',
|
|
238
255
|
)
|
|
239
256
|
}
|
|
240
257
|
if (method !== undefined && !REPLAYABLE_METHODS.has(method.toUpperCase())) {
|
|
241
258
|
throw new Error(
|
|
242
|
-
`[abide] cache():
|
|
259
|
+
`[abide] cache(): swr re-runs the call unprompted — ${method.toUpperCase()} is a write and must not be replayed`,
|
|
243
260
|
)
|
|
244
261
|
}
|
|
245
262
|
}
|
|
@@ -334,12 +351,11 @@ function registerEntry(
|
|
|
334
351
|
refetch: () => Promise<unknown>,
|
|
335
352
|
): CacheEntry {
|
|
336
353
|
const ttl = options?.ttl
|
|
337
|
-
/* Capture the refetch thunk +
|
|
338
|
-
const policy = options
|
|
339
|
-
const invalidation =
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
: undefined
|
|
354
|
+
/* Capture the refetch thunk + window only when swr was asked for. */
|
|
355
|
+
const policy = swrWindow(options)
|
|
356
|
+
const invalidation = policy
|
|
357
|
+
? { refetch, throttle: policy.throttle, debounce: policy.debounce }
|
|
358
|
+
: undefined
|
|
343
359
|
/*
|
|
344
360
|
A prior entry for this key was dropped by invalidate() and is awaiting its
|
|
345
361
|
next read — consume the marker so this replacement read reports as a reload
|
|
@@ -477,7 +493,7 @@ Invalidates every entry matching the selector (see selectorMatcher) across both
|
|
|
477
493
|
the request/tab store and the process-level store, and notifies readers.
|
|
478
494
|
`args` narrows a fn selector to exactly that call's entry — derived through
|
|
479
495
|
the same encoders the read path uses, so other args variants stay warm. An entry
|
|
480
|
-
with an
|
|
496
|
+
with an swr policy is kept and its refetch coalesced (stale served
|
|
481
497
|
until it resolves); every other match is dropped so the next read refetches —
|
|
482
498
|
its key recorded in pendingRefresh so that read reports as a reload (refreshing())
|
|
483
499
|
rather than a first-ever load. An empty or unmatched selector is a no-op on the
|
|
@@ -722,9 +738,9 @@ cache.on = on
|
|
|
722
738
|
Optimistic write: applies `updater` as a prediction now — the reactive read
|
|
723
739
|
shows it immediately — runs `call`, then reconciles. On resolve the server is
|
|
724
740
|
the truth: the prediction is dropped and the selector invalidated, so the value
|
|
725
|
-
refetches authoritatively, coalesced per the read's own
|
|
726
|
-
(cache(fn, {
|
|
727
|
-
extra knob here; without
|
|
741
|
+
refetches authoritatively, coalesced per the read's own swr window
|
|
742
|
+
(cache(fn, { swr: { throttle } }) bounds an optimistic-write storm with no
|
|
743
|
+
extra knob here; without swr it is a plain drop-and-refetch). On reject the
|
|
728
744
|
prediction rolls back. The returned promise is transparent over `call` —
|
|
729
745
|
resolves to `call`'s value (the mutation result, e.g. a created id), rejects
|
|
730
746
|
with its error, settling only after the cache reflects the reconciled state: an
|
|
@@ -769,10 +785,12 @@ function patch<Args, Return, Result>(
|
|
|
769
785
|
cache.patch = patch
|
|
770
786
|
|
|
771
787
|
/*
|
|
772
|
-
Schedules a coalesced refetch per the entry's
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
the
|
|
788
|
+
Schedules a coalesced refetch per the entry's swr policy. No window (swr: true):
|
|
789
|
+
fire immediately (throttle defaults to 0, so the leading-edge branch always
|
|
790
|
+
takes). debounce: (re)arm a timer that fires after N ms of quiet. throttle: fire
|
|
791
|
+
on the leading edge when a full window has elapsed since the last fire, else arm
|
|
792
|
+
a single trailing timer for the remainder — so a continuous invalidation stream
|
|
793
|
+
refetches at most once per window.
|
|
776
794
|
*/
|
|
777
795
|
function scheduleInvalidationRefetch(store: CacheStore, entry: CacheEntry): void {
|
|
778
796
|
const policy = entry.invalidation
|
|
@@ -932,12 +950,8 @@ function attachPolicy(
|
|
|
932
950
|
options: CacheOptions | undefined,
|
|
933
951
|
refetch: () => Promise<unknown>,
|
|
934
952
|
): void {
|
|
935
|
-
const policy = options
|
|
936
|
-
if (
|
|
937
|
-
entry.invalidation ||
|
|
938
|
-
!policy ||
|
|
939
|
-
(policy.throttle === undefined && policy.debounce === undefined)
|
|
940
|
-
) {
|
|
953
|
+
const policy = swrWindow(options)
|
|
954
|
+
if (entry.invalidation || !policy) {
|
|
941
955
|
return
|
|
942
956
|
}
|
|
943
957
|
entry.invalidation = { refetch, throttle: policy.throttle, debounce: policy.debounce }
|
|
@@ -39,20 +39,20 @@ into `__SSR__`; ones still pending were consumed via `{#await}` (render emitted
|
|
|
39
39
|
the pending branch without blocking) and stream a resolve chunk instead.
|
|
40
40
|
|
|
41
41
|
`refreshing` flips true while this entry is reloading data it already held —
|
|
42
|
-
either
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
either an `swr` refetch (stale value still visible) or the default
|
|
43
|
+
drop-then-reload (the prior entry was invalidated and dropped, this is its
|
|
44
|
+
replacement read). It backs refreshing(), distinguishing a reload from a
|
|
45
45
|
first-ever load; cleared when the read settles.
|
|
46
46
|
|
|
47
|
-
`invalidation` holds
|
|
48
|
-
|
|
49
|
-
state, so invalidate()
|
|
50
|
-
the
|
|
51
|
-
creating read declared
|
|
52
|
-
an entry that lacks it (hydrated snapshot entries always start without one) —
|
|
53
|
-
first
|
|
54
|
-
|
|
55
|
-
|
|
47
|
+
`invalidation` holds the entry's `swr` policy: the refetch thunk (the call
|
|
48
|
+
captured with its args) plus its optional throttle/debounce window and runtime
|
|
49
|
+
timer state, so invalidate() keeps the stale value and revalidates this key —
|
|
50
|
+
rate-limited by the window — instead of dropping the entry. Set at registration
|
|
51
|
+
when the creating read declared `swr`, or attached by a later read declaring it
|
|
52
|
+
on an entry that lacks it (hydrated snapshot entries always start without one) —
|
|
53
|
+
first wins. An armed `timer` is cleared if the entry is evicted, so a dead key
|
|
54
|
+
never refetches. Wrap-time validation guarantees `swr` never coexists with
|
|
55
|
+
ttl: 0 and never sits on a non-replayable remote method.
|
|
56
56
|
*/
|
|
57
57
|
export type CacheEntry = {
|
|
58
58
|
key: string
|