@rangojs/router 0.16.0 → 0.18.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 +255 -89
- package/dist/testing/vitest.js +252 -0
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -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-key-utils.d.ts +17 -2
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +132 -19
- package/dist/types/cache/cache-tag.d.ts +92 -7
- package/dist/types/cache/cf/cf-base64.d.ts +6 -0
- package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
- package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
- package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
- package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
- package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
- package/dist/types/cache/handle-capture.d.ts +41 -3
- package/dist/types/cache/handle-snapshot.d.ts +54 -13
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/segment-codec.d.ts +10 -3
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +110 -21
- package/dist/types/cache/tag-invalidation.d.ts +30 -3
- package/dist/types/cache/taint.d.ts +1 -1
- package/dist/types/cache/types.d.ts +220 -44
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- 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 +7 -4
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- 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 +33 -9
- package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
- package/dist/types/route-types.d.ts +1 -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/instrument.d.ts +8 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
- package/dist/types/router/middleware-types.d.ts +9 -3
- 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 +66 -1
- package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
- package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
- 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/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.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-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
- package/dist/types/rsc/shell-capture.d.ts +151 -19
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/ssr-setup.d.ts +6 -6
- package/dist/types/rsc/types.d.ts +17 -0
- package/dist/types/server/context.d.ts +139 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/handle-store.d.ts +26 -2
- package/dist/types/server/request-context.d.ts +182 -53
- package/dist/types/ssr/index.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/static-handler.d.ts +4 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/run-loader.d.ts +18 -3
- package/dist/types/testing/serve-shell-request.d.ts +132 -0
- package/dist/types/testing/shell-status.d.ts +23 -10
- 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/types/cache-types.d.ts +14 -10
- package/dist/types/types/handler-context.d.ts +28 -1
- package/dist/types/types/loader-types.d.ts +8 -6
- package/dist/types/urls/pattern-types.d.ts +22 -18
- 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/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1420 -1348
- package/package.json +2 -2
- package/skills/api-client/SKILL.md +22 -10
- package/skills/breadcrumbs/SKILL.md +75 -22
- package/skills/bundle-analysis/SKILL.md +22 -22
- package/skills/cache-guide/SKILL.md +306 -124
- package/skills/caching/SKILL.md +530 -98
- package/skills/catalog.json +16 -16
- package/skills/client-urls/SKILL.md +65 -38
- package/skills/cloudflare/SKILL.md +87 -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 +75 -57
- 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 +52 -31
- package/skills/deployment-caching/SKILL.md +11 -3
- package/skills/document-cache/SKILL.md +98 -28
- package/skills/fonts/SKILL.md +16 -8
- package/skills/handler-use/SKILL.md +55 -17
- package/skills/hooks/SKILL.md +28 -20
- package/skills/hooks/data.md +23 -3
- package/skills/hooks/handle-and-actions.md +34 -17
- package/skills/hooks/navigation.md +39 -10
- package/skills/hooks/outlets.md +13 -6
- package/skills/hooks/state.md +39 -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 +386 -128
- package/skills/middleware/SKILL.md +129 -26
- package/skills/migrate-nextjs/SKILL.md +156 -70
- package/skills/migrate-nextjs/backend-host-swap.md +5 -2
- package/skills/migrate-react-router/SKILL.md +51 -39
- package/skills/migrate-react-router/component-migration.md +28 -7
- package/skills/migrate-react-router/data-and-actions.md +21 -6
- package/skills/migrate-react-router/route-mapping.md +19 -10
- package/skills/mime-routes/SKILL.md +12 -7
- package/skills/observability/SKILL.md +92 -14
- package/skills/parallel/SKILL.md +88 -47
- package/skills/ppr/SKILL.md +866 -379
- package/skills/prerender/SKILL.md +210 -108
- package/skills/rango/SKILL.md +115 -65
- package/skills/react-compiler/SKILL.md +6 -5
- package/skills/response-routes/SKILL.md +133 -25
- package/skills/route/SKILL.md +170 -60
- package/skills/router-setup/SKILL.md +420 -240
- package/skills/scripts/SKILL.md +39 -21
- package/skills/server-actions/SKILL.md +231 -59
- package/skills/shell-manifest/SKILL.md +28 -14
- package/skills/streams-and-websockets/SKILL.md +32 -14
- package/skills/tailwind/SKILL.md +37 -33
- package/skills/testing/SKILL.md +73 -33
- package/skills/testing/cache-prerender.md +60 -33
- 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 +26 -6
- 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/serve-shell-request.md +188 -0
- 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 +155 -30
- package/skills/typesafety/SKILL.md +20 -9
- package/skills/typesafety/env-and-bindings.md +137 -50
- 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 +224 -83
- package/skills/vercel/SKILL.md +35 -13
- package/skills/view-transitions/SKILL.md +42 -21
- package/src/__augment-tests__/augmented.check.ts +26 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- 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 +35 -25
- package/src/browser/react/ScrollRestoration.tsx +6 -6
- package/src/browser/rsc-router.tsx +6 -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 +23 -8
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +264 -68
- package/src/cache/cache-scope.ts +419 -43
- package/src/cache/cache-tag.ts +375 -11
- package/src/cache/cf/cf-base64.ts +36 -4
- package/src/cache/cf/cf-cache-constants.ts +12 -0
- package/src/cache/cf/cf-cache-store.ts +990 -348
- package/src/cache/cf/cf-cache-types.ts +32 -4
- package/src/cache/cf/cf-shell-frame.ts +258 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
- package/src/cache/document-cache.ts +15 -0
- package/src/cache/handle-capture.ts +90 -10
- package/src/cache/handle-snapshot.ts +133 -28
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/segment-codec.ts +26 -7
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +108 -22
- package/src/cache/taint.ts +1 -1
- package/src/cache/types.ts +225 -44
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- 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 +7 -4
- package/src/prerender/build-shell-capture.ts +73 -22
- package/src/reverse.ts +44 -4
- package/src/route-definition/dsl-helpers.ts +121 -33
- package/src/route-definition/helpers-types.ts +33 -9
- package/src/route-definition/resolve-handler-use.ts +121 -6
- package/src/route-types.ts +0 -3
- package/src/router/debug-manifest.ts +10 -2
- package/src/router/error-handling.ts +51 -15
- package/src/router/handler-context.ts +71 -28
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +141 -43
- package/src/router/loader-resolution.ts +115 -39
- package/src/router/match-api.ts +40 -26
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/background-revalidation.ts +161 -96
- package/src/router/match-middleware/cache-lookup.ts +110 -10
- package/src/router/match-middleware/cache-store.ts +48 -101
- package/src/router/middleware-types.ts +9 -6
- package/src/router/middleware.ts +75 -13
- package/src/router/prerender-match.ts +59 -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 +62 -21
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +407 -76
- package/src/router/segment-resolution/loader-mask.ts +12 -22
- package/src/router/segment-resolution/loader-snapshot.ts +208 -56
- package/src/router/segment-resolution/mask-nested.ts +138 -44
- package/src/router/segment-resolution/revalidation.ts +61 -16
- package/src/router/segment-wrappers.ts +4 -0
- package/src/rsc/full-payload.ts +25 -1
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/progressive-enhancement.ts +3 -2
- package/src/rsc/render-pipeline.ts +3 -0
- package/src/rsc/response-cache-serve.ts +9 -6
- package/src/rsc/response-route-handler.ts +11 -3
- package/src/rsc/rsc-rendering.ts +608 -225
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +73 -6
- package/src/rsc/shell-capture.ts +1068 -296
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/ssr-setup.ts +14 -8
- package/src/rsc/types.ts +18 -0
- package/src/server/context.ts +300 -32
- package/src/server/cookie-store.ts +87 -61
- package/src/server/handle-store.ts +232 -7
- package/src/server/request-context.ts +297 -67
- package/src/ssr/index.tsx +136 -44
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/static-handler.ts +4 -1
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +14 -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 +68 -6
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
- package/src/testing/vitest.ts +62 -1
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/theme/ThemeScript.tsx +3 -3
- package/src/types/cache-types.ts +14 -10
- package/src/types/handler-context.ts +28 -1
- package/src/types/loader-types.ts +8 -6
- package/src/urls/pattern-types.ts +22 -18
- package/src/urls/response-types.ts +3 -23
- package/src/use-loader.tsx +12 -3
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/plugins/use-cache-transform.ts +118 -74
- package/src/vite/rango.ts +7 -3
- package/src/vite/router-discovery.ts +14 -4
|
@@ -16,10 +16,46 @@
|
|
|
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
|
+
* Identity (#972): without a key() or a store keyGenerator the entry is
|
|
21
|
+
* shared across users, so a fill whose execution recorded a request-identity
|
|
22
|
+
* read fails and is not stored, whoever started that execution
|
|
23
|
+
* (server/context.ts recordLoaderIdentityRead).
|
|
24
|
+
*
|
|
25
|
+
* Handle pushes (`ctx.use(Handle)(...)` in the loader body) are a side effect
|
|
26
|
+
* the HIT must reproduce: the MISS records the pushes of the body and of the
|
|
27
|
+
* loaders it awaits via ctx.use into the entry's `handles` blob (the
|
|
28
|
+
* "use cache" capture/encode), and every HIT — stale included — replays
|
|
29
|
+
* them, each loader's pushes at most once per request (a dependency another
|
|
30
|
+
* reader already ran keeps its live pushes). See replayLoaderHandles.
|
|
31
|
+
*
|
|
32
|
+
* Tags too (#964): the entry stores its cache() tags plus every tag its
|
|
33
|
+
* execution recorded, those of the loader values it read included
|
|
34
|
+
* (cache-tag.ts "Recorded-tag sets"), so updateTag() of any of them drops it.
|
|
35
|
+
* Every HIT records the stored tags through the loader's tag owner
|
|
36
|
+
* (recordLoaderTags), so a route cache() record, PPR shell or document built
|
|
37
|
+
* over the HIT carries them, and a loader that reads this one takes them on.
|
|
38
|
+
* Once the binding started, a loader body's ctx.use of it gets the same
|
|
39
|
+
* dataPromise as a handler read (createLoaderExecutor's useLoader); a stale
|
|
40
|
+
* refresh does not (setupLoaderAccess _runLoaderIsolated).
|
|
19
41
|
*/
|
|
20
42
|
|
|
21
|
-
import
|
|
43
|
+
import {
|
|
44
|
+
getCurrentLoaderBodyId,
|
|
45
|
+
isInsideLoaderBody,
|
|
46
|
+
loaderCacheIdentityError,
|
|
47
|
+
type EntryData,
|
|
48
|
+
type LoaderEntry,
|
|
49
|
+
type LoaderIdentityRead,
|
|
50
|
+
} from "../../server/context.js";
|
|
22
51
|
import type { HandlerContext, InternalHandlerContext } from "../../types.js";
|
|
52
|
+
import type { HandleStore } from "../../server/handle-store.js";
|
|
53
|
+
import type { CacheItemResult } from "../../cache/types.js";
|
|
54
|
+
import {
|
|
55
|
+
startHandleCapture,
|
|
56
|
+
type HandleCapture,
|
|
57
|
+
} from "../../cache/handle-capture.js";
|
|
58
|
+
import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
|
|
23
59
|
import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
|
|
24
60
|
import {
|
|
25
61
|
getRequestContext,
|
|
@@ -41,7 +77,20 @@ import {
|
|
|
41
77
|
maskNestedContainerThenables,
|
|
42
78
|
overlayLoaderContainer,
|
|
43
79
|
} from "./loader-snapshot.js";
|
|
44
|
-
import {
|
|
80
|
+
import {
|
|
81
|
+
SHELL_BAKE_TAG_OWNER,
|
|
82
|
+
armLoaderTagSets,
|
|
83
|
+
captureRecordedTags,
|
|
84
|
+
flattenRecordedTags,
|
|
85
|
+
linkLoaderTags,
|
|
86
|
+
linkLoaderTagsTo,
|
|
87
|
+
linkRecordedTags,
|
|
88
|
+
markIdentityRead,
|
|
89
|
+
readValueTags,
|
|
90
|
+
recordedIdentityRead,
|
|
91
|
+
recordLoaderTags,
|
|
92
|
+
tagLoaderValue,
|
|
93
|
+
} from "../../cache/cache-tag.js";
|
|
45
94
|
import {
|
|
46
95
|
isShellCaptureActive,
|
|
47
96
|
createMaskedLoaderPromise,
|
|
@@ -80,7 +129,8 @@ function getDefaultLoaderCacheKey(
|
|
|
80
129
|
}
|
|
81
130
|
|
|
82
131
|
/**
|
|
83
|
-
* Resolve cache key using the shared 3-tier priority.
|
|
132
|
+
* Resolve cache key using the shared 3-tier priority. `declared`: a key() or
|
|
133
|
+
* store keyGenerator produced it, so it can carry request identity (#972).
|
|
84
134
|
*/
|
|
85
135
|
async function resolveLoaderKey(
|
|
86
136
|
loaderEntry: LoaderEntry,
|
|
@@ -88,7 +138,7 @@ async function resolveLoaderKey(
|
|
|
88
138
|
loaderId: string,
|
|
89
139
|
pathname: string,
|
|
90
140
|
params: Record<string, string>,
|
|
91
|
-
): Promise<string> {
|
|
141
|
+
): Promise<{ key: string; declared: boolean }> {
|
|
92
142
|
const options = loaderEntry.cache!.options;
|
|
93
143
|
// The host is part of the loader cache identity, matching the route-level
|
|
94
144
|
// cache (cache-scope getCacheKeyBase: `${host}${pathname}`) and "use cache"
|
|
@@ -97,8 +147,11 @@ async function resolveLoaderKey(
|
|
|
97
147
|
// loader data to another.
|
|
98
148
|
const host = getRequestContext()?.url?.host ?? "localhost";
|
|
99
149
|
const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
|
|
100
|
-
if (options === false) return defaultKey;
|
|
101
|
-
return
|
|
150
|
+
if (options === false) return { key: defaultKey, declared: false };
|
|
151
|
+
return {
|
|
152
|
+
key: await resolveCacheKey(options.key, store, defaultKey, "LoaderCache"),
|
|
153
|
+
declared: Boolean(options.key || store.keyGenerator),
|
|
154
|
+
};
|
|
102
155
|
}
|
|
103
156
|
|
|
104
157
|
/**
|
|
@@ -113,6 +166,39 @@ function resolveTags(loaderEntry: LoaderEntry): string[] | undefined {
|
|
|
113
166
|
return resolveTagsOption(options.tags, getRequestContext(), "LoaderCache");
|
|
114
167
|
}
|
|
115
168
|
|
|
169
|
+
const bindsLoaderCacheMemo = new WeakMap<EntryData, boolean>();
|
|
170
|
+
|
|
171
|
+
const hasLoaderCache = (loaders: readonly LoaderEntry[] | undefined) =>
|
|
172
|
+
loaders?.some((l) => l.cache !== undefined && l.cache.options !== false) ??
|
|
173
|
+
false;
|
|
174
|
+
|
|
175
|
+
function entryBindsLoaderCache(entry: EntryData): boolean {
|
|
176
|
+
let binds = bindsLoaderCacheMemo.get(entry);
|
|
177
|
+
if (binds === undefined) {
|
|
178
|
+
binds =
|
|
179
|
+
hasLoaderCache(entry.loader) ||
|
|
180
|
+
(entry.intercept?.some((i) => hasLoaderCache(i.loader)) ?? false) ||
|
|
181
|
+
(entry.layout?.some(entryBindsLoaderCache) ?? false) ||
|
|
182
|
+
Object.values(entry.parallel ?? {}).some(
|
|
183
|
+
(p) => p !== undefined && entryBindsLoaderCache(p),
|
|
184
|
+
);
|
|
185
|
+
bindsLoaderCacheMemo.set(entry, binds);
|
|
186
|
+
}
|
|
187
|
+
return binds;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Whether a request over the matched chain `entries` resolves a loader bound
|
|
192
|
+
* with its own cache(): on a chain entry, or on its parallel slots, orphan
|
|
193
|
+
* layouts or intercepts. The match arms the per-execution loader tag sets on
|
|
194
|
+
* it (cache-tag.ts armLoaderTagSets), before any loader starts, so a
|
|
195
|
+
* dependency the handler or another binding starts first still records.
|
|
196
|
+
* Memoized per entry.
|
|
197
|
+
*/
|
|
198
|
+
export function bindsLoaderCache(entries: readonly EntryData[]): boolean {
|
|
199
|
+
return entries.some(entryBindsLoaderCache);
|
|
200
|
+
}
|
|
201
|
+
|
|
116
202
|
function getLoaderStore(
|
|
117
203
|
loaderEntry: LoaderEntry,
|
|
118
204
|
): import("../../cache/types.js").SegmentCacheStore | null {
|
|
@@ -121,6 +207,104 @@ function getLoaderStore(
|
|
|
121
207
|
return resolveCacheStore(cacheConfig.options.store);
|
|
122
208
|
}
|
|
123
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Replay a loader-cache entry's recorded handle pushes on a HIT.
|
|
212
|
+
*
|
|
213
|
+
* Pushed to the CURRENT owning segment (the recorded ids are the MISS
|
|
214
|
+
* request's), in recorded order.
|
|
215
|
+
*
|
|
216
|
+
* One delivery per loader per request: each recorded group is one loader
|
|
217
|
+
* body's pushes (recordOwnerKey), and `claim` (setupLoaderAccess
|
|
218
|
+
* _claimLoaderPushes) skips a group whose loader already ran in this request
|
|
219
|
+
* — a sibling loader or the handler read the dependency, and its live pushes
|
|
220
|
+
* stand — or that another replay delivered. A claimed loader that runs later
|
|
221
|
+
* (loaders stay live) replaces its replayed values with its live pushes
|
|
222
|
+
* (HandleStore.pushReplayed).
|
|
223
|
+
*
|
|
224
|
+
* Deliberately NOT inside the loader's body scope: a stale hit's background
|
|
225
|
+
* revalidation of the same loader can be running with a diverting capture
|
|
226
|
+
* keyed on that body scope (executeLoaderData), which would swallow the
|
|
227
|
+
* replay; and the store reads the body scope to tell a live push from a
|
|
228
|
+
* replayed one.
|
|
229
|
+
* A PPR shell capture records the replayed pushes under this loader (its
|
|
230
|
+
* push funnel reads the pushReplayed owner, shell-capture.ts), so a shell
|
|
231
|
+
* HIT restores them owned: the claim above then skips this replay when the
|
|
232
|
+
* loader runs on that HIT, and its live pushes replace the restored ones.
|
|
233
|
+
*/
|
|
234
|
+
async function replayLoaderHandles(
|
|
235
|
+
encoded: string,
|
|
236
|
+
handleStore: HandleStore,
|
|
237
|
+
segmentId: string,
|
|
238
|
+
claim: ((loaderId: string) => boolean) | undefined,
|
|
239
|
+
): Promise<void> {
|
|
240
|
+
const recorded = await decodeHandles(encoded);
|
|
241
|
+
if (!recorded) return;
|
|
242
|
+
const delivers = new Map<string, boolean>();
|
|
243
|
+
for (const key in recorded) {
|
|
244
|
+
const owner = key.slice(key.indexOf(":") + 1);
|
|
245
|
+
let deliver = delivers.get(owner);
|
|
246
|
+
if (deliver === undefined) {
|
|
247
|
+
deliver = claim ? claim(owner) : true;
|
|
248
|
+
delivers.set(owner, deliver);
|
|
249
|
+
}
|
|
250
|
+
if (!deliver) continue;
|
|
251
|
+
for (const [handleName, values] of Object.entries(recorded[key])) {
|
|
252
|
+
for (const value of values) {
|
|
253
|
+
handleStore.pushReplayed(handleName, segmentId, value, owner);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Identity mark (#972): a declared-key entry whose MISS recorded a
|
|
261
|
+
* request-identity read stores that read ahead of its payload. A HIT skips the
|
|
262
|
+
* body, so without the mark an unkeyed loader reading this value on a HIT
|
|
263
|
+
* would store another user's data. The value string is this module's own
|
|
264
|
+
* (serialize/deserialize below), so the mark needs no store support; Flight
|
|
265
|
+
* and JSON payloads never start with "~".
|
|
266
|
+
*/
|
|
267
|
+
const IDENTITY_MARK = "~identity:";
|
|
268
|
+
|
|
269
|
+
function markIdentity(
|
|
270
|
+
value: string,
|
|
271
|
+
read: LoaderIdentityRead | undefined,
|
|
272
|
+
): string {
|
|
273
|
+
return read ? `${IDENTITY_MARK}${JSON.stringify(read)}\n${value}` : value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function unmarkIdentity(value: string): {
|
|
277
|
+
read: LoaderIdentityRead | undefined;
|
|
278
|
+
payload: string;
|
|
279
|
+
} {
|
|
280
|
+
if (!value.startsWith(IDENTITY_MARK))
|
|
281
|
+
return { read: undefined, payload: value };
|
|
282
|
+
const end = value.indexOf("\n");
|
|
283
|
+
return {
|
|
284
|
+
read: JSON.parse(value.slice(IDENTITY_MARK.length, end)),
|
|
285
|
+
payload: value.slice(end + 1),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Record key for the MISS capture: run-length groups by the innermost loader
|
|
291
|
+
* body, `${seq}:${loaderId}` — the cached loader or a dependency it awaits
|
|
292
|
+
* via ctx.use. Groups keep push order across bodies; the owner lets the
|
|
293
|
+
* replay deliver each loader's pushes at most once per request.
|
|
294
|
+
*/
|
|
295
|
+
function recordOwnerKey(cachedLoaderId: string): () => string {
|
|
296
|
+
let seq = 0;
|
|
297
|
+
let owner: string | undefined;
|
|
298
|
+
return () => {
|
|
299
|
+
const current = getCurrentLoaderBodyId() ?? cachedLoaderId;
|
|
300
|
+
if (current !== owner) {
|
|
301
|
+
owner = current;
|
|
302
|
+
seq++;
|
|
303
|
+
}
|
|
304
|
+
return `${seq}:${current}`;
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
124
308
|
/**
|
|
125
309
|
* Resolve loader data with optional caching.
|
|
126
310
|
*
|
|
@@ -131,6 +315,40 @@ function getLoaderStore(
|
|
|
131
315
|
* (observePhase; see instrument.ts). A cache HIT returns without calling ctx.use,
|
|
132
316
|
* so it emits no loader phase (the loader did not execute; the hit is only a
|
|
133
317
|
* LoaderCache debug log).
|
|
318
|
+
*
|
|
319
|
+
* PPR loader lane rule — the canonical source statement; other sites point
|
|
320
|
+
* here (consumer docs: skills/ppr/SKILL.md "The loader lane rule";
|
|
321
|
+
* docs/design/loader-container-bake.md). This is the single funnel every
|
|
322
|
+
* loader segment path routes through (fresh resolveLoaders, cache-hit
|
|
323
|
+
* resolveLoadersOnly, revalidation resolveLoadersOnlyWithRevalidation, and
|
|
324
|
+
* intercept loaders in intercept-resolution.ts, which pass no bake key and
|
|
325
|
+
* never run under a shell capture), so the lane is decided here, per LOADER,
|
|
326
|
+
* never by the entry's loading():
|
|
327
|
+
*
|
|
328
|
+
* - BAKE lane: `loader(Def, { ssr: false })` (LoaderEntry `bake`: the
|
|
329
|
+
* caller passes a `bakeSegmentKey` for it on document and navigation
|
|
330
|
+
* evaluations alike; a capture, a document evaluation, also has
|
|
331
|
+
* awaitBeforeFlush). The loader EXECUTES at capture (the
|
|
332
|
+
* flag's "data in the HTML before first flush" maps to the frozen prelude)
|
|
333
|
+
* and its settled non-promise data bakes into the shell. Promises nested in
|
|
334
|
+
* plain objects, arrays and JSX props are masked (mask-nested.ts) and stay
|
|
335
|
+
* live holes at the consumer's own Suspense — per-request material must be
|
|
336
|
+
* promise-shaped (the #692 cross-session scar). The masked container
|
|
337
|
+
* registers on `_shellCaptureLoaderRecords`: it holds the capture gate
|
|
338
|
+
* (bounded by `ppr.captureTimeout`) and pins into the snapshot's loader
|
|
339
|
+
* family, and on a HIT the recorded container is overlaid onto the fresh
|
|
340
|
+
* run (below) so the payload matches the frozen prelude outside the holes.
|
|
341
|
+
* A server-component element holding a promise is a hole as a whole, so its
|
|
342
|
+
* other props come from the fresh run (loader-snapshot.ts
|
|
343
|
+
* elideLoaderContainer). cookies()/headers() in its body (nested promise
|
|
344
|
+
* bodies included) trip the capture guard.
|
|
345
|
+
* - LIVE lane: every other loader, whatever its entry's loading(). Never
|
|
346
|
+
* executes at capture: the slot gets a never-resolving promise
|
|
347
|
+
* (loader-mask.ts) and postpones at the reader's boundary — loading() or an
|
|
348
|
+
* inline Suspense — then streams fresh per request. A masked read with NO
|
|
349
|
+
* boundary above it root-postpones and the <body> sanity gate refuses the
|
|
350
|
+
* shell (eternal-MISS warning). A route whose loaders are ALL `ssr: false`
|
|
351
|
+
* needs no loading(): nothing masks.
|
|
134
352
|
*/
|
|
135
353
|
export function resolveLoaderData<TEnv>(
|
|
136
354
|
loaderEntry: LoaderEntry,
|
|
@@ -141,42 +359,8 @@ export function resolveLoaderData<TEnv>(
|
|
|
141
359
|
// One ALS read serves the capture check, the record registration, and the
|
|
142
360
|
// seed lookup — this runs for every loader on every request.
|
|
143
361
|
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
362
|
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.
|
|
363
|
+
// Lane rule: see this function's JSDoc. Flagged = bake, else masked.
|
|
180
364
|
if (loaderEntry.awaitBeforeFlush && bakeSegmentKey) {
|
|
181
365
|
const containerPromise = executeLoaderData(loaderEntry, ctx, pathname);
|
|
182
366
|
// Pre-attach a no-op catch: a bake-lane rejection during capture must
|
|
@@ -189,6 +373,8 @@ export function resolveLoaderData<TEnv>(
|
|
|
189
373
|
);
|
|
190
374
|
maskedPromise.catch(() => {});
|
|
191
375
|
reqCtx?._shellCaptureLoaderRecords?.set(bakeSegmentKey, maskedPromise);
|
|
376
|
+
// Its data bakes into the shell, so its tags tag the shell.
|
|
377
|
+
linkLoaderTagsTo(SHELL_BAKE_TAG_OWNER, loaderEntry.loader.$$id, reqCtx);
|
|
192
378
|
return maskedPromise;
|
|
193
379
|
}
|
|
194
380
|
return createMaskedLoaderPromise();
|
|
@@ -200,31 +386,31 @@ export function resolveLoaderData<TEnv>(
|
|
|
200
386
|
const recorded = seed.get(bakeSegmentKey)!;
|
|
201
387
|
if (!recorded.holes) {
|
|
202
388
|
// Pin-first (hole-free record): the pinned container is what the
|
|
203
|
-
// payload serves
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
389
|
+
// payload serves (recorded paths win wholesale), resolved
|
|
390
|
+
// immediately. The body does not run: a HIT is rendered from the
|
|
391
|
+
// shell, like the handlers it replays, and the loader's settled
|
|
392
|
+
// pushes (and those of loaders it awaits) are restored from the
|
|
393
|
+
// record. Only a record whose capture saw a push it could not keep
|
|
394
|
+
// (`runs`) still runs the body, in the background, lifetime-extended
|
|
395
|
+
// so the runtime cannot cancel it when the stream closes first; its
|
|
396
|
+
// rejection is swallowed (the payload already matches the prelude,
|
|
397
|
+
// which a fresh error value never could).
|
|
211
398
|
//
|
|
212
399
|
// CONTRACT (deliberate divergence from the gated overlay's
|
|
213
400
|
// fresh-only-keys passthrough): a hole-free pin serves the pinned
|
|
214
|
-
// SHAPE wholesale
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
reqCtx?.executionContext?.waitUntil?.(fresh);
|
|
401
|
+
// SHAPE wholesale. A field the body would return now had no hole
|
|
402
|
+
// postponed for it at capture, so the prelude froze the
|
|
403
|
+
// without-that-field branch and the resume pass has nothing to
|
|
404
|
+
// fill. A field that is per-request must be promise-shaped at
|
|
405
|
+
// capture (masked -> hole marker -> holes: 1 -> the gated path
|
|
406
|
+
// below, which preserves fresh-only passthrough) or live behind
|
|
407
|
+
// loading(). Anything else is uncached nondeterminism in shell
|
|
408
|
+
// material — the documented drift residual.
|
|
409
|
+
if (recorded.runs) {
|
|
410
|
+
const fresh = executeLoaderData(loaderEntry, ctx, pathname);
|
|
411
|
+
fresh.catch(() => {});
|
|
412
|
+
reqCtx?.executionContext?.waitUntil?.(fresh);
|
|
413
|
+
}
|
|
228
414
|
return Promise.resolve(
|
|
229
415
|
overlayLoaderContainer(undefined, recorded.container),
|
|
230
416
|
);
|
|
@@ -288,7 +474,13 @@ function executeLoaderData<TEnv>(
|
|
|
288
474
|
internal._loaderCacheOriginalUse = originalUse;
|
|
289
475
|
ctx.use = ((item: any) => {
|
|
290
476
|
const cached = overrides!.get(item?.$$id);
|
|
291
|
-
if (cached)
|
|
477
|
+
if (cached) {
|
|
478
|
+
// A read is a consumption (#957, #964): see createLoaderExecutor's
|
|
479
|
+
// useLoader.
|
|
480
|
+
linkLoaderTags(item.$$id);
|
|
481
|
+
readValueTags(cached);
|
|
482
|
+
return cached;
|
|
483
|
+
}
|
|
292
484
|
return originalUse(item);
|
|
293
485
|
}) as typeof ctx.use;
|
|
294
486
|
}
|
|
@@ -315,11 +507,35 @@ function executeLoaderData<TEnv>(
|
|
|
315
507
|
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
316
508
|
const swr = swrWindow || undefined;
|
|
317
509
|
const tags = resolveTags(loaderEntry);
|
|
318
|
-
|
|
510
|
+
recordLoaderTags(loaderId, tags);
|
|
511
|
+
// Production arms at match, before any loader runs (match-api.ts,
|
|
512
|
+
// bindsLoaderCache); no production path relies on this. It only keeps a
|
|
513
|
+
// resolveLoaders call outside a match from running unarmed after this
|
|
514
|
+
// point (loaders it started earlier stay unrecorded).
|
|
515
|
+
armLoaderTagSets();
|
|
516
|
+
// An execution of this loader outside the binding (a reader started it
|
|
517
|
+
// first, a stale refresh) answers for the cache() tags too.
|
|
518
|
+
if (tags && tags.length > 0) {
|
|
519
|
+
internal._bindLoaderCacheTags?.(loaderId, new Set(tags));
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Handle pushes: the store and the owning segment are read synchronously
|
|
523
|
+
// at kickoff, as createLoaderExecutor does for the body's own pushes.
|
|
524
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
525
|
+
const owningSegmentId = internal._currentSegmentId;
|
|
526
|
+
// Record only pushes from THIS loader's body and the loaders it awaits via
|
|
527
|
+
// ctx.use: the handler and sibling loaders push into the same store
|
|
528
|
+
// concurrently. A dependency can still run on a HIT for another reader
|
|
529
|
+
// (memoized per request); the replay's claim keeps its pushes to one copy.
|
|
530
|
+
const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
|
|
531
|
+
// What a reader of this value takes on (tagLoaderValue below,
|
|
532
|
+
// readValueTags): the cache() tags, plus the entry's tags on a HIT or the
|
|
533
|
+
// execution's on a MISS.
|
|
534
|
+
const valueTags = new Set(tags);
|
|
319
535
|
|
|
320
536
|
const dataPromise = (async () => {
|
|
321
537
|
const codec = await getCodec();
|
|
322
|
-
const key = await resolveLoaderKey(
|
|
538
|
+
const { key, declared } = await resolveLoaderKey(
|
|
323
539
|
loaderEntry,
|
|
324
540
|
store,
|
|
325
541
|
loaderId,
|
|
@@ -335,11 +551,101 @@ function executeLoaderData<TEnv>(
|
|
|
335
551
|
// The wrap is applied via wrapBackground (background path only); the
|
|
336
552
|
// foreground miss runs execute() directly since its context is present.
|
|
337
553
|
const requestCtxForExecute = getRequestContext();
|
|
338
|
-
|
|
554
|
+
|
|
555
|
+
// One readThroughItem call runs at most one execution — the foreground
|
|
556
|
+
// MISS or the background stale revalidation (flagged by wrapBackground)
|
|
557
|
+
// — so one handle capture and one tag set serve the setItem after it.
|
|
558
|
+
// The entry's tags: its cache() tags plus what the execution recorded.
|
|
559
|
+
let capture: HandleCapture | undefined;
|
|
560
|
+
const bodyTags = new Set(tags);
|
|
561
|
+
let revalidating = false;
|
|
562
|
+
let hitHandles: string | undefined;
|
|
563
|
+
const onCachedRead = (label: string, cached: CacheItemResult) => {
|
|
564
|
+
hitHandles = cached.handles;
|
|
565
|
+
const { read } = unmarkIdentity(cached.value);
|
|
566
|
+
if (read) markIdentityRead(valueTags, read);
|
|
567
|
+
recordLoaderTags(loaderId, cached.tags);
|
|
568
|
+
for (const tag of cached.tags ?? []) valueTags.add(tag);
|
|
569
|
+
debugLoaderCacheLog(`[LoaderCache] ${label}: ${key}`);
|
|
570
|
+
};
|
|
571
|
+
const runBody = async (): Promise<any> => {
|
|
572
|
+
// A stale-hit revalidation runs on its own executor: it diverts its
|
|
573
|
+
// pushes (below), so sharing the page's memoized run of a dependency
|
|
574
|
+
// would take that dependency's pushes off the page.
|
|
575
|
+
const run = (revalidating && internal._runLoaderIsolated) || runMiss;
|
|
576
|
+
if (!handleStore) return run(loaderEntry.loader);
|
|
577
|
+
// A stale-hit revalidation diverts the body's pushes: the foreground
|
|
578
|
+
// already replayed the stale entry's, so the fresh ones belong only to
|
|
579
|
+
// the refreshed entry (not a duplicate in the live response).
|
|
580
|
+
const c = startHandleCapture(handleStore, {
|
|
581
|
+
accept: isOwnBodyPush,
|
|
582
|
+
divert: revalidating,
|
|
583
|
+
key: recordOwnerKey(loaderId),
|
|
584
|
+
});
|
|
585
|
+
capture = c.capture;
|
|
586
|
+
try {
|
|
587
|
+
return await run(loaderEntry.loader);
|
|
588
|
+
} finally {
|
|
589
|
+
c.stop();
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
// #972: the execution's identity reads, and those of the loader values it
|
|
593
|
+
// read, are on the bodyTags links whoever started them. With an undeclared
|
|
594
|
+
// key one fails the fill (the MISS or the stale refresh); one that settles
|
|
595
|
+
// after the value (a nested promise, a pending handle push) skips the write
|
|
596
|
+
// instead. With a declared key it is stored as the entry's identity mark
|
|
597
|
+
// for the HITs, as this loader's readers see it (through this loader when
|
|
598
|
+
// another made it), and a foreground execution marks the value it serves.
|
|
599
|
+
// A stale refresh does not: the page is served the stale entry, which its
|
|
600
|
+
// own mark (or its lack) describes.
|
|
601
|
+
const identityRead = (): LoaderIdentityRead | undefined => {
|
|
602
|
+
const read = recordedIdentityRead(bodyTags);
|
|
603
|
+
if (!read) return undefined;
|
|
604
|
+
if (!declared) throw loaderCacheIdentityError(read, loaderId);
|
|
605
|
+
const seen =
|
|
606
|
+
read.bodyId === undefined || read.bodyId === loaderId
|
|
607
|
+
? { ...read, bodyId: loaderId, via: undefined }
|
|
608
|
+
: { ...read, via: loaderId };
|
|
609
|
+
if (!revalidating) markIdentityRead(valueTags, seen);
|
|
610
|
+
return seen;
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
// Flight encodes a rejected promise in the value or a handle push as an
|
|
614
|
+
// error row and completes normally. setItem runs after serialize and
|
|
615
|
+
// throws instead of storing it, into readThroughItem's cache-write /
|
|
616
|
+
// stale-revalidation report; a stale entry keeps serving.
|
|
617
|
+
const flightErrors: unknown[] = [];
|
|
618
|
+
const onFlightError = (error: unknown): void => {
|
|
619
|
+
flightErrors.push(error);
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
const data = await readThroughItem({
|
|
339
623
|
getItem: (k) => store.getItem!(k),
|
|
340
|
-
|
|
624
|
+
// Handles ride the entry like "use cache" (encodeHandles: Flight, pending
|
|
625
|
+
// pushes awaited up to its timeout, the whole blob dropped on a timeout
|
|
626
|
+
// or a thrown encode). Encoded here, inside the deferred write, so a
|
|
627
|
+
// MISS response never waits on it.
|
|
628
|
+
setItem: (k, v, o) =>
|
|
629
|
+
captureRecordedTags(bodyTags, async () => {
|
|
630
|
+
const handles = capture
|
|
631
|
+
? await encodeHandles(capture.data, onFlightError)
|
|
632
|
+
: "";
|
|
633
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
634
|
+
// After the handle encode: it settles pending pushes, which can read.
|
|
635
|
+
const read = identityRead();
|
|
636
|
+
const entryTags = [...flattenRecordedTags(bodyTags)];
|
|
637
|
+
await store.setItem!(k, markIdentity(v, read), {
|
|
638
|
+
...o,
|
|
639
|
+
tags: entryTags.length > 0 ? entryTags : undefined,
|
|
640
|
+
...(handles ? { handles } : {}),
|
|
641
|
+
});
|
|
642
|
+
}),
|
|
341
643
|
key,
|
|
342
|
-
execute: () =>
|
|
644
|
+
execute: async () => {
|
|
645
|
+
const value = await captureRecordedTags(bodyTags, runBody);
|
|
646
|
+
identityRead();
|
|
647
|
+
return value;
|
|
648
|
+
},
|
|
343
649
|
// The rango.background span (kind=loader-revalidation) wraps the WHOLE
|
|
344
650
|
// stale revalidation — the re-execution AND the serialize/setItem write
|
|
345
651
|
// (read-through-swr routes the full task through wrapBackground) — so
|
|
@@ -347,21 +653,46 @@ function executeLoaderData<TEnv>(
|
|
|
347
653
|
// store write all nest under one explanatory parent instead of dangling
|
|
348
654
|
// under the ended foreground phases. Inside runWithRequestContext so
|
|
349
655
|
// observePhase can read tracing on workerd (ALS detaches in waitUntil).
|
|
350
|
-
wrapBackground: (run) =>
|
|
351
|
-
|
|
656
|
+
wrapBackground: (run) => {
|
|
657
|
+
revalidating = true;
|
|
658
|
+
return runWithRequestContext(requestCtxForExecute, () =>
|
|
352
659
|
observePhase(PHASES.background("loader-revalidation"), run),
|
|
660
|
+
);
|
|
661
|
+
},
|
|
662
|
+
serialize: (d) =>
|
|
663
|
+
captureRecordedTags(bodyTags, () =>
|
|
664
|
+
codec.serializeResult(d, onFlightError),
|
|
353
665
|
),
|
|
354
|
-
|
|
355
|
-
deserialize: (v) => codec.deserializeResult(v),
|
|
666
|
+
deserialize: (v) => codec.deserializeResult(unmarkIdentity(v).payload),
|
|
356
667
|
storeOptions: { ttl, swr, tags },
|
|
357
|
-
onHit: () =>
|
|
358
|
-
onStale: () =>
|
|
359
|
-
onMiss: () =>
|
|
668
|
+
onHit: (cached) => onCachedRead("HIT", cached),
|
|
669
|
+
onStale: (cached) => onCachedRead("STALE", cached),
|
|
670
|
+
onMiss: () => {
|
|
671
|
+
linkRecordedTags(valueTags, bodyTags);
|
|
672
|
+
debugLoaderCacheLog(`[LoaderCache] MISS: ${key}`);
|
|
673
|
+
},
|
|
360
674
|
onCached: () => debugLoaderCacheLog(`[LoaderCache] Cached: ${key}`),
|
|
361
675
|
host: requestCtxForExecute,
|
|
362
676
|
});
|
|
677
|
+
|
|
678
|
+
// An entry written before handles were recorded has none: no replay.
|
|
679
|
+
if (hitHandles && handleStore && owningSegmentId) {
|
|
680
|
+
await replayLoaderHandles(
|
|
681
|
+
hitHandles,
|
|
682
|
+
handleStore,
|
|
683
|
+
owningSegmentId,
|
|
684
|
+
internal._claimLoaderPushes,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
687
|
+
return data;
|
|
363
688
|
})();
|
|
364
689
|
|
|
690
|
+
// Keep the store open for the replay: a HIT runs no loader body, so nothing
|
|
691
|
+
// else holds the auxiliary lane while getItem/decode are in flight, and a
|
|
692
|
+
// push after full drain throws LateHandlePushError.
|
|
693
|
+
handleStore?.trackAuxiliary(dataPromise);
|
|
694
|
+
|
|
695
|
+
tagLoaderValue(dataPromise, valueTags);
|
|
365
696
|
overrides.set(loaderId, dataPromise);
|
|
366
697
|
|
|
367
698
|
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;
|