@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.
Files changed (152) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/history-state.d.ts +16 -0
  4. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  5. package/dist/types/cache/cache-policy.d.ts +17 -0
  6. package/dist/types/cache/cache-scope.d.ts +98 -14
  7. package/dist/types/cache/cache-tag.d.ts +92 -7
  8. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  9. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  10. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  11. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  12. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  13. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  14. package/dist/types/cache/handle-snapshot.d.ts +4 -3
  15. package/dist/types/cache/index.d.ts +1 -0
  16. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  17. package/dist/types/cache/shell-memo.d.ts +178 -0
  18. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  19. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  20. package/dist/types/cache/types.d.ts +207 -43
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  22. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  23. package/dist/types/router/instrument.d.ts +8 -0
  24. package/dist/types/router/middleware-types.d.ts +6 -1
  25. package/dist/types/router/segment-resolution/loader-cache.d.ts +40 -11
  26. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +21 -10
  27. package/dist/types/router/segment-resolution/mask-nested.d.ts +65 -24
  28. package/dist/types/rsc/full-payload.d.ts +14 -0
  29. package/dist/types/rsc/handler.d.ts +1 -1
  30. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  31. package/dist/types/rsc/shell-capture-constants.d.ts +53 -6
  32. package/dist/types/rsc/shell-capture.d.ts +146 -15
  33. package/dist/types/rsc/shell-serve.d.ts +140 -17
  34. package/dist/types/rsc/types.d.ts +7 -0
  35. package/dist/types/server/context.d.ts +109 -15
  36. package/dist/types/server/cookie-store.d.ts +21 -0
  37. package/dist/types/server/request-context.d.ts +159 -33
  38. package/dist/types/ssr/index.d.ts +9 -0
  39. package/dist/types/ssr/ssr-root.d.ts +9 -8
  40. package/dist/types/testing/flight.entry.d.ts +7 -0
  41. package/dist/types/testing/run-loader.d.ts +18 -3
  42. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  43. package/dist/types/testing/shell-status.d.ts +23 -10
  44. package/dist/types/types/handler-context.d.ts +14 -1
  45. package/dist/types/urls/pattern-types.d.ts +22 -18
  46. package/dist/types/vite/router-discovery.d.ts +29 -2
  47. package/dist/vite/index.js +46 -9
  48. package/package.json +1 -1
  49. package/skills/breadcrumbs/SKILL.md +17 -7
  50. package/skills/cache-guide/SKILL.md +123 -51
  51. package/skills/caching/SKILL.md +253 -23
  52. package/skills/catalog.json +1 -1
  53. package/skills/client-urls/SKILL.md +11 -6
  54. package/skills/cloudflare/SKILL.md +15 -0
  55. package/skills/comparison/references/framework-comparison.md +44 -32
  56. package/skills/defer-hydration/SKILL.md +50 -30
  57. package/skills/deployment-caching/SKILL.md +5 -2
  58. package/skills/document-cache/SKILL.md +8 -2
  59. package/skills/handler-use/SKILL.md +2 -0
  60. package/skills/hooks/handle-and-actions.md +3 -1
  61. package/skills/hooks/state.md +3 -1
  62. package/skills/loader/SKILL.md +161 -39
  63. package/skills/middleware/SKILL.md +16 -9
  64. package/skills/migrate-nextjs/SKILL.md +67 -36
  65. package/skills/migrate-react-router/SKILL.md +2 -1
  66. package/skills/migrate-react-router/component-migration.md +3 -1
  67. package/skills/migrate-react-router/data-and-actions.md +3 -1
  68. package/skills/migrate-react-router/route-mapping.md +7 -4
  69. package/skills/observability/SKILL.md +64 -0
  70. package/skills/parallel/SKILL.md +13 -8
  71. package/skills/ppr/SKILL.md +655 -363
  72. package/skills/prerender/SKILL.md +27 -14
  73. package/skills/rango/SKILL.md +52 -33
  74. package/skills/route/SKILL.md +12 -7
  75. package/skills/router-setup/SKILL.md +4 -2
  76. package/skills/scripts/SKILL.md +8 -1
  77. package/skills/server-actions/SKILL.md +11 -0
  78. package/skills/shell-manifest/SKILL.md +21 -12
  79. package/skills/testing/SKILL.md +25 -21
  80. package/skills/testing/cache-prerender.md +15 -14
  81. package/skills/testing/loader.md +18 -1
  82. package/skills/testing/serve-shell-request.md +188 -0
  83. package/skills/theme/SKILL.md +52 -4
  84. package/skills/typesafety/env-and-bindings.md +10 -2
  85. package/skills/use-cache/SKILL.md +39 -16
  86. package/skills/vercel/SKILL.md +14 -2
  87. package/skills/view-transitions/SKILL.md +1 -1
  88. package/src/browser/cookie-name.ts +23 -0
  89. package/src/browser/history-state.ts +23 -0
  90. package/src/browser/rsc-router.tsx +5 -0
  91. package/src/cache/cache-key-utils.ts +22 -2
  92. package/src/cache/cache-policy.ts +22 -0
  93. package/src/cache/cache-runtime.ts +34 -7
  94. package/src/cache/cache-scope.ts +345 -33
  95. package/src/cache/cache-tag.ts +375 -11
  96. package/src/cache/cf/cf-base64.ts +36 -4
  97. package/src/cache/cf/cf-cache-constants.ts +12 -0
  98. package/src/cache/cf/cf-cache-store.ts +990 -348
  99. package/src/cache/cf/cf-cache-types.ts +32 -4
  100. package/src/cache/cf/cf-shell-frame.ts +258 -0
  101. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  102. package/src/cache/handle-snapshot.ts +4 -3
  103. package/src/cache/index.ts +2 -0
  104. package/src/cache/isolate-tag-memo.ts +308 -0
  105. package/src/cache/shell-memo.ts +341 -0
  106. package/src/cache/shell-snapshot.ts +217 -27
  107. package/src/cache/tag-invalidation.ts +104 -19
  108. package/src/cache/types.ts +211 -43
  109. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  110. package/src/prerender/build-shell-capture.ts +65 -11
  111. package/src/route-definition/dsl-helpers.ts +4 -3
  112. package/src/router/handler-context.ts +38 -4
  113. package/src/router/instrument.ts +54 -2
  114. package/src/router/intercept-resolution.ts +19 -7
  115. package/src/router/loader-resolution.ts +63 -32
  116. package/src/router/match-api.ts +28 -9
  117. package/src/router/match-handlers.ts +5 -0
  118. package/src/router/match-middleware/cache-lookup.ts +17 -0
  119. package/src/router/match-middleware/cache-store.ts +31 -11
  120. package/src/router/middleware-types.ts +6 -1
  121. package/src/router/middleware.ts +23 -4
  122. package/src/router/prerender-match.ts +6 -0
  123. package/src/router/segment-resolution/fresh.ts +18 -9
  124. package/src/router/segment-resolution/helpers.ts +4 -2
  125. package/src/router/segment-resolution/loader-cache.ts +227 -56
  126. package/src/router/segment-resolution/loader-snapshot.ts +203 -51
  127. package/src/router/segment-resolution/mask-nested.ts +136 -42
  128. package/src/router/segment-resolution/revalidation.ts +27 -10
  129. package/src/rsc/full-payload.ts +25 -1
  130. package/src/rsc/handler.ts +14 -2
  131. package/src/rsc/progressive-enhancement.ts +3 -2
  132. package/src/rsc/response-cache-serve.ts +9 -6
  133. package/src/rsc/rsc-rendering.ts +601 -181
  134. package/src/rsc/shell-build-manifest.ts +27 -9
  135. package/src/rsc/shell-capture-constants.ts +60 -6
  136. package/src/rsc/shell-capture.ts +1001 -250
  137. package/src/rsc/shell-serve.ts +316 -34
  138. package/src/rsc/types.ts +7 -0
  139. package/src/server/context.ts +219 -28
  140. package/src/server/cookie-store.ts +87 -62
  141. package/src/server/request-context.ts +263 -38
  142. package/src/ssr/index.tsx +96 -37
  143. package/src/ssr/ssr-root.tsx +9 -8
  144. package/src/testing/flight.entry.ts +14 -0
  145. package/src/testing/run-loader.ts +63 -5
  146. package/src/testing/serve-shell-request.ts +378 -0
  147. package/src/testing/shell-status.ts +34 -12
  148. package/src/theme/ThemeProvider.tsx +7 -6
  149. package/src/types/handler-context.ts +14 -1
  150. package/src/urls/pattern-types.ts +22 -18
  151. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  152. 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, KV tag-marker comparison, marker memoization, or per-tier
