@rangojs/router 0.18.0 → 0.19.1

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 +37 -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 +48 -5
  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 +126 -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 +962 -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 +67 -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 +54 -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 +202 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +209 -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 +42 -5
  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 +152 -26
  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 +263 -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 +134 -87
  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
@@ -47,7 +47,9 @@ import { encodeKV } from "../encode-kv.js";
47
47
  import { runBackground } from "./background-task.js";
48
48
  import { observePhase, PHASES } from "../router/instrument.js";
49
49
  import {
50
+ assertLoaderReadsClean,
50
51
  normalizeTags,
52
+ outsideCacheTagScope,
51
53
  recordRequestTags,
52
54
  runWithCacheTagScope,
53
55
  } from "./cache-tag.js";
@@ -58,7 +60,12 @@ import {
58
60
  type CacheExecScope,
59
61
  } from "./cache-exec-scope.js";
60
62
  import { reportCacheError } from "./cache-error.js";
61
- import { currentCacheSeq, invalidatedSince } from "./tag-invalidation.js";
63
+ import {
64
+ executionStart,
65
+ invalidatedSince,
66
+ predatesInvalidation,
67
+ type ExecutionStart,
68
+ } from "./tag-invalidation.js";
62
69
  import type { CacheItemResult } from "./types.js";
63
70
  import type { InternalHandlerContext } from "../types.js";
64
71
 
@@ -267,6 +274,12 @@ interface CacheEnvelope {
267
274
  tags: string[];
268
275
  /** RSC-encoded handle blob captured during execution, if any. */
269
276
  handles?: string;
277
+ /**
278
+ * One of `tags` was invalidated after the leader started, as the store
279
+ * tells it (another isolate, #977): the leader does not write it, and a
280
+ * follower does not serve it.
281
+ */
282
+ predates?: boolean;
270
283
  }
271
284
 
272
285
  /**
@@ -295,8 +308,8 @@ interface InFlightExecution {
295
308
  promise: Promise<CacheEnvelope>;
296
309
  /** Date.now() at registration, for IN_FLIGHT_LEADER_MAX_WAIT_MS. */
297
310
  registeredAt: number;
298
- /** currentCacheSeq() at registration, for invalidatedSince (#973). */
299
- startSeq: number;
311
+ /** Where the leader started, for invalidatedSince (#973, #977). */
312
+ start: ExecutionStart;
300
313
  }
301
314
 
302
315
  const inFlightExecutions = new Map<string, InFlightExecution>();
@@ -313,6 +326,18 @@ const IN_FLIGHT_LEADER_MAX_WAIT_MS = 15_000;
313
326
  /** Distinguishes a leader timeout from a leader rejection in raceLeader. */
314
327
  const LEADER_TIMED_OUT = Symbol("leader-timed-out");
315
328
 
