@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
@@ -37,6 +37,12 @@ export interface TrieLeaf {
37
37
  pr?: true;
38
38
  /** Passthrough: handler kept in bundle for live fallback on unknown params */
39
39
  pt?: true;
40
+ /**
41
+ * On-demand prerender eligible: the route opted into router.prerender()
42
+ * refresh. Distinct from `pr` — an od route may have no build-baked entry yet
43
+ * still needs a writable-overlay lookup at serve time.
44
+ */
45
+ od?: true;
40
46
  /** Response type for non-RSC routes (json, text, image, any) */
41
47
  rt?: string;
42
48
  /** Negotiate variants: response-type routes sharing this path */
@@ -72,6 +78,7 @@ export interface TrieNode {
72
78
  * @param prerenderRouteNames - Optional set of prerendered route names (sets leaf.pr)
73
79
  * @param passthroughRouteNames - Optional set of passthrough route names (sets leaf.pt)
74
80
  * @param responseTypeRoutes - Optional map of route name to response type (sets leaf.rt)
81
+ * @param onDemandRouteNames - Optional set of on-demand-eligible route names (sets leaf.od)
75
82
  */
76
83
  export function buildRouteTrie(
77
84
  routeManifest: Record<string, string>,
@@ -80,6 +87,7 @@ export function buildRouteTrie(
80
87
  prerenderRouteNames?: Set<string>,
81
88
  passthroughRouteNames?: Set<string>,
82
89
  responseTypeRoutes?: Record<string, string>,
90
+ onDemandRouteNames?: Set<string>,
83
91
  ): TrieNode {
84
92
  const root: TrieNode = {};
85
93
 
@@ -99,6 +107,7 @@ export function buildRouteTrie(
99
107
  ...(trailingSlash ? { ts: trailingSlash } : {}),
100
108
  ...(prerenderRouteNames?.has(routeName) ? { pr: true } : {}),
101
109
  ...(passthroughRouteNames?.has(routeName) ? { pt: true } : {}),
110
+ ...(onDemandRouteNames?.has(routeName) ? { od: true } : {}),
102
111
  ...(responseType ? { rt: responseType } : {}),
103
112
  });
104
113
  }
@@ -100,6 +100,7 @@ export interface RouterContext<TEnv = any> {
100
100
  parent: any,
101
101
  fn: () => T,
102
102
  ) => T;
103
+ runIsolated: <T>(forRoute: string, fn: () => T) => T;
103
104
  };
104
105
 
105
106
  getMetricsStore: () => MetricsStore | undefined;
@@ -225,6 +225,29 @@ export interface Rango<
225
225
  * ```
226
226
  */
227
227
  fetch(request: Request, input?: RouterRequestInput<TEnv>): Promise<Response>;
228
+
229
+ /**
230
+ * Bind `{ env, ctx }` and get the on-demand (ISR-style) prerender runner, to refresh from a running app —
231
+ * fetch handler, cron, queue, workflow, webhook, or server action.
232
+ *
233
+ * Requestless: the producer renders with route params and env only, never the
234
+ * caller's cookies/headers/session. Only routes that opted in via
235
+ * `Prerender(..., { onDemand })` are persistable; others return a skipped
236
+ * result. Configure the store via the `prerender` router option.
237
+ *
238
+ * @example
239
+ * ```ts
240
+ * const prerender = router.prerender({ env, ctx });
241
+ * await prerender("/products/42");
242
+ * await prerender(
243
+ * { route: "products.detail", params: { id: "42" } },
244
+ * { throwOnError: true },
245
+ * );
246
+ * await prerender.many(targets, { concurrency: 4 });
247
+ * await prerender.markStale(["product:42"]);
248
+ * ```
249
+ */
250
+ prerender: import("../prerender/on-demand.js").PrerenderFn<TEnv, TRoutes>;
228
251
  }
229
252
 
230
253
  /**
@@ -332,6 +355,13 @@ export interface RangoInternal<
332
355
  */
333
356
  readonly cache?: RangoOptions<TEnv>["cache"];
334
357
 
358
+ /**
359
+ * Prerender store configuration (the writable durable overlay for on-demand
360
+ * prerender). Stored separately from the `prerender` trigger method so the RSC
361
+ * handler can resolve it per request. @internal
362
+ */
363
+ readonly _prerenderConfig?: RangoOptions<TEnv>["prerender"];
364
+
335
365
  /**
336
366
  * Not found component to render when no route matches
337
367
  */
@@ -620,6 +650,9 @@ export interface RangoInternal<
620
650
  * Handle an RSC request.
621
651
  */
622
652
  fetch(request: Request, input?: RouterRequestInput<TEnv>): Promise<Response>;
653
+
654
+ /** On-demand prerender binder: router.prerender({ env, ctx }) returns the runner. */
655
+ prerender: import("../prerender/on-demand.js").PrerenderFn<TEnv, TRoutes>;
623
656
  }
624
657
 
625
658
  /**
@@ -341,6 +341,36 @@ export interface RangoOptions<TEnv = any> {
341
341
  searchParams?: CacheSearchParams;
342
342
  });
343
343
 
344
+ /**
345
+ * Writable prerender store for on-demand (ISR-style) prerender refresh.
346
+ *
347
+ * A durable overlay in front of the bundled build manifest. When provided,
348
+ * `router.prerender(target, { env, ctx })` renders a route requestlessly and
349
+ * writes a versioned payload here; the serve path reads the overlay before the
350
+ * bundled manifest. Same union shape as `cache`: a plain config object or a
351
+ * factory resolved per request/per call (never at `createRouter()` time), so
352
+ * multi-tenant apps never memoize one request's binding for another.
353
+ *
354
+ * @example Cloudflare KV overlay with SWR revalidation
355
+ * ```typescript
356
+ * import { createKVPrerenderStore } from "@rangojs/router/prerender/cloudflare";
357
+ *
358
+ * const router = createRouter<AppEnv>({
359
+ * prerender: (env) => ({
360
+ * store: createKVPrerenderStore(env.PRERENDER_KV),
361
+ * ttl: 3600,
362
+ * onRevalidate: (target, e) => e.PRERENDER_QUEUE.send({ target }),
363
+ * }),
364
+ * });
365
+ * ```
366
+ */
367
+ prerender?:
368
+ | import("../prerender/on-demand.js").PrerenderConfig<TEnv>
369
+ | ((
370
+ env: TEnv,
371
+ ctx?: ExecutionContext,
372
+ ) => import("../prerender/on-demand.js").PrerenderConfig<TEnv>);
373
+
344
374
  /**
345
375
  * Named cache profiles for "use cache" directive.
346
376
  * Profile names map to TTL/SWR configuration.
@@ -36,7 +36,7 @@ import {
36
36
  import { applyViewTransitionDefault } from "./view-transition-default.js";
37
37
  import { _getRequestContext } from "../../server/request-context.js";
38
38
  import { getRouterContext } from "../router-context.js";
39
- import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
39
+ import { trackStreamedHandler } from "./streamed-handler-boundary.js";
40
40
  import { observeSegmentHandler } from "../instrument.js";
41
41
  import {
42
42
  track,
@@ -280,6 +280,13 @@ export interface ResolveSegmentOptions {
280
280
  * #918: resolved twice on a MISS, once and discarded on a HIT).
281
281
  */
282
282
  chain?: readonly EntryData[];
283
+ /**
284
+ * The matched chain entry being resolved: where the sync path catches a
285
+ * handler error, so a streamed handler (or slot owned by a routeless layout)
286
+ * recovers into the same boundary. resolveSegment sets it for the entry it
287
+ * resolves; nested resolutions keep the outer value.
288
+ */
289
+ boundaryEntry?: EntryData;
283
290
  }
284
291
 
285
292
  /**
@@ -298,6 +305,8 @@ export async function resolveSegment<TEnv>(
298
305
  options?: ResolveSegmentOptions,
299
306
  ): Promise<ResolvedSegment[]> {
300
307
  const segments: ResolvedSegment[] = [];
308
+ const boundaryEntry = options?.boundaryEntry ?? entry;
309
+ if (!options?.boundaryEntry) options = { ...options, boundaryEntry };
301
310
 
302
311
  if (entry.type === "layout" || entry.type === "cache") {
303
312
  if (!options?.skipLoaders) {
@@ -398,19 +407,15 @@ export async function resolveSegment<TEnv>(
398
407
  if (result instanceof Promise) {
399
408
  warnOnStreamedResponse(result, entry.id);
400
409
  result.finally(doneRouteHandler).catch(() => {});
401
- const tracked = deps.trackHandler(result, {
410
+ component = trackStreamedHandler(deps, result, {
411
+ boundaryEntry,
402
412
  segmentId: entry.shortCode,
403
413
  segmentType: entry.type,
404
- });
405
- observeStreamedHandler(
406
- tracked,
407
- entry.shortCode,
408
- entry.type,
409
- context.pathname,
414
+ context,
410
415
  routeKey,
411
416
  params,
412
- );
413
- component = tracked;
417
+ recover: !options?.throwOnError,
418
+ });
414
419
  } else {
415
420
  doneRouteHandler();
416
421
  component = result;
@@ -679,19 +684,15 @@ export async function resolveParallelEntry<TEnv>(
679
684
  : handler;
680
685
  if (result instanceof Promise) {
681
686
  result.finally(doneParallelHandler).catch(() => {});
682
- const tracked = deps.trackHandler(result, {
687
+ component = trackStreamedHandler(deps, result, {
688
+ boundaryEntry: options?.boundaryEntry ?? parallelEntry,
683
689
  segmentId: `${parentShortCode}.${slot}`,
684
690
  segmentType: "parallel",
685
- });
686
- observeStreamedHandler(
687
- tracked,
688
- `${parentShortCode}.${slot}`,
689
- "parallel",
690
- context.pathname,
691
+ context,
691
692
  routeKey,
692
693
  params,
693
- );
694
- component = tracked as ReactNode;
694
+ recover: !options?.throwOnError,
695
+ });
695
696
  } else {
696
697
  doneParallelHandler();
697
698
  component = result as ReactNode;
@@ -9,7 +9,8 @@
9
9
  * 1. options.key(requestCtx) — full override, stored namespaced as
10
10
  * loader:{loaderId}:key:{encodeURIComponent(result)} (#1009)
11
11
  * 2. store.keyGenerator(requestCtx, defaultKey) — store-level modification
12
- * 3. loader:{loaderId}:{host}{pathname}:{sortedParams} — default
12
+ * 3. loader:{loaderId}:{routerId}@{host}{pathname}:{sortedParams} — default
13
+ * (a loader's ctx is one router's: cache-key-utils.ts, the router rule)
13
14
  *
14
15
  * Values are serialized via RSC Flight (serializeResult/deserializeResult),
15
16
  * supporting ReactNode, Promises, null, and all RSC-serializable types.
@@ -77,7 +78,7 @@ import {
77
78
  import { observePhase, PHASES } from "../instrument.js";
78
79
  import {
79
80
  KEY_PART_PREFIX,
80
- sortedRouteParams,
81
+ requestKeyBase,
81
82
  } from "../../cache/cache-key-utils.js";
82
83
  import {
83
84
  resolveTtl,
@@ -88,6 +89,7 @@ import {
88
89
  DEFAULT_ROUTE_TTL,
89
90
  } from "../../cache/cache-policy.js";
90
91
  import { readThroughItem } from "../../cache/read-through-swr.js";
92
+ import { isWarmReplace, noteWarmWrite } from "../../prerender/warm-request.js";
91
93
  import {
92
94
  executionStart,
93
95
  predatesInvalidation,
@@ -138,17 +140,6 @@ function debugLoaderCacheLog(message: string): void {
138
140
  }
139
141
  }
140
142
 
141
- function getDefaultLoaderCacheKey(
142
- loaderId: string,
143
- host: string,
144
- pathname: string,
145
- params: Record<string, string>,
146
- ): string {
147
- const paramStr = sortedRouteParams(params);
148
- const base = paramStr ? `${pathname}:${paramStr}` : pathname;
149
- return `loader:${loaderId}:${host}${base}`;
150
- }
151
-
152
143
  /**
153
144
  * A loader's own `key()` result as its entry key (#1009). The result is
154
145
  * often request input, and the item family (`getItem`/`setItem`) has no
@@ -159,7 +150,7 @@ function getDefaultLoaderCacheKey(
159
150
  * - an encoded result holds no `:`, so the key reads unambiguously from
160
151
  * the right (result, `key:`, loader id), and two namespaced keys are
161
152
  * equal only for the same loader and result;
162
- * - a default key `loader:<id>:<host><pathname>...` holds a `/` past its
153
+ * - a default key `loader:<id>:[<router>@]<host><pathname>...` holds a `/` past its
163
154
  * id, and an encoded result holds none, so the two are equal only if one
164
155
  * loader's id is literally `<other id>:<host>/<path>...`. Loader ids can
165
156
  * hold `:` (a dev id is a root-relative path, absolute `D:/...` on
@@ -188,8 +179,9 @@ async function resolveLoaderKey(
188
179
  // (cache-runtime pushes ctx.url.host). Without it, a multi-tenant host router
189
180
  // serving the same pathname for different hosts would leak one host's cached
190
181
  // loader data to another.
191
- const host = getRequestContext()?.url?.host ?? "localhost";
192
- const defaultKey = getDefaultLoaderCacheKey(loaderId, host, pathname, params);
182
+ const requestCtx = getRequestContext();
183
+ const host = requestCtx?.url?.host ?? "localhost";
184
+ const defaultKey = `loader:${loaderId}:${requestKeyBase(requestCtx, host, pathname, undefined, params)}`;
193
185
  if (options === false) return { key: defaultKey, declared: false };
194
186
  const keyFn = options.key;
195
187
  return {
@@ -768,7 +760,11 @@ function executeLoaderData<TEnv>(
768
760
  };
769
761
 
770
762
  const data = await readThroughItem({
771
- getItem: (k) => store.getItem!(k),
763
+ // A router.prerender() warm takes the miss path: the body runs and
764
+ // setItem replaces the entry.
765
+ getItem: isWarmReplace(requestCtxForExecute)
766
+ ? async () => null
767
+ : (k) => store.getItem!(k),
772
768
  // Handles ride the entry like "use cache" (encodeHandles: Flight, pending
773
769
  // pushes awaited up to its timeout, the whole blob dropped on a timeout
774
770
  // or a thrown encode). Encoded here, inside the deferred write, so a
@@ -795,7 +791,9 @@ function executeLoaderData<TEnv>(
795
791
  ...o,
796
792
  tags: entryTags.length > 0 ? entryTags : undefined,
797
793
  ...(handles ? { handles } : {}),
794
+ startedAt: start?.at,
798
795
  });
796
+ noteWarmWrite(requestCtxForExecute, "item");
799
797
  }),
800
798
  key,
801
799
  execute: async () => {
@@ -47,7 +47,7 @@ import { applyViewTransitionDefault } from "./view-transition-default.js";
47
47
  import { getRouterContext } from "../router-context.js";
48
48
  import { observeEvent, observeSegmentHandler } from "../instrument.js";
49
49
  import { runInSegmentTagScope } from "../../cache/cache-tag.js";
50
- import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
50
+ import { trackStreamedHandler } from "./streamed-handler-boundary.js";
51
51
  import {
52
52
  track,
53
53
  RangoContext,
@@ -441,6 +441,8 @@ async function resolveParallelSlotComponent<TEnv>(args: {
441
441
  deps: SegmentResolutionDeps<TEnv>;
442
442
  routeKey: string;
443
443
  params: Record<string, string>;
444
+ /** Matched chain entry (where the sync path catches), else the slot's owner. */
445
+ boundaryEntry: EntryData;
444
446
  }): Promise<{ component: ReactNode | undefined; handlerRan: boolean }> {
445
447
  const {
446
448
  shouldResolve,
@@ -452,6 +454,7 @@ async function resolveParallelSlotComponent<TEnv>(args: {
452
454
  deps,
453
455
  routeKey,
454
456
  params,
457
+ boundaryEntry,
455
458
  } = args;
456
459
 
457
460
  let component: ReactNode | undefined;
@@ -484,19 +487,15 @@ async function resolveParallelSlotComponent<TEnv>(args: {
484
487
  : handler;
485
488
  if (result instanceof Promise) {
486
489
  warnOnStreamedResponse(result, parallelId);
487
- const tracked = deps.trackHandler(result, {
490
+ component = trackStreamedHandler(deps, result, {
491
+ boundaryEntry,
488
492
  segmentId: parallelId,
489
493
  segmentType: "parallel",
490
- });
491
- observeStreamedHandler(
492
- tracked,
493
- parallelId,
494
- "parallel",
495
- context.pathname,
494
+ context,
496
495
  routeKey,
497
496
  params,
498
- );
499
- component = tracked as ReactNode;
497
+ recover: true,
498
+ });
500
499
  } else {
501
500
  component = result as ReactNode;
502
501
  }
@@ -547,6 +546,8 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
547
546
  * to it: loader sub-ids are filtered out and slots are re-grouped by parent).
548
547
  */
549
548
  loaderOrder?: "after" | "before";
549
+ /** Matched chain entry being resolved; see ResolveSegmentOptions.boundaryEntry. */
550
+ boundaryEntry?: EntryData;
550
551
  },
551
552
  ): Promise<SegmentRevalidationResult> {
552
553
  const segments: ResolvedSegment[] = [];
@@ -696,6 +697,7 @@ export async function resolveParallelSegmentsWithRevalidation<TEnv>(
696
697
  deps,
697
698
  routeKey,
698
699
  params,
700
+ boundaryEntry: options?.boundaryEntry ?? entry,
699
701
  });
700
702
 
701
703
  segments.push({
@@ -865,19 +867,17 @@ export async function resolveEntryHandlerWithRevalidation<TEnv>(
865
867
  if (result instanceof Promise) {
866
868
  warnOnStreamedResponse(result, routeEntry.id);
867
869
  result.finally(doneHandler).catch(() => {});
868
- const tracked = deps.trackHandler(result, {
869
- segmentId: entry.shortCode,
870
- segmentType: entry.type,
871
- });
872
- observeStreamedHandler(
873
- tracked,
874
- entry.shortCode,
875
- entry.type,
876
- context.pathname,
877
- routeKey,
878
- params,
879
- );
880
- return { content: tracked };
870
+ return {
871
+ content: trackStreamedHandler(deps, result, {
872
+ boundaryEntry: entry,
873
+ segmentId: entry.shortCode,
874
+ segmentType: entry.type,
875
+ context,
876
+ routeKey,
877
+ params,
878
+ recover: true,
879
+ }),
880
+ };
881
881
  }
882
882
  doneHandler();
883
883
  return { content: result };
@@ -1098,6 +1098,8 @@ export async function resolveSegmentWithRevalidation<TEnv>(
1098
1098
  deps,
1099
1099
  actionContext,
1100
1100
  stale,
1101
+ undefined,
1102
+ entry,
1101
1103
  );
1102
1104
  segments.push(...orphanResult.segments);
1103
1105
  matchedIds.push(...orphanResult.matchedIds);
@@ -1126,6 +1128,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1126
1128
  stale?: boolean,
1127
1129
  /** Parent route entry — its loaders are inherited so parallel slots can access them. */
1128
1130
  parentRouteEntry?: EntryData,
1131
+ /** Matched chain entry being resolved (see ResolveSegmentOptions.boundaryEntry); defaults to parentRouteEntry. */
1132
+ boundaryEntry: EntryData | undefined = parentRouteEntry,
1129
1133
  ): Promise<SegmentRevalidationResult> {
1130
1134
  invariant(
1131
1135
  orphan.type === "layout" || orphan.type === "cache",
@@ -1286,7 +1290,11 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1286
1290
  deps,
1287
1291
  actionContext,
1288
1292
  stale,
1289
- { parentChainDefault: "force-render", loaderOrder: "before" },
1293
+ {
1294
+ parentChainDefault: "force-render",
1295
+ loaderOrder: "before",
1296
+ boundaryEntry,
1297
+ },
1290
1298
  );
1291
1299
  segments.push(...parallelResult.segments);
1292
1300
  matchedIds.push(...parallelResult.matchedIds);
@@ -1307,6 +1315,8 @@ export async function resolveOrphanLayoutWithRevalidation<TEnv>(
1307
1315
  deps,
1308
1316
  actionContext,
1309
1317
  stale,
1318
+ undefined,
1319
+ boundaryEntry,
1310
1320
  );
1311
1321
  segments.push(...nestedResult.segments);
1312
1322
  matchedIds.push(...nestedResult.matchedIds);
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Streamed handlers (loading()): track, observe, and recover a rejection into
3
+ * the declared errorBoundary() / notFoundBoundary() fallback.
4
+ *
5
+ * Invariants:
6
+ * - No status write and no onError call: the status is committed at first
7
+ * flush, and trackHandler already reports each rejection once.
8
+ * - The recovered promise resolves, so Flight reports no error. The writers
9
+ * that refuse a failed render through Flight errors need the failure
10
+ * recorded here: `_renderErrors` for the document cache and a prerender
11
+ * warm, `_recoveredHandlerErrors` for cacheRoute (cache(), and the doc record
12
+ * a PPR capture needs). The shell capture and the SWR re-render own both,
13
+ * so a derived render never writes into (or reads) the foreground's.
14
+ * - Nothing to render to rethrows the original rejection: no boundary found, a
15
+ * thrown Response (redirect), a Skip, a throwing fallback.
16
+ * - No recovery when `recover` is false (prerender throwOnError, the intercept
17
+ * background re-render): those lanes must reject so nothing is baked/cached.
18
+ */
19
+
20
+ import type { ReactNode } from "react";
21
+ import { isDataNotFoundError } from "../../errors.js";
22
+ import {
23
+ createErrorInfo,
24
+ createNotFoundInfo,
25
+ renderErrorFallback,
26
+ renderNotFoundFallback,
27
+ resolveNotFoundFallback,
28
+ } from "../error-handling.js";
29
+ import { _getRequestContext } from "../../server/request-context.js";
30
+ import type { EntryData } from "../../server/context.js";
31
+ import type { ErrorInfo, HandlerContext } from "../../types/index.js";
32
+ import type { SegmentResolutionDeps } from "../types.js";
33
+ import { observeStreamedHandler } from "./streamed-handler-telemetry.js";
34
+
35
+ export interface StreamedHandlerOptions<TEnv> {
36
+ /**
37
+ * Entry to look boundaries up from: the matched chain entry being resolved
38
+ * (where the sync path catches, whatever routeless layout owns a slot), or
39
+ * the intercept's declaring entry.
40
+ */
41
+ boundaryEntry: EntryData;
42
+ segmentId: string;
43
+ segmentType: ErrorInfo["segmentType"];
44
+ context: HandlerContext<any, TEnv>;
45
+ routeKey?: string;
46
+ params: Record<string, string>;
47
+ /** False where the rejection must reach the caller (prerender, intercept background re-render). */
48
+ recover: boolean;
49
+ }
50
+
51
+ const NOOP = (): void => {};
52
+
53
+ /**
54
+ * Track a streamed handler promise and return the segment component: the
55
+ * tracked promise itself when there is nothing to recover to, else a promise
56
+ * that resolves to the declared fallback on rejection.
57
+ */
58
+ export function trackStreamedHandler<TEnv>(
59
+ deps: SegmentResolutionDeps<TEnv>,
60
+ result: Promise<unknown>,
61
+ options: StreamedHandlerOptions<TEnv>,
62
+ ): ReactNode {
63
+ const { segmentId, segmentType, context } = options;
64
+ const tracked = deps.trackHandler(result as Promise<ReactNode>, {
65
+ segmentId,
66
+ segmentType,
67
+ });
68
+ observeStreamedHandler(
69
+ tracked,
70
+ segmentId,
71
+ segmentType,
72
+ context.pathname,
73
+ options.routeKey,
74
+ options.params,
75
+ );
76
+
77
+ if (!options.recover) return tracked as ReactNode;
78
+ const { boundaryEntry } = options;
79
+ const hasBoundary =
80
+ deps.findNearestErrorBoundary(boundaryEntry) != null ||
81
+ deps.findNearestNotFoundBoundary(boundaryEntry) != null ||
82
+ deps.notFoundComponent !== undefined;
83
+ if (!hasBoundary) return tracked as ReactNode;
84
+
85
+ // Captured now: the rejection can settle outside the request's ALS frame.
86
+ const reqCtx = _getRequestContext();
87
+ const recovered = tracked.catch((error: unknown): ReactNode => {
88
+ const node = renderBoundary(error, deps, options, boundaryEntry);
89
+ if (node === undefined) throw error;
90
+ if (reqCtx)
91
+ (reqCtx._recoveredHandlerErrors ??= new Map()).set(segmentId, error);
92
+ reqCtx?._renderErrors?.push(error);
93
+ return node;
94
+ });
95
+ // A dropped segment (never rendered) must not become an unhandledRejection.
96
+ recovered.catch(NOOP);
97
+ return recovered as ReactNode;
98
+ }
99
+
100
+ function renderBoundary<TEnv>(
101
+ error: unknown,
102
+ deps: SegmentResolutionDeps<TEnv>,
103
+ options: StreamedHandlerOptions<TEnv>,
104
+ boundaryEntry: EntryData,
105
+ ): ReactNode | undefined {
106
+ if (error instanceof Response) return undefined;
107
+ if ((error as { name?: string } | null)?.name === "Skip") return undefined;
108
+ const { segmentId, segmentType, context } = options;
109
+ try {
110
+ if (isDataNotFoundError(error)) {
111
+ const boundary = deps.findNearestNotFoundBoundary(boundaryEntry);
112
+ if (boundary == null && deps.notFoundComponent === undefined) {
113
+ return undefined;
114
+ }
115
+ return renderNotFoundFallback(
116
+ resolveNotFoundFallback(
117
+ boundary,
118
+ deps.notFoundComponent,
119
+ context.pathname,
120
+ ),
121
+ createNotFoundInfo(error, segmentId, segmentType, context.pathname),
122
+ );
123
+ }
124
+ const fallback = deps.findNearestErrorBoundary(boundaryEntry);
125
+ if (fallback == null) return undefined;
126
+ return renderErrorFallback(
127
+ fallback,
128
+ createErrorInfo(error, segmentId, segmentType),
129
+ );
130
+ } catch {
131
+ return undefined;
132
+ }
133
+ }
@@ -25,6 +25,8 @@ export interface TrieMatchResult {
25
25
  pr?: true;
26
26
  /** Passthrough: handler kept for live fallback on unknown params */
27
27
  pt?: true;
28
+ /** On-demand prerender eligible (router.prerender() refreshable) */
29
+ od?: true;
28
30
  /** Response type for non-RSC routes (json, text, image, any) */
29
31
  responseType?: string;
30
32
  /** Negotiate variants: response-type routes sharing this path */
@@ -343,6 +345,7 @@ function validateAndBuild(
343
345
  if (redirectTo) result.redirectTo = redirectTo;
344
346
  if (leaf.pr) result.pr = true;
345
347
  if (leaf.pt) result.pt = true;
348
+ if (leaf.od) result.od = true;
346
349
  if (leaf.rt) result.responseType = leaf.rt;
347
350
  if (leaf.nv) result.negotiateVariants = leaf.nv;
348
351
  if (leaf.rf) result.rscFirst = true;