@rangojs/router 0.21.0 → 0.22.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/bin/rango.js +13 -1
  2. package/dist/types/browser/event-controller.d.ts +30 -13
  3. package/dist/types/browser/navigation-store.d.ts +9 -0
  4. package/dist/types/browser/partial-update.d.ts +2 -1
  5. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  6. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  7. package/dist/types/browser/react/context.d.ts +14 -0
  8. package/dist/types/browser/rsc-router.d.ts +13 -2
  9. package/dist/types/browser/types.d.ts +10 -0
  10. package/dist/types/build/generate-manifest.d.ts +2 -0
  11. package/dist/types/cache/background-task.d.ts +22 -0
  12. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  13. package/dist/types/cache/cache-policy.d.ts +17 -0
  14. package/dist/types/cache/cache-scope.d.ts +1 -1
  15. package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
  16. package/dist/types/cache/document-cache.d.ts +8 -5
  17. package/dist/types/cache/handle-snapshot.d.ts +23 -0
  18. package/dist/types/cache/index.d.ts +1 -1
  19. package/dist/types/cache/invalidation-order.d.ts +14 -0
  20. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  21. package/dist/types/cache/shell-snapshot.d.ts +10 -1
  22. package/dist/types/cache/store-scope.d.ts +22 -0
  23. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  24. package/dist/types/cache/types.d.ts +63 -14
  25. package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
  26. package/dist/types/errors.d.ts +6 -0
  27. package/dist/types/handles/deferred-resolution.d.ts +7 -0
  28. package/dist/types/internal-suspense-audit.d.ts +1 -0
  29. package/dist/types/loader-store.d.ts +4 -8
  30. package/dist/types/outlet-provider.d.ts +2 -1
  31. package/dist/types/prerender/cloudflare.d.ts +49 -0
  32. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  33. package/dist/types/prerender/index.d.ts +12 -0
  34. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  35. package/dist/types/prerender/on-demand.d.ts +272 -0
  36. package/dist/types/prerender/producer-guard.d.ts +33 -0
  37. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  38. package/dist/types/prerender/warm-request.d.ts +65 -0
  39. package/dist/types/prerender/warm.d.ts +65 -0
  40. package/dist/types/prerender/writable-store.d.ts +170 -0
  41. package/dist/types/prerender.d.ts +27 -0
  42. package/dist/types/reverse.d.ts +1 -1
  43. package/dist/types/route-content-wrapper.d.ts +3 -1
  44. package/dist/types/router/error-handling.d.ts +2 -0
  45. package/dist/types/router/handler-context.d.ts +1 -1
  46. package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
  47. package/dist/types/router/pattern-matching.d.ts +2 -0
  48. package/dist/types/router/prefetch-default.d.ts +4 -4
  49. package/dist/types/router/prerender-match.d.ts +23 -1
  50. package/dist/types/router/route-trie-builder.d.ts +8 -1
  51. package/dist/types/router/router-context.d.ts +1 -0
  52. package/dist/types/router/router-interfaces.d.ts +30 -0
  53. package/dist/types/router/router-options.d.ts +24 -0
  54. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
  56. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  57. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  58. package/dist/types/router/trie-matching.d.ts +2 -0
  59. package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
  60. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  61. package/dist/types/rsc/shell-capture.d.ts +14 -1
  62. package/dist/types/rsc/shell-serve.d.ts +2 -2
  63. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  64. package/dist/types/rsc/types.d.ts +8 -0
  65. package/dist/types/segment-boundary-content.d.ts +17 -0
  66. package/dist/types/segment-loader-promise.d.ts +2 -6
  67. package/dist/types/server/build-version-table.d.ts +9 -0
  68. package/dist/types/server/context.d.ts +12 -4
  69. package/dist/types/server/handle-store.d.ts +2 -0
  70. package/dist/types/server/request-context.d.ts +63 -2
  71. package/dist/types/suspense-audit-react.d.ts +26 -0
  72. package/dist/types/suspense-audit.d.ts +157 -0
  73. package/dist/types/testing/action-form.d.ts +34 -0
  74. package/dist/types/testing/flight.entry.d.ts +2 -0
  75. package/dist/types/testing/index.d.ts +4 -0
  76. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  77. package/dist/types/testing/serve-shell-request.d.ts +34 -4
  78. package/dist/types/testing/shell-status.d.ts +14 -6
  79. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  80. package/dist/types/thenable-status.d.ts +6 -0
  81. package/dist/types/types/handler-context.d.ts +7 -0
  82. package/dist/types/types/route-entry.d.ts +5 -0
  83. package/dist/types/urls/path-helper.d.ts +18 -0
  84. package/dist/types/vite/discovery/state.d.ts +11 -0
  85. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  86. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  87. package/dist/vite/index.js +180 -497
  88. package/package.json +13 -3
  89. package/skills/cache-guide/SKILL.md +18 -17
  90. package/skills/caching/SKILL.md +40 -4
  91. package/skills/catalog.json +1 -1
  92. package/skills/cloudflare/SKILL.md +131 -0
  93. package/skills/comparison/references/framework-comparison.md +1 -1
  94. package/skills/deployment-caching/SKILL.md +33 -12
  95. package/skills/document-cache/SKILL.md +16 -14
  96. package/skills/hooks/SKILL.md +22 -0
  97. package/skills/hooks/handle-and-actions.md +3 -4
  98. package/skills/hooks/navigation.md +10 -0
  99. package/skills/hooks/state.md +5 -4
  100. package/skills/host-router/SKILL.md +18 -0
  101. package/skills/loader/SKILL.md +6 -4
  102. package/skills/middleware/SKILL.md +1 -1
  103. package/skills/migrate-nextjs/SKILL.md +82 -9
  104. package/skills/ppr/SKILL.md +31 -8
  105. package/skills/prerender/SKILL.md +645 -8
  106. package/skills/rango/SKILL.md +1 -0
  107. package/skills/response-routes/SKILL.md +5 -5
  108. package/skills/route/SKILL.md +29 -0
  109. package/skills/testing/SKILL.md +2 -1
  110. package/skills/testing/cache-prerender.md +18 -17
  111. package/skills/testing/serve-shell-request.md +104 -8
  112. package/skills/use-cache/SKILL.md +9 -5
  113. package/skills/vercel/SKILL.md +2 -0
  114. package/skills/view-transitions/SKILL.md +9 -4
  115. package/src/browser/event-controller.ts +69 -25
  116. package/src/browser/link-interceptor.ts +2 -2
  117. package/src/browser/navigation-bridge.ts +75 -35
  118. package/src/browser/navigation-store.ts +26 -0
  119. package/src/browser/navigation-transaction.ts +9 -2
  120. package/src/browser/network-error-handler.ts +2 -0
  121. package/src/browser/partial-update.ts +48 -12
  122. package/src/browser/prefetch/default-strategy.ts +22 -42
  123. package/src/browser/react/Link.tsx +37 -42
  124. package/src/browser/react/NavigationProvider.tsx +63 -3
  125. package/src/browser/react/context.ts +17 -0
  126. package/src/browser/react/location-state.ts +5 -19
  127. package/src/browser/react/use-action.ts +4 -1
  128. package/src/browser/react/use-handle.ts +4 -20
  129. package/src/browser/react/use-link-status.ts +4 -3
  130. package/src/browser/react/use-navigation.ts +4 -1
  131. package/src/browser/react/use-params.ts +4 -1
  132. package/src/browser/react/use-pathname.ts +4 -1
  133. package/src/browser/react/use-search-params.ts +5 -3
  134. package/src/browser/react/use-segments.ts +36 -54
  135. package/src/browser/rsc-router.tsx +79 -39
  136. package/src/browser/segment-reconciler.ts +3 -3
  137. package/src/browser/server-action-bridge.ts +26 -6
  138. package/src/browser/types.ts +11 -0
  139. package/src/build/generate-manifest.ts +17 -0
  140. package/src/build/merge-full-manifests.ts +5 -0
  141. package/src/build/route-trie.ts +1 -0
  142. package/src/cache/background-task.ts +61 -0
  143. package/src/cache/cache-key-utils.ts +91 -12
  144. package/src/cache/cache-policy.ts +11 -2
  145. package/src/cache/cache-runtime.ts +23 -23
  146. package/src/cache/cache-scope.ts +38 -36
  147. package/src/cache/cf/cf-cache-store.ts +16 -11
  148. package/src/cache/document-cache.ts +35 -14
  149. package/src/cache/handle-snapshot.ts +50 -0
  150. package/src/cache/index.ts +1 -0
  151. package/src/cache/invalidation-order.ts +53 -0
  152. package/src/cache/memory-segment-store.ts +52 -1
  153. package/src/cache/shell-snapshot.ts +17 -0
  154. package/src/cache/store-scope.ts +36 -0
  155. package/src/cache/tag-invalidation.ts +7 -5
  156. package/src/cache/types.ts +69 -14
  157. package/src/cache/vercel/vercel-cache-store.ts +20 -6
  158. package/src/client.tsx +40 -3
  159. package/src/errors.ts +13 -6
  160. package/src/handles/deferred-resolution.ts +7 -2
  161. package/src/internal-suspense-audit.ts +12 -0
  162. package/src/loader-store.ts +4 -9
  163. package/src/outlet-provider.tsx +25 -1
  164. package/src/prerender/cloudflare.ts +130 -0
  165. package/src/prerender/create-prerender-trigger.ts +780 -0
  166. package/src/prerender/index.ts +39 -0
  167. package/src/prerender/memory-prerender-store.ts +115 -0
  168. package/src/prerender/on-demand.ts +339 -0
  169. package/src/prerender/producer-guard.ts +66 -0
  170. package/src/prerender/shell-manifest-key.ts +12 -17
  171. package/src/prerender/warm-request.ts +154 -0
  172. package/src/prerender/warm.ts +193 -0
  173. package/src/prerender/writable-store.ts +278 -0
  174. package/src/prerender.ts +29 -0
  175. package/src/reverse.ts +1 -1
  176. package/src/route-content-wrapper.tsx +107 -13
  177. package/src/router/error-handling.ts +12 -12
  178. package/src/router/find-match.ts +1 -0
  179. package/src/router/handler-context.ts +2 -0
  180. package/src/router/intercept-resolution.ts +15 -8
  181. package/src/router/loader-resolution.ts +2 -10
  182. package/src/router/match-middleware/background-revalidation.ts +2 -0
  183. package/src/router/match-middleware/cache-lookup.ts +230 -38
  184. package/src/router/pattern-matching.ts +14 -0
  185. package/src/router/prefetch-default.ts +4 -4
  186. package/src/router/prerender-match.ts +460 -352
  187. package/src/router/route-trie-builder.ts +9 -0
  188. package/src/router/router-context.ts +1 -0
  189. package/src/router/router-interfaces.ts +33 -0
  190. package/src/router/router-options.ts +30 -0
  191. package/src/router/segment-resolution/fresh.ts +20 -19
  192. package/src/router/segment-resolution/loader-cache.ts +15 -17
  193. package/src/router/segment-resolution/revalidation.ts +34 -24
  194. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  195. package/src/router/trie-matching.ts +3 -0
  196. package/src/router.ts +107 -1
  197. package/src/rsc/handler.ts +51 -4
  198. package/src/rsc/progressive-enhancement.ts +5 -1
  199. package/src/rsc/response-cache-serve.ts +10 -4
  200. package/src/rsc/rsc-rendering.ts +53 -11
  201. package/src/rsc/shell-build-manifest.ts +10 -8
  202. package/src/rsc/shell-capture-constants.ts +39 -7
  203. package/src/rsc/shell-capture.ts +108 -19
  204. package/src/rsc/shell-serve.ts +12 -6
  205. package/src/rsc/ssr-module-loader.ts +26 -0
  206. package/src/rsc/types.ts +8 -0
  207. package/src/segment-boundary-content.ts +25 -0
  208. package/src/segment-loader-promise.ts +28 -38
  209. package/src/segment-system.tsx +117 -57
  210. package/src/server/build-version-table.ts +15 -0
  211. package/src/server/context.ts +40 -17
  212. package/src/server/cookie-store.ts +5 -0
  213. package/src/server/handle-store.ts +13 -0
  214. package/src/server/loader-registry.ts +6 -1
  215. package/src/server/request-context.ts +87 -1
  216. package/src/ssr/ssr-root.tsx +20 -13
  217. package/src/suspense-audit-react.tsx +98 -0
  218. package/src/suspense-audit.ts +1030 -0
  219. package/src/testing/action-form.ts +66 -0
  220. package/src/testing/flight-runtime.d.ts +27 -0
  221. package/src/testing/flight.entry.ts +3 -0
  222. package/src/testing/index.ts +19 -0
  223. package/src/testing/internal/server-modules.ts +32 -0
  224. package/src/testing/render-route.tsx +25 -12
  225. package/src/testing/serve-shell-request.ts +190 -34
  226. package/src/testing/shell-status.ts +17 -6
  227. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  228. package/src/thenable-status.ts +10 -0
  229. package/src/types/handler-context.ts +7 -0
  230. package/src/types/route-entry.ts +6 -0
  231. package/src/urls/path-helper.ts +69 -9
  232. package/src/use-loader.tsx +57 -30
  233. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  234. package/src/vite/discovery/discover-routers.ts +15 -0
  235. package/src/vite/discovery/shell-prerender-phase.ts +5 -5
  236. package/src/vite/discovery/state.ts +17 -1
  237. package/src/vite/inject-client-debug.ts +42 -20
  238. package/src/vite/plugins/virtual-entries.ts +8 -1
  239. package/src/vite/router-discovery.ts +14 -0
  240. package/src/vite/utils/bundle-analysis.ts +39 -7
  241. package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
  242. package/dist/types/segment-content-promise.d.ts +0 -13
  243. package/src/browser/react/subscribe-to-nothing.ts +0 -7
  244. package/src/segment-content-promise.ts +0 -67
