@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
@@ -6,7 +6,8 @@
6
6
  * getItem()/setItem() methods on SegmentCacheStore.
7
7
  *
8
8
  * Cache key resolution (3-tier, matching CacheScope.resolveKey):
9
- * 1. options.key(requestCtx) — full override
9
+ * 1. options.key(requestCtx) — full override, stored namespaced as
10
+ * loader:{loaderId}:key:{encodeURIComponent(result)} (#1009)
10
11
  * 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
11
12
  * 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
12
13
  *
@@ -57,13 +58,17 @@ import {
57
58
  } from "../../cache/handle-capture.js";
58
59
  import { encodeHandles, decodeHandles } from "../../cache/handle-snapshot.js";
59
60
  import { INTERNAL_RANGO_DEBUG } from "../../internal-debug.js";
61
+ import { runIdentityExempt } from "../../cache/cache-exec-scope.js";
60
62
  import {
61
63
  getRequestContext,
62
64
  _getRequestContext,
63
65
  runWithRequestContext,
64
66
  } from "../../server/request-context.js";
65
67
  import { observePhase, PHASES } from "../instrument.js";
66
- import { sortedRouteParams } from "../../cache/cache-key-utils.js";
68
+ import {
69
+ KEY_PART_PREFIX,
70
+ sortedRouteParams,
71
+ } from "../../cache/cache-key-utils.js";
67
72
  import {
68
73
  resolveTtl,
69
74
  resolveSwrWindow,
@@ -73,19 +78,24 @@ import {
73
78
  DEFAULT_ROUTE_TTL,
74
79
  } from "../../cache/cache-policy.js";
75
80
  import { readThroughItem } from "../../cache/read-through-swr.js";
81
+ import {
82
+ executionStart,
83
+ predatesInvalidation,
84
+ } from "../../cache/tag-invalidation.js";
76
85
  import {
77
86
  maskNestedContainerThenables,
78
87
  overlayLoaderContainer,
79
88
  } from "./loader-snapshot.js";
80
89
  import {
81
90
  SHELL_BAKE_TAG_OWNER,
82
- armLoaderTagSets,
83
91
  captureRecordedTags,
92
+ earliestRecordedStart,
84
93
  flattenRecordedTags,
85
94
  linkLoaderTags,
86
95
  linkLoaderTagsTo,
87
96
  linkRecordedTags,
88
97
  markIdentityRead,
98
+ markTagSetStart,
89
99
  readValueTags,
90
100
  recordedIdentityRead,
91
101
  recordLoaderTags,
@@ -128,6 +138,28 @@ function getDefaultLoaderCacheKey(
128
138
  return `loader:${loaderId}:${host}${base}`;
129
139
  }
130
140
 
141
+ /**
142
+ * A loader's own `key()` result as its entry key (#1009). The result is
143
+ * often request input, and the item family (`getItem`/`setItem`) has no
144
+ * other discriminator, so stored raw it could name another loader's entry
145
+ * or a `"use cache"` entry (`use-cache:<id>:...`, cache-runtime.ts), which
146
+ * shares that family; route records use `get`/`set`. Namespaced, it can't:
147
+ * - it starts with `loader:`, which no `"use cache"` key does;
148
+ * - an encoded result holds no `:`, so the key reads unambiguously from
149
+ * the right (result, `key:`, loader id), and two namespaced keys are
150
+ * equal only for the same loader and result;
151
+ * - a default key `loader:<id>:<host><pathname>...` holds a `/` past its
152
+ * id, and an encoded result holds none, so the two are equal only if one
153
+ * loader's id is literally `<other id>:<host>/<path>...`. Loader ids can
154
+ * hold `:` (a dev id is a root-relative path, absolute `D:/...` on
155
+ * Windows), so this is the guarantee, not "ids hold no `:`".
156
+ * A store keyGenerator result stays the store's own namespace, as on routes
157
+ * (composeCacheKeys).
158
+ */
159
+ function loaderKeyFromResult(loaderId: string, result: string): string {
160
+ return `loader:${loaderId}:${KEY_PART_PREFIX}${encodeURIComponent(result)}`;
161
+ }
162
+
131
163
  /**
132
164
  * Resolve cache key using the shared 3-tier priority. `declared`: a key() or
133
165
  * store keyGenerator produced it, so it can carry request identity (#972).
@@ -148,9 +180,14 @@ async function resolveLoaderKey(
148
180
  const host = getRequestContext()?.url?.host ?? "localhost";
149
181
  const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
150
182
  if (options === false) return { key: defaultKey, declared: false };
183
+ const keyFn = options.key;
151
184
  return {
152
- key: await resolveCacheKey(options.key, store, defaultKey, "LoaderCache"),
153
- declared: Boolean(options.key || store.keyGenerator),
185
+ key: await resolveCacheKey(
186
+ keyFn && (async (ctx) => loaderKeyFromResult(loaderId, await keyFn(ctx))),
187
+ store,
188
+ defaultKey,
189
+ ),
190
+ declared: Boolean(keyFn || store.keyGenerator),
154
191
  };
155
192
  }
156
193
 
@@ -199,6 +236,64 @@ export function bindsLoaderCache(entries: readonly EntryData[]): boolean {
199
236
  return entries.some(entryBindsLoaderCache);
200
237
  }
201
238
 
239
+ const chainLoaderEntriesMemo = new WeakMap<EntryData, readonly LoaderEntry[]>();
240
+
241
+ // The loaders an entry registers, its orphan layouts' and its parallel
242
+ // slots' included.
243
+ function chainLoaderEntries(entry: EntryData): readonly LoaderEntry[] {
244
+ let loaders = chainLoaderEntriesMemo.get(entry);
245
+ if (loaders === undefined) {
246
+ loaders = [
247
+ ...(entry.loader ?? []),
248
+ ...(entry.layout ?? []).flatMap(chainLoaderEntries),
249
+ ...Object.values(entry.parallel ?? {}).flatMap((p) =>
250
+ p ? chainLoaderEntries(p) : [],
251
+ ),
252
+ ];
253
+ chainLoaderEntriesMemo.set(entry, loaders);
254
+ }
255
+ return loaders;
256
+ }
257
+
258
+ /**
259
+ * The ids of the loaders a request over the matched chain `entries` runs on
260
+ * the live lane (no `ssr: false`), on a chain entry, its orphan layouts or
261
+ * its parallel slots. A document HIT tail restores the record's pushes of
262
+ * every other owner as authoritative (withCacheLookup, HandleStore
263
+ * pushRestored); a live-lane loader is a hole, so its live run replaces its
264
+ * recorded pushes instead. Memoized per entry.
265
+ */
266
+ export function liveLaneLoaderIds(
267
+ entries: readonly EntryData[],
268
+ ): ReadonlySet<string> {
269
+ const ids = new Set<string>();
270
+ for (const entry of entries) {
271
+ for (const l of chainLoaderEntries(entry)) {
272
+ if (!l.bake) ids.add(l.loader.$$id);
273
+ }
274
+ }
275
+ return ids;
276
+ }
277
+
278
+ /**
279
+ * The lane of each loader the matched chain `entries` registers: "live" (no
280
+ * `ssr: false`) or "bake". A loader registered on both is "live". A PPR
281
+ * capture credits a push to the first registered loader around it when that
282
+ * one is live (shell-capture.ts deriveShellCaptureContext).
283
+ */
284
+ export function routeLoaderLanes(
285
+ entries: readonly EntryData[],
286
+ ): ReadonlyMap<string, "live" | "bake"> {
287
+ const lanes = new Map<string, "live" | "bake">();
288
+ for (const entry of entries) {
289
+ for (const l of chainLoaderEntries(entry)) {
290
+ if (!l.bake) lanes.set(l.loader.$$id, "live");
291
+ else if (!lanes.has(l.loader.$$id)) lanes.set(l.loader.$$id, "bake");
292
+ }
293
+ }
294
+ return lanes;
295
+ }
296
+
202
297
  function getLoaderStore(
203
298
  loaderEntry: LoaderEntry,
204
299
  ): import("../../cache/types.js").SegmentCacheStore | null {
@@ -228,31 +323,47 @@ function getLoaderStore(
228
323
  * replayed one.
229
324
  * A PPR shell capture records the replayed pushes under this loader (its
230
325
  * push funnel reads the pushReplayed owner, shell-capture.ts), so a shell
231
- * HIT restores them owned: the claim above then skips this replay when the
232
- * loader runs on that HIT, and its live pushes replace the restored ones.
326
+ * HIT restores them owned (HandleStore.pushRestored). This replay still
327
+ * runs on that HIT: the store drops its settled values, which the record
328
+ * already restored, and keeps the thenable ones the record could not keep
329
+ * (a deferred push, the reason the loader's record carries `runs`).
233
330
  */
234
331
  async function replayLoaderHandles(
235
- encoded: string,
332
+ encoded: string | undefined,
236
333
  handleStore: HandleStore,
237
334
  segmentId: string,
335
+ cachedLoaderId: string,
238
336
  claim: ((loaderId: string) => boolean) | undefined,
239
337
  ): Promise<void> {
240
- const recorded = await decodeHandles(encoded);
338
+ const recorded = encoded ? await decodeHandles(encoded) : {};
241
339
  if (!recorded) return;
242
340
  const delivers = new Map<string, boolean>();
243
- for (const key in recorded) {
244
- const owner = key.slice(key.indexOf(":") + 1);
341
+ const claims = (owner: string): boolean => {
245
342
  let deliver = delivers.get(owner);
246
343
  if (deliver === undefined) {
247
344
  deliver = claim ? claim(owner) : true;
248
345
  delivers.set(owner, deliver);
249
346
  }
250
- if (!deliver) continue;
251
- for (const [handleName, values] of Object.entries(recorded[key])) {
252
- for (const value of values) {
253
- handleStore.pushReplayed(handleName, segmentId, value, owner);
347
+ return deliver;
348
+ };
349
+ const replay = (): void => {
350
+ for (const key in recorded) {
351
+ const owner = key.slice(key.indexOf(":") + 1);
352
+ if (!claims(owner)) continue;
353
+ for (const [handleName, values] of Object.entries(recorded[key])) {
354
+ for (const value of values) {
355
+ handleStore.pushReplayed(handleName, segmentId, value, owner);
356
+ }
254
357
  }
255
358
  }
359
+ };
360
+ // A hole's entry, pushes or none, takes the place of the shell copies of
361
+ // its pushes (HandleStore.redeliverReplays). Not once the hole delivered
362
+ // in this request: a run a reader started first replaces them itself.
363
+ if (claims(cachedLoaderId)) {
364
+ handleStore.redeliverReplays(cachedLoaderId, replay);
365
+ } else {
366
+ replay();
256
367
  }
257
368
  }
258
369
 
@@ -449,9 +560,10 @@ function executeLoaderData<TEnv>(
449
560
 
450
561
  // Evaluate runtime condition if provided
451
562
  const options = cacheConfig.options;
452
- if (options.condition) {
563
+ const condition = options.condition;
564
+ if (condition) {
453
565
  const requestCtx = getRequestContext();
454
- if (requestCtx && !options.condition(requestCtx)) {
566
+ if (requestCtx && !runIdentityExempt(() => condition(requestCtx))) {
455
567
  return ctx.use(loaderEntry.loader);
456
568
  }
457
569
  }
@@ -508,11 +620,8 @@ function executeLoaderData<TEnv>(
508
620
  const swr = swrWindow || undefined;
509
621
  const tags = resolveTags(loaderEntry);
510
622
  recordLoaderTags(loaderId, tags);
511
- // Production arms at match, before any loader runs (match-api.ts,
512
- // bindsLoaderCache); no production path relies on this. It only keeps a
513
- // resolveLoaders call outside a match from running unarmed after this
514
- // point (loaders it started earlier stay unrecorded).
515
- armLoaderTagSets();
623
+ // The per-execution loader tag sets were armed before any loader ran: by
624
+ // the match (match-api.ts, bindsLoaderCache), or by runLoader.
516
625
  // An execution of this loader outside the binding (a reader started it
517
626
  // first, a stale refresh) answers for the cache() tags too.
518
627
  if (tags && tags.length > 0) {
@@ -530,8 +639,10 @@ function executeLoaderData<TEnv>(
530
639
  const isOwnBodyPush = () => isInsideLoaderBody(loaderId);
531
640
  // What a reader of this value takes on (tagLoaderValue below,
532
641
  // readValueTags): the cache() tags, plus the entry's tags on a HIT or the
533
- // execution's on a MISS.
642
+ // execution's on a MISS. A HIT's value is as old as its lookup: a cached
643
+ // reader gates its own write from here at the latest (#977).
534
644
  const valueTags = new Set(tags);
645
+ markTagSetStart(valueTags, executionStart());
535
646
 
536
647
  const dataPromise = (async () => {
537
648
  const codec = await getCodec();
@@ -560,7 +671,9 @@ function executeLoaderData<TEnv>(
560
671
  const bodyTags = new Set(tags);
561
672
  let revalidating = false;
562
673
  let hitHandles: string | undefined;
674
+ let cachedRead = false;
563
675
  const onCachedRead = (label: string, cached: CacheItemResult) => {
676
+ cachedRead = true;
564
677
  hitHandles = cached.handles;
565
678
  const { read } = unmarkIdentity(cached.value);
566
679
  if (read) markIdentityRead(valueTags, read);
@@ -634,6 +747,15 @@ function executeLoaderData<TEnv>(
634
747
  // After the handle encode: it settles pending pushes, which can read.
635
748
  const read = identityRead();
636
749
  const entryTags = [...flattenRecordedTags(bodyTags)];
750
+ // One of the entry's tags was invalidated after the value's oldest
751
+ // part started (#977): this execution, or the run of this loader or
752
+ // of a dependency another reader started earlier. Written now, a
753
+ // value that may predate it would outlive it. Skip the write; the
754
+ // next read runs the body.
755
+ const start = earliestRecordedStart(bodyTags);
756
+ if (start && (await predatesInvalidation(store, entryTags, start))) {
757
+ return;
758
+ }
637
759
  await store.setItem!(k, markIdentity(v, read), {
638
760
  ...o,
639
761
  tags: entryTags.length > 0 ? entryTags : undefined,
@@ -642,6 +764,7 @@ function executeLoaderData<TEnv>(
642
764
  }),
643
765
  key,
644
766
  execute: async () => {
767
+ markTagSetStart(bodyTags, executionStart());
645
768
  const value = await captureRecordedTags(bodyTags, runBody);
646
769
  identityRead();
647
770
  return value;
@@ -675,12 +798,14 @@ function executeLoaderData<TEnv>(
675
798
  host: requestCtxForExecute,
676
799
  });
677
800
 
678
- // An entry written before handles were recorded has none: no replay.
679
- if (hitHandles && handleStore && owningSegmentId) {
801
+ // An entry without handles (none recorded, or dropped by an encode
802
+ // timeout) replays none, and still stands for a hole's shell copies.
803
+ if (cachedRead && handleStore && owningSegmentId) {
680
804
  await replayLoaderHandles(
681
805
  hitHandles,
682
806
  handleStore,
683
807
  owningSegmentId,
808
+ loaderId,
684
809
  internal._claimLoaderPushes,
685
810
  );
686
811
  }
@@ -8,10 +8,9 @@
8
8
  * fresh into it.
9
9
  *
10
10
  * Capture mode is signalled by `requestCtx._shellCaptureRun`, set to true ONLY on
11
- * the derived request context of the background capture task (shell-capture.ts) —
12
- * NOT by the foreground render, whose `_shellCapture` descriptor merely means "a
13
- * capture is wanted" and must not change behavior. This module is the single home
14
- * for the mask so every loader execution site gates the same way (loader-cache.ts
11
+ * the derived request context of the background capture task (shell-capture.ts);
12
+ * the foreground render never sets it. This module is the single home for the
13
+ * mask so every loader execution site gates the same way (loader-cache.ts
15
14
  * `resolveLoaderData`, fresh.ts `resolveLoaders`).
16
15
  */
17
16
 
@@ -10,24 +10,18 @@
10
10
  * server-component element holding a promise, which is a hole as a whole (see
11
11
  * elideLoaderContainer):
12
12
  *
13
- * - elide: deep-walk the settled container; a PENDING nested promise is a
14
- * hole, replaced by {@link LOADER_HOLE_KEY} — and since
15
- * loader-cache masks every nested thenable at capture
16
- * ({@link maskNestedContainerThenables}), nested promises are
17
- * ALWAYS pending here for new captures. The SETTLED-marker branch
18
- * below is legacy: it fires only if a settled thenable reaches
19
- * elide anyway, and the overlay keeps decoding SETTLED markers
20
- * from pre-mask snapshots. The result is promise-free and
21
- * Flight-serializable.
13
+ * - elide: deep-walk the settled container; a nested promise is a hole,
14
+ * replaced by {@link LOADER_HOLE_KEY}. loader-cache masks every
15
+ * nested thenable at capture ({@link maskNestedContainerThenables},
16
+ * a never-settling promise), so the promise shape alone decides:
17
+ * a value that settled in time does not bake (#692). The result
18
+ * is promise-free and Flight-serializable.
22
19
  * - overlay: on a HIT the loader runs fresh (only the loader body can mint
23
20
  * the live nested promises), then the recorded container is laid
24
21
  * over it: recorded paths win (they are what the prelude froze),
25
22
  * hole-marker paths take the fresh run's value (the live hole),
26
- * SETTLED-marker paths become Promise.resolve(pinned) — consumers
27
- * wrote use(data.x) against a promise-shaped container, and
28
- * handing them the raw value throws React #438 on every HIT (the
29
- * storefront PDP regression) — and fresh-only paths pass through
30
- * (they cannot contradict prelude bytes that never rendered them).
23
+ * and fresh-only paths pass through (they cannot contradict
24
+ * prelude bytes that never rendered them).
31
25
  */
32
26
 
33
27
  import { isValidElement, type ReactElement } from "react";
@@ -64,37 +58,6 @@ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
64
58
  );
65
59
  }
66
60
 
67
- /**
68
- * Marker wrapping the inlined value of a NESTED promise that settled during
69
- * capture. The value baked (physics), but the container key was a promise —
70
- * the overlay must hand consumers a Promise.resolve(value), not the raw value,
71
- * or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
72
- * container is never wrapped: loader-cache overlays against the awaited fresh
73
- * container value.
74
- */
75
- export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
76
-
77
- export interface LoaderSettledMarker {
78
- [LOADER_SETTLED_KEY]: 1;
79
- value: unknown;
80
- /**
81
- * Present when the pinned subtree contains hole markers. Computed once at
82
- * capture (elide already visits every node) so the per-HIT overlay never
83
- * rescans the pinned structure to pick its rehydration path.
84
- */
85
- holes?: 1;
86
- }
87
-
88
- export function isLoaderSettledMarker(
89
- value: unknown,
90
- ): value is LoaderSettledMarker {
91
- return (
92
- typeof value === "object" &&
93
- value !== null &&
94
- (value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
95
- );
96
- }
97
-
98
61
  /**
99
62
  * Probe whether a promise is already settled without waiting for it: races it
100
63
  * against an immediately-resolved sentinel across two microtask hops (then
@@ -128,12 +91,12 @@ export type ElideResult =
128
91
  | { state: "rejected" };
129
92
 
130
93
  /**
131
- * Deep-elide a settled bake-lane container for recording. Settled nested
132
- * promises pin their value behind a settled marker (they baked, but consumers
133
- * hold a promise-shaped key); pending ones become hole markers; a REJECTED
134
- * nested promise poisons the record (error UI must never bake into a shared
135
- * shell) — the caller refuses the capture. Plain data objects, arrays and the
136
- * props of host, Suspense/Fragment and client-reference elements are
94
+ * Deep-elide a settled bake-lane container for recording. Nested promises
95
+ * become hole markers: the capture masked each of them (never settles), so
96
+ * it postponed as a hole. A REJECTED container poisons the record (error UI
97
+ * must never bake into a shared shell) — the caller refuses the capture.
98
+ * Plain data objects, arrays and the props of host, Suspense/Fragment and
99
+ * client-reference elements are
137
100
  * traversed copy-on-write (an element through cloneElementWithProps); a shared
138
101
  * subtree or a cycle resolves to the same elided result. An element Flight
139
102
  * renders by calling user code (see rendersOnServer) is a leaf, or ONE hole
@@ -165,17 +128,7 @@ async function elideNested(
165
128
  seen: Map<object, ElideResult>,
166
129
  ): Promise<ElideResult> {
167
130
  if (isThenable(value)) {
168
- const probed = await probeSettled(value);
169
- if (probed.state === "pending") {
170
- return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
171
- }
172
- if (probed.state === "rejected") return { state: "rejected" };
173
- const inner = await elideNested(probed.value, seen);
174
- if (inner.state === "rejected") return inner;
175
- const marker: LoaderSettledMarker = inner.hasHole
176
- ? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
177
- : { [LOADER_SETTLED_KEY]: 1, value: inner.value };
178
- return { state: "ok", value: marker, hasHole: inner.hasHole };
131
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
179
132
  }
180
133
  if (typeof value !== "object" || value === null) {
181
134
  return { state: "ok", value, hasHole: false };
@@ -324,22 +277,6 @@ export function overlayLoaderContainer(
324
277
  ): unknown {
325
278
  if (isLoaderHoleMarker(recorded)) return fresh;
326
279
 
327
- if (isLoaderSettledMarker(recorded)) {
328
- const pinned = recorded.value;
329
- // Deep holes inside a settled container (capture-computed `holes` bit)
330
- // need the fresh promise's resolved value to fill them; a fully-pinned
331
- // container resolves immediately (the prelude already shows it — never
332
- // gate it on fresh latency). A rejecting fresh run degrades holes to
333
- // undefined instead of poisoning the pin.
334
- if (recorded.holes === 1) {
335
- return Promise.resolve(fresh).then(
336
- (freshValue) => overlayLoaderContainer(freshValue, pinned),
337
- () => overlayLoaderContainer(undefined, pinned),
338
- );
339
- }
340
- return Promise.resolve(overlayLoaderContainer(undefined, pinned));
341
- }
342
-
343
280
  if (Array.isArray(recorded)) {
344
281
  const freshArr = Array.isArray(fresh) ? fresh : [];
345
282
  let out: unknown[] | undefined;
@@ -81,30 +81,55 @@ export function cloneElementWithProps(
81
81
  */
82
82
  export function settleNestedThenables(value: unknown): Promise<void> {
83
83
  const pending: Promise<void>[] = [];
84
- const seen = new Set<object>();
85
- const walk = (v: unknown): void => {
86
- if (isThenable(v)) {
87
- pending.push(
88
- Promise.resolve(v).then(settleNestedThenables, () => undefined),
89
- );
90
- return;
91
- }
92
- if (isValidElement<Record<string, unknown>>(v)) {
93
- walk(v.props);
94
- return;
95
- }
96
- if (Array.isArray(v) || isPlainDataObject(v)) {
97
- if (seen.has(v)) return;
98
- seen.add(v);
99
- for (const item of Array.isArray(v) ? v : Object.values(v)) walk(item);
100
- }
101
- };
102
- walk(value);
84
+ someNestedThenable(value, (thenable) => {
85
+ pending.push(
86
+ Promise.resolve(thenable).then(settleNestedThenables, () => undefined),
87
+ );
88
+ return false;
89
+ });
103
90
  return pending.length === 0
104
91
  ? Promise.resolve()
105
92
  : Promise.all(pending).then(() => undefined);
106
93
  }
107
94
 
95
+ /**
96
+ * The walk settleNestedThenables and holdsThenable share: visit each thenable
97
+ * reachable from `value` through plain data objects, arrays and React element
98
+ * props (never an element's owner or type), the shapes
99
+ * maskNestedContainerThenables masks; every other object is a leaf. Stops at
100
+ * the first visit that returns true and reports whether one did.
101
+ */
102
+ function someNestedThenable(
103
+ value: unknown,
104
+ visit: (thenable: PromiseLike<unknown>) => boolean,
105
+ ): boolean {
106
+ let seen: Set<object> | undefined;
107
+ const walk = (v: unknown): boolean => {
108
+ if (isThenable(v)) return visit(v);
109
+ if (typeof v !== "object" || v === null) return false;
110
+ if (isValidElement<Record<string, unknown>>(v)) return walk(v.props);
111
+ if (!Array.isArray(v) && !isPlainDataObject(v)) return false;
112
+ seen ??= new Set();
113
+ if (seen.has(v)) return false;
114
+ seen.add(v);
115
+ // Own enumerable values, as maskNestedContainerThenables walks
116
+ // Object.keys: an array's non-index properties are walked too.
117
+ return Object.values(v).some(walk);
118
+ };
119
+ return walk(value);
120
+ }
121
+
122
+ /**
123
+ * True when `value` is a thenable or holds one where the capture's loader
124
+ * push funnel masks it (maskNestedContainerThenables): through plain data
125
+ * objects, arrays and React element props. Such a push stays out of a shell's
126
+ * record and reaches a HIT only from a run of its loader there, so a HIT
127
+ * keeps it; a push without one is the record's (HandleStore.pushRestored).
128
+ */
129
+ export function holdsThenable(value: unknown): boolean {
130
+ return someNestedThenable(value, () => true);
131
+ }
132
+
108
133
  /**
109
134
  * Optional single-walk report for maskNestedContainerThenables: `thenable`
110
135
  * flips true when the walk masked at least one thenable. The capture push
@@ -139,9 +164,9 @@ export interface MaskReport {
139
164
  * nodes included, are leaves. Copy-on-write: a subtree without a thenable
140
165
  * comes back by identity, so an element is rebuilt only when its props hold
141
166
  * one, and then through cloneElementWithProps, which keeps its type, key,
142
- * owner, dev fields and key validation. The INPUT IS NEVER MUTATED — handler-side loader consumption (the
143
- * consumption-lane rule, semantic-matrix PPR3) shares the raw container and
144
- * must keep real values. Cycles are preserved as cycles in the copy.
167
+ * owner, dev fields and key validation. The INPUT IS NEVER MUTATED: a handler
168
+ * that reads the loader (`ctx.use`) reads the raw container and must keep
169
+ * real values. Cycles are preserved as cycles in the copy.
145
170
  */
146
171
  export function maskNestedContainerThenables(
147
172
  value: unknown,
@@ -708,7 +708,6 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
708
708
  transition: applyViewTransitionDefault(
709
709
  parallelEntry.transition,
710
710
  deps.viewTransitionDefault,
711
- parallelId,
712
711
  ),
713
712
  params,
714
713
  slot,
@@ -920,7 +919,6 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
920
919
  transition: applyViewTransitionDefault(
921
920
  entry.transition,
922
921
  deps.viewTransitionDefault,
923
- entry.shortCode,
924
922
  ),
925
923
  params,
926
924
  belongsToRoute,
@@ -1261,7 +1259,6 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1261
1259
  transition: applyViewTransitionDefault(
1262
1260
  orphan.transition,
1263
1261
  deps.viewTransitionDefault,
1264
- orphan.shortCode,
1265
1262
  ),
1266
1263
  ...(orphan.mountPath ? { mountPath: orphan.mountPath } : {}),
1267
1264
  });
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import type { EntryData } from "../../server/context";
11
- import { getRequestContext } from "../../server/request-context.js";
12
11
 
13
12
  /**
14
13
  * Resolve a segment's transition config: stamp the `viewTransition` default and
@@ -22,34 +21,20 @@ import { getRequestContext } from "../../server/request-context.js";
22
21
  * (or "auto") value is left untouched because it already means "wrap" at the
23
22
  * gate, which also avoids needless object allocation and payload growth.
24
23
  *
25
- * `when`: a server-only predicate. It is STRIPPED from the returned config (a
26
- * function cannot cross Flight or the segment cache). PPR routes evaluate it
27
- * from the manifest before the pipeline; other routes record it here for the
28
- * existing post-handler gate. Used by both fresh and revalidation resolution.
24
+ * `when`: a client reference the server never calls. It is STRIPPED here so no
25
+ * segment record (segment cache, prerender artifact, shell snapshot) carries
26
+ * it: those stores JSON-serialize the config and would silently drop it on
27
+ * some stores and keep it on others. rsc/attach-transition-when.ts attaches it
28
+ * from the route definition right before Flight instead. Used by both fresh
29
+ * and revalidation resolution.
29
30
  */
30
31
  export function applyViewTransitionDefault(
31
32
  transition: EntryData["transition"],
32
33
  viewTransitionDefault: "auto" | false | undefined,
33
- segmentId?: string,
34
34
  ): EntryData["transition"] {
35
35
  if (!transition) return transition;
36
36
  let result = transition;
37
37
  if (result.when) {
38
- if (segmentId !== undefined) {
39
- try {
40
- const ctx = getRequestContext();
41
- if (!ctx._pprTransitionDecisions?.has(segmentId)) {
42
- (ctx._transitionWhen ??= []).push({
43
- id: segmentId,
44
- when: result.when,
45
- });
46
- }
47
- } catch {
48
- // No active request context (e.g. a unit test calling this util
49
- // directly). Skip collection; the strip below still applies so the
50
- // serialized config never carries the function.
51
- }
52
- }
53
38
  const { when: _when, ...rest } = result;
54
39
  result = rest;
55
40
  }
@@ -14,6 +14,8 @@
14
14
  * - revalidation.decision (revalidation evaluation)
15
15
  */
16
16
 
17
+ import { requestHeaders } from "../server/request-headers.js";
18
+
17
19
  interface BaseEvent {
18
20
  /** Monotonic timestamp from performance.now() */
19
21
  timestamp: number;
@@ -294,10 +296,11 @@ export function getRequestId(request: Request): string {
294
296
  const existing = requestIds.get(request);
295
297
  if (existing) return existing;
296
298
 
299
+ const headers = requestHeaders(request);
297
300
  const candidate =
298
- request.headers.get("x-rsc-router-request-id") ??
299
- request.headers.get("x-request-id") ??
300
- request.headers.get("cf-ray");
301
+ headers.get("x-rsc-router-request-id") ??
302
+ headers.get("x-request-id") ??
303
+ headers.get("cf-ray");
301
304
 
302
305
  let id: string;
303
306
  if (candidate) {