@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
|
@@ -6,10 +6,82 @@
|
|
|
6
6
|
* pulling in @vitejs/plugin-rsc/rsc dependencies.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { getCurrentLoaderBodyId } from "../server/context.js";
|
|
9
10
|
import type { HandleStore } from "../server/handle-store.js";
|
|
10
11
|
import type { SegmentHandleData } from "./types.js";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Scoping for one capture of a cached unit (a loader's own cache(),
|
|
15
|
+
* loader-cache.ts; a "use cache" function, cache-runtime.ts) that runs
|
|
16
|
+
* concurrently with live pushes into the same store.
|
|
17
|
+
*/
|
|
18
|
+
export interface HandleCaptureOptions {
|
|
19
|
+
/** Push-time predicate; a rejected push is not recorded by this capture. */
|
|
20
|
+
accept?: () => boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Recorded pushes do not reach the store. A stale hit's background
|
|
23
|
+
* revalidation: the foreground already replayed the entry's pushes, so the
|
|
24
|
+
* fresh body's pushes belong only to the refreshed entry.
|
|
25
|
+
*/
|
|
26
|
+
divert?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Record key for an accepted push, in place of its segment id. A loader's
|
|
29
|
+
* own cache() (loader-cache.ts recordOwnerKey) and "use cache"
|
|
30
|
+
* (useCacheRecordKey) group pushes by the loader body that made them.
|
|
31
|
+
*/
|
|
32
|
+
key?: () => string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The loader whose recorded push a "use cache" HIT is replaying
|
|
36
|
+
// (replayLoaderPush), for an enclosing "use cache" MISS's record key.
|
|
37
|
+
let replayingOwner: string | undefined;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* HandleStore.pushReplayed, attributed to `loaderId` for the record key of an
|
|
41
|
+
* enclosing "use cache" MISS: an outer cached function records an inner HIT's
|
|
42
|
+
* replayed loader pushes under that loader, not as its own.
|
|
43
|
+
*/
|
|
44
|
+
export function replayLoaderPush(
|
|
45
|
+
handleStore: HandleStore,
|
|
46
|
+
handleName: string,
|
|
47
|
+
segmentId: string,
|
|
48
|
+
value: unknown,
|
|
49
|
+
loaderId: string,
|
|
50
|
+
): void {
|
|
51
|
+
const prev = replayingOwner;
|
|
52
|
+
replayingOwner = loaderId;
|
|
53
|
+
try {
|
|
54
|
+
handleStore.pushReplayed(handleName, segmentId, value, loaderId);
|
|
55
|
+
} finally {
|
|
56
|
+
replayingOwner = prev;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record key for a "use cache" capture: run-length groups `${seq}:${owner}`,
|
|
62
|
+
* so the HIT (handle-snapshot.ts appendHandles) delivers each loader's pushes
|
|
63
|
+
* at most once per request. The owner is the loader whose body, entered
|
|
64
|
+
* inside this execution via ctx.use, made the push; empty for the function's
|
|
65
|
+
* own pushes and those of cached functions it calls. Groups keep push order
|
|
66
|
+
* across owners. Create at call time: the caller's loader body is the
|
|
67
|
+
* function's own scope.
|
|
68
|
+
*/
|
|
69
|
+
export function useCacheRecordKey(): () => string {
|
|
70
|
+
const callerBody = getCurrentLoaderBodyId();
|
|
71
|
+
let seq = 0;
|
|
72
|
+
let last: string | undefined;
|
|
73
|
+
return () => {
|
|
74
|
+
const body = replayingOwner ?? getCurrentLoaderBodyId();
|
|
75
|
+
const owner = body === callerBody ? "" : (body ?? "");
|
|
76
|
+
if (owner !== last) {
|
|
77
|
+
last = owner;
|
|
78
|
+
seq++;
|
|
79
|
+
}
|
|
80
|
+
return `${seq}:${owner}`;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface HandleCapture extends HandleCaptureOptions {
|
|
13
85
|
data: Record<string, SegmentHandleData>;
|
|
14
86
|
}
|
|
15
87
|
|
|
@@ -42,33 +114,41 @@ function ensureInterceptorInstalled(handleStore: HandleStore): void {
|
|
|
42
114
|
segmentId: string,
|
|
43
115
|
value: unknown,
|
|
44
116
|
) => {
|
|
117
|
+
let diverted = false;
|
|
45
118
|
for (const capture of captures) {
|
|
46
|
-
if (!capture.
|
|
47
|
-
|
|
119
|
+
if (capture.accept && !capture.accept()) continue;
|
|
120
|
+
if (capture.divert) diverted = true;
|
|
121
|
+
const key = capture.key ? capture.key() : segmentId;
|
|
122
|
+
if (!capture.data[key]) {
|
|
123
|
+
capture.data[key] = {};
|
|
48
124
|
}
|
|
49
|
-
if (!capture.data[
|
|
50
|
-
capture.data[
|
|
125
|
+
if (!capture.data[key][handleName]) {
|
|
126
|
+
capture.data[key][handleName] = [];
|
|
51
127
|
}
|
|
52
|
-
capture.data[
|
|
128
|
+
capture.data[key][handleName].push(value);
|
|
53
129
|
}
|
|
54
|
-
originalPush(handleName, segmentId, value);
|
|
130
|
+
if (!diverted) originalPush(handleName, segmentId, value);
|
|
55
131
|
};
|
|
56
132
|
}
|
|
57
133
|
|
|
58
134
|
/**
|
|
59
|
-
* Start capturing handle pushes for a cached function
|
|
135
|
+
* Start capturing handle pushes for a cached function or cached loader
|
|
136
|
+
* execution (`options`: see HandleCaptureOptions).
|
|
60
137
|
*
|
|
61
138
|
* Concurrency-safe: multiple overlapping captures on the same
|
|
62
139
|
* HandleStore are independent. Each capture registers a token in a
|
|
63
140
|
* Set; stopping removes it. No ordering requirement (LIFO not needed).
|
|
64
141
|
*/
|
|
65
|
-
export function startHandleCapture(
|
|
142
|
+
export function startHandleCapture(
|
|
143
|
+
handleStore: HandleStore,
|
|
144
|
+
options?: HandleCaptureOptions,
|
|
145
|
+
): {
|
|
66
146
|
capture: HandleCapture;
|
|
67
147
|
stop: () => void;
|
|
68
148
|
} {
|
|
69
149
|
ensureInterceptorInstalled(handleStore);
|
|
70
150
|
|
|
71
|
-
const capture: HandleCapture = { data: {} };
|
|
151
|
+
const capture: HandleCapture = { data: {}, ...options };
|
|
72
152
|
const captures = activeCapturesMap.get(handleStore)!;
|
|
73
153
|
captures.add(capture);
|
|
74
154
|
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* during segment resolution and stored alongside cached segments.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { replayLoaderPush } from "./handle-capture.js";
|
|
9
10
|
import type { ResolvedSegment } from "../types.js";
|
|
10
11
|
import type { HandleStore } from "../server/handle-store.js";
|
|
11
|
-
import type { SegmentHandleData } from "./types.js";
|
|
12
|
+
import type { HandleOwners, SegmentHandleData } from "./types.js";
|
|
12
13
|
// segment-codec eagerly pulls @vitejs/plugin-rsc (a virtual: module unresolvable
|
|
13
14
|
// in plain node/vitest). It is imported LAZILY inside the two async encode/decode
|
|
14
15
|
// helpers below so that modules which import handle-snapshot only for the
|
|
@@ -47,19 +48,32 @@ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
|
|
|
47
48
|
]);
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
/**
|
|
52
|
+
* `onError` reaches the Flight encode (see serializeResult). A cache writer
|
|
53
|
+
* passes the collector it gives the value's encode, so a handle value that
|
|
54
|
+
* fails to encode (a rejected pushed promise) refuses the whole entry like a
|
|
55
|
+
* failed value does; prerender (prerender-match.ts) passes one too and
|
|
56
|
+
* hands the error to its prerender.onError policy.
|
|
57
|
+
*/
|
|
58
|
+
export async function encodeHandles(
|
|
59
|
+
handles: HandleRecord,
|
|
60
|
+
onError?: (error: unknown) => void,
|
|
61
|
+
): Promise<string> {
|
|
51
62
|
if (!hasHandleData(handles)) return "";
|
|
52
|
-
return encodeHandleValue(handles);
|
|
63
|
+
return encodeHandleValue(handles, onError);
|
|
53
64
|
}
|
|
54
65
|
|
|
55
66
|
export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
56
67
|
return decodeHandleValue<HandleRecord>(encoded);
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
export async function encodeHandleValue(
|
|
70
|
+
export async function encodeHandleValue(
|
|
71
|
+
value: unknown,
|
|
72
|
+
onError?: (error: unknown) => void,
|
|
73
|
+
): Promise<string> {
|
|
60
74
|
const { serializeResult } = await import("./segment-codec.js");
|
|
61
75
|
const encoded = await withTimeout(
|
|
62
|
-
serializeResult(value),
|
|
76
|
+
serializeResult(value, onError),
|
|
63
77
|
HANDLE_ENCODE_TIMEOUT_MS,
|
|
64
78
|
null,
|
|
65
79
|
);
|
|
@@ -84,49 +98,140 @@ export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
|
84
98
|
* Capture handle data for a set of segments from the handle store.
|
|
85
99
|
* Used when caching segments to preserve their handle data.
|
|
86
100
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
101
|
+
* DSL-loader pushes stay out of the CACHE WRITE: a HIT runs loaders exactly
|
|
102
|
+
* as an uncached render of the same request would (cache-lookup.ts
|
|
103
|
+
* resolveFreshLoadersAndYield), so replaying a recorded copy would duplicate
|
|
104
|
+
* the live push. Handler pushes, including
|
|
105
|
+
* handler-invoked ctx.use(Loader) bodies (skipped with their handler on a
|
|
106
|
+
* HIT), are kept. The store tags loader pushes by array position at push
|
|
107
|
+
* time, so primitive values are covered. A PPR shell capture untags the
|
|
108
|
+
* settled bake-lane pushes its record keeps (shell-capture.ts) and gives
|
|
109
|
+
* them an owner (captureHandleOwners). Only this call site passes
|
|
110
|
+
* excludeLoaderPushes; every other getDataForSegment consumer (the
|
|
111
|
+
* render-barrier snapshot, prerender) sees every push.
|
|
94
112
|
*/
|
|
95
113
|
export function captureHandles(
|
|
96
114
|
segments: ResolvedSegment[],
|
|
97
115
|
handleStore: HandleStore,
|
|
98
|
-
exclude?: WeakSet<object>,
|
|
99
116
|
): Record<string, SegmentHandleData> {
|
|
100
117
|
const handles: Record<string, SegmentHandleData> = {};
|
|
101
118
|
for (const seg of segments) {
|
|
102
|
-
|
|
103
|
-
if (!exclude) {
|
|
104
|
-
handles[seg.id] = data;
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
const filtered: SegmentHandleData = {};
|
|
108
|
-
for (const [handleName, values] of Object.entries(data)) {
|
|
109
|
-
const kept = values.filter(
|
|
110
|
-
(v) => typeof v !== "object" || v === null || !exclude.has(v),
|
|
111
|
-
);
|
|
112
|
-
if (kept.length > 0) filtered[handleName] = kept;
|
|
113
|
-
}
|
|
114
|
-
handles[seg.id] = filtered;
|
|
119
|
+
handles[seg.id] = handleStore.getDataForSegment(seg.id, true);
|
|
115
120
|
}
|
|
116
121
|
return handles;
|
|
117
122
|
}
|
|
118
123
|
|
|
124
|
+
/** The owners of captureHandles' values; undefined when none is owned. */
|
|
125
|
+
export function captureHandleOwners(
|
|
126
|
+
segments: ResolvedSegment[],
|
|
127
|
+
handleStore: HandleStore,
|
|
128
|
+
): HandleOwners | undefined {
|
|
129
|
+
let owners: HandleOwners | undefined;
|
|
130
|
+
for (const seg of segments) {
|
|
131
|
+
const segOwners = handleStore.getRecordOwners(seg.id);
|
|
132
|
+
if (segOwners) (owners ??= {})[seg.id] = segOwners;
|
|
133
|
+
}
|
|
134
|
+
return owners;
|
|
135
|
+
}
|
|
136
|
+
|
|
119
137
|
/**
|
|
120
138
|
* Restore handle data from a cached snapshot into the handle store.
|
|
121
|
-
* Used when serving cached segments to replay their handle data
|
|
139
|
+
* Used when serving cached segments to replay their handle data: a route
|
|
140
|
+
* cache() record owns its segments' arrays, so it REPLACES them.
|
|
141
|
+
*
|
|
142
|
+
* A segment with `owners` is replaced with empty arrays and re-pushed in
|
|
143
|
+
* recorded order, an owned value through pushReplayed: if the owning loader
|
|
144
|
+
* runs on the HIT (a live loader, or a promise-carrying `ssr: false` one;
|
|
145
|
+
* a promise-free one is served from the shell and does not), its live pushes
|
|
146
|
+
* replace the recorded ones in place instead of appending a second copy. `claim`
|
|
147
|
+
* (setupLoaderAccess _claimLoaderPushes) is asked once per owner, so the
|
|
148
|
+
* loader's own cache() HIT does not replay its pushes a second time
|
|
149
|
+
* (loader-cache.ts replayLoaderHandles); an owner it refuses is skipped. A
|
|
150
|
+
* record without owners restores as a plain replay.
|
|
122
151
|
*/
|
|
123
152
|
export function restoreHandles(
|
|
124
153
|
handles: Record<string, SegmentHandleData>,
|
|
125
154
|
handleStore: HandleStore,
|
|
155
|
+
owners?: HandleOwners,
|
|
156
|
+
claim?: (loaderId: string) => boolean,
|
|
126
157
|
): void {
|
|
158
|
+
let delivers: Map<string, boolean> | undefined;
|
|
127
159
|
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
128
|
-
if (Object.keys(segHandles).length
|
|
160
|
+
if (Object.keys(segHandles).length === 0) continue;
|
|
161
|
+
const segOwners = owners?.[segId];
|
|
162
|
+
if (!segOwners) {
|
|
129
163
|
handleStore.replaySegmentData(segId, segHandles);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const cleared: SegmentHandleData = {};
|
|
167
|
+
for (const handleName in segHandles) cleared[handleName] = [];
|
|
168
|
+
handleStore.replaySegmentData(segId, cleared);
|
|
169
|
+
for (const [handleName, values] of Object.entries(segHandles)) {
|
|
170
|
+
const valueOwners = segOwners[handleName];
|
|
171
|
+
for (let i = 0; i < values.length; i++) {
|
|
172
|
+
const owner = valueOwners?.[i];
|
|
173
|
+
if (!owner) {
|
|
174
|
+
handleStore.push(handleName, segId, values[i]);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
delivers ??= new Map();
|
|
178
|
+
let deliver = delivers.get(owner);
|
|
179
|
+
if (deliver === undefined) {
|
|
180
|
+
deliver = claim ? claim(owner) : true;
|
|
181
|
+
delivers.set(owner, deliver);
|
|
182
|
+
}
|
|
183
|
+
if (deliver) {
|
|
184
|
+
handleStore.pushReplayed(handleName, segId, values[i], owner);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Append recorded handle pushes to the store, in recorded order, via push()
|
|
193
|
+
* so active captures and loader-scope tagging see them like live pushes. For
|
|
194
|
+
* a cached unit that shares its segments with live pushes ("use cache");
|
|
195
|
+
* restoreHandles would wipe those. `segmentId` redirects every value to one
|
|
196
|
+
* segment (the caller's).
|
|
197
|
+
*
|
|
198
|
+
* Owner-keyed groups (useCacheRecordKey, `${seq}:${loaderId}`) are a loader's
|
|
199
|
+
* pushes, which reach the page once per request: `claim` (setupLoaderAccess
|
|
200
|
+
* _claimLoaderPushes) skips a loader that already ran or was replayed in this
|
|
201
|
+
* request, and a claimed loader that runs later replaces its replayed values
|
|
202
|
+
* (HandleStore.pushReplayed). `${seq}:` groups are the function's own pushes.
|
|
203
|
+
* A key without ":" is a segment id (records written before owner keys) and
|
|
204
|
+
* replays unchanged. A loader's own cache() replays through loader-cache.ts
|
|
205
|
+
* replayLoaderHandles.
|
|
206
|
+
*/
|
|
207
|
+
export function appendHandles(
|
|
208
|
+
handles: Record<string, SegmentHandleData>,
|
|
209
|
+
handleStore: HandleStore,
|
|
210
|
+
segmentId?: string,
|
|
211
|
+
claim?: (loaderId: string) => boolean,
|
|
212
|
+
): void {
|
|
213
|
+
let claims: Map<string, boolean> | undefined;
|
|
214
|
+
for (const [key, segHandles] of Object.entries(handles)) {
|
|
215
|
+
const colon = key.indexOf(":");
|
|
216
|
+
const owner = colon < 0 ? "" : key.slice(colon + 1);
|
|
217
|
+
if (owner) {
|
|
218
|
+
claims ??= new Map();
|
|
219
|
+
let deliver = claims.get(owner);
|
|
220
|
+
if (deliver === undefined) {
|
|
221
|
+
deliver = claim ? claim(owner) : true;
|
|
222
|
+
claims.set(owner, deliver);
|
|
223
|
+
}
|
|
224
|
+
if (!deliver) continue;
|
|
225
|
+
}
|
|
226
|
+
const target = segmentId ?? key;
|
|
227
|
+
for (const [handleName, values] of Object.entries(segHandles)) {
|
|
228
|
+
for (const value of values) {
|
|
229
|
+
if (owner) {
|
|
230
|
+
replayLoaderPush(handleStore, handleName, target, value, owner);
|
|
231
|
+
} else {
|
|
232
|
+
handleStore.push(handleName, target, value);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
130
235
|
}
|
|
131
236
|
}
|
|
132
237
|
}
|
package/src/cache/index.ts
CHANGED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-isolate tag-marker memos shared by the built-in stores (issue #941;
|
|
3
|
+
* docs/design/shell-entry-layout.md "The tag-marker memo").
|
|
4
|
+
*
|
|
5
|
+
* A tagged shell HIT waits for its tag-invalidation markers before the first
|
|
6
|
+
* byte. Two things here shorten that wait:
|
|
7
|
+
*
|
|
8
|
+
* - {@link TagNameHints}: which tag NAMES a shell key carried last time, so a
|
|
9
|
+
* HIT can start the marker reads alongside the entry read instead of after
|
|
10
|
+
* the entry's head names its tags. Never marker values.
|
|
11
|
+
* - {@link TagMarkerMemo}: the marker VALUES (a tag's latest invalidatedAt,
|
|
12
|
+
* or null), served stale-while-revalidate. A value younger than
|
|
13
|
+
* `markerFreshMs` is used as is; one younger than `markerMaxStaleMs` is used
|
|
14
|
+
* and refreshed from the store in the background; an older one blocks on
|
|
15
|
+
* the store read as before.
|
|
16
|
+
*
|
|
17
|
+
* Simple reads get SWR; a mutation gets correct reads. The request that runs
|
|
18
|
+
* updateTag()/revalidateTag() reads its own writes through the per-request
|
|
19
|
+
* memo; the isolate that runs it writes the new value through into this memo;
|
|
20
|
+
* and its response sets the fresh-reads cookie (request-context.ts), so the
|
|
21
|
+
* same user's next requests on OTHER isolates skip this memo and the shell
|
|
22
|
+
* memo for as long as either can be stale. Other users can see a marker up to
|
|
23
|
+
* `markerMaxStaleMs` old, on top of the platform's own consistency
|
|
24
|
+
* (Cloudflare KV is eventually consistent across locations, up to about 60 s;
|
|
25
|
+
* Vercel's `tm` markers are regional).
|
|
26
|
+
*
|
|
27
|
+
* Only PPR shell reads use the value memo. Cached data families (`cache()`,
|
|
28
|
+
* `"use cache"`, responses) and the shell write gate
|
|
29
|
+
* (`isTagsInvalidatedSince`) keep reading their markers, in every request,
|
|
30
|
+
* whatever its shell read found: a shell read's memoized values never reach
|
|
31
|
+
* the data families' per-request memo (cf-tag-marker-memo.ts
|
|
32
|
+
* ShellMarkerReads).
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
36
|
+
import type { ShellReadStats } from "./types.js";
|
|
37
|
+
|
|
38
|
+
/** Default freshness of a memoized marker on CFCacheStore (ms). */
|
|
39
|
+
export const DEFAULT_CF_MARKER_FRESH_MS = 1000;
|
|
40
|
+
/** Default freshness of a memoized marker on VercelCacheStore (ms). */
|
|
41
|
+
export const DEFAULT_VERCEL_MARKER_FRESH_MS = 300;
|
|
42
|
+
/** Default oldest memoized marker served while it refreshes (ms), CF. */
|
|
43
|
+
export const DEFAULT_CF_MARKER_MAX_STALE_MS = 10_000;
|
|
44
|
+
/**
|
|
45
|
+
* The same cap on VercelCacheStore: the runtime cache expires tags globally
|
|
46
|
+
* within about 300 ms, so a much longer stale window would widen that for
|
|
47
|
+
* other users more than KV's own up-to-60 s cross-location lag does on CF.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_VERCEL_MARKER_MAX_STALE_MS = 2000;
|
|
50
|
+
/** Most tags a marker memo holds (least recently used evicted). */
|
|
51
|
+
const MARKER_MEMO_MAX_ENTRIES = 4096;
|
|
52
|
+
/**
|
|
53
|
+
* Most shell keys a hint map holds (least recently used evicted).
|
|
54
|
+
* @internal Exported for tests that fill the map to its cap.
|
|
55
|
+
*/
|
|
56
|
+
export const TAG_HINTS_MAX_ENTRIES = 2048;
|
|
57
|
+
|
|
58
|
+
/** How a marker read was answered, for the debugPerformance marker row. */
|
|
59
|
+
export type MarkerMemoOutcome = "fresh" | "stale" | "read" | "bypass";
|
|
60
|
+
|
|
61
|
+
interface MarkerSlot {
|
|
62
|
+
value: number | null;
|
|
63
|
+
readAt: number;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A memoized marker a read may use: its value, and whether it is stale. */
|
|
67
|
+
export interface MarkerMemoHit {
|
|
68
|
+
value: number | null;
|
|
69
|
+
stale: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A store read of a marker for {@link TagMarkerMemo.readThrough}: the value,
|
|
74
|
+
* and whether it may be memoized (a timed-out read that failed open may not).
|
|
75
|
+
*/
|
|
76
|
+
export interface MarkerFetch {
|
|
77
|
+
value: number | null;
|
|
78
|
+
memoize: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Options of {@link TagMarkerMemo.readThrough}. */
|
|
82
|
+
export interface MarkerReadThroughOptions {
|
|
83
|
+
/** Fresh window (ms); 0 or less reads the store and memoizes nothing. */
|
|
84
|
+
freshMs: number;
|
|
85
|
+
/** The oldest value served while it refreshes (ms). */
|
|
86
|
+
maxStaleMs: number;
|
|
87
|
+
/** The request carries the fresh-reads cookie: read the store. */
|
|
88
|
+
bypass: boolean;
|
|
89
|
+
/** Keeps a background refresh alive after the response (waitUntil). */
|
|
90
|
+
keepAlive?: (pending: Promise<void>) => void;
|
|
91
|
+
/** How the read was answered, for the debugPerformance marker row. */
|
|
92
|
+
onOutcome?: (outcome: MarkerMemoOutcome) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class TagMarkerMemo {
|
|
96
|
+
private readonly slots = new Map<string, MarkerSlot>();
|
|
97
|
+
private readonly refreshing = new Map<string, Promise<void>>();
|
|
98
|
+
|
|
99
|
+
constructor(private readonly maxEntries: number = MARKER_MEMO_MAX_ENTRIES) {}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The memoized marker for `key` if it is younger than `maxStaleMs` (stale
|
|
103
|
+
* when older than `freshMs`), or undefined when the caller must read the
|
|
104
|
+
* store. `freshMs <= 0` disables the memo.
|
|
105
|
+
*/
|
|
106
|
+
lookup(
|
|
107
|
+
key: string,
|
|
108
|
+
freshMs: number,
|
|
109
|
+
maxStaleMs: number,
|
|
110
|
+
now: number = Date.now(),
|
|
111
|
+
): MarkerMemoHit | undefined {
|
|
112
|
+
if (freshMs <= 0) return undefined;
|
|
113
|
+
const slot = this.slots.get(key);
|
|
114
|
+
if (!slot) return undefined;
|
|
115
|
+
const age = now - slot.readAt;
|
|
116
|
+
if (age >= Math.max(freshMs, maxStaleMs)) return undefined;
|
|
117
|
+
this.slots.delete(key);
|
|
118
|
+
this.slots.set(key, slot);
|
|
119
|
+
return { value: slot.value, stale: age >= freshMs };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record a marker the store returned to a read that started at
|
|
124
|
+
* `readStartedAt`. The age counts from the read's start, not its end: a
|
|
125
|
+
* marker written while the read was in flight may be missing from it, so
|
|
126
|
+
* the value is only known to be current as of the start. Markers only move
|
|
127
|
+
* forward, so a value older than one already held (a write-through that
|
|
128
|
+
* landed during the read) is kept at the newer value, with the later stamp.
|
|
129
|
+
*/
|
|
130
|
+
store(
|
|
131
|
+
key: string,
|
|
132
|
+
value: number | null,
|
|
133
|
+
readStartedAt: number = Date.now(),
|
|
134
|
+
): void {
|
|
135
|
+
const held = this.slots.get(key);
|
|
136
|
+
const heldValue = held?.value ?? null;
|
|
137
|
+
const next =
|
|
138
|
+
heldValue !== null && (value === null || heldValue > value)
|
|
139
|
+
? heldValue
|
|
140
|
+
: value;
|
|
141
|
+
this.slots.delete(key);
|
|
142
|
+
this.slots.set(key, {
|
|
143
|
+
value: next,
|
|
144
|
+
readAt: Math.max(held?.readAt ?? readStartedAt, readStartedAt),
|
|
145
|
+
});
|
|
146
|
+
if (this.slots.size > this.maxEntries) {
|
|
147
|
+
const oldest = this.slots.keys().next().value as string;
|
|
148
|
+
this.slots.delete(oldest);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Refresh a stale marker in the background: one read per key at a time,
|
|
154
|
+
* handed to `keepAlive` (the store's waitUntil) so the runtime lets it
|
|
155
|
+
* finish after the response; without it the read runs detached. A read
|
|
156
|
+
* that fails, or may not be memoized, stores nothing.
|
|
157
|
+
*/
|
|
158
|
+
refresh(
|
|
159
|
+
key: string,
|
|
160
|
+
read: () => Promise<MarkerFetch>,
|
|
161
|
+
keepAlive?: (pending: Promise<void>) => void,
|
|
162
|
+
): void {
|
|
163
|
+
if (this.refreshing.has(key)) return;
|
|
164
|
+
const pending = (async (): Promise<void> => {
|
|
165
|
+
const startedAt = Date.now();
|
|
166
|
+
try {
|
|
167
|
+
const { value, memoize } = await read();
|
|
168
|
+
if (memoize) this.store(key, value, startedAt);
|
|
169
|
+
} catch {
|
|
170
|
+
// The next read past the stale window blocks on the store anyway.
|
|
171
|
+
} finally {
|
|
172
|
+
this.refreshing.delete(key);
|
|
173
|
+
}
|
|
174
|
+
})();
|
|
175
|
+
this.refreshing.set(key, pending);
|
|
176
|
+
keepAlive?.(pending);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A marker through the memo, stale-while-revalidate: a value within
|
|
181
|
+
* `freshMs` is used as is; one within `maxStaleMs` is used while one
|
|
182
|
+
* background read refreshes it; otherwise, and for a request carrying the
|
|
183
|
+
* fresh-reads cookie, the store is read (`fetch(false)`) and the result
|
|
184
|
+
* memoized. `fetch(true)` is the background refresh, outside any request.
|
|
185
|
+
*/
|
|
186
|
+
async readThrough(
|
|
187
|
+
key: string,
|
|
188
|
+
fetch: (background: boolean) => Promise<MarkerFetch>,
|
|
189
|
+
options: MarkerReadThroughOptions,
|
|
190
|
+
): Promise<number | null> {
|
|
191
|
+
if (options.freshMs <= 0) return (await fetch(false)).value;
|
|
192
|
+
if (!options.bypass) {
|
|
193
|
+
const hit = this.lookup(key, options.freshMs, options.maxStaleMs);
|
|
194
|
+
if (hit) {
|
|
195
|
+
options.onOutcome?.(hit.stale ? "stale" : "fresh");
|
|
196
|
+
if (hit.stale) this.refresh(key, () => fetch(true), options.keepAlive);
|
|
197
|
+
return hit.value;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const startedAt = Date.now();
|
|
201
|
+
const { value, memoize } = await fetch(false);
|
|
202
|
+
if (memoize) this.store(key, value, startedAt);
|
|
203
|
+
options.onOutcome?.(options.bypass ? "bypass" : "read");
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
clear(): void {
|
|
208
|
+
this.slots.clear();
|
|
209
|
+
this.refreshing.clear();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* The tag names a shell key's entry carried when it was last read or written
|
|
215
|
+
* in this isolate, so the next HIT can start their marker reads before the
|
|
216
|
+
* entry names them. A wrong or missing hint costs a wasted read or nothing;
|
|
217
|
+
* the freshness check always uses the entry's own tags.
|
|
218
|
+
*/
|
|
219
|
+
export class TagNameHints {
|
|
220
|
+
private readonly keys = new Map<string, readonly string[]>();
|
|
221
|
+
|
|
222
|
+
constructor(private readonly maxEntries: number = TAG_HINTS_MAX_ENTRIES) {}
|
|
223
|
+
|
|
224
|
+
get(key: string): readonly string[] | undefined {
|
|
225
|
+
return this.keys.get(key);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
remember(key: string, tags: readonly string[] | undefined): void {
|
|
229
|
+
this.keys.delete(key);
|
|
230
|
+
if (!tags || tags.length === 0) return;
|
|
231
|
+
this.keys.set(key, tags);
|
|
232
|
+
if (this.keys.size > this.maxEntries) {
|
|
233
|
+
const oldest = this.keys.keys().next().value as string;
|
|
234
|
+
this.keys.delete(oldest);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
clear(): void {
|
|
239
|
+
this.keys.clear();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** The union of a key's remembered tag names and the route's static tags. */
|
|
244
|
+
export function hintedTags(
|
|
245
|
+
remembered: readonly string[] | undefined,
|
|
246
|
+
routeTags: readonly string[] | undefined,
|
|
247
|
+
): string[] {
|
|
248
|
+
if (!routeTags || routeTags.length === 0)
|
|
249
|
+
return remembered ? [...remembered] : [];
|
|
250
|
+
if (!remembered || remembered.length === 0) return [...routeTags];
|
|
251
|
+
return [...new Set([...remembered, ...routeTags])];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The marker row's memo outcome for a read's tags: the most store-bound one
|
|
256
|
+
* (a bypass or store read outranks a stale value, which outranks a fresh one).
|
|
257
|
+
*/
|
|
258
|
+
export function summarizeMarkerOutcomes(
|
|
259
|
+
tags: readonly string[] | undefined,
|
|
260
|
+
outcomes: ReadonlyMap<string, MarkerMemoOutcome>,
|
|
261
|
+
): MarkerMemoOutcome | undefined {
|
|
262
|
+
let summary: MarkerMemoOutcome | undefined;
|
|
263
|
+
for (const tag of tags ?? []) {
|
|
264
|
+
const outcome = outcomes.get(tag);
|
|
265
|
+
if (
|
|
266
|
+
outcome &&
|
|
267
|
+
(!summary || OUTCOME_RANK[outcome] > OUTCOME_RANK[summary])
|
|
268
|
+
) {
|
|
269
|
+
summary = outcome;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return summary;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const OUTCOME_RANK: Record<MarkerMemoOutcome, number> = {
|
|
276
|
+
fresh: 0,
|
|
277
|
+
stale: 1,
|
|
278
|
+
read: 2,
|
|
279
|
+
bypass: 3,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Fill a shell read's marker-row stats (debugPerformance) for the entry's
|
|
284
|
+
* own tags: the memo outcome, and how many of them the read had hinted.
|
|
285
|
+
*/
|
|
286
|
+
export function recordMarkerRow(
|
|
287
|
+
stats: ShellReadStats,
|
|
288
|
+
tags: readonly string[] | undefined,
|
|
289
|
+
outcomes: ReadonlyMap<string, MarkerMemoOutcome> | undefined,
|
|
290
|
+
): void {
|
|
291
|
+
if (!tags || tags.length === 0) return;
|
|
292
|
+
if (outcomes) stats.markerMemo = summarizeMarkerOutcomes(tags, outcomes);
|
|
293
|
+
if (stats.markerHinted) {
|
|
294
|
+
const hinted = stats.markerHinted;
|
|
295
|
+
stats.markerHintHits = tags.filter((tag) => hinted.includes(tag)).length;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* True when this request carries the fresh-reads cookie: a request of the same
|
|
301
|
+
* user after an updateTag()/revalidateTag(), which must not be served from an
|
|
302
|
+
* isolate memo (shell or marker) that could predate the mutation.
|
|
303
|
+
*/
|
|
304
|
+
export function freshReadsRequired(
|
|
305
|
+
ctx: Pick<RequestContext, "_freshReads"> | undefined,
|
|
306
|
+
): boolean {
|
|
307
|
+
return ctx?._freshReads === true;
|
|
308
|
+
}
|