@rangojs/router 0.15.1 → 0.17.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/README.md +254 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/navigation-transaction.d.ts +6 -0
- package/dist/types/browser/partial-update.d.ts +11 -0
- package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
- package/dist/types/browser/rsc-router.d.ts +1 -1
- package/dist/types/browser/scroll-restoration.d.ts +7 -0
- package/dist/types/cache/cache-exec-scope.d.ts +18 -1
- package/dist/types/cache/cache-scope.d.ts +34 -5
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +53 -13
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +15 -3
- package/dist/types/client.d.ts +4 -4
- package/dist/types/client.rsc.d.ts +4 -5
- package/dist/types/components/DefaultDocument.d.ts +5 -2
- package/dist/types/context-var.d.ts +6 -5
- package/dist/types/escape-script.d.ts +1 -1
- package/dist/types/handles/MetaTags.d.ts +5 -4
- package/dist/types/handles/Scripts.d.ts +8 -7
- package/dist/types/handles/meta.d.ts +2 -2
- package/dist/types/handles/script.d.ts +17 -16
- package/dist/types/href-client.d.ts +1 -1
- package/dist/types/html.d.ts +12 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +3 -2
- package/dist/types/loader-store.d.ts +52 -0
- package/dist/types/reverse.d.ts +36 -4
- package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
- package/dist/types/route-definition/helpers-types.d.ts +36 -10
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +3 -1
- package/dist/types/router/debug-manifest.d.ts +3 -2
- package/dist/types/router/error-handling.d.ts +20 -0
- package/dist/types/router/handler-context.d.ts +6 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +3 -2
- package/dist/types/router/router-context.d.ts +3 -1
- package/dist/types/router/router-interfaces.d.ts +24 -2
- package/dist/types/router/router-options.d.ts +8 -5
- package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +37 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +3 -3
- package/dist/types/router/segment-resolution/mask-nested.d.ts +7 -7
- package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
- package/dist/types/router/segment-wrappers.d.ts +1 -1
- package/dist/types/rsc/response-route-handler.d.ts +1 -1
- package/dist/types/rsc/rsc-rendering.d.ts +1 -2
- package/dist/types/rsc/shell-capture-constants.d.ts +8 -0
- package/dist/types/rsc/shell-capture.d.ts +5 -4
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +10 -0
- package/dist/types/server/context.d.ts +33 -3
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +24 -21
- package/dist/types/ssr/index.d.ts +15 -0
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/generated-routes.d.ts +2 -2
- package/dist/types/testing/internal/context.d.ts +1 -1
- package/dist/types/testing/render-route.d.ts +32 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
- package/dist/types/testing/vitest.d.ts +35 -1
- package/dist/types/theme/ThemeScript.d.ts +3 -3
- package/dist/types/thenable-status.d.ts +9 -0
- package/dist/types/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +14 -0
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/response-types.d.ts +3 -12
- package/dist/types/use-loader.d.ts +12 -3
- package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
- package/dist/types/vite/rango.d.ts +1 -1
- package/dist/vite/index.js +884 -768
- package/package.json +4 -4
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +65 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +207 -97
- package/skills/caching/SKILL.md +295 -93
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +53 -31
- package/skills/cloudflare/SKILL.md +72 -20
- package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
- package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
- package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
- package/skills/comparison/SKILL.md +5 -0
- package/skills/comparison/references/framework-comparison.md +39 -33
- package/skills/composability/SKILL.md +38 -29
- package/skills/css/SKILL.md +10 -13
- package/skills/debug-manifest/SKILL.md +74 -31
- package/skills/defer-hydration/SKILL.md +2 -1
- package/skills/deployment-caching/SKILL.md +6 -1
- package/skills/document-cache/SKILL.md +92 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +53 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +43 -1
- package/skills/hooks/handle-and-actions.md +31 -16
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +37 -15
- package/skills/hooks/urls.md +7 -5
- package/skills/host-router/SKILL.md +64 -12
- package/skills/i18n/SKILL.md +60 -34
- package/skills/intercept/SKILL.md +161 -26
- package/skills/layout/SKILL.md +106 -50
- package/skills/links/SKILL.md +51 -19
- package/skills/loader/SKILL.md +242 -106
- package/skills/middleware/SKILL.md +119 -23
- package/skills/migrate-nextjs/SKILL.md +100 -45
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +49 -38
- package/skills/migrate-react-router/component-migration.md +25 -6
- package/skills/migrate-react-router/data-and-actions.md +18 -5
- package/skills/migrate-react-router/route-mapping.md +12 -6
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +28 -14
- package/skills/parallel/SKILL.md +75 -39
- package/skills/ppr/SKILL.md +345 -150
- package/skills/prerender/SKILL.md +189 -100
- package/skills/rango/SKILL.md +65 -34
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +165 -60
- package/skills/router-setup/SKILL.md +418 -240
- package/skills/scripts/SKILL.md +31 -20
- package/skills/server-actions/SKILL.md +220 -59
- package/skills/shell-manifest/SKILL.md +12 -7
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +68 -32
- package/skills/testing/cache-prerender.md +55 -29
- package/skills/testing/client-components.md +59 -6
- package/skills/testing/e2e-parity.md +12 -2
- package/skills/testing/flight.md +19 -11
- package/skills/testing/handles.md +8 -5
- package/skills/testing/loader.md +8 -5
- package/skills/testing/middleware.md +8 -3
- package/skills/testing/predicates.md +120 -0
- package/skills/testing/render-handler.md +70 -22
- package/skills/testing/response-routes.md +11 -4
- package/skills/testing/reverse-and-types.md +20 -14
- package/skills/testing/server-actions.md +12 -3
- package/skills/testing/server-tree.md +9 -0
- package/skills/testing/setup.md +9 -5
- package/skills/theme/SKILL.md +106 -29
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +126 -47
- package/skills/typesafety/generated-files-and-cli.md +26 -14
- package/skills/typesafety/params-and-search.md +44 -19
- package/skills/typesafety/route-types.md +50 -14
- package/skills/use-cache/SKILL.md +200 -82
- package/skills/vercel/SKILL.md +21 -11
- package/skills/view-transitions/SKILL.md +41 -20
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/index.ts +2 -2
- package/src/browser/navigation-bridge.ts +3 -4
- package/src/browser/navigation-transaction.ts +22 -2
- package/src/browser/partial-update.ts +75 -80
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +1 -1
- package/src/browser/scroll-restoration.ts +53 -6
- package/src/cache/cache-exec-scope.ts +37 -4
- package/src/cache/cache-key-utils.ts +1 -6
- package/src/cache/cache-runtime.ts +230 -61
- package/src/cache/cache-scope.ts +76 -12
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +132 -28
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/tag-invalidation.ts +4 -3
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +16 -3
- package/src/client.rsc.tsx +10 -9
- package/src/client.tsx +5 -4
- package/src/components/DefaultDocument.tsx +7 -2
- package/src/context-var.ts +6 -5
- package/src/escape-script.ts +1 -1
- package/src/handles/MetaTags.tsx +13 -11
- package/src/handles/Scripts.tsx +11 -10
- package/src/handles/meta.ts +2 -2
- package/src/handles/script.ts +17 -16
- package/src/href-client.ts +1 -1
- package/src/html.ts +12 -0
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +1 -0
- package/src/loader-store.ts +106 -0
- package/src/prerender/build-shell-capture.ts +8 -11
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +134 -42
- package/src/route-definition/helpers-types.ts +36 -10
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +2 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +34 -25
- package/src/router/intercept-resolution.ts +125 -39
- package/src/router/loader-resolution.ts +56 -11
- package/src/router/match-api.ts +12 -17
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +93 -10
- package/src/router/match-middleware/cache-store.ts +17 -90
- package/src/router/middleware-types.ts +3 -5
- package/src/router/middleware.ts +52 -9
- package/src/router/prerender-match.ts +53 -19
- package/src/router/route-snapshot.ts +11 -1
- package/src/router/router-context.ts +2 -0
- package/src/router/router-interfaces.ts +25 -2
- package/src/router/router-options.ts +8 -5
- package/src/router/segment-resolution/fresh.ts +44 -12
- package/src/router/segment-resolution/loader-cache.ts +205 -45
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +6 -6
- package/src/router/segment-resolution/mask-nested.ts +7 -7
- package/src/router/segment-resolution/revalidation.ts +34 -6
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +8 -45
- package/src/rsc/shell-capture-constants.ts +13 -0
- package/src/rsc/shell-capture.ts +111 -90
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +11 -0
- package/src/server/context.ts +88 -11
- package/src/server/cookie-store.ts +5 -4
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +35 -30
- package/src/ssr/index.tsx +40 -7
- package/src/ssr-suspension-warning.ts +2 -13
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/generated-routes.ts +2 -2
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/internal/flight-client-globals.ts +7 -5
- package/src/testing/render-route.tsx +85 -24
- package/src/testing/run-loader.ts +5 -1
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +14 -12
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/thenable-status.ts +16 -0
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +14 -0
- package/src/types/loader-types.ts +8 -6
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +24 -4
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
|
@@ -120,12 +120,8 @@ export async function resolveLoaders<TEnv>(
|
|
|
120
120
|
// loading-disabled entries consult it), so normal requests are unchanged.
|
|
121
121
|
const emitStreaming = !loadingDisabled || isShellCaptureActive();
|
|
122
122
|
|
|
123
|
-
// PPR
|
|
124
|
-
//
|
|
125
|
-
// executed at capture (container bakes, nested pending promises hole at the
|
|
126
|
-
// consumer's Suspense) and overlay-pinned from the shell snapshot on a HIT.
|
|
127
|
-
// Renderable loading() keeps the LIVE lane (masked at capture, always
|
|
128
|
-
// fresh). Computed per entry; resolveLoaderData applies the policy.
|
|
123
|
+
// PPR bake/seed key input only; lane rule: see resolveLoaderData
|
|
124
|
+
// (loader-cache.ts). Unflagged loaders mask at capture regardless of the key.
|
|
129
125
|
const bakeLane = !entryLoadingMasksLoaders(entry.loading);
|
|
130
126
|
|
|
131
127
|
// Error context for wrapLoaderPromise: without it, a throwing DSL loader never
|
|
@@ -146,9 +142,9 @@ export async function resolveLoaders<TEnv>(
|
|
|
146
142
|
// barrier), and the loader body can call rendered() before the await below
|
|
147
143
|
// is reached.
|
|
148
144
|
//
|
|
149
|
-
// Shell-capture renders await too: flagged loaders
|
|
150
|
-
// (
|
|
151
|
-
//
|
|
145
|
+
// Shell-capture renders await too: flagged loaders execute at capture
|
|
146
|
+
// (lane rule: see resolveLoaderData, loader-cache.ts), so their handle
|
|
147
|
+
// pushes must land in the prelude.
|
|
152
148
|
const awaitedIndices = registerAwaitBeforeFlushIds(loaderEntries);
|
|
153
149
|
|
|
154
150
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
@@ -276,6 +272,16 @@ export interface ResolveSegmentOptions {
|
|
|
276
272
|
* so error boundaries keep catching at request time.
|
|
277
273
|
*/
|
|
278
274
|
throwOnError?: boolean;
|
|
275
|
+
/**
|
|
276
|
+
* The matched route's full entry chain. resolveAllSegments defaults it to
|
|
277
|
+
* its `entries`; withCacheLookup passes it when it resolves only the entries
|
|
278
|
+
* above a cache() boundary. An orphan list can hold an entry of the chain: a
|
|
279
|
+
* bare cache() marker sits in its layout's layout[] (it wraps that layout's
|
|
280
|
+
* routes) and is the parent of the routes after it. For those routes it
|
|
281
|
+
* resolves once, as a chain entry, below the cache() header latch (issue
|
|
282
|
+
* #918: resolved twice on a MISS, once and discarded on a HIT).
|
|
283
|
+
*/
|
|
284
|
+
chain?: readonly EntryData[];
|
|
279
285
|
}
|
|
280
286
|
|
|
281
287
|
/**
|
|
@@ -349,6 +355,7 @@ export async function resolveSegment<TEnv>(
|
|
|
349
355
|
}
|
|
350
356
|
|
|
351
357
|
for (const orphan of entry.layout) {
|
|
358
|
+
if (options?.chain?.includes(orphan)) continue;
|
|
352
359
|
const orphanSegments = await resolveOrphanLayout(
|
|
353
360
|
orphan,
|
|
354
361
|
params,
|
|
@@ -578,6 +585,26 @@ export async function resolveOrphanLayout<TEnv>(
|
|
|
578
585
|
resolvedParallelEntries.add(parallelEntry.id);
|
|
579
586
|
}
|
|
580
587
|
|
|
588
|
+
// Routeless entries nested in this one (a layout() after a bare cache()
|
|
589
|
+
// marker, a wrapper inside a routeless wrapper) wrap the same content one
|
|
590
|
+
// level deeper. Issue #918: they were never rendered. None is in the
|
|
591
|
+
// matched chain: that would put this orphan in it, and resolveSegment
|
|
592
|
+
// skips those.
|
|
593
|
+
for (const nested of orphan.layout) {
|
|
594
|
+
segments.push(
|
|
595
|
+
...(await resolveOrphanLayout(
|
|
596
|
+
nested,
|
|
597
|
+
params,
|
|
598
|
+
context,
|
|
599
|
+
loaderPromises,
|
|
600
|
+
belongsToRoute,
|
|
601
|
+
deps,
|
|
602
|
+
options,
|
|
603
|
+
routeKey,
|
|
604
|
+
)),
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
|
|
581
608
|
return segments;
|
|
582
609
|
}
|
|
583
610
|
|
|
@@ -736,6 +763,10 @@ export async function resolveAllSegments<TEnv>(
|
|
|
736
763
|
): Promise<ResolvedSegment[]> {
|
|
737
764
|
const allSegments: ResolvedSegment[] = [];
|
|
738
765
|
const seenIds = new Set<string>();
|
|
766
|
+
const segmentOptions: ResolveSegmentOptions = {
|
|
767
|
+
...options,
|
|
768
|
+
chain: options?.chain ?? entries,
|
|
769
|
+
};
|
|
739
770
|
|
|
740
771
|
// ppr routes are document-scoped cached territory: the whole chain (root
|
|
741
772
|
// layout down to the page) bakes into the shared shell, so the header-write
|
|
@@ -760,8 +791,9 @@ export async function resolveAllSegments<TEnv>(
|
|
|
760
791
|
// Set ALS flag when entering a cache() boundary so that ctx.get()
|
|
761
792
|
// can guard non-cacheable variable reads. Also latch the header-write
|
|
762
793
|
// scope (response-level side effects — headers/cookies/status).
|
|
763
|
-
// Persists for all descendant entries.
|
|
764
|
-
|
|
794
|
+
// Persists for all descendant entries. A route entry carries `cache` when
|
|
795
|
+
// cache() is among its own children (dsl-helpers.ts cache()).
|
|
796
|
+
if (entry.cache) {
|
|
765
797
|
const store = RangoContext.getStore();
|
|
766
798
|
if (store) store.insideCacheScope = true;
|
|
767
799
|
latchCachedHeaderScope("cache", routeKey);
|
|
@@ -779,7 +811,7 @@ export async function resolveAllSegments<TEnv>(
|
|
|
779
811
|
loaderPromises,
|
|
780
812
|
deps,
|
|
781
813
|
false,
|
|
782
|
-
|
|
814
|
+
segmentOptions,
|
|
783
815
|
),
|
|
784
816
|
(seg) => [seg],
|
|
785
817
|
deps,
|
|
@@ -16,10 +16,28 @@
|
|
|
16
16
|
* On hit: returns cached data directly, skips loader execution.
|
|
17
17
|
* On stale hit (SWR): returns stale data, schedules background revalidation.
|
|
18
18
|
* On miss: executes loader, schedules non-blocking cache write.
|
|
19
|
+
*
|
|
20
|
+
* Handle pushes (`ctx.use(Handle)(...)` in the loader body) are a side effect
|
|
21
|
+
* the HIT must reproduce: the MISS records the pushes of the body and of the
|
|
22
|
+
* loaders it awaits via ctx.use into the entry's `handles` blob (the
|
|
23
|
+
* "use cache" capture/encode), and every HIT — stale included — replays
|
|
24
|
+
* them, each loader's pushes at most once per request (a dependency another
|
|
25
|
+
* reader already ran keeps its live pushes). See replayLoaderHandles.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
|
-
import
|
|
28
|
+
import {
|
|
29
|
+
getCurrentLoaderBodyId,
|
|
30
|
+
isInsideLoaderBody,
|
|
31
|
+
type LoaderEntry,
|
|
32
|
+
} from "../../server/context.js";
|
|
22
33
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
34
|
+
import type { HandleStore } from "../../server/handle-store.js";
|
|
35
|
+
import type { CacheItemResult } from "../../cache/types.js";
|
|
36
|
+
import {
|
|
37
|
+
startHandleCapture,
|
|
38
|
+
type HandleCapture,
|
|
39
|
+
} from "../../cache/handle-capture.js";
|
|
40
|
+
import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
|
|
23
41
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
24
42
|
import {
|
|
25
43
|
getRequestContext,
|
|
@@ -121,6 +139,73 @@ function getLoaderStore(
|
|
|
121
139
|
return resolveCacheStore(cacheConfig.options.store);
|
|
122
140
|
}
|
|
123
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Replay a loader-cache entry's recorded handle pushes on a HIT.
|
|
144
|
+
*
|
|
145
|
+
* Pushed to the CURRENT owning segment (the recorded ids are the MISS
|
|
146
|
+
* request's), in recorded order.
|
|
147
|
+
*
|
|
148
|
+
* One delivery per loader per request: each recorded group is one loader
|
|
149
|
+
* body's pushes (recordOwnerKey), and `claim` (setupLoaderAccess
|
|
150
|
+
* _claimLoaderPushes) skips a group whose loader already ran in this request
|
|
151
|
+
* — a sibling loader or the handler read the dependency, and its live pushes
|
|
152
|
+
* stand — or that another replay delivered. A claimed loader that runs later
|
|
153
|
+
* (loaders stay live) replaces its replayed values with its live pushes
|
|
154
|
+
* (HandleStore.pushReplayed).
|
|
155
|
+
*
|
|
156
|
+
* Deliberately NOT inside the loader's body scope: a stale hit's background
|
|
157
|
+
* revalidation of the same loader can be running with a diverting capture
|
|
158
|
+
* keyed on that body scope (executeLoaderData), which would swallow the
|
|
159
|
+
* replay; and the store reads the body scope to tell a live push from a
|
|
160
|
+
* replayed one.
|
|
161
|
+
* Outside it, a PPR shell capture classifies replayed pushes as
|
|
162
|
+
* loader-scoped and unbaked (shell-capture.ts), so a shell HIT gets them
|
|
163
|
+
* from the loader's own re-run, not twice.
|
|
164
|
+
*/
|
|
165
|
+
async function replayLoaderHandles(
|
|
166
|
+
encoded: string,
|
|
167
|
+
handleStore: HandleStore,
|
|
168
|
+
segmentId: string,
|
|
169
|
+
claim: ((loaderId: string) => boolean) | undefined,
|
|
170
|
+
): Promise<void> {
|
|
171
|
+
const recorded = await decodeHandles(encoded);
|
|
172
|
+
if (!recorded) return;
|
|
173
|
+
const delivers = new Map<string, boolean>();
|
|
174
|
+
for (const key in recorded) {
|
|
175
|
+
const owner = key.slice(key.indexOf(":") + 1);
|
|
176
|
+
let deliver = delivers.get(owner);
|
|
177
|
+
if (deliver === undefined) {
|
|
178
|
+
deliver = claim ? claim(owner) : true;
|
|
179
|
+
delivers.set(owner, deliver);
|
|
180
|
+
}
|
|
181
|
+
if (!deliver) continue;
|
|
182
|
+
for (const [handleName, values] of Object.entries(recorded[key])) {
|
|
183
|
+
for (const value of values) {
|
|
184
|
+
handleStore.pushReplayed(handleName, segmentId, value, owner);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Record key for the MISS capture: run-length groups by the innermost loader
|
|
192
|
+
* body, `${seq}:${loaderId}` — the cached loader or a dependency it awaits
|
|
193
|
+
* via ctx.use. Groups keep push order across bodies; the owner lets the
|
|
194
|
+
* replay deliver each loader's pushes at most once per request.
|
|
195
|
+
*/
|
|
196
|
+
function recordOwnerKey(cachedLoaderId: string): () => string {
|
|
197
|
+
let seq = 0;
|
|
198
|
+
let owner: string | undefined;
|
|
199
|
+
return () => {
|
|
200
|
+
const current = getCurrentLoaderBodyId() ?? cachedLoaderId;
|
|
201
|
+
if (current !== owner) {
|
|
202
|
+
owner = current;
|
|
203
|
+
seq++;
|
|
204
|
+
}
|
|
205
|
+
return `${seq}:${current}`;
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
124
209
|
/**
|
|
125
210
|
* Resolve loader data with optional caching.
|
|
126
211
|
*
|
|
@@ -131,6 +216,35 @@ function getLoaderStore(
|
|
|
131
216
|
* (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
|
|
132
217
|
* so it emits no loader phase (the loader did not execute; the hit is only a
|
|
133
218
|
* LoaderCache debug log).
|
|
219
|
+
*
|
|
220
|
+
* PPR loader lane rule — the canonical source statement; other sites point
|
|
221
|
+
* here (consumer docs: skills/ppr/SKILL.md "The loader lane rule";
|
|
222
|
+
* docs/design/loader-container-bake.md). This is the single funnel every
|
|
223
|
+
* loader segment path routes through (fresh resolveLoaders, cache-hit
|
|
224
|
+
* resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation, and
|
|
225
|
+
* intercept loaders in intercept-resolution.ts, which pass no bake key and
|
|
226
|
+
* never run under a shell capture), so the lane is decided here, per LOADER,
|
|
227
|
+
* never by the entry's loading():
|
|
228
|
+
*
|
|
229
|
+
* - BAKE lane: `loader(Def, { ssr: false })` (awaitBeforeFlush) with a
|
|
230
|
+
* `bakeSegmentKey` from the caller. The loader EXECUTES at capture (the
|
|
231
|
+
* flag's "data in the HTML before first flush" maps to the frozen prelude)
|
|
232
|
+
* and its settled non-promise data bakes into the shell. Promises nested in
|
|
233
|
+
* plain objects/arrays are masked (mask-nested.ts) and stay live holes at
|
|
234
|
+
* the consumer's own Suspense — per-request material must be promise-shaped
|
|
235
|
+
* (the #692 cross-session scar). The masked container registers on
|
|
236
|
+
* `_shellCaptureLoaderRecords`: it holds the capture gate (bounded by
|
|
237
|
+
* `ppr.captureTimeout`) and pins into the snapshot's loader family, and on a
|
|
238
|
+
* HIT the recorded container is overlaid onto the fresh run (below) so the
|
|
239
|
+
* payload matches the frozen prelude byte-for-byte. cookies()/headers() in
|
|
240
|
+
* its body (nested promise bodies included) trip the capture guard.
|
|
241
|
+
* - LIVE lane: every other loader, whatever its entry's loading(). Never
|
|
242
|
+
* executes at capture: the slot gets a never-resolving promise
|
|
243
|
+
* (loader-mask.ts) and postpones at the reader's boundary — loading() or an
|
|
244
|
+
* inline Suspense — then streams fresh per request. A masked read with NO
|
|
245
|
+
* boundary above it root-postpones and the <body> sanity gate refuses the
|
|
246
|
+
* shell (eternal-MISS warning). A route whose loaders are ALL `ssr: false`
|
|
247
|
+
* needs no loading(): nothing masks.
|
|
134
248
|
*/
|
|
135
249
|
export function resolveLoaderData<TEnv>(
|
|
136
250
|
loaderEntry: LoaderEntry,
|
|
@@ -141,42 +255,8 @@ export function resolveLoaderData<TEnv>(
|
|
|
141
255
|
// One ALS read serves the capture check, the record registration, and the
|
|
142
256
|
// seed lookup — this runs for every loader on every request.
|
|
143
257
|
const reqCtx = _getRequestContext();
|
|
144
|
-
// PPR shell capture policy — gated here, the single funnel every loader
|
|
145
|
-
// segment path routes through (fresh resolveLoaders, cache-hit
|
|
146
|
-
// resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation).
|
|
147
|
-
//
|
|
148
|
-
// Two lanes (docs/design/loader-container-bake.md):
|
|
149
|
-
// - LIVE lane (entry has renderable loading(); no `bakeSegmentKey`): never
|
|
150
|
-
// execute during capture. The slot gets a never-resolving promise so the
|
|
151
|
-
// LoaderBoundary postpones (a hole). See loader-mask.ts.
|
|
152
|
-
// - BAKE lane (no renderable loading(); callers pass `bakeSegmentKey`):
|
|
153
|
-
// execute during capture exactly like axis 1 — the settled container bakes
|
|
154
|
-
// into the prelude, nested pending promises postpone at the consumer's own
|
|
155
|
-
// Suspense. The container promise is registered on the derived context so
|
|
156
|
-
// captureAndStoreShell pins it into the snapshot's loader family; on a
|
|
157
|
-
// shell HIT the recorded container is overlaid onto the fresh run so the
|
|
158
|
-
// payload matches the frozen prelude byte-for-byte.
|
|
159
258
|
if (isShellCaptureActive(reqCtx)) {
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// - `ssr: false` (awaitBeforeFlush) — the BAKE lane. The flag's
|
|
163
|
-
// document promise is "this loader's data is in the HTML before first
|
|
164
|
-
// flush"; under ppr the pre-flush HTML IS the frozen prelude, so the
|
|
165
|
-
// loader executes at capture and its SETTLED return bakes into the
|
|
166
|
-
// shell. Nested-promise SHAPE stays the liveness declaration: nested
|
|
167
|
-
// thenables are masked so their subtrees postpone as holes (per-request
|
|
168
|
-
// material must be promise-shaped — the #692 cross-session scar). The
|
|
169
|
-
// masked container registers on _shellCaptureLoaderRecords: it holds
|
|
170
|
-
// the capture gate (bounded by ppr.captureTimeout) and pins into the
|
|
171
|
-
// shell snapshot, so a HIT tail replays the baked value and the
|
|
172
|
-
// hydration payload matches the frozen prelude byte-for-byte.
|
|
173
|
-
// - Every other loader — LIVE unconditionally: masked with a
|
|
174
|
-
// never-resolving promise, postponed at its boundary (loading() or an
|
|
175
|
-
// inline Suspense), streamed fresh per request. A masked reader with NO
|
|
176
|
-
// boundary above it root-postpones and the <body> sanity gate refuses
|
|
177
|
-
// the shell (eternal-MISS warning) — the degrade for boundary-less ppr
|
|
178
|
-
// routes. A route whose loaders are ALL flagged therefore needs no
|
|
179
|
-
// loading() at all: nothing masks, the shell captures complete.
|
|
259
|
+
// Lane rule: see this function's JSDoc. Flagged = bake, else masked.
|
|
180
260
|
if (loaderEntry.awaitBeforeFlush && bakeSegmentKey) {
|
|
181
261
|
const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
|
|
182
262
|
// Pre-attach a no-op catch: a bake-lane rejection during capture must
|
|
@@ -317,6 +397,16 @@ function executeLoaderData<TEnv>(
|
|
|
317
397
|
const tags = resolveTags(loaderEntry);
|
|
318
398
|
recordRequestTags(tags);
|
|
319
399
|
|
|
400
|
+
// Handle pushes: the store and the owning segment are read synchronously
|
|
401
|
+
// at kickoff, as createLoaderExecutor does for the body's own pushes.
|
|
402
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
403
|
+
const owningSegmentId = internal._currentSegmentId;
|
|
404
|
+
// Record only pushes from THIS loader's body and the loaders it awaits via
|
|
405
|
+
// ctx.use: the handler and sibling loaders push into the same store
|
|
406
|
+
// concurrently. A dependency can still run on a HIT for another reader
|
|
407
|
+
// (memoized per request); the replay's claim keeps its pushes to one copy.
|
|
408
|
+
const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
|
|
409
|
+
|
|
320
410
|
const dataPromise = (async () => {
|
|
321
411
|
const codec = await getCodec();
|
|
322
412
|
const key = await resolveLoaderKey(
|
|
@@ -335,11 +425,63 @@ function executeLoaderData<TEnv>(
|
|
|
335
425
|
// The wrap is applied via wrapBackground (background path only); the
|
|
336
426
|
// foreground miss runs execute() directly since its context is present.
|
|
337
427
|
const requestCtxForExecute = getRequestContext();
|
|
338
|
-
|
|
428
|
+
|
|
429
|
+
// One readThroughItem call runs at most one execution — the foreground
|
|
430
|
+
// MISS or the background stale revalidation (flagged by wrapBackground)
|
|
431
|
+
// — so a single capture slot serves the setItem that follows it.
|
|
432
|
+
let capture: HandleCapture | undefined;
|
|
433
|
+
let revalidating = false;
|
|
434
|
+
let hitHandles: string | undefined;
|
|
435
|
+
const onCachedRead = (label: string, cached: CacheItemResult) => {
|
|
436
|
+
hitHandles = cached.handles;
|
|
437
|
+
debugLoaderCacheLog(`[LoaderCache] ${label}: ${key}`);
|
|
438
|
+
};
|
|
439
|
+
const execute = async (): Promise<any> => {
|
|
440
|
+
// A stale-hit revalidation runs on its own executor: it diverts its
|
|
441
|
+
// pushes (below), so sharing the page's memoized run of a dependency
|
|
442
|
+
// would take that dependency's pushes off the page.
|
|
443
|
+
const run = (revalidating && internal._runLoaderIsolated) || runMiss;
|
|
444
|
+
if (!handleStore) return run(loaderEntry.loader);
|
|
445
|
+
// A stale-hit revalidation diverts the body's pushes: the foreground
|
|
446
|
+
// already replayed the stale entry's, so the fresh ones belong only to
|
|
447
|
+
// the refreshed entry (not a duplicate in the live response).
|
|
448
|
+
const c = startHandleCapture(handleStore, {
|
|
449
|
+
accept: isOwnBodyPush,
|
|
450
|
+
divert: revalidating,
|
|
451
|
+
key: recordOwnerKey(loaderId),
|
|
452
|
+
});
|
|
453
|
+
capture = c.capture;
|
|
454
|
+
try {
|
|
455
|
+
return await run(loaderEntry.loader);
|
|
456
|
+
} finally {
|
|
457
|
+
c.stop();
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Flight encodes a rejected promise in the value or a handle push as an
|
|
462
|
+
// error row and completes normally. setItem runs after serialize and
|
|
463
|
+
// throws instead of storing it, into readThroughItem's cache-write /
|
|
464
|
+
// stale-revalidation report; a stale entry keeps serving.
|
|
465
|
+
const flightErrors: unknown[] = [];
|
|
466
|
+
const onFlightError = (error: unknown): void => {
|
|
467
|
+
flightErrors.push(error);
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const data = await readThroughItem({
|
|
339
471
|
getItem: (k) => store.getItem!(k),
|
|
340
|
-
|
|
472
|
+
// Handles ride the entry like "use cache" (encodeHandles: Flight, pending
|
|
473
|
+
// pushes awaited up to its timeout, the whole blob dropped on a timeout
|
|
474
|
+
// or a thrown encode). Encoded here, inside the deferred write, so a
|
|
475
|
+
// MISS response never waits on it.
|
|
476
|
+
setItem: async (k, v, o) => {
|
|
477
|
+
const handles = capture
|
|
478
|
+
? await encodeHandles(capture.data, onFlightError)
|
|
479
|
+
: "";
|
|
480
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
481
|
+
await store.setItem!(k, v, handles ? { ...o, handles } : o);
|
|
482
|
+
},
|
|
341
483
|
key,
|
|
342
|
-
execute
|
|
484
|
+
execute,
|
|
343
485
|
// The rango.background span (kind=loader-revalidation) wraps the WHOLE
|
|
344
486
|
// stale revalidation — the re-execution AND the serialize/setItem write
|
|
345
487
|
// (read-through-swr routes the full task through wrapBackground) — so
|
|
@@ -347,21 +489,39 @@ function executeLoaderData<TEnv>(
|
|
|
347
489
|
// store write all nest under one explanatory parent instead of dangling
|
|
348
490
|
// under the ended foreground phases. Inside runWithRequestContext so
|
|
349
491
|
// observePhase can read tracing on workerd (ALS detaches in waitUntil).
|
|
350
|
-
wrapBackground: (run) =>
|
|
351
|
-
|
|
492
|
+
wrapBackground: (run) => {
|
|
493
|
+
revalidating = true;
|
|
494
|
+
return runWithRequestContext(requestCtxForExecute, () =>
|
|
352
495
|
observePhase(PHASES.background("loader-revalidation"), run),
|
|
353
|
-
)
|
|
354
|
-
|
|
496
|
+
);
|
|
497
|
+
},
|
|
498
|
+
serialize: (d) => codec.serializeResult(d, onFlightError),
|
|
355
499
|
deserialize: (v) => codec.deserializeResult(v),
|
|
356
500
|
storeOptions: { ttl, swr, tags },
|
|
357
|
-
onHit: () =>
|
|
358
|
-
onStale: () =>
|
|
501
|
+
onHit: (cached) => onCachedRead("HIT", cached),
|
|
502
|
+
onStale: (cached) => onCachedRead("STALE", cached),
|
|
359
503
|
onMiss: () => debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`),
|
|
360
504
|
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
361
505
|
host: requestCtxForExecute,
|
|
362
506
|
});
|
|
507
|
+
|
|
508
|
+
// An entry written before handles were recorded has none: no replay.
|
|
509
|
+
if (hitHandles && handleStore && owningSegmentId) {
|
|
510
|
+
await replayLoaderHandles(
|
|
511
|
+
hitHandles,
|
|
512
|
+
handleStore,
|
|
513
|
+
owningSegmentId,
|
|
514
|
+
internal._claimLoaderPushes,
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
return data;
|
|
363
518
|
})();
|
|
364
519
|
|
|
520
|
+
// Keep the store open for the replay: a HIT runs no loader body, so nothing
|
|
521
|
+
// else holds the auxiliary lane while getItem/decode are in flight, and a
|
|
522
|
+
// push after full drain throws LateHandlePushError.
|
|
523
|
+
handleStore?.trackAuxiliary(dataPromise);
|
|
524
|
+
|
|
365
525
|
overrides.set(loaderId, dataPromise);
|
|
366
526
|
|
|
367
527
|
return dataPromise;
|
|
@@ -2,13 +2,10 @@
|
|
|
2
2
|
* PPR shell-capture loader masking.
|
|
3
3
|
*
|
|
4
4
|
* During a shell CAPTURE re-render (Axis 2, see docs/design/ppr-shell-resume.md)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a
|
|
8
|
-
*
|
|
9
|
-
* shell (prelude) captures only the fallback; the resumed serve pass runs the
|
|
10
|
-
* loaders fresh through the unchanged execution path and streams their output
|
|
11
|
-
* into the holes.
|
|
5
|
+
* live-lane loaders (lane rule: see resolveLoaderData, loader-cache.ts) must
|
|
6
|
+
* NOT execute; their value slot gets a never-resolving promise so the
|
|
7
|
+
* consuming Suspense subtree postpones as a hole, and the serve pass runs them
|
|
8
|
+
* fresh into it.
|
|
12
9
|
*
|
|
13
10
|
* Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
|
|
14
11
|
* the derived request context of the background capture task (shell-capture.ts) —
|
|
@@ -36,24 +33,17 @@ export function isShellCaptureActive(
|
|
|
36
33
|
return reqCtx?._shellCaptureRun === true;
|
|
37
34
|
}
|
|
38
35
|
|
|
39
|
-
// createMaskedLoaderPromise
|
|
40
|
-
//
|
|
41
|
-
//
|
|
36
|
+
// createMaskedLoaderPromise lives in the leaf module mask-nested.ts, beside
|
|
37
|
+
// maskNestedContainerThenables (also used by the capture handle-store push
|
|
38
|
+
// wrap in rsc/shell-capture.ts). Re-exported to keep the mask API in one place.
|
|
42
39
|
export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
43
40
|
|
|
44
41
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* the frozen prelude — while every other loader is LIVE (masked at capture,
|
|
51
|
-
* fresh on every serve), postponing at loading() or an inline Suspense.
|
|
52
|
-
* This helper still gates which entries pass a bake segment key for the
|
|
53
|
-
* legacy loading()-less shape and the HIT-tail seed overlay.
|
|
54
|
-
*
|
|
55
|
-
* Mirrors segment-system's isRenderableLoading so the mask decision and the
|
|
56
|
-
* tree's boundary placement can never disagree.
|
|
42
|
+
* True when the entry's loading() is renderable (mirrors segment-system's
|
|
43
|
+
* isRenderableLoading). Not the lane decision (lane rule: see
|
|
44
|
+
* resolveLoaderData, loader-cache.ts): callers only use it to decide whether
|
|
45
|
+
* an unflagged loader's segment key rides as the bake/seed key, which is inert
|
|
46
|
+
* at capture.
|
|
57
47
|
*/
|
|
58
48
|
export function entryLoadingMasksLoaders(loading: unknown): boolean {
|
|
59
49
|
return loading !== undefined && loading !== null && loading !== false;
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Loader-family capture snapshot: elide + overlay for bake-lane loader
|
|
3
3
|
* containers (docs/design/loader-container-bake.md).
|
|
4
4
|
*
|
|
5
|
-
* A loader
|
|
6
|
-
* capture; its settled container bakes into the prelude
|
|
7
|
-
*
|
|
5
|
+
* A bake-lane loader (lane rule: see resolveLoaderData, loader-cache.ts)
|
|
6
|
+
* executes during shell capture; its settled container bakes into the prelude
|
|
7
|
+
* while nested promises postpone as holes. To keep a HIT's fresh
|
|
8
8
|
* payload byte-identical to that frozen prelude, the capture pins the container
|
|
9
9
|
* in the shell snapshot:
|
|
10
10
|
*
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
import { isThenable } from "../../handles/is-thenable.js";
|
|
32
32
|
|
|
33
33
|
// Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
|
|
34
|
-
// (
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// (shared with the capture handle-store push wrap in rsc/shell-capture.ts).
|
|
35
|
+
// Re-exported here so loader-cache and the unit tests keep one import site for
|
|
36
|
+
// the snapshot family.
|
|
37
37
|
export { maskNestedContainerThenables } from "./mask-nested.js";
|
|
38
38
|
|
|
39
39
|
/**
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Capture-side nested-thenable masking — the mechanism behind "nested-promise
|
|
3
3
|
* shape is the liveness declaration" for BOTH bake-lane loader containers
|
|
4
|
-
* (loader-cache.ts) and pushed handle containers (
|
|
5
|
-
*
|
|
4
|
+
* (loader-cache.ts) and pushed handle containers (the capture handle store's
|
|
5
|
+
* push wrap in rsc/shell-capture.ts).
|
|
6
6
|
*
|
|
7
|
-
* Deliberately a LEAF module:
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Deliberately a LEAF module: loader-mask (the other natural home) imports
|
|
8
|
+
* request-context, so any funnel importing the mask from there inherits that
|
|
9
|
+
* graph. This module imports only is-thenable.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { isThenable } from "../../handles/is-thenable.js";
|
|
@@ -33,8 +33,8 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
33
33
|
* Deep-copy a container with every NESTED thenable replaced by a masked
|
|
34
34
|
* (never-resolving) promise. Applied during shell capture to (a) bake-lane
|
|
35
35
|
* loader containers (loader-cache.ts) and (b) pushed handle containers
|
|
36
|
-
* (
|
|
37
|
-
* per-request data by promise SHAPE.
|
|
36
|
+
* (shell-capture.ts capture store push wrap) — the two rango-owned funnels
|
|
37
|
+
* where consumers declare per-request data by promise SHAPE.
|
|
38
38
|
*
|
|
39
39
|
* Why: a nested promise that happened to SETTLE before the capture's quiet
|
|
40
40
|
* window closed used to bake its value into the SHARED shell (and, for
|
|
@@ -215,11 +215,11 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
215
215
|
// onError/loader.error. isPartial flags the reporting phase accordingly.
|
|
216
216
|
const errorContext = { ...buildLoaderErrorContext(ctx), isPartial: true };
|
|
217
217
|
|
|
218
|
-
// PPR
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
218
|
+
// PPR bake/seed key, same as fresh.ts (lane rule: see resolveLoaderData,
|
|
219
|
+
// loader-cache.ts). The key must thread through EVERY resolveLoaderData
|
|
220
|
+
// funnel — omitting it here made a capture-active context
|
|
221
|
+
// whole-container-mask every loader on this path (a never-settling promise
|
|
222
|
+
// for a loader that should have executed).
|
|
223
223
|
// Outside capture the key only activates the _shellLoaderSeed overlay,
|
|
224
224
|
// which document-only serveShellHit seeds — inert for partial requests.
|
|
225
225
|
const bakeLane = !entryLoadingMasksLoaders(entry.loading);
|
|
@@ -934,6 +934,8 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
934
934
|
deps: SegmentResolutionDeps<TEnv>,
|
|
935
935
|
actionContext?: ActionContext,
|
|
936
936
|
stale?: boolean,
|
|
937
|
+
/** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
|
|
938
|
+
chain?: readonly EntryData[],
|
|
937
939
|
): Promise<SegmentRevalidationResult> {
|
|
938
940
|
const segments: ResolvedSegment[] = [];
|
|
939
941
|
const matchedIds: string[] = [];
|
|
@@ -1066,6 +1068,7 @@ export async function resolveSegmentWithRevalidation<TEnv>(
|
|
|
1066
1068
|
matchedIds.push(...parallelResult.matchedIds);
|
|
1067
1069
|
|
|
1068
1070
|
for (const orphan of entry.layout) {
|
|
1071
|
+
if (chain?.includes(orphan)) continue;
|
|
1069
1072
|
const orphanResult = await resolveOrphanLayoutWithRevalidation(
|
|
1070
1073
|
orphan,
|
|
1071
1074
|
params,
|
|
@@ -1274,6 +1277,27 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
|
|
|
1274
1277
|
segments.push(...parallelResult.segments);
|
|
1275
1278
|
matchedIds.push(...parallelResult.matchedIds);
|
|
1276
1279
|
|
|
1280
|
+
// Nested routeless entries; see resolveOrphanLayout (fresh.ts).
|
|
1281
|
+
for (const nested of orphan.layout) {
|
|
1282
|
+
const nestedResult = await resolveOrphanLayoutWithRevalidation(
|
|
1283
|
+
nested,
|
|
1284
|
+
params,
|
|
1285
|
+
context,
|
|
1286
|
+
clientSegmentIds,
|
|
1287
|
+
prevParams,
|
|
1288
|
+
request,
|
|
1289
|
+
prevUrl,
|
|
1290
|
+
nextUrl,
|
|
1291
|
+
routeKey,
|
|
1292
|
+
belongsToRoute,
|
|
1293
|
+
deps,
|
|
1294
|
+
actionContext,
|
|
1295
|
+
stale,
|
|
1296
|
+
);
|
|
1297
|
+
segments.push(...nestedResult.segments);
|
|
1298
|
+
matchedIds.push(...nestedResult.matchedIds);
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1277
1301
|
return { segments, matchedIds };
|
|
1278
1302
|
}
|
|
1279
1303
|
|
|
@@ -1296,6 +1320,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1296
1320
|
pathname: string,
|
|
1297
1321
|
deps: SegmentResolutionDeps<TEnv>,
|
|
1298
1322
|
stale?: boolean,
|
|
1323
|
+
/** The matched route's full chain; see ResolveSegmentOptions.chain (fresh.ts). */
|
|
1324
|
+
chain: readonly EntryData[] = entries,
|
|
1299
1325
|
): Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }> {
|
|
1300
1326
|
const allSegments: ResolvedSegment[] = [];
|
|
1301
1327
|
const matchedIds: string[] = [];
|
|
@@ -1325,7 +1351,8 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1325
1351
|
}
|
|
1326
1352
|
|
|
1327
1353
|
const nonParallelEntry = entry as Exclude<EntryData, { type: "parallel" }>;
|
|
1328
|
-
|
|
1354
|
+
// Same latch as resolveAllSegments (fresh.ts), route-level cache() included.
|
|
1355
|
+
if (entry.cache) {
|
|
1329
1356
|
const store = RangoContext.getStore();
|
|
1330
1357
|
if (store) store.insideCacheScope = true;
|
|
1331
1358
|
latchCachedHeaderScope("cache", routeKey);
|
|
@@ -1348,6 +1375,7 @@ export async function resolveAllSegmentsWithRevalidation<TEnv>(
|
|
|
1348
1375
|
deps,
|
|
1349
1376
|
actionContext,
|
|
1350
1377
|
stale,
|
|
1378
|
+
chain,
|
|
1351
1379
|
),
|
|
1352
1380
|
(seg) => ({ segments: [seg], matchedIds: [seg.id] }),
|
|
1353
1381
|
deps,
|
|
@@ -80,6 +80,8 @@ export interface SegmentWrappers<TEnv = any> {
|
|
|
80
80
|
interceptResult: { intercept: InterceptEntry; entry: EntryData } | null,
|
|
81
81
|
localRouteName: string,
|
|
82
82
|
pathname: string,
|
|
83
|
+
stale?: boolean,
|
|
84
|
+
chain?: readonly EntryData[],
|
|
83
85
|
) => Promise<{ segments: ResolvedSegment[]; matchedIds: string[] }>;
|
|
84
86
|
findInterceptForRoute: (
|
|
85
87
|
targetRouteKey: string,
|
|
@@ -205,6 +207,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
205
207
|
localRouteName: string,
|
|
206
208
|
pathname: string,
|
|
207
209
|
stale?: boolean,
|
|
210
|
+
chain?: readonly EntryData[],
|
|
208
211
|
): ReturnType<typeof _resolveAllSegmentsWithRevalidation> {
|
|
209
212
|
return _resolveAllSegmentsWithRevalidation(
|
|
210
213
|
entries,
|
|
@@ -222,6 +225,7 @@ export function createSegmentWrappers<TEnv = any>(
|
|
|
222
225
|
pathname,
|
|
223
226
|
segmentDeps,
|
|
224
227
|
stale,
|
|
228
|
+
chain,
|
|
225
229
|
);
|
|
226
230
|
}
|
|
227
231
|
|
|
@@ -155,9 +155,12 @@ function createFlightStage<TEnv>(
|
|
|
155
155
|
if (input.ctx.devDiscoveryEpoch !== undefined && input.payload.metadata) {
|
|
156
156
|
input.payload.metadata.devDiscoveryEpoch = input.ctx.devDiscoveryEpoch;
|
|
157
157
|
}
|
|
158
|
+
// Read once here: onError can fire outside the ALS frame.
|
|
159
|
+
const renderErrors = _getRequestContext()?._renderErrors;
|
|
158
160
|
const stream = input.ctx.renderToReadableStream<RscPayload>(input.payload, {
|
|
159
161
|
temporaryReferences: input.temporaryReferences,
|
|
160
162
|
onError: (error: unknown) => {
|
|
163
|
+
renderErrors?.push(error);
|
|
161
164
|
input.ctx.callOnError(error, "rendering", {
|
|
162
165
|
request: input.request,
|
|
163
166
|
url: input.url,
|