@@ -91,7 +91,7 @@
91
91
  * - Request object
92
92
  * - Action context (if POST)
93
93
  */
94
- import type { ResolvedSegment } from "../../types.js";
94
+ import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
95
95
  import { isPprEntry, type EntryData } from "../../server/context.js";
96
96
  import type { MatchContext, MatchPipelineState } from "../match-context.js";
97
97
  import { getRouterContext, type RouterContext } from "../router-context.js";
@@ -100,6 +100,17 @@ import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
100
100
  import { treeHasStreaming } from "./segment-resolution.js";
101
101
  import { loaderPins } from "../segment-resolution/loader-cache.js";
102
102
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
103
+ import {
104
+ isStoredEntryStale,
105
+ readVerifiedStoredEntry,
106
+ serializePrerenderKey,
107
+ type PrerenderKey,
108
+ } from "../../prerender/writable-store.js";
109
+ import { IN_FLIGHT_LEADER_MAX_WAIT_MS } from "../../cache/cache-policy.js";
110
+ import type {
111
+ PrerenderTargetObject,
112
+ ResolvedPrerender,
113
+ } from "../../prerender/on-demand.js";
103
114
  import {
104
115
  _getRequestContext,
105
116
  type RequestContext,
@@ -111,6 +122,7 @@ import {
111
122
  import { ShellRecordUnavailableError } from "../../cache/shell-snapshot.js";
112
123
  import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
113
124
  import { paramsEqual } from "../params-util.js";
125
+ import { requestHeaders } from "../../server/request-headers.js";
114
126
 
115
127
  // Lazily initialized prerender store singleton and dynamically imported deps.
116
128
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
@@ -128,10 +140,27 @@ let _restoreHandles:
128
140
  let _decodeHandles:
129
141
  | typeof import("../../cache/handle-snapshot.js").decodeHandles
130
142
  | undefined;
143
+ let _restoreRecordHandles:
144
+ | typeof import("../../cache/handle-snapshot.js").restoreRecordHandles
145
+ | undefined;
131
146
  let _hashParams:
132
147
  | typeof import("../../prerender/param-hash.js").hashParams
133
148
  | undefined;
134
149
 
150
+ /**
151
+ * @internal Replace the prerender store this module serves from, and return
152
+ * the one it replaced. For serveShellRequest (testing/serve-shell-request.ts),
153
+ * which installs the production store over the artifacts it bakes for a call
154
+ * and puts the previous one back after it.
155
+ */
156
+ export function setPrerenderStoreForTests(
157
+ store: PrerenderStore | null | undefined,
158
+ ): PrerenderStore | null | undefined {
159
+ const previous = prerenderStoreInstance;
160
+ prerenderStoreInstance = store;
161
+ return previous;
162
+ }
163
+
135
164
  async function ensurePrerenderDeps() {
136
165
  if (!_deserializeSegments) {
137
166
  const [codec, snapshot, paramHash, store] = await Promise.all([
@@ -144,6 +173,7 @@ async function ensurePrerenderDeps() {
144
173
  _fragmentSegments = codec.fragmentSegments;
145
174
  _restoreHandles = snapshot.restoreHandles;
146
175
  _decodeHandles = snapshot.decodeHandles;
176
+ _restoreRecordHandles = snapshot.restoreRecordHandles;
147
177
  _hashParams = paramHash.hashParams;
148
178
  if (prerenderStoreInstance === undefined) {
149
179
  prerenderStoreInstance = store.createPrerenderStore();
@@ -240,12 +270,21 @@ async function* yieldFromStore<TEnv>(
240
270
  reqCtx: RequestContext<TEnv> | undefined,
241
271
  resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
242
272
  resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
273
+ /**
274
+ * The entry may be newer than the client's copy: an on-demand overlay entry
275
+ * that a refresh (possibly inside this very action) replaced. On an action
276
+ * the route's own segments are then re-sent, as a live action re-render
277
+ * re-sends them (revalidation.ts "action:route-segment"); the immutable
278
+ * bundled manifest keeps the client's copy.
279
+ */
280
+ replaceable?: boolean,
243
281
  ): AsyncGenerator<ResolvedSegment> {
244
282
  if (
245
283
  !_deserializeSegments ||
246
284
  !_fragmentSegments ||
247
285
  !_restoreHandles ||
248
286
  !_decodeHandles ||
287
+ !_restoreRecordHandles ||
249
288
  !_hashParams
250
289
  ) {
251
290
  throw new Error("yieldFromStore called before ensurePrerenderDeps");
@@ -270,6 +309,16 @@ async function* yieldFromStore<TEnv>(
270
309
  }
271
310
  }
272
311
 
312
+ // Before the render barrier freezes the document snapshot.
313
+ const shellHandles = reqCtx?._shellImplicitCache?.prerenderHandles;
314
+ if (handleStore && shellHandles) {
315
+ await _restoreRecordHandles!(
316
+ handleStore,
317
+ shellHandles,
318
+ loaderPins(ctx.entries, reqCtx),
319
+ );
320
+ }
321
+
273
322
  state.cacheHit = true;
274
323
  state.cacheSource = "prerender";
275
324
  state.cachedSegments = segments;
@@ -292,11 +341,16 @@ async function* yieldFromStore<TEnv>(
292
341
  // different content, so we must NOT nullify.
293
342
  const paramsChanged =
294
343
  !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
344
+ const resendRouteSegments = replaceable === true && ctx.isAction;
295
345
  for (const segment of segments) {
296
346
  if (
297
347
  !ctx.isFullMatch &&
298
348
  !paramsChanged &&
299
- ctx.clientSegmentSet.has(segment.id)
349
+ ctx.clientSegmentSet.has(segment.id) &&
350
+ !(
351
+ resendRouteSegments &&
352
+ (segment.type === "route" || segment.belongsToRoute === true)
353
+ )
300
354
  ) {
301
355
  keepClientSegment(segment);
302
356
  }
@@ -345,11 +399,9 @@ export async function prerenderEntryExists(
345
399
  ).createPrerenderStore();
346
400
  }
347
401
  if (!prerenderStoreInstance) return false;
348
- // Non-intercept variant only: whether the navigation IS an intercept (and
349
- // therefore whether tryPrerenderLookup reads `paramHash + "/i"`) resolves
350
- // during the match, so this pre-match fast path can only guess the normal
351
- // artifact. A wrong guess is reclassified post-match from the match
352
- // pipeline's `_pprReplayPostMatchReason` stamp.
402
+ // Non-intercept variant only: whether the navigation is an intercept (which
403
+ // reads `paramHash + "/i"`) resolves during the match. A wrong guess is
404
+ // reclassified post-match from `_pprReplayPostMatchReason`.
353
405
  const entry = await prerenderStoreInstance.get(
354
406
  routeKey,
355
407
  _hashParams!(params),
@@ -363,6 +415,63 @@ export async function prerenderEntryExists(
363
415
  return entry != null;
364
416
  }
365
417
 
418
+ /**
419
+ * Serialized overlay key -> start time of its in-flight `onRevalidate`.
420
+ * Dedupes the obvious Node wiring, `(t, env) => router.prerender(t, { env })`,
421
+ * and queue sends alike, per isolate. Entries older than
422
+ * IN_FLIGHT_LEADER_MAX_WAIT_MS count as free: a never-settling onRevalidate
423
+ * (hung queue send) or a killed execution context never runs `.finally`, and
424
+ * would otherwise pin the key for the isolate's lifetime.
425
+ */
426
+ const overlayRevalidationsInFlight = new Map<string, number>();
427
+
428
+ /** @internal Tests only (testing/serve-shell-request.ts resetShellTestState). */
429
+ export function resetOverlayRevalidationsForTests(): void {
430
+ overlayRevalidationsInFlight.clear();
431
+ }
432
+
433
+ /** @internal Exported for the eviction unit test. */
434
+ export function scheduleOverlayRevalidation<TEnv>(
435
+ serializedKey: string,
436
+ onRevalidate: NonNullable<ResolvedPrerender["config"]["onRevalidate"]>,
437
+ target: PrerenderTargetObject,
438
+ reqCtx: RequestContext<TEnv>,
439
+ now: number = Date.now(),
440
+ ): void {
441
+ // waitUntil is a no-op under build contexts: nothing would clear the key.
442
+ if (reqCtx.build) return;
443
+ const startedAt = overlayRevalidationsInFlight.get(serializedKey);
444
+ if (
445
+ startedAt !== undefined &&
446
+ now - startedAt < IN_FLIGHT_LEADER_MAX_WAIT_MS
447
+ ) {
448
+ return;
449
+ }
450
+ overlayRevalidationsInFlight.set(serializedKey, now);
451
+ const env = reqCtx.env;
452
+ const executionContext = reqCtx.executionContext;
453
+ const reportError = reqCtx._reportBackgroundError;
454
+ // The stale entry still serves this request; this only schedules.
455
+ reqCtx.waitUntil(() =>
456
+ Promise.resolve()
457
+ .then(() => onRevalidate(target, env, executionContext))
458
+ .then(
459
+ () => {},
460
+ (err) => {
461
+ // A failed onRevalidate must not fail the response, but must reach
462
+ // onError like the runtime cache's stale-revalidation failures.
463
+ reportError?.(err, "stale-revalidation");
464
+ },
465
+ )
466
+ .finally(() => {
467
+ // An evicted-and-rescheduled entry belongs to the newer run.
468
+ if (overlayRevalidationsInFlight.get(serializedKey) === now) {
469
+ overlayRevalidationsInFlight.delete(serializedKey);
470
+ }
471
+ }),
472
+ );
473
+ }
474
+
366
475
  /**
367
476
  * Look up a prerendered (build-time cached) entry for the current route and, on
368
477
  * a hit, yield its segments. Returns true when an entry was served (the caller
@@ -378,31 +487,99 @@ async function* tryPrerenderLookup<TEnv>(
378
487
  reqCtx: RequestContext<TEnv> | undefined,
379
488
  resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
380
489
  resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
490
+ isPassthroughPrerenderRoute: boolean,
491
+ overlay?: ResolvedPrerender,
381
492
  ): AsyncGenerator<ResolvedSegment, boolean> {
382
- const paramHash = _hashParams!(ctx.matched.params);
383
- const isPassthroughPrerenderRoute = ctx.entries.some(
384
- (entry) => entry.type === "route" && entry.isPassthrough === true,
385
- );
386
- const lookupHash = ctx.isIntercept ? paramHash + "/i" : paramHash;
387
- const entry = await prerenderStoreInstance!.get(
388
- ctx.matched.routeKey,
389
- lookupHash,
390
- {
391
- pathname: ctx.pathname,
392
- isPassthroughRoute: isPassthroughPrerenderRoute,
393
- },
394
- );
395
- if (!entry) return false;
396
- yield* yieldFromStore(
397
- entry,
398
- ctx,
399
- state,
400
- pipelineStart,
401
- reqCtx,
402
- resolveLoadersOnly,
403
- resolveLoadersOnlyWithRevalidation,
404
- );
405
- return true;
493
+ // 1. Writable durable overlay (per-request, env-scoped), read before the
494
+ // bundled manifest: it is always newer. Only on-demand routes are read
495
+ // (router.prerender() refuses non-od routes, so a pr-only route always
496
+ // misses), and never for intercept navigations (only the main variant is
497
+ // written). Actions read it too: a plain on-demand route has no live
498
+ // handler, so skipping the overlay would 404 an overlay-only param
499
+ // (gateOnDemandProducer). A store error degrades to a miss
500
+ // (readVerifiedStoredEntry); a stale hit serves and may schedule onRevalidate.
501
+ if (overlay && ctx.matched.od && !ctx.isIntercept) {
502
+ const key: PrerenderKey = {
503
+ routerId: overlay.routerId,
504
+ version: overlay.version,
505
+ routeName: ctx.matched.routeKey,
506
+ paramHash: _hashParams!(ctx.matched.params),
507
+ };
508
+ const stored = await readVerifiedStoredEntry(
509
+ overlay.config.store,
510
+ key,
511
+ ctx.matched.params,
512
+ );
513
+ if (stored) {
514
+ // A stale "removed" marker is rechecked like a stale page: only a
515
+ // refresh that hit notFound() (or declined) stamps one with a ttl or
516
+ // tags. A prerender.remove() marker has neither and never gets here.
517
+ if (
518
+ overlay.config.onRevalidate &&
519
+ reqCtx &&
520
+ isStoredEntryStale(stored, Date.now())
521
+ ) {
522
+ scheduleOverlayRevalidation(
523
+ serializePrerenderKey(key),
524
+ overlay.config.onRevalidate,
525
+ {
526
+ route: ctx.matched.routeKey,
527
+ params: ctx.matched.params,
528
+ } as PrerenderTargetObject,
529
+ reqCtx,
530
+ );
531
+ }
532
+ if (stored.removed === true) {
533
+ // The page was removed. Not served, and step 2 is skipped: the
534
+ // build-time entry below must not come back. The route's handler
535
+ // answers: a Passthrough route's live handler, or a plain route's
536
+ // gated producer, which `_prerenderRemoved` makes a 404 in dev too
537
+ // (gateOnDemandProducer).
538
+ (
539
+ ctx.handlerContext as InternalHandlerContext<any, TEnv>
540
+ )._prerenderRemoved = true;
541
+ return false;
542
+ }
543
+ yield* yieldFromStore(
544
+ stored.entry,
545
+ ctx,
546
+ state,
547
+ pipelineStart,
548
+ reqCtx,
549
+ resolveLoadersOnly,
550
+ resolveLoadersOnlyWithRevalidation,
551
+ true,
552
+ );
553
+ return true;
554
+ }
555
+ }
556
+
557
+ // 2. Bundled build manifest (immutable). Absent for on-demand-only routes.
558
+ if (prerenderStoreInstance) {
559
+ const paramHash = _hashParams!(ctx.matched.params);
560
+ const entry = await prerenderStoreInstance.get(
561
+ ctx.matched.routeKey,
562
+ ctx.isIntercept ? paramHash + "/i" : paramHash,
563
+ {
564
+ pathname: ctx.pathname,
565
+ isPassthroughRoute: isPassthroughPrerenderRoute,
566
+ },
567
+ );
568
+ if (entry) {
569
+ yield* yieldFromStore(
570
+ entry,
571
+ ctx,
572
+ state,
573
+ pipelineStart,
574
+ reqCtx,
575
+ resolveLoadersOnly,
576
+ resolveLoadersOnlyWithRevalidation,
577
+ );
578
+ return true;
579
+ }
580
+ }
581
+
582
+ return false;
406
583
  }
407
584
 
408
585
  /**
@@ -463,6 +640,8 @@ export function withCacheLookup<TEnv>(
463
640
  ? "intercept"
464
641
  : undefined;
465
642
  }
643
+ // Per-request writable prerender overlay (durable), resolved by the handler.
644
+ const prerenderOverlay = pipelineReqCtx?._prerender;
466
645
 
467
646
  const {
468
647
  evaluateRevalidation,
@@ -473,20 +652,30 @@ export function withCacheLookup<TEnv>(
473
652
  resolveAllSegmentsWithRevalidation,
474
653
  } = getRouterContext<TEnv>();
475
654
 
476
- if (prerenderStoreShortCircuits(ctx.matched.pr, ctx.request)) {
655
+ // An on-demand route may have no build-baked entry yet still needs a
656
+ // writable-overlay lookup, so od joins pr in the gate. The retained producer
657
+ // is NEVER run by this pipeline: a miss falls through like pr + miss.
658
+ const isPassthroughPrerenderRoute = ctx.entries.some(
659
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
660
+ );
661
+ const overlayEligible =
662
+ ctx.matched.od === true && !requestHeaders(ctx.request).get("X-RSC-HMR");
663
+ if (
664
+ prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
665
+ overlayEligible
666
+ ) {
477
667
  // Actions normally re-render fresh and skip the prerender store. But a pure
478
668
  // Prerender route's handler is evicted at build, so there is no fresh
479
669
  // handler to run on an action re-render -- without the fallback the
480
670
  // re-render falls through to the evicted handler and throws "No prerender
481
671
  // data found". Serve the prerendered entry instead (the action ran already;
482
- // its result is applied client-side via useActionState). Passthrough routes
483
- // keep a liveHandler, so they still re-render fresh on actions.
484
- const isPassthroughPrerenderRoute = ctx.entries.some(
485
- (entry) => entry.type === "route" && entry.isPassthrough === true,
486
- );
672
+ // its result is applied client-side via useActionState). A plain on-demand
673
+ // route is the same case, served from the overlay first (an overlay entry
674
+ // is newer than the build one, and may be the only one). Passthrough
675
+ // routes keep a liveHandler, so they still re-render fresh on actions.
487
676
  if (!ctx.isAction || !isPassthroughPrerenderRoute) {
488
677
  await ensurePrerenderDeps();
489
- if (prerenderStoreInstance) {
678
+ if (prerenderStoreInstance || prerenderOverlay) {
490
679
  const served = yield* tryPrerenderLookup(
491
680
  ctx,
492
681
  state,
@@ -494,6 +683,8 @@ export function withCacheLookup<TEnv>(
494
683
  pipelineReqCtx,
495
684
  resolveLoadersOnly,
496
685
  resolveLoadersOnlyWithRevalidation,
686
+ isPassthroughPrerenderRoute,
687
+ prerenderOverlay,
497
688
  );
498
689
  if (served) return;
499
690
  }
@@ -518,6 +709,7 @@ export function withCacheLookup<TEnv>(
518
709
  pipelineReqCtx,
519
710
  resolveLoadersOnly,
520
711
  resolveLoadersOnlyWithRevalidation,
712
+ isPassthroughPrerenderRoute,
521
713
  );
522
714
  if (served) return;
523
715
  }
@@ -330,6 +330,8 @@ export interface RouteMatchResult<TEnv = any> {
330
330
  pr?: true;
331
331
  /** Passthrough: handler kept for live fallback on unknown params (from trie) */
332
332
  pt?: true;
333
+ /** On-demand prerender eligible (router.prerender() refreshable) */
334
+ od?: true;
333
335
  /** Response type for non-RSC routes (json, text, image, any) */
334
336
  responseType?: string;
335
337
  /** Negotiate variants: response-type routes sharing this path */
@@ -455,6 +457,9 @@ export function findMatch<TEnv>(
455
457
  const ptFlag = entry.passthroughRouteKeys?.has(routeKey)
456
458
  ? { pt: true as const }
457
459
  : {};
460
+ const odFlag = entry.onDemandRouteKeys?.has(routeKey)
461
+ ? { od: true as const }
462
+ : {};
458
463
 
459
464
  const match = regex.exec(pathname);
460
465
  if (match) {
@@ -484,6 +489,7 @@ export function findMatch<TEnv>(
484
489
  redirectTo: pathname + "/",
485
490
  ...prFlag,
486
491
  ...ptFlag,
492
+ ...odFlag,
487
493
  };
488
494
  } else if (trailingSlashMode === "never" && pathnameHasTrailingSlash) {
489
495
  return {
@@ -493,6 +499,7 @@ export function findMatch<TEnv>(
493
499
  redirectTo: pathname.slice(0, -1),
494
500
  ...prFlag,
495
501
  ...ptFlag,
502
+ ...odFlag,
496
503
  };
497
504
  }
498
505
 
@@ -502,6 +509,7 @@ export function findMatch<TEnv>(
502
509
  params,
503
510
  ...prFlag,
504
511
  ...ptFlag,
512
+ ...odFlag,
505
513
  };
506
514
  }
507
515
 
@@ -520,6 +528,7 @@ export function findMatch<TEnv>(
520
528
  params,
521
529
  ...prFlag,
522
530
  ...ptFlag,
531
+ ...odFlag,
523
532
  };
524
533
  } else if (trailingSlashMode === "never") {
525
534
  if (pathnameHasTrailingSlash) {
@@ -530,6 +539,7 @@ export function findMatch<TEnv>(
530
539
  redirectTo: alternatePathname,
531
540
  ...prFlag,
532
541
  ...ptFlag,
542
+ ...odFlag,
533
543
  };
534
544
  }
535
545
  return {
@@ -538,6 +548,7 @@ export function findMatch<TEnv>(
538
548
  params,
539
549
  ...prFlag,
540
550
  ...ptFlag,
551
+ ...odFlag,
541
552
  };
542
553
  } else if (trailingSlashMode === "always") {
543
554
  if (!pathnameHasTrailingSlash) {
@@ -548,6 +559,7 @@ export function findMatch<TEnv>(
548
559
  redirectTo: alternatePathname,
549
560
  ...prFlag,
550
561
  ...ptFlag,
562
+ ...odFlag,
551
563
  };
552
564
  }
553
565
  return {
@@ -556,6 +568,7 @@ export function findMatch<TEnv>(
556
568
  params,
557
569
  ...prFlag,
558
570
  ...ptFlag,
571
+ ...odFlag,
559
572
  };
560
573
  } else {
561
574
  const canonicalPath = hasTrailingSlash
@@ -568,6 +581,7 @@ export function findMatch<TEnv>(
568
581
  redirectTo: canonicalPath,
569
582
  ...prFlag,
570
583
  ...ptFlag,
584
+ ...odFlag,
571
585
  };
572
586
  }
573
587
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Resolve the router-wide default Link prefetch strategy once, at router init.
3
- * The resolved value ships to the client in payload metadata; the browser
4
- * entry applies it via `setDefaultPrefetchStrategy` (browser/prefetch/
5
- * default-strategy.ts) and every `<Link>` without an explicit `prefetch` prop
6
- * falls back to it.
3
+ * The resolved value ships to the client in payload metadata; the router
4
+ * store carries it (`NavigationStoreContext.defaultPrefetch`, see
5
+ * browser/prefetch/default-strategy.ts) and every `<Link>` without an explicit
6
+ * `prefetch` prop falls back to it.
7
7
  *
8
8
  * The type lives HERE (not in browser/react/Link.tsx) because both seats need
9
9
  * it: this server-side resolver and the client Link/default-strategy modules.