@rangojs/router 0.27.0 → 0.28.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 (212) hide show
  1. package/dist/bin/rango.js +1 -1
  2. package/dist/types/__internal.d.ts +7 -1
  3. package/dist/types/browser/event-controller.d.ts +2 -1
  4. package/dist/types/browser/history-state.d.ts +3 -9
  5. package/dist/types/browser/navigation-store.d.ts +8 -6
  6. package/dist/types/browser/react/filter-segment-order.d.ts +2 -2
  7. package/dist/types/browser/segment-reconciler.d.ts +6 -4
  8. package/dist/types/browser/types.d.ts +2 -1
  9. package/dist/types/browser/wire-protocol.d.ts +25 -0
  10. package/dist/types/cache/cache-key-utils.d.ts +11 -5
  11. package/dist/types/cache/cache-scope.d.ts +34 -22
  12. package/dist/types/cache/cf/cf-cache-constants.d.ts +2 -2
  13. package/dist/types/cache/cf/cf-cache-store.d.ts +47 -65
  14. package/dist/types/cache/cf/cf-kv-utils.d.ts +1 -1
  15. package/dist/types/cache/handle-snapshot.d.ts +9 -4
  16. package/dist/types/cache/segment-codec.d.ts +14 -0
  17. package/dist/types/cache/shell-snapshot.d.ts +1 -1
  18. package/dist/types/cache/tag-invalidation.d.ts +10 -0
  19. package/dist/types/cache/taint.d.ts +5 -3
  20. package/dist/types/cache/types.d.ts +7 -3
  21. package/dist/types/cache/vercel/vercel-cache-store.d.ts +8 -6
  22. package/dist/types/prefetch-markers.d.ts +5 -14
  23. package/dist/types/prerender/store.d.ts +31 -0
  24. package/dist/types/prerender/warm-request.d.ts +3 -3
  25. package/dist/types/prerender.d.ts +2 -2
  26. package/dist/types/route-name.d.ts +5 -0
  27. package/dist/types/router/error-handling.d.ts +4 -4
  28. package/dist/types/router/instrument.d.ts +4 -3
  29. package/dist/types/router/intercept-resolution.d.ts +25 -9
  30. package/dist/types/router/match-context.d.ts +4 -11
  31. package/dist/types/router/match-handlers.d.ts +2 -6
  32. package/dist/types/router/match-middleware/background-revalidation.d.ts +10 -20
  33. package/dist/types/router/match-middleware/cache-lookup.d.ts +34 -86
  34. package/dist/types/router/match-middleware/segment-resolution.d.ts +8 -6
  35. package/dist/types/router/prerender-match.d.ts +21 -6
  36. package/dist/types/router/preview-match.d.ts +2 -7
  37. package/dist/types/router/request-classification.d.ts +5 -0
  38. package/dist/types/router/router-context.d.ts +7 -46
  39. package/dist/types/router/router-interfaces.d.ts +8 -14
  40. package/dist/types/router/segment-resolution/loader-cache.d.ts +3 -3
  41. package/dist/types/router/segment-resolution/loader-mask.d.ts +4 -4
  42. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +20 -18
  43. package/dist/types/router/segment-resolution/resolution-pass.d.ts +93 -0
  44. package/dist/types/router/segment-resolution/revalidation.d.ts +82 -31
  45. package/dist/types/router/segment-resolution.d.ts +1 -2
  46. package/dist/types/router/segment-wrappers.d.ts +11 -48
  47. package/dist/types/router/telemetry.d.ts +2 -0
  48. package/dist/types/router/types.d.ts +10 -0
  49. package/dist/types/rsc/full-payload.d.ts +2 -2
  50. package/dist/types/rsc/handler-context.d.ts +6 -6
  51. package/dist/types/rsc/json-route-result.d.ts +5 -9
  52. package/dist/types/rsc/request-stages.d.ts +203 -0
  53. package/dist/types/rsc/response-cache-serve.d.ts +7 -12
  54. package/dist/types/rsc/response-route-handler.d.ts +2 -2
  55. package/dist/types/rsc/routine-plan.d.ts +1 -1
  56. package/dist/types/rsc/server-action.d.ts +2 -2
  57. package/dist/types/rsc/shell-capture-constants.d.ts +1 -1
  58. package/dist/types/rsc/shell-capture.d.ts +132 -15
  59. package/dist/types/rsc/shell-serve.d.ts +37 -12
  60. package/dist/types/server/context.d.ts +9 -8
  61. package/dist/types/server/handle-store.d.ts +16 -0
  62. package/dist/types/server/render-pass.d.ts +72 -0
  63. package/dist/types/server/request-context.d.ts +24 -104
  64. package/dist/types/server/rewrite-trail.d.ts +2 -2
  65. package/dist/types/testing/dispatch.d.ts +35 -18
  66. package/dist/types/types/cache-types.d.ts +6 -3
  67. package/dist/types/types/error-types.d.ts +9 -1
  68. package/dist/types/types/handler-context.d.ts +9 -1
  69. package/dist/types/types/segments.d.ts +1 -1
  70. package/dist/types/vite/discovery/build-discovery-phase.d.ts +40 -0
  71. package/dist/types/vite/discovery/build-env.d.ts +22 -0
  72. package/dist/types/vite/discovery/bundle-scan-phase.d.ts +12 -0
  73. package/dist/types/vite/discovery/dev-prerender-cache.d.ts +3 -2
  74. package/dist/types/vite/discovery/dev-server-phase.d.ts +26 -0
  75. package/dist/types/vite/discovery/discovery-errors.d.ts +1 -1
  76. package/dist/types/vite/discovery/state.d.ts +12 -0
  77. package/dist/types/vite/plugins/version-plugin.d.ts +1 -1
  78. package/dist/types/vite/plugins/virtual-entries.d.ts +1 -1
  79. package/dist/types/vite/router-discovery.d.ts +8 -47
  80. package/dist/types/vite/temp-rsc-server.d.ts +39 -0
  81. package/dist/types/vite/utils/prerender-utils.d.ts +9 -0
  82. package/dist/types/wire-params.d.ts +34 -0
  83. package/dist/vite/index.js +3809 -3710
  84. package/package.json +1 -1
  85. package/skills/cache-guide/SKILL.md +8 -5
  86. package/skills/caching/SKILL.md +37 -18
  87. package/skills/document-cache/SKILL.md +13 -1
  88. package/skills/handler-use/SKILL.md +1 -1
  89. package/skills/intercept/SKILL.md +17 -7
  90. package/skills/middleware/SKILL.md +18 -0
  91. package/skills/ppr/SKILL.md +18 -3
  92. package/skills/prerender/SKILL.md +39 -15
  93. package/skills/rango/SKILL.md +5 -1
  94. package/skills/server-actions/SKILL.md +8 -1
  95. package/skills/testing/response-routes.md +6 -4
  96. package/skills/use-cache/SKILL.md +46 -8
  97. package/src/__internal.ts +7 -1
  98. package/src/browser/connection-warmup.ts +3 -1
  99. package/src/browser/event-controller.ts +5 -3
  100. package/src/browser/history-state.ts +6 -12
  101. package/src/browser/navigation-bridge.ts +14 -13
  102. package/src/browser/navigation-client.ts +18 -25
  103. package/src/browser/navigation-store.ts +25 -27
  104. package/src/browser/navigation-transaction.ts +4 -1
  105. package/src/browser/prefetch/cache.ts +2 -1
  106. package/src/browser/prefetch/fetch.ts +8 -16
  107. package/src/browser/prefetch/queue.ts +4 -4
  108. package/src/browser/react/NavigationProvider.tsx +2 -1
  109. package/src/browser/react/filter-segment-order.ts +2 -2
  110. package/src/browser/rsc-router.tsx +1 -0
  111. package/src/browser/segment-reconciler.ts +66 -53
  112. package/src/browser/server-action-bridge.ts +50 -57
  113. package/src/browser/types.ts +2 -1
  114. package/src/browser/wire-protocol.ts +41 -0
  115. package/src/cache/cache-key-utils.ts +11 -5
  116. package/src/cache/cache-runtime.ts +110 -50
  117. package/src/cache/cache-scope.ts +110 -64
  118. package/src/cache/cf/cf-cache-constants.ts +2 -2
  119. package/src/cache/cf/cf-cache-store.ts +486 -874
  120. package/src/cache/cf/cf-kv-utils.ts +1 -1
  121. package/src/cache/document-cache.ts +34 -13
  122. package/src/cache/handle-snapshot.ts +19 -7
  123. package/src/cache/segment-codec.ts +55 -0
  124. package/src/cache/shell-snapshot.ts +2 -2
  125. package/src/cache/tag-invalidation.ts +21 -2
  126. package/src/cache/taint.ts +5 -3
  127. package/src/cache/types.ts +7 -3
  128. package/src/cache/vercel/vercel-cache-store.ts +243 -310
  129. package/src/handles/pushed-by.ts +1 -1
  130. package/src/prefetch-markers.ts +5 -16
  131. package/src/prerender/store.ts +76 -0
  132. package/src/prerender/warm-request.ts +8 -4
  133. package/src/prerender.ts +2 -2
  134. package/src/route-name.ts +8 -0
  135. package/src/router/error-handling.ts +4 -4
  136. package/src/router/instrument.ts +4 -3
  137. package/src/router/intercept-resolution.ts +77 -75
  138. package/src/router/loader-resolution.ts +5 -4
  139. package/src/router/match-api.ts +4 -5
  140. package/src/router/match-context.ts +20 -12
  141. package/src/router/match-handlers.ts +8 -20
  142. package/src/router/match-middleware/background-revalidation.ts +41 -53
  143. package/src/router/match-middleware/cache-lookup.ts +675 -726
  144. package/src/router/match-middleware/cache-store.ts +4 -8
  145. package/src/router/match-middleware/intercept-resolution.ts +3 -23
  146. package/src/router/match-middleware/segment-resolution.ts +34 -61
  147. package/src/router/middleware.ts +69 -11
  148. package/src/router/navigation-snapshot.ts +4 -4
  149. package/src/router/prerender-match.ts +100 -44
  150. package/src/router/preview-match.ts +6 -41
  151. package/src/router/request-classification.ts +13 -7
  152. package/src/router/route-snapshot.ts +2 -5
  153. package/src/router/router-context.ts +10 -95
  154. package/src/router/router-interfaces.ts +8 -14
  155. package/src/router/segment-resolution/loader-cache.ts +14 -9
  156. package/src/router/segment-resolution/loader-mask.ts +5 -5
  157. package/src/router/segment-resolution/prefetch-deferral.ts +30 -27
  158. package/src/router/segment-resolution/resolution-pass.ts +137 -0
  159. package/src/router/segment-resolution/revalidation.ts +897 -776
  160. package/src/router/segment-resolution.ts +4 -13
  161. package/src/router/segment-wrappers.ts +51 -269
  162. package/src/router/telemetry.ts +5 -0
  163. package/src/router/types.ts +10 -0
  164. package/src/router.ts +12 -13
  165. package/src/rsc/full-payload.ts +6 -5
  166. package/src/rsc/handler-context.ts +8 -14
  167. package/src/rsc/handler.ts +126 -507
  168. package/src/rsc/json-route-result.ts +5 -9
  169. package/src/rsc/loader-fetch.ts +6 -5
  170. package/src/rsc/progressive-enhancement.ts +5 -0
  171. package/src/rsc/request-stages.ts +757 -0
  172. package/src/rsc/response-cache-serve.ts +10 -14
  173. package/src/rsc/response-route-handler.ts +13 -5
  174. package/src/rsc/rewrite-passes.ts +2 -1
  175. package/src/rsc/routine-plan.ts +1 -1
  176. package/src/rsc/rsc-rendering.ts +164 -349
  177. package/src/rsc/server-action.ts +87 -113
  178. package/src/rsc/shell-capture-constants.ts +1 -1
  179. package/src/rsc/shell-capture.ts +212 -169
  180. package/src/rsc/shell-serve.ts +121 -18
  181. package/src/rsc/ssr-setup.ts +5 -4
  182. package/src/segment-system.tsx +3 -3
  183. package/src/server/context.ts +22 -25
  184. package/src/server/handle-store.ts +55 -0
  185. package/src/server/render-pass.ts +291 -0
  186. package/src/server/request-context.ts +26 -115
  187. package/src/server/rewrite-trail.ts +6 -4
  188. package/src/ssr/index.tsx +1 -1
  189. package/src/testing/dispatch.ts +223 -710
  190. package/src/testing/e2e/page-helpers.ts +2 -2
  191. package/src/testing/serve-shell-request.ts +23 -36
  192. package/src/types/cache-types.ts +6 -3
  193. package/src/types/error-types.ts +9 -1
  194. package/src/types/handler-context.ts +12 -1
  195. package/src/types/segments.ts +1 -1
  196. package/src/vite/discovery/build-discovery-phase.ts +204 -0
  197. package/src/vite/discovery/build-env.ts +148 -0
  198. package/src/vite/discovery/bundle-scan-phase.ts +128 -0
  199. package/src/vite/discovery/dev-prerender-cache.ts +8 -3
  200. package/src/vite/discovery/dev-server-phase.ts +1776 -0
  201. package/src/vite/discovery/discover-routers.ts +1 -1
  202. package/src/vite/discovery/discovery-errors.ts +1 -1
  203. package/src/vite/discovery/prerender-collection.ts +24 -39
  204. package/src/vite/discovery/state.ts +15 -0
  205. package/src/vite/plugins/version-plugin.ts +1 -1
  206. package/src/vite/plugins/virtual-entries.ts +1 -1
  207. package/src/vite/router-discovery.ts +29 -2428
  208. package/src/vite/temp-rsc-server.ts +223 -0
  209. package/src/vite/utils/prerender-utils.ts +47 -0
  210. package/src/wire-params.ts +35 -0
  211. package/dist/types/router/segment-resolution/fresh.d.ts +0 -72
  212. package/src/router/segment-resolution/fresh.ts +0 -945
