@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.
- package/dist/bin/rango.js +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +0 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
- package/dist/types/cache/handle-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +8 -6
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
- package/dist/types/handles/deferred-resolution.d.ts +3 -2
- package/dist/types/handles/meta.d.ts +5 -0
- package/dist/types/prerender/build-shell-capture.d.ts +1 -2
- package/dist/types/prerender/writable-store.d.ts +2 -3
- package/dist/types/router/router-interfaces.d.ts +0 -5
- package/dist/types/router/router-options.d.ts +0 -20
- package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
- package/dist/types/router-versions.d.ts +8 -6
- package/dist/types/rsc/shell-capture-constants.d.ts +4 -3
- package/dist/types/rsc/shell-capture.d.ts +16 -2
- package/dist/types/rsc/types.d.ts +0 -8
- package/dist/types/server/build-version-table.d.ts +10 -13
- package/dist/types/server/context.d.ts +26 -2
- package/dist/types/server/handle-store.d.ts +17 -6
- package/dist/types/server/request-context.d.ts +4 -3
- package/dist/types/testing/build-versions.d.ts +0 -3
- package/dist/types/testing/internal/seed-vars.d.ts +4 -4
- package/dist/types/testing/serve-shell-request.d.ts +1 -1
- package/dist/types/types/loader-types.d.ts +6 -1
- package/dist/vite/index.js +3 -8
- package/package.json +1 -1
- package/skills/cache-guide/SKILL.md +4 -8
- package/skills/caching/SKILL.md +3 -5
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/hooks/handle-and-actions.md +6 -6
- package/skills/host-router/SKILL.md +1 -1
- package/skills/loader/SKILL.md +10 -9
- package/skills/ppr/SKILL.md +60 -44
- package/skills/prerender/SKILL.md +3 -3
- package/skills/router-setup/SKILL.md +0 -2
- package/skills/testing/render-handler.md +17 -17
- package/skills/testing/serve-shell-request.md +1 -1
- package/skills/testing/server-actions.md +16 -16
- package/skills/vercel/SKILL.md +1 -1
- package/src/cache/cf/cf-cache-store.ts +4 -7
- package/src/cache/cf/cf-cache-types.ts +0 -17
- package/src/cache/handle-snapshot.ts +2 -2
- package/src/cache/types.ts +8 -6
- package/src/cache/vercel/vercel-cache-store.ts +2 -18
- package/src/handles/deferred-resolution.ts +3 -2
- package/src/handles/meta.ts +5 -0
- package/src/prerender/build-shell-capture.ts +1 -2
- package/src/prerender/writable-store.ts +2 -3
- package/src/route-definition/dsl-helpers.ts +14 -14
- package/src/router/manifest.ts +9 -2
- package/src/router/match-middleware/intercept-resolution.ts +2 -2
- package/src/router/router-interfaces.ts +0 -6
- package/src/router/router-options.ts +0 -21
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/mask-nested.ts +5 -3
- package/src/router-versions.ts +7 -7
- package/src/router.ts +3 -8
- package/src/rsc/handler.ts +3 -7
- package/src/rsc/shell-capture-constants.ts +4 -3
- package/src/rsc/shell-capture.ts +126 -51
- package/src/rsc/types.ts +0 -9
- package/src/server/build-version-table.ts +12 -24
- package/src/server/context.ts +64 -2
- package/src/server/handle-store.ts +77 -10
- package/src/server/request-context.ts +4 -3
- package/src/testing/build-versions.ts +0 -3
- package/src/testing/dispatch.ts +1 -3
- package/src/testing/internal/seed-vars.ts +4 -4
- package/src/testing/serve-shell-request.ts +2 -5
- package/src/types/loader-types.ts +6 -1
- 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,
|
|
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
|
|
package/skills/loader/SKILL.md
CHANGED
|
@@ -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
|
|
672
|
-
|
|
673
|
-
serves the loader's data from the shell, the
|
|
674
|
-
and the entry only adds
|
|
675
|
-
|
|
676
|
-
|
|
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
|
|
1044
|
-
|
|
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 —
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -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
|
-
-
|
|
845
|
-
|
|
846
|
-
|
|
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 (
|
|
1001
|
-
loader record of the page to run: those bodies then still run on each
|
|
1002
|
-
in the background, and that push reaches the page (from the loader's
|
|
1003
|
-
`cache()` entry when it hits) with the run's value, on a document HIT and
|
|
1004
|
-
a client navigation alike. On a document HIT it arrives after hydration.
|
|
1005
|
-
|
|
1006
|
-
**Handles on a shell HIT.**
|
|
1007
|
-
|
|
1008
|
-
(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
|
1029
|
-
|
|
|
1030
|
-
| a handler's push, a pinned `ssr: false` loader's
|
|
1031
|
-
| a
|
|
1032
|
-
| a live-lane loader's push
|
|
1033
|
-
| the run of an `ssr: false` loader the shell has no pin for
|
|
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
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1042
|
-
ctx.use(Notes)(
|
|
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
|
-
|
|
1046
|
-
|
|
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
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
|
1333
|
-
entry that builds its own `createRSCHandler({ cache
|
|
1334
|
-
|
|
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` (
|
|
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` (
|
|
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
|
|
package/skills/vercel/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
1632
|
-
//
|
|
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(
|
|
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(
|
|
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
|
|
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>,
|
package/src/cache/types.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
690
|
-
*
|
|
691
|
-
* body): a HIT then still runs the loader
|
|
692
|
-
* (pin-first) so those pushes reach the page.
|
|
693
|
-
*
|
|
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
|
-
* `
|
|
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(
|
|
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:
|
|
120
|
-
*
|
|
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,
|
package/src/handles/meta.ts
CHANGED
|
@@ -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,
|