@rangojs/router 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -1,4 +1,5 @@
1
- import { getCurrentLoaderBodyId, isInsideLoaderScope } from "./context.js";
1
+ import { findEnclosingLoaderBody, isInsideLoaderScope } from "./context.js";
2
+ import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
2
3
 
3
4
  /**
4
5
  * Handle data structure: handleName -> segmentId -> entries[]
@@ -60,6 +61,10 @@ interface SlotTag {
60
61
  liveOf?: string;
61
62
  /** Pushed by this loader's body and kept in records (push `owner`). */
62
63
  owner?: string;
64
+ /** A document HIT's restore of this loader's record copy (pushRestored). */
65
+ restoredOf?: string;
66
+ /** A restored slot a dropped settled push of its loader stood in for. */
67
+ matched?: true;
63
68
  }
64
69
 
65
70
  // Shared by every plain DSL-loader push; tags are never mutated in place.
@@ -157,7 +162,11 @@ export interface HandleStore {
157
162
  * body (loader-cache.ts replayLoaderHandles). If that loader then runs live
158
163
  * in this request, its first push removes every value replayed for it and
159
164
  * takes the first one's position (later live pushes follow it), so the
160
- * handle output stays live without a duplicate.
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).
161
170
  */
162
171
  pushReplayed(
163
172
  handleName: string,
@@ -166,6 +175,62 @@ export interface HandleStore {
166
175
  loaderId: string,
167
176
  ): void;
168
177
 
178
+ /**
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.
189
+ */
190
+ pushRestored(
191
+ handleName: string,
192
+ segmentId: string,
193
+ data: unknown,
194
+ loaderId: string,
195
+ ): void;
196
+
197
+ /**
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.
207
+ */
208
+ markLiveLane(loaderIds: ReadonlySet<string>): void;
209
+
210
+ /**
211
+ * `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.
219
+ */
220
+ settleLoaderRun(loaderId: string): void;
221
+
222
+ /**
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.
231
+ */
232
+ redeliverReplays(loaderId: string, deliver: () => void): void;
233
+
169
234
  /**
170
235
  * Get all collected handle data after all handlers have settled.
171
236
  * Waits for `settled` (handler lane), then returns the data at that point.
@@ -257,6 +322,42 @@ export function createHandleStore(): HandleStore {
257
322
  const pendingReplays = new Map<string, Set<unknown[]>>();
258
323
  // Replayed loaders whose live run already replaced those slots.
259
324
  const replacedReplays = new Set<string>();
325
+ // Loaders a document HIT restored record copies for (pushRestored).
326
+ const restoredLoaders = new Set<string>();
327
+ 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;
332
+ const isHole = (loaderId: string): boolean =>
333
+ liveLane?.has(loaderId) ?? false;
334
+
335
+ const hasReplays = (loaderId: string): boolean =>
336
+ pendingReplays.has(loaderId) || replacedReplays.has(loaderId);
337
+
338
+ // The restored loader a push counts for, as the capture credits it
339
+ // (shell-capture.ts deriveShellCaptureContext): the replayed loader when it
340
+ // restored one, else the innermost enclosing loader body that did. A
341
+ // "use cache" hit inside a restored body replays a dependency's push the
342
+ // capture recorded under that body. A hole's body, or a replay of a hole's
343
+ // push, ends the search: a live loader a restored loader awaits is still
344
+ // live.
345
+ function restoringLoader(replayOf: string | undefined): string | undefined {
346
+ if (replayOf !== undefined) {
347
+ if (restoredLoaders.has(replayOf)) return replayOf;
348
+ // Defensive: a hole's own cache() replay runs outside any loader body,
349
+ // so the walk below finds no restored loader either.
350
+ if (isHole(replayOf)) return undefined;
351
+ }
352
+ return findEnclosingLoaderBody(isRestored, isHole);
353
+ }
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
359
+ // pushReplayed value goes (the removed values' place).
360
+ let redelivery: Map<unknown[], number> | undefined;
260
361
 
261
362
  // Aligned to `values` so a mid-array insert or removal keeps positions.
262
363
  function tagsFor(values: unknown[]): (SlotTag | undefined)[] {
@@ -331,6 +432,59 @@ export function createHandleStore(): HandleStore {
331
432
  return undefined;
332
433
  }
333
434
 
435
+ // A restored loader's settled push: the first of its restored slots here
436
+ // not yet matched stands in for it. Returns nothing; the push is dropped.
437
+ function matchRestoredSlot(values: unknown[], loaderId: string): void {
438
+ const tags = slotTags.get(values);
439
+ if (!tags) return;
440
+ for (let i = 0; i < values.length; i++) {
441
+ const tag = tags[i];
442
+ if (tag?.restoredOf === loaderId && !tag.matched) {
443
+ tags[i] = { ...tag, matched: true };
444
+ return;
445
+ }
446
+ }
447
+ }
448
+
449
+ // Where a kept push by a restored loader lands: before its first unmatched
450
+ // restored slot here (it came before that push), else after its last slot
451
+ // here, else appended.
452
+ function restoredInsertIndex(
453
+ values: unknown[],
454
+ loaderId: string,
455
+ ): number | undefined {
456
+ const tags = slotTags.get(values);
457
+ if (!tags) return undefined;
458
+ let after: number | undefined;
459
+ for (let i = 0; i < values.length; i++) {
460
+ const tag = tags[i];
461
+ if (tag?.restoredOf === loaderId && !tag.matched) return i;
462
+ if (
463
+ tag?.restoredOf === loaderId ||
464
+ tag?.liveOf === loaderId ||
465
+ tag?.replayOf === loaderId
466
+ ) {
467
+ after = i + 1;
468
+ }
469
+ }
470
+ return after;
471
+ }
472
+
473
+ function replayPush(
474
+ store: HandleStore,
475
+ replay: NonNullable<typeof replaying>,
476
+ handleName: string,
477
+ segmentId: string,
478
+ value: unknown,
479
+ ): void {
480
+ replaying = replay;
481
+ try {
482
+ store.push(handleName, segmentId, value);
483
+ } finally {
484
+ replaying = undefined;
485
+ }
486
+ }
487
+
334
488
  // Settlement barriers: `settled` (handler lane) resolves when sealed AND
335
489
  // handler inflight === 0. `fullySettled` additionally waits for the
336
490
  // auxiliary (loader) lane. seal() signals "no more track() calls"; each
@@ -477,6 +631,28 @@ export function createHandleStore(): HandleStore {
477
631
  throw error;
478
632
  }
479
633
 
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
637
+ // 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).
642
+ let pusher: string | undefined;
643
+ let restoredBy: string | undefined;
644
+ if (!restoredOf) {
645
+ if (pendingReplays.size > 0 || replacedReplays.size > 0) {
646
+ pusher = findEnclosingLoaderBody(hasReplays, isHole);
647
+ }
648
+ if (restoredLoaders.size > 0) restoredBy = restoringLoader(replayOf);
649
+ }
650
+ if (restoredBy !== undefined && !holdsThenable(value)) {
651
+ const existing = data[handleName]?.[segmentId];
652
+ if (existing) matchRestoredSlot(existing, restoredBy);
653
+ return;
654
+ }
655
+
480
656
  if (!data[handleName]) {
481
657
  data[handleName] = {};
482
658
  }
@@ -487,52 +663,94 @@ export function createHandleStore(): HandleStore {
487
663
  const loader = (loaderPush ?? isInsideLoaderScope()) || undefined;
488
664
  let at: number | undefined;
489
665
  let liveOf: string | undefined;
490
- if (pendingReplays.size > 0 || replacedReplays.size > 0) {
491
- const bodyId = getCurrentLoaderBodyId();
492
- if (
493
- bodyId !== undefined &&
494
- (pendingReplays.has(bodyId) || replacedReplays.has(bodyId))
495
- ) {
496
- at = liveReplacementIndex(bodyId, values);
497
- liveOf = bodyId;
498
- }
666
+ if (restoredOf) {
667
+ restoredLoaders.add(restoredOf);
668
+ } else if (replayOf !== undefined && redelivery?.has(values)) {
669
+ at = redelivery.get(values)!;
670
+ redelivery.set(values, at + 1);
671
+ } else if (pusher !== undefined) {
672
+ at = liveReplacementIndex(pusher, values);
673
+ liveOf = pusher;
674
+ } else if (restoredBy !== undefined) {
675
+ at = restoredInsertIndex(values, restoredBy);
676
+ if (!replayOf) liveOf = restoredBy;
499
677
  }
500
678
  insertSlot(
501
679
  values,
502
680
  at,
503
681
  value,
504
- liveOf || owner
505
- ? { loader, liveOf, owner }
682
+ replayOf || liveOf || owner || restoredOf
683
+ ? { loader, replayOf, liveOf, owner, restoredOf }
506
684
  : loader
507
685
  ? LOADER_SLOT
508
686
  : undefined,
509
687
  );
688
+ if (replayOf) {
689
+ let arrays = pendingReplays.get(replayOf);
690
+ if (!arrays) pendingReplays.set(replayOf, (arrays = new Set()));
691
+ arrays.add(values);
692
+ }
510
693
 
511
694
  // Bump the version; each consumer's cursor decides when to clone+yield.
512
695
  version++;
513
696
  signalEmission();
514
697
  },
515
698
 
699
+ // Both go through this.push: active captures see a replay like a live
700
+ // push, and a diverting capture keeps it out of the store.
516
701
  pushReplayed(
517
702
  handleName: string,
518
703
  segmentId: string,
519
704
  value: unknown,
520
705
  loaderId: string,
521
706
  ): void {
522
- const before = data[handleName]?.[segmentId]?.length ?? 0;
523
- // Through this.push: active captures see the replay like a live push.
524
- this.push(handleName, segmentId, value);
525
- const values = data[handleName]?.[segmentId];
526
- // A diverting capture kept it out of the store.
527
- if (!values || values.length !== before + 1) return;
528
- const tags = tagsFor(values);
529
- tags[values.length - 1] = {
530
- ...tags[values.length - 1],
531
- replayOf: loaderId,
532
- };
533
- let arrays = pendingReplays.get(loaderId);
534
- if (!arrays) pendingReplays.set(loaderId, (arrays = new Set()));
535
- arrays.add(values);
707
+ replayPush(this, { replayOf: loaderId }, handleName, segmentId, value);
708
+ },
709
+
710
+ pushRestored(
711
+ handleName: string,
712
+ segmentId: string,
713
+ value: unknown,
714
+ loaderId: string,
715
+ ): void {
716
+ replayPush(this, { restoredOf: loaderId }, handleName, segmentId, value);
717
+ },
718
+
719
+ markLiveLane(loaderIds: ReadonlySet<string>): void {
720
+ liveLane = loaderIds;
721
+ },
722
+
723
+ 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);
729
+ version++;
730
+ signalEmission();
731
+ },
732
+
733
+ redeliverReplays(loaderId: string, deliver: () => void): void {
734
+ const pending = pendingReplays.get(loaderId);
735
+ if (!pending || completed || !isHole(loaderId)) {
736
+ deliver();
737
+ return;
738
+ }
739
+ pendingReplays.delete(loaderId);
740
+ const anchors = new Map<unknown[], number>();
741
+ for (const values of pending) {
742
+ const first = removeReplayedSlots(values, loaderId);
743
+ if (first !== undefined) anchors.set(values, first);
744
+ }
745
+ const outer = redelivery;
746
+ redelivery = anchors;
747
+ try {
748
+ deliver();
749
+ } finally {
750
+ redelivery = outer;
751
+ }
752
+ version++;
753
+ signalEmission();
536
754
  },
537
755
 
538
756
  getData(): Promise<HandleData> {