@@ -1,104 +1,55 @@
1
1
  /**
2
2
  * Cache Lookup Middleware
3
3
  *
4
- * First middleware in the pipeline. Checks cache before segment resolution.
4
+ * First middleware in the pipeline. Before segment resolution runs, it serves
5
+ * a stored match from the first source that answers:
5
6
  *
6
- * FLOW DIAGRAM
7
- * ============
8
- *
9
- * source (empty)
10
- * |
11
- * v
12
- * +---------------------+
13
- * | Is action request? |──yes──> yield* source (pass through)
14
- * +---------------------+
15
- * | no
16
- * v
17
- * +---------------------+
18
- * | Cache enabled? |──no───> yield* source (pass through)
19
- * +---------------------+
20
- * | yes
21
- * v
22
- * +---------------------+
23
- * | Lookup cache |
24
- * | (pathname, params) |
25
- * +---------------------+
26
- * |
27
- * +-----+-----+
28
- * | |
29
- * miss hit
30
- * | |
31
- * v v
32
- * yield* Set state.cacheHit = true
33
- * source Set state.shouldRevalidate
34
- * | |
35
- * | v
36
- * | +---------------------------+
37
- * | | For each cached segment: |
38
- * | | - Apply revalidation |
39
- * | | - Set component = null |
40
- * | | if client has it |
41
- * | +---------------------------+
42
- * | |
43
- * | v
44
- * | +---------------------------+
45
- * | | Resolve fresh loaders | <-- Loaders are NEVER cached
46
- * | | (always fresh data) |
47
- * | +---------------------------+
48
- * | |
49
- * | v
50
- * | yield cached segments
51
- * | yield fresh loader segments
52
- * | |
53
- * +-----------+
54
- * |
55
- * v
56
- * next middleware
7
+ * 1. Prerender: the writable on-demand overlay, then the bundled build
8
+ * manifest. A live render (an action or a stale partial) reads them too
9
+ * when the route has no live handler (not a Passthrough).
10
+ * 2. The dev Static store (`__PRERENDER_DEV_URL`, isStaticPrerender entries).
11
+ * 3. The route's cache() record (the segment cache). An action or a stale
12
+ * partial renders live (rendersLive) and reads no record.
57
13
  *
14
+ * Every source serves through serveStoredSegments: pre-rendering is caching
15
+ * at build time, so a prerender entry is a cache hit through the same segment
16
+ * path. When no source answers, the request passes through to
17
+ * segment-resolution.
58
18
  *
59
- * CACHE BEHAVIOR
60
- * ==============
19
+ * A STORED HIT
20
+ * ============
61
21
  *
62
- * Cache HIT:
63
22
  * - state.cacheHit = true signals downstream middleware to skip
64
- * - Cached segments have their components nullified if client already has them
65
- * - Loaders are always re-resolved for fresh data
66
- * - state.shouldRevalidate triggers background SWR if cache was stale
67
- *
68
- * Cache MISS:
69
- * - Passes through to segment-resolution middleware
70
- * - No segments yielded from this middleware
71
- *
72
- * Loaders:
73
- * - NEVER cached in the segment cache
74
- * - Always resolved fresh on every request
75
- * - Ensures data freshness even with cached UI components
76
- * - Segment cache staleness does NOT propagate to loader revalidation;
23
+ * - A segment the client already holds and keeps has its component nullified
24
+ * - Loaders are always re-resolved fresh: never cached or pre-rendered.
25
+ * Segment cache staleness does NOT propagate to loader revalidation;
77
26
  * loaders use their own revalidation rules (actionId, user-defined)
27
+ * - A cache() record sets state.shouldRevalidate, which triggers background
28
+ * SWR when the record was stale
78
29
  *
30
+ * REVALIDATION RULES (cache() record)
31
+ * ===================================
79
32
  *
80
- * REVALIDATION RULES
81
- * ==================
33
+ * Each record segment the client holds is evaluated against its rules:
82
34
  *
83
- * Each cached segment is evaluated against its revalidation rules:
84
- *
85
- * 1. No rules defined -> use default (skip if client has segment)
35
+ * 1. No rules defined -> re-render the route's own segments when the search
36
+ * or the route params changed; otherwise skip (nullify component)
86
37
  * 2. Rules return false -> skip re-render (nullify component)
87
38
  * 3. Rules return true -> re-render (keep component)
88
39
  *
89
- * Revalidation context includes:
90
- * - Previous/next URL and params
91
- * - Request object
92
- * - Action context (if POST)
40
+ * Revalidation context includes the previous/next URL and params, the request
41
+ * and the action context (if POST).
93
42
  */
94
43
  import type { InternalHandlerContext, ResolvedSegment } from "../../types.js";
95
44
  import { isPprEntry, type EntryData } from "../../server/context.js";
96
45
  import {
97
46
  rendersLive,
47
+ resolutionPassInput,
98
48
  type MatchContext,
99
49
  type MatchPipelineState,
100
50
  } from "../match-context.js";
101
51
  import { getRouterContext, type RouterContext } from "../router-context.js";
52
+ import { freshPassInput } from "../segment-resolution/resolution-pass.js";
102
53
  import { observeEvent } from "../instrument.js";
103
54
  import { pushRevalidationTraceEntry, isTraceActive } from "../logging.js";
104
55
  import { treeHasStreaming } from "./segment-resolution.js";
@@ -106,6 +57,7 @@ import { loaderPins } from "../segment-resolution/loader-cache.js";
106
57
  import {
107
58
  fillSends,
108
59
  interceptFillSkips,
60
+ isFillRequest,
109
61
  recordUse,
110
62
  } from "../segment-resolution/prefetch-deferral.js";
111
63
  import type { PrerenderStore, PrerenderEntry } from "../../prerender/store.js";
@@ -133,29 +85,21 @@ import { prerenderStoreShortCircuits } from "../navigation-snapshot.js";
133
85
  import { paramsEqual } from "../params-util.js";
134
86
  import { requestHeaders } from "../../server/request-headers.js";
135
87
  import { isInterceptSegment } from "../../browser/intercept-utils.js";
88
+ import {
89
+ interceptRecordKey,
90
+ interceptSlotId,
91
+ } from "../intercept-resolution.js";
136
92
 
137
93
  // Lazily initialized prerender store singleton and dynamically imported deps.
138
94
  // Dynamic imports prevent pulling in @vitejs/plugin-rsc/rsc virtual module at
139
95
  // top-level, which breaks vitest (only URLs with file:, data:, node: schemes).
140
96
  let prerenderStoreInstance: PrerenderStore | null | undefined;
