@rangojs/router 0.25.0 → 0.26.1

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 (79) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
  3. package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
  4. package/dist/types/cache/handle-snapshot.d.ts +2 -2
  5. package/dist/types/cache/shell-snapshot.d.ts +6 -4
  6. package/dist/types/cache/types.d.ts +8 -6
  7. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  8. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  9. package/dist/types/handles/meta.d.ts +5 -0
  10. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  11. package/dist/types/prerender/writable-store.d.ts +2 -3
  12. package/dist/types/router/dev-reload-handoff.d.ts +26 -0
  13. package/dist/types/router/router-interfaces.d.ts +0 -5
  14. package/dist/types/router/router-options.d.ts +0 -20
  15. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  16. package/dist/types/router-versions.d.ts +8 -6
  17. package/dist/types/rsc/shell-capture-constants.d.ts +10 -6
  18. package/dist/types/rsc/shell-capture.d.ts +25 -8
  19. package/dist/types/rsc/types.d.ts +0 -8
  20. package/dist/types/server/build-version-table.d.ts +10 -13
  21. package/dist/types/server/context.d.ts +26 -2
  22. package/dist/types/server/handle-store.d.ts +17 -6
  23. package/dist/types/server/request-context.d.ts +4 -3
  24. package/dist/types/testing/build-versions.d.ts +0 -3
  25. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  26. package/dist/types/testing/serve-shell-request.d.ts +1 -1
  27. package/dist/types/types/loader-types.d.ts +6 -1
  28. package/dist/types/urls/pattern-types.d.ts +7 -6
  29. package/dist/vite/index.js +3 -8
  30. package/package.json +1 -1
  31. package/skills/cache-guide/SKILL.md +4 -8
  32. package/skills/caching/SKILL.md +3 -5
  33. package/skills/comparison/references/framework-comparison.md +1 -1
  34. package/skills/hooks/handle-and-actions.md +6 -6
  35. package/skills/host-router/SKILL.md +1 -1
  36. package/skills/loader/SKILL.md +10 -9
  37. package/skills/ppr/SKILL.md +83 -60
  38. package/skills/prerender/SKILL.md +3 -3
  39. package/skills/router-setup/SKILL.md +0 -2
  40. package/skills/testing/render-handler.md +17 -17
  41. package/skills/testing/serve-shell-request.md +1 -1
  42. package/skills/testing/server-actions.md +16 -16
  43. package/skills/vercel/SKILL.md +1 -1
  44. package/src/cache/cf/cf-cache-store.ts +4 -7
  45. package/src/cache/cf/cf-cache-types.ts +0 -17
  46. package/src/cache/handle-snapshot.ts +2 -2
  47. package/src/cache/shell-snapshot.ts +7 -3
  48. package/src/cache/types.ts +8 -6
  49. package/src/cache/vercel/vercel-cache-store.ts +2 -18
  50. package/src/handles/deferred-resolution.ts +3 -2
  51. package/src/handles/meta.ts +5 -0
  52. package/src/prerender/build-shell-capture.ts +1 -2
  53. package/src/prerender/writable-store.ts +2 -3
  54. package/src/route-definition/dsl-helpers.ts +14 -14
  55. package/src/router/dev-reload-handoff.ts +73 -0
  56. package/src/router/manifest.ts +9 -2
  57. package/src/router/match-middleware/intercept-resolution.ts +2 -2
  58. package/src/router/router-interfaces.ts +0 -6
  59. package/src/router/router-options.ts +0 -21
  60. package/src/router/segment-resolution/loader-cache.ts +4 -3
  61. package/src/router/segment-resolution/mask-nested.ts +5 -3
  62. package/src/router-versions.ts +7 -7
  63. package/src/router.ts +19 -8
  64. package/src/rsc/handler.ts +3 -7
  65. package/src/rsc/rsc-rendering.ts +44 -12
  66. package/src/rsc/shell-capture-constants.ts +10 -6
  67. package/src/rsc/shell-capture.ts +163 -88
  68. package/src/rsc/types.ts +0 -9
  69. package/src/server/build-version-table.ts +12 -24
  70. package/src/server/context.ts +64 -2
  71. package/src/server/handle-store.ts +77 -10
  72. package/src/server/request-context.ts +4 -3
  73. package/src/testing/build-versions.ts +0 -3
  74. package/src/testing/dispatch.ts +1 -3
  75. package/src/testing/internal/seed-vars.ts +4 -4
  76. package/src/testing/serve-shell-request.ts +2 -5
  77. package/src/types/loader-types.ts +6 -1
  78. package/src/urls/pattern-types.ts +7 -6
  79. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
