@rangojs/router 0.20.0 → 0.22.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 (331) hide show
  1. package/dist/bin/rango.js +39 -15
  2. package/dist/types/browser/event-controller.d.ts +49 -0
  3. package/dist/types/browser/history-state.d.ts +16 -4
  4. package/dist/types/browser/navigation-store.d.ts +9 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +7 -0
  6. package/dist/types/browser/partial-update.d.ts +2 -1
  7. package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
  8. package/dist/types/browser/rango-state.d.ts +3 -1
  9. package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
  10. package/dist/types/browser/react/context.d.ts +25 -0
  11. package/dist/types/browser/react/location-state-shared.d.ts +84 -10
  12. package/dist/types/browser/react/location-state.d.ts +7 -2
  13. package/dist/types/browser/rsc-router.d.ts +13 -2
  14. package/dist/types/browser/types.d.ts +10 -0
  15. package/dist/types/build/generate-manifest.d.ts +2 -0
  16. package/dist/types/cache/background-task.d.ts +22 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +50 -5
  18. package/dist/types/cache/cache-policy.d.ts +17 -0
  19. package/dist/types/cache/cache-scope.d.ts +5 -6
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +34 -7
  21. package/dist/types/cache/cf/cf-cache-types.d.ts +12 -4
  22. package/dist/types/cache/document-cache.d.ts +8 -5
  23. package/dist/types/cache/handle-snapshot.d.ts +59 -50
  24. package/dist/types/cache/index.d.ts +3 -1
  25. package/dist/types/cache/invalidation-order.d.ts +14 -0
  26. package/dist/types/cache/memory-segment-store.d.ts +33 -1
  27. package/dist/types/cache/shell-snapshot.d.ts +29 -4
  28. package/dist/types/cache/store-scope.d.ts +22 -0
  29. package/dist/types/cache/tag-invalidation.d.ts +7 -6
  30. package/dist/types/cache/types.d.ts +84 -24
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +35 -17
  32. package/dist/types/client-urls/navigation.d.ts +7 -1
  33. package/dist/types/client-urls/optimistic-location.d.ts +10 -4
  34. package/dist/types/errors.d.ts +6 -0
  35. package/dist/types/handles/deferred-resolution.d.ts +18 -1
  36. package/dist/types/internal-suspense-audit.d.ts +1 -0
  37. package/dist/types/loader-store.d.ts +4 -8
  38. package/dist/types/outlet-provider.d.ts +2 -1
  39. package/dist/types/prerender/build-shell-capture.d.ts +8 -6
  40. package/dist/types/prerender/cloudflare.d.ts +49 -0
  41. package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
  42. package/dist/types/prerender/index.d.ts +12 -0
  43. package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
  44. package/dist/types/prerender/on-demand.d.ts +272 -0
  45. package/dist/types/prerender/producer-guard.d.ts +33 -0
  46. package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
  47. package/dist/types/prerender/warm-request.d.ts +65 -0
  48. package/dist/types/prerender/warm.d.ts +65 -0
  49. package/dist/types/prerender/writable-store.d.ts +170 -0
  50. package/dist/types/prerender.d.ts +27 -0
  51. package/dist/types/reverse.d.ts +1 -1
  52. package/dist/types/route-content-wrapper.d.ts +3 -1
  53. package/dist/types/router/error-handling.d.ts +2 -0
  54. package/dist/types/router/handler-context.d.ts +1 -1
  55. package/dist/types/router/match-middleware/cache-lookup.d.ts +15 -1
  56. package/dist/types/router/navigation-snapshot.d.ts +8 -1
  57. package/dist/types/router/pattern-matching.d.ts +2 -0
  58. package/dist/types/router/prefetch-default.d.ts +4 -4
  59. package/dist/types/router/prerender-match.d.ts +23 -1
  60. package/dist/types/router/route-trie-builder.d.ts +8 -1
  61. package/dist/types/router/router-context.d.ts +1 -0
  62. package/dist/types/router/router-interfaces.d.ts +32 -1
  63. package/dist/types/router/router-options.d.ts +39 -5
  64. package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
  65. package/dist/types/router/segment-resolution/loader-cache.d.ts +23 -10
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
  67. package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
  68. package/dist/types/router/trie-matching.d.ts +2 -0
  69. package/dist/types/router-versions.d.ts +39 -0
  70. package/dist/types/rsc/handler-context.d.ts +5 -0
  71. package/dist/types/rsc/shell-build-manifest.d.ts +9 -6
  72. package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
  73. package/dist/types/rsc/shell-capture.d.ts +14 -1
  74. package/dist/types/rsc/shell-serve.d.ts +2 -2
  75. package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
  76. package/dist/types/rsc/types.d.ts +12 -6
  77. package/dist/types/segment-boundary-content.d.ts +17 -0
  78. package/dist/types/segment-loader-promise.d.ts +2 -6
  79. package/dist/types/server/build-version-table.d.ts +53 -0
  80. package/dist/types/server/context.d.ts +12 -4
  81. package/dist/types/server/handle-store.d.ts +89 -46
  82. package/dist/types/server/request-context.d.ts +84 -9
  83. package/dist/types/suspense-audit-react.d.ts +26 -0
  84. package/dist/types/suspense-audit.d.ts +157 -0
  85. package/dist/types/testing/action-form.d.ts +34 -0
  86. package/dist/types/testing/build-versions.d.ts +32 -0
  87. package/dist/types/testing/dom.entry.d.ts +1 -1
  88. package/dist/types/testing/flight.entry.d.ts +3 -1
  89. package/dist/types/testing/index.d.ts +6 -0
  90. package/dist/types/testing/internal/seed-vars.d.ts +7 -2
  91. package/dist/types/testing/internal/server-modules.d.ts +8 -0
  92. package/dist/types/testing/location-state-key.d.ts +5 -1
  93. package/dist/types/testing/render-route.d.ts +77 -6
  94. package/dist/types/testing/serve-shell-request.d.ts +55 -4
  95. package/dist/types/testing/shell-status.d.ts +14 -6
  96. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
  97. package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
  98. package/dist/types/thenable-status.d.ts +6 -0
  99. package/dist/types/types/handler-context.d.ts +15 -5
  100. package/dist/types/types/route-entry.d.ts +5 -0
  101. package/dist/types/urls/path-helper.d.ts +18 -0
  102. package/dist/types/vite/discovery/build-versions.d.ts +393 -0
  103. package/dist/types/vite/discovery/installed-packages.d.ts +26 -0
  104. package/dist/types/vite/discovery/router-versions-phase.d.ts +52 -0
  105. package/dist/types/vite/discovery/shell-prerender-phase.d.ts +7 -17
  106. package/dist/types/vite/discovery/state.d.ts +25 -0
  107. package/dist/types/vite/encryption-key.d.ts +18 -0
  108. package/dist/types/vite/inject-client-debug.d.ts +6 -25
  109. package/dist/types/vite/plugin-types.d.ts +27 -0
  110. package/dist/types/vite/plugins/{version-injector.d.ts → entry-bootstrap-injector.d.ts} +4 -6
  111. package/dist/types/vite/plugins/expose-ids/export-analysis.d.ts +1 -0
  112. package/dist/types/vite/plugins/expose-ids/router-transform.d.ts +14 -0
  113. package/dist/types/vite/plugins/stable-rsc-output.d.ts +51 -0
  114. package/dist/types/vite/plugins/version-plugin.d.ts +9 -11
  115. package/dist/types/vite/plugins/virtual-entries.d.ts +18 -1
  116. package/dist/types/vite/router-discovery.d.ts +6 -0
  117. package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
  118. package/dist/types/vite/utils/client-chunks.d.ts +2 -0
  119. package/dist/types/vite/utils/compare-strings.d.ts +7 -0
  120. package/dist/types/vite/utils/package-resolution.d.ts +8 -0
  121. package/dist/types/vite/utils/prerender-utils.d.ts +2 -0
  122. package/dist/vite/index.js +1707 -931
  123. package/package.json +12 -3
  124. package/skills/cache-guide/SKILL.md +102 -37
  125. package/skills/caching/SKILL.md +56 -9
  126. package/skills/catalog.json +1 -1
  127. package/skills/client-urls/SKILL.md +16 -0
  128. package/skills/cloudflare/SKILL.md +133 -1
  129. package/skills/comparison/references/framework-comparison.md +17 -13
  130. package/skills/deployment-caching/SKILL.md +33 -12
  131. package/skills/document-cache/SKILL.md +16 -14
  132. package/skills/hooks/SKILL.md +25 -0
  133. package/skills/hooks/handle-and-actions.md +5 -0
  134. package/skills/hooks/navigation.md +38 -0
  135. package/skills/hooks/state.md +213 -11
  136. package/skills/hooks/urls.md +40 -1
  137. package/skills/host-router/SKILL.md +31 -0
  138. package/skills/intercept/SKILL.md +6 -0
  139. package/skills/loader/SKILL.md +25 -5
  140. package/skills/middleware/SKILL.md +2 -0
  141. package/skills/migrate-nextjs/SKILL.md +82 -9
  142. package/skills/ppr/SKILL.md +150 -48
  143. package/skills/prerender/SKILL.md +651 -10
  144. package/skills/rango/SKILL.md +9 -7
  145. package/skills/response-routes/SKILL.md +5 -5
  146. package/skills/route/SKILL.md +29 -0
  147. package/skills/router-setup/SKILL.md +15 -1
  148. package/skills/testing/SKILL.md +32 -27
  149. package/skills/testing/cache-prerender.md +18 -17
  150. package/skills/testing/client-components.md +193 -8
  151. package/skills/testing/handles.md +2 -0
  152. package/skills/testing/loader.md +1 -1
  153. package/skills/testing/middleware.md +1 -1
  154. package/skills/testing/render-handler.md +18 -18
  155. package/skills/testing/serve-shell-request.md +167 -22
  156. package/skills/testing/server-actions.md +17 -17
  157. package/skills/testing/server-tree.md +1 -1
  158. package/skills/typesafety/generated-files-and-cli.md +1 -1
  159. package/skills/use-cache/SKILL.md +25 -10
  160. package/skills/vercel/SKILL.md +5 -3
  161. package/skills/view-transitions/SKILL.md +9 -4
  162. package/src/bin/rango.ts +1 -1
  163. package/src/browser/event-controller.ts +119 -2
  164. package/src/browser/history-state.ts +39 -17
  165. package/src/browser/link-interceptor.ts +2 -2
  166. package/src/browser/navigation-bridge.ts +104 -41
  167. package/src/browser/navigation-store.ts +26 -0
  168. package/src/browser/navigation-transaction.ts +24 -12
  169. package/src/browser/network-error-handler.ts +2 -0
  170. package/src/browser/partial-update.ts +58 -14
  171. package/src/browser/prefetch/default-strategy.ts +22 -42
  172. package/src/browser/rango-state.ts +3 -1
  173. package/src/browser/react/Link.tsx +37 -43
  174. package/src/browser/react/NavigationProvider.tsx +85 -1
  175. package/src/browser/react/context.ts +30 -0
  176. package/src/browser/react/location-state-shared.ts +200 -34
  177. package/src/browser/react/location-state.ts +28 -77
  178. package/src/browser/react/use-action.ts +4 -1
  179. package/src/browser/react/use-handle.ts +3 -0
  180. package/src/browser/react/use-link-status.ts +4 -3
  181. package/src/browser/react/use-navigation.ts +4 -1
  182. package/src/browser/react/use-params.ts +4 -1
  183. package/src/browser/react/use-pathname.ts +4 -1
  184. package/src/browser/react/use-search-params.ts +5 -3
  185. package/src/browser/react/use-segments.ts +36 -54
  186. package/src/browser/rsc-router.tsx +93 -31
  187. package/src/browser/segment-reconciler.ts +3 -3
  188. package/src/browser/server-action-bridge.ts +43 -7
  189. package/src/browser/types.ts +11 -0
  190. package/src/build/generate-manifest.ts +17 -0
  191. package/src/build/merge-full-manifests.ts +5 -0
  192. package/src/build/route-trie.ts +1 -0
  193. package/src/cache/background-task.ts +61 -0
  194. package/src/cache/cache-key-utils.ts +91 -12
  195. package/src/cache/cache-policy.ts +11 -2
  196. package/src/cache/cache-runtime.ts +23 -23
  197. package/src/cache/cache-scope.ts +48 -36
  198. package/src/cache/cf/cf-cache-store.ts +91 -66
  199. package/src/cache/cf/cf-cache-types.ts +12 -4
  200. package/src/cache/document-cache.ts +35 -14
  201. package/src/cache/handle-snapshot.ts +118 -86
  202. package/src/cache/index.ts +5 -0
  203. package/src/cache/invalidation-order.ts +53 -0
  204. package/src/cache/memory-segment-store.ts +52 -1
  205. package/src/cache/shell-snapshot.ts +52 -5
  206. package/src/cache/store-scope.ts +36 -0
  207. package/src/cache/tag-invalidation.ts +7 -5
  208. package/src/cache/types.ts +90 -24
  209. package/src/cache/vercel/vercel-cache-store.ts +68 -26
  210. package/src/client-urls/client-root.tsx +1 -0
  211. package/src/client-urls/navigation.ts +10 -0
  212. package/src/client-urls/optimistic-location.ts +10 -4
  213. package/src/client.tsx +40 -3
  214. package/src/errors.ts +13 -6
  215. package/src/handles/deferred-resolution.ts +36 -0
  216. package/src/internal-suspense-audit.ts +12 -0
  217. package/src/loader-store.ts +4 -9
  218. package/src/outlet-provider.tsx +25 -1
  219. package/src/prerender/build-shell-capture.ts +11 -9
  220. package/src/prerender/cloudflare.ts +130 -0
  221. package/src/prerender/create-prerender-trigger.ts +780 -0
  222. package/src/prerender/index.ts +39 -0
  223. package/src/prerender/memory-prerender-store.ts +115 -0
  224. package/src/prerender/on-demand.ts +339 -0
  225. package/src/prerender/producer-guard.ts +66 -0
  226. package/src/prerender/shell-manifest-key.ts +12 -17
  227. package/src/prerender/warm-request.ts +154 -0
  228. package/src/prerender/warm.ts +193 -0
  229. package/src/prerender/writable-store.ts +278 -0
  230. package/src/prerender.ts +29 -0
  231. package/src/reverse.ts +1 -1
  232. package/src/route-content-wrapper.tsx +107 -13
  233. package/src/router/error-handling.ts +12 -12
  234. package/src/router/find-match.ts +1 -0
  235. package/src/router/handler-context.ts +2 -0
  236. package/src/router/intercept-resolution.ts +15 -8
  237. package/src/router/loader-resolution.ts +2 -10
  238. package/src/router/manifest.ts +2 -0
  239. package/src/router/match-middleware/background-revalidation.ts +2 -0
  240. package/src/router/match-middleware/cache-lookup.ts +239 -54
  241. package/src/router/navigation-snapshot.ts +25 -6
  242. package/src/router/pattern-matching.ts +14 -0
  243. package/src/router/prefetch-default.ts +4 -4
  244. package/src/router/prerender-match.ts +460 -352
  245. package/src/router/request-classification.ts +15 -12
  246. package/src/router/route-trie-builder.ts +9 -0
  247. package/src/router/router-context.ts +1 -0
  248. package/src/router/router-interfaces.ts +35 -1
  249. package/src/router/router-options.ts +45 -5
  250. package/src/router/segment-resolution/fresh.ts +20 -19
  251. package/src/router/segment-resolution/loader-cache.ts +110 -77
  252. package/src/router/segment-resolution/revalidation.ts +34 -24
  253. package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
  254. package/src/router/trie-matching.ts +3 -0
  255. package/src/router-versions.ts +60 -0
  256. package/src/router.ts +107 -1
  257. package/src/rsc/full-payload.ts +6 -2
  258. package/src/rsc/handler-context.ts +5 -0
  259. package/src/rsc/handler.ts +64 -7
  260. package/src/rsc/progressive-enhancement.ts +5 -1
  261. package/src/rsc/response-cache-serve.ts +10 -4
  262. package/src/rsc/rsc-rendering.ts +65 -12
  263. package/src/rsc/shell-build-manifest.ts +17 -12
  264. package/src/rsc/shell-capture-constants.ts +39 -7
  265. package/src/rsc/shell-capture.ts +144 -45
  266. package/src/rsc/shell-serve.ts +12 -6
  267. package/src/rsc/ssr-module-loader.ts +26 -0
  268. package/src/rsc/types.ts +12 -6
  269. package/src/segment-boundary-content.ts +25 -0
  270. package/src/segment-loader-promise.ts +28 -38
  271. package/src/segment-system.tsx +117 -57
  272. package/src/server/build-version-table.ts +130 -0
  273. package/src/server/context.ts +40 -17
  274. package/src/server/cookie-store.ts +5 -0
  275. package/src/server/handle-store.ts +357 -127
  276. package/src/server/request-context.ts +149 -12
  277. package/src/ssr/ssr-root.tsx +32 -13
  278. package/src/suspense-audit-react.tsx +98 -0
  279. package/src/suspense-audit.ts +1030 -0
  280. package/src/testing/action-form.ts +66 -0
  281. package/src/testing/build-versions.ts +64 -0
  282. package/src/testing/dispatch.ts +6 -0
  283. package/src/testing/dom.entry.ts +1 -0
  284. package/src/testing/flight-runtime.d.ts +27 -0
  285. package/src/testing/flight.entry.ts +4 -0
  286. package/src/testing/index.ts +22 -0
  287. package/src/testing/internal/context.ts +2 -1
  288. package/src/testing/internal/seed-vars.ts +14 -2
  289. package/src/testing/internal/server-modules.ts +32 -0
  290. package/src/testing/location-state-key.ts +18 -8
  291. package/src/testing/render-handler.ts +2 -1
  292. package/src/testing/render-route.tsx +293 -46
  293. package/src/testing/serve-shell-request.ts +262 -37
  294. package/src/testing/shell-status.ts +17 -6
  295. package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
  296. package/src/testing/vitest-stubs/version.ts +4 -1
  297. package/src/thenable-status.ts +10 -0
  298. package/src/types/handler-context.ts +15 -5
  299. package/src/types/route-entry.ts +6 -0
  300. package/src/urls/path-helper.ts +69 -9
  301. package/src/use-loader.tsx +57 -30
  302. package/src/vite/discovery/build-versions.ts +1281 -0
  303. package/src/vite/discovery/bundle-postprocess.ts +4 -0
  304. package/src/vite/discovery/discover-routers.ts +15 -0
  305. package/src/vite/discovery/installed-packages.ts +159 -0
  306. package/src/vite/discovery/prerender-collection.ts +38 -1
  307. package/src/vite/discovery/router-versions-phase.ts +336 -0
  308. package/src/vite/discovery/shell-prerender-phase.ts +33 -43
  309. package/src/vite/discovery/state.ts +36 -1
  310. package/src/vite/discovery/virtual-module-codegen.ts +7 -2
  311. package/src/vite/encryption-key.ts +101 -9
  312. package/src/vite/inject-client-debug.ts +42 -20
  313. package/src/vite/plugin-types.ts +28 -0
  314. package/src/vite/plugins/{version-injector.ts → entry-bootstrap-injector.ts} +7 -22
  315. package/src/vite/plugins/expose-ids/export-analysis.ts +1 -1
  316. package/src/vite/plugins/expose-ids/router-transform.ts +36 -15
  317. package/src/vite/plugins/expose-internal-ids.ts +12 -3
  318. package/src/vite/plugins/stable-rsc-output.ts +147 -0
  319. package/src/vite/plugins/version-plugin.ts +19 -24
  320. package/src/vite/plugins/version.d.ts +17 -2
  321. package/src/vite/plugins/virtual-entries.ts +39 -6
  322. package/src/vite/rango.ts +28 -3
  323. package/src/vite/router-discovery.ts +96 -13
  324. package/src/vite/utils/bundle-analysis.ts +39 -7
  325. package/src/vite/utils/client-chunks.ts +5 -1
  326. package/src/vite/utils/compare-strings.ts +9 -0
  327. package/src/vite/utils/package-resolution.ts +9 -0
  328. package/src/vite/utils/prerender-utils.ts +4 -1
  329. package/src/vite/utils/shared-utils.ts +1 -1
  330. package/dist/types/segment-content-promise.d.ts +0 -13
  331. package/src/segment-content-promise.ts +0 -67