141
- let _deserializeSegments:
142
- | typeof import("../../cache/segment-codec.js").deserializeSegments
143
- | undefined;
144
- let _fragmentSegments:
145
- | typeof import("../../cache/segment-codec.js").fragmentSegments
146
- | undefined;
147
- let _restoreHandles:
148
- | typeof import("../../cache/handle-snapshot.js").restoreHandles
149
- | undefined;
150
- let _decodeHandles:
151
- | typeof import("../../cache/handle-snapshot.js").decodeHandles
152
- | undefined;
153
- let _restoreRecordHandles:
154
- | typeof import("../../cache/handle-snapshot.js").restoreRecordHandles
155
- | undefined;
97
+ let codec: typeof import("../../cache/segment-codec.js") | undefined;
98
+ let handleSnapshot: typeof import("../../cache/handle-snapshot.js") | undefined;
156
99
  let _hashParams:
157
100
  | typeof import("../../prerender/param-hash.js").hashParams
158
101
  | undefined;
102
+ let storeModule: typeof import("../../prerender/store.js") | undefined;
159
103
 
160
104
  /**
161
105
  * @internal Replace the prerender store this module serves from, and return
@@ -171,215 +115,538 @@ export function setPrerenderStoreForTests(
171
115
  return previous;
172
116
  }
173
117
 
174
- async function ensurePrerenderDeps() {
175
- if (!_deserializeSegments) {
176
- const [codec, snapshot, paramHash, store] = await Promise.all([
177
- import("../../cache/segment-codec.js"),
178
- import("../../cache/handle-snapshot.js"),
179
- import("../../prerender/param-hash.js"),
180
- import("../../prerender/store.js"),
181
- ]);
182
- _deserializeSegments = codec.deserializeSegments;
183
- _fragmentSegments = codec.fragmentSegments;
184
- _restoreHandles = snapshot.restoreHandles;
185
- _decodeHandles = snapshot.decodeHandles;
186
- _restoreRecordHandles = snapshot.restoreRecordHandles;
187
- _hashParams = paramHash.hashParams;
188
- if (prerenderStoreInstance === undefined) {
189
- prerenderStoreInstance = store.createPrerenderStore();
190
- }
118
+ async function ensurePrerenderDeps(): Promise<void> {
119
+ if (codec) return;
120
+ const [segmentCodec, snapshot, paramHash, store] = await Promise.all([
121
+ import("../../cache/segment-codec.js"),
122
+ import("../../cache/handle-snapshot.js"),
123
+ import("../../prerender/param-hash.js"),
124
+ import("../../prerender/store.js"),
125
+ ]);
126
+ codec = segmentCodec;
127
+ handleSnapshot = snapshot;
128
+ _hashParams = paramHash.hashParams;
129
+ storeModule = store;
130
+ if (prerenderStoreInstance === undefined) {
131
+ prerenderStoreInstance = store.createPrerenderStore();
191
132
  }
192
133
  }
193
134
 
194
135
  /**
195
- * Shared yield logic for prerender and static handler store entries.
196
- * Deserializes segments, replays handle data, yields segments with partial
197
- * navigation nullification, and resolves fresh loaders.
136
+ * Where a stored match is read from. Each source serves through
137
+ * serveStoredSegments: a prerender entry is a cache hit through the same
138
+ * segment path (prerender-api-design.md, "Core Principle").
139
+ *
140
+ * - `segment-cache`: a cache() record, decoded by the lookup, which replays
141
+ * its handles on a document request; on a partial one `restoreHandles`
142
+ * replays those of the segments it sends. `scope` is the scope whose
143
+ * record answered.
144
+ * - `prerender-build`: the bundled build manifest (immutable).
145
+ * - `prerender-overlay`: the writable on-demand overlay. Its entry may be
146
+ * newer than the client's copy: one that a refresh (possibly inside this
147
+ * very action) replaced.
198
148
  */