9
- * timeout budgets are needed here - the platform does that work. What this store
10
- * adds on top of the raw primitive is the parts Vercel does NOT give us:
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 type { RequestContext } from "../../server/request-context.js";
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 { bufferToBase64, base64ToBuffer } from "../cf/cf-base64.js";
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
- * ShellCacheEntry.handlerLiveHoles. Must round-trip: the serve side arms the
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`. Used only to run the stale-read lock
275
- * write (herd dampening) off the response path - the router already
276
- * backgrounds the actual writes. When omitted, the lock write runs detached
277
- * (fire-and-forget) instead.
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
- * // `process.env` (not `import.meta.env`, which needs a VITE_ prefix and
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 = { d, s: staleAt, e: expiresAt };
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
- if (isStale && options?.claimRevalidation !== false) {
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
- return {
826
- entry: {
827
- prelude: env.p,
828
- postponed: env.po,
829
- reactVersion: env.rv,
830
- buildVersion: env.bv,
831
- initialTheme: env.i,
832
- snapshot: env.sn,
833
- docKey: env.dk,
834
- handlerLiveHoles: env.lh,
835
- transitionWhen: env.tw,
836
- navigationOnly: env.no,
837
- createdAt: env.c,
838
- },
839
- shouldRevalidate,
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
- lh: entry.handlerLiveHoles,
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((tag) => this.cache.get(this.toStoreKey(tag, "tm"))),
1339
+ tags.map(
1340
+ (tag) =>
1341
+ prefetched?.get(tag) ??
1342
+ this.readTagMarker(tag, isolateMemo, outcomes),
1343
+ ),
923
1344
  );
924
- for (const raw of markers) {
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
- this.toStoreKey(tag, "tm"),
960
- JSON.stringify({ at: Date.now() }),
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 { d: d as unknown as CachedEntryData, s, e };
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, lh, tw, no } = raw;
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
- lh: lh === true ? true : undefined,
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
  }