329
+ /**
330
+ * @internal Reset this module's per-isolate state: the in-flight leaders a
331
+ * follower would join for up to IN_FLIGHT_LEADER_MAX_WAIT_MS, and the
332
+ * once-per-function warnings. Tests only (testing/serve-shell-request.ts
333
+ * resetShellTestState), never while a request is in flight.
334
+ */
335
+ export function resetCacheRuntimeForTests(): void {
336
+ inFlightExecutions.clear();
337
+ warnedUncachedUnderTest.clear();
338
+ warnedUnkeyableArgs.clear();
339
+ }
340
+
316
341
  /**
317
342
  * Await a leader's envelope for at most `remainingMs`. Resolves with the
318
343
  * envelope, `undefined` on leader rejection (the leader already cleared its
@@ -431,6 +456,20 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
431
456
  );
432
457
  }
433
458
 
459
+ // An entry's tags: the profile/DSL tags plus what its execution
460
+ // recorded, normalized (drops empty profile tags, matching the
461
+ // invalidate path) and deduped.
462
+ const entryTags = (recorded: Set<string>): string[] => [
463
+ ...new Set(normalizeTags([...(profile.tags ?? []), ...recorded])),
464
+ ];
465
+ // The value's Flight encode runs back inside its execution's tag scope:
466
+ // it renders the server components in the value, and a "use cache" call
467
+ // or cacheTag() among them belongs to this entry (#980).
468
+ const inTagScope = <T>(
469
+ recorded: Set<string>,
470
+ encode: () => Promise<T>,
471
+ ): Promise<T> => runWithCacheTagScope(encode, recorded).result;
472
+
434
473
  // Separate request-scoped args (ctx, a Request, the request's env) from
435
474
  // key-generating args. For those that carry route context (ctx params,
436
475
  // pathname, searchParams; a Request's URL), extract serializable values
@@ -631,124 +670,122 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
631
670
  const liveStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
632
671
  const recordKey = ownerKeyed ? useCacheRecordKey() : undefined;
633
672
  // Background revalidation — must capture handles if tainted args present.
634
- runBackground(requestCtx, async () => {
635
- // The background body's handle pushes belong to the refreshed entry
636
- // only: the foreground already replayed the stale entry's. They
637
- // reach the REQUEST's store (a handler ctx.use(Handle) binds it at
638
- // setupLoaderAccess, so a derived context with its own store never
639
- // sees them), so the capture accepts only this body's chain and
640
- // diverts what it records away from the live store. The foreground
641
- // is still rendering here (runBackground starts on the next
642
- // microtask), and its concurrent pushes are neither recorded nor
643
- // diverted (issue #684, plan 010). A root scope: the task is
644
- // detached, so an outer "use cache" body that scheduled it must not
645
- // record the refresh's pushes.
646
- const bgScope: CacheExecScope = {};
647
- const bgCapture = liveStore
648
- ? startHandleCapture(liveStore, {
649
- accept: () => isInCacheExecChain(bgScope),
650
- divert: true,
651
- key: recordKey,
652
- })
653
- : undefined;
654
-
655
- // Tainted args are NOT stamped here, in contrast to the foreground
656
- // miss path below. The args include the live HandlerContext the
657
- // still-rendering foreground holds, and INSIDE_CACHE_EXEC is a
658
- // property stamped onto that SHARED object — so for the whole
659
- // revalidation window a concurrent foreground ctx.set() /
660
- // ctx.headers.*() would throw (issue #684, plan 010). requestCtx is
661
- // never stamped anywhere for the same reason. In-body misuse IS
662
- // still guarded here: runWithCacheExecScope below follows only this
663
- // body's async chain, so a conditional cookies()/headers() read
664
- // that fires only during revalidation throws instead of baking one
665
- // user's value into the shared entry — with zero visibility to the
666
- // concurrent foreground.
667
-
668
- try {
669
- // Re-establish the request-context ALS so a "use cache" body that
670
- // reads the ambient getRequestContext() (e.g.
671
- // getRequestContext().env.ApiKey) resolves during the background
672
- // revalidation instead of throwing "called outside of a request
673
- // context". runWithRequestContext sets the store for fn's
674
- // synchronous kickoff; its async continuations inherit it.
675
- //
676
- // The span opens inside the request ALS and wraps the WHOLE task —
677
- // fn kickoff through the store write — so its platform spans nest
678
- // under rango.background and a slow or failing setItem is part of
679
- // the traced revalidation, not an untraced tail.
680
- await runWithRequestContext(requestCtx, () =>
681
- observePhase(
682
- PHASES.background("use-cache-revalidation"),
683
- async () => {
684
- const refreshArgs = args.map(refreshView);
685
- // The refresh reads its data from here on (#973).
686
- const startSeq = currentCacheSeq();
687
- const scoped = runWithCacheTagScope(() =>
688
- runWithCacheExecScope(
689
- () => fn.apply(this, refreshArgs),
690
- bgScope,
691
- ),
692
- );
693
- const freshResult = await scoped.result;
694
- bgCapture?.stop();
695
- // Merge profile/DSL tags with runtime cacheTag() tags, read
696
- // after awaiting so post-await cacheTag() calls are included.
697
- // Normalize (drops empty profile tags, matching the
698
- // invalidate path) + dedupe.
699
- const freshTags = [
700
- ...new Set(
701
- normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
702
- ),
703
- ];
704
- recordRequestTags(freshTags, requestCtx);
705
- const flightErrors: unknown[] = [];
706
- const onFlightError = (error: unknown): void => {
707
- flightErrors.push(error);
708
- };
709
- const serialized = await serializeResult(
710
- freshResult,
711
- onFlightError,
712
- );
713
- if (serialized !== null) {
714
- const encodedHandles = bgCapture
715
- ? await encodeHandles(
716
- bgCapture.capture.data,
717
- onFlightError,
718
- )
719
- : undefined;
720
- // An error row would replace the stale entry; the catch
721
- // below reports it and the stale entry keeps serving.
722
- if (flightErrors.length > 0) throw flightErrors[0];
723
- // The request invalidated one of the refreshed value's
724
- // tags after the refresh started: the value may predate
725
- // it, and written now it would outlive it.
726
- if (invalidatedSince(requestCtx, freshTags, startSeq)) {
727
- return;
673
+ // Scheduled outside the caller's tag scope: an enclosing "use cache"
674
+ // entry bakes the stale value, not the refreshed one (#980).
675
+ outsideCacheTagScope(() =>
676
+ runBackground(requestCtx, async () => {
677
+ // The background body's handle pushes belong to the refreshed entry
678
+ // only: the foreground already replayed the stale entry's. They
679
+ // reach the REQUEST's store (a handler ctx.use(Handle) binds it at
680
+ // setupLoaderAccess, so a derived context with its own store never
681
+ // sees them), so the capture accepts only this body's chain and
682
+ // diverts what it records away from the live store. The foreground
683
+ // is still rendering here (runBackground starts on the next
684
+ // microtask), and its concurrent pushes are neither recorded nor
685
+ // diverted (issue #684, plan 010). A root scope: the task is
686
+ // detached, so an outer "use cache" body that scheduled it must not
687
+ // record the refresh's pushes.
688
+ const bgScope: CacheExecScope = {};
689
+ const bgCapture = liveStore
690
+ ? startHandleCapture(liveStore, {
691
+ accept: () => isInCacheExecChain(bgScope),
692
+ divert: true,
693
+ key: recordKey,
694
+ })
695
+ : undefined;
696
+
697
+ // Tainted args are NOT stamped here, in contrast to the foreground
698
+ // miss path below. The args include the live HandlerContext the
699
+ // still-rendering foreground holds, and INSIDE_CACHE_EXEC is a
700
+ // property stamped onto that SHARED object — so for the whole
701
+ // revalidation window a concurrent foreground ctx.set() /
702
+ // ctx.headers.*() would throw (issue #684, plan 010). requestCtx is
703
+ // never stamped anywhere for the same reason. In-body misuse IS
704
+ // still guarded here: runWithCacheExecScope below follows only this
705
+ // body's async chain, so a conditional cookies()/headers() read
706
+ // that fires only during revalidation throws instead of baking one
707
+ // user's value into the shared entry — with zero visibility to the
708
+ // concurrent foreground.
709
+
710
+ try {
711
+ // Re-establish the request-context ALS so a "use cache" body that
712
+ // reads the ambient getRequestContext() (e.g.
713
+ // getRequestContext().env.ApiKey) resolves during the background
714
+ // revalidation instead of throwing "called outside of a request
715
+ // context". runWithRequestContext sets the store for fn's
716
+ // synchronous kickoff; its async continuations inherit it.
717
+ //
718
+ // The span opens inside the request ALS and wraps the WHOLE task —
719
+ // fn kickoff through the store write — so its platform spans nest
720
+ // under rango.background and a slow or failing setItem is part of
721
+ // the traced revalidation, not an untraced tail.
722
+ await runWithRequestContext(requestCtx, () =>
723
+ observePhase(
724
+ PHASES.background("use-cache-revalidation"),
725
+ async () => {
726
+ const refreshArgs = args.map(refreshView);
727
+ // The refresh reads its data from here on (#973, #977).
728
+ const start = executionStart();
729
+ const scoped = runWithCacheTagScope(() =>
730
+ runWithCacheExecScope(
731
+ () => fn.apply(this, refreshArgs),
732
+ bgScope,
733
+ ),
734
+ );
735
+ const freshResult = await scoped.result;
736
+ bgCapture?.stop();
737
+ recordRequestTags(entryTags(scoped.tags), requestCtx);
738
+ const flightErrors: unknown[] = [];
739
+ const onFlightError = (error: unknown): void => {
740
+ flightErrors.push(error);
741
+ };
742
+ const serialized = await inTagScope(scoped.tags, () =>
743
+ serializeResult(freshResult, onFlightError),
744
+ );
745
+ if (serialized !== null) {
746
+ const encodedHandles = bgCapture
747
+ ? await inTagScope(scoped.tags, () =>
748
+ encodeHandles(
749
+ bgCapture.capture.data,
750
+ onFlightError,
751
+ ),
752
+ )
753
+ : undefined;
754
+ // An error row would replace the stale entry; the catch
755
+ // below reports it and the stale entry keeps serving.
756
+ if (flightErrors.length > 0) throw flightErrors[0];
757
+ assertLoaderReadsClean(bgScope);
758
+ const freshTags = entryTags(scoped.tags);
759
+ // One of the refreshed value's tags was invalidated after
760
+ // the refresh started: written now, a value that may
761
+ // predate it would outlive it.
762
+ if (await predatesInvalidation(store, freshTags, start)) {
763
+ return;
764
+ }
765
+ await store.setItem!(cacheKey, serialized, {
766
+ handles: encodedHandles,
767
+ ttl: profile.ttl,
768
+ swr: profile.swr,
769
+ tags: freshTags.length > 0 ? freshTags : undefined,
770
+ });
728
771
  }
729
- await store.setItem!(cacheKey, serialized, {
730
- handles: encodedHandles,
731
- ttl: profile.ttl,
732
- swr: profile.swr,
733
- tags: freshTags.length > 0 ? freshTags : undefined,
734
- });
735
- }
736
- },
737
- ),
738
- );
739
- } catch (bgError) {
740
- bgCapture?.stop();
741
- // Pass requestCtx explicitly: this runs in a detached background
742
- // task where the ALS context is gone, so onError can only fire if
743
- // we hand it the context captured up front.
744
- reportCacheError(
745
- bgError,
746
- "stale-revalidation",
747
- "[use cache] background revalidation failed",
748
- requestCtx,
749
- );
750
- }
751
- });
772
+ },
773
+ ),
774
+ );
775
+ } catch (bgError) {
776
+ bgCapture?.stop();
777
+ // Pass requestCtx explicitly: this runs in a detached background
778
+ // task where the ALS context is gone, so onError can only fire if
779
+ // we hand it the context captured up front.
780
+ reportCacheError(
781
+ bgError,
782
+ "stale-revalidation",
783
+ "[use cache] background revalidation failed",
784
+ requestCtx,
785
+ );
786
+ }
787
+ }),
788
+ );
752
789
  return result;
753
790
  } catch (error) {
754
791
  // Stale value is corrupt/partial; report and fall through to a fresh
@@ -797,7 +834,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
797
834
  // Leader rejected: its map entry is already cleared; run fresh.
798
835
  if (!raced) break;
799
836
  // Checked after the wait, so an invalidation made meanwhile counts too.
800
- if (!invalidatedSince(requestCtx, raced.tags, existing.startSeq)) {
837
+ if (
838
+ !raced.predates &&
839
+ !invalidatedSince(raced.tags, existing.start.seq)
840
+ ) {
801
841
  try {
802
842
  return await serveCached({
803
843
  value: raced.serialized,
@@ -814,11 +854,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
814
854
  break; // Run fresh below.
815
855
  }
816
856
  }
817
- // This request invalidated one of the leader's tags after it started
818
- // (#973): its value may predate the invalidation. The leader cleared
819
- // its entry before resolving, so an entry now is a newer execution
820
- // (another caller that made this same decision): join it rather than
821
- // start one more.
857
+ // One of the leader's tags was invalidated after it started, in any
858
+ // request or, as the store tells it, another isolate (#973, #977): its
859
+ // value may predate the invalidation. The leader cleared its entry
860
+ // before resolving, so an entry now is a newer execution (another
861
+ // caller that made this same decision): join it rather than start one
862
+ // more.
822
863
  const next = inFlightExecutions.get(cacheKey);
823
864
  existing = next !== existing ? next : undefined;
824
865
  }
@@ -837,11 +878,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
837
878
  // Followers attach their own catch; guard the map's own reference so a
838
879
  // rejected envelope with no waiter is not an unhandled rejection.
839
880
  envelopePromise.catch(() => {});
840
- const startSeq = currentCacheSeq();
881
+ const start = executionStart();
841
882
  inFlightExecutions.set(cacheKey, {
842
883
  promise: envelopePromise,
843
884
  registeredAt: Date.now(),
844
- startSeq,
885
+ start,
845
886
  });
846
887
  const clearSelf = (): void => {
847
888
  if (inFlightExecutions.get(cacheKey)?.promise === envelopePromise) {
@@ -904,13 +945,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
904
945
  capture?.stop();
905
946
  }
906
947
 
907
- // Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
908
- // after awaiting result so post-await cacheTag() calls are included.
909
- // Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
910
- const allTags = [
911
- ...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
912
- ];
913
- recordRequestTags(allTags, requestCtx);
948
+ // Read scoped.tags after awaiting result so post-await cacheTag() calls
949
+ // are included.
950
+ const recorded = scoped!.tags;
951
+ recordRequestTags(entryTags(recorded), requestCtx);
914
952
 
915
953
  // Serialize + encode handles ONCE, resolve the in-flight envelope so any
916
954
  // concurrent followers can serve a synthetic hit, then persist to the store.
@@ -924,14 +962,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
924
962
  flightErrors.push(error);
925
963
  };
926
964
  try {
927
- serialized = await serializeResult(result, onFlightError);
965
+ serialized = await inTagScope(recorded, () =>
966
+ serializeResult(result, onFlightError),
967
+ );
928
968
  encodedHandles = capture
929
- ? await encodeHandles(capture.capture.data, onFlightError)
969
+ ? await inTagScope(recorded, () =>
970
+ encodeHandles(capture.capture.data, onFlightError),
971
+ )
930
972
  : undefined;
931
973
  // Flight encodes an async component that throws, or a rejected
932
974
  // promise in the result or a handle value, as an error row and
933
975
  // completes normally; stored, every hit would serve that error.
934
976
  if (flightErrors.length > 0) throw flightErrors[0];
977
+ // The encode awaited the result's nested promises: an identity read a
978
+ // loader value made after it settled is recorded now (#1011).
979
+ assertLoaderReadsClean(execScope);
935
980
  } catch (buildError) {
936
981
  // Serialize/handle-encode failed: no envelope for followers (they run
937
982
  // fresh) and nothing to write.
@@ -940,8 +985,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
940
985
  requestCtx?._reportBackgroundError?.(buildError, "cache-write");
941
986
  return;
942
987
  }
943
- clearSelf();
944
988
  if (serialized === null) {
989
+ clearSelf();
945
990
  // Non-serializable result: no store write (matches the prior silent
946
991
  // skip); reject so any waiter falls through to a fresh execution.
947
992
  rejectEnvelope(
@@ -951,19 +996,32 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
951
996
  );
952
997
  return;
953
998
  }
999
+ const tags = entryTags(recorded);
1000
+ // One of the value's tags was invalidated after the execution started
1001
+ // (#973, #977): written now, a value that may predate it would outlive
1002
+ // it. Asked before the followers get the value: another isolate's
1003
+ // invalidation is only in the store's answer, and a follower serving
1004
+ // the value would bake it into its own entries. Yes also when the store
1005
+ // cannot read a marker (predatesInvalidation asks it fail-closed) or
1006
+ // throws: the cost is a later miss.
1007
+ let predates = true;
1008
+ try {
1009
+ predates = await predatesInvalidation(store, tags, start);
1010
+ } catch (gateError) {
1011
+ requestCtx?._reportBackgroundError?.(gateError, "cache-write");
1012
+ }
1013
+ clearSelf();
954
1014
  // Hand followers the envelope before the store write so a slow/failed
955
1015
  // write never stalls them.
956
- resolveEnvelope({ serialized, tags: allTags, handles: encodedHandles });
957
- // This request invalidated one of the value's tags after the execution
958
- // started (#973): the value may predate the invalidation, and written
959
- // now it would outlive it. Skip the write; the next read runs fresh.
960
- if (invalidatedSince(requestCtx, allTags, startSeq)) return;
1016
+ resolveEnvelope({ serialized, tags, handles: encodedHandles, predates });
1017
+ // Skipped, the next read runs fresh.
1018
+ if (predates) return;
961
1019
  try {
962
1020
  await store.setItem!(cacheKey, serialized, {
963
1021
  handles: encodedHandles,
964
1022
  ttl: profile.ttl,
965
1023
  swr: profile.swr,
966
- tags: allTags.length > 0 ? allTags : undefined,
1024
+ tags: tags.length > 0 ? tags : undefined,
967
1025
  });
968
1026
  } catch (writeError) {
969
1027
  requestCtx?._reportBackgroundError?.(writeError, "cache-write");