@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
package/src/cache/cache-scope.ts
CHANGED
|
@@ -17,7 +17,12 @@ import {
|
|
|
17
17
|
getRequestContext,
|
|
18
18
|
_getRequestContext,
|
|
19
19
|
} from "../server/request-context.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
getSegmentTags,
|
|
22
|
+
recordRequestTags,
|
|
23
|
+
recordSegmentTags,
|
|
24
|
+
runInSegmentTagScope,
|
|
25
|
+
} from "./cache-tag.js";
|
|
21
26
|
import { reportCacheError } from "./cache-error.js";
|
|
22
27
|
// segment-codec is the only module on cache-scope's import graph that eagerly
|
|
23
28
|
// pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
|
|
@@ -28,11 +33,12 @@ import { reportCacheError } from "./cache-error.js";
|
|
|
28
33
|
// is unchanged: both methods are async and already awaited the codec.
|
|
29
34
|
import {
|
|
30
35
|
captureHandles,
|
|
36
|
+
captureHandleOwners,
|
|
31
37
|
restoreHandles,
|
|
32
38
|
encodeHandles,
|
|
33
39
|
decodeHandles,
|
|
34
40
|
} from "./handle-snapshot.js";
|
|
35
|
-
import { cacheKeyBase } from "./cache-key-utils.js";
|
|
41
|
+
import { cacheKeyBase, composeCacheKeys } from "./cache-key-utils.js";
|
|
36
42
|
import {
|
|
37
43
|
DEFAULT_ROUTE_TTL,
|
|
38
44
|
isFiniteNonNegativeSeconds,
|
|
@@ -50,6 +56,38 @@ export function resolveCacheTags(
|
|
|
50
56
|
return resolveTagsOption(config.tags, ctx, "CacheScope");
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Narrow the request's route-record window (RequestContext._routeRecordWindow)
|
|
61
|
+
* to a record this request read or wrote: fresh until `freshUntil` (the
|
|
62
|
+
* record's `expiresAt`), then stale for `swr` seconds. A shell capture starts
|
|
63
|
+
* with no window of its own (shell-capture.ts deriveShellCaptureContext sets
|
|
64
|
+
* it undefined), so it narrows only to the records the capture itself read
|
|
65
|
+
* or wrote.
|
|
66
|
+
*/
|
|
67
|
+
function noteRouteRecordWindow(
|
|
68
|
+
requestCtx: RequestContext,
|
|
69
|
+
freshUntil: number,
|
|
70
|
+
ttl: number,
|
|
71
|
+
swr: number,
|
|
72
|
+
written: boolean,
|
|
73
|
+
): void {
|
|
74
|
+
const next = {
|
|
75
|
+
freshUntil,
|
|
76
|
+
staleUntil: freshUntil + swr * 1000,
|
|
77
|
+
ttl,
|
|
78
|
+
swr,
|
|
79
|
+
written,
|
|
80
|
+
};
|
|
81
|
+
const current = requestCtx._routeRecordWindow;
|
|
82
|
+
// The record that runs out first names the config; freshness is the least.
|
|
83
|
+
requestCtx._routeRecordWindow = current
|
|
84
|
+
? {
|
|
85
|
+
...(current.staleUntil <= next.staleUntil ? current : next),
|
|
86
|
+
freshUntil: Math.min(current.freshUntil, freshUntil),
|
|
87
|
+
}
|
|
88
|
+
: next;
|
|
89
|
+
}
|
|
90
|
+
|
|
53
91
|
function debugCacheLog(message: string): void {
|
|
54
92
|
if (INTERNAL_RANGO_DEBUG) {
|
|
55
93
|
console.log(message);
|
|
@@ -109,7 +147,43 @@ function getDefaultRouteCacheKey(
|
|
|
109
147
|
// CacheScope
|
|
110
148
|
// ============================================================================
|
|
111
149
|
|
|
112
|
-
const CACHE_HIT_OBSERVERS = new WeakMap<
|
|
150
|
+
const CACHE_HIT_OBSERVERS = new WeakMap<
|
|
151
|
+
CacheScope,
|
|
152
|
+
() => void | Promise<void>
|
|
153
|
+
>();
|
|
154
|
+
|
|
155
|
+
type CacheKeyFn = NonNullable<PartialCacheOptions["key"]>;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* A configured `key()` cannot run without a request context. Falling back to
|
|
159
|
+
* the default key would store the key()'s partitioned output under the broad
|
|
160
|
+
* key and serve it across partitions, so key resolution fails instead and
|
|
161
|
+
* every caller degrades to an uncached render (as for a throwing `key()`).
|
|
162
|
+
*/
|
|
163
|
+
function keyWithoutContextError(): Error {
|
|
164
|
+
return new Error(
|
|
165
|
+
"[CacheScope] a cache() key() needs a request context; rendering uncached",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* One key resolution per request, resolver (`by`: a `key()` or a store's
|
|
171
|
+
* keyGenerator) and `slot`, held on the request context (`_resolvedCacheKeys`,
|
|
172
|
+
* see CacheScope.resolveKeyFrom).
|
|
173
|
+
*/
|
|
174
|
+
function memoizedKey(
|
|
175
|
+
requestCtx: RequestContext,
|
|
176
|
+
by: unknown,
|
|
177
|
+
slot: string,
|
|
178
|
+
resolve: () => Promise<string>,
|
|
179
|
+
): Promise<string> {
|
|
180
|
+
const memo = (requestCtx._resolvedCacheKeys ??= new Map());
|
|
181
|
+
let resolved = memo.get(by);
|
|
182
|
+
if (!resolved) memo.set(by, (resolved = new Map()));
|
|
183
|
+
let key = resolved.get(slot);
|
|
184
|
+
if (!key) resolved.set(slot, (key = resolve()));
|
|
185
|
+
return key;
|
|
186
|
+
}
|
|
113
187
|
|
|
114
188
|
/**
|
|
115
189
|
* Discriminated outcome of a route cache lookup — see
|
|
@@ -144,16 +218,63 @@ export class CacheScope {
|
|
|
144
218
|
readonly parent: CacheScope | null;
|
|
145
219
|
/** Explicit store from cache() options, if specified */
|
|
146
220
|
private readonly explicitStore: SegmentCacheStore | undefined;
|
|
221
|
+
/**
|
|
222
|
+
* @internal The `key()` functions that partition this scope's records,
|
|
223
|
+
* outermost first: every cache() on the parent chain that sets one, this
|
|
224
|
+
* scope's last (#970). `cache(false)` adds none and does not cut the chain,
|
|
225
|
+
* so a cache() re-enabled under it stays in the partition. Before, the
|
|
226
|
+
* innermost config alone decided the key: an inner cache() without `key()`
|
|
227
|
+
* wrote under the default key and an inner `key()` dropped the outer one,
|
|
228
|
+
* so a request in one partition HIT another partition's inner records.
|
|
229
|
+
*/
|
|
230
|
+
readonly keyFns: readonly CacheKeyFn[];
|
|
231
|
+
/** Whether this scope's own config sets `key()` (the last of keyFns). */
|
|
232
|
+
private readonly hasOwnKey: boolean;
|
|
147
233
|
|
|
148
234
|
constructor(
|
|
149
235
|
config: PartialCacheOptions | false,
|
|
150
236
|
parent: CacheScope | null = null,
|
|
151
237
|
private readonly defaultKeyPrefix?: "doc",
|
|
238
|
+
/**
|
|
239
|
+
* @internal shortCode of the outermost cache() entry of this scope's
|
|
240
|
+
* enabled chain (createCacheScope). Entries above it are live: their
|
|
241
|
+
* segments are not stored and resolve fresh on a hit (withCacheLookup).
|
|
242
|
+
* Undefined covers the whole matched chain (the implicit shell doc scope,
|
|
243
|
+
* a ppr route).
|
|
244
|
+
*/
|
|
245
|
+
readonly boundary?: string,
|
|
152
246
|
) {
|
|
153
247
|
this.config = config;
|
|
154
248
|
this.parent = parent;
|
|
155
249
|
// Extract and store explicit store reference
|
|
156
250
|
this.explicitStore = config !== false ? config.store : undefined;
|
|
251
|
+
// Only a function counts: a conditional `key: flag ? fn : null` from
|
|
252
|
+
// untyped code is no key(), as resolveCacheKey always treated it.
|
|
253
|
+
const ownKey =
|
|
254
|
+
config !== false && typeof config.key === "function"
|
|
255
|
+
? config.key
|
|
256
|
+
: undefined;
|
|
257
|
+
const inherited = parent?.keyFns ?? [];
|
|
258
|
+
this.hasOwnKey = ownKey !== undefined;
|
|
259
|
+
this.keyFns = ownKey ? [...inherited, ownKey] : inherited;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @internal Whether a segment belongs to this scope's cache entry. ShortCodes are
|
|
264
|
+
* hierarchical (server/context.ts getShortCode: parent shortCode + include
|
|
265
|
+
* scope + type letter + counter), so the boundary's subtree is every id that
|
|
266
|
+
* extends the boundary shortCode at a non-digit (`C1` must not claim `C10`).
|
|
267
|
+
* Intercept slot segments ride the target route's entry wherever the
|
|
268
|
+
* intercept is declared.
|
|
269
|
+
*/
|
|
270
|
+
covers(id: string, namespace?: string): boolean {
|
|
271
|
+
const boundary = this.boundary;
|
|
272
|
+
if (boundary === undefined) return true;
|
|
273
|
+
if (id.startsWith(boundary)) {
|
|
274
|
+
const next = id.charCodeAt(boundary.length);
|
|
275
|
+
return !(next >= 48 && next <= 57);
|
|
276
|
+
}
|
|
277
|
+
return namespace?.startsWith("intercept:") === true;
|
|
157
278
|
}
|
|
158
279
|
|
|
159
280
|
/**
|
|
@@ -222,19 +343,151 @@ export class CacheScope {
|
|
|
222
343
|
* Resolve the cache key using the shared 3-tier priority.
|
|
223
344
|
* @internal
|
|
224
345
|
*/
|
|
225
|
-
private
|
|
346
|
+
private resolveKey(
|
|
226
347
|
pathname: string,
|
|
227
348
|
params: Record<string, string>,
|
|
228
349
|
isIntercept?: boolean,
|
|
229
350
|
): Promise<string> {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
351
|
+
return this.resolveKeyFrom(
|
|
352
|
+
getDefaultRouteCacheKey(
|
|
353
|
+
pathname,
|
|
354
|
+
params,
|
|
355
|
+
isIntercept,
|
|
356
|
+
this.defaultKeyPrefix,
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* A shell captured from a route record this request read or wrote must not
|
|
363
|
+
* outlive it (noteRouteRecordWindow). The implicit doc scope's own record
|
|
364
|
+
* is the shell's, so it notes nothing.
|
|
365
|
+
*/
|
|
366
|
+
private noteRecordWindow(
|
|
367
|
+
requestCtx: RequestContext,
|
|
368
|
+
freshUntil: number,
|
|
369
|
+
written: boolean,
|
|
370
|
+
): void {
|
|
371
|
+
if (!this.isShellImplicitDocScope) {
|
|
372
|
+
noteRouteRecordWindow(
|
|
373
|
+
requestCtx,
|
|
374
|
+
freshUntil,
|
|
375
|
+
this.ttl,
|
|
376
|
+
this.swr ?? 0,
|
|
377
|
+
written,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @internal This scope's record key for `defaultKey`, resolved once per
|
|
384
|
+
* request: a ppr route's shell partition (resolvePartition) and the record
|
|
385
|
+
* lookup share the `key()` runs, and a rejection is shared too.
|
|
386
|
+
*
|
|
387
|
+
* With `key()` functions on the chain (keyFns) the parts are their results,
|
|
388
|
+
* outermost first, composed by composeCacheKeys (a single part raw), so a
|
|
389
|
+
* nested cache() keys its records within the enclosing partition (#970).
|
|
390
|
+
* A scope whose own config sets no `key()` appends its own default key
|
|
391
|
+
* (resolveDefaultKey). Reusing the outer `key()` result alone would give
|
|
392
|
+
* every route under the inner cache() one record: a `key()` replaces the
|
|
393
|
+
* whole default key, so an outer key that names no route (`tier:gold`)
|
|
394
|
+
* would make `/a` HIT `/b`'s record. A scope with its own `key()` adds no
|
|
395
|
+
* default key: a `key()` is a full override, and no keyGenerator runs.
|
|
396
|
+
* Without `key()` on the chain it is resolveDefaultKey alone. With one but
|
|
397
|
+
* no request context it rejects (keyWithoutContextError).
|
|
398
|
+
*
|
|
399
|
+
* Each `key()` result lives on the request context (`_resolvedCacheKeys`),
|
|
400
|
+
* by function: it runs once per request whatever the default key
|
|
401
|
+
* (document, partial or `doc` shell prefix) and however many scopes share
|
|
402
|
+
* it (sibling routes under one keyed cache()). A shell capture, whose
|
|
403
|
+
* context is `Object.create` of the request's (shell-capture.ts), reads the
|
|
404
|
+
* foreground's results through the prototype chain and never runs `key()`
|
|
405
|
+
* itself: a `key()` calling `cookies()` would trip the capture guard there,
|
|
406
|
+
* and a capture must store the partition its request resolved.
|
|
407
|
+
*/
|
|
408
|
+
resolveKeyFrom(defaultKey: string): Promise<string> {
|
|
409
|
+
const requestCtx = _getRequestContext();
|
|
410
|
+
if (this.keyFns.length === 0) {
|
|
411
|
+
return this.resolveDefaultKey(requestCtx, defaultKey);
|
|
412
|
+
}
|
|
413
|
+
if (!requestCtx) return Promise.reject(keyWithoutContextError());
|
|
414
|
+
const parts = this.resolveKeyResults(requestCtx, defaultKey);
|
|
415
|
+
if (!this.hasOwnKey) {
|
|
416
|
+
parts.push(this.resolveDefaultKey(requestCtx, defaultKey));
|
|
417
|
+
}
|
|
418
|
+
return parts.length === 1
|
|
419
|
+
? parts[0]
|
|
420
|
+
: Promise.all(parts).then(composeCacheKeys);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* @internal The request partition of a ppr route's shell under this scope
|
|
425
|
+
* (resolveShellPartition): the chain's `key()` results, composed like the
|
|
426
|
+
* record key but without the default key, which the URL-based shell key
|
|
427
|
+
* already carries. A scope without its own `key()` whose store has a
|
|
428
|
+
* keyGenerator adds that result when it differs from the `doc` default
|
|
429
|
+
* key, as an unkeyed scope's partition always did: the record is split by
|
|
430
|
+
* it, so the shell must be too. Undefined (synchronously, with no work)
|
|
431
|
+
* when nothing can partition; null when nothing does.
|
|
432
|
+
*/
|
|
433
|
+
resolvePartition(
|
|
434
|
+
pathname: string,
|
|
435
|
+
params: Record<string, string> | undefined,
|
|
436
|
+
): Promise<string | null> | undefined {
|
|
437
|
+
const keyGenerator = this.hasOwnKey
|
|
438
|
+
? undefined
|
|
439
|
+
: this.getStore()?.keyGenerator;
|
|
440
|
+
if (this.keyFns.length === 0 && !keyGenerator) return undefined;
|
|
441
|
+
const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
|
|
442
|
+
const requestCtx = _getRequestContext();
|
|
443
|
+
if (!requestCtx) {
|
|
444
|
+
return this.keyFns.length > 0
|
|
445
|
+
? Promise.reject(keyWithoutContextError())
|
|
446
|
+
: Promise.resolve(null);
|
|
447
|
+
}
|
|
448
|
+
const generated = keyGenerator
|
|
449
|
+
? this.resolveDefaultKey(requestCtx, defaultKey)
|
|
450
|
+
: undefined;
|
|
451
|
+
return Promise.all([
|
|
452
|
+
Promise.all(this.resolveKeyResults(requestCtx, defaultKey)),
|
|
453
|
+
generated,
|
|
454
|
+
]).then(([keys, generatedKey]) => {
|
|
455
|
+
const parts =
|
|
456
|
+
generatedKey === undefined || generatedKey === defaultKey
|
|
457
|
+
? keys
|
|
458
|
+
: [...keys, generatedKey];
|
|
459
|
+
const partition = parts.length === 0 ? null : composeCacheKeys(parts);
|
|
460
|
+
return partition === defaultKey ? null : partition;
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/** The chain's `key()` results, each once per request (memoizedKey). */
|
|
465
|
+
private resolveKeyResults(
|
|
466
|
+
requestCtx: RequestContext,
|
|
467
|
+
defaultKey: string,
|
|
468
|
+
): Promise<string>[] {
|
|
469
|
+
return this.keyFns.map((keyFn) =>
|
|
470
|
+
memoizedKey(requestCtx, keyFn, "", () =>
|
|
471
|
+
resolveCacheKey(keyFn, null, defaultKey, "CacheScope"),
|
|
472
|
+
),
|
|
235
473
|
);
|
|
236
|
-
|
|
237
|
-
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* This scope's store keyGenerator result for `defaultKey` (once per request
|
|
478
|
+
* and default key), else `defaultKey` (resolveCacheKey).
|
|
479
|
+
*/
|
|
480
|
+
private resolveDefaultKey(
|
|
481
|
+
requestCtx: RequestContext | undefined,
|
|
482
|
+
defaultKey: string,
|
|
483
|
+
): Promise<string> {
|
|
484
|
+
const store = this.getStore();
|
|
485
|
+
const keyGenerator = store?.keyGenerator;
|
|
486
|
+
const resolve = () =>
|
|
487
|
+
resolveCacheKey(undefined, store, defaultKey, "CacheScope");
|
|
488
|
+
return requestCtx && keyGenerator
|
|
489
|
+
? memoizedKey(requestCtx, keyGenerator, defaultKey, resolve)
|
|
490
|
+
: resolve();
|
|
238
491
|
}
|
|
239
492
|
|
|
240
493
|
/**
|
|
@@ -316,11 +569,14 @@ export class CacheScope {
|
|
|
316
569
|
* @param pathname - URL pathname for cache key generation
|
|
317
570
|
* @param params - Route params for cache key generation
|
|
318
571
|
* @param isIntercept - Whether this is an intercept navigation (uses different cache key)
|
|
572
|
+
* @param claimLoaderPushes - The handler context's _claimLoaderPushes, for
|
|
573
|
+
* restoreHandles' loader-owned values
|
|
319
574
|
*/
|
|
320
575
|
async lookupRoute(
|
|
321
576
|
pathname: string,
|
|
322
577
|
params: Record<string, string>,
|
|
323
578
|
isIntercept?: boolean,
|
|
579
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
324
580
|
): Promise<{
|
|
325
581
|
segments: ResolvedSegment[];
|
|
326
582
|
shouldRevalidate: boolean;
|
|
@@ -329,6 +585,7 @@ export class CacheScope {
|
|
|
329
585
|
pathname,
|
|
330
586
|
params,
|
|
331
587
|
isIntercept,
|
|
588
|
+
claimLoaderPushes,
|
|
332
589
|
);
|
|
333
590
|
return outcome.status === "hit" ? outcome.result : null;
|
|
334
591
|
}
|
|
@@ -355,6 +612,7 @@ export class CacheScope {
|
|
|
355
612
|
pathname: string,
|
|
356
613
|
params: Record<string, string>,
|
|
357
614
|
isIntercept?: boolean,
|
|
615
|
+
claimLoaderPushes?: (loaderId: string) => boolean,
|
|
358
616
|
): Promise<CacheRouteLookupOutcome> {
|
|
359
617
|
if (!this.enabled) return { status: "bypass" };
|
|
360
618
|
if (!this.conditionAllows("read")) return { status: "bypass" };
|
|
@@ -399,7 +657,8 @@ export class CacheScope {
|
|
|
399
657
|
// expand them (segment-fragments.ts). Read off the ambient context at the
|
|
400
658
|
// same point the cache key was resolved (getDefaultRouteCacheKey), so the
|
|
401
659
|
// flag shares fate with the key: a disrupted ALS already missed the
|
|
402
|
-
// seeded record
|
|
660
|
+
// seeded record, which a document tail turns into the degrade
|
|
661
|
+
// (ShellRecordUnavailableError).
|
|
403
662
|
let segments: ResolvedSegment[];
|
|
404
663
|
const ambientContext = _getRequestContext();
|
|
405
664
|
try {
|
|
@@ -424,11 +683,25 @@ export class CacheScope {
|
|
|
424
683
|
return { status: "miss" };
|
|
425
684
|
}
|
|
426
685
|
|
|
686
|
+
// A record from a whole-chain writer (a previous build on an unversioned
|
|
687
|
+
// store) can hold segments above the boundary. withCacheLookup resolves
|
|
688
|
+
// those fresh, so replaying them too would duplicate them and their
|
|
689
|
+
// handle pushes.
|
|
690
|
+
if (this.boundary !== undefined) {
|
|
691
|
+
segments = segments.filter((s) => this.covers(s.id, s.namespace));
|
|
692
|
+
}
|
|
693
|
+
|
|
427
694
|
// A hit serves content that was tagged at write time, so the document
|
|
428
695
|
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
429
696
|
// to invalidate any full-page entry built on top of it. The write path
|
|
430
|
-
// records via cacheRoute (resolveCacheTags); the hit path records here
|
|
697
|
+
// records via cacheRoute (resolveCacheTags); the hit path records here,
|
|
698
|
+
// onto the replayed segments too, for a record written from this replay
|
|
699
|
+
// (a shell capture's doc record).
|
|
431
700
|
recordRequestTags(cached.tags);
|
|
701
|
+
recordSegmentTags(
|
|
702
|
+
segments.map((s) => s.id),
|
|
703
|
+
cached.tags,
|
|
704
|
+
);
|
|
432
705
|
|
|
433
706
|
// Replay handle data. An empty string means the route pushed no handles —
|
|
434
707
|
// skip the decode entirely (the common case). Otherwise decode the
|
|
@@ -438,7 +711,18 @@ export class CacheScope {
|
|
|
438
711
|
if (handleStore && cached.handles) {
|
|
439
712
|
const handlesRecord = await decodeHandles(cached.handles);
|
|
440
713
|
if (handlesRecord) {
|
|
441
|
-
|
|
714
|
+
if (this.boundary !== undefined) {
|
|
715
|
+
const kept = new Set(segments.map((s) => s.id));
|
|
716
|
+
for (const id of Object.keys(handlesRecord)) {
|
|
717
|
+
if (!kept.has(id)) delete handlesRecord[id];
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
restoreHandles(
|
|
721
|
+
handlesRecord,
|
|
722
|
+
handleStore,
|
|
723
|
+
cached.handleOwners,
|
|
724
|
+
claimLoaderPushes,
|
|
725
|
+
);
|
|
442
726
|
}
|
|
443
727
|
}
|
|
444
728
|
|
|
@@ -451,7 +735,10 @@ export class CacheScope {
|
|
|
451
735
|
);
|
|
452
736
|
}
|
|
453
737
|
|
|
454
|
-
|
|
738
|
+
if (ambientContext)
|
|
739
|
+
this.noteRecordWindow(ambientContext, cached.expiresAt, false);
|
|
740
|
+
|
|
741
|
+
await CACHE_HIT_OBSERVERS.get(this)?.();
|
|
455
742
|
return { status: "hit", result: { segments, shouldRevalidate } };
|
|
456
743
|
} catch (error) {
|
|
457
744
|
// Covers a store.get() failure AND a throwing consumer key()/keyGenerator
|
|
@@ -515,12 +802,16 @@ export class CacheScope {
|
|
|
515
802
|
if (!handleStore || !requestCtx) return;
|
|
516
803
|
|
|
517
804
|
// Exclude loader segments - loaders are always fresh by default
|
|
518
|
-
// Loaders can opt-in to caching with their own cache() config
|
|
519
|
-
|
|
805
|
+
// Loaders can opt-in to caching with their own cache() config.
|
|
806
|
+
// Segments above the boundary are live and never stored.
|
|
807
|
+
const nonLoaderSegments = segments.filter(
|
|
808
|
+
(s) => s.type !== "loader" && this.covers(s.id, s.namespace),
|
|
809
|
+
);
|
|
520
810
|
if (nonLoaderSegments.length === 0) return;
|
|
521
811
|
|
|
522
812
|
const ttl = this.ttl;
|
|
523
813
|
const swr = this.swr;
|
|
814
|
+
this.noteRecordWindow(requestCtx, Date.now() + ttl * 1000, true);
|
|
524
815
|
|
|
525
816
|
// Resolve cache key early (while request context is available)
|
|
526
817
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
@@ -585,11 +876,8 @@ export class CacheScope {
|
|
|
585
876
|
}
|
|
586
877
|
|
|
587
878
|
// Collect handle data for non-loader segments only
|
|
588
|
-
const handles = captureHandles(
|
|
589
|
-
|
|
590
|
-
handleStore,
|
|
591
|
-
requestCtx._shellCaptureLoaderHandleValues,
|
|
592
|
-
);
|
|
879
|
+
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
880
|
+
const handleOwners = captureHandleOwners(nonLoaderSegments, handleStore);
|
|
593
881
|
|
|
594
882
|
try {
|
|
595
883
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -602,17 +890,41 @@ export class CacheScope {
|
|
|
602
890
|
// through the codec (not raw into the entry) so Promise/ReactNode handle
|
|
603
891
|
// values survive a JSON-serializing store — see encodeHandles.
|
|
604
892
|
const { serializeSegments } = await import("./segment-codec.js");
|
|
893
|
+
const flightErrors: unknown[] = [];
|
|
894
|
+
const onFlightError = (error: unknown): void => {
|
|
895
|
+
flightErrors.push(error);
|
|
896
|
+
};
|
|
897
|
+
// Each segment serializes inside its own tag scope, and the handles
|
|
898
|
+
// inside one for this write: Flight re-renders their server
|
|
899
|
+
// components, so their render-called tags land on the record.
|
|
900
|
+
const handlesOwnerId = `${key}#handles`;
|
|
605
901
|
const [serializedSegments, encodedHandles] = await Promise.all([
|
|
606
|
-
|
|
607
|
-
|
|
902
|
+
Promise.all(
|
|
903
|
+
nonLoaderSegments.map((segment) =>
|
|
904
|
+
runInSegmentTagScope(segment.id, () =>
|
|
905
|
+
serializeSegments([segment], onFlightError),
|
|
906
|
+
),
|
|
907
|
+
),
|
|
908
|
+
).then((parts) => parts.flat()),
|
|
909
|
+
runInSegmentTagScope(handlesOwnerId, () =>
|
|
910
|
+
encodeHandles(handles, onFlightError),
|
|
911
|
+
),
|
|
608
912
|
]);
|
|
913
|
+
// Flight encodes a component that throws (an async server component in
|
|
914
|
+
// the tree) or a rejected handle value as an error row and completes
|
|
915
|
+
// normally; stored, every HIT would render that error until expiry.
|
|
916
|
+
if (flightErrors.length > 0) throw flightErrors[0];
|
|
609
917
|
|
|
610
918
|
const data: CachedEntryData = {
|
|
611
919
|
segments: serializedSegments,
|
|
612
920
|
handles: encodedHandles,
|
|
613
921
|
expiresAt: Date.now() + ttl * 1000,
|
|
614
|
-
tags,
|
|
922
|
+
tags: collectRecordTags(requestCtx, tags, [
|
|
923
|
+
...nonLoaderSegments.map((s) => s.id),
|
|
924
|
+
handlesOwnerId,
|
|
925
|
+
]),
|
|
615
926
|
};
|
|
927
|
+
if (handleOwners && encodedHandles) data.handleOwners = handleOwners;
|
|
616
928
|
|
|
617
929
|
if (INTERNAL_RANGO_DEBUG) {
|
|
618
930
|
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
@@ -640,14 +952,67 @@ export class CacheScope {
|
|
|
640
952
|
}
|
|
641
953
|
|
|
642
954
|
/**
|
|
643
|
-
*
|
|
955
|
+
* A route cache() record's tags (#957): its config tags plus every tag its
|
|
956
|
+
* content recorded under `ownerIds` (cache-tag.ts getSegmentTags — the covered
|
|
957
|
+
* segments' handlers, the server components their Flight serialization
|
|
958
|
+
* re-rendered, the loaders those handlers consumed, and the handle values).
|
|
959
|
+
* A HIT replays all of that output, loading() subtrees included, so every one
|
|
960
|
+
* of these tags describes it.
|
|
961
|
+
*/
|
|
962
|
+
function collectRecordTags(
|
|
963
|
+
requestCtx: RequestContext,
|
|
964
|
+
configTags: string[] | undefined,
|
|
965
|
+
ownerIds: string[],
|
|
966
|
+
): string[] | undefined {
|
|
967
|
+
const tags = new Set(configTags);
|
|
968
|
+
const before = tags.size;
|
|
969
|
+
for (const id of ownerIds) {
|
|
970
|
+
for (const tag of getSegmentTags(requestCtx, id)) tags.add(tag);
|
|
971
|
+
}
|
|
972
|
+
return tags.size > before ? [...tags] : configTags;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* @internal The request partition of a ppr route's shell: with a route
|
|
977
|
+
* cache() scope, its CacheScope.resolvePartition (the chain's `key()`
|
|
978
|
+
* results, composed, plus a keyGenerator's contribution for a scope without
|
|
979
|
+
* its own `key()` (#970)); with none, the app store's keyGenerator result
|
|
980
|
+
* given the document default key. Undefined (synchronously, with no work)
|
|
981
|
+
* when neither applies: the shell key stays host + path + filtered search.
|
|
982
|
+
* Resolves to null when the partition is the default key itself (a
|
|
983
|
+
* keyGenerator that returns it unchanged): that partitions nothing, so the
|
|
984
|
+
* shell stays unpartitioned and keeps its build shell. The shell's key, its
|
|
985
|
+
* read and capture, and partial replay all use it, so a visitor is only
|
|
986
|
+
* served its own partition. A rejected promise is a failed key resolution:
|
|
987
|
+
* the caller serves no shell.
|
|
988
|
+
*/
|
|
989
|
+
export function resolveShellPartition(
|
|
990
|
+
routeScope: CacheScope | null | undefined,
|
|
991
|
+
appStore: SegmentCacheStore | null | undefined,
|
|
992
|
+
pathname: string,
|
|
993
|
+
params: Record<string, string> | undefined,
|
|
994
|
+
): Promise<string | null> | undefined {
|
|
995
|
+
if (routeScope?.enabled) return routeScope.resolvePartition(pathname, params);
|
|
996
|
+
if (!appStore?.keyGenerator) return undefined;
|
|
997
|
+
const defaultKey = getDefaultRouteCacheKey(pathname, params, false, "doc");
|
|
998
|
+
return resolveCacheKey(undefined, appStore, defaultKey, "CacheScope").then(
|
|
999
|
+
(resolved) => (resolved === defaultKey ? null : resolved),
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/**
|
|
1004
|
+
* Create a cache scope from entry's cache config. `shortCode` is the cache()
|
|
1005
|
+
* entry's: a scope nested in an enabled parent keeps the parent's boundary
|
|
1006
|
+
* (one entry covers both), any other opens its own at this entry.
|
|
644
1007
|
*/
|
|
645
1008
|
export function createCacheScope(
|
|
646
1009
|
config: { options: PartialCacheOptions | false } | undefined,
|
|
647
1010
|
parent: CacheScope | null = null,
|
|
1011
|
+
shortCode?: string,
|
|
648
1012
|
): CacheScope | null {
|
|
649
1013
|
if (!config) return parent; // No config, inherit parent
|
|
650
|
-
|
|
1014
|
+
const boundary = parent?.enabled ? parent.boundary : shortCode;
|
|
1015
|
+
return new CacheScope(config.options, parent, undefined, boundary);
|
|
651
1016
|
}
|
|
652
1017
|
|
|
653
1018
|
type ShellImplicitCacheMarker = NonNullable<
|
|
@@ -661,13 +1026,21 @@ type ShellImplicitCacheMarker = NonNullable<
|
|
|
661
1026
|
* {@link resolveShellImplicitCacheScope} (routes that derived no scope) and
|
|
662
1027
|
* the explicit-scope composition sites (capture doc record in the cache-store
|
|
663
1028
|
* middleware, seeded replay fallback in withCacheLookup) so both ends of the
|
|
664
|
-
* shell contract resolve the SAME canonical document key.
|
|
1029
|
+
* shell contract resolve the SAME canonical document key. A document HIT
|
|
1030
|
+
* tail's marker carries the entry's own key (`fixedDocKey`), which wins over
|
|
1031
|
+
* key resolution.
|
|
665
1032
|
*/
|
|
666
1033
|
export function createShellImplicitDocScope(
|
|
667
1034
|
marker: ShellImplicitCacheMarker,
|
|
668
1035
|
): CacheScope {
|
|
1036
|
+
const fixedDocKey = marker.fixedDocKey;
|
|
669
1037
|
const implicitScope = new CacheScope(
|
|
670
|
-
{
|
|
1038
|
+
{
|
|
1039
|
+
ttl: marker.ttl,
|
|
1040
|
+
swr: marker.swr,
|
|
1041
|
+
store: marker.store,
|
|
1042
|
+
...(fixedDocKey !== undefined && { key: () => fixedDocKey }),
|
|
1043
|
+
},
|
|
671
1044
|
null,
|
|
672
1045
|
marker.keyPrefix,
|
|
673
1046
|
);
|
|
@@ -676,26 +1049,29 @@ export function createShellImplicitDocScope(
|
|
|
676
1049
|
}
|
|
677
1050
|
|
|
678
1051
|
/**
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
* (resolveFreshLoadersAndYield).
|
|
1052
|
+
* The doc record scope: when the current request context carries the
|
|
1053
|
+
* `_shellImplicitCache` marker (a shell capture, a document HIT tail, or a
|
|
1054
|
+
* normal partial navigation replay), substitute an implicit doc-level scope
|
|
1055
|
+
* so withCacheLookup/withCacheStore treat the WHOLE matched route as a
|
|
1056
|
+
* cache() boundary — the shell entry IS a cache() of the handler layer, with
|
|
1057
|
+
* loaders as the live carve-outs (resolveFreshLoadersAndYield).
|
|
686
1058
|
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
1059
|
+
* A document HIT tail (`docTail`) always gets the implicit scope: it replays
|
|
1060
|
+
* the shell's own record and never consults a route-derived scope, whose
|
|
1061
|
+
* opt-outs the serve gate evaluated before the commit (shellServePlan). Every
|
|
1062
|
+
* other marker leaves an existing scope — including an explicit cache(false)
|
|
1063
|
+
* — in place: the consumer's cache() semantics (their ttl/swr/store/condition)
|
|
1064
|
+
* are never overridden. On the navigation-replay serve path the marker still
|
|
1065
|
+
* composes with an explicit scope downstream (withCacheLookup's seeded
|
|
1066
|
+
* fallback after an explicit-tier miss) — see `onExplicitHit` on
|
|
1067
|
+
* `_shellImplicitCache`; during a capture, recordShellCaptureDocRecord
|
|
1068
|
+
* (cache-store.ts) writes the record for such a scope.
|
|
693
1069
|
*/
|
|
694
1070
|
export function resolveShellImplicitCacheScope(
|
|
695
1071
|
scope: CacheScope | null,
|
|
696
1072
|
): CacheScope | null {
|
|
697
|
-
if (scope) return scope;
|
|
698
1073
|
const marker = getRequestContext()?._shellImplicitCache;
|
|
1074
|
+
if (scope && !marker?.docTail) return scope;
|
|
699
1075
|
if (!marker) return null;
|
|
700
1076
|
return createShellImplicitDocScope(marker);
|
|
701
1077
|
}
|