@rangojs/router 0.17.0 → 0.19.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/event-controller.d.ts +3 -3
- package/dist/types/browser/history-state.d.ts +18 -0
- package/dist/types/browser/navigation-transaction.d.ts +20 -2
- package/dist/types/browser/partial-update.d.ts +16 -1
- package/dist/types/browser/prefetch/cache.d.ts +6 -6
- package/dist/types/browser/react/Link.d.ts +19 -9
- package/dist/types/browser/react/context.d.ts +2 -2
- package/dist/types/browser/react/location-state-shared.d.ts +95 -56
- package/dist/types/browser/react/location-state.d.ts +1 -1
- package/dist/types/browser/transition-when.d.ts +80 -0
- package/dist/types/browser/types.d.ts +104 -9
- package/dist/types/build/generate-manifest.d.ts +8 -0
- package/dist/types/cache/background-task.d.ts +24 -2
- package/dist/types/cache/cache-exec-scope.d.ts +31 -0
- package/dist/types/cache/cache-key-utils.d.ts +48 -2
- package/dist/types/cache/cache-policy.d.ts +18 -1
- package/dist/types/cache/cache-runtime.d.ts +7 -0
- package/dist/types/cache/cache-scope.d.ts +192 -26
- package/dist/types/cache/cache-tag.d.ts +116 -11
- 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 +224 -72
- 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 +19 -0
- package/dist/types/cache/document-cache.d.ts +10 -0
- package/dist/types/cache/handle-snapshot.d.ts +41 -8
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/invalidation-order.d.ts +23 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- package/dist/types/cache/request-tag-mask.d.ts +54 -0
- package/dist/types/cache/shell-memo.d.ts +178 -0
- package/dist/types/cache/shell-snapshot.d.ts +141 -105
- package/dist/types/cache/tag-invalidation.d.ts +54 -3
- package/dist/types/cache/types.d.ts +267 -96
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
- package/dist/types/client-urls/navigation.d.ts +28 -2
- package/dist/types/client-urls/server-projection.d.ts +5 -2
- package/dist/types/client-urls/types.d.ts +5 -5
- package/dist/types/client.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/route-definition/helpers-types.d.ts +11 -11
- package/dist/types/route-name.d.ts +7 -0
- package/dist/types/router/error-handling.d.ts +1 -1
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/intercept-resolution.d.ts +2 -0
- package/dist/types/router/match-context.d.ts +5 -0
- package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/router-options.d.ts +4 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
- package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
- package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
- package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
- package/dist/types/router/transition-when.d.ts +24 -10
- package/dist/types/rsc/attach-transition-when.d.ts +18 -0
- package/dist/types/rsc/full-payload.d.ts +21 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/response-cache-serve.d.ts +15 -12
- package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
- package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
- package/dist/types/rsc/shell-capture.d.ts +202 -74
- package/dist/types/rsc/shell-serve.d.ts +144 -50
- package/dist/types/rsc/types.d.ts +9 -0
- package/dist/types/segment-system.d.ts +7 -0
- package/dist/types/server/context.d.ts +155 -33
- package/dist/types/server/cookie-store.d.ts +33 -0
- package/dist/types/server/handle-store.d.ts +52 -1
- package/dist/types/server/request-context.d.ts +210 -63
- package/dist/types/server/request-headers.d.ts +44 -0
- package/dist/types/server.d.ts +1 -0
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +7 -0
- package/dist/types/testing/index.d.ts +3 -2
- package/dist/types/testing/internal/action-id.d.ts +7 -0
- package/dist/types/testing/location-state-key.d.ts +24 -0
- package/dist/types/testing/render-route.d.ts +17 -1
- package/dist/types/testing/run-loader.d.ts +18 -3
- package/dist/types/testing/run-transition-when.d.ts +44 -69
- package/dist/types/testing/serve-shell-request.d.ts +133 -0
- package/dist/types/testing/shell-status.d.ts +44 -19
- package/dist/types/transition-when-ref.d.ts +79 -0
- package/dist/types/types/cache-types.d.ts +15 -3
- package/dist/types/types/handler-context.d.ts +14 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/types/segments.d.ts +86 -71
- package/dist/types/urls/path-helper-types.d.ts +3 -4
- package/dist/types/urls/pattern-types.d.ts +21 -18
- package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +1005 -105
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +175 -76
- package/skills/caching/SKILL.md +361 -33
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +12 -7
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +52 -37
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +32 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/SKILL.md +3 -1
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/navigation.md +23 -2
- package/skills/hooks/state.md +101 -7
- package/skills/intercept/SKILL.md +29 -12
- package/skills/loader/SKILL.md +197 -44
- package/skills/middleware/SKILL.md +34 -9
- package/skills/migrate-nextjs/SKILL.md +68 -37
- package/skills/migrate-react-router/SKILL.md +2 -1
- package/skills/migrate-react-router/component-migration.md +3 -1
- package/skills/migrate-react-router/data-and-actions.md +3 -1
- package/skills/migrate-react-router/route-mapping.md +7 -4
- package/skills/observability/SKILL.md +65 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +715 -371
- package/skills/prerender/SKILL.md +30 -17
- package/skills/rango/SKILL.md +72 -34
- package/skills/route/SKILL.md +14 -7
- package/skills/router-setup/SKILL.md +4 -2
- package/skills/scripts/SKILL.md +8 -1
- package/skills/server-actions/SKILL.md +11 -0
- package/skills/shell-manifest/SKILL.md +21 -12
- package/skills/testing/SKILL.md +49 -43
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/client-components.md +45 -3
- package/skills/testing/loader.md +19 -1
- package/skills/testing/middleware.md +1 -0
- package/skills/testing/predicates.md +49 -49
- package/skills/testing/render-handler.md +1 -0
- package/skills/testing/serve-shell-request.md +202 -0
- package/skills/testing/server-actions.md +9 -3
- package/skills/theme/SKILL.md +56 -4
- package/skills/typesafety/env-and-bindings.md +18 -2
- package/skills/use-cache/SKILL.md +91 -23
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +150 -29
- package/src/__augment-tests__/augmented.check.ts +23 -0
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/event-controller.ts +4 -4
- package/src/browser/history-state.ts +75 -1
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +77 -18
- package/src/browser/navigation-client.ts +4 -3
- package/src/browser/navigation-store.ts +43 -1
- package/src/browser/navigation-transaction.ts +31 -11
- package/src/browser/partial-update.ts +146 -35
- package/src/browser/prefetch/cache.ts +6 -6
- package/src/browser/prefetch/fetch.ts +2 -2
- package/src/browser/react/Link.tsx +38 -33
- package/src/browser/react/NavigationProvider.tsx +2 -2
- package/src/browser/react/context.ts +2 -2
- package/src/browser/react/location-state-shared.ts +241 -49
- package/src/browser/react/location-state.ts +5 -2
- package/src/browser/rsc-router.tsx +9 -0
- package/src/browser/server-action-bridge.ts +84 -15
- package/src/browser/transition-when.ts +205 -0
- package/src/browser/types.ts +124 -10
- package/src/build/generate-manifest.ts +77 -1
- package/src/cache/background-task.ts +55 -2
- package/src/cache/cache-exec-scope.ts +48 -1
- package/src/cache/cache-key-utils.ts +62 -2
- package/src/cache/cache-policy.ts +31 -4
- package/src/cache/cache-runtime.ts +209 -129
- package/src/cache/cache-scope.ts +573 -69
- package/src/cache/cache-tag.ts +475 -19
- 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 +1094 -376
- 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 +55 -40
- package/src/cache/document-cache.ts +53 -7
- package/src/cache/handle-snapshot.ts +53 -9
- package/src/cache/index.ts +2 -0
- package/src/cache/invalidation-order.ts +84 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/request-tag-mask.ts +147 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +249 -277
- package/src/cache/tag-invalidation.ts +109 -18
- package/src/cache/types.ts +274 -102
- package/src/cache/vercel/vercel-cache-store.ts +561 -74
- package/src/client-urls/client-root.tsx +8 -4
- package/src/client-urls/client-urls.ts +8 -6
- package/src/client-urls/navigation.ts +55 -4
- package/src/client-urls/server-projection.ts +31 -10
- package/src/client-urls/types.ts +12 -5
- package/src/client.tsx +2 -0
- package/src/host/cookie-handler.ts +2 -1
- package/src/index.rsc.ts +3 -0
- package/src/index.ts +3 -0
- package/src/loader-redirect.tsx +7 -3
- package/src/prerender/build-shell-capture.ts +173 -110
- package/src/route-definition/dsl-helpers.ts +6 -3
- package/src/route-definition/helpers-types.ts +11 -11
- package/src/route-name.ts +12 -0
- package/src/router/content-negotiation.ts +4 -1
- package/src/router/error-handling.ts +5 -2
- package/src/router/handler-context.ts +42 -5
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +30 -8
- package/src/router/loader-resolution.ts +89 -33
- package/src/router/logging.ts +5 -3
- package/src/router/match-api.ts +89 -42
- package/src/router/match-context.ts +5 -0
- package/src/router/match-handlers.ts +14 -29
- package/src/router/match-middleware/background-revalidation.ts +3 -5
- package/src/router/match-middleware/cache-lookup.ts +52 -19
- package/src/router/match-middleware/cache-store.ts +36 -10
- package/src/router/match-result.ts +1 -0
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +20 -4
- package/src/router/navigation-snapshot.ts +6 -5
- package/src/router/prerender-match.ts +10 -0
- package/src/router/request-classification.ts +7 -4
- package/src/router/router-options.ts +4 -1
- package/src/router/segment-resolution/fresh.ts +18 -13
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +366 -70
- package/src/router/segment-resolution/loader-mask.ts +3 -4
- package/src/router/segment-resolution/loader-snapshot.ts +215 -126
- package/src/router/segment-resolution/mask-nested.ts +161 -42
- package/src/router/segment-resolution/revalidation.ts +27 -13
- package/src/router/segment-resolution/view-transition-default.ts +6 -21
- package/src/router/telemetry.ts +6 -3
- package/src/router/transition-when.ts +34 -52
- package/src/router.ts +2 -1
- package/src/rsc/attach-transition-when.ts +47 -0
- package/src/rsc/full-payload.ts +47 -3
- package/src/rsc/handler.ts +24 -10
- package/src/rsc/loader-fetch.ts +2 -1
- package/src/rsc/manifest-init.ts +4 -0
- package/src/rsc/origin-guard.ts +8 -5
- package/src/rsc/progressive-enhancement.ts +10 -32
- package/src/rsc/response-cache-serve.ts +69 -78
- package/src/rsc/response-route-handler.ts +2 -2
- package/src/rsc/rsc-rendering.ts +712 -338
- package/src/rsc/server-action.ts +8 -20
- package/src/rsc/shell-build-manifest.ts +49 -29
- package/src/rsc/shell-capture-constants.ts +182 -12
- package/src/rsc/shell-capture.ts +1126 -612
- package/src/rsc/shell-serve.ts +331 -85
- package/src/rsc/ssr-setup.ts +3 -2
- package/src/rsc/types.ts +9 -0
- package/src/segment-system.tsx +40 -7
- package/src/server/context.ts +328 -72
- package/src/server/cookie-store.ts +184 -133
- package/src/server/handle-store.ts +245 -27
- package/src/server/request-context.ts +364 -100
- package/src/server/request-headers.ts +134 -0
- package/src/server.ts +4 -0
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/dispatch.ts +4 -7
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/index.ts +5 -0
- package/src/testing/internal/action-id.ts +25 -0
- package/src/testing/internal/context.ts +1 -1
- package/src/testing/location-state-key.ts +45 -0
- package/src/testing/render-handler.ts +1 -1
- package/src/testing/render-route.tsx +109 -16
- package/src/testing/run-client-revalidate.ts +3 -19
- package/src/testing/run-loader.ts +67 -5
- package/src/testing/run-transition-when.ts +109 -172
- package/src/testing/serve-shell-request.ts +381 -0
- package/src/testing/shell-status.ts +73 -47
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/transition-when-ref.ts +220 -0
- package/src/types/cache-types.ts +15 -3
- package/src/types/handler-context.ts +14 -1
- package/src/types/index.ts +3 -0
- package/src/types/segments.ts +94 -91
- package/src/urls/path-helper-types.ts +3 -4
- package/src/urls/pattern-types.ts +21 -18
- package/src/vite/discovery/discover-routers.ts +3 -0
- package/src/vite/discovery/shell-prerender-phase.ts +9 -2
- package/src/vite/discovery/virtual-module-codegen.ts +16 -6
- package/src/vite/plugins/transition-when-hoist.ts +1064 -0
- package/src/vite/rango.ts +2 -0
- package/src/vite/router-discovery.ts +89 -9
- package/dist/types/rsc/transition-gate.d.ts +0 -23
- package/src/rsc/transition-gate.ts +0 -86
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* `@vercel/functions`). It is the production store analogue to CFCacheStore, but
|
|
6
6
|
* far smaller: Vercel's Runtime Cache already IS a distributed, tag-aware cache
|
|
7
7
|
* (regional storage, global tag-expire within ~300ms), so none of CFCacheStore's
|
|
8
|
-
* L1/L2 tiering
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* L1/L2 tiering or per-tier timeout budgets are needed here - the platform does
|
|
9
|
+
* that work. Tag markers (`tm`, see isTagsInvalidatedSince) gate shell reads
|
|
10
|
+
* and writes and every cache write that started before an invalidation
|
|
11
|
+
* (#977); only PPR shell reads memoize them. What this
|
|
12
|
+
* store adds on top of the raw primitive is the parts Vercel does NOT give us:
|
|
11
13
|
*
|
|
12
14
|
* 1. Stale-while-revalidate. `getCache` has no stale-but-serve: a TTL'd entry
|
|
13
15
|
* simply becomes a miss. We store our own {staleAt, expiresAt} envelope and
|
|
@@ -40,16 +42,23 @@ import type {
|
|
|
40
42
|
CacheItemOptions,
|
|
41
43
|
ShellCacheEntry,
|
|
42
44
|
ShellSnapshotRecord,
|
|
45
|
+
ShellDocumentRead,
|
|
46
|
+
ShellDocumentReadOptions,
|
|
47
|
+
ShellReadStats,
|
|
43
48
|
CacheReadError,
|
|
44
49
|
} from "../types.js";
|
|
45
50
|
import { CACHE_READ_ERROR } from "../types.js";
|
|
46
|
-
import
|
|
51
|
+
import {
|
|
52
|
+
_getRequestContext,
|
|
53
|
+
type RequestContext,
|
|
54
|
+
} from "../../server/request-context.js";
|
|
47
55
|
import { isPerClientSignalHeader } from "../../browser/cookie-name.js";
|
|
48
56
|
import {
|
|
49
57
|
resolveTtl,
|
|
50
58
|
resolveSwrWindow,
|
|
51
59
|
computeExpiration,
|
|
52
60
|
DEFAULT_FUNCTION_TTL,
|
|
61
|
+
staleShellRecaptureDue,
|
|
53
62
|
} from "../cache-policy.js";
|
|
54
63
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
55
64
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
@@ -61,7 +70,37 @@ import type { CacheErrorCategory } from "../cache-error.js";
|
|
|
61
70
|
// byte-identical to a per-byte encoder (chunk size does not affect base64), so
|
|
62
71
|
// this is a robustness fix, not a format change. Do NOT reintroduce a local
|
|
63
72
|
// spread-based encoder or raise the chunk here; cf-base64.ts is import-pure.
|
|
64
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
bufferToBase64,
|
|
75
|
+
base64ByteLength,
|
|
76
|
+
base64ToBuffer,
|
|
77
|
+
base64ToBytes,
|
|
78
|
+
} from "../cf/cf-base64.js";
|
|
79
|
+
import {
|
|
80
|
+
gateMarkerRead,
|
|
81
|
+
maskRequestTags,
|
|
82
|
+
maskedForRequest,
|
|
83
|
+
} from "../request-tag-mask.js";
|
|
84
|
+
import {
|
|
85
|
+
ShellMemo,
|
|
86
|
+
freshReadsWindowMs,
|
|
87
|
+
isShellFresh,
|
|
88
|
+
resolveShellMemoOptions,
|
|
89
|
+
shellHasAnyTag,
|
|
90
|
+
type ResolvedShellMemoOptions,
|
|
91
|
+
type StoreMemoOptions,
|
|
92
|
+
} from "../shell-memo.js";
|
|
93
|
+
import {
|
|
94
|
+
TagMarkerMemo,
|
|
95
|
+
TagNameHints,
|
|
96
|
+
hintedTags,
|
|
97
|
+
freshReadsRequired,
|
|
98
|
+
recordMarkerRow,
|
|
99
|
+
DEFAULT_VERCEL_MARKER_FRESH_MS,
|
|
100
|
+
DEFAULT_VERCEL_MARKER_MAX_STALE_MS,
|
|
101
|
+
type MarkerFetch,
|
|
102
|
+
type MarkerMemoOutcome,
|
|
103
|
+
} from "../isolate-tag-memo.js";
|
|
65
104
|
|
|
66
105
|
/**
|
|
67
106
|
* Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
|
|
@@ -133,8 +172,9 @@ type CacheFamily = "s" | "i" | "r" | "h" | "tm";
|
|
|
133
172
|
|
|
134
173
|
/**
|
|
135
174
|
* TTL for tag-invalidation marker entries ("tm" family), written by
|
|
136
|
-
* invalidateTags for the build-shell read-through
|
|
137
|
-
* gate
|
|
175
|
+
* invalidateTags for isTagsInvalidatedSince (the build-shell read-through,
|
|
176
|
+
* the shell write gate, and the write gate of cache executions, #977).
|
|
177
|
+
* The platform's expireTag() DELETES tagged entries (no queryable
|
|
138
178
|
* history), so the markers are rango's own record of "tag X was invalidated
|
|
139
179
|
* at T". One year: runtime tagged-shell retention is capped to this lifetime,
|
|
140
180
|
* while buildVersion retires build shells on the next deploy. An expired marker
|
|
@@ -150,6 +190,8 @@ interface VercelSegmentEnvelope {
|
|
|
150
190
|
s: number;
|
|
151
191
|
/** expiresAt (ms since epoch). */
|
|
152
192
|
e: number;
|
|
193
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
194
|
+
ta?: number;
|
|
153
195
|
}
|
|
154
196
|
|
|
155
197
|
/** Stored envelope for a "use cache" function result (getItem/setItem). */
|
|
@@ -164,6 +206,8 @@ interface VercelItemEnvelope {
|
|
|
164
206
|
e: number;
|
|
165
207
|
/** Tags, surfaced on read so a hit still contributes to the document tag set. */
|
|
166
208
|
t?: string[];
|
|
209
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
210
|
+
ta?: number;
|
|
167
211
|
}
|
|
168
212
|
|
|
169
213
|
/** Stored envelope for a full Response (getResponse/putResponse). */
|
|
@@ -180,6 +224,8 @@ interface VercelResponseEnvelope {
|
|
|
180
224
|
e: number;
|
|
181
225
|
/** Tags, preserved so a background revalidation re-write keeps them. */
|
|
182
226
|
t?: string[];
|
|
227
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
228
|
+
ta?: number;
|
|
183
229
|
}
|
|
184
230
|
|
|
185
231
|
/** Stored envelope for a PPR shell entry (getShell/putShell). */
|
|
@@ -194,7 +240,7 @@ interface VercelShellEnvelope {
|
|
|
194
240
|
/** React.version at capture. */
|
|
195
241
|
rv: string;
|
|
196
242
|
/** Build version at capture (ShellCacheEntry.buildVersion). */
|
|
197
|
-
bv
|
|
243
|
+
bv: string;
|
|
198
244
|
/** Capture-generation start time, used by tag marker checks. */
|
|
199
245
|
c: number;
|
|
200
246
|
/** staleAt (ms since epoch). */
|
|
@@ -205,8 +251,8 @@ interface VercelShellEnvelope {
|
|
|
205
251
|
t?: string[];
|
|
206
252
|
/** initialTheme the capture render was built with (resume theme fidelity). */
|
|
207
253
|
i?: string;
|
|
208
|
-
/** Capture data snapshot
|
|
209
|
-
sn
|
|
254
|
+
/** Capture data snapshot (ShellCacheEntry.snapshot). */
|
|
255
|
+
sn: ShellSnapshotRecord[];
|
|
210
256
|
/**
|
|
211
257
|
* ShellCacheEntry.docKey. Must round-trip: navigation-replay eligibility
|
|
212
258
|
* requires the exact canonical doc segment record named here — dropping the
|
|
@@ -214,15 +260,8 @@ interface VercelShellEnvelope {
|
|
|
214
260
|
* reports `no-segment-snapshot` after a store round trip.
|
|
215
261
|
*/
|
|
216
262
|
dk?: string;
|
|
217
|
-
/**
|
|
218
|
-
|
|
219
|
-
* handler-free fast path on `!entry.handlerLiveHoles`, so dropping the flag
|
|
220
|
-
* here silently fast-pathed handler-live entries after a store round trip —
|
|
221
|
-
* their holes only a handler re-run can fill.
|
|
222
|
-
*/
|
|
223
|
-
lh?: boolean;
|
|
224
|
-
/** ShellCacheEntry.transitionWhen; conditional transitions must re-run. */
|
|
225
|
-
tw?: true;
|
|
263
|
+
/** ShellCacheEntry.prunedRecords (diagnostic). */
|
|
264
|
+
pr?: string;
|
|
226
265
|
/** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
|
|
227
266
|
no?: true;
|
|
228
267
|
}
|
|
@@ -230,6 +269,7 @@ interface VercelShellEnvelope {
|
|
|
230
269
|
/** Read-path outcome for the debug sink. */
|
|
231
270
|
export type VercelCacheReadOutcome =
|
|
232
271
|
| "miss"
|
|
272
|
+
| "memo-hit"
|
|
233
273
|
| "fresh"
|
|
234
274
|
| "stale-revalidate"
|
|
235
275
|
| "expired"
|
|
@@ -271,10 +311,13 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
271
311
|
cache: VercelRuntimeCache;
|
|
272
312
|
|
|
273
313
|
/**
|
|
274
|
-
* `waitUntil` from `@vercel/functions`.
|
|
275
|
-
*
|
|
276
|
-
* backgrounds the actual writes
|
|
277
|
-
* (
|
|
314
|
+
* `waitUntil` from `@vercel/functions`. Keeps two background reads/writes
|
|
315
|
+
* alive past the response: the stale-read lock write (herd dampening; the
|
|
316
|
+
* router already backgrounds the actual writes) and the PPR shell reads'
|
|
317
|
+
* tag-marker memo refreshes (`memo.markerMaxStaleMs`). When omitted, both
|
|
318
|
+
* run detached (fire-and-forget), and the platform may end a refresh when
|
|
319
|
+
* the response ends; the next read past the stale window then reads the
|
|
320
|
+
* marker itself.
|
|
278
321
|
*/
|
|
279
322
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
280
323
|
|
|
@@ -316,12 +359,76 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
316
359
|
* receives the structured VercelCacheReadDebugEvent.
|
|
317
360
|
*/
|
|
318
361
|
debug?: VercelCacheDebug;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Per-process memos ({@link StoreMemoOptions}), kept per `cache` handle:
|
|
365
|
+
* create the `getCache()` handle once per process, not per request, or the
|
|
366
|
+
* memos never hit. `shellMs` (default 2000) and `shellMaxBytes` (default
|
|
367
|
+
* 16 MiB) size the PPR shell memo; `markerFreshMs` (default 300) and
|
|
368
|
+
* `markerMaxStaleMs` (default 2000) the memo of the tag markers
|
|
369
|
+
* `invalidateTags()` writes, which PPR shell reads use
|
|
370
|
+
* (stale-while-revalidate). The process that invalidates writes the new
|
|
371
|
+
* marker into its memo; another process in the region rejects a memoized
|
|
372
|
+
* shell once its marker memo sees it (up to `markerMaxStaleMs`). The
|
|
373
|
+
* markers are a regional `cache.set` (only `expireTag` is global), so
|
|
374
|
+
* another region serves a shell it memoized before the invalidation until
|
|
375
|
+
* its window passes, where without the memo `expireTag` removes it within
|
|
376
|
+
* about 300 ms; a platform `expireTag` issued outside rango is likewise
|
|
377
|
+
* seen once the window passes. The response of a request that ran
|
|
378
|
+
* `updateTag()` / `revalidateTag()` sets the fresh-reads cookie, and the
|
|
379
|
+
* same user's requests carrying it skip both memos. `{ shellMs: 0 }` turns
|
|
380
|
+
* the shell memo off, `{ markerFreshMs: 0 }` the marker memo.
|
|
381
|
+
*/
|
|
382
|
+
memo?: StoreMemoOptions;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** A memoized fresh shell read (shell-memo.ts). */
|
|
386
|
+
interface VercelShellMemoValue {
|
|
387
|
+
entry: ShellCacheEntry;
|
|
388
|
+
/** The prelude decoded by the first document read that needed it. */
|
|
389
|
+
preludeBytes?: Uint8Array;
|
|
390
|
+
tags?: string[];
|
|
391
|
+
createdAt: number;
|
|
392
|
+
staleAt: number;
|
|
393
|
+
expiresAt: number;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* The per-process memos of one runtime-cache handle: memoized shells,
|
|
398
|
+
* tag-marker values, and shell tag-name hints. Kept per handle because the
|
|
399
|
+
* handle carries the namespace, so stores over different namespaces never
|
|
400
|
+
* share them.
|
|
401
|
+
*/
|
|
402
|
+
interface VercelHandleMemos {
|
|
403
|
+
shells: ShellMemo<VercelShellMemoValue>;
|
|
404
|
+
markers: TagMarkerMemo;
|
|
405
|
+
hints: TagNameHints;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const vercelHandleMemos = new WeakMap<VercelRuntimeCache, VercelHandleMemos>();
|
|
409
|
+
|
|
410
|
+
function handleMemos(cache: VercelRuntimeCache): VercelHandleMemos {
|
|
411
|
+
let memos = vercelHandleMemos.get(cache);
|
|
412
|
+
if (!memos) {
|
|
413
|
+
memos = {
|
|
414
|
+
shells: new ShellMemo<VercelShellMemoValue>(),
|
|
415
|
+
markers: new TagMarkerMemo(),
|
|
416
|
+
hints: new TagNameHints(),
|
|
417
|
+
};
|
|
418
|
+
vercelHandleMemos.set(cache, memos);
|
|
419
|
+
}
|
|
420
|
+
return memos;
|
|
319
421
|
}
|
|
320
422
|
|
|
321
423
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
322
424
|
return typeof value === "object" && value !== null;
|
|
323
425
|
}
|
|
324
426
|
|
|
427
|
+
/** The envelope's `ta` write stamp, on tagged entries only. */
|
|
428
|
+
function tagStamp(tags: string[]): { ta?: number } {
|
|
429
|
+
return tags.length > 0 ? { ta: Date.now() } : {};
|
|
430
|
+
}
|
|
431
|
+
|
|
325
432
|
/**
|
|
326
433
|
* Vercel Runtime Cache-backed segment cache store.
|
|
327
434
|
*
|
|
@@ -355,12 +462,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
355
462
|
* import { getCache, waitUntil } from "@vercel/functions";
|
|
356
463
|
* import { VercelCacheStore } from "@rangojs/router/cache";
|
|
357
464
|
*
|
|
465
|
+
* // One handle per process: getCache() resolves the platform cache on every
|
|
466
|
+
* // call, and the PPR shell and tag-marker memos are kept per handle.
|
|
467
|
+
* // `process.env` (not `import.meta.env`, which needs a VITE_ prefix and
|
|
468
|
+
* // would be undefined here) so cross-deploy busting via `version` works.
|
|
469
|
+
* const runtimeCache = getCache({
|
|
470
|
+
* namespace: process.env.VERCEL_DEPLOYMENT_ID,
|
|
471
|
+
* });
|
|
472
|
+
*
|
|
358
473
|
* export const router = createRouter({
|
|
359
474
|
* cache: () => ({
|
|
360
475
|
* store: new VercelCacheStore({
|
|
361
|
-
*
|
|
362
|
-
* // would be undefined here) so cross-deploy busting via `version` works.
|
|
363
|
-
* cache: getCache({ namespace: process.env.VERCEL_DEPLOYMENT_ID }),
|
|
476
|
+
* cache: runtimeCache,
|
|
364
477
|
* waitUntil,
|
|
365
478
|
* defaults: { ttl: 60, swr: 300 },
|
|
366
479
|
* }),
|
|
@@ -382,6 +495,16 @@ export class VercelCacheStore<
|
|
|
382
495
|
private readonly waitUntil?: (promise: Promise<unknown>) => void;
|
|
383
496
|
private readonly version?: string;
|
|
384
497
|
private readonly maxItemBytes: number;
|
|
498
|
+
private readonly memo: ResolvedShellMemoOptions;
|
|
499
|
+
private readonly handleMemos: VercelHandleMemos;
|
|
500
|
+
/** @internal SegmentCacheStore.freshReadsWindowMs */
|
|
501
|
+
readonly freshReadsWindowMs: number;
|
|
502
|
+
/**
|
|
503
|
+
* @internal SegmentCacheStore.maxShellEntryBytes: the item cap, less the
|
|
504
|
+
* base64 expansion of the prelude the shell envelope stores (the capture
|
|
505
|
+
* measures raw prelude bytes).
|
|
506
|
+
*/
|
|
507
|
+
readonly maxShellEntryBytes: number;
|
|
385
508
|
private readonly name?: string;
|
|
386
509
|
private readonly debug?: VercelCacheDebug;
|
|
387
510
|
|
|
@@ -397,6 +520,13 @@ export class VercelCacheStore<
|
|
|
397
520
|
this.keyGenerator = options.keyGenerator;
|
|
398
521
|
this.version = options.version;
|
|
399
522
|
this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
|
|
523
|
+
this.maxShellEntryBytes = Math.floor((this.maxItemBytes * 3) / 4);
|
|
524
|
+
this.memo = resolveShellMemoOptions(options.memo, {
|
|
525
|
+
markerFreshMs: DEFAULT_VERCEL_MARKER_FRESH_MS,
|
|
526
|
+
markerMaxStaleMs: DEFAULT_VERCEL_MARKER_MAX_STALE_MS,
|
|
527
|
+
});
|
|
528
|
+
this.handleMemos = handleMemos(options.cache);
|
|
529
|
+
this.freshReadsWindowMs = freshReadsWindowMs(this.memo, true);
|
|
400
530
|
this.name = options.name;
|
|
401
531
|
this.debug = options.debug;
|
|
402
532
|
}
|
|
@@ -447,6 +577,9 @@ export class VercelCacheStore<
|
|
|
447
577
|
});
|
|
448
578
|
return null;
|
|
449
579
|
}
|
|
580
|
+
if (this.maskedMiss("get", key, { t: env.d.tags, ta: env.ta }, readMs)) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
450
583
|
|
|
451
584
|
const isStale = env.s > 0 && now > env.s;
|
|
452
585
|
const shouldRevalidate = isStale
|
|
@@ -483,7 +616,12 @@ export class VercelCacheStore<
|
|
|
483
616
|
const d: CachedEntryData = data.tags
|
|
484
617
|
? { ...data, tags: safeTags.length > 0 ? safeTags : undefined }
|
|
485
618
|
: data;
|
|
486
|
-
const env: VercelSegmentEnvelope = {
|
|
619
|
+
const env: VercelSegmentEnvelope = {
|
|
620
|
+
d,
|
|
621
|
+
s: staleAt,
|
|
622
|
+
e: expiresAt,
|
|
623
|
+
...tagStamp(safeTags),
|
|
624
|
+
};
|
|
487
625
|
await this.write(
|
|
488
626
|
this.toStoreKey(key, "s"),
|
|
489
627
|
env,
|
|
@@ -554,6 +692,7 @@ export class VercelCacheStore<
|
|
|
554
692
|
});
|
|
555
693
|
return null;
|
|
556
694
|
}
|
|
695
|
+
if (this.maskedMiss("getResponse", key, env, readMs)) return null;
|
|
557
696
|
|
|
558
697
|
// Reconstruct the Response BEFORE claiming revalidation. A corrupt body
|
|
559
698
|
// (e.g. invalid base64 from base64ToBuffer, or bad header entries) would
|
|
@@ -629,6 +768,7 @@ export class VercelCacheStore<
|
|
|
629
768
|
s: staleAt,
|
|
630
769
|
e: expiresAt,
|
|
631
770
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
771
|
+
...tagStamp(safeTags),
|
|
632
772
|
};
|
|
633
773
|
await this.write(
|
|
634
774
|
this.toStoreKey(key, "r"),
|
|
@@ -686,6 +826,7 @@ export class VercelCacheStore<
|
|
|
686
826
|
});
|
|
687
827
|
return null;
|
|
688
828
|
}
|
|
829
|
+
if (this.maskedMiss("getItem", key, env, readMs)) return null;
|
|
689
830
|
|
|
690
831
|
const isStale = env.s > 0 && now > env.s;
|
|
691
832
|
const shouldRevalidate = isStale
|
|
@@ -733,6 +874,7 @@ export class VercelCacheStore<
|
|
|
733
874
|
s: staleAt,
|
|
734
875
|
e: expiresAt,
|
|
735
876
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
877
|
+
...tagStamp(safeTags),
|
|
736
878
|
};
|
|
737
879
|
await this.write(
|
|
738
880
|
this.toStoreKey(key, "i"),
|
|
@@ -752,8 +894,140 @@ export class VercelCacheStore<
|
|
|
752
894
|
key: string,
|
|
753
895
|
options?: { claimRevalidation?: boolean },
|
|
754
896
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
897
|
+
const read = await this.readShell(key, options, undefined);
|
|
898
|
+
return read
|
|
899
|
+
? { entry: read.entry, shouldRevalidate: read.shouldRevalidate }
|
|
900
|
+
: null;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* @internal The document serve path's read (SegmentCacheStore
|
|
905
|
+
* .readShellDocument): the same read as getShell, with the prelude decoded
|
|
906
|
+
* here, once per memoized shell rather than once per HIT, and the read's
|
|
907
|
+
* `debugPerformance` stats. The runtime cache returns a parsed value, so
|
|
908
|
+
* there is no prelude-first read and the tag-marker read follows the entry
|
|
909
|
+
* read (`markerSerial`).
|
|
910
|
+
*/
|
|
911
|
+
async readShellDocument(
|
|
912
|
+
key: string,
|
|
913
|
+
options?: ShellDocumentReadOptions,
|
|
914
|
+
): Promise<ShellDocumentRead | null> {
|
|
915
|
+
const stats: ShellReadStats | undefined = _getRequestContext()
|
|
916
|
+
?._metricsStore
|
|
917
|
+
? { tier: "store" }
|
|
918
|
+
: undefined;
|
|
919
|
+
const read = await this.readShell(key, undefined, stats, options?.tagHints);
|
|
920
|
+
if (!read) return null;
|
|
921
|
+
const { entry, memoized } = read;
|
|
922
|
+
let prelude: Uint8Array;
|
|
923
|
+
if (entry.prelude === undefined) {
|
|
924
|
+
prelude = new Uint8Array(0);
|
|
925
|
+
} else if (memoized?.preludeBytes) {
|
|
926
|
+
prelude = memoized.preludeBytes;
|
|
927
|
+
} else {
|
|
928
|
+
try {
|
|
929
|
+
prelude = base64ToBytes(entry.prelude);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
const storeKey = this.toStoreKey(key, "h");
|
|
932
|
+
reportCacheError(
|
|
933
|
+
error,
|
|
934
|
+
"cache-corrupt",
|
|
935
|
+
"[VercelCacheStore] getShell: undecodable prelude, evicting",
|
|
936
|
+
);
|
|
937
|
+
this.handleMemos.shells.delete(storeKey);
|
|
938
|
+
void this.safeDelete(storeKey);
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
if (memoized) memoized.preludeBytes = prelude;
|
|
942
|
+
}
|
|
943
|
+
if (stats) stats.preludeBytes = prelude.length;
|
|
944
|
+
const { prelude: _prelude, snapshot, ...rest } = entry;
|
|
945
|
+
return {
|
|
946
|
+
entry: rest,
|
|
947
|
+
prelude,
|
|
948
|
+
shouldRevalidate: read.shouldRevalidate,
|
|
949
|
+
snapshot: Promise.resolve(snapshot),
|
|
950
|
+
...(stats && { stats }),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** @internal SegmentCacheStore.dropShellMemo */
|
|
955
|
+
dropShellMemo(key: string): void {
|
|
956
|
+
this.handleMemos.shells.delete(this.toStoreKey(key, "h"));
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* getShell's read, shared with readShellDocument: the shell memo first
|
|
961
|
+
* (shell-memo.ts), then the runtime cache. A memoized shell still runs the
|
|
962
|
+
* tag-marker check; `memoized` is the memo slot's value when the read came
|
|
963
|
+
* from, or went into, the memo (readShellDocument keeps the decoded prelude
|
|
964
|
+
* there).
|
|
965
|
+
*/
|
|
966
|
+
private async readShell(
|
|
967
|
+
key: string,
|
|
968
|
+
options: { claimRevalidation?: boolean } | undefined,
|
|
969
|
+
stats: ShellReadStats | undefined,
|
|
970
|
+
routeTags?: readonly string[],
|
|
971
|
+
): Promise<{
|
|
972
|
+
entry: ShellCacheEntry;
|
|
973
|
+
shouldRevalidate: boolean;
|
|
974
|
+
memoized?: VercelShellMemoValue;
|
|
975
|
+
} | null> {
|
|
755
976
|
const storeKey = this.toStoreKey(key, "h");
|
|
977
|
+
const memo = this.handleMemos.shells;
|
|
978
|
+
// A request after the same user's updateTag() reads past the memos.
|
|
979
|
+
const freshReads = freshReadsRequired(_getRequestContext());
|
|
980
|
+
if (stats && freshReads) stats.freshReads = true;
|
|
981
|
+
const outcomes = stats ? new Map<string, MarkerMemoOutcome>() : undefined;
|
|
982
|
+
const prefetched = this.prefetchShellMarkers(
|
|
983
|
+
storeKey,
|
|
984
|
+
routeTags,
|
|
985
|
+
stats,
|
|
986
|
+
outcomes,
|
|
987
|
+
);
|
|
988
|
+
const memoized = freshReads
|
|
989
|
+
? undefined
|
|
990
|
+
: memo.get(storeKey, this.memo.shellMs);
|
|
991
|
+
if (memoized) {
|
|
992
|
+
if (isShellFresh(memoized.staleAt, memoized.expiresAt)) {
|
|
993
|
+
if (stats) stats.tier = "memo";
|
|
994
|
+
// The marker check is what decides whether a memoized shell may be
|
|
995
|
+
// served; it runs on every read, memoized or not.
|
|
996
|
+
if (
|
|
997
|
+
await this.shellTagsInvalidated(
|
|
998
|
+
memoized.tags,
|
|
999
|
+
memoized.createdAt,
|
|
1000
|
+
stats,
|
|
1001
|
+
prefetched,
|
|
1002
|
+
outcomes,
|
|
1003
|
+
)
|
|
1004
|
+
) {
|
|
1005
|
+
memo.delete(storeKey);
|
|
1006
|
+
void this.safeDelete(storeKey);
|
|
1007
|
+
this.emitDebug({ op: "getShell", key, outcome: "miss" });
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
if (stats) stats.memo = { hit: true, bytes: memo.size };
|
|
1011
|
+
this.emitDebug({
|
|
1012
|
+
op: "getShell",
|
|
1013
|
+
key,
|
|
1014
|
+
outcome: "memo-hit",
|
|
1015
|
+
staleAt: memoized.staleAt,
|
|
1016
|
+
expiresAt: memoized.expiresAt,
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
entry: memoized.entry,
|
|
1020
|
+
shouldRevalidate: false,
|
|
1021
|
+
memoized,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
memo.delete(storeKey);
|
|
1025
|
+
}
|
|
1026
|
+
if (stats && this.memo.shellMs > 0) {
|
|
1027
|
+
stats.memo = { hit: false, bytes: memo.size };
|
|
1028
|
+
}
|
|
756
1029
|
const started = Date.now();
|
|
1030
|
+
const readStartedAt = stats ? performance.now() : 0;
|
|
757
1031
|
let raw: unknown;
|
|
758
1032
|
try {
|
|
759
1033
|
raw = await this.cache.get(storeKey);
|
|
@@ -769,6 +1043,13 @@ export class VercelCacheStore<
|
|
|
769
1043
|
return null;
|
|
770
1044
|
}
|
|
771
1045
|
const env = this.asShellEnvelope(this.decodeRaw(raw));
|
|
1046
|
+
if (stats) {
|
|
1047
|
+
const readAt = performance.now();
|
|
1048
|
+
stats.matchMs = readAt - readStartedAt;
|
|
1049
|
+
if (stats.markerHintStartedAt !== undefined) {
|
|
1050
|
+
stats.markerLeadMs = readAt - stats.markerHintStartedAt;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
772
1053
|
if (!env) {
|
|
773
1054
|
reportCacheError(
|
|
774
1055
|
new Error("malformed shell envelope"),
|
|
@@ -794,10 +1075,9 @@ export class VercelCacheStore<
|
|
|
794
1075
|
return null;
|
|
795
1076
|
}
|
|
796
1077
|
|
|
1078
|
+
this.handleMemos.hints.remember(storeKey, env.t);
|
|
797
1079
|
if (
|
|
798
|
-
env.t
|
|
799
|
-
env.t.length > 0 &&
|
|
800
|
-
(await this.isTagsInvalidatedSince(env.t, env.c))
|
|
1080
|
+
await this.shellTagsInvalidated(env.t, env.c, stats, prefetched, outcomes)
|
|
801
1081
|
) {
|
|
802
1082
|
void this.safeDelete(storeKey);
|
|
803
1083
|
this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
|
|
@@ -806,7 +1086,14 @@ export class VercelCacheStore<
|
|
|
806
1086
|
|
|
807
1087
|
const isStale = env.s > 0 && now > env.s;
|
|
808
1088
|
let shouldRevalidate = isStale;
|
|
809
|
-
|
|
1089
|
+
// Inside the recapture floor the serve path schedules nothing, so the
|
|
1090
|
+
// lock is left for the first stale read past it: claimed here, it held
|
|
1091
|
+
// the shell REVALIDATION_LOCK_MS without a recapture.
|
|
1092
|
+
if (
|
|
1093
|
+
isStale &&
|
|
1094
|
+
options?.claimRevalidation !== false &&
|
|
1095
|
+
staleShellRecaptureDue({ createdAt: env.c }, now)
|
|
1096
|
+
) {
|
|
810
1097
|
shouldRevalidate = await this.claimRevalidation(
|
|
811
1098
|
storeKey,
|
|
812
1099
|
env.e,
|
|
@@ -822,22 +1109,132 @@ export class VercelCacheStore<
|
|
|
822
1109
|
expiresAt: env.e,
|
|
823
1110
|
readMs,
|
|
824
1111
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
navigationOnly: env.no,
|
|
837
|
-
createdAt: env.c,
|
|
838
|
-
},
|
|
839
|
-
shouldRevalidate,
|
|
1112
|
+
const entry: ShellCacheEntry = {
|
|
1113
|
+
prelude: env.p,
|
|
1114
|
+
postponed: env.po,
|
|
1115
|
+
reactVersion: env.rv,
|
|
1116
|
+
buildVersion: env.bv,
|
|
1117
|
+
initialTheme: env.i,
|
|
1118
|
+
snapshot: env.sn,
|
|
1119
|
+
docKey: env.dk,
|
|
1120
|
+
prunedRecords: env.pr,
|
|
1121
|
+
navigationOnly: env.no,
|
|
1122
|
+
createdAt: env.c,
|
|
840
1123
|
};
|
|
1124
|
+
if (isStale) return { entry, shouldRevalidate };
|
|
1125
|
+
const value: VercelShellMemoValue = {
|
|
1126
|
+
entry,
|
|
1127
|
+
tags: env.t,
|
|
1128
|
+
createdAt: env.c,
|
|
1129
|
+
staleAt: env.s,
|
|
1130
|
+
expiresAt: env.e,
|
|
1131
|
+
};
|
|
1132
|
+
// Counted: the envelope, plus the decoded prelude readShellDocument
|
|
1133
|
+
// attaches to the slot on first use.
|
|
1134
|
+
memo.set(
|
|
1135
|
+
storeKey,
|
|
1136
|
+
value,
|
|
1137
|
+
(typeof raw === "string" ? raw.length : JSON.stringify(env).length) +
|
|
1138
|
+
(env.p === undefined ? 0 : base64ByteLength(env.p)),
|
|
1139
|
+
this.memo.shellMs,
|
|
1140
|
+
this.memo.shellMaxBytes,
|
|
1141
|
+
);
|
|
1142
|
+
return { entry, shouldRevalidate, memoized: value };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* The shell's tag-marker check, timed into `stats`. Markers a prefetch
|
|
1147
|
+
* already started (hinted tags) are awaited, not re-read; the check itself
|
|
1148
|
+
* always uses the entry's own tags.
|
|
1149
|
+
*/
|
|
1150
|
+
private async shellTagsInvalidated(
|
|
1151
|
+
tags: string[] | undefined,
|
|
1152
|
+
createdAt: number,
|
|
1153
|
+
stats: ShellReadStats | undefined,
|
|
1154
|
+
prefetched: Map<string, Promise<number | null>> | undefined,
|
|
1155
|
+
outcomes: Map<string, MarkerMemoOutcome> | undefined,
|
|
1156
|
+
): Promise<boolean> {
|
|
1157
|
+
if (stats) {
|
|
1158
|
+
stats.tags = tags?.length ?? 0;
|
|
1159
|
+
stats.markerSerial = true;
|
|
1160
|
+
}
|
|
1161
|
+
if (!tags || tags.length === 0) {
|
|
1162
|
+
if (stats) stats.markerMs = stats.markerWaitMs = 0;
|
|
1163
|
+
return false;
|
|
1164
|
+
}
|
|
1165
|
+
const startedAt = stats ? performance.now() : 0;
|
|
1166
|
+
const invalidated = await this.tagsInvalidatedSince(
|
|
1167
|
+
tags,
|
|
1168
|
+
createdAt,
|
|
1169
|
+
true,
|
|
1170
|
+
prefetched,
|
|
1171
|
+
outcomes,
|
|
1172
|
+
);
|
|
1173
|
+
if (stats) {
|
|
1174
|
+
stats.markerMs = stats.markerWaitMs = performance.now() - startedAt;
|
|
1175
|
+
recordMarkerRow(stats, tags, outcomes);
|
|
1176
|
+
}
|
|
1177
|
+
return invalidated;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/**
|
|
1181
|
+
* Start the tm-marker reads of the tags this shell key carried last time
|
|
1182
|
+
* (and the route's static tags) alongside the entry read.
|
|
1183
|
+
*/
|
|
1184
|
+
private prefetchShellMarkers(
|
|
1185
|
+
storeKey: string,
|
|
1186
|
+
routeTags: readonly string[] | undefined,
|
|
1187
|
+
stats: ShellReadStats | undefined,
|
|
1188
|
+
outcomes: Map<string, MarkerMemoOutcome> | undefined,
|
|
1189
|
+
): Map<string, Promise<number | null>> | undefined {
|
|
1190
|
+
const tags = hintedTags(this.handleMemos.hints.get(storeKey), routeTags);
|
|
1191
|
+
if (tags.length === 0) return undefined;
|
|
1192
|
+
if (stats) {
|
|
1193
|
+
stats.markerHinted = tags;
|
|
1194
|
+
stats.markerHintStartedAt = performance.now();
|
|
1195
|
+
}
|
|
1196
|
+
const prefetched = new Map<string, Promise<number | null>>();
|
|
1197
|
+
for (const tag of tags) {
|
|
1198
|
+
const read = this.readTagMarker(tag, true, outcomes);
|
|
1199
|
+
read.catch(() => {});
|
|
1200
|
+
prefetched.set(tag, read);
|
|
1201
|
+
}
|
|
1202
|
+
return prefetched;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
/**
|
|
1206
|
+
* A tag's latest invalidatedAt (or null). With `isolateMemo` (PPR shell
|
|
1207
|
+
* reads only) through the per-process marker memo (isolate-tag-memo.ts):
|
|
1208
|
+
* fresh values are used as is, stale ones are used and refreshed in the
|
|
1209
|
+
* background, older ones block on the runtime-cache read. A request
|
|
1210
|
+
* carrying the fresh-reads cookie skips the memo. A failed read rejects and
|
|
1211
|
+
* is never memoized. The shell write gate, build shells and the data
|
|
1212
|
+
* write gates (isTagsInvalidatedSince) read the runtime cache each time
|
|
1213
|
+
* they ask; gates of a request asking while a read is in flight share it
|
|
1214
|
+
* (gateMarkerRead), and nothing is kept once it settles.
|
|
1215
|
+
*/
|
|
1216
|
+
private async readTagMarker(
|
|
1217
|
+
tag: string,
|
|
1218
|
+
isolateMemo: boolean,
|
|
1219
|
+
outcomes?: Map<string, MarkerMemoOutcome>,
|
|
1220
|
+
): Promise<number | null> {
|
|
1221
|
+
const key = this.toStoreKey(tag, "tm");
|
|
1222
|
+
const read = async (): Promise<MarkerFetch> => {
|
|
1223
|
+
const raw = await this.cache.get(key);
|
|
1224
|
+
const decoded =
|
|
1225
|
+
raw == null ? null : (this.decodeRaw(raw) as { at?: unknown } | null);
|
|
1226
|
+
const value =
|
|
1227
|
+
decoded && typeof decoded.at === "number" ? decoded.at : null;
|
|
1228
|
+
return { value, memoize: true };
|
|
1229
|
+
};
|
|
1230
|
+
if (!isolateMemo) return (await read()).value;
|
|
1231
|
+
return this.handleMemos.markers.readThrough(key, read, {
|
|
1232
|
+
freshMs: this.memo.markerFreshMs,
|
|
1233
|
+
maxStaleMs: this.memo.markerMaxStaleMs,
|
|
1234
|
+
bypass: freshReadsRequired(_getRequestContext()),
|
|
1235
|
+
keepAlive: this.waitUntil,
|
|
1236
|
+
onOutcome: outcomes ? (outcome) => outcomes.set(tag, outcome) : undefined,
|
|
1237
|
+
});
|
|
841
1238
|
}
|
|
842
1239
|
|
|
843
1240
|
async putShell(
|
|
@@ -847,6 +1244,9 @@ export class VercelCacheStore<
|
|
|
847
1244
|
swrSeconds?: number,
|
|
848
1245
|
tags?: string[],
|
|
849
1246
|
): Promise<"stored" | "invalidated" | void> {
|
|
1247
|
+
// This instance serves its own new capture from the next read on.
|
|
1248
|
+
this.handleMemos.shells.delete(this.toStoreKey(key, "h"));
|
|
1249
|
+
this.handleMemos.hints.remember(this.toStoreKey(key, "h"), tags);
|
|
850
1250
|
try {
|
|
851
1251
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
852
1252
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
@@ -885,8 +1285,7 @@ export class VercelCacheStore<
|
|
|
885
1285
|
i: entry.initialTheme,
|
|
886
1286
|
sn: entry.snapshot,
|
|
887
1287
|
dk: entry.docKey,
|
|
888
|
-
|
|
889
|
-
tw: entry.transitionWhen,
|
|
1288
|
+
pr: entry.prunedRecords,
|
|
890
1289
|
no: entry.navigationOnly,
|
|
891
1290
|
};
|
|
892
1291
|
// write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
|
|
@@ -908,24 +1307,62 @@ export class VercelCacheStore<
|
|
|
908
1307
|
// --- Tags ---
|
|
909
1308
|
|
|
910
1309
|
/**
|
|
911
|
-
*
|
|
912
|
-
*
|
|
913
|
-
*
|
|
914
|
-
*
|
|
1310
|
+
* SegmentCacheStore.isTagsInvalidatedSince. expireTag() keeps no queryable
|
|
1311
|
+
* history, so invalidateTags() writes "tm" markers for runtime capture
|
|
1312
|
+
* races, immutable build shells and data writes that started before an
|
|
1313
|
+
* invalidation (#977, tag-invalidation.ts predatesInvalidation). Marker >=
|
|
1314
|
+
* generation start wins. Read errors fail open like the CF marker check,
|
|
1315
|
+
* except for the write gate (`failClosed`), which counts them as an
|
|
1316
|
+
* invalidation. Each tag's marker is read through gateMarkerRead
|
|
1317
|
+
* (request-tag-mask.ts): gates asking while a read is in flight share it.
|
|
915
1318
|
*/
|
|
916
1319
|
async isTagsInvalidatedSince(
|
|
917
1320
|
tags: string[],
|
|
918
1321
|
sinceMs: number,
|
|
1322
|
+
options?: { failClosed?: boolean },
|
|
919
1323
|
): Promise<boolean> {
|
|
1324
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
1325
|
+
const ctx = _getRequestContext();
|
|
1326
|
+
const reads = new Map(
|
|
1327
|
+
tags.map((tag) => [
|
|
1328
|
+
tag,
|
|
1329
|
+
gateMarkerRead(ctx, this, tag, sinceMs, () =>
|
|
1330
|
+
this.readTagMarker(tag, false),
|
|
1331
|
+
),
|
|
1332
|
+
]),
|
|
1333
|
+
);
|
|
1334
|
+
return this.tagsInvalidatedSince(
|
|
1335
|
+
tags,
|
|
1336
|
+
sinceMs,
|
|
1337
|
+
false,
|
|
1338
|
+
reads,
|
|
1339
|
+
undefined,
|
|
1340
|
+
options?.failClosed === true,
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
private async tagsInvalidatedSince(
|
|
1345
|
+
tags: string[],
|
|
1346
|
+
sinceMs: number,
|
|
1347
|
+
isolateMemo: boolean,
|
|
1348
|
+
prefetched?: Map<string, Promise<number | null>>,
|
|
1349
|
+
outcomes?: Map<string, MarkerMemoOutcome>,
|
|
1350
|
+
/** What a failed marker read answers: true for the write gate (#977). */
|
|
1351
|
+
failClosed = false,
|
|
1352
|
+
): Promise<boolean> {
|
|
1353
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
920
1354
|
try {
|
|
921
1355
|
const markers = await Promise.all(
|
|
922
|
-
tags.map(
|
|
1356
|
+
tags.map(
|
|
1357
|
+
(tag) =>
|
|
1358
|
+
prefetched?.get(tag) ??
|
|
1359
|
+
this.readTagMarker(tag, isolateMemo, outcomes),
|
|
1360
|
+
),
|
|
923
1361
|
);
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
decoded && typeof decoded.at === "number" ? decoded.at : null;
|
|
1362
|
+
// A read in flight when this request invalidated one of the tags
|
|
1363
|
+
// resolved to the marker before it.
|
|
1364
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
1365
|
+
for (const at of markers) {
|
|
929
1366
|
if (at !== null && at >= sinceMs) return true;
|
|
930
1367
|
}
|
|
931
1368
|
return false;
|
|
@@ -935,12 +1372,18 @@ export class VercelCacheStore<
|
|
|
935
1372
|
"cache-read",
|
|
936
1373
|
"[VercelCacheStore] tag invalidation check",
|
|
937
1374
|
);
|
|
938
|
-
return
|
|
1375
|
+
return failClosed;
|
|
939
1376
|
}
|
|
940
1377
|
}
|
|
941
1378
|
|
|
942
1379
|
async invalidateTags(tags: string[]): Promise<void> {
|
|
943
1380
|
if (!tags || tags.length === 0) return;
|
|
1381
|
+
// Every memo hit reads the tag markers, so a read in flight that memoizes
|
|
1382
|
+
// one of these shells again is rejected on its next hit once the markers
|
|
1383
|
+
// below land (no in-flight record needed, unlike KV-less CFCacheStore).
|
|
1384
|
+
this.handleMemos.shells.deleteWhere((shell) =>
|
|
1385
|
+
shellHasAnyTag(shell.tags, tags),
|
|
1386
|
+
);
|
|
944
1387
|
// No per-item cap here: an invalidation must reach every requested tag.
|
|
945
1388
|
const safe = this.validateTags(
|
|
946
1389
|
tags,
|
|
@@ -948,6 +1391,8 @@ export class VercelCacheStore<
|
|
|
948
1391
|
"cache-invalidate",
|
|
949
1392
|
);
|
|
950
1393
|
if (safe.length === 0) return;
|
|
1394
|
+
const at = Date.now();
|
|
1395
|
+
this.maskTagsForRequest(safe, at);
|
|
951
1396
|
// Marker writes FIRST, and strict: isTagsInvalidatedSince() is how an
|
|
952
1397
|
// updateTag() reaches a build-time shell entry (expireTag cannot delete
|
|
953
1398
|
// what lives in the build manifest), so a failed marker write must reject
|
|
@@ -955,13 +1400,14 @@ export class VercelCacheStore<
|
|
|
955
1400
|
// the baked shell keeps serving.
|
|
956
1401
|
await Promise.all(
|
|
957
1402
|
safe.map((tag) =>
|
|
958
|
-
this.cache.set(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
{ ttl: TAG_MARKER_TTL_SECONDS },
|
|
962
|
-
),
|
|
1403
|
+
this.cache.set(this.toStoreKey(tag, "tm"), JSON.stringify({ at }), {
|
|
1404
|
+
ttl: TAG_MARKER_TTL_SECONDS,
|
|
1405
|
+
}),
|
|
963
1406
|
),
|
|
964
1407
|
);
|
|
1408
|
+
// Same process: later reads here see the invalidation at once.
|
|
1409
|
+
const { markers } = this.handleMemos;
|
|
1410
|
+
for (const tag of safe) markers.store(this.toStoreKey(tag, "tm"), at);
|
|
965
1411
|
try {
|
|
966
1412
|
await this.cache.expireTag(safe);
|
|
967
1413
|
} catch (error) {
|
|
@@ -980,6 +1426,40 @@ export class VercelCacheStore<
|
|
|
980
1426
|
}
|
|
981
1427
|
}
|
|
982
1428
|
|
|
1429
|
+
/**
|
|
1430
|
+
* The request-local half of invalidateTags(), run before its first await
|
|
1431
|
+
* (#973): the request masks the tags (request-tag-mask.ts). The platform's
|
|
1432
|
+
* expireTag() and the tm marker writes are what other requests see, and
|
|
1433
|
+
* revalidateTag() does not wait for them.
|
|
1434
|
+
*/
|
|
1435
|
+
private maskTagsForRequest(tags: string[], at: number): void {
|
|
1436
|
+
const ctx = _getRequestContext();
|
|
1437
|
+
if (ctx) maskRequestTags(ctx, this, tags, at);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
/**
|
|
1441
|
+
* Whether this request masked one of `tags` through this store at or after
|
|
1442
|
+
* `taggedAt`. An entry without a stamp counts as older than any mask.
|
|
1443
|
+
*/
|
|
1444
|
+
private maskedForRequest(
|
|
1445
|
+
tags: string[] | undefined,
|
|
1446
|
+
taggedAt: number | undefined,
|
|
1447
|
+
): boolean {
|
|
1448
|
+
return maskedForRequest(_getRequestContext(), this, tags, taggedAt ?? 0);
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
/** A data read's mask check: reports the miss and returns true when masked. */
|
|
1452
|
+
private maskedMiss(
|
|
1453
|
+
op: "get" | "getItem" | "getResponse",
|
|
1454
|
+
key: string,
|
|
1455
|
+
env: { t?: string[]; ta?: number },
|
|
1456
|
+
readMs: number,
|
|
1457
|
+
): boolean {
|
|
1458
|
+
if (!this.maskedForRequest(env.t, env.ta)) return false;
|
|
1459
|
+
this.emitDebug({ op, key, outcome: "miss", readMs });
|
|
1460
|
+
return true;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
983
1463
|
// --- Internals ---
|
|
984
1464
|
|
|
985
1465
|
// The `rg:{family}:` prefix keeps the three value tiers in disjoint
|
|
@@ -1188,7 +1668,7 @@ export class VercelCacheStore<
|
|
|
1188
1668
|
|
|
1189
1669
|
private asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
|
|
1190
1670
|
if (!isRecord(raw)) return null;
|
|
1191
|
-
const { d, s, e } = raw;
|
|
1671
|
+
const { d, s, e, ta } = raw;
|
|
1192
1672
|
if (
|
|
1193
1673
|
!isRecord(d) ||
|
|
1194
1674
|
!Array.isArray((d as Record<string, unknown>).segments)
|
|
@@ -1196,12 +1676,17 @@ export class VercelCacheStore<
|
|
|
1196
1676
|
return null;
|
|
1197
1677
|
}
|
|
1198
1678
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1199
|
-
return {
|
|
1679
|
+
return {
|
|
1680
|
+
d: d as unknown as CachedEntryData,
|
|
1681
|
+
s,
|
|
1682
|
+
e,
|
|
1683
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1684
|
+
};
|
|
1200
1685
|
}
|
|
1201
1686
|
|
|
1202
1687
|
private asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
|
|
1203
1688
|
if (!isRecord(raw)) return null;
|
|
1204
|
-
const { v, h, s, e, t } = raw;
|
|
1689
|
+
const { v, h, s, e, t, ta } = raw;
|
|
1205
1690
|
if (typeof v !== "string") return null;
|
|
1206
1691
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1207
1692
|
return {
|
|
@@ -1209,14 +1694,15 @@ export class VercelCacheStore<
|
|
|
1209
1694
|
h: typeof h === "string" ? h : undefined,
|
|
1210
1695
|
s,
|
|
1211
1696
|
e,
|
|
1697
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1212
1698
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1213
1699
|
};
|
|
1214
1700
|
}
|
|
1215
1701
|
|
|
1216
1702
|
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1217
1703
|
if (!isRecord(raw)) return null;
|
|
1218
|
-
const { p, po, rv, bv, c, s, e, t, i, sn, dk,
|
|
1219
|
-
if (typeof rv !== "string") return null;
|
|
1704
|
+
const { p, po, rv, bv, c, s, e, t, i, sn, dk, pr, no } = raw;
|
|
1705
|
+
if (typeof rv !== "string" || typeof bv !== "string") return null;
|
|
1220
1706
|
// Document half: required unless navigationOnly (`no`), which stores
|
|
1221
1707
|
// neither field. Tolerate a legacy navigationOnly envelope that still
|
|
1222
1708
|
// carries them.
|
|
@@ -1232,23 +1718,23 @@ export class VercelCacheStore<
|
|
|
1232
1718
|
...(typeof p === "string" ? { p } : {}),
|
|
1233
1719
|
...(po !== undefined ? { po: po as string | null } : {}),
|
|
1234
1720
|
rv,
|
|
1235
|
-
bv
|
|
1721
|
+
bv,
|
|
1236
1722
|
c,
|
|
1237
1723
|
s,
|
|
1238
1724
|
e,
|
|
1239
1725
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1240
1726
|
i: typeof i === "string" ? i : undefined,
|
|
1241
|
-
|
|
1727
|
+
// A non-array field reads as no records.
|
|
1728
|
+
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : [],
|
|
1242
1729
|
dk: typeof dk === "string" ? dk : undefined,
|
|
1243
|
-
|
|
1244
|
-
tw: tw === true ? true : undefined,
|
|
1730
|
+
pr: typeof pr === "string" ? pr : undefined,
|
|
1245
1731
|
no: no === true ? true : undefined,
|
|
1246
1732
|
};
|
|
1247
1733
|
}
|
|
1248
1734
|
|
|
1249
1735
|
private asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
|
|
1250
1736
|
if (!isRecord(raw)) return null;
|
|
1251
|
-
const { b, st, hd, s, e, t } = raw;
|
|
1737
|
+
const { b, st, hd, s, e, t, ta } = raw;
|
|
1252
1738
|
if (typeof b !== "string" || typeof st !== "number") return null;
|
|
1253
1739
|
if (!Array.isArray(hd)) return null;
|
|
1254
1740
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
@@ -1259,6 +1745,7 @@ export class VercelCacheStore<
|
|
|
1259
1745
|
s,
|
|
1260
1746
|
e,
|
|
1261
1747
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1748
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1262
1749
|
};
|
|
1263
1750
|
}
|
|
1264
1751
|
}
|