@rangojs/router 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) 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/types.d.ts +8 -6
  6. package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
  7. package/dist/types/handles/deferred-resolution.d.ts +3 -2
  8. package/dist/types/handles/meta.d.ts +5 -0
  9. package/dist/types/prerender/build-shell-capture.d.ts +1 -2
  10. package/dist/types/prerender/writable-store.d.ts +2 -3
  11. package/dist/types/router/router-interfaces.d.ts +0 -5
  12. package/dist/types/router/router-options.d.ts +0 -20
  13. package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
  14. package/dist/types/router-versions.d.ts +8 -6
  15. package/dist/types/rsc/shell-capture-constants.d.ts +4 -3
  16. package/dist/types/rsc/shell-capture.d.ts +16 -2
  17. package/dist/types/rsc/types.d.ts +0 -8
  18. package/dist/types/server/build-version-table.d.ts +10 -13
  19. package/dist/types/server/context.d.ts +26 -2
  20. package/dist/types/server/handle-store.d.ts +17 -6
  21. package/dist/types/server/request-context.d.ts +4 -3
  22. package/dist/types/testing/build-versions.d.ts +0 -3
  23. package/dist/types/testing/internal/seed-vars.d.ts +4 -4
  24. package/dist/types/testing/serve-shell-request.d.ts +1 -1
  25. package/dist/types/types/loader-types.d.ts +6 -1
  26. package/dist/vite/index.js +3 -8
  27. package/package.json +1 -1
  28. package/skills/cache-guide/SKILL.md +4 -8
  29. package/skills/caching/SKILL.md +3 -5
  30. package/skills/comparison/references/framework-comparison.md +1 -1
  31. package/skills/hooks/handle-and-actions.md +6 -6
  32. package/skills/host-router/SKILL.md +1 -1
  33. package/skills/loader/SKILL.md +10 -9
  34. package/skills/ppr/SKILL.md +60 -44
  35. package/skills/prerender/SKILL.md +3 -3
  36. package/skills/router-setup/SKILL.md +0 -2
  37. package/skills/testing/render-handler.md +17 -17
  38. package/skills/testing/serve-shell-request.md +1 -1
  39. package/skills/testing/server-actions.md +16 -16
  40. package/skills/vercel/SKILL.md +1 -1
  41. package/src/cache/cf/cf-cache-store.ts +4 -7
  42. package/src/cache/cf/cf-cache-types.ts +0 -17
  43. package/src/cache/handle-snapshot.ts +2 -2
  44. package/src/cache/types.ts +8 -6
  45. package/src/cache/vercel/vercel-cache-store.ts +2 -18
  46. package/src/handles/deferred-resolution.ts +3 -2
  47. package/src/handles/meta.ts +5 -0
  48. package/src/prerender/build-shell-capture.ts +1 -2
  49. package/src/prerender/writable-store.ts +2 -3
  50. package/src/route-definition/dsl-helpers.ts +14 -14
  51. package/src/router/manifest.ts +9 -2
  52. package/src/router/match-middleware/intercept-resolution.ts +2 -2
  53. package/src/router/router-interfaces.ts +0 -6
  54. package/src/router/router-options.ts +0 -21
  55. package/src/router/segment-resolution/loader-cache.ts +4 -3
  56. package/src/router/segment-resolution/mask-nested.ts +5 -3
  57. package/src/router-versions.ts +7 -7
  58. package/src/router.ts +3 -8
  59. package/src/rsc/handler.ts +3 -7
  60. package/src/rsc/shell-capture-constants.ts +4 -3
  61. package/src/rsc/shell-capture.ts +126 -51
  62. package/src/rsc/types.ts +0 -9
  63. package/src/server/build-version-table.ts +12 -24
  64. package/src/server/context.ts +64 -2
  65. package/src/server/handle-store.ts +77 -10
  66. package/src/server/request-context.ts +4 -3
  67. package/src/testing/build-versions.ts +0 -3
  68. package/src/testing/dispatch.ts +1 -3
  69. package/src/testing/internal/seed-vars.ts +4 -4
  70. package/src/testing/serve-shell-request.ts +2 -5
  71. package/src/types/loader-types.ts +6 -1
  72. package/src/vite/discovery/shell-prerender-phase.ts +1 -5
