@rangojs/router 0.18.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.
Files changed (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -22,9 +22,33 @@ import {
22
22
  type LoaderIdentityRead,
23
23
  type LoaderIdentityReadVerb,
24
24
  } from "../server/context.js";
25
+ import type { ExecutionStart } from "./tag-invalidation.js";
25
26
 
26
27
  const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
27
28
 
29
+ /**
30
+ * Each "use cache" tag scope's enclosing scope when it opened (#980). A tag
31
+ * recorded in a scope goes to every scope above it: an intermediate call can
32
+ * return a value holding a nested call still running, whose tags land after
33
+ * the intermediate call reported its own to its caller. Its caller's encode
34
+ * awaits the same promise, so the tags are in before it reads its own.
35
+ */
36
+ const scopeParents = new WeakMap<Set<string>, Set<string>>();
37
+
38
+ /** Add `tag` to `scope` and every scope enclosing it. */
39
+ function addToScopes(scope: Set<string> | undefined, tag: string): void {
40
+ for (let s = scope; s; s = scopeParents.get(s)) s.add(tag);
41
+ }
42
+
43
+ /**
44
+ * Call `fn` outside any "use cache" tag scope: detached work a cached body
45
+ * schedules (a stale entry's background refresh) records nothing onto the
46
+ * entry being filled.
47
+ */
48
+ export function outsideCacheTagScope<T>(fn: () => T): T {
49
+ return cacheTagStorage.exit(fn);
50
+ }
51
+
28
52
  /**
29
53
  * Tag owners (#957). A route cache() record replays its segments without
30
54
  * running their handlers or re-rendering their server components, so the
@@ -287,7 +311,7 @@ export function cacheTag(...tags: string[]): void {
287
311
  }
288
312
  continue;
289
313
  }
290
- store.add(normalized);
314
+ addToScopes(store, normalized);
291
315
  }
292
316
  return;
293
317
  }
@@ -457,21 +481,76 @@ export function recordedIdentityRead(
457
481
  return undefined;
458
482
  }
459
483
 
460
- /** `root`'s tags plus those of every set it links to, transitively. */
461
- export function flattenRecordedTags(root: Set<string>): Set<string> {
462
- const out = new Set<string>();
484
+ /** `root` and every set it links to, transitively. */
485
+ function linkedSets(root: Set<string>): Set<Set<string>> {
463
486
  const seen = new Set<Set<string>>();
464
487
  const pending = [root];
465
488
  for (let set = pending.pop(); set !== undefined; set = pending.pop()) {
466
489
  if (seen.has(set)) continue;
467
490
  seen.add(set);
468
- for (const tag of set) out.add(tag);
469
491
  for (const linked of tagLinks.get(set) ?? []) pending.push(linked);
470
492
  }
493
+ return seen;
494
+ }
495
+
496
+ /** `root`'s tags plus those of every set it links to, transitively. */
497
+ export function flattenRecordedTags(root: Set<string>): Set<string> {
498
+ const out = new Set<string>();
499
+ for (const set of linkedSets(root)) {
500
+ for (const tag of set) out.add(tag);
501
+ }
471
502
  return out;
472
503
  }
473
504
 
474
- /** recordRequestTags, also recording onto `owner`'s set (#957). */
505
+ /**
506
+ * Where each execution that records into a set started (#977): a loader
507
+ * execution (createLoaderExecutor), a loader-cache MISS or refresh, a
508
+ * loader-cache binding's value.
509
+ */
510
+ const tagSetStarts = new WeakMap<Set<string>, ExecutionStart>();
511
+
512
+ /** Record where the execution recording into `set` started. */
513
+ export function markTagSetStart(set: Set<string>, start: ExecutionStart): void {
514
+ tagSetStarts.set(set, start);
515
+ }
516
+
517
+ /**
518
+ * The earliest start among `root` and every set it links to: a value built
519
+ * from another execution's value is as old as the oldest of them. A
520
+ * loader-cache write gates on it (loader-cache.ts), because the loader value
521
+ * it stores can be a run a reader started before the binding executed, and
522
+ * the values that run read can be older still.
523
+ */
524
+ export function earliestRecordedStart(
525
+ root: Set<string>,
526
+ ): ExecutionStart | undefined {
527
+ let earliest: ExecutionStart | undefined;
528
+ for (const set of linkedSets(root)) {
529
+ const start = tagSetStarts.get(set);
530
+ if (!start) continue;
531
+ earliest = earliest
532
+ ? {
533
+ seq: Math.min(earliest.seq, start.seq),
534
+ at: Math.min(earliest.at, start.at),
535
+ }
536
+ : start;
537
+ }
538
+ return earliest;
539
+ }
540
+
541
+ /**
542
+ * recordRequestTags, also recording onto `owner`'s set (#957), the active
543
+ * loader tag set (#964), and the enclosing "use cache" execution's tag
544
+ * scope (#980).
545
+ *
546
+ * The last one is how a nested "use cache" call's tags reach the entry that
547
+ * bakes its value: the inner wrapper records its tags (a miss's, a hit's
548
+ * stored ones, a joined execution's) after its own scope closed, so the
549
+ * scope current then is the caller's. Without it an outer entry kept the
550
+ * inner value past updateTag() of an inner tag, and the same-request gate
551
+ * (tag-invalidation.ts invalidatedSince) did not see the outer execution
552
+ * as tagged by it.
553
+ */
475
554
  function recordOwnedTags(
476
555
  tags: Iterable<string> | undefined,
477
556
  ctx: RequestContext | undefined,
@@ -480,12 +559,14 @@ function recordOwnedTags(
480
559
  if (!tags || !ctx?._requestTags) return;
481
560
  const set = ctx._requestTags;
482
561
  const captured = activeTagSet();
562
+ const enclosing = cacheTagStorage.getStore();
483
563
  let owned: Set<string> | undefined;
484
564
  for (const tag of tags) {
485
565
  const normalized = normalizeTag(tag);
486
566
  if (normalized === null) continue;
487
567
  set.add(normalized);
488
568
  captured?.add(normalized);
569
+ addToScopes(enclosing, normalized);
489
570
  if (owner !== undefined) {
490
571
  owned ??= setFor(ownersFor(ctx).tags, owner);
491
572
  owned.add(normalized);
@@ -494,20 +575,31 @@ function recordOwnedTags(
494
575
  }
495
576
 
496
577
  /**
497
- * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
498
- * accumulate into the returned Set.
578
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`,
579
+ * and the tags a nested "use cache" call records (recordOwnedTags, #980),
580
+ * accumulate into the returned Set and every scope enclosing it
581
+ * (scopeParents).
499
582
  *
500
583
  * The returned Set is the LIVE reference - the caller must await `result`
501
584
  * before reading `tags`, because an async cached function may call cacheTag()
502
- * after an await boundary.
585
+ * after an await boundary. Pass `tagSet` to re-enter an execution's scope
586
+ * (its result's Flight encode, #980); it keeps the parent it opened under.
503
587
  *
504
588
  * @internal Used by cache-runtime.ts to wrap "use cache" execution.
505
589
  */
506
- export function runWithCacheTagScope<T>(fn: () => T): {
590
+ export function runWithCacheTagScope<T>(
591
+ fn: () => T,
592
+ tagSet?: Set<string>,
593
+ ): {
507
594
  result: T;
508
595
  tags: Set<string>;
509
596
  } {
510
- const tagSet = new Set<string>();
511
- const result = cacheTagStorage.run(tagSet, fn);
512
- return { result, tags: tagSet };
597
+ let scope = tagSet;
598
+ if (!scope) {
599
+ scope = new Set<string>();
600
+ const parent = cacheTagStorage.getStore();
601
+ if (parent) scopeParents.set(scope, parent);
602
+ }
603
+ const result = cacheTagStorage.run(scope, fn);
604
+ return { result, tags: scope };
513
605
  }
@@ -27,6 +27,7 @@ declare global {
27
27
  * - KV L2 for cross-colo cache persistence
28
28
  */
29
29
 
30
+ import { requestHeaders } from "../../server/request-headers.js";
30
31
  import type {
31
32
  SegmentCacheStore,
32
33
  CachedEntryData,
@@ -37,6 +38,7 @@ import type {
37
38
  ShellCacheEntry,
38
39
  ShellDocumentRead,
39
40
  ShellDocumentReadOptions,
41
+ ShellEntryHead,
40
42
  ShellReadStats,
41
43
  ShellSnapshotFailure,
42
44
  ShellSnapshotRecord,
@@ -99,9 +101,12 @@ import {
99
101
  getTagMarkerMemo,
100
102
  getTagMarkerInflight,
101
103
  getShellMarkerReads,
102
- isRequestMasked,
103
- maskRequestTags,
104
104
  } from "./cf-tag-marker-memo.js";
105
+ import {
106
+ gateMarkerRead,
107
+ maskRequestTags,
108
+ maskedForRequest,
109
+ } from "../request-tag-mask.js";
105
110
  import {
106
111
  TagMarkerMemo,
107
112
  TagNameHints,
@@ -353,12 +358,21 @@ function isMemoizableShell(head: ShellFrameHead): boolean {
353
358
  );
354
359
  }
355
360
 
356
- /** @internal Reset the per-isolate shell and marker memos (tests). */
361
+ /**
362
+ * @internal Reset the per-isolate shell and marker memos and the once-per-
363
+ * isolate warnings (tests).
364
+ */
357
365
  export function resetCFShellMemoForTests(): void {
358
366
  cfShellMemo.clear();
359
367
  recentShellInvalidations.clear();
360
368
  cfMarkerMemo.clear();
361
369
  cfTagHints.clear();
370
+ warnedNoKvReadInvalidation.clear();
371
+ warnedTagInvalidationTtlFloor.clear();
372
+ warnedShellTagsNoEviction.clear();
373
+ warnedNoTagInvalidationTtl.clear();
374
+ warnedCacheTagHeaderOverflow.clear();
375
+ warnedCacheTagOverflowUncacheable.clear();
362
376
  }
363
377
 
364
378
  /** openShellFrame outcome: the head and prelude, or why the read failed. */
@@ -397,7 +411,7 @@ function debugTimings(
397
411
  }
398
412
 
399
413
  /** The public entry shape of a frame head (no prelude, no snapshot). */
400
- function shellHeadToEntry(head: ShellFrameHead): ShellCacheEntry {
414
+ function shellHeadToEntry(head: ShellFrameHead): ShellEntryHead {
401
415
  return {
402
416
  ...(head.po !== undefined ? { postponed: head.po } : {}),
403
417
  reactVersion: head.rv,
@@ -583,7 +597,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
583
597
  this.waitUntil = (fn) => options.ctx.waitUntil(fn());
584
598
  this.kv = options.kv;
585
599
  // Without KV, isTagsInvalidatedSince has no durable history — it answers
586
- // from the per-request memo at best. Runtime shells tolerate that (purge
600
+ // from the per-request mask at best. Runtime shells tolerate that (purge
587
601
  // eviction + ttl/swr bound the staleness), but an immutable TAGGED
588
602
  // build-manifest shell must not serve on such a store: nothing could ever
589
603
  // evict it (purge cannot delete a build asset), so the manifest gate
@@ -712,7 +726,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
712
726
  const request = _getRequestContext()?.request as
713
727
  | (Request & { cf?: { colo?: unknown } })
714
728
  | undefined;
715
- const ray = request?.headers.get("cf-ray") ?? undefined;
729
+ const ray = request
730
+ ? (requestHeaders(request).get("cf-ray") ?? undefined)
731
+ : undefined;
716
732
  const cfColo = request?.cf?.colo;
717
733
  const colo =
718
734
  typeof cfColo === "string"
@@ -2008,7 +2024,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2008
2024
  // serves its own shell from the Cache API. Tag eviction then needs purge
2009
2025
  // mode (tagPurge) — shell L1 entries carry the same namespaced Cache-Tag
2010
2026
  // tokens as the data families, read-your-own-writes comes from the
2011
- // per-request marker memo, and the capture resurrection race narrows to
2027
+ // per-request tag mask, and the capture resurrection race narrows to
2012
2028
  // cross-request timing bounded by ttl+swr (the data families' documented
2013
2029
  // purge-mode stance). KV-less WITHOUT tagPurge still caches: freshness is
2014
2030
  // ttl/swr only, and a tagged write warns once that invalidation cannot
@@ -2038,7 +2054,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2038
2054
  /**
2039
2055
  * Generation gate for shell writes and the capture scheduler
2040
2056
  * (isTagsInvalidatedSince). With KV it is the durable marker cascade.
2041
- * Without KV there are no markers: in purge mode the per-request memo is
2057
+ * Without KV there are no markers: in purge mode the per-request mask is
2042
2058
  * the only signal — a capture racing THIS request's updateTag() is still
2043
2059
  * rejected (read-your-own-writes), while cross-request races are bounded
2044
2060
  * by ttl+swr exactly like the data families' purge-mode writes. Without
@@ -2052,14 +2068,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2052
2068
  if (this.kv) return this.isGloballyInvalidated(tags, since);
2053
2069
  if (!this.tagPurge || !Array.isArray(tags) || tags.length === 0 || !since)
2054
2070
  return false;
2055
- const ctx = _getRequestContext();
2056
- if (!ctx) return false;
2057
- const memo = getTagMarkerMemo(ctx, this);
2058
- for (const tag of tags) {
2059
- const marker = memo.get(tag);
2060
- if (marker != null && marker >= since) return true;
2061
- }
2062
- return false;
2071
+ return maskedForRequest(_getRequestContext(), this, tags, since);
2063
2072
  }
2064
2073
 
2065
2074
  /**
@@ -2082,9 +2091,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2082
2091
  if (!read) return null;
2083
2092
  const snapshot = await read.snapshot;
2084
2093
  if (await read.snapshotFailure) return null;
2085
- const entry: ShellCacheEntry = { ...read.entry };
2094
+ // A frame with no snapshot bytes carries no records.
2095
+ const entry: ShellCacheEntry = { ...read.entry, snapshot: snapshot ?? [] };
2086
2096
  if (!entry.navigationOnly) entry.prelude = bytesToBase64(read.prelude);
2087
- if (snapshot) entry.snapshot = snapshot;
2088
2097
  return { entry, shouldRevalidate: read.shouldRevalidate };
2089
2098
  }
2090
2099
 
@@ -2179,7 +2188,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
2179
2188
  // generation marker — see the capture-start/purge race documented
2180
2189
  // above. Without KV there are no markers: L1-only shells adopt the
2181
2190
  // data families' purge-mode read semantics (a hit that survived the
2182
- // purge is trusted; the per-request memo masks this request's own
2191
+ // purge is trusted; the per-request mask covers this request's own
2183
2192
  // updateTag() writes; entries a purge cannot reach fall back to the
2184
2193
  // marker check, which fails open KV-less).
2185
2194
  const opened = await this.openShellFrame(
@@ -3239,6 +3248,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3239
3248
  if (!this.kv || !Array.isArray(tags) || tags.length === 0 || !taggedAt)
3240
3249
  return false;
3241
3250
  const ctx = _getRequestContext();
3251
+ // This request's own invalidations first (#973): no marker read needed.
3252
+ if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
3242
3253
  const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
3243
3254
  const inflight = ctx ? getTagMarkerInflight(ctx, this) : undefined;
3244
3255
  try {
@@ -3249,6 +3260,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3249
3260
  : this.readTagMarker(tag, memo, inflight),
3250
3261
  ),
3251
3262
  );
3263
+ // A read in flight when this request invalidated one of the tags (a
3264
+ // sibling segment's updateTag()) resolved to the marker before it.
3265
+ if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
3252
3266
  for (const marker of markers) {
3253
3267
  if (marker != null && marker >= taggedAt) return true;
3254
3268
  }
@@ -3268,16 +3282,16 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3268
3282
  * configured), invalidateTags() evicts L1 entries via a Cloudflare
3269
3283
  * purge-by-tag call, so a hit that SURVIVED is trusted without a per-read
3270
3284
  * marker lookup — that skipped lookup is the entire point of purge mode.
3271
- * Only the per-request memo is consulted (synchronous, no KV read) so a
3272
- * request that ran updateTag() still masks its own entries during the purge
3273
- * propagation window (read-your-own-writes).
3285
+ * Only the request's mask and its memo are consulted (synchronous, no KV
3286
+ * read) so a request that ran updateTag() still misses its own entries
3287
+ * during the purge propagation window (read-your-own-writes).
3274
3288
  *
3275
3289
  * The trust is conditional on the entry actually CARRYING this store's
3276
3290
  * entry Cache-Tag tokens (`headers`): an entry a purge cannot reach — one
3277
3291
  * written before the tokens existed, or whose tag set overflowed the
3278
3292
  * Cache-Tag header limit — keeps the full marker check, or purge mode
3279
3293
  * would serve it stale until TTL with no eviction path. KV-less there are
3280
- * no markers, so it gets the per-request memo check like any hit (#973).
3294
+ * no markers, so it gets the per-request mask check like any hit (#973).
3281
3295
  *
3282
3296
  * Without tagPurge this is the full marker cascade. KV-tier reads and
3283
3297
  * shells always use isGloballyInvalidated directly: purge cannot reach KV,
@@ -3296,6 +3310,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3296
3310
  }
3297
3311
  const ctx = _getRequestContext();
3298
3312
  if (!ctx) return false;
3313
+ if (maskedForRequest(ctx, this, tags, taggedAt)) return true;
3314
+ // Markers this request read from KV for other entries.
3299
3315
  const memo = getTagMarkerMemo(ctx, this);
3300
3316
  for (const tag of tags) {
3301
3317
  const marker = memo.get(tag);
@@ -3312,9 +3328,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3312
3328
  /**
3313
3329
  * Read a tag's latest invalidation timestamp (or null if never invalidated)
3314
3330
  * through the cascade: per-request memo -> per-colo L1 cache (only when
3315
- * tagCacheTtl > 0) -> KV (the global truth). The memo is always consulted
3316
- * first so it stays authoritative within a request (read-your-own-writes),
3317
- * and every KV/L1 result is written back into the memo. A Cache API miss
3331
+ * tagCacheTtl > 0) -> KV (the global truth). Every KV/L1 result is written
3332
+ * back into the memo, so the request reads one value per tag. A Cache API miss
3318
3333
  * always falls through to KV; absence is represented by a cached sentinel,
3319
3334
  * never by a miss.
3320
3335
  *
@@ -3348,9 +3363,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3348
3363
  }
3349
3364
 
3350
3365
  /**
3351
- * A PPR shell read's marker for `tag`: this request's own value if it has
3352
- * one (its invalidateTags() writes, or a store read), else through the
3353
- * per-isolate marker memo (isolate-tag-memo.ts, stale-while-revalidate
3366
+ * A PPR shell read's marker for `tag`: this request's own store read if it
3367
+ * has one, else through the per-isolate marker memo (isolate-tag-memo.ts,
3368
+ * stale-while-revalidate
3354
3369
  * under `markerFreshMs` / `markerMaxStaleMs`; a request carrying the
3355
3370
  * fresh-reads cookie reads L1/KV). The value is kept in the request's
3356
3371
  * shell-read record, never in the per-request memo the data families read,
@@ -3368,16 +3383,13 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3368
3383
  const read = cfMarkerMemo.readThrough(
3369
3384
  this.markerMemoKey(tag),
3370
3385
  async (background) => {
3371
- const outcome = { timedOut: false, fromRequest: false };
3386
+ const outcome = { timedOut: false, masked: false };
3372
3387
  const value = await this.fetchTagMarker(
3373
3388
  tag,
3374
3389
  background ? undefined : memo,
3375
3390
  outcome,
3376
3391
  );
3377
- return {
3378
- value,
3379
- memoize: !outcome.timedOut && !outcome.fromRequest,
3380
- };
3392
+ return { value, memoize: !outcome.timedOut && !outcome.masked };
3381
3393
  },
3382
3394
  {
3383
3395
  freshMs: this.memo.markerFreshMs,
@@ -3404,31 +3416,16 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3404
3416
  private async fetchTagMarker(
3405
3417
  tag: string,
3406
3418
  memo: Map<string, number | null> | undefined,
3407
- read?: { timedOut: boolean; fromRequest?: boolean },
3419
+ read?: { timedOut: boolean; masked?: boolean },
3420
+ /** false: populate no L1 marker (isInvalidatedSincePastMemo). */
3421
+ publish = true,
3408
3422
  ): Promise<number | null> {
3409
- // Write the resolved marker into the memo WITHOUT clobbering a value a
3410
- // concurrent invalidateTags() wrote during our await. The router resolves
3411
- // sibling slots in parallel, so a slot's updateTag() can land its
3412
- // invalidatedAt into the memo while this read is still in flight;
3413
- // overwriting it with our (pre-invalidation) read result would break
3414
- // read-your-own-writes for the rest of the request. If the tag was memoized
3415
- // mid-read, that value wins and is returned. Without a memo, the read result
3416
- // stands as-is.
3417
- //
3418
- // A mask memoized mid-read stays in this request: maskTagsForRequest
3419
- // wrote it before the KV put was confirmed, and a failed put must not
3420
- // reach other requests (#973). So it is not written to L1 here, and
3421
- // `read.fromRequest` keeps the shell read from storing it in the isolate
3422
- // marker memo; writeTagInvalidation writes both after the put. A value
3423
- // another read memoized mid-read came from KV or L1 and is published.
3424
- let fromRequest = false;
3423
+ // Write the resolved marker into the memo WITHOUT clobbering a value
3424
+ // another read of the tag memoized during our await: that value wins and
3425
+ // is returned, so the request keeps one value per tag. Without a memo,
3426
+ // the read result stands.
3425
3427
  const memoize = (value: number | null): number | null => {
3426
- if (memo && memo.has(tag)) {
3427
- const ctx = _getRequestContext();
3428
- fromRequest = ctx !== undefined && isRequestMasked(ctx, this, tag);
3429
- if (read) read.fromRequest = fromRequest;
3430
- return memo.get(tag) ?? null;
3431
- }
3428
+ if (memo && memo.has(tag)) return memo.get(tag) ?? null;
3432
3429
  memo?.set(tag, value);
3433
3430
  return value;
3434
3431
  };
@@ -3486,10 +3483,17 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3486
3483
  const value = raw != null ? Number(raw) : null;
3487
3484
  const resolved = memoize(value);
3488
3485
 
3489
- // Populate L1 for subsequent reads in this colo (non-blocking), from KV's
3490
- // answer only. A value memoized mid-read is skipped: KV's answer predates
3491
- // it, and the memo's may be an unconfirmed mask (see memoize above).
3492
- if (this.tagCacheTtl > 0 && !fromRequest) {
3486
+ // A value read while this request invalidates the tag may predate the
3487
+ // invalidation's KV put: an L1 put of it could land after the put's own
3488
+ // write-through and hide the invalidation from the colo. The request
3489
+ // itself answers from its mask (#973). A shell read also skips the isolate
3490
+ // marker memo through `read.masked`; that memo keeps the higher value, so
3491
+ // the skip is conservative there.
3492
+ const masked = maskedForRequest(_getRequestContext(), this, [tag], 0);
3493
+ if (read) read.masked = masked;
3494
+
3495
+ // Populate L1 for subsequent reads in this colo (non-blocking).
3496
+ if (this.tagCacheTtl > 0 && !masked && publish) {
3493
3497
  const put = () => this.putTagMarkerL1(tag, resolved);
3494
3498
  if (this.waitUntil) this.waitUntil(put);
3495
3499
  else void put();
@@ -3665,18 +3669,93 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3665
3669
  }
3666
3670
 
3667
3671
  /**
3668
- * Shell tag-generation gate (SegmentCacheStore.isTagsInvalidatedSince): the
3669
- * SAME KV markers used by runtime envelopes also evict immutable build shells
3670
- * and captures whose write races updateTag(). Thin public wrapper over the
3671
- * shell generation check (marker >= since, fail open); KV-less it degrades
3672
- * to the per-request memo in purge mode and to false otherwise — see
3672
+ * SegmentCacheStore.isTagsInvalidatedSince: the SAME KV markers used by
3673
+ * runtime envelopes also evict immutable build shells, and gate the writes
3674
+ * of cache executions that started before an invalidation (#977,
3675
+ * tag-invalidation.ts predatesInvalidation). Marker >= since. A marker read
3676
+ * that fails or times out answers false (fail open), or true for the write
3677
+ * gate (`failClosed`). With KV each marker is read past the per-request
3678
+ * memo (isInvalidatedSincePastMemo); KV-less it degrades to the per-request
3679
+ * mask in purge mode and to false otherwise — see
3673
3680
  * isShellGenerationInvalidated.
3674
3681
  */
3675
3682
  async isTagsInvalidatedSince(
3676
3683
  tags: string[],
3677
3684
  sinceMs: number,
3685
+ options?: { failClosed?: boolean },
3686
+ ): Promise<boolean> {
3687
+ if (!this.kv) return this.isShellGenerationInvalidated(tags, sinceMs);
3688
+ return this.isInvalidatedSincePastMemo(
3689
+ tags,
3690
+ sinceMs,
3691
+ options?.failClosed === true,
3692
+ );
3693
+ }
3694
+
3695
+ /**
3696
+ * isTagsInvalidatedSince with KV, never answered from a marker value this
3697
+ * request read before the asking gate. A write gate asks after an
3698
+ * execution that can run for seconds, and the per-request memo can hold
3699
+ * the marker the request read before it (a stale hit reads its entry's
3700
+ * markers, then its refresh runs): answered from it, another isolate's
3701
+ * invalidation in between let the refresh write.
3702
+ *
3703
+ * - This request's own invalidations answer from its mask, checked again
3704
+ * once the reads are back: a read in flight when the request invalidated
3705
+ * one of the tags resolved to the marker before it.
3706
+ * - Each tag is read through gateMarkerRead (request-tag-mask.ts): gates
3707
+ * asking while a read is in flight share it; a settled read is not
3708
+ * reused.
3709
+ * - The read returns its own value from L1 (tagCacheTtl) or KV, not one an
3710
+ * older read of the tag memoized while it was in flight, and fills the
3711
+ * memo only when it holds nothing, so a build shell's check still serves
3712
+ * the request's later reads. A timed-out read fills nothing.
3713
+ * - It publishes to L1 unless the request masked the tag
3714
+ * (fetchTagMarker), and never without a request context (a detached
3715
+ * waitUntil task), where a mask cannot be told apart.
3716
+ * - A read that fails or times out answers `failClosed`: the write gate
3717
+ * counts it as an invalidation, other callers as none.
3718
+ * @internal
3719
+ */
3720
+ private async isInvalidatedSincePastMemo(
3721
+ tags: string[],
3722
+ since: number,
3723
+ failClosed: boolean,
3678
3724
  ): Promise<boolean> {
3679
- return this.isShellGenerationInvalidated(tags, sinceMs);
3725
+ if (!Array.isArray(tags) || tags.length === 0 || !since) return false;
3726
+ const ctx = _getRequestContext();
3727
+ if (maskedForRequest(ctx, this, tags, since)) return true;
3728
+ const memo = ctx ? getTagMarkerMemo(ctx, this) : undefined;
3729
+ try {
3730
+ const reads = await Promise.all(
3731
+ tags.map((tag) =>
3732
+ gateMarkerRead(ctx, this, tag, since, async () => {
3733
+ const outcome = { timedOut: false };
3734
+ const marker = await this.fetchTagMarker(
3735
+ tag,
3736
+ undefined,
3737
+ outcome,
3738
+ ctx !== undefined,
3739
+ );
3740
+ if (outcome.timedOut) return { marker, answered: false };
3741
+ if (memo && !memo.has(tag)) memo.set(tag, marker);
3742
+ return { marker, answered: true };
3743
+ }),
3744
+ ),
3745
+ );
3746
+ if (maskedForRequest(ctx, this, tags, since)) return true;
3747
+ return reads.some(
3748
+ ({ marker, answered }) =>
3749
+ (!answered && failClosed) || (marker != null && marker >= since),
3750
+ );
3751
+ } catch (error) {
3752
+ reportCacheError(
3753
+ error,
3754
+ "cache-read",
3755
+ "[CFCacheStore] tag invalidation check",
3756
+ );
3757
+ return failClosed;
3758
+ }
3680
3759
  }
3681
3760
 
3682
3761
  /**
@@ -3730,14 +3809,11 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3730
3809
 
3731
3810
  /**
3732
3811
  * The request-local half of invalidateTags(), run before its first await
3733
- * (#973): this request's marker memo takes the new marker, so every later
3812
+ * (#973): the request masks the tags (request-tag-mask.ts), so every later
3734
3813
  * read in the request (data families, purge-mode L1 hits, shell reads)
3735
- * treats the tags as invalidated at `invalidatedAt` without waiting for the
3736
- * KV put or the purge that revalidateTag() leaves to waitUntil. Written
3737
- * before the durable write is confirmed: if that write fails, this request
3738
- * pays extra misses, never a stale read, and later requests read the
3739
- * durable state. Skipped where no read consults the memo (neither KV nor
3740
- * tagPurge).
3814
+ * treats them as invalidated at `invalidatedAt` without waiting for the KV
3815
+ * put or the purge that revalidateTag() leaves to waitUntil. Skipped where
3816
+ * no read consults the mask (neither KV nor tagPurge).
3741
3817
  */
3742
3818
  private maskTagsForRequest(tags: string[], invalidatedAt: number): void {
3743
3819
  if (!this.kv && !this.tagPurge) return;
@@ -3798,8 +3874,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
3798
3874
  );
3799
3875
 
3800
3876
  // Write-through (isolate marker memo + L1) only for tags with a
3801
- // confirmed durable marker; this request's own memo was written up
3802
- // front (maskTagsForRequest). The L1 Cache API writes are independent,
3877
+ // confirmed durable marker; this request's own mask was set up front
3878
+ // (maskTagsForRequest). The L1 Cache API writes are independent,
3803
3879
  // so fan them out in parallel rather than awaiting each.
3804
3880
  const l1Writes: Promise<void>[] = [];
3805
3881
  for (const tag of tags) {
@@ -34,7 +34,7 @@ export interface ShellFrameHead {
34
34
  /** ShellCacheEntry.reactVersion */
35
35
  rv: string;
36
36
  /** ShellCacheEntry.buildVersion */
37
- bv?: string;
37
+ bv: string;
38
38
  /** Capture-generation start time (ms epoch), used by tag marker checks. */
39
39
  c: number;
40
40
  /** When the entry becomes stale (ms epoch). */
@@ -70,7 +70,7 @@ function isShellFrameHead(value: unknown): value is ShellFrameHead {
70
70
  const head = value as Partial<ShellFrameHead>;
71
71
  return (
72
72
  typeof head.rv === "string" &&
73
- (head.bv === undefined || typeof head.bv === "string") &&
73
+ typeof head.bv === "string" &&
74
74
  isFiniteNumber(head.c) &&
75
75
  isFiniteNumber(head.s) &&
76
76
  isFiniteNumber(head.e) &&