@@ -55,13 +55,23 @@ function createLateHandlePushError(
55
55
  interface SlotTag {
56
56
  /** Pushed inside a DSL loader scope; captureHandles drops it. */
57
57
  loader?: boolean;
58
- /** A loader-cache replay of this loader's recorded push (pushReplayed). */
58
+ /** A cached unit's replay of this loader's recorded push (pushReplayed). */
59
59
  replayOf?: string;
60
- /** Pushed by this loader's live run after it replaced its replayed slots. */
60
+ /**
61
+ * A record's copy for a loader that record does not supply the value of
62
+ * (pushPlaceholder): it stands in until the loader's own source delivers.
63
+ */
64
+ placeholderOf?: string;
65
+ /** Pushed by this loader's live run after it replaced its copies. */
61
66
  liveOf?: string;
67
+ /**
68
+ * The loaders around that run's body whose runs replaced copies too: their
69
+ * next push follows this one, so a run's pushes keep their order.
70
+ */
71
+ under?: readonly string[];
62
72
  /** Pushed by this loader's body and kept in records (push `owner`). */
63
73
  owner?: string;
64
- /** A document HIT's restore of this loader's record copy (pushRestored). */
74
+ /** A record's copy for a loader served from that record's pin (pushRestored). */
65
75
  restoredOf?: string;
66
76
  /** A restored slot a dropped settled push of its loader stood in for. */
67
77
  matched?: true;
@@ -70,6 +80,26 @@ interface SlotTag {
70
80
  // Shared by every plain DSL-loader push; tags are never mutated in place.
71
81
  const LOADER_SLOT: SlotTag = Object.freeze({ loader: true });
72
82
 
83
+ /**
84
+ * What a restored record is to one loader's handle pushes on this request
85
+ * (loader-cache.ts loaderPins; docs/design/handle-push-ownership.md). Two
86
+ * answers let the record's copies stand, two make them placeholders, and in
87
+ * each pair one also speaks for the loader's other pushes:
88
+ * - "pin": the request serves the loader's value from the pin stored with
89
+ * the record, and the record lists every settled push of that run. Its
90
+ * copies stand, and any other settled push of the loader is dropped.
91
+ * - "copies": its copies stand and nothing else is known: a dependency of
92
+ * pinned loaders, or a pin written before captures recorded every push.
93
+ * - "hole": the route runs the loader on this request (it registers it and
94
+ * the record does not pin it). Its copies are placeholders and a push made
95
+ * in its body is its own, with or without copies.
96
+ * - "placeholders": its copies are placeholders and nothing else is known: a
97
+ * dependency the route does not register while a pin is missing (the
98
+ * record does not name the loader that ran it). Without copies its push
99
+ * counts for the loader that awaits it.
100
+ */
101
+ export type RecordAuthority = "pin" | "copies" | "hole" | "placeholders";
102
+
73
103
  function cloneHandleData(data: HandleData): HandleData {
74
104
  const clone: HandleData = {};
75
105
  for (const handleName in data) {
@@ -158,15 +188,17 @@ export interface HandleStore {
158
188
  ): void;
159
189
 
160
190
  /**
161
- * push() a loader-cache HIT's replayed value, recorded from `loaderId`'s
162
- * body (loader-cache.ts replayLoaderHandles). If that loader then runs live
163
- * in this request, its first push removes every value replayed for it and
164
- * takes the first one's position (later live pushes follow it), so the
165
- * handle output stays live without a duplicate. A push counts as that
166
- * loader's when it is made in its body, or in a body it entered that has
167
- * no replayed values of its own (the capture credits a dependency's push
168
- * under a live-lane loader to that loader); a hole's run that ends without
169
- * one drops them (settleLoaderRun).
191
+ * push() a cached unit's copy of a value `loaderId`'s body pushed
192
+ * (handle-snapshot.ts appendHandles: a loader-cache or "use cache" HIT). If
193
+ * that loader then runs live in this request, its first push removes every
194
+ * copy pushed for it (replays and placeholders) and takes the first one's
195
+ * position, and the pushes of that run follow in push order, a dependency's
196
+ * included, so the handle output stays live without a duplicate. A push
197
+ * counts as that loader's when it is made in its body, or in a body it
198
+ * entered that has no copies of its own (the capture credits a dependency's
199
+ * push under a live-lane loader to that loader). A run that ends without a
200
+ * push leaves a replay in place, unless the loader is a hole
201
+ * (settleLoaderRun).
170
202
  */
171
203
  pushReplayed(
172
204
  handleName: string,
@@ -176,16 +208,27 @@ export interface HandleStore {
176
208
  ): void;
177
209
 
178
210
  /**
179
- * push() a document HIT's copy of `loaderId`'s settled push, restored from
180
- * the shell's doc record (handle-snapshot.ts restoreHandles). The record is
181
- * authoritative for that loader's settled pushes: the prelude was rendered
182
- * from it, and a run of the loader on the HIT reads the store, not the
183
- * capture's values. So from then on this request drops every settled,
184
- * thenable-free push the loader makes (live, or replayed from a cache
185
- * entry, anywhere inside its body), and keeps the ones holding a thenable
186
- * (holdsThenable: what the record could not keep, `runs`), placed among the
187
- * restored copies in the loader's push order. Only bake-lane owners are
188
- * restored this way (withCacheLookup): a live-lane loader is a hole.
211
+ * push() a record's copy of `loaderId`'s push as a placeholder
212
+ * (handle-snapshot.ts restoreHandles): the record does not supply that
213
+ * loader's value on this request, so its own source decides its pushes. A
214
+ * live run replaces it as it does a replay; a run that ends without a push
215
+ * drops it (settleLoaderRun); a cached unit that claims the loader delivers
216
+ * in its place (replacePlaceholders).
217
+ */
218
+ pushPlaceholder(
219
+ handleName: string,
220
+ segmentId: string,
221
+ data: unknown,
222
+ loaderId: string,
223
+ ): void;
224
+
225
+ /**
226
+ * push() a record's copy of `loaderId`'s settled push that stands
227
+ * (restoreHandles): the record supplies the loader's value too. From then
228
+ * on this request drops every settled, thenable-free push the loader makes
229
+ * (live, or replayed from a cache entry, anywhere inside its body), and
230
+ * keeps the ones holding a thenable (holdsThenable: what the record could
231
+ * not keep), placed among the restored copies in the loader's push order.
189
232
  */
190
233
  pushRestored(
191
234
  handleName: string,
@@ -195,41 +238,34 @@ export interface HandleStore {
195
238
  ): void;
196
239
 
197
240
  /**
198
- * The loaders the route runs on the live lane (liveLaneLoaderIds), from a
199
- * PPR shell record's restore (handle-snapshot.ts restoreHandles): the
200
- * holes. A push counts for a restored loader when that loader is the
201
- * replayed one or the innermost enclosing loader body that restored
202
- * copies; the walk out through enclosing bodies stops at a hole's body,
203
- * and a replay of a hole's push counts for none. So a restored loader
204
- * never takes a push made inside a hole it awaited, while a body inside the
205
- * hole that restored copies of its own (a dependency the capture ran under
206
- * a bake-lane loader) still takes its pushes.
241
+ * What the record this request restored is to each loader (RecordAuthority;
242
+ * CacheScope.lookupRouteDetailed sets it on a hit). A "pin" loader counts
243
+ * as restored with or without copies. A hole is a "hole" loader or one with
244
+ * placeholders: the walk out through enclosing loader bodies that finds the
245
+ * loader a push counts for stops at its body, so a pinned loader never
246
+ * takes a push made inside a hole it awaited.
207
247
  */
208
- markLiveLane(loaderIds: ReadonlySet<string>): void;
248
+ setRecordAuthority(authority: (loaderId: string) => RecordAuthority): void;
209
249
 
210
250
  /**
211
251
  * `loaderId`'s run in this request settled (resolved or rejected), its
212
- * pushes made (loader-resolution.ts createLoaderExecutor). When the loader
213
- * is a hole (markLiveLane), values replayed for it that no push of the run
214
- * replaced are dropped: the run's output decides, so a hole that throws or
215
- * skips its push shows none rather than the capture's. Any other loader's
216
- * replayed values stay: a bake-lane loader's record is authoritative for
217
- * its settled pushes, as its pin is for its data. Restored copies are not
218
- * replays and stand.
252
+ * pushes made (loader-resolution.ts createLoaderExecutor). Its placeholders
253
+ * that no push of the run replaced are dropped: the run's output decides,
254
+ * so a loader that throws or skips its push shows none rather than the
255
+ * record's. A hole's replays go with them. Any other loader's replays stay
256
+ * (a dependency another entry's HIT replayed), and so do restored copies.
219
257
  */
220
258
  settleLoaderRun(loaderId: string): void;
221
259
 
222
260
  /**
223
- * A hole's own cache() HIT (loader-cache.ts replayLoaderHandles, once it
224
- * claims the hole): the values replayed for hole `loaderId` before (its
225
- * shell copies, which restoreHandles leaves unclaimed, those of the
226
- * dependencies credited to it included) give way to the pushes the entry
227
- * recorded, none when it recorded none, so they match the entry's data.
228
- * `deliver` makes the pushReplayed calls, which take the removed values'
229
- * place in each array. Any other loader, or a hole with nothing replayed,
230
- * delivers as a plain replay.
261
+ * A cached unit's HIT delivering the pushes it recorded for `loaderIds`,
262
+ * the loaders it claimed (appendHandles). Their placeholders give way to
263
+ * what `deliver` pushes for them, nothing when it pushes nothing, so the
264
+ * page shows the pushes of the run that produced the unit's value. The
265
+ * pushReplayed calls `deliver` makes take the removed values' place in each
266
+ * array, in order. Without placeholders it is a plain replay.
231
267
  */
232
- redeliverReplays(loaderId: string, deliver: () => void): void;
268
+ replacePlaceholders(loaderIds: readonly string[], deliver: () => void): void;
233
269
 
234
270
  /**
235
271
  * Get all collected handle data after all handlers have settled.
@@ -237,6 +273,24 @@ export interface HandleStore {
237
273
  */
238
274
  getData(): Promise<HandleData>;
239
275
 
276
+ /**
277
+ * Fix the document lane at the store's current state. From then on
278
+ * `stream("settled")` yields this state and nothing newer, and
279
+ * `streamLate()` delivers every change made after it, whether or not it
280
+ * beat the handler barrier. The first call wins. It must run before the
281
+ * document stream is first read: a stream that started on the live state
282
+ * stays on it (reported through `onError` in development).
283
+ *
284
+ * A PPR shell HIT calls it once its record is replayed and before a loader
285
+ * runs (rsc-rendering.ts serveShellHit). The prelude was rendered from the
286
+ * record's handle data (resolvedHandleStream `recordedOnly`), so that is
287
+ * what the client must hydrate with. What the request's loaders then push,
288
+ * replace or drop is theirs, not the shell's: in the document snapshot it
289
+ * rendered elements the prelude never had, or left out ones it has (issue
290
+ * #1035), and it got there or not by a race with the lane's first read.
291
+ */
292
+ freezeDocumentSnapshot(): void;
293
+
240
294
  /**
241
295
  * Get an async iterator that yields handle data on each push.
242
296
  * Completes at the chosen settlement barrier: "fullySettled" (default —
@@ -245,6 +299,9 @@ export interface HandleStore {
245
299
  * blocking positions and must not wait on loader bodies).
246
300
  * Each yield contains the full accumulated state (not just the delta).
247
301
  * Safe for concurrent consumers (per-consumer version cursor).
302
+ * The "settled" lane of a frozen store (freezeDocumentSnapshot) yields the
303
+ * frozen state once, at once, and ends. That yield is the store's own
304
+ * frozen object: a consumer must not write to it.
248
305
  */
249
306
  stream(
250
307
  until?: "settled" | "fullySettled",
@@ -256,7 +313,9 @@ export interface HandleStore {
256
313
  * fullySettled. Returns without yielding when the auxiliary lane is empty
257
314
  * by the time `settled` resolves. The client consumes this post-hydration
258
315
  * (rsc-router.tsx) and merges via the same application path as nav-lane
259
- * progressive handle updates.
316
+ * progressive handle updates. On a frozen store (freezeDocumentSnapshot)
317
+ * it starts from the frozen state instead: a change made before the
318
+ * handler barrier is late too.
260
319
  */
261
320
  streamLate(): AsyncGenerator<HandleData, void, unknown>;
262
321
 
@@ -281,6 +340,9 @@ export interface HandleStore {
281
340
  segmentId: string,
282
341
  ): Record<string, (string | null)[]> | undefined;
283
342
 
343
+ /** The segment ids for which getRecordOwners returns an entry. */
344
+ getOwnedSegmentIds(): string[];
345
+
284
346
  /**
285
347
  * Replay cached handle data back into the store (for cache hits).
286
348
  * Used to restore handle data when serving cached segments.
@@ -317,45 +379,51 @@ export function createHandleStore(): HandleStore {
317
379
  // (not value identity) so primitive values are covered too.
318
380
  // replaySegmentData installs fresh arrays, so restored values are untagged.
319
381
  const slotTags = new WeakMap<unknown[], (SlotTag | undefined)[]>();
320
- // Replayed loaders that have not pushed live yet: loaderId -> the arrays
321
- // holding their replayed slots.
382
+ // Copies standing in for a loader that has not pushed live yet: loaderId ->
383
+ // the arrays holding them. Kept apart by kind: only a placeholder gives way
384
+ // to a run that ends without a push, or to a cached unit's delivery.
322
385
  const pendingReplays = new Map<string, Set<unknown[]>>();
323
- // Replayed loaders whose live run already replaced those slots.
324
- const replacedReplays = new Set<string>();
325
- // Loaders a document HIT restored record copies for (pushRestored).
386
+ const pendingPlaceholders = new Map<string, Set<unknown[]>>();
387
+ // Loaders whose live run already replaced its copies.
388
+ const replaced = new Set<string>();
389
+ // Loaders a record restored standing copies for (pushRestored), and those
390
+ // it restored placeholders for (pushPlaceholder).
326
391
  const restoredLoaders = new Set<string>();
392
+ const placeholderLoaders = new Set<string>();
393
+ let authority: ((loaderId: string) => RecordAuthority) | undefined;
327
394
  const isRestored = (loaderId: string): boolean =>
328
- restoredLoaders.has(loaderId);
329
- // The route's live-lane loaders (markLiveLane): holes a restore never
330
- // reaches into.
331
- let liveLane: ReadonlySet<string> | undefined;
395
+ restoredLoaders.has(loaderId) || authority?.(loaderId) === "pin";
332
396
  const isHole = (loaderId: string): boolean =>
333
- liveLane?.has(loaderId) ?? false;
397
+ placeholderLoaders.has(loaderId) || authority?.(loaderId) === "hole";
334
398
 
335
- const hasReplays = (loaderId: string): boolean =>
336
- pendingReplays.has(loaderId) || replacedReplays.has(loaderId);
399
+ const hasCopies = (loaderId: string): boolean =>
400
+ pendingReplays.has(loaderId) ||
401
+ pendingPlaceholders.has(loaderId) ||
402
+ replaced.has(loaderId);
337
403
 
338
404
  // The restored loader a push counts for, as the capture credits it
339
405
  // (shell-capture.ts deriveShellCaptureContext): the replayed loader when it
340
- // restored one, else the innermost enclosing loader body that did. A
406
+ // is restored, else the innermost enclosing loader body that is. A
341
407
  // "use cache" hit inside a restored body replays a dependency's push the
342
408
  // capture recorded under that body. A hole's body, or a replay of a hole's
343
409
  // push, ends the search: a live loader a restored loader awaits is still
344
410
  // live.
345
411
  function restoringLoader(replayOf: string | undefined): string | undefined {
346
412
  if (replayOf !== undefined) {
347
- if (restoredLoaders.has(replayOf)) return replayOf;
413
+ if (isRestored(replayOf)) return replayOf;
348
414
  // Defensive: a hole's own cache() replay runs outside any loader body,
349
415
  // so the walk below finds no restored loader either.
350
416
  if (isHole(replayOf)) return undefined;
351
417
  }
352
418
  return findEnclosingLoaderBody(isRestored, isHole);
353
419
  }
354
- // The replay or restore whose this.push() is in flight, as the tag its
355
- // slot takes. Set around the call, not passed, so a capture wrapping
356
- // push() sees it as a push.
357
- let replaying: Pick<SlotTag, "replayOf" | "restoredOf"> | undefined;
358
- // A redeliverReplays delivery in flight: per array, where its next
420
+ // The copy whose this.push() is in flight, as the tag its slot takes. Set
421
+ // around the call, not passed, so a capture wrapping push() sees it as a
422
+ // push.
423
+ let replaying:
424
+ | Pick<SlotTag, "replayOf" | "placeholderOf" | "restoredOf">
425
+ | undefined;
426
+ // A replacePlaceholders delivery in flight: per array, where its next
359
427
  // pushReplayed value goes (the removed values' place).
360
428
  let redelivery: Map<unknown[], number> | undefined;
361
429
 
@@ -383,22 +451,23 @@ export function createHandleStore(): HandleStore {
383
451
  tags.splice(at, 0, tag);
384
452
  }
385
453
 
386
- // Remove `loaderId`'s replayed slots; returns the first one's position.
387
- function removeReplayedSlots(
454
+ // Remove the slots `drops` accepts; returns the first one's position.
455
+ function removeSlots(
388
456
  values: unknown[],
389
- loaderId: string,
457
+ drops: (tag: SlotTag) => boolean,
390
458
  ): number | undefined {
391
459
  const tags = slotTags.get(values);
392
460
  if (!tags) return undefined;
393
461
  let first: number | undefined;
394
462
  let kept = 0;
395
463
  for (let i = 0; i < values.length; i++) {
396
- if (tags[i]?.replayOf === loaderId) {
464
+ const tag = tags[i];
465
+ if (tag && drops(tag)) {
397
466
  first ??= kept;
398
467
  continue;
399
468
  }
400
469
  values[kept] = values[i];
401
- tags[kept] = tags[i];
470
+ tags[kept] = tag;
402
471
  kept++;
403
472
  }
404
473
  values.length = kept;
@@ -406,32 +475,80 @@ export function createHandleStore(): HandleStore {
406
475
  return first;
407
476
  }
408
477
 
409
- // Where a live push by a replayed loader lands in `values`: its first push
410
- // removes all of its replayed slots and takes the first one's position in
411
- // this array; later pushes follow its previous live push. undefined appends.
412
- function liveReplacementIndex(
413
- loaderId: string,
414
- values: unknown[],
415
- ): number | undefined {
416
- const pending = pendingReplays.get(loaderId);
417
- if (pending) {
418
- pendingReplays.delete(loaderId);
419
- replacedReplays.add(loaderId);
420
- let at: number | undefined;
421
- for (const replayed of pending) {
422
- const first = removeReplayedSlots(replayed, loaderId);
423
- if (replayed === values) at = first;
424
- }
425
- return at;
426
- }
478
+ // After the last push of `loaderId`'s run in `values`: its own, or one made
479
+ // in a body it entered. undefined: none yet.
480
+ function runCursor(loaderId: string, values: unknown[]): number | undefined {
427
481
  const tags = slotTags.get(values);
428
482
  if (!tags) return undefined;
429
483
  for (let i = tags.length - 1; i >= 0; i--) {
430
- if (tags[i]?.liveOf === loaderId) return i + 1;
484
+ const tag = tags[i];
485
+ if (tag?.liveOf === loaderId || tag?.under?.includes(loaderId)) {
486
+ return i + 1;
487
+ }
431
488
  }
432
489
  return undefined;
433
490
  }
434
491
 
492
+ // The loaders around `pusher`'s body, innermost first, whose runs replaced
493
+ // copies (SlotTag.under).
494
+ function replacedAround(pusher: string): string[] | undefined {
495
+ let around: string[] | undefined;
496
+ let outside = false;
497
+ findEnclosingLoaderBody((loaderId) => {
498
+ if (outside && replaced.has(loaderId)) (around ??= []).push(loaderId);
499
+ if (loaderId === pusher) outside = true;
500
+ return false;
501
+ });
502
+ return around;
503
+ }
504
+
505
+ // Remove `loaderId`'s pending placeholders, and its replays with `replays`.
506
+ // Returns the first position removed from `values`.
507
+ function dropCopies(
508
+ loaderId: string,
509
+ replays: boolean,
510
+ values?: unknown[],
511
+ ): number | undefined {
512
+ const placeholderArrays = pendingPlaceholders.get(loaderId);
513
+ const replayArrays = replays ? pendingReplays.get(loaderId) : undefined;
514
+ if (!placeholderArrays && !replayArrays) return undefined;
515
+ pendingPlaceholders.delete(loaderId);
516
+ if (replayArrays) pendingReplays.delete(loaderId);
517
+ replaced.add(loaderId);
518
+ const own = (tag: SlotTag): boolean =>
519
+ tag.placeholderOf === loaderId || (replays && tag.replayOf === loaderId);
520
+ let at: number | undefined;
521
+ for (const arrays of [placeholderArrays, replayArrays]) {
522
+ if (!arrays) continue;
523
+ for (const copied of arrays) {
524
+ const first = removeSlots(copied, own);
525
+ if (copied === values) at ??= first;
526
+ }
527
+ }
528
+ return at;
529
+ }
530
+
531
+ // Where a live push by a loader with copies lands in `values`: its first
532
+ // push removes all of its copies and takes the first one's position in this
533
+ // array; later pushes follow the last push of its run. undefined: no place
534
+ // of its own here.
535
+ function liveReplacementIndex(
536
+ loaderId: string,
537
+ values: unknown[],
538
+ ): number | undefined {
539
+ return dropCopies(loaderId, true, values) ?? runCursor(loaderId, values);
540
+ }
541
+
542
+ function addPending(
543
+ pending: Map<string, Set<unknown[]>>,
544
+ loaderId: string,
545
+ values: unknown[],
546
+ ): void {
547
+ let arrays = pending.get(loaderId);
548
+ if (!arrays) pending.set(loaderId, (arrays = new Set()));
549
+ arrays.add(values);
550
+ }
551
+
435
552
  // A restored loader's settled push: the first of its restored slots here
436
553
  // not yet matched stands in for it. Returns nothing; the push is dropped.
437
554
  function matchRestoredSlot(values: unknown[], loaderId: string): void {
@@ -561,6 +678,12 @@ export function createHandleStore(): HandleStore {
561
678
  let emissionWaiters: (() => void)[] = [];
562
679
  let completed = false;
563
680
  let streamConsumed = false;
681
+ // freezeDocumentSnapshot: the state the document lane serves, and the
682
+ // version the late channel starts after.
683
+ let documentSnapshot: { version: number; data: HandleData } | undefined;
684
+ // A stream("settled") consumer started on the live state: a freeze after
685
+ // that does not reach it.
686
+ let documentStreamRead = false;
564
687
 
565
688
  // Wake every waiting consumer (new push or a settlement barrier fired).
566
689
  function signalEmission() {
@@ -631,21 +754,27 @@ export function createHandleStore(): HandleStore {
631
754
  throw error;
632
755
  }
633
756
 
634
- const { replayOf, restoredOf } = replaying ?? {};
635
- // A restore is no loader's push. Any other push, while a loader has
636
- // replayed or restored slots to reconcile, is attributed as the capture
757
+ const { replayOf, placeholderOf, restoredOf } = replaying ?? {};
758
+ // A record's copy is no loader's push. Any other push, while a loader
759
+ // has copies or a restore to reconcile, is attributed as the capture
637
760
  // credits it: to the innermost loader body it is made in that has
638
- // replayed slots, up to a hole's body, so a replay made in a body
639
- // replaces that body's replayed copies and a dependency's push replaces
640
- // those of the live-lane loader it ran under. Its record copy stands
641
- // when a restored loader counts it (restoringLoader).
761
+ // copies, up to a hole's body, so a replay made in a body replaces
762
+ // that body's copies and a dependency's push replaces those of the
763
+ // live-lane loader it ran under. Its record copy stands when a
764
+ // restored loader counts it (restoringLoader).
642
765
  let pusher: string | undefined;
643
766
  let restoredBy: string | undefined;
644
- if (!restoredOf) {
645
- if (pendingReplays.size > 0 || replacedReplays.size > 0) {
646
- pusher = findEnclosingLoaderBody(hasReplays, isHole);
767
+ if (restoredOf === undefined && placeholderOf === undefined) {
768
+ if (
769
+ pendingReplays.size > 0 ||
770
+ pendingPlaceholders.size > 0 ||
771
+ replaced.size > 0
772
+ ) {
773
+ pusher = findEnclosingLoaderBody(hasCopies, isHole);
774
+ }
775
+ if (restoredLoaders.size > 0 || authority) {
776
+ restoredBy = restoringLoader(replayOf);
647
777
  }
648
- if (restoredLoaders.size > 0) restoredBy = restoringLoader(replayOf);
649
778
  }
650
779
  if (restoredBy !== undefined && !holdsThenable(value)) {
651
780
  const existing = data[handleName]?.[segmentId];
@@ -663,32 +792,52 @@ export function createHandleStore(): HandleStore {
663
792
  const loader = (loaderPush ?? isInsideLoaderScope()) || undefined;
664
793
  let at: number | undefined;
665
794
  let liveOf: string | undefined;
666
- if (restoredOf) {
795
+ let under: string[] | undefined;
796
+ if (restoredOf !== undefined) {
667
797
  restoredLoaders.add(restoredOf);
798
+ } else if (placeholderOf !== undefined) {
799
+ placeholderLoaders.add(placeholderOf);
668
800
  } else if (replayOf !== undefined && redelivery?.has(values)) {
669
801
  at = redelivery.get(values)!;
670
802
  redelivery.set(values, at + 1);
671
803
  } else if (pusher !== undefined) {
804
+ under = replacedAround(pusher);
672
805
  at = liveReplacementIndex(pusher, values);
806
+ // No place of its own here: it follows the run it was made in.
807
+ for (let i = 0; at === undefined && under && i < under.length; i++) {
808
+ at = runCursor(under[i]!, values);
809
+ }
673
810
  liveOf = pusher;
674
811
  } else if (restoredBy !== undefined) {
675
812
  at = restoredInsertIndex(values, restoredBy);
676
- if (!replayOf) liveOf = restoredBy;
813
+ if (replayOf === undefined) liveOf = restoredBy;
677
814
  }
678
815
  insertSlot(
679
816
  values,
680
817
  at,
681
818
  value,
682
- replayOf || liveOf || owner || restoredOf
683
- ? { loader, replayOf, liveOf, owner, restoredOf }
819
+ replayOf !== undefined ||
820
+ placeholderOf !== undefined ||
821
+ restoredOf !== undefined ||
822
+ liveOf !== undefined ||
823
+ owner !== undefined
824
+ ? {
825
+ loader,
826
+ replayOf,
827
+ placeholderOf,
828
+ restoredOf,
829
+ liveOf,
830
+ under,
831
+ owner,
832
+ }
684
833
  : loader
685
834
  ? LOADER_SLOT
686
835
  : undefined,
687
836
  );
688
- if (replayOf) {
689
- let arrays = pendingReplays.get(replayOf);
690
- if (!arrays) pendingReplays.set(replayOf, (arrays = new Set()));
691
- arrays.add(values);
837
+ if (replayOf !== undefined) {
838
+ addPending(pendingReplays, replayOf, values);
839
+ } else if (placeholderOf !== undefined) {
840
+ addPending(pendingPlaceholders, placeholderOf, values);
692
841
  }
693
842
 
694
843
  // Bump the version; each consumer's cursor decides when to clone+yield.
@@ -696,7 +845,7 @@ export function createHandleStore(): HandleStore {
696
845
  signalEmission();
697
846
  },
698
847
 
699
- // Both go through this.push: active captures see a replay like a live
848
+ // All three go through this.push: active captures see a copy like a live
700
849
  // push, and a diverting capture keeps it out of the store.
701
850
  pushReplayed(
702
851
  handleName: string,
@@ -707,6 +856,21 @@ export function createHandleStore(): HandleStore {
707
856
  replayPush(this, { replayOf: loaderId }, handleName, segmentId, value);
708
857
  },
709
858
 
859
+ pushPlaceholder(
860
+ handleName: string,
861
+ segmentId: string,
862
+ value: unknown,
863
+ loaderId: string,
864
+ ): void {
865
+ replayPush(
866
+ this,
867
+ { placeholderOf: loaderId },
868
+ handleName,
869
+ segmentId,
870
+ value,
871
+ );
872
+ },
873
+
710
874
  pushRestored(
711
875
  handleName: string,
712
876
  segmentId: string,
@@ -716,30 +880,52 @@ export function createHandleStore(): HandleStore {
716
880
  replayPush(this, { restoredOf: loaderId }, handleName, segmentId, value);
717
881
  },
718
882
 
719
- markLiveLane(loaderIds: ReadonlySet<string>): void {
720
- liveLane = loaderIds;
883
+ setRecordAuthority(of: (loaderId: string) => RecordAuthority): void {
884
+ authority = of;
721
885
  },
722
886
 
723
887
  settleLoaderRun(loaderId: string): void {
724
- const pending = pendingReplays.get(loaderId);
725
- if (!pending || completed || !isHole(loaderId)) return;
726
- pendingReplays.delete(loaderId);
727
- replacedReplays.add(loaderId);
728
- for (const values of pending) removeReplayedSlots(values, loaderId);
888
+ if (completed) return;
889
+ const hole = isHole(loaderId);
890
+ if (
891
+ !pendingPlaceholders.has(loaderId) &&
892
+ !(hole && pendingReplays.has(loaderId))
893
+ ) {
894
+ return;
895
+ }
896
+ dropCopies(loaderId, hole);
729
897
  version++;
730
898
  signalEmission();
731
899
  },
732
900
 
733
- redeliverReplays(loaderId: string, deliver: () => void): void {
734
- const pending = pendingReplays.get(loaderId);
735
- if (!pending || completed || !isHole(loaderId)) {
901
+ replacePlaceholders(
902
+ loaderIds: readonly string[],
903
+ deliver: () => void,
904
+ ): void {
905
+ // One pass per array over every loader's placeholders: an array two of
906
+ // them share gets one anchor, the first removed position.
907
+ let given: Set<string> | undefined;
908
+ let arrays: Set<unknown[]> | undefined;
909
+ if (!completed) {
910
+ for (const loaderId of loaderIds) {
911
+ const pending = pendingPlaceholders.get(loaderId);
912
+ if (!pending) continue;
913
+ pendingPlaceholders.delete(loaderId);
914
+ (given ??= new Set()).add(loaderId);
915
+ arrays ??= new Set();
916
+ for (const values of pending) arrays.add(values);
917
+ }
918
+ }
919
+ if (!given || !arrays) {
736
920
  deliver();
737
921
  return;
738
922
  }
739
- pendingReplays.delete(loaderId);
923
+ const owners = given;
924
+ const ofGiven = (tag: SlotTag): boolean =>
925
+ tag.placeholderOf !== undefined && owners.has(tag.placeholderOf);
740
926
  const anchors = new Map<unknown[], number>();
741
- for (const values of pending) {
742
- const first = removeReplayedSlots(values, loaderId);
927
+ for (const values of arrays) {
928
+ const first = removeSlots(values, ofGiven);
743
929
  if (first !== undefined) anchors.set(values, first);
744
930
  }
745
931
  const outer = redelivery;
@@ -758,6 +944,23 @@ export function createHandleStore(): HandleStore {
758
944
  return this.settled.then(() => cloneHandleData(data));
759
945
  },
760
946
 
947
+ freezeDocumentSnapshot(): void {
948
+ if (documentSnapshot) return;
949
+ if (process.env.NODE_ENV !== "production" && documentStreamRead) {
950
+ const error = new Error(
951
+ "HandleStore.freezeDocumentSnapshot() ran after the document's " +
952
+ 'handle stream (stream("settled")) was first read. That stream ' +
953
+ "keeps reading the live store, so the document hydrates with " +
954
+ "whatever a loader pushed by then instead of the frozen state " +
955
+ "(issue #1035). Freeze before the payload that carries the " +
956
+ "stream starts rendering.",
957
+ );
958
+ if (this.onError) this.onError(error);
959
+ else console.error(error);
960
+ }
961
+ documentSnapshot = { version, data: cloneHandleData(data) };
962
+ },
963
+
761
964
  async *stream(
762
965
  until: "settled" | "fullySettled" = "fullySettled",
763
966
  ): AsyncGenerator<HandleData, void, unknown> {
@@ -768,6 +971,22 @@ export function createHandleStore(): HandleStore {
768
971
  // then, so the completed flag must be armed here.
769
972
  notifyDrain();
770
973
 
974
+ if (until === "settled") {
975
+ // A frozen document lane cannot change, so it is served at once:
976
+ // SSR and the pre-hydration drain block on this stream, and the
977
+ // batching hop below would cost every shell HIT a timer tick. It is
978
+ // yielded as the frozen object itself, already a copy: the lane's
979
+ // one consumer (resolvedHandleStream) builds a new object from a
980
+ // yield and never writes to it.
981
+ if (documentSnapshot) {
982
+ if (Object.keys(documentSnapshot.data).length > 0) {
983
+ yield documentSnapshot.data;
984
+ }
985
+ return;
986
+ }
987
+ documentStreamRead = true;
988
+ }
989
+
771
990
  // Per-consumer termination flag driven by the chosen barrier. The
772
991
  // global `completed` (late-push guard) always keys on FULL drain via
773
992
  // notifyDrain — a "settled"-scoped consumer finishing does not make
@@ -806,8 +1025,9 @@ export function createHandleStore(): HandleStore {
806
1025
  // state. This channel exists only for pushes that land AFTER `settled`
807
1026
  // — streaming loader bodies writing meta/breadcrumbs mid-body.
808
1027
  await this.settled;
809
- let seen = version;
810
- if (auxInflightCount === 0) {
1028
+ // A frozen document snapshot carried nothing newer than its version.
1029
+ let seen = documentSnapshot?.version ?? version;
1030
+ if (auxInflightCount === 0 && version === seen) {
811
1031
  // Auxiliary lane already drained: every loader push (if any) beat the
812
1032
  // handler barrier and rode the normal snapshot. Nothing late follows —
813
1033
  // handler pushes after settled are illegal by contract.
@@ -870,6 +1090,16 @@ export function createHandleStore(): HandleStore {
870
1090
  return result;
871
1091
  },
872
1092
 
1093
+ getOwnedSegmentIds(): string[] {
1094
+ const ids = new Set<string>();
1095
+ for (const handleName in data) {
1096
+ for (const id in data[handleName]) {
1097
+ if (!ids.has(id) && this.getRecordOwners(id)) ids.add(id);
1098
+ }
1099
+ }
1100
+ return [...ids];
1101
+ },
1102
+
873
1103
  replaySegmentData(
874
1104
  segmentId: string,
875
1105
  segmentHandles: Record<string, unknown[]>,