@rangojs/router 0.17.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 +3 -2
- package/dist/types/browser/cookie-name.d.ts +6 -0
- package/dist/types/browser/history-state.d.ts +16 -0
- 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 +98 -14
- 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-snapshot.d.ts +4 -3
- package/dist/types/cache/index.d.ts +1 -0
- package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
- 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/types.d.ts +207 -43
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
- package/dist/types/prerender/build-shell-capture.d.ts +11 -4
- package/dist/types/router/instrument.d.ts +8 -0
- package/dist/types/router/middleware-types.d.ts +6 -1
- package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
- package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
- package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
- package/dist/types/rsc/full-payload.d.ts +14 -0
- package/dist/types/rsc/handler.d.ts +1 -1
- package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
- package/dist/types/rsc/shell-capture.d.ts +146 -15
- package/dist/types/rsc/shell-serve.d.ts +140 -17
- package/dist/types/rsc/types.d.ts +7 -0
- package/dist/types/server/context.d.ts +109 -15
- package/dist/types/server/cookie-store.d.ts +21 -0
- package/dist/types/server/request-context.d.ts +159 -33
- package/dist/types/ssr/index.d.ts +9 -0
- package/dist/types/ssr/ssr-root.d.ts +9 -8
- package/dist/types/testing/flight.entry.d.ts +7 -0
- 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/types/handler-context.d.ts +14 -1
- package/dist/types/urls/pattern-types.d.ts +22 -18
- package/dist/types/vite/router-discovery.d.ts +29 -2
- package/dist/vite/index.js +46 -9
- package/package.json +1 -1
- package/skills/breadcrumbs/SKILL.md +17 -7
- package/skills/cache-guide/SKILL.md +123 -51
- package/skills/caching/SKILL.md +253 -23
- package/skills/catalog.json +1 -1
- package/skills/client-urls/SKILL.md +11 -6
- package/skills/cloudflare/SKILL.md +15 -0
- package/skills/comparison/references/framework-comparison.md +44 -32
- package/skills/defer-hydration/SKILL.md +50 -30
- package/skills/deployment-caching/SKILL.md +5 -2
- package/skills/document-cache/SKILL.md +8 -2
- package/skills/handler-use/SKILL.md +2 -0
- package/skills/hooks/handle-and-actions.md +3 -1
- package/skills/hooks/state.md +3 -1
- package/skills/loader/SKILL.md +161 -39
- package/skills/middleware/SKILL.md +16 -9
- package/skills/migrate-nextjs/SKILL.md +67 -36
- 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 +64 -0
- package/skills/parallel/SKILL.md +13 -8
- package/skills/ppr/SKILL.md +655 -363
- package/skills/prerender/SKILL.md +27 -14
- package/skills/rango/SKILL.md +52 -33
- package/skills/route/SKILL.md +12 -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 +25 -21
- package/skills/testing/cache-prerender.md +15 -14
- package/skills/testing/loader.md +18 -1
- package/skills/testing/serve-shell-request.md +188 -0
- package/skills/theme/SKILL.md +52 -4
- package/skills/typesafety/env-and-bindings.md +10 -2
- package/skills/use-cache/SKILL.md +39 -16
- package/skills/vercel/SKILL.md +14 -2
- package/skills/view-transitions/SKILL.md +1 -1
- package/src/browser/cookie-name.ts +23 -0
- package/src/browser/history-state.ts +23 -0
- package/src/browser/rsc-router.tsx +5 -0
- package/src/cache/cache-key-utils.ts +22 -2
- package/src/cache/cache-policy.ts +22 -0
- package/src/cache/cache-runtime.ts +34 -7
- package/src/cache/cache-scope.ts +345 -33
- 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/handle-snapshot.ts +4 -3
- package/src/cache/index.ts +2 -0
- package/src/cache/isolate-tag-memo.ts +308 -0
- package/src/cache/shell-memo.ts +341 -0
- package/src/cache/shell-snapshot.ts +217 -27
- package/src/cache/tag-invalidation.ts +104 -19
- package/src/cache/types.ts +211 -43
- package/src/cache/vercel/vercel-cache-store.ts +545 -61
- package/src/prerender/build-shell-capture.ts +65 -11
- package/src/route-definition/dsl-helpers.ts +4 -3
- package/src/router/handler-context.ts +38 -4
- package/src/router/instrument.ts +54 -2
- package/src/router/intercept-resolution.ts +19 -7
- package/src/router/loader-resolution.ts +63 -32
- package/src/router/match-api.ts +28 -9
- package/src/router/match-handlers.ts +5 -0
- package/src/router/match-middleware/cache-lookup.ts +17 -0
- package/src/router/match-middleware/cache-store.ts +31 -11
- package/src/router/middleware-types.ts +6 -1
- package/src/router/middleware.ts +23 -4
- package/src/router/prerender-match.ts +6 -0
- package/src/router/segment-resolution/fresh.ts +18 -9
- package/src/router/segment-resolution/helpers.ts +4 -2
- package/src/router/segment-resolution/loader-cache.ts +227 -56
- package/src/router/segment-resolution/loader-snapshot.ts +203 -51
- package/src/router/segment-resolution/mask-nested.ts +136 -42
- package/src/router/segment-resolution/revalidation.ts +27 -10
- 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/response-cache-serve.ts +9 -6
- package/src/rsc/rsc-rendering.ts +601 -181
- package/src/rsc/shell-build-manifest.ts +27 -9
- package/src/rsc/shell-capture-constants.ts +60 -6
- package/src/rsc/shell-capture.ts +1001 -250
- package/src/rsc/shell-serve.ts +316 -34
- package/src/rsc/types.ts +7 -0
- package/src/server/context.ts +219 -28
- package/src/server/cookie-store.ts +87 -62
- package/src/server/request-context.ts +263 -38
- package/src/ssr/index.tsx +96 -37
- package/src/ssr/ssr-root.tsx +9 -8
- package/src/testing/flight.entry.ts +14 -0
- package/src/testing/run-loader.ts +63 -5
- package/src/testing/serve-shell-request.ts +378 -0
- package/src/testing/shell-status.ts +34 -12
- package/src/theme/ThemeProvider.tsx +7 -6
- package/src/types/handler-context.ts +14 -1
- package/src/urls/pattern-types.ts +22 -18
- package/src/vite/discovery/shell-prerender-phase.ts +9 -0
- package/src/vite/router-discovery.ts +14 -4
|
@@ -5,9 +5,10 @@
|
|
|
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 exist only for the shell family (`tm`, see
|
|
10
|
+
* isTagsInvalidatedSince), and only PPR shell reads memoize them. What this
|
|
11
|
+
* store adds on top of the raw primitive is the parts Vercel does NOT give us:
|
|
11
12
|
*
|
|
12
13
|
* 1. Stale-while-revalidate. `getCache` has no stale-but-serve: a TTL'd entry
|
|
13
14
|
* simply becomes a miss. We store our own {staleAt, expiresAt} envelope and
|
|
@@ -40,16 +41,23 @@ import type {
|
|
|
40
41
|
CacheItemOptions,
|
|
41
42
|
ShellCacheEntry,
|
|
42
43
|
ShellSnapshotRecord,
|
|
44
|
+
ShellDocumentRead,
|
|
45
|
+
ShellDocumentReadOptions,
|
|
46
|
+
ShellReadStats,
|
|
43
47
|
CacheReadError,
|
|
44
48
|
} from "../types.js";
|
|
45
49
|
import { CACHE_READ_ERROR } from "../types.js";
|
|
46
|
-
import
|
|
50
|
+
import {
|
|
51
|
+
_getRequestContext,
|
|
52
|
+
type RequestContext,
|
|
53
|
+
} from "../../server/request-context.js";
|
|
47
54
|
import { isPerClientSignalHeader } from "../../browser/cookie-name.js";
|
|
48
55
|
import {
|
|
49
56
|
resolveTtl,
|
|
50
57
|
resolveSwrWindow,
|
|
51
58
|
computeExpiration,
|
|
52
59
|
DEFAULT_FUNCTION_TTL,
|
|
60
|
+
staleShellRecaptureDue,
|
|
53
61
|
} from "../cache-policy.js";
|
|
54
62
|
import { reportCacheError, reportingAsync } from "../cache-error.js";
|
|
55
63
|
import type { CacheErrorCategory } from "../cache-error.js";
|
|
@@ -61,7 +69,36 @@ import type { CacheErrorCategory } from "../cache-error.js";
|
|
|
61
69
|
// byte-identical to a per-byte encoder (chunk size does not affect base64), so
|
|
62
70
|
// this is a robustness fix, not a format change. Do NOT reintroduce a local
|
|
63
71
|
// spread-based encoder or raise the chunk here; cf-base64.ts is import-pure.
|
|
64
|
-
import {
|
|
72
|
+
import {
|
|
73
|
+
bufferToBase64,
|
|
74
|
+
base64ByteLength,
|
|
75
|
+
base64ToBuffer,
|
|
76
|
+
base64ToBytes,
|
|
77
|
+
} from "../cf/cf-base64.js";
|
|
78
|
+
import {
|
|
79
|
+
perRequestStoreValue,
|
|
80
|
+
type PerRequestStoreMap,
|
|
81
|
+
} from "../cf/cf-tag-marker-memo.js";
|
|
82
|
+
import {
|
|
83
|
+
ShellMemo,
|
|
84
|
+
freshReadsWindowMs,
|
|
85
|
+
isShellFresh,
|
|
86
|
+
resolveShellMemoOptions,
|
|
87
|
+
shellHasAnyTag,
|
|
88
|
+
type ResolvedShellMemoOptions,
|
|
89
|
+
type StoreMemoOptions,
|
|
90
|
+
} from "../shell-memo.js";
|
|
91
|
+
import {
|
|
92
|
+
TagMarkerMemo,
|
|
93
|
+
TagNameHints,
|
|
94
|
+
hintedTags,
|
|
95
|
+
freshReadsRequired,
|
|
96
|
+
recordMarkerRow,
|
|
97
|
+
DEFAULT_VERCEL_MARKER_FRESH_MS,
|
|
98
|
+
DEFAULT_VERCEL_MARKER_MAX_STALE_MS,
|
|
99
|
+
type MarkerFetch,
|
|
100
|
+
type MarkerMemoOutcome,
|
|
101
|
+
} from "../isolate-tag-memo.js";
|
|
65
102
|
|
|
66
103
|
/**
|
|
67
104
|
* Minimal structural shape of the Vercel Runtime Cache returned by `getCache()`
|
|
@@ -150,6 +187,8 @@ interface VercelSegmentEnvelope {
|
|
|
150
187
|
s: number;
|
|
151
188
|
/** expiresAt (ms since epoch). */
|
|
152
189
|
e: number;
|
|
190
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
191
|
+
ta?: number;
|
|
153
192
|
}
|
|
154
193
|
|
|
155
194
|
/** Stored envelope for a "use cache" function result (getItem/setItem). */
|
|
@@ -164,6 +203,8 @@ interface VercelItemEnvelope {
|
|
|
164
203
|
e: number;
|
|
165
204
|
/** Tags, surfaced on read so a hit still contributes to the document tag set. */
|
|
166
205
|
t?: string[];
|
|
206
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
207
|
+
ta?: number;
|
|
167
208
|
}
|
|
168
209
|
|
|
169
210
|
/** Stored envelope for a full Response (getResponse/putResponse). */
|
|
@@ -180,6 +221,8 @@ interface VercelResponseEnvelope {
|
|
|
180
221
|
e: number;
|
|
181
222
|
/** Tags, preserved so a background revalidation re-write keeps them. */
|
|
182
223
|
t?: string[];
|
|
224
|
+
/** Write time of a tagged entry (ms), for the request tag mask. */
|
|
225
|
+
ta?: number;
|
|
183
226
|
}
|
|
184
227
|
|
|
185
228
|
/** Stored envelope for a PPR shell entry (getShell/putShell). */
|
|
@@ -214,15 +257,8 @@ interface VercelShellEnvelope {
|
|
|
214
257
|
* reports `no-segment-snapshot` after a store round trip.
|
|
215
258
|
*/
|
|
216
259
|
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;
|
|
260
|
+
/** ShellCacheEntry.prunedRecords (diagnostic). */
|
|
261
|
+
pr?: string;
|
|
226
262
|
/** ShellCacheEntry.navigationOnly; its partial-context prelude is not document-safe. */
|
|
227
263
|
no?: true;
|
|
228
264
|
}
|
|
@@ -230,6 +266,7 @@ interface VercelShellEnvelope {
|
|
|
230
266
|
/** Read-path outcome for the debug sink. */
|
|
231
267
|
export type VercelCacheReadOutcome =
|
|
232
268
|
| "miss"
|
|
269
|
+
| "memo-hit"
|
|
233
270
|
| "fresh"
|
|
234
271
|
| "stale-revalidate"
|
|
235
272
|
| "expired"
|
|
@@ -271,10 +308,13 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
271
308
|
cache: VercelRuntimeCache;
|
|
272
309
|
|
|
273
310
|
/**
|
|
274
|
-
* `waitUntil` from `@vercel/functions`.
|
|
275
|
-
*
|
|
276
|
-
* backgrounds the actual writes
|
|
277
|
-
* (
|
|
311
|
+
* `waitUntil` from `@vercel/functions`. Keeps two background reads/writes
|
|
312
|
+
* alive past the response: the stale-read lock write (herd dampening; the
|
|
313
|
+
* router already backgrounds the actual writes) and the PPR shell reads'
|
|
314
|
+
* tag-marker memo refreshes (`memo.markerMaxStaleMs`). When omitted, both
|
|
315
|
+
* run detached (fire-and-forget), and the platform may end a refresh when
|
|
316
|
+
* the response ends; the next read past the stale window then reads the
|
|
317
|
+
* marker itself.
|
|
278
318
|
*/
|
|
279
319
|
waitUntil?: (promise: Promise<unknown>) => void;
|
|
280
320
|
|
|
@@ -316,12 +356,88 @@ export interface VercelCacheStoreOptions<TEnv = unknown> {
|
|
|
316
356
|
* receives the structured VercelCacheReadDebugEvent.
|
|
317
357
|
*/
|
|
318
358
|
debug?: VercelCacheDebug;
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Per-process memos ({@link StoreMemoOptions}), kept per `cache` handle:
|
|
362
|
+
* create the `getCache()` handle once per process, not per request, or the
|
|
363
|
+
* memos never hit. `shellMs` (default 2000) and `shellMaxBytes` (default
|
|
364
|
+
* 16 MiB) size the PPR shell memo; `markerFreshMs` (default 300) and
|
|
365
|
+
* `markerMaxStaleMs` (default 2000) the memo of the tag markers
|
|
366
|
+
* `invalidateTags()` writes, which PPR shell reads use
|
|
367
|
+
* (stale-while-revalidate). The process that invalidates writes the new
|
|
368
|
+
* marker into its memo; another process in the region rejects a memoized
|
|
369
|
+
* shell once its marker memo sees it (up to `markerMaxStaleMs`). The
|
|
370
|
+
* markers are a regional `cache.set` (only `expireTag` is global), so
|
|
371
|
+
* another region serves a shell it memoized before the invalidation until
|
|
372
|
+
* its window passes, where without the memo `expireTag` removes it within
|
|
373
|
+
* about 300 ms; a platform `expireTag` issued outside rango is likewise
|
|
374
|
+
* seen once the window passes. The response of a request that ran
|
|
375
|
+
* `updateTag()` / `revalidateTag()` sets the fresh-reads cookie, and the
|
|
376
|
+
* same user's requests carrying it skip both memos. `{ shellMs: 0 }` turns
|
|
377
|
+
* the shell memo off, `{ markerFreshMs: 0 }` the marker memo.
|
|
378
|
+
*/
|
|
379
|
+
memo?: StoreMemoOptions;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/** A memoized fresh shell read (shell-memo.ts). */
|
|
383
|
+
interface VercelShellMemoValue {
|
|
384
|
+
entry: ShellCacheEntry;
|
|
385
|
+
/** The prelude decoded by the first document read that needed it. */
|
|
386
|
+
preludeBytes?: Uint8Array;
|
|
387
|
+
tags?: string[];
|
|
388
|
+
createdAt: number;
|
|
389
|
+
staleAt: number;
|
|
390
|
+
expiresAt: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The per-process memos of one runtime-cache handle: memoized shells,
|
|
395
|
+
* tag-marker values, and shell tag-name hints. Kept per handle because the
|
|
396
|
+
* handle carries the namespace, so stores over different namespaces never
|
|
397
|
+
* share them.
|
|
398
|
+
*/
|
|
399
|
+
interface VercelHandleMemos {
|
|
400
|
+
shells: ShellMemo<VercelShellMemoValue>;
|
|
401
|
+
markers: TagMarkerMemo;
|
|
402
|
+
hints: TagNameHints;
|
|
319
403
|
}
|
|
320
404
|
|
|
405
|
+
const vercelHandleMemos = new WeakMap<VercelRuntimeCache, VercelHandleMemos>();
|
|
406
|
+
|
|
407
|
+
function handleMemos(cache: VercelRuntimeCache): VercelHandleMemos {
|
|
408
|
+
let memos = vercelHandleMemos.get(cache);
|
|
409
|
+
if (!memos) {
|
|
410
|
+
memos = {
|
|
411
|
+
shells: new ShellMemo<VercelShellMemoValue>(),
|
|
412
|
+
markers: new TagMarkerMemo(),
|
|
413
|
+
hints: new TagNameHints(),
|
|
414
|
+
};
|
|
415
|
+
vercelHandleMemos.set(cache, memos);
|
|
416
|
+
}
|
|
417
|
+
return memos;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* The tags a request invalidated through a store, with their invalidatedAt:
|
|
422
|
+
* request root context (RequestContext._requestRoot, so contexts derived
|
|
423
|
+
* from the request share it) -> store -> tag -> ms (#973). invalidateTags()
|
|
424
|
+
* writes it before its first await, and every read in that request treats an entry
|
|
425
|
+
* carrying one of the tags, written at or before that time, as invalidated.
|
|
426
|
+
* The platform's expireTag() and the tm marker writes are what other
|
|
427
|
+
* requests see, and revalidateTag() does not wait for them.
|
|
428
|
+
*/
|
|
429
|
+
const requestInvalidations: PerRequestStoreMap<Map<string, number>> =
|
|
430
|
+
new WeakMap();
|
|
431
|
+
|
|
321
432
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
322
433
|
return typeof value === "object" && value !== null;
|
|
323
434
|
}
|
|
324
435
|
|
|
436
|
+
/** The envelope's `ta` write stamp, on tagged entries only. */
|
|
437
|
+
function tagStamp(tags: string[]): { ta?: number } {
|
|
438
|
+
return tags.length > 0 ? { ta: Date.now() } : {};
|
|
439
|
+
}
|
|
440
|
+
|
|
325
441
|
/**
|
|
326
442
|
* Vercel Runtime Cache-backed segment cache store.
|
|
327
443
|
*
|
|
@@ -355,12 +471,18 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
355
471
|
* import { getCache, waitUntil } from "@vercel/functions";
|
|
356
472
|
* import { VercelCacheStore } from "@rangojs/router/cache";
|
|
357
473
|
*
|
|
474
|
+
* // One handle per process: getCache() resolves the platform cache on every
|
|
475
|
+
* // call, and the PPR shell and tag-marker memos are kept per handle.
|
|
476
|
+
* // `process.env` (not `import.meta.env`, which needs a VITE_ prefix and
|
|
477
|
+
* // would be undefined here) so cross-deploy busting via `version` works.
|
|
478
|
+
* const runtimeCache = getCache({
|
|
479
|
+
* namespace: process.env.VERCEL_DEPLOYMENT_ID,
|
|
480
|
+
* });
|
|
481
|
+
*
|
|
358
482
|
* export const router = createRouter({
|
|
359
483
|
* cache: () => ({
|
|
360
484
|
* 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 }),
|
|
485
|
+
* cache: runtimeCache,
|
|
364
486
|
* waitUntil,
|
|
365
487
|
* defaults: { ttl: 60, swr: 300 },
|
|
366
488
|
* }),
|
|
@@ -382,6 +504,16 @@ export class VercelCacheStore<
|
|
|
382
504
|
private readonly waitUntil?: (promise: Promise<unknown>) => void;
|
|
383
505
|
private readonly version?: string;
|
|
384
506
|
private readonly maxItemBytes: number;
|
|
507
|
+
private readonly memo: ResolvedShellMemoOptions;
|
|
508
|
+
private readonly handleMemos: VercelHandleMemos;
|
|
509
|
+
/** @internal SegmentCacheStore.freshReadsWindowMs */
|
|
510
|
+
readonly freshReadsWindowMs: number;
|
|
511
|
+
/**
|
|
512
|
+
* @internal SegmentCacheStore.maxShellEntryBytes: the item cap, less the
|
|
513
|
+
* base64 expansion of the prelude the shell envelope stores (the capture
|
|
514
|
+
* measures raw prelude bytes).
|
|
515
|
+
*/
|
|
516
|
+
readonly maxShellEntryBytes: number;
|
|
385
517
|
private readonly name?: string;
|
|
386
518
|
private readonly debug?: VercelCacheDebug;
|
|
387
519
|
|
|
@@ -397,6 +529,13 @@ export class VercelCacheStore<
|
|
|
397
529
|
this.keyGenerator = options.keyGenerator;
|
|
398
530
|
this.version = options.version;
|
|
399
531
|
this.maxItemBytes = options.maxItemBytes ?? VERCEL_MAX_ITEM_BYTES;
|
|
532
|
+
this.maxShellEntryBytes = Math.floor((this.maxItemBytes * 3) / 4);
|
|
533
|
+
this.memo = resolveShellMemoOptions(options.memo, {
|
|
534
|
+
markerFreshMs: DEFAULT_VERCEL_MARKER_FRESH_MS,
|
|
535
|
+
markerMaxStaleMs: DEFAULT_VERCEL_MARKER_MAX_STALE_MS,
|
|
536
|
+
});
|
|
537
|
+
this.handleMemos = handleMemos(options.cache);
|
|
538
|
+
this.freshReadsWindowMs = freshReadsWindowMs(this.memo, true);
|
|
400
539
|
this.name = options.name;
|
|
401
540
|
this.debug = options.debug;
|
|
402
541
|
}
|
|
@@ -447,6 +586,9 @@ export class VercelCacheStore<
|
|
|
447
586
|
});
|
|
448
587
|
return null;
|
|
449
588
|
}
|
|
589
|
+
if (this.maskedMiss("get", key, { t: env.d.tags, ta: env.ta }, readMs)) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
450
592
|
|
|
451
593
|
const isStale = env.s > 0 && now > env.s;
|
|
452
594
|
const shouldRevalidate = isStale
|
|
@@ -483,7 +625,12 @@ export class VercelCacheStore<
|
|
|
483
625
|
const d: CachedEntryData = data.tags
|
|
484
626
|
? { ...data, tags: safeTags.length > 0 ? safeTags : undefined }
|
|
485
627
|
: data;
|
|
486
|
-
const env: VercelSegmentEnvelope = {
|
|
628
|
+
const env: VercelSegmentEnvelope = {
|
|
629
|
+
d,
|
|
630
|
+
s: staleAt,
|
|
631
|
+
e: expiresAt,
|
|
632
|
+
...tagStamp(safeTags),
|
|
633
|
+
};
|
|
487
634
|
await this.write(
|
|
488
635
|
this.toStoreKey(key, "s"),
|
|
489
636
|
env,
|
|
@@ -554,6 +701,7 @@ export class VercelCacheStore<
|
|
|
554
701
|
});
|
|
555
702
|
return null;
|
|
556
703
|
}
|
|
704
|
+
if (this.maskedMiss("getResponse", key, env, readMs)) return null;
|
|
557
705
|
|
|
558
706
|
// Reconstruct the Response BEFORE claiming revalidation. A corrupt body
|
|
559
707
|
// (e.g. invalid base64 from base64ToBuffer, or bad header entries) would
|
|
@@ -629,6 +777,7 @@ export class VercelCacheStore<
|
|
|
629
777
|
s: staleAt,
|
|
630
778
|
e: expiresAt,
|
|
631
779
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
780
|
+
...tagStamp(safeTags),
|
|
632
781
|
};
|
|
633
782
|
await this.write(
|
|
634
783
|
this.toStoreKey(key, "r"),
|
|
@@ -686,6 +835,7 @@ export class VercelCacheStore<
|
|
|
686
835
|
});
|
|
687
836
|
return null;
|
|
688
837
|
}
|
|
838
|
+
if (this.maskedMiss("getItem", key, env, readMs)) return null;
|
|
689
839
|
|
|
690
840
|
const isStale = env.s > 0 && now > env.s;
|
|
691
841
|
const shouldRevalidate = isStale
|
|
@@ -733,6 +883,7 @@ export class VercelCacheStore<
|
|
|
733
883
|
s: staleAt,
|
|
734
884
|
e: expiresAt,
|
|
735
885
|
t: safeTags.length > 0 ? safeTags : undefined,
|
|
886
|
+
...tagStamp(safeTags),
|
|
736
887
|
};
|
|
737
888
|
await this.write(
|
|
738
889
|
this.toStoreKey(key, "i"),
|
|
@@ -752,8 +903,140 @@ export class VercelCacheStore<
|
|
|
752
903
|
key: string,
|
|
753
904
|
options?: { claimRevalidation?: boolean },
|
|
754
905
|
): Promise<{ entry: ShellCacheEntry; shouldRevalidate?: boolean } | null> {
|
|
906
|
+
const read = await this.readShell(key, options, undefined);
|
|
907
|
+
return read
|
|
908
|
+
? { entry: read.entry, shouldRevalidate: read.shouldRevalidate }
|
|
909
|
+
: null;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* @internal The document serve path's read (SegmentCacheStore
|
|
914
|
+
* .readShellDocument): the same read as getShell, with the prelude decoded
|
|
915
|
+
* here, once per memoized shell rather than once per HIT, and the read's
|
|
916
|
+
* `debugPerformance` stats. The runtime cache returns a parsed value, so
|
|
917
|
+
* there is no prelude-first read and the tag-marker read follows the entry
|
|
918
|
+
* read (`markerSerial`).
|
|
919
|
+
*/
|
|
920
|
+
async readShellDocument(
|
|
921
|
+
key: string,
|
|
922
|
+
options?: ShellDocumentReadOptions,
|
|
923
|
+
): Promise<ShellDocumentRead | null> {
|
|
924
|
+
const stats: ShellReadStats | undefined = _getRequestContext()
|
|
925
|
+
?._metricsStore
|
|
926
|
+
? { tier: "store" }
|
|
927
|
+
: undefined;
|
|
928
|
+
const read = await this.readShell(key, undefined, stats, options?.tagHints);
|
|
929
|
+
if (!read) return null;
|
|
930
|
+
const { entry, memoized } = read;
|
|
931
|
+
let prelude: Uint8Array;
|
|
932
|
+
if (entry.prelude === undefined) {
|
|
933
|
+
prelude = new Uint8Array(0);
|
|
934
|
+
} else if (memoized?.preludeBytes) {
|
|
935
|
+
prelude = memoized.preludeBytes;
|
|
936
|
+
} else {
|
|
937
|
+
try {
|
|
938
|
+
prelude = base64ToBytes(entry.prelude);
|
|
939
|
+
} catch (error) {
|
|
940
|
+
const storeKey = this.toStoreKey(key, "h");
|
|
941
|
+
reportCacheError(
|
|
942
|
+
error,
|
|
943
|
+
"cache-corrupt",
|
|
944
|
+
"[VercelCacheStore] getShell: undecodable prelude, evicting",
|
|
945
|
+
);
|
|
946
|
+
this.handleMemos.shells.delete(storeKey);
|
|
947
|
+
void this.safeDelete(storeKey);
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
if (memoized) memoized.preludeBytes = prelude;
|
|
951
|
+
}
|
|
952
|
+
if (stats) stats.preludeBytes = prelude.length;
|
|
953
|
+
const { prelude: _prelude, snapshot, ...rest } = entry;
|
|
954
|
+
return {
|
|
955
|
+
entry: rest,
|
|
956
|
+
prelude,
|
|
957
|
+
shouldRevalidate: read.shouldRevalidate,
|
|
958
|
+
snapshot: Promise.resolve(snapshot),
|
|
959
|
+
...(stats && { stats }),
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
/** @internal SegmentCacheStore.dropShellMemo */
|
|
964
|
+
dropShellMemo(key: string): void {
|
|
965
|
+
this.handleMemos.shells.delete(this.toStoreKey(key, "h"));
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* getShell's read, shared with readShellDocument: the shell memo first
|
|
970
|
+
* (shell-memo.ts), then the runtime cache. A memoized shell still runs the
|
|
971
|
+
* tag-marker check; `memoized` is the memo slot's value when the read came
|
|
972
|
+
* from, or went into, the memo (readShellDocument keeps the decoded prelude
|
|
973
|
+
* there).
|
|
974
|
+
*/
|
|
975
|
+
private async readShell(
|
|
976
|
+
key: string,
|
|
977
|
+
options: { claimRevalidation?: boolean } | undefined,
|
|
978
|
+
stats: ShellReadStats | undefined,
|
|
979
|
+
routeTags?: readonly string[],
|
|
980
|
+
): Promise<{
|
|
981
|
+
entry: ShellCacheEntry;
|
|
982
|
+
shouldRevalidate: boolean;
|
|
983
|
+
memoized?: VercelShellMemoValue;
|
|
984
|
+
} | null> {
|
|
755
985
|
const storeKey = this.toStoreKey(key, "h");
|
|
986
|
+
const memo = this.handleMemos.shells;
|
|
987
|
+
// A request after the same user's updateTag() reads past the memos.
|
|
988
|
+
const freshReads = freshReadsRequired(_getRequestContext());
|
|
989
|
+
if (stats && freshReads) stats.freshReads = true;
|
|
990
|
+
const outcomes = stats ? new Map<string, MarkerMemoOutcome>() : undefined;
|
|
991
|
+
const prefetched = this.prefetchShellMarkers(
|
|
992
|
+
storeKey,
|
|
993
|
+
routeTags,
|
|
994
|
+
stats,
|
|
995
|
+
outcomes,
|
|
996
|
+
);
|
|
997
|
+
const memoized = freshReads
|
|
998
|
+
? undefined
|
|
999
|
+
: memo.get(storeKey, this.memo.shellMs);
|
|
1000
|
+
if (memoized) {
|
|
1001
|
+
if (isShellFresh(memoized.staleAt, memoized.expiresAt)) {
|
|
1002
|
+
if (stats) stats.tier = "memo";
|
|
1003
|
+
// The marker check is what decides whether a memoized shell may be
|
|
1004
|
+
// served; it runs on every read, memoized or not.
|
|
1005
|
+
if (
|
|
1006
|
+
await this.shellTagsInvalidated(
|
|
1007
|
+
memoized.tags,
|
|
1008
|
+
memoized.createdAt,
|
|
1009
|
+
stats,
|
|
1010
|
+
prefetched,
|
|
1011
|
+
outcomes,
|
|
1012
|
+
)
|
|
1013
|
+
) {
|
|
1014
|
+
memo.delete(storeKey);
|
|
1015
|
+
void this.safeDelete(storeKey);
|
|
1016
|
+
this.emitDebug({ op: "getShell", key, outcome: "miss" });
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
if (stats) stats.memo = { hit: true, bytes: memo.size };
|
|
1020
|
+
this.emitDebug({
|
|
1021
|
+
op: "getShell",
|
|
1022
|
+
key,
|
|
1023
|
+
outcome: "memo-hit",
|
|
1024
|
+
staleAt: memoized.staleAt,
|
|
1025
|
+
expiresAt: memoized.expiresAt,
|
|
1026
|
+
});
|
|
1027
|
+
return {
|
|
1028
|
+
entry: memoized.entry,
|
|
1029
|
+
shouldRevalidate: false,
|
|
1030
|
+
memoized,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
memo.delete(storeKey);
|
|
1034
|
+
}
|
|
1035
|
+
if (stats && this.memo.shellMs > 0) {
|
|
1036
|
+
stats.memo = { hit: false, bytes: memo.size };
|
|
1037
|
+
}
|
|
756
1038
|
const started = Date.now();
|
|
1039
|
+
const readStartedAt = stats ? performance.now() : 0;
|
|
757
1040
|
let raw: unknown;
|
|
758
1041
|
try {
|
|
759
1042
|
raw = await this.cache.get(storeKey);
|
|
@@ -769,6 +1052,13 @@ export class VercelCacheStore<
|
|
|
769
1052
|
return null;
|
|
770
1053
|
}
|
|
771
1054
|
const env = this.asShellEnvelope(this.decodeRaw(raw));
|
|
1055
|
+
if (stats) {
|
|
1056
|
+
const readAt = performance.now();
|
|
1057
|
+
stats.matchMs = readAt - readStartedAt;
|
|
1058
|
+
if (stats.markerHintStartedAt !== undefined) {
|
|
1059
|
+
stats.markerLeadMs = readAt - stats.markerHintStartedAt;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
772
1062
|
if (!env) {
|
|
773
1063
|
reportCacheError(
|
|
774
1064
|
new Error("malformed shell envelope"),
|
|
@@ -794,10 +1084,9 @@ export class VercelCacheStore<
|
|
|
794
1084
|
return null;
|
|
795
1085
|
}
|
|
796
1086
|
|
|
1087
|
+
this.handleMemos.hints.remember(storeKey, env.t);
|
|
797
1088
|
if (
|
|
798
|
-
env.t
|
|
799
|
-
env.t.length > 0 &&
|
|
800
|
-
(await this.isTagsInvalidatedSince(env.t, env.c))
|
|
1089
|
+
await this.shellTagsInvalidated(env.t, env.c, stats, prefetched, outcomes)
|
|
801
1090
|
) {
|
|
802
1091
|
void this.safeDelete(storeKey);
|
|
803
1092
|
this.emitDebug({ op: "getShell", key, outcome: "miss", readMs });
|
|
@@ -806,7 +1095,14 @@ export class VercelCacheStore<
|
|
|
806
1095
|
|
|
807
1096
|
const isStale = env.s > 0 && now > env.s;
|
|
808
1097
|
let shouldRevalidate = isStale;
|
|
809
|
-
|
|
1098
|
+
// Inside the recapture floor the serve path schedules nothing, so the
|
|
1099
|
+
// lock is left for the first stale read past it: claimed here, it held
|
|
1100
|
+
// the shell REVALIDATION_LOCK_MS without a recapture.
|
|
1101
|
+
if (
|
|
1102
|
+
isStale &&
|
|
1103
|
+
options?.claimRevalidation !== false &&
|
|
1104
|
+
staleShellRecaptureDue({ createdAt: env.c }, now)
|
|
1105
|
+
) {
|
|
810
1106
|
shouldRevalidate = await this.claimRevalidation(
|
|
811
1107
|
storeKey,
|
|
812
1108
|
env.e,
|
|
@@ -822,22 +1118,130 @@ export class VercelCacheStore<
|
|
|
822
1118
|
expiresAt: env.e,
|
|
823
1119
|
readMs,
|
|
824
1120
|
});
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
1121
|
+
const entry: ShellCacheEntry = {
|
|
1122
|
+
prelude: env.p,
|
|
1123
|
+
postponed: env.po,
|
|
1124
|
+
reactVersion: env.rv,
|
|
1125
|
+
buildVersion: env.bv,
|
|
1126
|
+
initialTheme: env.i,
|
|
1127
|
+
snapshot: env.sn,
|
|
1128
|
+
docKey: env.dk,
|
|
1129
|
+
prunedRecords: env.pr,
|
|
1130
|
+
navigationOnly: env.no,
|
|
1131
|
+
createdAt: env.c,
|
|
1132
|
+
};
|
|
1133
|
+
if (isStale) return { entry, shouldRevalidate };
|
|
1134
|
+
const value: VercelShellMemoValue = {
|
|
1135
|
+
entry,
|
|
1136
|
+
tags: env.t,
|
|
1137
|
+
createdAt: env.c,
|
|
1138
|
+
staleAt: env.s,
|
|
1139
|
+
expiresAt: env.e,
|
|
1140
|
+
};
|
|
1141
|
+
// Counted: the envelope, plus the decoded prelude readShellDocument
|
|
1142
|
+
// attaches to the slot on first use.
|
|
1143
|
+
memo.set(
|
|
1144
|
+
storeKey,
|
|
1145
|
+
value,
|
|
1146
|
+
(typeof raw === "string" ? raw.length : JSON.stringify(env).length) +
|
|
1147
|
+
(env.p === undefined ? 0 : base64ByteLength(env.p)),
|
|
1148
|
+
this.memo.shellMs,
|
|
1149
|
+
this.memo.shellMaxBytes,
|
|
1150
|
+
);
|
|
1151
|
+
return { entry, shouldRevalidate, memoized: value };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* The shell's tag-marker check, timed into `stats`. Markers a prefetch
|
|
1156
|
+
* already started (hinted tags) are awaited, not re-read; the check itself
|
|
1157
|
+
* always uses the entry's own tags.
|
|
1158
|
+
*/
|
|
1159
|
+
private async shellTagsInvalidated(
|
|
1160
|
+
tags: string[] | undefined,
|
|
1161
|
+
createdAt: number,
|
|
1162
|
+
stats: ShellReadStats | undefined,
|
|
1163
|
+
prefetched: Map<string, Promise<number | null>> | undefined,
|
|
1164
|
+
outcomes: Map<string, MarkerMemoOutcome> | undefined,
|
|
1165
|
+
): Promise<boolean> {
|
|
1166
|
+
if (stats) {
|
|
1167
|
+
stats.tags = tags?.length ?? 0;
|
|
1168
|
+
stats.markerSerial = true;
|
|
1169
|
+
}
|
|
1170
|
+
if (!tags || tags.length === 0) {
|
|
1171
|
+
if (stats) stats.markerMs = stats.markerWaitMs = 0;
|
|
1172
|
+
return false;
|
|
1173
|
+
}
|
|
1174
|
+
const startedAt = stats ? performance.now() : 0;
|
|
1175
|
+
const invalidated = await this.tagsInvalidatedSince(
|
|
1176
|
+
tags,
|
|
1177
|
+
createdAt,
|
|
1178
|
+
true,
|
|
1179
|
+
prefetched,
|
|
1180
|
+
outcomes,
|
|
1181
|
+
);
|
|
1182
|
+
if (stats) {
|
|
1183
|
+
stats.markerMs = stats.markerWaitMs = performance.now() - startedAt;
|
|
1184
|
+
recordMarkerRow(stats, tags, outcomes);
|
|
1185
|
+
}
|
|
1186
|
+
return invalidated;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Start the tm-marker reads of the tags this shell key carried last time
|
|
1191
|
+
* (and the route's static tags) alongside the entry read.
|
|
1192
|
+
*/
|
|
1193
|
+
private prefetchShellMarkers(
|
|
1194
|
+
storeKey: string,
|
|
1195
|
+
routeTags: readonly string[] | undefined,
|
|
1196
|
+
stats: ShellReadStats | undefined,
|
|
1197
|
+
outcomes: Map<string, MarkerMemoOutcome> | undefined,
|
|
1198
|
+
): Map<string, Promise<number | null>> | undefined {
|
|
1199
|
+
const tags = hintedTags(this.handleMemos.hints.get(storeKey), routeTags);
|
|
1200
|
+
if (tags.length === 0) return undefined;
|
|
1201
|
+
if (stats) {
|
|
1202
|
+
stats.markerHinted = tags;
|
|
1203
|
+
stats.markerHintStartedAt = performance.now();
|
|
1204
|
+
}
|
|
1205
|
+
const prefetched = new Map<string, Promise<number | null>>();
|
|
1206
|
+
for (const tag of tags) {
|
|
1207
|
+
const read = this.readTagMarker(tag, true, outcomes);
|
|
1208
|
+
read.catch(() => {});
|
|
1209
|
+
prefetched.set(tag, read);
|
|
1210
|
+
}
|
|
1211
|
+
return prefetched;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/**
|
|
1215
|
+
* A tag's latest invalidatedAt (or null). With `isolateMemo` (PPR shell
|
|
1216
|
+
* reads only) through the per-process marker memo (isolate-tag-memo.ts):
|
|
1217
|
+
* fresh values are used as is, stale ones are used and refreshed in the
|
|
1218
|
+
* background, older ones block on the runtime-cache read. A request
|
|
1219
|
+
* carrying the fresh-reads cookie skips the memo. A failed read rejects and
|
|
1220
|
+
* is never memoized. The shell write gate and build shells
|
|
1221
|
+
* (isTagsInvalidatedSince) read the runtime cache every time.
|
|
1222
|
+
*/
|
|
1223
|
+
private async readTagMarker(
|
|
1224
|
+
tag: string,
|
|
1225
|
+
isolateMemo: boolean,
|
|
1226
|
+
outcomes?: Map<string, MarkerMemoOutcome>,
|
|
1227
|
+
): Promise<number | null> {
|
|
1228
|
+
const key = this.toStoreKey(tag, "tm");
|
|
1229
|
+
const read = async (): Promise<MarkerFetch> => {
|
|
1230
|
+
const raw = await this.cache.get(key);
|
|
1231
|
+
const decoded =
|
|
1232
|
+
raw == null ? null : (this.decodeRaw(raw) as { at?: unknown } | null);
|
|
1233
|
+
const value =
|
|
1234
|
+
decoded && typeof decoded.at === "number" ? decoded.at : null;
|
|
1235
|
+
return { value, memoize: true };
|
|
840
1236
|
};
|
|
1237
|
+
if (!isolateMemo) return (await read()).value;
|
|
1238
|
+
return this.handleMemos.markers.readThrough(key, read, {
|
|
1239
|
+
freshMs: this.memo.markerFreshMs,
|
|
1240
|
+
maxStaleMs: this.memo.markerMaxStaleMs,
|
|
1241
|
+
bypass: freshReadsRequired(_getRequestContext()),
|
|
1242
|
+
keepAlive: this.waitUntil,
|
|
1243
|
+
onOutcome: outcomes ? (outcome) => outcomes.set(tag, outcome) : undefined,
|
|
1244
|
+
});
|
|
841
1245
|
}
|
|
842
1246
|
|
|
843
1247
|
async putShell(
|
|
@@ -847,6 +1251,9 @@ export class VercelCacheStore<
|
|
|
847
1251
|
swrSeconds?: number,
|
|
848
1252
|
tags?: string[],
|
|
849
1253
|
): Promise<"stored" | "invalidated" | void> {
|
|
1254
|
+
// This instance serves its own new capture from the next read on.
|
|
1255
|
+
this.handleMemos.shells.delete(this.toStoreKey(key, "h"));
|
|
1256
|
+
this.handleMemos.hints.remember(this.toStoreKey(key, "h"), tags);
|
|
850
1257
|
try {
|
|
851
1258
|
const ttl = resolveTtl(ttlSeconds, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
852
1259
|
const swrWindow = resolveSwrWindow(swrSeconds, this.defaults);
|
|
@@ -885,8 +1292,7 @@ export class VercelCacheStore<
|
|
|
885
1292
|
i: entry.initialTheme,
|
|
886
1293
|
sn: entry.snapshot,
|
|
887
1294
|
dk: entry.docKey,
|
|
888
|
-
|
|
889
|
-
tw: entry.transitionWhen,
|
|
1295
|
+
pr: entry.prunedRecords,
|
|
890
1296
|
no: entry.navigationOnly,
|
|
891
1297
|
};
|
|
892
1298
|
// write() enforces the 2 MB per-item ceiling (withinSizeLimit): an
|
|
@@ -917,15 +1323,26 @@ export class VercelCacheStore<
|
|
|
917
1323
|
tags: string[],
|
|
918
1324
|
sinceMs: number,
|
|
919
1325
|
): Promise<boolean> {
|
|
1326
|
+
return this.tagsInvalidatedSince(tags, sinceMs, false);
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
private async tagsInvalidatedSince(
|
|
1330
|
+
tags: string[],
|
|
1331
|
+
sinceMs: number,
|
|
1332
|
+
isolateMemo: boolean,
|
|
1333
|
+
prefetched?: Map<string, Promise<number | null>>,
|
|
1334
|
+
outcomes?: Map<string, MarkerMemoOutcome>,
|
|
1335
|
+
): Promise<boolean> {
|
|
1336
|
+
if (this.maskedForRequest(tags, sinceMs)) return true;
|
|
920
1337
|
try {
|
|
921
1338
|
const markers = await Promise.all(
|
|
922
|
-
tags.map(
|
|
1339
|
+
tags.map(
|
|
1340
|
+
(tag) =>
|
|
1341
|
+
prefetched?.get(tag) ??
|
|
1342
|
+
this.readTagMarker(tag, isolateMemo, outcomes),
|
|
1343
|
+
),
|
|
923
1344
|
);
|
|
924
|
-
for (const
|
|
925
|
-
if (raw == null) continue;
|
|
926
|
-
const decoded = this.decodeRaw(raw) as { at?: unknown } | null;
|
|
927
|
-
const at =
|
|
928
|
-
decoded && typeof decoded.at === "number" ? decoded.at : null;
|
|
1345
|
+
for (const at of markers) {
|
|
929
1346
|
if (at !== null && at >= sinceMs) return true;
|
|
930
1347
|
}
|
|
931
1348
|
return false;
|
|
@@ -941,6 +1358,12 @@ export class VercelCacheStore<
|
|
|
941
1358
|
|
|
942
1359
|
async invalidateTags(tags: string[]): Promise<void> {
|
|
943
1360
|
if (!tags || tags.length === 0) return;
|
|
1361
|
+
// Every memo hit reads the tag markers, so a read in flight that memoizes
|
|
1362
|
+
// one of these shells again is rejected on its next hit once the markers
|
|
1363
|
+
// below land (no in-flight record needed, unlike KV-less CFCacheStore).
|
|
1364
|
+
this.handleMemos.shells.deleteWhere((shell) =>
|
|
1365
|
+
shellHasAnyTag(shell.tags, tags),
|
|
1366
|
+
);
|
|
944
1367
|
// No per-item cap here: an invalidation must reach every requested tag.
|
|
945
1368
|
const safe = this.validateTags(
|
|
946
1369
|
tags,
|
|
@@ -948,6 +1371,8 @@ export class VercelCacheStore<
|
|
|
948
1371
|
"cache-invalidate",
|
|
949
1372
|
);
|
|
950
1373
|
if (safe.length === 0) return;
|
|
1374
|
+
const at = Date.now();
|
|
1375
|
+
this.maskTagsForRequest(safe, at);
|
|
951
1376
|
// Marker writes FIRST, and strict: isTagsInvalidatedSince() is how an
|
|
952
1377
|
// updateTag() reaches a build-time shell entry (expireTag cannot delete
|
|
953
1378
|
// what lives in the build manifest), so a failed marker write must reject
|
|
@@ -955,13 +1380,14 @@ export class VercelCacheStore<
|
|
|
955
1380
|
// the baked shell keeps serving.
|
|
956
1381
|
await Promise.all(
|
|
957
1382
|
safe.map((tag) =>
|
|
958
|
-
this.cache.set(
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
{ ttl: TAG_MARKER_TTL_SECONDS },
|
|
962
|
-
),
|
|
1383
|
+
this.cache.set(this.toStoreKey(tag, "tm"), JSON.stringify({ at }), {
|
|
1384
|
+
ttl: TAG_MARKER_TTL_SECONDS,
|
|
1385
|
+
}),
|
|
963
1386
|
),
|
|
964
1387
|
);
|
|
1388
|
+
// Same process: later reads here see the invalidation at once.
|
|
1389
|
+
const { markers } = this.handleMemos;
|
|
1390
|
+
for (const tag of safe) markers.store(this.toStoreKey(tag, "tm"), at);
|
|
965
1391
|
try {
|
|
966
1392
|
await this.cache.expireTag(safe);
|
|
967
1393
|
} catch (error) {
|
|
@@ -980,6 +1406,58 @@ export class VercelCacheStore<
|
|
|
980
1406
|
}
|
|
981
1407
|
}
|
|
982
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* The request-local half of invalidateTags(), run before its first await
|
|
1411
|
+
* (#973): see requestInvalidations. revalidateTag() waits for nothing
|
|
1412
|
+
* after it.
|
|
1413
|
+
*/
|
|
1414
|
+
private maskTagsForRequest(tags: string[], at: number): void {
|
|
1415
|
+
const ctx = _getRequestContext();
|
|
1416
|
+
if (!ctx) return;
|
|
1417
|
+
const mask = perRequestStoreValue(
|
|
1418
|
+
requestInvalidations,
|
|
1419
|
+
ctx._requestRoot ?? ctx,
|
|
1420
|
+
this,
|
|
1421
|
+
() => new Map<string, number>(),
|
|
1422
|
+
);
|
|
1423
|
+
for (const tag of tags) mask.set(tag, at);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Whether this request invalidated one of `tags` through this store at or
|
|
1428
|
+
* after `taggedAt`. `>=`, as the marker check: an entry written in the
|
|
1429
|
+
* invalidation's millisecond may hold a value computed before it, so it
|
|
1430
|
+
* misses (a false miss, never a stale read). An entry without a stamp was
|
|
1431
|
+
* written before the stamp existed and counts as older.
|
|
1432
|
+
*/
|
|
1433
|
+
private maskedForRequest(
|
|
1434
|
+
tags: string[] | undefined,
|
|
1435
|
+
taggedAt: number | undefined,
|
|
1436
|
+
): boolean {
|
|
1437
|
+
if (!tags?.length) return false;
|
|
1438
|
+
const ctx = _getRequestContext();
|
|
1439
|
+
const mask =
|
|
1440
|
+
ctx && requestInvalidations.get(ctx._requestRoot ?? ctx)?.get(this);
|
|
1441
|
+
if (!mask) return false;
|
|
1442
|
+
for (const tag of tags) {
|
|
1443
|
+
const at = mask.get(tag);
|
|
1444
|
+
if (at !== undefined && at >= (taggedAt ?? 0)) return true;
|
|
1445
|
+
}
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/** A data read's mask check: reports the miss and returns true when masked. */
|
|
1450
|
+
private maskedMiss(
|
|
1451
|
+
op: "get" | "getItem" | "getResponse",
|
|
1452
|
+
key: string,
|
|
1453
|
+
env: { t?: string[]; ta?: number },
|
|
1454
|
+
readMs: number,
|
|
1455
|
+
): boolean {
|
|
1456
|
+
if (!this.maskedForRequest(env.t, env.ta)) return false;
|
|
1457
|
+
this.emitDebug({ op, key, outcome: "miss", readMs });
|
|
1458
|
+
return true;
|
|
1459
|
+
}
|
|
1460
|
+
|
|
983
1461
|
// --- Internals ---
|
|
984
1462
|
|
|
985
1463
|
// The `rg:{family}:` prefix keeps the three value tiers in disjoint
|
|
@@ -1188,7 +1666,7 @@ export class VercelCacheStore<
|
|
|
1188
1666
|
|
|
1189
1667
|
private asSegmentEnvelope(raw: unknown): VercelSegmentEnvelope | null {
|
|
1190
1668
|
if (!isRecord(raw)) return null;
|
|
1191
|
-
const { d, s, e } = raw;
|
|
1669
|
+
const { d, s, e, ta } = raw;
|
|
1192
1670
|
if (
|
|
1193
1671
|
!isRecord(d) ||
|
|
1194
1672
|
!Array.isArray((d as Record<string, unknown>).segments)
|
|
@@ -1196,12 +1674,17 @@ export class VercelCacheStore<
|
|
|
1196
1674
|
return null;
|
|
1197
1675
|
}
|
|
1198
1676
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1199
|
-
return {
|
|
1677
|
+
return {
|
|
1678
|
+
d: d as unknown as CachedEntryData,
|
|
1679
|
+
s,
|
|
1680
|
+
e,
|
|
1681
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1682
|
+
};
|
|
1200
1683
|
}
|
|
1201
1684
|
|
|
1202
1685
|
private asItemEnvelope(raw: unknown): VercelItemEnvelope | null {
|
|
1203
1686
|
if (!isRecord(raw)) return null;
|
|
1204
|
-
const { v, h, s, e, t } = raw;
|
|
1687
|
+
const { v, h, s, e, t, ta } = raw;
|
|
1205
1688
|
if (typeof v !== "string") return null;
|
|
1206
1689
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
1207
1690
|
return {
|
|
@@ -1209,13 +1692,14 @@ export class VercelCacheStore<
|
|
|
1209
1692
|
h: typeof h === "string" ? h : undefined,
|
|
1210
1693
|
s,
|
|
1211
1694
|
e,
|
|
1695
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1212
1696
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1213
1697
|
};
|
|
1214
1698
|
}
|
|
1215
1699
|
|
|
1216
1700
|
private asShellEnvelope(raw: unknown): VercelShellEnvelope | null {
|
|
1217
1701
|
if (!isRecord(raw)) return null;
|
|
1218
|
-
const { p, po, rv, bv, c, s, e, t, i, sn, dk,
|
|
1702
|
+
const { p, po, rv, bv, c, s, e, t, i, sn, dk, pr, no } = raw;
|
|
1219
1703
|
if (typeof rv !== "string") return null;
|
|
1220
1704
|
// Document half: required unless navigationOnly (`no`), which stores
|
|
1221
1705
|
// neither field. Tolerate a legacy navigationOnly envelope that still
|
|
@@ -1240,15 +1724,14 @@ export class VercelCacheStore<
|
|
|
1240
1724
|
i: typeof i === "string" ? i : undefined,
|
|
1241
1725
|
sn: Array.isArray(sn) ? (sn as ShellSnapshotRecord[]) : undefined,
|
|
1242
1726
|
dk: typeof dk === "string" ? dk : undefined,
|
|
1243
|
-
|
|
1244
|
-
tw: tw === true ? true : undefined,
|
|
1727
|
+
pr: typeof pr === "string" ? pr : undefined,
|
|
1245
1728
|
no: no === true ? true : undefined,
|
|
1246
1729
|
};
|
|
1247
1730
|
}
|
|
1248
1731
|
|
|
1249
1732
|
private asResponseEnvelope(raw: unknown): VercelResponseEnvelope | null {
|
|
1250
1733
|
if (!isRecord(raw)) return null;
|
|
1251
|
-
const { b, st, hd, s, e, t } = raw;
|
|
1734
|
+
const { b, st, hd, s, e, t, ta } = raw;
|
|
1252
1735
|
if (typeof b !== "string" || typeof st !== "number") return null;
|
|
1253
1736
|
if (!Array.isArray(hd)) return null;
|
|
1254
1737
|
if (typeof s !== "number" || typeof e !== "number") return null;
|
|
@@ -1259,6 +1742,7 @@ export class VercelCacheStore<
|
|
|
1259
1742
|
s,
|
|
1260
1743
|
e,
|
|
1261
1744
|
t: Array.isArray(t) ? (t as string[]) : undefined,
|
|
1745
|
+
ta: typeof ta === "number" ? ta : undefined,
|
|
1262
1746
|
};
|
|
1263
1747
|
}
|
|
1264
1748
|
}
|