@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
|
@@ -106,8 +106,7 @@ export interface BuildShellCaptureOptions {
|
|
|
106
106
|
/** Build-time env bindings (rango plugin buildEnv), if configured. */
|
|
107
107
|
buildEnv?: unknown;
|
|
108
108
|
/**
|
|
109
|
-
* The versions the SHIPPED build serves this router with (the build table
|
|
110
|
-
* or the router's consumer-set version) — NOT the temp server's own
|
|
109
|
+
* The versions the SHIPPED build serves this router with (the build table) — NOT the temp server's own
|
|
111
110
|
* version-plugin stamp. The serve-side isValidShellHit gate compares
|
|
112
111
|
* entry.buildVersion against the running worker's ctx.version, which is the
|
|
113
112
|
* document version; stamping the temp server's would make every build entry
|
|
@@ -22,9 +22,8 @@ import { paramsEqual } from "../router/params-util.js";
|
|
|
22
22
|
export interface PrerenderKey {
|
|
23
23
|
routerId: string;
|
|
24
24
|
/**
|
|
25
|
-
* The owning router's cache version (its data version
|
|
26
|
-
*
|
|
27
|
-
* store keys off it as given.
|
|
25
|
+
* The owning router's cache version (its data version). Opaque: never set
|
|
26
|
+
* or read by app code; a store keys off it as given.
|
|
28
27
|
*/
|
|
29
28
|
version: string;
|
|
30
29
|
routeName: string;
|
|
@@ -9,6 +9,8 @@ import type {
|
|
|
9
9
|
TransitionConfig,
|
|
10
10
|
} from "../types.js";
|
|
11
11
|
import {
|
|
12
|
+
applyDocumentModeLoading,
|
|
13
|
+
documentModeLoader,
|
|
12
14
|
getContext,
|
|
13
15
|
getNamePrefix,
|
|
14
16
|
getReverseScope,
|
|
@@ -928,17 +930,14 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
928
930
|
|
|
929
931
|
const name = `${ctx.namespace}.$${store.getNextIndex("loader")}`;
|
|
930
932
|
|
|
931
|
-
// Create loader entry with empty revalidate array.
|
|
932
|
-
//
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
// `bake` is the lane, the same on both evaluations.
|
|
933
|
+
// Create loader entry with empty revalidate array. `bake` is the lane, the
|
|
934
|
+
// same on both evaluations; a document evaluation adds awaitBeforeFlush at
|
|
935
|
+
// the push (documentModeLoader) — per-isSSR entry caching makes the flag
|
|
936
|
+
// request-mode-correct with no isSSR threading; see LoaderEntry.
|
|
936
937
|
const loaderEntry: LoaderEntry = {
|
|
937
938
|
loader: loaderDef,
|
|
938
939
|
revalidate: [] as ShouldRevalidateFn<any, any>[],
|
|
939
940
|
...(optionsGiven?.ssr === false && { bake: true as const }),
|
|
940
|
-
...(optionsGiven?.ssr === false &&
|
|
941
|
-
ctx.isSSR && { awaitBeforeFlush: true as const }),
|
|
942
941
|
...(optionsGiven?.prefetch === false && { prefetch: false as const }),
|
|
943
942
|
// The declaring layout's / parallel's / cache()'s scope. A route's (and
|
|
944
943
|
// an intercept's) loaders have none: they resolve from the route's name,
|
|
@@ -978,7 +977,9 @@ const loader: RouteHelpers<any, any>["loader"] = (
|
|
|
978
977
|
validateUseItems(result, name, "loader", "use");
|
|
979
978
|
}
|
|
980
979
|
|
|
981
|
-
ctx.parent.loader.push(
|
|
980
|
+
ctx.parent.loader.push(
|
|
981
|
+
ctx.isSSR ? documentModeLoader(loaderEntry) : loaderEntry,
|
|
982
|
+
);
|
|
982
983
|
return { name, type: "loader" } as LoaderItem;
|
|
983
984
|
};
|
|
984
985
|
|
|
@@ -1006,12 +1007,11 @@ const loading: RouteHelpers<any, any>["loading"] = (component, options) => {
|
|
|
1006
1007
|
const resolved =
|
|
1007
1008
|
typeof component === "function" ? (component as () => any)() : component;
|
|
1008
1009
|
|
|
1009
|
-
//
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
}
|
|
1010
|
+
// `loadingSsr` records ssr: false on both evaluations; a document drops
|
|
1011
|
+
// the fallback (applyDocumentModeLoading).
|
|
1012
|
+
parent.loading = resolved;
|
|
1013
|
+
parent.loadingSsr = options?.ssr === false ? false : undefined;
|
|
1014
|
+
if (ctx.isSSR) applyDocumentModeLoading(parent);
|
|
1015
1015
|
|
|
1016
1016
|
// The flag means "what this fallback covers": without a fallback to show
|
|
1017
1017
|
// there is nothing to defer behind. Recorded from `resolved`, not
|
package/src/router/manifest.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createRouteHelpers } from "../route-definition";
|
|
|
3
3
|
import {
|
|
4
4
|
getContext,
|
|
5
5
|
runWithPrefixes,
|
|
6
|
+
getDocumentModeEntry,
|
|
6
7
|
getIsolatedLazyParent,
|
|
7
8
|
type EntryData,
|
|
8
9
|
type MetricsStore,
|
|
@@ -120,12 +121,18 @@ export async function loadManifest(
|
|
|
120
121
|
mountIndex !== undefined ? `#router.M${mountIndex}` : "#router";
|
|
121
122
|
|
|
122
123
|
// For lazy entries, use the captured parent from include() context
|
|
123
|
-
// This ensures routes are registered under the correct layout hierarchy
|
|
124
|
+
// This ensures routes are registered under the correct layout hierarchy.
|
|
125
|
+
// The captured chain comes from an evaluation without isSSR: a document
|
|
126
|
+
// request takes its document-mode copy (getDocumentModeEntry, #1179).
|
|
124
127
|
const lazyContext =
|
|
125
128
|
entry.lazy && entry.lazyPatterns ? entry.lazyContext : null;
|
|
129
|
+
const capturedParent =
|
|
130
|
+
(lazyContext?.parent as EntryData | null | undefined) ?? Store.parent;
|
|
126
131
|
const parentForContext = lazyContext
|
|
127
132
|
? getIsolatedLazyParent(
|
|
128
|
-
|
|
133
|
+
isSSR && capturedParent
|
|
134
|
+
? getDocumentModeEntry(capturedParent)
|
|
135
|
+
: capturedParent,
|
|
129
136
|
)
|
|
130
137
|
: Store.parent;
|
|
131
138
|
|
|
@@ -232,8 +232,8 @@ async function handleCacheHitIntercept<TEnv>(
|
|
|
232
232
|
const cachedSlot = segments.filter(isInterceptSegment);
|
|
233
233
|
// A record stored before 0.25.0 carries the loaders' aggregate on the slot;
|
|
234
234
|
// the loader segments below replace it. Build versions hash the router's
|
|
235
|
-
// code, so
|
|
236
|
-
//
|
|
235
|
+
// code, so an upgrade does not read one; a custom store that ignores
|
|
236
|
+
// versions can.
|
|
237
237
|
for (const slot of cachedSlot) {
|
|
238
238
|
delete slot.loaderDataPromise;
|
|
239
239
|
delete slot.loaderIds;
|
|
@@ -479,12 +479,6 @@ export interface RangoInternal<
|
|
|
479
479
|
*/
|
|
480
480
|
readonly nonce?: NonceProvider<TEnv>;
|
|
481
481
|
|
|
482
|
-
/**
|
|
483
|
-
* The consumer-set version (createRouter({ version })), used for both of the
|
|
484
|
-
* router's versions. Undefined when the router uses its build versions.
|
|
485
|
-
*/
|
|
486
|
-
readonly version?: string;
|
|
487
|
-
|
|
488
482
|
/**
|
|
489
483
|
* URL patterns reference for build-time manifest generation
|
|
490
484
|
*/
|
|
@@ -531,27 +531,6 @@ export interface RangoOptions<TEnv = any> {
|
|
|
531
531
|
*/
|
|
532
532
|
nonce?: NonceProvider<TEnv>;
|
|
533
533
|
|
|
534
|
-
/**
|
|
535
|
-
* Version override. When set, this exact value is used for both of the
|
|
536
|
-
* router's versions: it keys the router's cached data (segments, `"use
|
|
537
|
-
* cache"` values, loader data) and its stored HTML (PPR shells,
|
|
538
|
-
* document-cache responses), is sent in payload metadata, and is what the
|
|
539
|
-
* browser's `_rsc_v` is compared with (a mismatch reloads the tab).
|
|
540
|
-
*
|
|
541
|
-
* Leave it unset for the default: rango computes two versions per router at
|
|
542
|
-
* build time, a data version from the router's server code and a document
|
|
543
|
-
* version that also covers the client assets. The same code builds to the
|
|
544
|
-
* same versions, so a deploy that does not change this router keeps its
|
|
545
|
-
* cache. In dev both are a stamp bumped on every RSC module edit.
|
|
546
|
-
*
|
|
547
|
-
* Set it to clear this router's cache on demand (change the value), or to
|
|
548
|
-
* tie the cache to your own release id (a per-deploy value clears the cache
|
|
549
|
-
* on every deploy).
|
|
550
|
-
*
|
|
551
|
-
* @default the router's build versions
|
|
552
|
-
*/
|
|
553
|
-
version?: string;
|
|
554
|
-
|
|
555
534
|
/**
|
|
556
535
|
* TTL (in seconds) for the client-side in-memory prefetch cache.
|
|
557
536
|
*
|
|
@@ -378,9 +378,10 @@ function getLoaderStore(
|
|
|
378
378
|
* push funnel reads the pushReplayed owner, shell-capture.ts), so a replay
|
|
379
379
|
* of that shell that serves this loader from its pin restores them owned
|
|
380
380
|
* (HandleStore.pushRestored), on a document HIT and on a navigation alike.
|
|
381
|
-
* This replay still runs there: the store drops its
|
|
382
|
-
* the record already restored, and keeps the
|
|
383
|
-
* not keep (
|
|
381
|
+
* This replay still runs there: the store drops its values that settle
|
|
382
|
+
* thenable-free, which the record already restored, and keeps the ones
|
|
383
|
+
* holding a promise the record could not keep (the reason the loader's pin
|
|
384
|
+
* carries `runs`).
|
|
384
385
|
*/
|
|
385
386
|
async function replayLoaderHandles(
|
|
386
387
|
encoded: string | undefined,
|
|
@@ -125,9 +125,11 @@ function someNestedThenable(
|
|
|
125
125
|
/**
|
|
126
126
|
* True when `value` is a thenable or holds one where the capture's loader
|
|
127
127
|
* push funnel masks it (maskNestedContainerThenables): through plain data
|
|
128
|
-
* objects, arrays and React element props.
|
|
129
|
-
* record and reaches a HIT only from a run of its
|
|
130
|
-
* keeps it; a push without one is the record's
|
|
128
|
+
* objects, arrays and React element props. A settled push that holds one
|
|
129
|
+
* stays out of a shell's record and reaches a HIT only from a run of its
|
|
130
|
+
* loader there, so a HIT keeps it; a push without one is the record's
|
|
131
|
+
* (HandleStore.pushRestored). A top-level thenable is judged by the value it
|
|
132
|
+
* settles to, on both sides (#1182).
|
|
131
133
|
*/
|
|
132
134
|
export function holdsThenable(value: unknown): boolean {
|
|
133
135
|
return someNestedThenable(value, () => true);
|
package/src/router-versions.ts
CHANGED
|
@@ -33,24 +33,24 @@ export type RouterVersionsTable = Readonly<
|
|
|
33
33
|
/** Table key of the whole-build versions. Not a legal `$$id` (8 hex chars). */
|
|
34
34
|
export const DEFAULT_ROUTER_VERSIONS_KEY = "*";
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* One value for both versions: the dev stamp, the version a build-time shell
|
|
38
|
+
* capture is asked for, or a test's seed.
|
|
39
|
+
*/
|
|
37
40
|
export function uniformVersions(version: string): RouterVersions {
|
|
38
41
|
return { data: version, document: version };
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
42
|
-
* The versions a router serves with under `table`:
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* tests) or it has neither entry. One function for the request handler and
|
|
45
|
+
* The versions a router serves with under `table`: the router's own entry,
|
|
46
|
+
* else the whole-build pair, for a router the table does not list.
|
|
47
|
+
* `undefined` when there is no table (dev, tests) or it has neither entry. One function for the request handler and
|
|
46
48
|
* the build's shell capture, which must stamp what the handler will check.
|
|
47
49
|
*/
|
|
48
50
|
export function resolveVersionsFrom(
|
|
49
51
|
table: RouterVersionsTable | undefined,
|
|
50
52
|
routerId: string | undefined,
|
|
51
|
-
override: string | undefined,
|
|
52
53
|
): RouterVersions | undefined {
|
|
53
|
-
if (override !== undefined) return uniformVersions(override);
|
|
54
54
|
if (!table) return undefined;
|
|
55
55
|
const pair =
|
|
56
56
|
(routerId !== undefined && Object.hasOwn(table, routerId)
|
package/src/router.ts
CHANGED
|
@@ -170,7 +170,6 @@ export function createRouter<TEnv = any>(
|
|
|
170
170
|
$$routeNames: staticRouteNames,
|
|
171
171
|
$$sourceFile: injectedSourceFile,
|
|
172
172
|
nonce,
|
|
173
|
-
version,
|
|
174
173
|
prefetchCacheTTL: prefetchCacheTTLOption,
|
|
175
174
|
prefetchCacheSize: prefetchCacheSizeOption,
|
|
176
175
|
prefetchConcurrency: prefetchConcurrencyOption,
|
|
@@ -771,7 +770,7 @@ export function createRouter<TEnv = any>(
|
|
|
771
770
|
// handler's overlay read makes (resolvePrerenderVersion), so the keys agree.
|
|
772
771
|
const prerenderTrigger = createPrerenderTrigger<TEnv, {}>({
|
|
773
772
|
routerId,
|
|
774
|
-
resolveVersion: () => resolvePrerenderVersion(routerId
|
|
773
|
+
resolveVersion: () => resolvePrerenderVersion(routerId),
|
|
775
774
|
isDev: isDevEnvironment,
|
|
776
775
|
isViteDevServer,
|
|
777
776
|
ensureManifest: () => ensureRouterManifest(routerId),
|
|
@@ -803,7 +802,7 @@ export function createRouter<TEnv = any>(
|
|
|
803
802
|
{},
|
|
804
803
|
{
|
|
805
804
|
...prerenderDeps,
|
|
806
|
-
versions: resolveRouterVersions(routerId
|
|
805
|
+
versions: resolveRouterVersions(routerId),
|
|
807
806
|
},
|
|
808
807
|
undefined,
|
|
809
808
|
isPassthrough,
|
|
@@ -812,7 +811,7 @@ export function createRouter<TEnv = any>(
|
|
|
812
811
|
true,
|
|
813
812
|
),
|
|
814
813
|
// A warm goes through router.fetch's handler: the one createRouter's own
|
|
815
|
-
// cache
|
|
814
|
+
// cache and nonce configure. An entry that passes them to
|
|
816
815
|
// createRSCHandler itself is not what a warm runs.
|
|
817
816
|
resolveCacheConfig: (env, ctx) =>
|
|
818
817
|
typeof cache === "function" ? cache(env, ctx) : cache,
|
|
@@ -1222,9 +1221,6 @@ export function createRouter<TEnv = any>(
|
|
|
1222
1221
|
// Expose nonce provider for fetch
|
|
1223
1222
|
nonce,
|
|
1224
1223
|
|
|
1225
|
-
// Expose version for fetch
|
|
1226
|
-
version,
|
|
1227
|
-
|
|
1228
1224
|
// Expose urlpatterns for runtime manifest generation
|
|
1229
1225
|
get urlpatterns() {
|
|
1230
1226
|
return storedUrlPatterns ?? undefined;
|
|
@@ -1292,7 +1288,6 @@ export function createRouter<TEnv = any>(
|
|
|
1292
1288
|
router: router as any,
|
|
1293
1289
|
cache,
|
|
1294
1290
|
nonce,
|
|
1295
|
-
version,
|
|
1296
1291
|
}) as (
|
|
1297
1292
|
request: Request,
|
|
1298
1293
|
input: RouterRequestInput<TEnv>,
|
package/src/rsc/handler.ts
CHANGED
|
@@ -165,12 +165,8 @@ export function createRSCHandler<
|
|
|
165
165
|
TRoutes extends Record<string, string> = Record<string, string>,
|
|
166
166
|
>(options: CreateRSCHandlerOptions<TEnv, TRoutes>) {
|
|
167
167
|
const { router, nonce: nonceProvider } = options;
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
const versions = resolveRouterVersions(
|
|
171
|
-
router.id,
|
|
172
|
-
options.version ?? router.version,
|
|
173
|
-
);
|
|
168
|
+
// The router's own pair from the build.
|
|
169
|
+
const versions = resolveRouterVersions(router.id);
|
|
174
170
|
// Everything the handler itself versions is tied to the HTML a tab holds:
|
|
175
171
|
// payload metadata (what the browser echoes as `_rsc_v`), the reload check,
|
|
176
172
|
// shell stamping and gating, the rango state value. The data version is only
|
|
@@ -548,7 +544,7 @@ export function createRSCHandler<
|
|
|
548
544
|
resolvedPrerender = {
|
|
549
545
|
config: prerenderConfig,
|
|
550
546
|
routerId: router.id,
|
|
551
|
-
version: resolvePrerenderVersion(router.id
|
|
547
|
+
version: resolvePrerenderVersion(router.id),
|
|
552
548
|
};
|
|
553
549
|
}
|
|
554
550
|
}
|
|
@@ -208,9 +208,10 @@ export const SHELL_CAPTURE_TASK_HARD_CAP_MS = 25_000;
|
|
|
208
208
|
* (ShellCaptureDebugEvent.refusal, `caches.refusal` on a `router.prerender()`
|
|
209
209
|
* result): `identity` (a request-scoped read: cookies(), headers(), the
|
|
210
210
|
* theme, a `{ cache: false }` variable), `dynamic` (`ctx.dynamic()`),
|
|
211
|
-
* `loader` (a `{ ssr: false }` loader rejected,
|
|
212
|
-
* redirect()/notFound()
|
|
213
|
-
*
|
|
211
|
+
* `loader` (a `{ ssr: false }` loader rejected, settled with
|
|
212
|
+
* redirect()/notFound(), or a promise it pushed rejected),
|
|
213
|
+
* `no-record` (no doc record: the route's cache() refused the write, or the
|
|
214
|
+
* handler output failed), `handles` (a pushed
|
|
214
215
|
* handle value did not finish encoding), `size` (over the store's value
|
|
215
216
|
* limit), `record-expired` (the route cache() record the capture wrote ran
|
|
216
217
|
* out first), `invalidated` (a tag of the shell was invalidated after the
|
package/src/rsc/shell-capture.ts
CHANGED
|
@@ -508,6 +508,11 @@ function refuse(
|
|
|
508
508
|
return "refused";
|
|
509
509
|
}
|
|
510
510
|
|
|
511
|
+
/** A bake-lane loader's promise push, as a refusal names it. */
|
|
512
|
+
function describeLoaderPromisePush(push: LoaderPromisePush): string {
|
|
513
|
+
return `the promise the loader "${push.loaderId}" pushed to the handle "${push.handleName}"`;
|
|
514
|
+
}
|
|
515
|
+
|
|
511
516
|
/** The no-shell cause for handler output that missed the capture deadline. */
|
|
512
517
|
const HANDLER_OUTPUT_TIMEOUT_REASON: string =
|
|
513
518
|
"the handler output (a promise it passes or pushes, or an async server " +
|
|
@@ -1667,8 +1672,9 @@ export type CaptureRecordOutcome =
|
|
|
1667
1672
|
* component showed one value in the prelude and another on every HIT.
|
|
1668
1673
|
*
|
|
1669
1674
|
* Order, all bounded by the one capture deadline (`ppr.captureTimeout`):
|
|
1670
|
-
* top-level handle promises
|
|
1671
|
-
*
|
|
1675
|
+
* top-level handle promises (a bake-lane loader's pushed promises among
|
|
1676
|
+
* them, #1182), the handler pushes' nested promises, and the bake-lane
|
|
1677
|
+
* loader containers settle first (the record encodes the handles
|
|
1672
1678
|
* a HIT restores, and its handle encode has its own 5s timeout that must
|
|
1673
1679
|
* never be the one that fires); then the capture's onResponse callbacks fire
|
|
1674
1680
|
* with a synthetic 200 (the implicit doc scope's cacheRoute, an explicit
|
|
@@ -1711,7 +1717,33 @@ export async function settleCaptureRecord(
|
|
|
1711
1717
|
if (!settled.done) {
|
|
1712
1718
|
if (refuseOnCaptureGuard(capture.key, derivedCtx))
|
|
1713
1719
|
return { kind: "refused", refusal: guardRefusal(derivedCtx) };
|
|
1714
|
-
|
|
1720
|
+
const pending = derivation.loaderPromisePushes.find(
|
|
1721
|
+
(p) => p.state === "pending",
|
|
1722
|
+
);
|
|
1723
|
+
return {
|
|
1724
|
+
kind: "timeout",
|
|
1725
|
+
reason: pending
|
|
1726
|
+
? `${describeLoaderPromisePush(pending)} did not settle within ppr.captureTimeout`
|
|
1727
|
+
: HANDLER_OUTPUT_TIMEOUT_REASON,
|
|
1728
|
+
};
|
|
1729
|
+
}
|
|
1730
|
+
// A rejected value must not bake, and the record's encode would fail on
|
|
1731
|
+
// it anyway: refuse before the write starts. A guard trip in that promise
|
|
1732
|
+
// names the read instead.
|
|
1733
|
+
const rejected = derivation.loaderPromisePushes.find(
|
|
1734
|
+
(p) => p.state === "rejected",
|
|
1735
|
+
);
|
|
1736
|
+
if (rejected) {
|
|
1737
|
+
if (refuseOnCaptureGuard(capture.key, derivedCtx))
|
|
1738
|
+
return { kind: "refused", refusal: guardRefusal(derivedCtx) };
|
|
1739
|
+
warnCaptureRefusedOnce(
|
|
1740
|
+
capture.key,
|
|
1741
|
+
`${describeLoaderPromisePush(rejected)} rejected during capture; a ` +
|
|
1742
|
+
"failed value must not bake into the shared shell. Catch it and push " +
|
|
1743
|
+
"a fallback, or push it from a loader without ssr: false.",
|
|
1744
|
+
PPR_LANE_HINT,
|
|
1745
|
+
);
|
|
1746
|
+
return { kind: "refused", refusal: "loader" };
|
|
1715
1747
|
}
|
|
1716
1748
|
|
|
1717
1749
|
const callbacks = derivedCtx._onResponseCallbacks?.splice(0) ?? [];
|
|
@@ -2006,6 +2038,20 @@ export interface CaptureContextDerivation {
|
|
|
2006
2038
|
* record encodes its handles, so that encode never races a pending value.
|
|
2007
2039
|
*/
|
|
2008
2040
|
handlerPushesSettled: () => Promise<void>;
|
|
2041
|
+
/**
|
|
2042
|
+
* Every promise a bake-lane loader body pushed (#1182), in push order.
|
|
2043
|
+
* The record-first step waits for them (they are in the recorded cut) and
|
|
2044
|
+
* names the loader and the handle of one that rejects or misses the
|
|
2045
|
+
* deadline.
|
|
2046
|
+
*/
|
|
2047
|
+
loaderPromisePushes: readonly LoaderPromisePush[];
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
/** A promise a bake-lane loader pushed at capture, owned by that loader. */
|
|
2051
|
+
export interface LoaderPromisePush {
|
|
2052
|
+
loaderId: string;
|
|
2053
|
+
handleName: string;
|
|
2054
|
+
state: "pending" | "fulfilled" | "rejected";
|
|
2009
2055
|
}
|
|
2010
2056
|
|
|
2011
2057
|
/**
|
|
@@ -2041,30 +2087,35 @@ export function deriveShellCaptureContext(
|
|
|
2041
2087
|
// masked), their pushes are in the captured HTML (<head> title/meta,
|
|
2042
2088
|
// useHandle echoes), and a HIT does not run a promise-free bake-lane
|
|
2043
2089
|
// loader (loader-cache.ts), so the stored record carries them: each
|
|
2044
|
-
//
|
|
2045
|
-
// `owner` (HandleStore.push), the record stores it
|
|
2090
|
+
// push from a loader body whose value, once settled, is thenable-free
|
|
2091
|
+
// keeps that loader as `owner` (HandleStore.push), the record stores it
|
|
2046
2092
|
// (CachedEntryData.handleOwners), and a replay that serves the loader
|
|
2047
|
-
// from its pin restores it through pushRestored (restoreHandles).
|
|
2048
|
-
//
|
|
2049
|
-
//
|
|
2050
|
-
//
|
|
2051
|
-
//
|
|
2052
|
-
//
|
|
2053
|
-
//
|
|
2054
|
-
//
|
|
2055
|
-
//
|
|
2056
|
-
//
|
|
2057
|
-
//
|
|
2058
|
-
//
|
|
2059
|
-
//
|
|
2060
|
-
//
|
|
2061
|
-
//
|
|
2062
|
-
//
|
|
2063
|
-
//
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2093
|
+
// from its pin restores it through pushRestored (restoreHandles). A
|
|
2094
|
+
// pushed promise (push(promise), .defer()) is judged by its settled
|
|
2095
|
+
// value, as push(await promise) would be (#1182): the record-first step
|
|
2096
|
+
// waits for it under the capture deadline, and one that rejects or
|
|
2097
|
+
// misses the deadline refuses the capture. Those copies stand: a
|
|
2098
|
+
// promise-carrying bake-lane loader that runs on the replay reads the
|
|
2099
|
+
// store, so its pushes that settle thenable-free (and those replayed
|
|
2100
|
+
// inside its body) are dropped and only the ones holding a promise are
|
|
2101
|
+
// added. A loader the replay does not serve from a pin is a hole: one
|
|
2102
|
+
// the route also runs on the live lane, or one whose pin the entry
|
|
2103
|
+
// lost. Its run replaces its copies (pushPlaceholder, #936), those of
|
|
2104
|
+
// the dependencies it awaits included (they are credited to it). Nested
|
|
2105
|
+
// thenables stay masked (the promise shape is the liveness declaration,
|
|
2106
|
+
// mask-nested.ts). A push whose settled value holds a masked nested
|
|
2107
|
+
// promise, one credited to a live-lane body, or one no body or replay
|
|
2108
|
+
// names (a loader-cache replay names its loader: pushReplayed below)
|
|
2109
|
+
// stays tagged (HandleStore.push loaderPush) so the handle encode cannot
|
|
2110
|
+
// stall on a never-resolving mask. A tagged push is out of the record
|
|
2111
|
+
// (captureHandles) and out of the capture's own render
|
|
2112
|
+
// (resolvedHandleStream `recordedOnly`, issue #1035): the prelude is
|
|
2113
|
+
// rendered from what the record keeps, which is what a HIT hydrates
|
|
2114
|
+
// with. The loader records then carry `runs: 1` so a HIT runs those
|
|
2115
|
+
// bodies for them, and what they push reaches the client after
|
|
2066
2116
|
// hydration (HandleStore.freezeDocumentSnapshot).
|
|
2067
2117
|
const handlerPushSettles: Promise<void>[] = [];
|
|
2118
|
+
const loaderPromisePushes: LoaderPromisePush[] = [];
|
|
2068
2119
|
// A replay (a loader-cache HIT, loader-cache.ts replayLoaderHandles, or a
|
|
2069
2120
|
// route cache() record's owned values, restoreHandles) re-pushes a loader's
|
|
2070
2121
|
// recorded values through pushReplayed or pushPlaceholder, which call
|
|
@@ -2118,39 +2169,62 @@ export function deriveShellCaptureContext(
|
|
|
2118
2169
|
if (report.thenable) maskedNestedThenable = true;
|
|
2119
2170
|
return masked;
|
|
2120
2171
|
};
|
|
2172
|
+
// The loader the push is credited to (see the funnel comment above). A
|
|
2173
|
+
// push under a live-lane loader's body, at any depth, is that hole's
|
|
2174
|
+
// output: its live run replaces it on a HIT and a navigation. Otherwise
|
|
2175
|
+
// the first loader around the push that the route registers decides: a
|
|
2176
|
+
// bake-lane one keeps the push under the innermost body, and a
|
|
2177
|
+
// dependency on neither lane is walked past. A replay of a hole's push
|
|
2178
|
+
// is that hole's, unless a live-lane body around it takes it first.
|
|
2179
|
+
const live = liveLaneOwner();
|
|
2180
|
+
const bodyLoaderId = live ?? getCurrentLoaderBodyId() ?? replayOwner;
|
|
2121
2181
|
let masked: unknown;
|
|
2122
|
-
let loaderPush = true;
|
|
2123
2182
|
let owner: string | undefined;
|
|
2124
|
-
if (isThenable(value)) {
|
|
2125
|
-
// Deferred (thenable) loader pushes always keep the tag — the
|
|
2126
|
-
// carve-out below is for settled values only. Promise.resolve first: a
|
|
2127
|
-
// loader cache() entry replays a deferred push as the thenable Flight
|
|
2128
|
-
// decoded it to, whose then() returns nothing, and the slot was
|
|
2129
|
-
// `undefined` (the empty element of issue #1035).
|
|
2130
|
-
masked = Promise.resolve(value).then(mask);
|
|
2131
|
-
} else {
|
|
2183
|
+
if (!isThenable(value)) {
|
|
2132
2184
|
masked = mask(value);
|
|
2133
|
-
if (!maskedNestedThenable)
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2185
|
+
if (!maskedNestedThenable) owner = bodyLoaderId;
|
|
2186
|
+
} else if (live === undefined && bodyLoaderId !== undefined) {
|
|
2187
|
+
// Owned now, judged by its settled value (funnel comment above).
|
|
2188
|
+
// Promise.resolve first: a loader cache() entry replays a deferred
|
|
2189
|
+
// push as the thenable Flight decoded it to, whose then() returns
|
|
2190
|
+
// nothing (issue #1035). The retag runs inside the slot's own then(),
|
|
2191
|
+
// so it lands before the wait on it (settleCaptureRecord) resumes and
|
|
2192
|
+
// the record encodes. The state handler is attached now, which also
|
|
2193
|
+
// marks a rejection as handled.
|
|
2194
|
+
const slot: Promise<unknown> = Promise.resolve(value).then((settled) => {
|
|
2195
|
+
const judged = mask(settled);
|
|
2196
|
+
if (maskedNestedThenable) {
|
|
2197
|
+
freshHandleStore.unrecordPush(handleName, segmentId, slot);
|
|
2198
|
+
derivedCtx._shellCaptureUnrecordedLoaderPush = true;
|
|
2199
|
+
}
|
|
2200
|
+
return judged;
|
|
2201
|
+
});
|
|
2202
|
+
const push: LoaderPromisePush = {
|
|
2203
|
+
loaderId: bodyLoaderId,
|
|
2204
|
+
handleName,
|
|
2205
|
+
state: "pending",
|
|
2206
|
+
};
|
|
2207
|
+
slot.then(
|
|
2208
|
+
() => {
|
|
2209
|
+
push.state = "fulfilled";
|
|
2210
|
+
},
|
|
2211
|
+
() => {
|
|
2212
|
+
push.state = "rejected";
|
|
2213
|
+
},
|
|
2214
|
+
);
|
|
2215
|
+
loaderPromisePushes.push(push);
|
|
2216
|
+
masked = slot;
|
|
2217
|
+
owner = bodyLoaderId;
|
|
2218
|
+
} else {
|
|
2219
|
+
// A hole's promise, or one no body or replay names: tagged. Nothing
|
|
2220
|
+
// awaits the slot at capture, so its rejection is handled here.
|
|
2221
|
+
const tagged = Promise.resolve(value).then(mask);
|
|
2222
|
+
tagged.catch(() => {});
|
|
2223
|
+
masked = tagged;
|
|
2151
2224
|
}
|
|
2152
2225
|
// A push the record cannot keep reaches a HIT only from a live run of
|
|
2153
2226
|
// its loader, so the loader records ask for one (runs: 1).
|
|
2227
|
+
const loaderPush = owner === undefined;
|
|
2154
2228
|
if (loaderPush) derivedCtx._shellCaptureUnrecordedLoaderPush = true;
|
|
2155
2229
|
rawCapturePush(handleName, segmentId, masked, loaderPush, owner);
|
|
2156
2230
|
};
|
|
@@ -2271,6 +2345,7 @@ export function deriveShellCaptureContext(
|
|
|
2271
2345
|
freshHandleStore,
|
|
2272
2346
|
handlerPushesSettled: () =>
|
|
2273
2347
|
Promise.all(handlerPushSettles).then(() => undefined),
|
|
2348
|
+
loaderPromisePushes,
|
|
2274
2349
|
};
|
|
2275
2350
|
}
|
|
2276
2351
|
|
package/src/rsc/types.ts
CHANGED
|
@@ -369,15 +369,6 @@ export interface CreateRSCHandlerOptions<
|
|
|
369
369
|
*/
|
|
370
370
|
cache?: HandlerCacheConfig | ((env: TEnv) => HandlerCacheConfig);
|
|
371
371
|
|
|
372
|
-
/**
|
|
373
|
-
* Version override for this handler. Same meaning as
|
|
374
|
-
* `createRouter({ version })`, which it takes precedence over: this exact
|
|
375
|
-
* value is used for both of the router's versions.
|
|
376
|
-
*
|
|
377
|
-
* @default createRouter({ version }), else the router's build versions
|
|
378
|
-
*/
|
|
379
|
-
version?: string;
|
|
380
|
-
|
|
381
372
|
/**
|
|
382
373
|
* Nonce provider for Content Security Policy (CSP).
|
|
383
374
|
*
|