199
- // Resolve loaders fresh on a cache hit (loaders are NEVER cached) and yield
200
- // the loader segments, updating state.matchedIds and pushing the loader-resolve
201
- // + cache-hit metrics. Shared verbatim by the prerender-store path
202
- // (yieldFromStore) and the runtime cache-hit path (withCacheLookup). The router
203
- // resolve fns are passed in (captured early by the callers) rather than re-read
204
- // from getRouterContext() here, because that is ALS-backed and the callers run
205
- // awaits before reaching this point (workerd can disrupt ALS mid-pipeline).
206
- async function* resolveFreshLoadersAndYield<TEnv>(
207
- ctx: MatchContext<TEnv>,
208
- entries: EntryData[],
209
- state: MatchPipelineState,
210
- pipelineStart: number,
211
- ms: MatchContext<TEnv>["metricsStore"],
212
- resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
213
- resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
214
- ): AsyncGenerator<ResolvedSegment> {
215
- const loaderStart = performance.now();
216
- entries = entries.filter((e) => !interceptFillSkips(ctx, e.shortCode));
217
-
218
- if (ctx.isFullMatch) {
219
- if (resolveLoadersOnly) {
220
- const loaderSegments = await ctx.Store.run(() =>
221
- resolveLoadersOnly(entries, ctx.handlerContext),
222
- );
223
- state.matchedIds = state.cachedMatchedIds!;
224
- for (const segment of loaderSegments) {
225
- yield segment;
226
- }
227
- } else {
228
- state.matchedIds = state.cachedMatchedIds!;
229
- }
230
- } else {
231
- if (resolveLoadersOnlyWithRevalidation) {
232
- const loaderResult = await ctx.Store.run(() =>
233
- resolveLoadersOnlyWithRevalidation(
234
- entries,
235
- ctx.handlerContext,
236
- ctx.clientSegmentSet,
237
- ctx.prevParams,
238
- ctx.request,
239
- ctx.prevUrl,
240
- ctx.url,
241
- ctx.routeKey,
242
- ctx.actionContext,
243
- // Loaders are never cached in the segment cache, so segment staleness
244
- // must not propagate; browser-sent staleness (ctx.stale) still must.
245
- ctx.stale || undefined,
246
- ),
247
- );
248
- state.matchedIds = [
249
- ...state.cachedMatchedIds!,
250
- ...loaderResult.matchedIds,
251
- ];
252
- for (const segment of loaderResult.segments) {
253
- yield segment;
254
- }
255
- } else {
256
- state.matchedIds = state.cachedMatchedIds!;
149
+ type StoredSegmentSource =
150
+ | {
151
+ kind: "segment-cache";
152
+ segments: ResolvedSegment[];
153
+ shouldRevalidate: boolean;
154
+ scope: CacheScope;
155
+ restoreHandles?: (sentIds: ReadonlySet<string>) => Promise<void>;
257
156
  }
258
- }
157
+ | { kind: "prerender-build" | "prerender-overlay"; entry: PrerenderEntry };
259
158
 
260
- if (ms) {
261
- const loaderEnd = performance.now();
262
- ms.metrics.push({
263
- label: "pipeline:loader-resolve",
264
- duration: loaderEnd - loaderStart,
265
- startTime: loaderStart - ms.requestStart,
266
- depth: 1,
267
- });
268
- ms.metrics.push({
269
- label: "pipeline:cache-hit",
270
- duration: loaderEnd - pipelineStart,
271
- startTime: pipelineStart - ms.requestStart,
272
- });
273
- }
274
- }
275
-
276
- async function* yieldFromStore<TEnv>(
159
+ /**
160
+ * Decode a prerender entry's segments and replay its handle data into the
161
+ * request's HandleStore, as the cache scope's lookup does for a record; on a
162
+ * partial request, only the buckets of the segments it `sends`.
163
+ */
164
+ async function decodeStoredEntry<TEnv>(
277
165
  entry: PrerenderEntry,
278
166
  ctx: MatchContext<TEnv>,
279
- state: MatchPipelineState,
280
- pipelineStart: number,
281
167
  reqCtx: RequestContext<TEnv> | undefined,
282
- resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
283
- resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
284
- /**
285
- * The entry may be newer than the client's copy: an on-demand overlay entry
286
- * that a refresh (possibly inside this very action) replaced. On an action
287
- * or a stale request the route's own segments are then re-sent, as a live
288
- * action re-render re-sends them (revalidation.ts "action:route-segment");
289
- * the immutable bundled manifest keeps the client's copy.
290
- */
291
- replaceable?: boolean,
292
- ): AsyncGenerator<ResolvedSegment> {
293
- if (
294
- !_deserializeSegments ||
295
- !_fragmentSegments ||
296
- !_restoreHandles ||
297
- !_decodeHandles ||
298
- !_restoreRecordHandles ||
299
- !_hashParams
300
- ) {
301
- throw new Error("yieldFromStore called before ensurePrerenderDeps");
302
- }
303
-
168
+ sends: (segment: ResolvedSegment) => boolean | Promise<boolean>,
169
+ ): Promise<ResolvedSegment[]> {
304
170
  // Shell-HIT tail (issue #700): a Prerender+ppr route's tail serves from THIS
305
171
  // store (the prerender lookup runs before the cache scope), so the fragment
306
172
  // splice must apply here too — otherwise producer B entries re-serialize the
307
173
  // whole tree per request while producer A entries do not.
308
174
  const segments = reqCtx?._shellFragmentPayload
309
- ? await _fragmentSegments(entry.segments)
310
- : await _deserializeSegments(entry.segments);
175
+ ? await codec!.fragmentSegments(entry.segments)
176
+ : await codec!.deserializeSegments(entry.segments);
311
177
 
312
- // Replay handle data (same as runtime cache hit path). entry.handles is a
313
- // Flight-encoded string ("" when none) — decode before restore so
314
- // Promise/ReactNode handle values are revived, not the corrupted JSON form.
178
+ // entry.handles is a Flight-encoded string ("" when none) — decode before
179
+ // restore so Promise/ReactNode handle values are revived, not the corrupted
180
+ // JSON form.
315
181
  const handleStore = reqCtx?._handleStore;
316
182
  if (handleStore && entry.handles) {
317
- const handlesRecord = await _decodeHandles(entry.handles);
183
+ const handlesRecord = await handleSnapshot!.decodeHandles(entry.handles);
318
184
  if (handlesRecord) {
319
- _restoreHandles(handlesRecord, handleStore);
185
+ // On a partial request, a bucket is replayed only when a segment with
186
+ // its id is sent: not one a kept segment holds (the client keeps its
187
+ // values, as on a live walk), nor one of an intercept the variant does
188
+ // not serve (selectInterceptVariant).
189
+ if (!ctx.isFullMatch) {
190
+ const sent = new Set<string>();
191
+ for (const segment of segments) {
192
+ let send = sends(segment);
193
+ if (typeof send !== "boolean") send = await send;
194
+ if (send) sent.add(segment.id);
195
+ }
196
+ for (const id of Object.keys(handlesRecord)) {
197
+ if (!sent.has(id)) delete handlesRecord[id];
198
+ }
199
+ }
200
+ handleSnapshot!.restoreHandles(handlesRecord, handleStore);
320
201
  }
321
202
  }
322
203
 
323
204
  // Before the render barrier freezes the document snapshot.
324
205
  const shellHandles = reqCtx?._shellImplicitCache?.prerenderHandles;
325
206
  if (handleStore && shellHandles) {
326
- await _restoreRecordHandles!(
207
+ await handleSnapshot!.restoreRecordHandles(
327
208
  handleStore,
328
209
  shellHandles,
329
210
  loaderPins(ctx.entries, reqCtx),
330
211
  );
331
212
  }
332
213
 
333
- state.cacheHit = true;
334
- state.cacheSource = "prerender";
335
- state.cachedSegments = segments;
336
- state.cachedMatchedIds = segments.map((s) => s.id);
337
-
338
- // Set streaming flag (once) and resolve render barrier.
339
214
  // Post-match serve-source truth for the PPR replay reporter. This overwrites
340
215
  // `intercept` because the prerender store is the response source.
216
+ if (reqCtx) reqCtx._pprReplayPostMatchReason = "prerender-store";
217
+ return segments;
218
+ }
219
+
220
+ /**
221
+ * Whether a cache() record's segment is sent: one the client lacks, an
222
+ * intercept's, or one its revalidate rules re-render. Without rules, the
223
+ * route's own segments are re-rendered when the search or the route params
224
+ * changed. A segment with rules answers with their pending decision, which
225
+ * serveStoredSegments awaits and observes.
226
+ */
227
+ function cachedSegmentSends<TEnv>(
228
+ ctx: MatchContext<TEnv>,
229
+ cacheStale: boolean,
230
+ reqCtx: RequestContext<TEnv> | undefined,
231
+ router: RouterContext<TEnv>,
232
+ ): (segment: ResolvedSegment) => boolean | Promise<boolean> {
233
+ const entryRevalidateMap =
234
+ !ctx.isFullMatch &&
235
+ ctx.clientSegmentSet.size > 0 &&
236
+ router.buildEntryRevalidateMap
237
+ ? router.buildEntryRevalidateMap(ctx.entries)
238
+ : undefined;
239
+ const searchChanged =
240
+ ctx.prevUrl.search !== ctx.url.search || reqCtx?._visitorMoved === true;
241
+ const routeParamsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
242
+ const trace = (
243
+ segment: ResolvedSegment,
244
+ shouldRevalidate: boolean,
245
+ reason: string,
246
+ ): boolean => {
247
+ if (isTraceActive()) {
248
+ pushRevalidationTraceEntry({
249
+ segmentId: segment.id,
250
+ segmentType: segment.type,
251
+ belongsToRoute: segment.belongsToRoute ?? false,
252
+ source: "cache-hit",
253
+ defaultShouldRevalidate: shouldRevalidate,
254
+ finalShouldRevalidate: shouldRevalidate,
255
+ reason,
256
+ });
257
+ }
258
+ return shouldRevalidate;
259
+ };
260
+
261
+ return (segment) => {
262
+ if (!ctx.clientSegmentSet.has(segment.id)) {
263
+ return trace(segment, true, "new-segment");
264
+ }
265
+ // A fill keeps a held segment outright (fillSends), an intercept's
266
+ // included.
267
+ if (
268
+ fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) === false
269
+ ) {
270
+ return false;
271
+ }
272
+ if (isInterceptSegment(segment)) return true;
273
+
274
+ const entryInfo = entryRevalidateMap?.get(segment.id);
275
+ if (!entryInfo || entryInfo.revalidate.length === 0) {
276
+ const shouldDefaultRevalidate =
277
+ (searchChanged || routeParamsChanged) &&
278
+ (segment.type === "route" ||
279
+ (segment.belongsToRoute === true &&
280
+ (segment.type === "layout" || segment.type === "parallel")));
281
+ return trace(
282
+ segment,
283
+ shouldDefaultRevalidate,
284
+ !shouldDefaultRevalidate
285
+ ? "cached-no-rules"
286
+ : routeParamsChanged
287
+ ? "cached-params-changed"
288
+ : "cached-search-changed",
289
+ );
290
+ }
291
+
292
+ return router.evaluateRevalidation({
293
+ segment,
294
+ prevParams: ctx.prevParams,
295
+ getPrevSegment: null,
296
+ request: ctx.request,
297
+ prevUrl: ctx.prevUrl,
298
+ nextUrl: ctx.url,
299
+ revalidations: entryInfo.revalidate.map((fn, i) => ({
300
+ name: `revalidate${i}`,
301
+ fn,
302
+ })),
303
+ routeKey: ctx.routeKey,
304
+ context: ctx.handlerContext,
305
+ actionContext: ctx.actionContext,
306
+ cacheStale,
307
+ traceSource: "cache-hit",
308
+ });
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Whether a prerender entry's segment is sent. On a partial navigation the
314
+ * client keeps the segments it holds. The route's own segments (an
315
+ * intercept's included) are sent again when the params changed (e.g., a
316
+ * different guide slug). An overlay entry may be newer than the client's
317
+ * copy: on an action or a stale request they are re-sent too, as a live
318
+ * action re-render re-sends them (revalidation.ts "action:route-segment");
319
+ * the immutable bundled manifest keeps the client's copy. A held segment
320
+ * above the route stays held, as a live walk keeps it ("nav:non-route-skip"),
321
+ * unless its revalidate() rules ask for it when the params changed.
322
+ *
323
+ * Decided once per decoded segment, by object: an intercept variant can hold
324
+ * a layout's parallel and the intercept's slot under one id. decodeStoredEntry
325
+ * asks first, to replay the handle values of the sent segments only.
326
+ */
327
+ function storedEntrySends<TEnv>(
328
+ ctx: MatchContext<TEnv>,
329
+ replaceable: boolean,
330
+ router: RouterContext<TEnv>,
331
+ ): (segment: ResolvedSegment) => boolean | Promise<boolean> {
332
+ if (ctx.isFullMatch) return () => true;
333
+ const paramsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
334
+ const resendRouteSegments = replaceable && (ctx.isAction || ctx.stale);
335
+ const entryRevalidateMap =
336
+ paramsChanged && router.buildEntryRevalidateMap
337
+ ? router.buildEntryRevalidateMap(ctx.entries)
338
+ : undefined;
339
+ const decide = (segment: ResolvedSegment): boolean | Promise<boolean> => {
340
+ if (!ctx.clientSegmentSet.has(segment.id)) return true;
341
+ // A fill keeps it (fillSends).
342
+ const fill = fillSends(
343
+ ctx.handlerContext,
344
+ segment.id,
345
+ ctx.clientSegmentSet,
346
+ );
347
+ if (fill !== undefined) return fill;
348
+ if (segment.type === "route" || segment.belongsToRoute === true) {
349
+ return paramsChanged || resendRouteSegments;
350
+ }
351
+ const revalidate = entryRevalidateMap?.get(segment.id)?.revalidate;
352
+ if (!revalidate?.length) return false;
353
+ return router.evaluateRevalidation({
354
+ segment,
355
+ prevParams: ctx.prevParams,
356
+ getPrevSegment: null,
357
+ request: ctx.request,
358
+ prevUrl: ctx.prevUrl,
359
+ nextUrl: ctx.url,
360
+ revalidations: revalidate.map((fn, i) => ({
361
+ name: `revalidate${i}`,
362
+ fn,
363
+ })),
364
+ routeKey: ctx.routeKey,
365
+ context: ctx.handlerContext,
366
+ actionContext: ctx.actionContext,
367
+ stale: ctx.stale,
368
+ traceSource: "cache-hit",
369
+ });
370
+ };
371
+ const decided = new WeakMap<ResolvedSegment, boolean | Promise<boolean>>();
372
+ return (segment) => {
373
+ let send = decided.get(segment);
374
+ if (send === undefined) decided.set(segment, (send = decide(segment)));
375
+ return send;
376
+ };
377
+ }
378
+
379
+ /**
380
+ * Serve a stored match from the first source that answers (the order is in
381
+ * the module header), or pass `upstream` through when none does. Every
382
+ * StoredSegmentSource serves here: the stored segments, minus the ones the
383
+ * client keeps, then the loaders resolved fresh (loaders are never cached or
384
+ * pre-rendered). By source: a prerender entry is decoded here, a cache()
385
+ * record also resolves the entries above its boundary live and sets
386
+ * state.shouldRevalidate, and each decides which held segments are sent
387
+ * again.
388
+ *
389
+ * The reads and the serve share this generator: a HIT is the request hot
390
+ * path, and serving from a second generator behind `yield*` adds a microtask
391
+ * turn and two promises for each segment it yields, and once more at its end.
392
+ */
393
+ async function* serveStoredSegments<TEnv>(
394
+ ctx: MatchContext<TEnv>,
395
+ state: MatchPipelineState,
396
+ upstream: AsyncGenerator<ResolvedSegment>,
397
+ ): AsyncGenerator<ResolvedSegment> {
398
+ const pipelineStart = performance.now();
399
+
400
+ // Eagerly capture the HandleStore before any async operations.
401
+ // In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
402
+ // can disrupt AsyncLocalStorage, causing getRequestContext() to return
403
+ // undefined afterward. Capturing the reference early ensures handle replay
404
+ // and handler handle-push work regardless of ALS state.
405
+ const reqCtx = _getRequestContext<TEnv>();
406
+ // Only the match can determine interception; the source header proves
407
+ // nothing in either direction. Clear a stale reason on normal matches.
341
408
  if (reqCtx) {
342
- reqCtx._pprReplayPostMatchReason = "prerender-store";
343
- if (reqCtx._treeHasStreaming === undefined) {
344
- reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
409
+ reqCtx._pprReplayPostMatchReason = ctx.isIntercept
410
+ ? "intercept"
411
+ : undefined;
412
+ }
413
+ // ALS-backed: captured before the first await, like reqCtx.
414
+ const router = getRouterContext<TEnv>();
415
+ let stored: StoredSegmentSource | null = null;
416
+
417
+ // An on-demand route may have no build-baked entry yet still needs a
418
+ // writable-overlay lookup, so od joins pr in the gate. The retained producer
419
+ // is NEVER run by this pipeline: a miss falls through like pr + miss.
420
+ const isPassthroughPrerenderRoute = ctx.entries.some(
421
+ (entry) => entry.type === "route" && entry.isPassthrough === true,
422
+ );
423
+ const overlayEligible =
424
+ ctx.matched.od === true && !requestHeaders(ctx.request).get("X-RSC-HMR");
425
+ // Actions normally re-render fresh and skip the prerender store. But a pure
426
+ // Prerender route's handler is evicted at build, so there is no fresh
427
+ // handler to run on an action re-render -- without the fallback the
428
+ // re-render falls through to the evicted handler and throws "No prerender
429
+ // data found". Serve the prerendered entry instead (the action ran already;
430
+ // its result is applied client-side via useActionState). A plain on-demand
431
+ // route is the same case, served from the overlay first (an overlay entry
432
+ // is newer than the build one, and may be the only one). Passthrough
433
+ // routes keep a liveHandler, so a live render (rendersLive) re-renders
434
+ // them fresh. A thrown action's boundary pass walks live too
435
+ // (ActionContext.boundary).
436
+ if (
437
+ (prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
438
+ overlayEligible) &&
439
+ (!rendersLive(ctx) || !isPassthroughPrerenderRoute) &&
440
+ !ctx.actionContext?.boundary
441
+ ) {
442
+ await ensurePrerenderDeps();
443
+ stored = await readPrerenderSource(
444
+ ctx,
445
+ reqCtx,
446
+ isPassthroughPrerenderRoute,
447
+ // Per-request writable prerender overlay (durable), resolved by the handler.
448
+ reqCtx?._prerender,
449
+ );
450
+ }
451
+
452
+ if (
453
+ !stored &&
454
+ !rendersLive(ctx) &&
455
+ !ctx.matched.pr &&
456
+ globalThis.__PRERENDER_DEV_URL &&
457
+ ctx.entries.some(
458
+ (e) =>
459
+ (e.type === "layout" || e.type === "route" || e.type === "parallel") &&
460
+ e.isStaticPrerender,
461
+ )
462
+ ) {
463
+ await ensurePrerenderDeps();
464
+ stored = await readPrerenderSource(
465
+ ctx,
466
+ reqCtx,
467
+ isPassthroughPrerenderRoute,
468
+ );
469
+ }
470
+
471
+ stored ??= await readSegmentCacheSource(ctx, reqCtx);
472
+ if (!stored) {
473
+ yield* upstream;
474
+ const ms = ctx.metricsStore;
475
+ if (ms) {
476
+ ms.metrics.push({
477
+ label: "pipeline:cache-miss",
478
+ duration: performance.now() - pipelineStart,
479
+ startTime: pipelineStart - ms.requestStart,
480
+ });
345
481
  }
346
- reqCtx._resolveRenderBarrier(segments);
482
+ return;
347
483
  }
484
+ const source: StoredSegmentSource = stored;
485
+
486
+ const fromCache = source.kind === "segment-cache";
487
+ const storedSends = fromCache
488
+ ? undefined
489
+ : storedEntrySends(ctx, source.kind === "prerender-overlay", router);
490
+ const segments = fromCache
491
+ ? source.segments
492
+ : await decodeStoredEntry(source.entry, ctx, reqCtx, storedSends!);
493
+
494
+ // Entries above the cache() boundary are not in the record: resolve them
495
+ // fresh, exactly as an uncached render of this request would.
496
+ const scope = fromCache ? source.scope : undefined;
497
+ const boundary = scope?.boundary;
498
+ const boundaryIndex =
499
+ boundary === undefined
500
+ ? 0
501
+ : Math.max(
502
+ 0,
503
+ ctx.entries.findIndex((e) => e.shortCode === boundary),
504
+ );
505
+ let liveSegments: ResolvedSegment[] = [];
506
+ let liveMatchedIds: string[] = [];
507
+ if (scope && boundaryIndex > 0) {
508
+ const liveEntries = ctx.entries.slice(0, boundaryIndex);
509
+ const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
510
+ await ctx.Store.run(async () => {
511
+ // The full chain keeps a bare cache() marker, which also sits in
512
+ // its layout's orphan list, out of the live pass: the record
513
+ // supplies it and its subtree (issue #918).
514
+ const result = await router.resolveAllSegments(
515
+ ctx.isFullMatch
516
+ ? freshPassInput(
517
+ ctx.handlerContext,
518
+ ctx.routeKey,
519
+ ctx.matched.params,
520
+ )
521
+ : resolutionPassInput(ctx),
522
+ liveEntries,
523
+ {
524
+ interceptResult: ctx.interceptResult,
525
+ localRouteName: ctx.localRouteName,
526
+ pathname: ctx.pathname,
527
+ chain: ctx.entries,
528
+ },
529
+ );
530
+ return ctx.isFullMatch
531
+ ? {
532
+ segments: result.segments,
533
+ matchedIds: result.segments.map((s) => s.id),
534
+ }
535
+ : result;
536
+ });
537
+ // The record supplies every covered segment.
538
+ liveSegments = live.segments.filter(
539
+ (s) => !scope.covers(s.id, s.namespace),
540
+ );
541
+ liveMatchedIds = live.matchedIds.filter((id) => !scope.covers(id));
542
+ }
543
+
544
+ state.cacheHit = true;
545
+ state.cacheSource = fromCache ? "runtime" : "prerender";
546
+ if (fromCache) state.shouldRevalidate = source.shouldRevalidate;
547
+ state.cachedSegments = segments;
548
+ const cachedMatchedIds = [...liveMatchedIds, ...segments.map((s) => s.id)];
549
+ state.cachedMatchedIds = cachedMatchedIds;
550
+
551
+ const sends = fromCache
552
+ ? cachedSegmentSends(ctx, source.shouldRevalidate, reqCtx, router)
553
+ : storedSends!;
554
+ const restoreHandles = fromCache ? source.restoreHandles : undefined;
555
+ const sentIds = restoreHandles && new Set<string>();
556
+
557
+ yield* liveSegments;
348
558
 
349
- // For partial navigation, nullify components the client already has
350
- // so parent layouts stay live (client keeps its existing versions).
351
- // When params changed (e.g., different guide slug), the segments have
352
- // different content, so we must NOT nullify.
353
- const paramsChanged =
354
- !ctx.isFullMatch && !paramsEqual(ctx.matched.params, ctx.prevParams);
355
- // The action defaults' route segment (revalidation.ts), not a cache skip.
356
- const resendRouteSegments =
357
- replaceable === true && (ctx.isAction || ctx.stale);
358
559
  for (const segment of segments) {
359
560
  if (interceptFillSkips(ctx, segment)) continue;
360
- if (!ctx.isFullMatch && ctx.clientSegmentSet.has(segment.id)) {
361
- // A fill keeps it (fillSends); otherwise it is sent again when the
362
- // params changed or an action re-sends the route's own segments.
363
- const sends =
364
- fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ??
365
- (paramsChanged ||
366
- (resendRouteSegments &&
367
- (segment.type === "route" || segment.belongsToRoute === true)));
368
- if (!sends) keepClientSegment(segment);
561
+ let send = sends(segment);
562
+ if (typeof send !== "boolean") {
563
+ // A cache() record segment's revalidate() rules decided. Awaited here,
564
+ // not chained in cachedSegmentSends: a .then() costs a turn and a
565
+ // promise per segment.
566
+ send = await send;
567
+ observeEvent({
568
+ type: "revalidation.decision",
569
+ timestamp: performance.now(),
570
+ segmentId: segment.id,
571
+ pathname: ctx.pathname,
572
+ routeKey: ctx.routeKey,
573
+ shouldRevalidate: send,
574
+ });
369
575
  }
576
+ if (!send) keepClientSegment(segment);
577
+ else sentIds?.add(segment.id);
370
578
  yield segment;
371
579
  }
372
580
 
373
- // Resolve loaders fresh (loaders are never pre-rendered/cached).
374
- yield* resolveFreshLoadersAndYield(
375
- ctx,
376
- ctx.entries,
377
- state,
378
- pipelineStart,
379
- ctx.metricsStore,
380
- resolveLoadersOnly,
381
- resolveLoadersOnlyWithRevalidation,
382
- );
581
+ // A partial cache() HIT replays handler values for the segments it sends
582
+ // only, decided above after the live entries: a kept segment keeps the
583
+ // client's, as on a live walk (handle-snapshot.ts restoreHandles;
584
+ // decodeStoredEntry for a bake).
585
+ if (sentIds) await restoreHandles!(sentIds);
586
+
587
+ // The stored handler values stand where the MISS's handlers pushed them:
588
+ // behind what a segment's loaders push before the walk reaches its handler
589
+ // (HandleStore.holdRecordedValues). A fill holds nothing: on a fill MISS the
590
+ // browser's merge appends what the fill runs to the held handler values.
591
+ if (!isFillRequest(ctx.handlerContext)) {
592
+ reqCtx?._handleStore.holdRecordedValues(segments.map((s) => s.id));
593
+ }
594
+
595
+ if (reqCtx) {
596
+ if (reqCtx._treeHasStreaming === undefined) {
597
+ reqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
598
+ }
599
+ reqCtx._resolveRenderBarrier(
600
+ liveSegments.length > 0 ? [...liveSegments, ...segments] : segments,
601
+ );
602
+ }
603
+
604
+ // Resolve loaders fresh. The live pass already resolved the loaders of the
605
+ // entries above the boundary.
606
+ const loaderStart = performance.now();
607
+ const loaderEntries = ctx.entries.slice(boundaryIndex);
608
+ const { resolveLoadersOnly } = router;
609
+ if (resolveLoadersOnly) {
610
+ const loaderResult = await ctx.Store.run(() =>
611
+ resolveLoadersOnly(
612
+ ctx.isFullMatch
613
+ ? freshPassInput(ctx.handlerContext, ctx.routeKey, ctx.matched.params)
614
+ : {
615
+ ...resolutionPassInput(ctx),
616
+ // Loaders are never cached in the segment cache, so segment
617
+ // staleness must not propagate; browser-sent staleness
618
+ // (ctx.stale) still must.
619
+ stale: ctx.stale || undefined,
620
+ },
621
+ loaderEntries,
622
+ { interceptResult: ctx.interceptResult },
623
+ ),
624
+ );
625
+ state.matchedIds = ctx.isFullMatch
626
+ ? cachedMatchedIds
627
+ : [...cachedMatchedIds, ...loaderResult.matchedIds];
628
+ for (const segment of loaderResult.segments) {
629
+ yield segment;
630
+ }
631
+ } else {
632
+ state.matchedIds = cachedMatchedIds;
633
+ }
634
+
635
+ const ms = ctx.metricsStore;
636
+ if (ms) {
637
+ const loaderEnd = performance.now();
638
+ ms.metrics.push({
639
+ label: "pipeline:loader-resolve",
640
+ duration: loaderEnd - loaderStart,
641
+ startTime: loaderStart - ms.requestStart,
642
+ depth: 1,
643
+ });
644
+ ms.metrics.push({
645
+ label: "pipeline:cache-hit",
646
+ duration: loaderEnd - pipelineStart,
647
+ startTime: pipelineStart - ms.requestStart,
648
+ });
649
+ }
383
650
  }
384
651
 
385
652
  /**
@@ -390,7 +657,7 @@ async function* yieldFromStore<TEnv>(
390
657
  * misses the store and renders live, and replay — including its heal
391
658
  * capture — must stay available for it (withCacheStore records the doc
392
659
  * record on that path; state.cacheSource is not "prerender"). The store
393
- * memoizes per routeKey/paramHash, so this probe and tryPrerenderLookup's
660
+ * memoizes per routeKey/paramHash, so this probe and readPrerenderSource's
394
661
  * subsequent get() share one underlying load.
395
662
  */
396
663
  export async function prerenderEntryExists(
@@ -486,23 +753,17 @@ export function scheduleOverlayRevalidation<TEnv>(
486
753
  }
487
754
 
488
755
  /**
489
- * Look up a prerendered (build-time cached) entry for the current route and, on
490
- * a hit, yield its segments. Returns true when an entry was served (the caller
491
- * should stop the pipeline) and false on a miss. Intercept navigations consult
492
- * only the intercept-specific entry (`paramHash + "/i"`); a miss there falls
493
- * through to the normal pipeline so intercept-resolution can run. Callers must
494
- * guard on `prerenderStoreInstance` after `ensurePrerenderDeps()`.
756
+ * The prerendered (build-time cached) entry for the current route, or null on
757
+ * a miss. Intercept navigations consult only the intercept-specific entry
758
+ * (`paramHash + "/i"`); a miss there falls through to the normal pipeline so
759
+ * intercept-resolution can run. Callers run `ensurePrerenderDeps()` first.
495
760
  */
496
- async function* tryPrerenderLookup<TEnv>(
761
+ async function readPrerenderSource<TEnv>(
497
762
  ctx: MatchContext<TEnv>,
498
- state: MatchPipelineState,
499
- pipelineStart: number,
500
763
  reqCtx: RequestContext<TEnv> | undefined,
501
- resolveLoadersOnly: RouterContext<TEnv>["resolveLoadersOnly"],
502
- resolveLoadersOnlyWithRevalidation: RouterContext<TEnv>["resolveLoadersOnlyWithRevalidation"],
503
764
  isPassthroughPrerenderRoute: boolean,
504
765
  overlay?: ResolvedPrerender,
505
- ): AsyncGenerator<ResolvedSegment, boolean> {
766
+ ): Promise<StoredSegmentSource | null> {
506
767
  // 1. Writable durable overlay (per-request, env-scoped), read before the
507
768
  // bundled manifest: it is always newer. Only on-demand routes are read
508
769
  // (router.prerender() refuses non-od routes, so a pr-only route always
@@ -551,19 +812,9 @@ async function* tryPrerenderLookup<TEnv>(
551
812
  (
552
813
  ctx.handlerContext as InternalHandlerContext<any, TEnv>
553
814
  )._prerenderRemoved = true;
554
- return false;
815
+ return null;
555
816
  }
556
- yield* yieldFromStore(
557
- stored.entry,
558
- ctx,
559
- state,
560
- pipelineStart,
561
- reqCtx,
562
- resolveLoadersOnly,
563
- resolveLoadersOnlyWithRevalidation,
564
- true,
565
- );
566
- return true;
817
+ return { kind: "prerender-overlay", entry: stored.entry };
567
818
  }
568
819
  }
569
820
 
@@ -578,21 +829,143 @@ async function* tryPrerenderLookup<TEnv>(
578
829
  isPassthroughRoute: isPassthroughPrerenderRoute,
579
830
  },
580
831
  );
581
- if (entry) {
582
- yield* yieldFromStore(
583
- entry,
584
- ctx,
585
- state,
586
- pipelineStart,
587
- reqCtx,
588
- resolveLoadersOnly,
589
- resolveLoadersOnlyWithRevalidation,
832
+ // The variant holds every intercept the build found; serve the one the
833
+ // match selected, or render live.
834
+ const selected = ctx.isIntercept ? ctx.interceptResult : null;
835
+ const served =
836
+ entry && selected
837
+ ? storeModule!.selectInterceptVariant(
838
+ entry,
839
+ interceptSlotId(selected.entry, selected.intercept),
840
+ selected.intercept.routeName,
841
+ )
842
+ : entry;
843
+ if (served) return { kind: "prerender-build", entry: served };
844
+ }
845
+
846
+ return null;
847
+ }
848
+
849
+ /**
850
+ * The cache() record that answers this request, or null when it renders: no
851
+ * enabled scope, a live render, a request that reads no record, or a lookup
852
+ * that did not hit.
853
+ */
854
+ async function readSegmentCacheSource<TEnv>(
855
+ ctx: MatchContext<TEnv>,
856
+ reqCtx: RequestContext<TEnv> | undefined,
857
+ ): Promise<StoredSegmentSource | null> {
858
+ // A document HIT tail replays the handler layer through the implicit doc
859
+ // scope and never runs a handler: without that scope (a context that lost
860
+ // the marker before the scope resolved), fail like a lookup miss instead
861
+ // of falling through to the handlers below.
862
+ const tailMarker = reqCtx?._shellImplicitCache;
863
+ if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
864
+ throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
865
+ }
866
+
867
+ // A prefetch that defers a layout above the boundary (prefetch: false)
868
+ // emits nothing the record holds: reading it would replay the handle
869
+ // pushes of segments the response does not carry. withCacheStore skips
870
+ // the write for the same request.
871
+ if (
872
+ rendersLive(ctx) ||
873
+ !ctx.cacheScope?.enabled ||
874
+ !recordUse(ctx.handlerContext).read
875
+ ) {
876
+ return null;
877
+ }
878
+
879
+ // Only a ppr route's records hold loader pushes, and only its shell pins
880
+ // loaders (CachedEntryData.handleOwners, the loader seed): any other
881
+ // route's record restores as a plain replay.
882
+ const leaf = ctx.entries[ctx.entries.length - 1];
883
+ const ownedPushes =
884
+ leaf !== undefined && isPprEntry(leaf)
885
+ ? () => loaderPins(ctx.entries, reqCtx)
886
+ : undefined;
887
+ // A partial request restores the handle values once it decided which
888
+ // segments it sends (serveStoredSegments).
889
+ const deferHandles = !ctx.isFullMatch;
890
+ const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
891
+ ctx.pathname,
892
+ ctx.matched.params,
893
+ interceptRecordKey(ctx.interceptResult),
894
+ ownedPushes,
895
+ deferHandles,
896
+ );
897
+ let cacheResult =
898
+ explicitLookup.status === "hit" ? explicitLookup.result : null;
899
+ // The scope whose record answered decides which entries it covers.
900
+ let hitScope: CacheScope = ctx.cacheScope;
901
+
902
+ // PPR navigation replay composed with a route-derived cache() scope. The
903
+ // explicit tier stays authoritative: its hit serves under its own
904
+ // key/ttl/swr semantics and reports `explicit-cache-hit` — never a false
905
+ // replay HIT. ONLY a true `miss` lets the seeded doc record supply the
906
+ // match (the marker's onHit observer then reports the true HIT). The
907
+ // other outcomes render fresh: `bypass` (cache(false), a false
908
+ // condition() — absolute opt-outs even when the pre-read gate saw a
909
+ // different condition() result — or no store) and `error` (a throwing
910
+ // key()/keyGenerator/store.get keeps lookupRoute's render-uncached
911
+ // contract; the canonical record must not serve across a broken key
912
+ // partition). The outcome comes from the lookup itself, not a re-run of
913
+ // the condition, so a flapping predicate cannot re-admit the fallback.
914
+ // Gated on the marker's `onExplicitHit`, set ONLY on the
915
+ // navigation-replay serve path: a CAPTURE render must never fall back
916
+ // here — its marker store reads through to the real store, and a
917
+ // doc-keyed hit would replay the previous generation's segments instead
918
+ // of re-running handlers (breaking SWR recapture freshness). Intercepts
919
+ // stay source-dependent on their normal cache path (match-api never arms
920
+ // replay for them).
921
+ const replayMarker = reqCtx?._shellImplicitCache;
922
+ if (
923
+ replayMarker?.onExplicitHit &&
924
+ !ctx.isIntercept &&
925
+ !ctx.cacheScope.isShellImplicitDocScope
926
+ ) {
927
+ if (explicitLookup.status === "hit") {
928
+ replayMarker.onExplicitHit();
929
+ } else if (explicitLookup.status === "miss" && replayMarker.store) {
930
+ // The store gate keeps report-only markers (installed on the
931
+ // no-eligible-snapshot path purely for truthful status) inert: a
932
+ // store-less marker minting a doc scope here would resolve the APP
933
+ // store and read the REAL doc: partition — a cross-partition serve.
934
+ hitScope = createShellImplicitDocScope(replayMarker);
935
+ cacheResult = await hitScope.lookupRoute(
936
+ ctx.pathname,
937
+ ctx.matched.params,
938
+ undefined,
939
+ ownedPushes,
940
+ deferHandles,
590
941
  );
591
- return true;
942
+ } else if (explicitLookup.status === "bypass") {
943
+ // condition() refused at lookup time (the gate only pre-decides the
944
+ // static cache(false) case) — report cache-disabled truthfully.
945
+ replayMarker.onExplicitBypass?.();
592
946
  }
947
+ // "error" stays unreported: the render is fresh and the seeded record
948
+ // was not consulted, which is exactly what snapshot-miss describes; the
949
+ // store already routed the failure through reportCacheError.
593
950
  }
594
951
 
595
- return false;
952
+ if (!cacheResult) {
953
+ // A document shell HIT tail replays the handler layer from the entry's
954
+ // doc record and must never run a handler behind the committed prelude:
955
+ // a record that did not hit (it failed to decode, or the entry lost it)
956
+ // ends the tail here; serveShellHit degrades the response.
957
+ if (replayMarker?.docTail) {
958
+ throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
959
+ }
960
+ return null;
961
+ }
962
+ return {
963
+ kind: "segment-cache",
964
+ segments: cacheResult.segments,
965
+ shouldRevalidate: cacheResult.shouldRevalidate,
966
+ scope: hitScope,
967
+ restoreHandles: cacheResult.restoreHandles,
968
+ };
596
969
  }
597
970
 
598
971
  /**
@@ -620,442 +993,18 @@ export type GeneratorMiddleware<T> = (
620
993
  /**
621
994
  * Creates cache lookup middleware
622
995
  *
623
- * Checks cache for segments. If cache hit:
624
- * - Applies revalidation to determine which segments need re-rendering
625
- * - Resolves loaders fresh (loaders are NOT cached by design)
626
- * - Sets state.cacheHit = true
627
- * - Sets state.shouldRevalidate if SWR needed
628
- * - Yields cached segments + fresh loader segments
996
+ * The middleware is serveStoredSegments. It serves a stored match when one
997
+ * answers, in this order: the prerender overlay and build store (Prerender
998
+ * and on-demand routes), the dev Static store, then the route's cache()
999
+ * record. It yields the stored segments and the loaders resolved fresh and
1000
+ * sets state.cacheHit (and state.shouldRevalidate for a cache() record, for
1001
+ * background SWR).
629
1002
  *
630
- * If cache miss:
631
- * - Passes through to next middleware
1003
+ * If nothing answers, passes through to the next middleware.
632
1004
  */
633
1005
  export function withCacheLookup<TEnv>(
634
1006
  ctx: MatchContext<TEnv>,
635
1007
  state: MatchPipelineState,
636
1008
  ): GeneratorMiddleware<ResolvedSegment> {
637
- return async function* (
638
- source: AsyncGenerator<ResolvedSegment>,
639
- ): AsyncGenerator<ResolvedSegment> {
640
- const pipelineStart = performance.now();
641
- const ms = ctx.metricsStore;
642
-
643
- // Eagerly capture the HandleStore before any async operations.
644
- // In workerd/Cloudflare, dynamic imports and fetch() inside the pipeline
645
- // can disrupt AsyncLocalStorage, causing getRequestContext() to return
646
- // undefined afterward. Capturing the reference early ensures handle replay
647
- // and handler handle-push work regardless of ALS state.
648
- const pipelineReqCtx = _getRequestContext<TEnv>();
649
- // Only the match can determine interception; the source header proves
650
- // nothing in either direction. Clear a stale reason on normal matches.
651
- if (pipelineReqCtx) {
652
- pipelineReqCtx._pprReplayPostMatchReason = ctx.isIntercept
653
- ? "intercept"
654
- : undefined;
655
- }
656
- // Per-request writable prerender overlay (durable), resolved by the handler.
657
- const prerenderOverlay = pipelineReqCtx?._prerender;
658
-
659
- const {
660
- evaluateRevalidation,
661
- buildEntryRevalidateMap,
662
- resolveLoadersOnlyWithRevalidation,
663
- resolveLoadersOnly,
664
- resolveAllSegments,
665
- resolveAllSegmentsWithRevalidation,
666
- } = getRouterContext<TEnv>();
667
-
668
- // An on-demand route may have no build-baked entry yet still needs a
669
- // writable-overlay lookup, so od joins pr in the gate. The retained producer
670
- // is NEVER run by this pipeline: a miss falls through like pr + miss.
671
- const isPassthroughPrerenderRoute = ctx.entries.some(
672
- (entry) => entry.type === "route" && entry.isPassthrough === true,
673
- );
674
- const overlayEligible =
675
- ctx.matched.od === true && !requestHeaders(ctx.request).get("X-RSC-HMR");
676
- if (
677
- prerenderStoreShortCircuits(ctx.matched.pr, ctx.request) ||
678
- overlayEligible
679
- ) {
680
- // Actions normally re-render fresh and skip the prerender store. But a pure
681
- // Prerender route's handler is evicted at build, so there is no fresh
682
- // handler to run on an action re-render -- without the fallback the
683
- // re-render falls through to the evicted handler and throws "No prerender
684
- // data found". Serve the prerendered entry instead (the action ran already;
685
- // its result is applied client-side via useActionState). A plain on-demand
686
- // route is the same case, served from the overlay first (an overlay entry
687
- // is newer than the build one, and may be the only one). Passthrough
688
- // routes keep a liveHandler, so a live render (rendersLive) re-renders
689
- // them fresh.
690
- if (!rendersLive(ctx) || !isPassthroughPrerenderRoute) {
691
- await ensurePrerenderDeps();
692
- if (prerenderStoreInstance || prerenderOverlay) {
693
- const served = yield* tryPrerenderLookup(
694
- ctx,
695
- state,
696
- pipelineStart,
697
- pipelineReqCtx,
698
- resolveLoadersOnly,
699
- resolveLoadersOnlyWithRevalidation,
700
- isPassthroughPrerenderRoute,
701
- prerenderOverlay,
702
- );
703
- if (served) return;
704
- }
705
- }
706
- }
707
-
708
- if (
709
- !rendersLive(ctx) &&
710
- !ctx.matched.pr &&
711
- globalThis.__PRERENDER_DEV_URL
712
- ) {
713
- const hasStatic = ctx.entries.some(
714
- (e) =>
715
- (e.type === "layout" ||
716
- e.type === "route" ||
717
- e.type === "parallel") &&
718
- e.isStaticPrerender,
719
- );
720
- if (hasStatic) {
721
- await ensurePrerenderDeps();
722
- if (prerenderStoreInstance) {
723
- const served = yield* tryPrerenderLookup(
724
- ctx,
725
- state,
726
- pipelineStart,
727
- pipelineReqCtx,
728
- resolveLoadersOnly,
729
- resolveLoadersOnlyWithRevalidation,
730
- isPassthroughPrerenderRoute,
731
- );
732
- if (served) return;
733
- }
734
- }
735
- }
736
-
737
- // A document HIT tail replays the handler layer through the implicit doc
738
- // scope and never runs a handler: without that scope (a context that lost
739
- // the marker before the scope resolved), fail like a lookup miss instead
740
- // of falling through to the handlers below.
741
- const tailMarker = pipelineReqCtx?._shellImplicitCache;
742
- if (tailMarker?.docTail && !ctx.cacheScope?.isShellImplicitDocScope) {
743
- throw new ShellRecordUnavailableError(tailMarker.fixedDocKey);
744
- }
745
-
746
- // A prefetch that defers a layout above the boundary (prefetch: false)
747
- // emits nothing the record holds: reading it would replay the handle
748
- // pushes of segments the response does not carry. withCacheStore skips
749
- // the write for the same request.
750
- if (
751
- rendersLive(ctx) ||
752
- !ctx.cacheScope?.enabled ||
753
- !recordUse(ctx.handlerContext).read
754
- ) {
755
- yield* source;
756
- if (ms) {
757
- ms.metrics.push({
758
- label: "pipeline:cache-miss",
759
- duration: performance.now() - pipelineStart,
760
- startTime: pipelineStart - ms.requestStart,
761
- });
762
- }
763
- return;
764
- }
765
-
766
- // Only a ppr route's records hold loader pushes, and only its shell pins
767
- // loaders (CachedEntryData.handleOwners, the loader seed): any other
768
- // route's record restores as a plain replay.
769
- const leaf = ctx.entries[ctx.entries.length - 1];
770
- const ownedPushes =
771
- leaf !== undefined && isPprEntry(leaf)
772
- ? () => loaderPins(ctx.entries, pipelineReqCtx)
773
- : undefined;
774
- const explicitLookup = await ctx.cacheScope.lookupRouteDetailed(
775
- ctx.pathname,
776
- ctx.matched.params,
777
- ctx.isIntercept,
778
- ownedPushes,
779
- );
780
- let cacheResult =
781
- explicitLookup.status === "hit" ? explicitLookup.result : null;
782
- // The scope whose record answered decides which entries it covers.
783
- let hitScope: CacheScope = ctx.cacheScope;
784
-
785
- // PPR navigation replay composed with a route-derived cache() scope. The
786
- // explicit tier stays authoritative: its hit serves under its own
787
- // key/ttl/swr semantics and reports `explicit-cache-hit` — never a false
788
- // replay HIT. ONLY a true `miss` lets the seeded doc record supply the
789
- // match (the marker's onHit observer then reports the true HIT). The
790
- // other outcomes render fresh: `bypass` (cache(false), a false
791
- // condition() — absolute opt-outs even when the pre-read gate saw a
792
- // different condition() result — or no store) and `error` (a throwing
793
- // key()/keyGenerator/store.get keeps lookupRoute's render-uncached
794
- // contract; the canonical record must not serve across a broken key
795
- // partition). The outcome comes from the lookup itself, not a re-run of
796
- // the condition, so a flapping predicate cannot re-admit the fallback.
797
- // Gated on the marker's `onExplicitHit`, set ONLY on the
798
- // navigation-replay serve path: a CAPTURE render must never fall back
799
- // here — its marker store reads through to the real store, and a
800
- // doc-keyed hit would replay the previous generation's segments instead
801
- // of re-running handlers (breaking SWR recapture freshness). Intercepts
802
- // stay source-dependent on their normal cache path (match-api never arms
803
- // replay for them).
804
- const replayMarker = pipelineReqCtx?._shellImplicitCache;
805
- if (
806
- replayMarker?.onExplicitHit &&
807
- !ctx.isIntercept &&
808
- !ctx.cacheScope.isShellImplicitDocScope
809
- ) {
810
- if (explicitLookup.status === "hit") {
811
- replayMarker.onExplicitHit();
812
- } else if (explicitLookup.status === "miss" && replayMarker.store) {
813
- // The store gate keeps report-only markers (installed on the
814
- // no-eligible-snapshot path purely for truthful status) inert: a
815
- // store-less marker minting a doc scope here would resolve the APP
816
- // store and read the REAL doc: partition — a cross-partition serve.
817
- hitScope = createShellImplicitDocScope(replayMarker);
818
- cacheResult = await hitScope.lookupRoute(
819
- ctx.pathname,
820
- ctx.matched.params,
821
- ctx.isIntercept,
822
- ownedPushes,
823
- );
824
- } else if (explicitLookup.status === "bypass") {
825
- // condition() refused at lookup time (the gate only pre-decides the
826
- // static cache(false) case) — report cache-disabled truthfully.
827
- replayMarker.onExplicitBypass?.();
828
- }
829
- // "error" stays unreported: the render is fresh and the seeded record
830
- // was not consulted, which is exactly what snapshot-miss describes; the
831
- // store already routed the failure through reportCacheError.
832
- }
833
-
834
- if (!cacheResult) {
835
- // A document shell HIT tail replays the handler layer from the entry's
836
- // doc record and must never run a handler behind the committed prelude:
837
- // a record that did not hit (it failed to decode, or the entry lost it)
838
- // ends the tail here; serveShellHit degrades the response.
839
- if (replayMarker?.docTail) {
840
- throw new ShellRecordUnavailableError(replayMarker.fixedDocKey);
841
- }
842
- yield* source;
843
- if (ms) {
844
- ms.metrics.push({
845
- label: "pipeline:cache-miss",
846
- duration: performance.now() - pipelineStart,
847
- startTime: pipelineStart - ms.requestStart,
848
- });
849
- }
850
- return;
851
- }
852
-
853
- // Entries above the cache() boundary are not in the record: resolve them
854
- // fresh, exactly as an uncached render of this request would.
855
- const boundaryIndex =
856
- hitScope.boundary === undefined
857
- ? 0
858
- : Math.max(
859
- 0,
860
- ctx.entries.findIndex((e) => e.shortCode === hitScope.boundary),
861
- );
862
- let liveSegments: ResolvedSegment[] = [];
863
- let liveMatchedIds: string[] = [];
864
- if (boundaryIndex > 0) {
865
- const liveEntries = ctx.entries.slice(0, boundaryIndex);
866
- const live: { segments: ResolvedSegment[]; matchedIds: string[] } =
867
- await ctx.Store.run(async () => {
868
- if (!ctx.isFullMatch) {
869
- return resolveAllSegmentsWithRevalidation(
870
- liveEntries,
871
- ctx.routeKey,
872
- ctx.matched.params,
873
- ctx.handlerContext,
874
- ctx.clientSegmentSet,
875
- ctx.prevParams,
876
- ctx.request,
877
- ctx.prevUrl,
878
- ctx.url,
879
- ctx.actionContext,
880
- ctx.interceptResult,
881
- ctx.localRouteName,
882
- ctx.pathname,
883
- ctx.stale,
884
- ctx.entries,
885
- );
886
- }
887
- // The full chain keeps a bare cache() marker, which also sits in
888
- // its layout's orphan list, out of the live pass: the record
889
- // supplies it and its subtree (issue #918).
890
- const segments = await resolveAllSegments(
891
- liveEntries,
892
- ctx.routeKey,
893
- ctx.matched.params,
894
- ctx.handlerContext,
895
- ctx.loaderPromises,
896
- { chain: ctx.entries },
897
- );
898
- return { segments, matchedIds: segments.map((s) => s.id) };
899
- });
900
- // The record supplies every covered segment.
901
- liveSegments = live.segments.filter(
902
- (s) => !hitScope.covers(s.id, s.namespace),
903
- );
904
- liveMatchedIds = live.matchedIds.filter((id) => !hitScope.covers(id));
905
- }
906
-
907
- state.cacheHit = true;
908
- state.cacheSource = "runtime";
909
- state.shouldRevalidate = cacheResult.shouldRevalidate;
910
- state.cachedSegments = cacheResult.segments;
911
- state.cachedMatchedIds = [
912
- ...liveMatchedIds,
913
- ...cacheResult.segments.map((s) => s.id),
914
- ];
915
-
916
- const canCheckSegmentRevalidation =
917
- !ctx.isFullMatch &&
918
- ctx.clientSegmentSet.size > 0 &&
919
- !!buildEntryRevalidateMap;
920
- const entryRevalidateMap = canCheckSegmentRevalidation
921
- ? buildEntryRevalidateMap(ctx.entries)
922
- : undefined;
923
- const searchChanged =
924
- ctx.prevUrl.search !== ctx.url.search ||
925
- pipelineReqCtx?._visitorMoved === true;
926
- const routeParamsChanged = !paramsEqual(ctx.matched.params, ctx.prevParams);
927
-
928
- yield* liveSegments;
929
-
930
- for (const segment of cacheResult.segments) {
931
- if (interceptFillSkips(ctx, segment)) continue;
932
- if (!ctx.clientSegmentSet.has(segment.id)) {
933
- if (isTraceActive()) {
934
- pushRevalidationTraceEntry({
935
- segmentId: segment.id,
936
- segmentType: segment.type,
937
- belongsToRoute: segment.belongsToRoute ?? false,
938
- source: "cache-hit",
939
- defaultShouldRevalidate: true,
940
- finalShouldRevalidate: true,
941
- reason: "new-segment",
942
- });
943
- }
944
- yield segment;
945
- continue;
946
- }
947
-
948
- // A fill keeps a held segment outright (fillSends), an intercept's
949
- // included.
950
- if (
951
- fillSends(ctx.handlerContext, segment.id, ctx.clientSegmentSet) ===
952
- false
953
- ) {
954
- keepClientSegment(segment);
955
- yield segment;
956
- continue;
957
- }
958
- if (isInterceptSegment(segment)) {
959
- yield segment;
960
- continue;
961
- }
962
-
963
- const entryInfo = entryRevalidateMap?.get(segment.id);
964
- const shouldDefaultRevalidate =
965
- (searchChanged || routeParamsChanged) &&
966
- (segment.type === "route" ||
967
- (segment.belongsToRoute &&
968
- (segment.type === "layout" || segment.type === "parallel")));
969
-
970
- if (!entryInfo || entryInfo.revalidate.length === 0) {
971
- if (shouldDefaultRevalidate) {
972
- if (isTraceActive()) {
973
- pushRevalidationTraceEntry({
974
- segmentId: segment.id,
975
- segmentType: segment.type,
976
- belongsToRoute: segment.belongsToRoute ?? false,
977
- source: "cache-hit",
978
- defaultShouldRevalidate: true,
979
- finalShouldRevalidate: true,
980
- reason: routeParamsChanged
981
- ? "cached-params-changed"
982
- : "cached-search-changed",
983
- });
984
- }
985
- yield segment;
986
- continue;
987
- }
988
- if (isTraceActive()) {
989
- pushRevalidationTraceEntry({
990
- segmentId: segment.id,
991
- segmentType: segment.type,
992
- belongsToRoute: segment.belongsToRoute ?? false,
993
- source: "cache-hit",
994
- defaultShouldRevalidate: false,
995
- finalShouldRevalidate: false,
996
- reason: "cached-no-rules",
997
- });
998
- }
999
- keepClientSegment(segment);
1000
- yield segment;
1001
- continue;
1002
- }
1003
-
1004
- const shouldRevalidate = await evaluateRevalidation({
1005
- segment,
1006
- prevParams: ctx.prevParams,
1007
- getPrevSegment: null,
1008
- request: ctx.request,
1009
- prevUrl: ctx.prevUrl,
1010
- nextUrl: ctx.url,
1011
- revalidations: entryInfo.revalidate.map((fn, i) => ({
1012
- name: `revalidate${i}`,
1013
- fn,
1014
- })),
1015
- routeKey: ctx.routeKey,
1016
- context: ctx.handlerContext,
1017
- actionContext: ctx.actionContext,
1018
- cacheStale: cacheResult.shouldRevalidate,
1019
- traceSource: "cache-hit",
1020
- });
1021
-
1022
- observeEvent({
1023
- type: "revalidation.decision",
1024
- timestamp: performance.now(),
1025
- segmentId: segment.id,
1026
- pathname: ctx.pathname,
1027
- routeKey: ctx.routeKey,
1028
- shouldRevalidate,
1029
- });
1030
-
1031
- if (!shouldRevalidate) keepClientSegment(segment);
1032
-
1033
- yield segment;
1034
- }
1035
-
1036
- const barrierReqCtx = pipelineReqCtx;
1037
- if (barrierReqCtx) {
1038
- if (barrierReqCtx._treeHasStreaming === undefined) {
1039
- barrierReqCtx._treeHasStreaming = treeHasStreaming(ctx.entries);
1040
- }
1041
- barrierReqCtx._resolveRenderBarrier(
1042
- liveSegments.length > 0
1043
- ? [...liveSegments, ...cacheResult.segments]
1044
- : cacheResult.segments,
1045
- );
1046
- }
1047
-
1048
- // Resolve loaders fresh (loaders are never cached). Shared with the
1049
- // prerender-store path via resolveFreshLoadersAndYield. The live pass
1050
- // already resolved the loaders of the entries above the boundary.
1051
- yield* resolveFreshLoadersAndYield(
1052
- ctx,
1053
- ctx.entries.slice(boundaryIndex),
1054
- state,
1055
- pipelineStart,
1056
- ms,
1057
- resolveLoadersOnly,
1058
- resolveLoadersOnlyWithRevalidation,
1059
- );
1060
- };
1009
+ return (upstream) => serveStoredSegments(ctx, state, upstream);
1061
1010
  }