@@ -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 —
@@ -836,14 +836,17 @@ that settled inside the capture window pinned its capture-time value into the
836
836
  shared shell; found live as a storefront basket — with the capturing session's
837
837
  identifiers — served to anonymous visitors.)
838
838
 
839
- ### Handles: handler pushes bake; loader pushes keep "nesting = liveness"
839
+ ### Handles: handler and bake-lane pushes bake; loader pushes keep "nesting = liveness"
840
840
 
841
841
  - `ctx.use(H)(promise)` from a handler — a TOP-LEVEL pushed promise is awaited
842
842
  at capture and BAKED into the shell (bounded by `ppr.captureTimeout`, 15s by
843
843
  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.
844
+ - `ctx.use(H)(promise)` (or `.defer()`) from an `ssr: false` LOADER, or a
845
+ loader it awaits — awaited at capture and BAKED the same way, judged by the
846
+ value it settles to, as `push(await promise)` would be (#1182). One that
847
+ rejects, or does not settle within `ppr.captureTimeout`, refuses the
848
+ capture, as a handler's does. A loader without `ssr: false` never runs at
849
+ capture, so its pushes are never in the shell.
847
850
  - `ctx.use(H)({ x: promise })` from a HANDLER — also baked. The capture waits
848
851
  for every promise nested in a handler's push, at any depth of plain objects,
849
852
  arrays and JSX props, before it records the handles every HIT replays. Keep
@@ -997,60 +1000,73 @@ pushes stay as the prelude rendered them, even where a live loader also
997
1000
  awaits it, while its data is fresh. (They follow the run instead once the
998
1001
  entry has lost a pin, because the loader that awaits it then runs.) To keep
999
1002
  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
1003
+ capture cannot record (one whose value, once settled, holds a promise) marks
1004
+ every loader record of the page to run: those bodies then still run on each
1005
+ replay, in the background, and that push reaches the page (from the loader's
1006
+ own `cache()` entry when it hits) with the run's value, on a document HIT and
1007
+ on a client navigation alike. On a document HIT it arrives after hydration.
1008
+
1009
+ **Handles on a shell HIT.** The shell holds what its capture's document
1010
+ rendered: the handlers' pushes and the `ssr: false` loaders' pushes, a
1011
+ promise (`push(promise)`, `.defer()`) resolved. A pushed promise is judged by
1012
+ the value it settles to, as `push(await promise)` would be (#1182). A
1013
+ live-lane loader's push is never in a shell.
1014
+
1015
+ Four things follow, in this order of scope:
1016
+
1017
+ 1. **What is in a shell's HTML.** The capture awaits a promise an
1018
+ `ssr: false` loader (or a loader it awaits) pushes, under
1019
+ `ppr.captureTimeout`, and bakes the value. One that settles to a value
1020
+ holding a promise is live: the shell renders no element for it. A loader
1021
+ without `ssr: false` never runs at capture, so nothing of it is ever in
1022
+ the shell.
1023
+ 2. **A pushed promise that fails refuses the capture.** One that rejects, or
1024
+ does not settle within `ppr.captureTimeout`, keeps the route on MISS, as
1025
+ a handler's does, and the warning names the loader and the handle. Catch
1026
+ it and push a fallback, or push it from a live-lane loader.
1027
+ 3. **On a shell HIT every loader push made during the request arrives after
1018
1028
  hydration**, a live-lane loader's included. The document hydrates with
1019
1029
  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
1030
+ pushes and the pushes of the loaders that ran at capture), and the
1031
+ request's own pushes follow as a normal `useHandle` update.
1032
+ 4. **`useHandle` on every document, `ppr` or not:** a reader that React is
1023
1033
  hydrating reads the handle data its HTML was rendered from, wherever its
1024
1034
  boundary hydrates, and takes the newer data right after. So a reader
1025
1035
  inside the `loading()` or `<Suspense>` of a loader that pushes after an
1026
1036
  `await` hydrates clean.
1027
1037
 
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 |
1038
+ | On the HIT's request | In the HIT's HTML | The client |
1039
+ | ------------------------------------------------------------------------------------------ | ---------------------- | ------------------------------------------------- |
1040
+ | a handler's push, a pinned `ssr: false` loader's push (`push(promise)` and `.defer()` too) | yes (the shell has it) | hydrates with it |
1041
+ | a push by an `ssr: false` loader whose value holds a promise | no | gets it right after hydration |
1042
+ | a live-lane loader's push | no | gets it right after hydration |
1043
+ | 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 |
1034
1044
 
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:
1045
+ So a title an `ssr: false` loader pushes to `Meta`, promise or not, is in
1046
+ every HIT's `<head>`: a crawler and a link preview see it. It bakes with the
1047
+ shell and is invalidated with it (its TTL, its tags, `updateTag()`). A value
1048
+ that must stay per request comes from a loader without `ssr: false`, which
1049
+ nothing has to read:
1039
1050
 
1040
1051
  ```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
1052
+ const StockNote = createLoader(async (ctx) => {
1053
+ ctx.use(Notes)(db.stockNote(ctx.params.id)); // live: after hydration on a HIT
1054
+ return null;
1055
+ });
1056
+ path("/product/:id", ProductPage, { name: "product", ppr: true }, () => [
1057
+ loader(Product, { ssr: false }), // bakes: its data and its pushes
1058
+ loader(StockNote), // live lane: runs on every request
1059
+ ]);
1043
1060
  ```
1044
1061
 
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.
1062
+ Or nest the value in a settled push (`{ label, count: promise }`): the whole
1063
+ push then stays live, and every pinned body of the page runs on each HIT.
1048
1064
 
1049
1065
  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.
1066
+ pushes a value only on some runs (behind a condition), a replay that runs it
1067
+ shows the capture's answer, a push or none, never a later run's next to the
1068
+ pinned data. A push whose value holds a promise is the exception by design:
1069
+ it is delivered on every replay.
1054
1070
 
1055
1071
  One arrangement mixes sources by design. A loader that reads a
1056
1072
  `cache()`-bound loader with `ctx.use()` before that binding starts (a loader
@@ -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
@@ -8,22 +8,22 @@
8
8
 
9
9
  ### Options — `CreateTestContextOptions<TEnv>`
10
10
 
11
- | Field | Type | Meaning |
12
- | --------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
- | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
- | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
- | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
- | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
- | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
- | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
- | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
- | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
- | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
- | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
- | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
- | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
- | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
- | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (the app's version, as `createRouter({ version })` would set it; value is `{version}:{timestamp}`, default `"0"`). |
11
+ | Field | Type | Meaning |
12
+ | --------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
13
+ | `env` | `TEnv` | Platform bindings the action reads (`ctx.env`). Default `{}`. Double them yourself (see `./bindings.md`). |
14
+ | `request` | `Request \| string` | The request to run under. A `string` becomes `new Request(url)`; pass a full `Request` to seed a `Cookie` header. Default origin `http://localhost/`. |
15
+ | `requestInit` | `RequestInit` | Init merged when `request` is a string (e.g. `{ method, headers, body }`). |
16
+ | `variables` | `Record<string, unknown>` | Raw backing store for `ctx.get()` / `ctx.set()`, pre-seeded from `vars`. |
17
+ | `vars` | `VarsInit` | Vars a prior middleware would have set (object or `[token, value]` list). |
18
+ | `routeMap` | `Record<string, string>` | Route name -> pattern map enabling `ctx.reverse()` without global state. |
19
+ | `routeName` | `string` | Current route name (drives `ctx.reverse()` self-references). |
20
+ | `params` | `Record<string, string>` | Route params on `ctx.params`. |
21
+ | `basename` | `string` | Router basename, normalized exactly like `createRouter({ basename })`; drives `redirect()` prefixing. Default `undefined`. |
22
+ | `cacheStore` | `SegmentCacheStore` | Backing store for `use cache` functions (same shape as `createRouter({ cache })`). Without it, cached functions run uncached and their guards never fire. |
23
+ | `cacheProfiles` | `Record<string, CacheProfile>` | Profiles for `use cache: "name"`, same shape as `createRouter({ cacheProfiles })`. An unknown profile throws. |
24
+ | `theme` | `ThemeConfig \| true` | Theme config (same shape as `createRouter({ theme })`). Without it `ctx.theme` / `ctx.setTheme` are inert. |
25
+ | `build` | `boolean` | Seed `ctx.build` (default `false`) for code that branches on the build-time PPR shell-capture pass. With `true`, `ctx.waitUntil()` is inert, as at build time. |
26
+ | `stateCookie` | `StateCookieSeed` (`{ prefix?, routerId?, version? }`) | Customize the rango state cookie an action calling `invalidateClientCache()` rotates. The name is ALWAYS seeded (default `rango-state_router_0`) so the rotation `Set-Cookie` fires like production; override `prefix`/`routerId` to match `createRouter({ stateCookiePrefix, id })`, or `version` (a seeded app version: the value is `{version}:{timestamp}`, default `"0"`). |
27
27
 
28
28
  ### Context — `RequestContext<TEnv>` (what your code receives)
29
29
 
@@ -120,7 +120,7 @@ The cache factory receives `(env, ctx)`: on Vercel `env` is `process.env` and `c
120
120
 
121
121
  `router.prerender()` can warm a route into this store before traffic (`/prerender` → "Warm any route before traffic"). The Runtime Cache is per region, so `VercelCacheStore` declares `scope: "regional"`: a warm fills the region the call runs in. That is all traffic when the project's functions run in one region (the default `iad1` for new projects, and every Hobby project); on a multi-region project it warms one region and the others fill on first use. The router does not detect the region count. The memory store used off-platform above is refused in production and allowed under the dev server.
122
122
 
123
- `getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. `version` on the store (or on `createRouter`) replaces both versions. To clear the cache on every deploy, set `version` to a per-deploy value or keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace or `version`.
123
+ `getCache()` takes no deployment id: `VercelCacheStore` puts a version in front of every key itself (data version for families `s`, `i`; document version for `r`, `h`; none for tag markers), computed per `createRouter()` from its built code. A deploy that does not change a router keeps its entries; a deploy that changes it reads under new keys. To clear the cache on every deploy, keep a deployment-scoped `getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID })`. Two environments built from the same code share entries if they share a cache namespace; give each its own namespace.
124
124
 
125
125
  ## Host routers (multi-app)
126
126
 
@@ -500,8 +500,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
500
500
  private readonly namespace?: string;
501
501
  private readonly explicitBaseUrl?: string;
502
502
  private readonly waitUntil?: (fn: () => Promise<void>) => void;
503
- /** The `version` option: used for every versioned family when set. */
504
- private readonly explicitVersion?: string;
505
503
  private readonly edgeLookupTimeoutMs: number;
506
504
  private readonly edgeReadTimeoutMs: number;
507
505
  private readonly kvReadTimeoutMs: number;
@@ -554,7 +552,6 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
554
552
  // the internal host. Only the explicit override can be captured eagerly.
555
553
  this.explicitBaseUrl = options.baseUrl;
556
554
  this.defaults = options.defaults;
557
- this.explicitVersion = options.version;
558
555
  // Coalesce only finite numbers to the override; a non-finite value (NaN from
559
556
  // `Number(env.UNSET)`, or Infinity) would otherwise sail past `?? DEFAULT`
560
557
  // (which only replaces null/undefined) into setTimeout, where NaN/Infinity
@@ -1628,8 +1625,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
1628
1625
  // stamp). Carried on doc L1 entries for the herd guard; never serve them.
1629
1626
  headers.delete(CACHE_EXPIRES_AT_HEADER);
1630
1627
  headers.delete(CACHE_REVALIDATING_AT_HEADER);
1631
- // Finding #3 (read side): strip per-client signals a pre-fix or
1632
- // pinned-version L1 entry may carry. See the read-side note in the design doc.
1628
+ // Finding #3 (read side): strip per-client signals a pre-fix L1 entry
1629
+ // may carry. See the read-side note in the design doc.
1633
1630
  stripPerClientSignals(headers);
1634
1631
  return new Response(response.body, {
1635
1632
  status: response.status,
@@ -2952,7 +2949,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2952
2949
  private keyToRequest(key: string, family: KeyVersion): Request {
2953
2950
  const encodedKey = encodeURIComponent(key);
2954
2951
  return new Request(
2955
- `${this.resolveBaseUrl()}${versionKeyPrefix(this.explicitVersion, family)}${encodedKey}`,
2952
+ `${this.resolveBaseUrl()}${versionKeyPrefix(family)}${encodedKey}`,
2956
2953
  { method: "GET" },
2957
2954
  );
2958
2955
  }
@@ -2976,7 +2973,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2976
2973
  * @internal
2977
2974
  */
2978
2975
  private async toKVKey(key: string, family: KeyVersion): Promise<string> {
2979
- const composed = `${versionKeyPrefix(this.explicitVersion, family)}${key}`;
2976
+ const composed = `${versionKeyPrefix(family)}${key}`;
2980
2977
  if (kvKeyByteLength(composed) <= KV_MAX_KEY_BYTES) return composed;
2981
2978
  const prefix = truncateToBytes(composed, KV_KEY_PRESERVED_PREFIX_BYTES);
2982
2979
  return `${prefix}~${await kvKeyDigest(composed)}`;
@@ -322,23 +322,6 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
322
322
  */
323
323
  tagCacheTtl?: number;
324
324
 
325
- /**
326
- * Cache version string override. When set, this exact value prefixes every
327
- * versioned key the store writes (`v/{version}/...`), for cached data and
328
- * stored HTML alike; changing it makes every earlier entry unreachable.
329
- *
330
- * Leave it unset for the default: the store keys with the versions of the
331
- * router serving the request. Segment entries and `"use cache"` items use
332
- * the router's data version; document responses and PPR shells use its
333
- * document version (see `createRouter({ version })`). Those are computed
334
- * from the router's built code, so a deploy that does not change the router
335
- * keeps its entries.
336
- *
337
- * Tag invalidation markers are never versioned: an invalidation applies to
338
- * the entries of every version, including one that comes back in a rollback.
339
- */
340
- version?: string;
341
-
342
325
  /**
343
326
  * Latency budget (ms) for an L1 edge cache (CF Cache API) read. A `match`
344
327
  * slower than this is abandoned and treated as a miss, so a degraded colo
@@ -177,8 +177,8 @@ export type OwnedPushDelivery = (loaderId: string) => RecordAuthority;
177
177
  * says the record supplies that loader's value too ("pin", "copies"), and is
178
178
  * a placeholder (pushPlaceholder) everywhere else: without `owned`, every
179
179
  * owner. Neither claims its loader, so the loader's own cache() HIT still
180
- * delivers (the thenable pushes a restored copy lets through, or its entry's
181
- * pushes in a placeholder's place).
180
+ * delivers (the pushes holding a promise a restored copy lets through, or its
181
+ * entry's pushes in a placeholder's place).
182
182
  */
183
183
  export function restoreHandles(
184
184
  handles: Record<string, SegmentHandleData>,
@@ -445,7 +445,7 @@ export interface ShellCacheEntry {
445
445
  /** React.version captured at prerender time; the read-time invalidation gate. */
446
446
  reactVersion: string;
447
447
  /**
448
- * Build version captured at prerender time: the RSC handler's `version`,
448
+ * Build version captured at prerender time: the RSC handler's version,
449
449
  * which is the router's DOCUMENT version (router-versions.ts) — a hash of
450
450
  * its server code and the client asset names by default, a stamp bumped on
451
451
  * dev RSC-module edits. The second read-time gate: a persistent shared
@@ -686,11 +686,13 @@ export interface ShellSnapshotLoaderValue {
686
686
  */
687
687
  holes: 0 | 1;
688
688
  /**
689
- * 1 when the capture saw a loader push it could not record (a deferred
690
- * push, one with masked nested promises, or one made outside any loader
691
- * body): a HIT then still runs the loader body in the background
692
- * (pin-first) so those pushes reach the page. 0: a hole-free record is
693
- * served from the pin alone and its body does not run on a HIT.
689
+ * 1 when the capture saw a loader push it could not record (one whose
690
+ * settled value holds masked nested promises, one a live-lane loader made,
691
+ * or one made outside any loader body): a HIT then still runs the loader
692
+ * body in the background (pin-first) so those pushes reach the page. A
693
+ * pushed promise that settles thenable-free is recorded (#1182).
694
+ * 0: a hole-free record is served from the pin alone and its body does not
695
+ * run on a HIT.
694
696
  *
695
697
  * Both bits are always written. A record stored before they existed (v0.17)
696
698
  * reads each missing bit as 1 (buildShellLoaderSeed): its snapshot lacks
@@ -330,7 +330,7 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
330
330
  *
331
331
  * Vercel does not reconcile entries between deployments; the store does it
332
332
  * through its keys, which carry the serving router's cache versions (see
333
- * `version`). So a deploy that changes a router's code stops reading that
333
+ * `getCacheVersions`). So a deploy that changes a router's code stops reading that
334
334
  * router's old entries, and a deploy that does not keeps them. A namespace
335
335
  * that changes per deployment (`getCache({ namespace: VERCEL_DEPLOYMENT_ID })`)
336
336
  * still works and clears the cache on every deploy, which is what you want
@@ -363,19 +363,6 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
363
363
  defaultKey: string,
364
364
  ) => string | Promise<string>;
365
365
 
366
- /**
367
- * Version override, folded into every versioned key as `v/{version}/...`.
368
- * When set, this exact value is used for cached data and stored HTML alike;
369
- * changing it makes everything this store wrote before unreachable.
370
- *
371
- * Leave it unset for the default: the store keys with the versions of the
372
- * router serving the request, as CFCacheStore does. Segments and `"use
373
- * cache"` items use the router's data version; responses and PPR shells use
374
- * its document version (see `createRouter({ version })`). Tag markers are
375
- * never versioned, so an invalidation applies to every version.
376
- */
377
- version?: string;
378
-
379
366
  /**
380
367
  * Max serialized entry size in bytes before a write is skipped. Defaults to
381
368
  * VERCEL_MAX_ITEM_BYTES (2 MB - the platform's hard cap).
@@ -537,8 +524,6 @@ export class VercelCacheStore<
537
524
 
538
525
  private readonly cache: VercelRuntimeCache;
539
526
  private readonly waitUntil?: (promise: Promise<unknown>) => void;
540
- /** The `version` option: used for every versioned family when set. */
541
- private readonly explicitVersion?: string;
542
527
  private readonly maxItemBytes: number;
543
528
  private readonly memo: ResolvedShellMemoOptions;
544
529
  private readonly handleMemos: VercelHandleMemos;
@@ -563,7 +548,6 @@ export class VercelCacheStore<
563
548
  this.waitUntil = options.waitUntil;
564
549
  this.defaults = options.defaults;
565
550
  this.keyGenerator = options.keyGenerator;
566
- this.explicitVersion = options.version;
567
551
  this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
568
552
  this.maxShellEntryBytes = Math.floor((this.maxItemBytes * 3) / 4);
569
553
  this.memo = resolveShellMemoOptions(options.memo, {
@@ -1516,7 +1500,7 @@ export class VercelCacheStore<
1516
1500
  // cross-family one reads as corrupt. See the class doc for passing a wider
1517
1501
  // hash (sha256) when many keys are live.
1518
1502
  private toStoreKey(key: string, family: CacheFamily): string {
1519
- return `${versionKeyPrefix(this.explicitVersion, FAMILY_VERSION[family])}rg:${family}:${key}`;
1503
+ return `${versionKeyPrefix(FAMILY_VERSION[family])}rg:${family}:${key}`;
1520
1504
  }
1521
1505
 
1522
1506
  private async write(
@@ -116,8 +116,9 @@ function keptValues(resolved: ResolvedValue[]): unknown[] {
116
116
  * so a push the record leaves out must not be in the prelude either: it
117
117
  * rendered an element no HIT's hydration data matched (issue #1035). Which
118
118
  * pushes a record keeps is the capture funnel's decision (shell-capture.ts
119
- * deriveShellCaptureContext: today it leaves out a deferred loader push and
120
- * one holding a masked promise), not this function's.
119
+ * deriveShellCaptureContext: it leaves out a loader push whose settled value
120
+ * holds a masked promise, and one a live-lane loader made), not this
121
+ * function's.
121
122
  */
122
123
  export async function* resolvedHandleStream(
123
124
  handleStore: HandleStore,
@@ -231,6 +231,11 @@ function collectMeta(segments: MetaDescriptor[][]): MetaDescriptor[] {
231
231
  * (resolve-by-default), so they participate in deduplication and `%s`
232
232
  * title-templating identically to synchronous descriptors. A descriptor that
233
233
  * resolves to `null`/`undefined` (or rejects) is dropped.
234
+ *
235
+ * On a ppr route, a descriptor a handler or an `ssr: false` loader pushes,
236
+ * promise or not, is in the shell's `<head>` on every HIT (#1182). One a
237
+ * loader without `ssr: false` pushes is live: on a HIT it arrives after
238
+ * hydration.
234
239
  */
235
240
  export const Meta: Handle<MetaDescriptor, MetaDescriptor[]> = createHandle<
236
241
  MetaDescriptor,