@@ -60,12 +60,13 @@ export interface PartialPrerenderProps {
60
60
  /**
61
61
  * Upper bound (serialized UTF-8 bytes) on the loader pins in the capture
62
62
  * data snapshot: the bake-lane loader records. The doc record (the handler
63
- * layer every HIT replays) is exempt. Over the cap the pins are dropped: the
64
- * shell is still stored and every HIT still replays the handler layer, but the
65
- * pinned loaders read the live store, so drifted cached content can
66
- * hydration-mismatch and be repaired client-side. Reported once per key.
67
- * Defaults to 8 MiB. The whole entry is bounded separately by the store's
68
- * value limit (Cloudflare KV: 25 MiB); an entry over it is refused.
63
+ * layer every HIT replays) is exempt. A document HIT is served only from an
64
+ * entry that holds a pin for every `ssr: false` loader its HTML baked, so
65
+ * over the cap no document shell is stored: documents render in full
66
+ * (MISS), and client navigations still replay the handler layer and run
67
+ * those loaders fresh. Reported once per key. Defaults to 8 MiB. The whole
68
+ * entry is bounded separately by the store's value limit (Cloudflare KV:
69
+ * 25 MiB); an entry over it is refused.
69
70
  */
70
71
  maxSnapshotBytes?: number;
71
72
  /**
@@ -7068,8 +7068,7 @@ var DEFAULT_ROUTER_VERSIONS_KEY = "*";
7068
7068
  function uniformVersions(version) {
7069
7069
  return { data: version, document: version };
7070
7070
  }
7071
- function resolveVersionsFrom(table, routerId2, override) {
7072
- if (override !== void 0) return uniformVersions(override);
7071
+ function resolveVersionsFrom(table, routerId2) {
7073
7072
  if (!table) return void 0;
7074
7073
  const pair = (routerId2 !== void 0 && Object.hasOwn(table, routerId2) ? table[routerId2] : void 0) ?? table[DEFAULT_ROUTER_VERSIONS_KEY];
7075
7074
  return pair ? { data: pair[0], document: pair[1] } : void 0;
@@ -7078,7 +7077,7 @@ function resolveVersionsFrom(table, routerId2, override) {
7078
7077
  // package.json
7079
7078
  var package_default = {
7080
7079
  name: "@rangojs/router",
7081
- version: "0.25.0",
7080
+ version: "0.26.1",
7082
7081
  description: "Django-inspired RSC router with composable URL patterns",
7083
7082
  keywords: [
7084
7083
  "react",
@@ -12257,11 +12256,7 @@ async function runShellPrerenderPhase(s, builder, versionsTable) {
12257
12256
  const mismatches = [];
12258
12257
  for (const [routerId2, routerInstance] of registry) {
12259
12258
  if (typeof routerInstance.match !== "function") continue;
12260
- const versions = resolveVersionsFrom(
12261
- versionsTable,
12262
- routerId2,
12263
- routerInstance.version
12264
- );
12259
+ const versions = resolveVersionsFrom(versionsTable, routerId2);
12265
12260
  try {
12266
12261
  const res = await captureMod.captureShellForBuild({
12267
12262
  router: routerInstance,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.25.0",
3
+ "version": "0.26.1",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -211,17 +211,13 @@ Tag invalidation markers are stored without a version, so `updateTag()` and
211
211
  `revalidateTag()` apply to entries of every version, including a version that
212
212
  comes back in a rollback.
213
213
 
214
- - **Force a clear:** change `version` (`createRouter({ version })`,
215
- `createRSCHandler({ version })`, `new CFCacheStore({ version })` or
216
- `new VercelCacheStore({ version })` all use that exact value for both
217
- versions), or invalidate tags.
218
- - **Keep the old "every deploy clears the cache" behavior:** set `version` to a
219
- per-deploy value (a build id), or on Vercel keep a deployment-scoped
214
+ - **Force a clear:** invalidate tags, or change the router's server code. There
215
+ is no `version` setting; the build versions are the only ones.
216
+ - **Clear on every deploy:** on Vercel, keep a deployment-scoped
220
217
  `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
221
218
  - **Separate environments:** two deployments built from the same code have the
222
219
  same versions. If they share a store (the same KV namespace), they share
223
- entries. Give each environment its own KV namespace or cache namespace, or its
224
- own `version`.
220
+ entries. Give each environment its own KV namespace or cache namespace.
225
221
 
226
222
  Two things give a router a new version on every build, and with it a cold
227
223
  cache on every deploy:
@@ -616,8 +616,7 @@ from that router's built code: segment entries and `fn:` items with the data
616
616
  version, `doc:` responses and `shell2:` shells with the document version. A
617
617
  deploy that changes a router's code reads under a new version; a deploy that does
618
618
  not keeps the entries. Tag invalidation markers carry no version, so `updateTag()`
619
- reaches entries of every version. `version` replaces both versions with one
620
- value. See `/cache-guide` → "Cross-deploy safety" and `/cloudflare` for bindings.
619
+ reaches entries of every version. See `/cache-guide` → "Cross-deploy safety" and `/cloudflare` for bindings.
621
620
 
622
621
  ### With KV L2 Persistence
623
622
 
@@ -859,12 +858,11 @@ global, so they read fresh in any region.
859
858
  Writes over 2 MB (the platform limit, `maxItemBytes`) are skipped, and tags
860
859
  beyond the per-item cap are dropped with a warning. The official client swallows
861
860
  `expireTag` failures, so treat `updateTag()` on Vercel as best-effort. See
862
- `/vercel` for the preset and the remaining options (`version`, `name`, `debug`).
861
+ `/vercel` for the preset and the remaining options (`name`, `debug`).
863
862
 
864
863
  `VercelCacheStore` versions its keys by default: families `s` and `i` take the
865
864
  router's data version, `r` and `h` its document version, and tag markers
866
- (`rg:tm:{tag}`) take none. `version` replaces both. To clear the cache on every
867
- deploy, set `version` to a per-deploy value or keep a deployment-scoped
865
+ (`rg:tm:{tag}`) take none. To clear the cache on every deploy, keep a deployment-scoped
868
866
  `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`.
869
867
 
870
868
  ## Cache purity & tainted objects
@@ -356,7 +356,7 @@ code, and stored HTML (PPR shells, document-cache responses) by the document
356
356
  version. A build whose router code changed cannot replay Flight containing an old
357
357
  component shape or dead client-chunk reference, and a rebuild of unchanged code
358
358
  keeps its cache. Tag invalidation markers carry no version, so `updateTag()`
359
- reaches entries of every version. The `version` option replaces both versions.
359
+ reaches entries of every version.
360
360
  The browser's Rango state also includes the document version, rotating HTTP and
361
361
  in-memory prefetch cache identity on boot.
362
362
 
@@ -46,12 +46,12 @@ belongs to the segment that pushed it and reaches `useHandle` when that push
46
46
  settles. The page never waits for a handle, and a handle never shows the next
47
47
  page's values over the page being left:
48
48
 
49
- | Case | What `useHandle` returns |
50
- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
51
- | Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. |
52
- | Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
53
- | Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
54
- | `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
49
+ | Case | What `useHandle` returns |
50
+ | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
51
+ | Document load | The values the HTML was rendered from. `push(promise)` and `.defer()` are awaited during SSR, so the document ships the resolved value. On a `ppr` shell HIT that holds for the handlers' and the `ssr: false` loaders' pushes, which the shell baked; a live-lane loader's push arrives right after hydration (see `/ppr`). |
52
+ | Navigation to a page that commits at once (a cold click that mounts new segments) | The destination's values, in the page's commit; the page's effects read them |
53
+ | Navigation to a page React holds in a transition (`transition()`, same-route search change, fully prefetched click, a handler that awaits) | The page being left keeps its values; the destination's land in the commit right after the page, in the same task, before paint |
54
+ | `push(promise)` or `.defer()` on a client navigation | The previous value, then the new one when the promise resolves, possibly well after the page |
55
55
 
56
56
  You never see a pending value, a blank, or the destination's title over the old
57
57
  page. A crumb whose `content` is a promise read with `use()` in `<Suspense>`
@@ -382,7 +382,7 @@ Each `createRouter()` app has its own cache version, computed by `vite build` fr
382
382
  | Client code of any app changes | kept for every app | cleared for every app | every app's tabs reload |
383
383
  | Server code shared by A and B changes | cleared for A and B | cleared for A and B | A's and B's tabs reload |
384
384
 
385
- The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, change that app's `version` or invalidate its tags. See `/cache-guide`.
385
+ The host entry (the file that creates the host router and its middleware) is not part of a lazily mounted sub-app's version. If host middleware changes what an app renders from inside a cache boundary, invalidate its tags. See `/cache-guide`.
386
386
 
387
387
  ## Shared cache store
388
388
 
@@ -668,12 +668,13 @@ rejects) skips the whole entry, as above. Entries written
668
668
  before handles were recorded replay none until they are rewritten (expiry or
669
669
  SWR revalidation).
670
670
 
671
- On a `ppr` route the shell keeps a copy of an `ssr: false` loader's settled
672
- pushes too. The two never disagree on the page: where a replay of the shell
673
- serves the loader's data from the shell, the shell's copy is the one shown
674
- and the entry only adds the deferred push the shell could not keep; where it
675
- does not (the loader runs or reads its entry), the entry's pushes take the
676
- shell copy's place, none if the entry recorded none. See `/ppr` → "On a shell
671
+ On a `ppr` route the shell keeps a copy of an `ssr: false` loader's pushes
672
+ too, a pushed promise resolved (#1182). The two never disagree on the page:
673
+ where a replay of the shell serves the loader's data from the shell, the
674
+ shell's copy is the one shown and the entry only adds a push whose value
675
+ holds a promise, which the shell could not keep; where it does not (the
676
+ loader runs or reads its entry), the entry's pushes take the shell copy's
677
+ place, none if the entry recorded none. See `/ppr` → "Handles on a shell
677
678
  HIT".
678
679
 
679
680
  ### Cache Key
@@ -1040,9 +1041,9 @@ handler reserved with `.defer()` instead (`/breadcrumbs`, "Deferred content").
1040
1041
 
1041
1042
  Under `cache()`, loader pushes are not stored with the cached segments: the
1042
1043
  loader re-runs on every hit and its live push is the only copy. (`ppr` keeps
1043
- the settled pushes of an `ssr: false` loader, and of the loaders it awaits, in
1044
- the shell, and a promise-free `ssr: false` loader does not run on a shell HIT —
1045
- see `/ppr`.)
1044
+ the pushes of an `ssr: false` loader, and of the loaders it awaits, in the
1045
+ shell, a pushed promise resolved, and a promise-free `ssr: false` loader does
1046
+ not run on a shell HIT — see `/ppr`.)
1046
1047
 
1047
1048
  Reads are the other direction and gated: `ctx.get(handle)` throws unless the
1048
1049
  loader first does `await ctx.rendered()` (DSL-registered loaders only —
@@ -372,7 +372,8 @@ replay its eligible snapshot. In both cases `matchPartial()`:
372
372
  - runs live DSL loaders fresh with their normal `loading()` streaming
373
373
  behavior, and serves bake-lane loaders from the pins of a shell captured by a
374
374
  document request; a snapshot captured by a navigation alone carries no pins,
375
- and its replay runs them fresh (see below);
375
+ nor does the navigation-only entry a capture over `ppr.maxSnapshotBytes`
376
+ stores, and their replay runs them fresh (see below);
376
377
  - keeps the existing prefetch key, source scope, and in-flight lock unchanged.
377
378
 
378
379
  This is deliberately invisible to the browser: the response is the same
@@ -382,8 +383,9 @@ snapshot is a shell a document request captured, as on a document HIT: a
382
383
  promise-free `ssr: false` loader is served from its pin without running
383
384
  (unless the capture marked it to run: see "On a shell HIT"), and one whose
384
385
  return holds promises runs with its baked parts overlaid. A snapshot captured
385
- by a navigation alone carries no pins, and its replay runs them fresh. Loaders
386
- without `ssr: false` stay live.
386
+ by a navigation alone carries no pins, nor does an entry over
387
+ `ppr.maxSnapshotBytes`, and its replay runs them fresh. Loaders without
388
+ `ssr: false` stay live.
387
389
 
388
390
  A route's own `cache()` scope — including one inherited from an ancestor, the
389
391
  common app-wide storefront shape — COMPOSES with replay instead of disabling
@@ -836,14 +838,17 @@ that settled inside the capture window pinned its capture-time value into the
836
838
  shared shell; found live as a storefront basket — with the capturing session's
837
839
  identifiers — served to anonymous visitors.)
838
840
 
839
- ### Handles: handler pushes bake; loader pushes keep "nesting = liveness"
841
+ ### Handles: handler and bake-lane pushes bake; loader pushes keep "nesting = liveness"
840
842
 
841
843
  - `ctx.use(H)(promise)` from a handler — a TOP-LEVEL pushed promise is awaited
842
844
  at capture and BAKED into the shell (bounded by `ppr.captureTimeout`, 15s by
843
845
  default).
844
- - A promise pushed from a LOADER is never in the shell and the capture does not
845
- wait for it: however slow it is (or if it never settles), the shell is still
846
- stored, and the HIT's own loader run delivers the value after hydration.
846
+ - `ctx.use(H)(promise)` (or `.defer()`) from an `ssr: false` LOADER, or a
847
+ loader it awaits — awaited at capture and BAKED the same way, judged by the
848
+ value it settles to, as `push(await promise)` would be (#1182). One that
849
+ rejects, or does not settle within `ppr.captureTimeout`, refuses the
850
+ capture, as a handler's does. A loader without `ssr: false` never runs at
851
+ capture, so its pushes are never in the shell.
847
852
  - `ctx.use(H)({ x: promise })` from a HANDLER — also baked. The capture waits
848
853
  for every promise nested in a handler's push, at any depth of plain objects,
849
854
  arrays and JSX props, before it records the handles every HIT replays. Keep
@@ -921,7 +926,9 @@ by a navigation alone carries no pins, and its replay runs these loaders fresh.
921
926
  fill the holes — and the recorded baked paths are overlaid onto the fresh
922
927
  result, so the payload matches the prelude while the nested promises stream
923
928
  fresh. A fresh rejection skips the overlay and goes to the loader's error
924
- boundary. Give it its own cache,
929
+ boundary: on a document HIT the payload then carries the error where the
930
+ HTML has the baked value, and React renders that part again on the client
931
+ (one hydration mismatch, the one known residual of a HIT). Give it its own cache,
925
932
  `loader(Def, { ssr: false }, () => [cache({ ttl })])`, so that run reads
926
933
  through the loader cache.
927
934
 
@@ -982,9 +989,12 @@ loader running on the replay awaits it. On a document HIT the page hydrates
982
989
  with the shell's copy and the hole's value takes its place after hydration
983
990
  ("Handles on a shell HIT" below). An
984
991
  `ssr: false` loader is served from the shell only while the entry holds its
985
- pin: a snapshot captured by a navigation alone carries none, and a shell over
986
- `ppr.maxSnapshotBytes` drops them. There the loader runs fresh, and its
987
- pushes are that run's, like its data.
992
+ pin: a snapshot captured by a navigation alone carries none, and a capture
993
+ over `ppr.maxSnapshotBytes` stores the navigation half only, without them.
994
+ There a navigation runs the loader fresh, and its pushes are that run's, like
995
+ its data. A document HIT is served only from an entry that holds a pin for
996
+ every `ssr: false` loader its HTML baked, so a document never hydrates one
997
+ loader's run over HTML rendered from the capture's value.
988
998
 
989
999
  A `Prerender` + `ppr` page follows the same table. Its handler layer comes
990
1000
  from the prerendered payload, which never holds a loader push (loaders do not
@@ -994,63 +1004,76 @@ on its own, and a HIT restores them as above (#1057).
994
1004
  A dependency the route does not register, which an `ssr: false` loader
995
1005
  awaited at capture outside any live loader, is on neither lane: its settled
996
1006
  pushes stay as the prelude rendered them, even where a live loader also
997
- awaits it, while its data is fresh. (They follow the run instead once the
998
- entry has lost a pin, because the loader that awaits it then runs.) To keep
1007
+ awaits it, while its data is fresh. (They follow the run instead when the
1008
+ replayed entry has no pin for the loader that awaits it, because that loader
1009
+ then runs.) To keep
999
1010
  its pushes live, declare it as its own `loader()` on the route. A push the
1000
- capture cannot record (a deferred push, or one holding a promise) marks every
1001
- loader record of the page to run: those bodies then still run on each replay,
1002
- in the background, and that push reaches the page (from the loader's own
1003
- `cache()` entry when it hits) with the run's value, on a document HIT and on
1004
- a client navigation alike. On a document HIT it arrives after hydration.
1005
-
1006
- **Handles on a shell HIT.** A promise pushed from a loader is live and is
1007
- never in a shell. To have a value in the shell's HTML, push it settled
1008
- (await it) from the `ssr: false` loader, or push it from the handler.
1009
-
1010
- Three things follow, in this order of scope:
1011
-
1012
- 1. **What is in a shell's HTML** changes only for promise pushes of
1013
- `ssr: false` loaders and of the loaders they await: `push(promise)`, or a
1014
- pushed value that holds a promise. Those run at capture, and the shell
1015
- renders no element for such a push. A loader without `ssr: false` never
1016
- ran at capture, so nothing of it was ever in the shell.
1017
- 2. **On a shell HIT every loader push made during the request arrives after
1011
+ capture cannot record (one whose value, once settled, holds a promise) marks
1012
+ every loader record of the page to run: those bodies then still run on each
1013
+ replay, in the background, and that push reaches the page (from the loader's
1014
+ own `cache()` entry when it hits) with the run's value, on a document HIT and
1015
+ on a client navigation alike. On a document HIT it arrives after hydration.
1016
+
1017
+ **Handles on a shell HIT.** The shell holds what its capture's document
1018
+ rendered: the handlers' pushes and the `ssr: false` loaders' pushes, a
1019
+ promise (`push(promise)`, `.defer()`) resolved. A pushed promise is judged by
1020
+ the value it settles to, as `push(await promise)` would be (#1182). A
1021
+ live-lane loader's push is never in a shell.
1022
+
1023
+ Four things follow, in this order of scope:
1024
+
1025
+ 1. **What is in a shell's HTML.** The capture awaits a promise an
1026
+ `ssr: false` loader (or a loader it awaits) pushes, under
1027
+ `ppr.captureTimeout`, and bakes the value. One that settles to a value
1028
+ holding a promise is live: the shell renders no element for it. A loader
1029
+ without `ssr: false` never runs at capture, so nothing of it is ever in
1030
+ the shell.
1031
+ 2. **A pushed promise that fails refuses the capture.** One that rejects, or
1032
+ does not settle within `ppr.captureTimeout`, keeps the route on MISS, as
1033
+ a handler's does, and the warning names the loader and the handle. Catch
1034
+ it and push a fallback, or push it from a live-lane loader.
1035
+ 3. **On a shell HIT every loader push made during the request arrives after
1018
1036
  hydration**, a live-lane loader's included. The document hydrates with
1019
1037
  exactly the handle data the shell's HTML was rendered from (the handlers'
1020
- pushes and the settled pushes of the loaders that ran at capture), and
1021
- the request's own pushes follow as a normal `useHandle` update.
1022
- 3. **`useHandle` on every document, `ppr` or not:** a reader that React is
1038
+ pushes and the pushes of the loaders that ran at capture), and the
1039
+ request's own pushes follow as a normal `useHandle` update.
1040
+ 4. **`useHandle` on every document, `ppr` or not:** a reader that React is
1023
1041
  hydrating reads the handle data its HTML was rendered from, wherever its
1024
1042
  boundary hydrates, and takes the newer data right after. So a reader
1025
1043
  inside the `loading()` or `<Suspense>` of a loader that pushes after an
1026
1044
  `await` hydrates clean.
1027
1045
 
1028
- | On the HIT's request | In the HIT's HTML | The client |
1029
- | --------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------- |
1030
- | a handler's push, a pinned `ssr: false` loader's settled push | yes (the shell has it) | hydrates with it |
1031
- | a deferred push (`push(promise)`), or a push holding a promise, by a loader | no | gets it right after hydration |
1032
- | a live-lane loader's push | no | gets it right after hydration |
1033
- | the run of an `ssr: false` loader the shell has no pin for | the capture's push | hydrates with the capture's, then shows the run's |
1046
+ | On the HIT's request | In the HIT's HTML | The client |
1047
+ | ------------------------------------------------------------------------------------------ | ---------------------- | ----------------------------- |
1048
+ | a handler's push, a pinned `ssr: false` loader's push (`push(promise)` and `.defer()` too) | yes (the shell has it) | hydrates with it |
1049
+ | a push by an `ssr: false` loader whose value holds a promise | no | gets it right after hydration |
1050
+ | a live-lane loader's push | no | gets it right after hydration |
1034
1051
 
1035
- A value a loader pushes as a promise is therefore in the HTML of a document
1036
- MISS (the render awaits it) and of a route without `ppr`, and not in the
1037
- HTML of a shell HIT. When a crawler or the first paint needs it, change one
1038
- line:
1052
+ So a title an `ssr: false` loader pushes to `Meta`, promise or not, is in
1053
+ every HIT's `<head>`: a crawler and a link preview see it. It bakes with the
1054
+ shell and is invalidated with it (its TTL, its tags, `updateTag()`). A value
1055
+ that must stay per request comes from a loader without `ssr: false`, which
1056
+ nothing has to read:
1039
1057
 
1040
1058
  ```ts
1041
- ctx.use(Notes)(db.stockNote(id)); // a promise: live, after hydration on a HIT
1042
- ctx.use(Notes)(await db.stockNote(id)); // settled: baked into the shell
1059
+ const StockNote = createLoader(async (ctx) => {
1060
+ ctx.use(Notes)(db.stockNote(ctx.params.id)); // live: after hydration on a HIT
1061
+ return null;
1062
+ });
1063
+ path("/product/:id", ProductPage, { name: "product", ppr: true }, () => [
1064
+ loader(Product, { ssr: false }), // bakes: its data and its pushes
1065
+ loader(StockNote), // live lane: runs on every request
1066
+ ]);
1043
1067
  ```
1044
1068
 
1045
- The settled push bakes with the shell and is invalidated with it (its TTL,
1046
- its tags). A promise is not baked, whenever it settles: it is the shape that
1047
- says "per request", the same as a promise in the loader's data.
1069
+ Or nest the value in a settled push (`{ label, count: promise }`): the whole
1070
+ push then stays live, and every pinned body of the page runs on each HIT.
1048
1071
 
1049
1072
  A pinned loader's pushes are exactly the ones its capture made. If its body
1050
- pushes a settled value only on some runs (behind a condition), a replay that
1051
- runs it shows the capture's answer, a push or none, never a later run's next
1052
- to the pinned data. A deferred push is the exception by design: it is
1053
- delivered on every replay.
1073
+ pushes a value only on some runs (behind a condition), a replay that runs it
1074
+ shows the capture's answer, a push or none, never a later run's next to the
1075
+ pinned data. A push whose value holds a promise is the exception by design:
1076
+ it is delivered on every replay.
1054
1077
 
1055
1078
  One arrangement mixes sources by design. A loader that reads a
1056
1079
  `cache()`-bound loader with `ctx.use()` before that binding starts (a loader
@@ -1392,13 +1415,13 @@ path(
1392
1415
  );
1393
1416
  ```
1394
1417
 
1395
- | Field | Default | Notes |
1396
- | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1397
- | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1398
- | `swr` | — | stale window: serve the stale shell + background recapture |
1399
- | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1400
- | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. Over the cap the pins are dropped and the shell is stored with its handler layer (warned once per key); bake-lane loaders then run on every replay and read the live store, their handle pushes come from that run like their data, and drift from the prelude is repaired client-side |
1401
- | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1418
+ | Field | Default | Notes |
1419
+ | ------------------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1420
+ | `ttl` | `300` | shell freshness window in seconds (`ppr: true` uses the default) |
1421
+ | `swr` | — | stale window: serve the stale shell + background recapture |
1422
+ | `tags` | — | operational tags UNIONED with the tags the capture collects — see "Invalidation" below |
1423
+ | `maxSnapshotBytes` | 8 MiB | cap on the loader pins in the entry's snapshot (the bake-lane loader records). The recorded handler layer is exempt. A document HIT is served only from an entry that holds a pin for every `ssr: false` loader its HTML baked, so over the cap no document shell is stored: the entry keeps the handler layer only, documents are a MISS (full SSR, no hydration error, no remount), client navigations still replay the handler layer and run the bake-lane loaders fresh, and the URL backs off like a refused capture. Warned once per key: "the document shell was not stored; raise maxSnapshotBytes or shrink the ssr: false data" |
1424
+ | `captureTimeout` | 15000ms | ONE capture deadline: the handler layer settling (promises it passes or pushes, async server components, loaders it awaits), bake-lane loaders, and the prerender; a capture that misses it stores nothing rather than a partial shell |
1402
1425
 
1403
1426
  A shell never outlives the route `cache()` entry it was captured from: under
1404
1427
  a route `cache()`, the shell is fresh no longer than that entry is (at most
@@ -1329,9 +1329,9 @@ write that started before an invalidation of one of its tags is refused.
1329
1329
  ### Limits worth knowing
1330
1330
 
1331
1331
  - **Only the router's own handler.** A warm goes through `router.fetch`, the
1332
- handler `createRouter`'s `cache`, `nonce` and `version` configure. An app
1333
- entry that builds its own `createRSCHandler({ cache, version })` is not what
1334
- a warm runs: put those options on `createRouter`.
1332
+ handler `createRouter`'s `cache` and `nonce` configure. An app
1333
+ entry that builds its own `createRSCHandler({ cache })` is not what a warm
1334
+ runs: put the option on `createRouter`.
1335
1335
  - **Document records only.** Navigation (`partial:`) and intercept records
1336
1336
  fill on the first navigation.
1337
1337
  - **One location at a time.** A warm does not reach a copy another location
@@ -266,8 +266,6 @@ Notes:
266
266
  a cleared cache) on every build; the build prints one note when that applies.
267
267
  A router that renders no such action keeps its cache without one (`/use-cache`,
268
268
  `/cache-guide`).
269
- - `createRouter({ version })` takes effect on every preset, including `node`
270
- and `vercel`. It replaces both computed cache versions with that exact value.
271
269
  - `@rangojs/router/vite` also exports `poke()`, a dev-server plugin: type `e`
272
270
  then Enter (or Ctrl+R where the terminal passes it through) to full-reload
273
271
  the browser.
@@ -8,23 +8,23 @@ A Rango route handler is a pure function `(ctx) => rsc` — the function you pas
8
8
 
9
9
  ### Options — `RenderHandlerOptions`
10
10
 
11
- | Field | Type | Meaning |
12
- | ---------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
14
- | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
15
- | `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
16
- | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. Used only when `request` is a string or omitted; a passed `Request` keeps its own headers. |
17
- | `build` | `boolean` | Seed `ctx.build` (default `false`) to test a handler that branches on the build-time pass, including calling `ctx.dynamic()` on a miss. Assert the opt-out via `result.dynamic`. |
18
- | `theme` | `ThemeConfig \| true` | Theme config (`createRouter({ theme })` shape). Without it `ctx.theme`/`ctx.setTheme` are inert; pass one to test a handler that reads the theme or writes the theme cookie. |
19
- | `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
20
- | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
21
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
- | `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
23
- | `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
24
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; the value is `{version}:{timestamp}`, default `"0"`). |
25
- | `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
26
- | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
27
- | `inActionRevalidation` | `boolean` | Render as if inside a server action's revalidation render (production sets this in `revalidateAfterAction`). A stale `"use cache"` entry whose profile sets `foregroundOnAction: true` then re-executes in the FOREGROUND (fresh result in this render) instead of SWR. Pair with `cacheStore` + `cacheProfiles` to exercise the opt-in. |
11
+ | Field | Type | Meaning |
12
+ | ---------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `params` | `Record<string, string>` | Route params surfaced as `ctx.params`. |
14
+ | `env` | `TEnv` | Environment bindings surfaced as `ctx.env`. |
15
+ | `request` | `Request \| string` | Backing Request (string or `Request`); defaults to a localhost GET. |
16
+ | `headers` | `HeadersInit` | Request headers (e.g. `Cookie`) the handler reads via `cookies()`. Used only when `request` is a string or omitted; a passed `Request` keeps its own headers. |
17
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) to test a handler that branches on the build-time pass, including calling `ctx.dynamic()` on a miss. Assert the opt-out via `result.dynamic`. |
18
+ | `theme` | `ThemeConfig \| true` | Theme config (`createRouter({ theme })` shape). Without it `ctx.theme`/`ctx.setTheme` are inert; pass one to test a handler that reads the theme or writes the theme cookie. |
19
+ | `vars` | `VarsInit` (object or `[[Var, value]]` tuples) | Variables a prior middleware set, read via `ctx.get(...)`. |
20
+ | `routeName` | `string` | Matched route name (drives `ctx.routeName` and scoped reverse). |
21
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()`. |
22
+ | `loaders` | `ReadonlyArray<readonly [LoaderDefinition, unknown]>` | Seed the data `ctx.use(SomeLoader)` returns. Matched by loader reference; NO real loader runs. |
23
+ | `clientComponents` | `Record<string, unknown>` | `"use client"` components in the handler's RSC, so they serialize as real boundaries when `rangoUseClientTransform()` is not wired. Keyed by name. |
24
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie a handler calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production rather than no-opping; override `prefix`/`routerId` to match your `createRouter({ stateCookiePrefix, id })`, or `version` (a seeded app version: the value is `{version}:{timestamp}`, default `"0"`). |
25
+ | `cacheStore` | `SegmentCacheStore` | Segment cache store backing a `"use cache"` function the handler invokes (e.g. `new MemorySegmentCacheStore()`). WITHOUT it, `registerCachedFunction` takes the uncached bypass and the cached path is NOT exercised (the runtime emits a one-time warning under the test runner). Pair with `cacheProfiles`. |
26
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Cache profiles in the `createRouter({ cacheProfiles })` shape, required for `"use cache: profileName"` resolution once a `cacheStore` is wired. |
27
+ | `inActionRevalidation` | `boolean` | Render as if inside a server action's revalidation render (production sets this in `revalidateAfterAction`). A stale `"use cache"` entry whose profile sets `foregroundOnAction: true` then re-executes in the FOREGROUND (fresh result in this render) instead of SWR. Pair with `cacheStore` + `cacheProfiles` to exercise the opt-in. |
28
28
 
29
29
  ### Context — `HandlerContext` (what your handler receives)
30
30
 
@@ -249,7 +249,7 @@ Vitest runs no Vite id transform, so give each router an explicit `id` when a te
249
249
  | `hydration` | What the document hydrates with (`metadata.handles`, read to its end before hydration starts). For a `partial` request, the last state it streamed. |
250
250
  | `late` | The states that arrived on the late channel (`metadata.handlesLate`), in order. Each replaces the client's handle data after hydration. Often `[]`. |
251
251
 
252
- On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds, and whatever this request's loaders push, replace or drop is in `late` (see `/ppr`, "Handles on a shell HIT"):
252
+ On a HIT the client hydrates with exactly what the shell was rendered from, so `hydration` is also what the shell's HTML holds (the handlers' pushes and the `ssr: false` loaders', a pushed promise resolved), and whatever this request's loaders push, replace or drop is in `late` (see `/ppr`, "Handles on a shell HIT"):
253
253
 
254
254
  ```ts
255
255
  // The handler pushed ctx.use(Notes)("from-handler"); a live loader pushes