@rangojs/router 0.22.1 → 0.24.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 (259) hide show
  1. package/dist/bin/rango.js +10 -4
  2. package/dist/testing/vitest.js +31 -1
  3. package/dist/types/browser/defer.d.ts +48 -0
  4. package/dist/types/browser/document-revealed.d.ts +17 -0
  5. package/dist/types/browser/event-controller.d.ts +7 -1
  6. package/dist/types/browser/merge-segment-loaders.d.ts +5 -2
  7. package/dist/types/browser/navigation-transaction.d.ts +21 -7
  8. package/dist/types/browser/partial-update.d.ts +39 -8
  9. package/dist/types/browser/react/NavigationProvider.d.ts +19 -5
  10. package/dist/types/browser/react/context.d.ts +3 -2
  11. package/dist/types/browser/react/filter-segment-order.d.ts +7 -0
  12. package/dist/types/browser/response-adapter.d.ts +7 -0
  13. package/dist/types/browser/types.d.ts +24 -10
  14. package/dist/types/browser/validate-redirect-origin.d.ts +11 -0
  15. package/dist/types/build/generate-route-types.d.ts +1 -1
  16. package/dist/types/build/route-types/router-processing.d.ts +1 -1
  17. package/dist/types/build/route-types/scan-filter.d.ts +22 -2
  18. package/dist/types/cache/cf/cf-cache-store.d.ts +16 -1
  19. package/dist/types/cache/cf/cf-cache-types.d.ts +23 -2
  20. package/dist/types/cache/document-cache-policy.d.ts +24 -0
  21. package/dist/types/cache/document-cache.d.ts +2 -10
  22. package/dist/types/client-urls/client-urls.d.ts +1 -1
  23. package/dist/types/client-urls/hoisted-revalidate.d.ts +13 -0
  24. package/dist/types/client-urls/navigation.d.ts +0 -37
  25. package/dist/types/client-urls/revalidate-registry.d.ts +4 -0
  26. package/dist/types/client-urls/server-projection.d.ts +29 -1
  27. package/dist/types/client-urls/types.d.ts +34 -69
  28. package/dist/types/client.d.ts +0 -1
  29. package/dist/types/context-var.d.ts +12 -0
  30. package/dist/types/errors.d.ts +15 -0
  31. package/dist/types/handles/deferred-resolution.d.ts +2 -1
  32. package/dist/types/handles/pushed-by.d.ts +31 -0
  33. package/dist/types/index.d.ts +4 -0
  34. package/dist/types/index.rsc.d.ts +2 -0
  35. package/dist/types/loader-store.d.ts +8 -0
  36. package/dist/types/prefetch-markers.d.ts +11 -0
  37. package/dist/types/response-utils.d.ts +7 -0
  38. package/dist/types/reverse.d.ts +8 -8
  39. package/dist/types/route-definition/helpers-types.d.ts +27 -0
  40. package/dist/types/route-definition/rewrite.d.ts +56 -0
  41. package/dist/types/route-map-builder.d.ts +7 -2
  42. package/dist/types/router/basename.d.ts +7 -0
  43. package/dist/types/router/handler-context.d.ts +1 -5
  44. package/dist/types/router/internal-params.d.ts +8 -0
  45. package/dist/types/router/is-action.d.ts +2 -1
  46. package/dist/types/router/middleware-types.d.ts +15 -0
  47. package/dist/types/router/revalidation.d.ts +1 -2
  48. package/dist/types/router/route-trie-builder.d.ts +10 -0
  49. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -2
  50. package/dist/types/router/segment-resolution/mask-nested.d.ts +4 -2
  51. package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +264 -0
  52. package/dist/types/rsc/full-payload.d.ts +11 -0
  53. package/dist/types/rsc/handler.d.ts +33 -0
  54. package/dist/types/rsc/helpers.d.ts +8 -0
  55. package/dist/types/rsc/loader-fetch.d.ts +16 -0
  56. package/dist/types/rsc/rewrite-passes.d.ts +74 -0
  57. package/dist/types/rsc/ssr-setup.d.ts +3 -1
  58. package/dist/types/server/context.d.ts +21 -1
  59. package/dist/types/server/handle-store.d.ts +9 -1
  60. package/dist/types/server/request-context.d.ts +52 -2
  61. package/dist/types/server/rewrite-trail.d.ts +49 -0
  62. package/dist/types/server/slot-accessors.d.ts +29 -0
  63. package/dist/types/server.d.ts +1 -0
  64. package/dist/types/testing/flight.entry.d.ts +1 -1
  65. package/dist/types/testing/index.d.ts +4 -4
  66. package/dist/types/testing/internal/action-id.d.ts +10 -1
  67. package/dist/types/testing/internal/client-urls-projection.d.ts +16 -0
  68. package/dist/types/testing/internal/test-action.d.ts +8 -0
  69. package/dist/types/testing/render-route.d.ts +8 -0
  70. package/dist/types/testing/run-loader.d.ts +2 -1
  71. package/dist/types/testing/run-middleware.d.ts +20 -0
  72. package/dist/types/testing/run-revalidate.d.ts +74 -0
  73. package/dist/types/testing/run-transition-when.d.ts +2 -3
  74. package/dist/types/testing/serve-shell-request.d.ts +40 -1
  75. package/dist/types/testing/vitest.d.ts +11 -0
  76. package/dist/types/thenable-status.d.ts +6 -0
  77. package/dist/types/types/boundaries.d.ts +9 -0
  78. package/dist/types/types/handler-context.d.ts +14 -0
  79. package/dist/types/types/loader-types.d.ts +30 -0
  80. package/dist/types/types/request-scope.d.ts +7 -0
  81. package/dist/types/types/segments.d.ts +21 -4
  82. package/dist/types/urls/path-helper-types.d.ts +25 -4
  83. package/dist/types/urls/type-extraction.d.ts +6 -2
  84. package/dist/types/vite/discovery/discover-routers.d.ts +9 -0
  85. package/dist/types/vite/discovery/state.d.ts +14 -4
  86. package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +54 -0
  87. package/dist/types/vite/plugins/hoist/literal.d.ts +83 -0
  88. package/dist/types/vite/plugins/hoist/scope.d.ts +40 -0
  89. package/dist/types/vite/plugins/hoist/source.d.ts +41 -0
  90. package/dist/types/vite/plugins/transition-when-hoist.d.ts +8 -11
  91. package/dist/types/vite/router-discovery.d.ts +10 -0
  92. package/dist/vite/index.js +974 -379
  93. package/package.json +1 -1
  94. package/skills/caching/SKILL.md +54 -5
  95. package/skills/catalog.json +3 -3
  96. package/skills/client-urls/SKILL.md +197 -24
  97. package/skills/cloudflare/SKILL.md +14 -9
  98. package/skills/hooks/SKILL.md +16 -3
  99. package/skills/hooks/data.md +3 -1
  100. package/skills/links/SKILL.md +24 -0
  101. package/skills/loader/SKILL.md +199 -2
  102. package/skills/middleware/SKILL.md +183 -2
  103. package/skills/migrate-nextjs/SKILL.md +2 -2
  104. package/skills/migrate-react-router/SKILL.md +10 -7
  105. package/skills/migrate-react-router/data-and-actions.md +1 -1
  106. package/skills/ppr/SKILL.md +22 -0
  107. package/skills/prerender/SKILL.md +22 -8
  108. package/skills/rango/SKILL.md +13 -7
  109. package/skills/route/SKILL.md +139 -0
  110. package/skills/server-actions/SKILL.md +27 -0
  111. package/skills/testing/SKILL.md +3 -2
  112. package/skills/testing/client-components.md +2 -1
  113. package/skills/testing/middleware.md +31 -10
  114. package/skills/testing/predicates.md +67 -32
  115. package/skills/testing/response-routes.md +1 -0
  116. package/skills/testing/serve-shell-request.md +107 -19
  117. package/skills/testing/setup.md +2 -2
  118. package/skills/typesafety/route-types.md +2 -2
  119. package/skills/use-cache/SKILL.md +4 -2
  120. package/skills/view-transitions/SKILL.md +26 -0
  121. package/src/__augment-tests__/augmented.check.ts +20 -0
  122. package/src/browser/defer.ts +82 -0
  123. package/src/browser/document-revealed.ts +52 -0
  124. package/src/browser/event-controller.ts +46 -32
  125. package/src/browser/merge-segment-loaders.ts +31 -18
  126. package/src/browser/navigation-bridge.ts +82 -16
  127. package/src/browser/navigation-client.ts +48 -36
  128. package/src/browser/navigation-store.ts +13 -0
  129. package/src/browser/navigation-transaction.ts +25 -8
  130. package/src/browser/partial-update.ts +554 -146
  131. package/src/browser/prefetch/fetch.ts +2 -1
  132. package/src/browser/react/Link.tsx +7 -9
  133. package/src/browser/react/NavigationProvider.tsx +88 -49
  134. package/src/browser/react/context.ts +3 -2
  135. package/src/browser/react/filter-segment-order.ts +13 -2
  136. package/src/browser/react/use-router.ts +2 -4
  137. package/src/browser/response-adapter.ts +20 -0
  138. package/src/browser/rsc-router.tsx +17 -45
  139. package/src/browser/server-action-bridge.ts +25 -63
  140. package/src/browser/types.ts +24 -10
  141. package/src/browser/validate-redirect-origin.ts +16 -0
  142. package/src/build/generate-route-types.ts +1 -0
  143. package/src/build/route-types/router-processing.ts +3 -6
  144. package/src/build/route-types/scan-filter.ts +45 -11
  145. package/src/cache/cf/cf-cache-store.ts +59 -21
  146. package/src/cache/cf/cf-cache-types.ts +24 -2
  147. package/src/cache/document-cache-policy.ts +131 -0
  148. package/src/cache/document-cache.ts +21 -107
  149. package/src/client-urls/client-urls.ts +22 -6
  150. package/src/client-urls/hoisted-revalidate.ts +25 -0
  151. package/src/client-urls/navigation.ts +0 -96
  152. package/src/client-urls/revalidate-registry.ts +30 -0
  153. package/src/client-urls/server-projection.ts +151 -65
  154. package/src/client-urls/types.ts +39 -71
  155. package/src/client.tsx +9 -5
  156. package/src/context-var.ts +32 -2
  157. package/src/errors.ts +34 -0
  158. package/src/handles/deferred-resolution.ts +28 -5
  159. package/src/handles/pushed-by.ts +148 -0
  160. package/src/index.rsc.ts +8 -0
  161. package/src/index.ts +7 -0
  162. package/src/loader-store.ts +18 -7
  163. package/src/prefetch-markers.ts +13 -0
  164. package/src/response-utils.ts +19 -0
  165. package/src/reverse.ts +17 -14
  166. package/src/route-content-wrapper.tsx +18 -4
  167. package/src/route-definition/dsl-helpers.ts +27 -4
  168. package/src/route-definition/helpers-types.ts +27 -1
  169. package/src/route-definition/redirect.ts +2 -15
  170. package/src/route-definition/rewrite.ts +146 -0
  171. package/src/route-map-builder.ts +14 -11
  172. package/src/router/basename.ts +20 -0
  173. package/src/router/handler-context.ts +3 -13
  174. package/src/router/intercept-resolution.ts +35 -16
  175. package/src/router/internal-params.ts +16 -0
  176. package/src/router/is-action.ts +2 -1
  177. package/src/router/loader-resolution.ts +85 -4
  178. package/src/router/match-api.ts +36 -1
  179. package/src/router/match-middleware/background-revalidation.ts +16 -4
  180. package/src/router/match-middleware/cache-lookup.ts +35 -7
  181. package/src/router/match-middleware/cache-store.ts +15 -1
  182. package/src/router/match-middleware/intercept-resolution.ts +4 -2
  183. package/src/router/match-result.ts +14 -2
  184. package/src/router/middleware-types.ts +16 -0
  185. package/src/router/middleware.ts +127 -49
  186. package/src/router/navigation-snapshot.ts +2 -1
  187. package/src/router/revalidation.ts +1 -1
  188. package/src/router/route-trie-builder.ts +28 -1
  189. package/src/router/segment-resolution/fresh.ts +22 -18
  190. package/src/router/segment-resolution/loader-cache.ts +2 -1
  191. package/src/router/segment-resolution/loader-mask.ts +5 -3
  192. package/src/router/segment-resolution/loader-snapshot.ts +18 -4
  193. package/src/router/segment-resolution/mask-nested.ts +8 -4
  194. package/src/router/segment-resolution/prefetch-deferral.ts +583 -0
  195. package/src/router/segment-resolution/revalidation.ts +215 -82
  196. package/src/rsc/full-payload.ts +37 -5
  197. package/src/rsc/handler.ts +82 -55
  198. package/src/rsc/helpers.ts +24 -0
  199. package/src/rsc/loader-fetch.ts +79 -24
  200. package/src/rsc/response-cache-serve.ts +18 -2
  201. package/src/rsc/response-route-handler.ts +4 -7
  202. package/src/rsc/rewrite-passes.ts +203 -0
  203. package/src/rsc/rsc-rendering.ts +54 -9
  204. package/src/rsc/shell-capture.ts +2 -1
  205. package/src/rsc/ssr-setup.ts +11 -5
  206. package/src/segment-loader-promise.ts +23 -20
  207. package/src/segment-system.tsx +60 -26
  208. package/src/server/context.ts +39 -5
  209. package/src/server/cookie-parse.ts +36 -12
  210. package/src/server/cookie-store.ts +7 -3
  211. package/src/server/handle-store.ts +152 -25
  212. package/src/server/request-context.ts +175 -64
  213. package/src/server/request-headers.ts +4 -14
  214. package/src/server/rewrite-trail.ts +190 -0
  215. package/src/server/slot-accessors.ts +57 -0
  216. package/src/server.ts +4 -0
  217. package/src/ssr/index.tsx +118 -16
  218. package/src/testing/action-form.ts +2 -11
  219. package/src/testing/dispatch.ts +138 -67
  220. package/src/testing/e2e/page-helpers.ts +3 -1
  221. package/src/testing/flight.entry.ts +1 -0
  222. package/src/testing/index.ts +7 -3
  223. package/src/testing/internal/action-id.ts +16 -1
  224. package/src/testing/internal/client-urls-projection.ts +70 -0
  225. package/src/testing/internal/test-action.ts +25 -0
  226. package/src/testing/render-route.tsx +16 -3
  227. package/src/testing/run-loader.ts +4 -1
  228. package/src/testing/run-middleware.ts +56 -11
  229. package/src/testing/run-revalidate.ts +142 -0
  230. package/src/testing/run-transition-when.ts +7 -8
  231. package/src/testing/serve-shell-request.ts +131 -8
  232. package/src/testing/vitest.ts +67 -1
  233. package/src/thenable-status.ts +11 -0
  234. package/src/types/boundaries.ts +12 -0
  235. package/src/types/handler-context.ts +14 -0
  236. package/src/types/loader-types.ts +30 -0
  237. package/src/types/request-scope.ts +8 -0
  238. package/src/types/segments.ts +26 -4
  239. package/src/urls/path-helper-types.ts +27 -5
  240. package/src/urls/type-extraction.ts +30 -10
  241. package/src/use-loader.tsx +18 -13
  242. package/src/vite/discovery/client-urls-projection.ts +20 -21
  243. package/src/vite/discovery/discover-routers.ts +94 -26
  244. package/src/vite/discovery/state.ts +28 -4
  245. package/src/vite/discovery/virtual-module-codegen.ts +22 -17
  246. package/src/vite/plugins/client-urls-revalidate-hoist.ts +564 -0
  247. package/src/vite/plugins/expose-internal-ids.ts +2 -3
  248. package/src/vite/plugins/hoist/literal.ts +417 -0
  249. package/src/vite/plugins/hoist/scope.ts +370 -0
  250. package/src/vite/plugins/hoist/source.ts +84 -0
  251. package/src/vite/plugins/transition-when-hoist.ts +113 -779
  252. package/src/vite/rango.ts +2 -0
  253. package/src/vite/router-discovery.ts +116 -53
  254. package/dist/types/client-urls/revalidate-chain.d.ts +0 -33
  255. package/dist/types/client-urls/revalidation-protocol.d.ts +0 -25
  256. package/dist/types/testing/run-client-revalidate.d.ts +0 -43
  257. package/src/client-urls/revalidate-chain.ts +0 -83
  258. package/src/client-urls/revalidation-protocol.ts +0 -56
  259. package/src/testing/run-client-revalidate.ts +0 -92
@@ -117,6 +117,13 @@ export type EntryPropDatas = {
117
117
  */
118
118
  export type EntryPropRender = {
119
119
  loading?: ReactNode | false;
120
+ /**
121
+ * loading(fallback, { prefetch: false }) with a renderable fallback: a
122
+ * prefetch defers what the fallback covers while the entry's segment is
123
+ * new to the client (router/segment-resolution/prefetch-deferral.ts). Set
124
+ * on both DSL evaluations, whatever `ssr` did to `loading`.
125
+ */
126
+ loadingPrefetch?: false;
120
127
  transition?: TransitionConfig;
121
128
  };
122
129
 
@@ -150,6 +157,12 @@ export type LoaderEntry = {
150
157
  * navigation evaluation never has it.
151
158
  */
152
159
  bake?: true;
160
+ /**
161
+ * loader(Def, { prefetch: false }): a prefetch defers this registration
162
+ * while its segment is new to the client
163
+ * (router/segment-resolution/prefetch-deferral.ts).
164
+ */
165
+ prefetch?: false;
153
166
  };
154
167
 
155
168
  /**
@@ -867,9 +880,14 @@ export function track(label: string, depth?: number): () => void {
867
880
  * nested RangoContext.run() calls in Vite's module runner.
868
881
  */
869
882
  const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
870
- const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
871
- globalThis as any
872
- )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
883
+ interface LoaderScope {
884
+ active: true;
885
+ /** $$id of the DSL loader this scope resolves (runInsideLoaderScope). */
886
+ loaderId?: string;
887
+ }
888
+ const loaderScopeALS: AsyncLocalStorage<LoaderScope> = ((globalThis as any)[
889
+ LOADER_SCOPE_KEY
890
+ ] ??= new AsyncLocalStorage<LoaderScope>());
873
891
 
874
892
  // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
875
893
  // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
@@ -1429,8 +1447,24 @@ export function assertCachedHeaderWriteAllowed(
1429
1447
  * are bypassed because loaders are always fresh (never cached) and
1430
1448
  * their side effects (setCookie, header, etc.) are safe.
1431
1449
  */
1432
- export function runInsideLoaderScope<T>(fn: () => T): T {
1433
- return loaderScopeALS.run({ active: true }, fn);
1450
+ export function runInsideLoaderScope<T>(fn: () => T, loaderId?: string): T {
1451
+ return loaderScopeALS.run({ active: true, loaderId }, fn);
1452
+ }
1453
+
1454
+ /**
1455
+ * The loader whose run a handle push here belongs to, for the browser's
1456
+ * merge of a partial response (handles/pushed-by.ts): the DSL loader
1457
+ * this scope resolves, else the outermost loader body around the push (a
1458
+ * loader a handler reads with ctx.use). Undefined for a handler's own push.
1459
+ */
1460
+ export function getHandlePushOwner(): string | undefined {
1461
+ const dsl = loaderScopeALS.getStore()?.loaderId;
1462
+ if (dsl !== undefined) return dsl;
1463
+ let outermost: string | undefined;
1464
+ for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
1465
+ if (s.loaderId !== undefined) outermost = s.loaderId;
1466
+ }
1467
+ return outermost;
1434
1468
  }
1435
1469
 
1436
1470
  /**
@@ -10,23 +10,47 @@
10
10
  export function parseCookiesFromHeader(
11
11
  cookieHeader: string | null,
12
12
  ): Record<string, string> {
13
- if (!cookieHeader) return {};
14
-
15
13
  const cookies: Record<string, string> = {};
16
- const pairs = cookieHeader.split(";");
17
14
 
18
- for (const pair of pairs) {
19
- const [name, ...rest] = pair.trim().split("=");
20
- if (name) {
21
- const raw = rest.join("=");
22
- try {
23
- cookies[name] = decodeURIComponent(raw);
24
- } catch {
25
- // Malformed percent-encoding: fall back to raw value
26
- cookies[name] = raw;
15
+ if (cookieHeader) {
16
+ for (const pair of cookieHeader.split(";")) {
17
+ const [name, ...rest] = pair.trim().split("=");
18
+ if (name) {
19
+ const raw = rest.join("=");
20
+ let value: string;
21
+ try {
22
+ value = decodeURIComponent(raw);
23
+ } catch {
24
+ // Malformed percent-encoding: fall back to raw value
25
+ value = raw;
26
+ }
27
+ setCookie(cookies, name, value);
27
28
  }
28
29
  }
29
30
  }
30
31
 
32
+ // Null prototype (#1109): cookie names come from the request, so a lookup
33
+ // must never answer `constructor` or `toString` with an inherited member.
34
+ // Set once at the end: Object.create(null) starts in dictionary mode, which
35
+ // makes a spread of the jar several times slower.
36
+ Object.setPrototypeOf(cookies, null);
31
37
  return cookies;
32
38
  }
39
+
40
+ /** `cookies[name] = value`, a cookie named `__proto__` as an own key too. */
41
+ function setCookie(
42
+ cookies: Record<string, string>,
43
+ name: string,
44
+ value: string,
45
+ ): void {
46
+ if (name === "__proto__") {
47
+ Object.defineProperty(cookies, name, {
48
+ value,
49
+ writable: true,
50
+ enumerable: true,
51
+ configurable: true,
52
+ });
53
+ } else {
54
+ cookies[name] = value;
55
+ }
56
+ }
@@ -367,12 +367,16 @@ function createCookieStore(
367
367
 
368
368
  getAll(name?: string): Cookie[] {
369
369
  recordLoaderIdentityRead("cookies()");
370
- const all = ctx._readCookies();
371
370
  if (name !== undefined) {
372
- const value = all[name];
371
+ // The single-name read, not an index into the plain object
372
+ // _readCookies() returns: that would answer an inherited member.
373
+ const value = ctx._readCookie(name);
373
374
  return value !== undefined ? [{ name, value }] : [];
374
375
  }
375
- return Object.entries(all).map(([n, v]) => ({ name: n, value: v }));
376
+ return Object.entries(ctx._readCookies()).map(([n, v]) => ({
377
+ name: n,
378
+ value: v,
379
+ }));
376
380
  },
377
381
 
378
382
  has(name: string): boolean {
@@ -1,5 +1,11 @@
1
- import { findEnclosingLoaderBody, isInsideLoaderScope } from "./context.js";
1
+ import {
2
+ findEnclosingLoaderBody,
3
+ getHandlePushOwner,
4
+ isInsideLoaderScope,
5
+ } from "./context.js";
2
6
  import { holdsThenable } from "../router/segment-resolution/mask-nested.js";
7
+ import { pushedByKey } from "../handles/pushed-by.js";
8
+ import { defineSlot } from "./slot-accessors.js";
3
9
 
4
10
  /**
5
11
  * Handle data structure: handleName -> segmentId -> entries[]
@@ -75,6 +81,12 @@ interface SlotTag {
75
81
  restoredOf?: string;
76
82
  /** A restored slot a dropped settled push of its loader stood in for. */
77
83
  matched?: true;
84
+ /**
85
+ * The loader whose run made the push, for the browser (pushed-by.ts): the
86
+ * DSL loader or loader body it is made in (getHandlePushOwner), else the
87
+ * loader a copy stands for. Unset for a handler's push.
88
+ */
89
+ by?: string;
78
90
  }
79
91
 
80
92
  // Shared by every plain DSL-loader push; tags are never mutated in place.
@@ -111,6 +123,24 @@ function cloneHandleData(data: HandleData): HandleData {
111
123
  return clone;
112
124
  }
113
125
 
126
+ // SlotTag.by of each value, index-aligned; undefined when no value has one.
127
+ function pushedByOf(
128
+ values: unknown[],
129
+ tags: (SlotTag | undefined)[] | undefined,
130
+ keep?: (i: number) => boolean,
131
+ ): (string | null)[] | undefined {
132
+ if (!tags) return undefined;
133
+ const by: (string | null)[] = [];
134
+ let any = false;
135
+ for (let i = 0; i < values.length; i++) {
136
+ if (keep && !keep(i)) continue;
137
+ const run = tags[i]?.by ?? null;
138
+ if (run !== null) any = true;
139
+ by.push(run);
140
+ }
141
+ return any ? by : undefined;
142
+ }
143
+
114
144
  /**
115
145
  * HandleStore tracks pending handler promises and stores handle data.
116
146
  *
@@ -297,7 +327,8 @@ export interface HandleStore {
297
327
  * nav/action payloads, whose consumer applies each yield progressively) or
298
328
  * "settled" (the document lane, whose consumers drain to completion in
299
329
  * blocking positions and must not wait on loader bodies).
300
- * Each yield contains the full accumulated state (not just the delta).
330
+ * Each yield contains the full accumulated state (not just the delta),
331
+ * with the pushedBy records of its buckets (handles/pushed-by.ts).
301
332
  * Safe for concurrent consumers (per-consumer version cursor).
302
333
  * The "settled" lane of a frozen store (freezeDocumentSnapshot) yields the
303
334
  * frozen state once, at once, and ends. That yield is the store's own
@@ -331,6 +362,17 @@ export interface HandleStore {
331
362
  excludeLoaderPushes?: boolean,
332
363
  ): Record<string, unknown[]>;
333
364
 
365
+ /**
366
+ * The loader whose run pushed each value in getDataForSegment(segmentId,
367
+ * excludeLoaderPushes), index-aligned per handle (null: the handler's).
368
+ * Handles without such a value are omitted; undefined when none has one.
369
+ * The browser's pushedBy record (handles/pushed-by.ts).
370
+ */
371
+ getPushedBy(
372
+ segmentId: string,
373
+ excludeLoaderPushes?: boolean,
374
+ ): Record<string, (string | null)[]> | undefined;
375
+
334
376
  /**
335
377
  * The push `owner` of each value in getDataForSegment(segmentId, true),
336
378
  * index-aligned per handle (null: no owner). Handles without an owned
@@ -353,6 +395,34 @@ export interface HandleStore {
353
395
  ): void;
354
396
  }
355
397
 
398
+ /**
399
+ * settled and fullySettled: shared descriptors (slot-accessors.ts) that call
400
+ * the store's closure in SETTLEMENT_SLOT with the lane they wait for.
401
+ */
402
+ const SETTLEMENT_SLOT: unique symbol = Symbol("rango.handleStore.settlement");
403
+
404
+ type SettlementSlot = { [SETTLEMENT_SLOT]: (full: boolean) => Promise<void> };
405
+
406
+ const SETTLEMENT_ACCESSORS: Record<
407
+ "settled" | "fullySettled",
408
+ PropertyDescriptor
409
+ > = {
410
+ settled: {
411
+ get(this: SettlementSlot): Promise<void> {
412
+ return this[SETTLEMENT_SLOT](false);
413
+ },
414
+ enumerable: true,
415
+ configurable: true,
416
+ },
417
+ fullySettled: {
418
+ get(this: SettlementSlot): Promise<void> {
419
+ return this[SETTLEMENT_SLOT](true);
420
+ },
421
+ enumerable: true,
422
+ configurable: true,
423
+ },
424
+ };
425
+
356
426
  /**
357
427
  * Create a new HandleStore instance.
358
428
  *
@@ -379,6 +449,14 @@ export function createHandleStore(): HandleStore {
379
449
  // (not value identity) so primitive values are covered too.
380
450
  // replaySegmentData installs fresh arrays, so restored values are untagged.
381
451
  const slotTags = new WeakMap<unknown[], (SlotTag | undefined)[]>();
452
+ // Frozen tags of plain pushes made by a run (SlotTag.by), one per loader.
453
+ const pushedBySlots = new Map<string, SlotTag>();
454
+ const pushedBySlot = (loader: true | undefined, by: string): SlotTag => {
455
+ const key = loader ? `L${by}` : `H${by}`;
456
+ let tag = pushedBySlots.get(key);
457
+ if (!tag) pushedBySlots.set(key, (tag = Object.freeze({ loader, by })));
458
+ return tag;
459
+ };
382
460
  // Copies standing in for a loader that has not pushed live yet: loaderId ->
383
461
  // the arrays holding them. Kept apart by kind: only a placeholder gives way
384
462
  // to a run that ends without a push, or to a cached unit's delivery.
@@ -602,6 +680,25 @@ export function createHandleStore(): HandleStore {
602
680
  }
603
681
  }
604
682
 
683
+ // What a stream yields: a copy of the data, with each bucket's pushedBy
684
+ // record (pushed-by.ts) for the browser's merge.
685
+ function snapshot(): HandleData {
686
+ const out: HandleData = {};
687
+ for (const handleName in data) {
688
+ const buckets: Record<string, unknown[]> = {};
689
+ let pushedBy: Record<string, (string | null)[]> | undefined;
690
+ for (const segmentId in data[handleName]) {
691
+ const values = data[handleName][segmentId];
692
+ buckets[segmentId] = [...values];
693
+ const by = pushedByOf(values, slotTags.get(values));
694
+ if (by) (pushedBy ??= {})[segmentId] = by;
695
+ }
696
+ out[handleName] = buckets;
697
+ if (pushedBy) out[pushedByKey(handleName)] = pushedBy;
698
+ }
699
+ return out;
700
+ }
701
+
605
702
  // Settlement barriers: `settled` (handler lane) resolves when sealed AND
606
703
  // handler inflight === 0. `fullySettled` additionally waits for the
607
704
  // auxiliary (loader) lane. seal() signals "no more track() calls"; each
@@ -706,7 +803,9 @@ export function createHandleStore(): HandleStore {
706
803
  });
707
804
  }
708
805
 
709
- return {
806
+ // Data members only: the accessors are SETTLEMENT_ACCESSORS.
807
+ const store: Omit<HandleStore, "settled" | "fullySettled"> &
808
+ ThisType<HandleStore> = {
710
809
  track<T>(promise: Promise<T>): Promise<T> {
711
810
  inflightCount++;
712
811
  return trackSettlement(promise, () => {
@@ -725,22 +824,6 @@ export function createHandleStore(): HandleStore {
725
824
  sealInternal();
726
825
  },
727
826
 
728
- get settled(): Promise<void> {
729
- if (sealed && inflightCount === 0) return Promise.resolve();
730
- return new Promise<void>((resolve) => {
731
- drainWaiters.push(resolve);
732
- });
733
- },
734
-
735
- get fullySettled(): Promise<void> {
736
- if (sealed && inflightCount === 0 && auxInflightCount === 0) {
737
- return Promise.resolve();
738
- }
739
- return new Promise<void>((resolve) => {
740
- fullDrainWaiters.push(resolve);
741
- });
742
- },
743
-
744
827
  push(
745
828
  handleName: string,
746
829
  segmentId: string,
@@ -812,6 +895,12 @@ export function createHandleStore(): HandleStore {
812
895
  at = restoredInsertIndex(values, restoredBy);
813
896
  if (replayOf === undefined) liveOf = restoredBy;
814
897
  }
898
+ const by =
899
+ getHandlePushOwner() ??
900
+ replayOf ??
901
+ restoredOf ??
902
+ placeholderOf ??
903
+ owner;
815
904
  insertSlot(
816
905
  values,
817
906
  at,
@@ -829,10 +918,13 @@ export function createHandleStore(): HandleStore {
829
918
  liveOf,
830
919
  under,
831
920
  owner,
921
+ by,
832
922
  }
833
- : loader
834
- ? LOADER_SLOT
835
- : undefined,
923
+ : by !== undefined
924
+ ? pushedBySlot(loader, by)
925
+ : loader
926
+ ? LOADER_SLOT
927
+ : undefined,
836
928
  );
837
929
  if (replayOf !== undefined) {
838
930
  addPending(pendingReplays, replayOf, values);
@@ -958,7 +1050,7 @@ export function createHandleStore(): HandleStore {
958
1050
  if (this.onError) this.onError(error);
959
1051
  else console.error(error);
960
1052
  }
961
- documentSnapshot = { version, data: cloneHandleData(data) };
1053
+ documentSnapshot = { version, data: snapshot() };
962
1054
  },
963
1055
 
964
1056
  async *stream(
@@ -1005,7 +1097,7 @@ export function createHandleStore(): HandleStore {
1005
1097
  while (true) {
1006
1098
  if (version > seen && Object.keys(data).length > 0) {
1007
1099
  seen = version;
1008
- yield cloneHandleData(data);
1100
+ yield snapshot();
1009
1101
  }
1010
1102
  if (done) break;
1011
1103
  await waitForEmission(seen, () => done);
@@ -1043,7 +1135,7 @@ export function createHandleStore(): HandleStore {
1043
1135
  while (true) {
1044
1136
  if (version > seen) {
1045
1137
  seen = version;
1046
- yield cloneHandleData(data);
1138
+ yield snapshot();
1047
1139
  }
1048
1140
  if (done) break;
1049
1141
  await waitForEmission(seen, () => done);
@@ -1069,6 +1161,25 @@ export function createHandleStore(): HandleStore {
1069
1161
  return result;
1070
1162
  },
1071
1163
 
1164
+ getPushedBy(
1165
+ segmentId: string,
1166
+ excludeLoaderPushes?: boolean,
1167
+ ): Record<string, (string | null)[]> | undefined {
1168
+ let result: Record<string, (string | null)[]> | undefined;
1169
+ for (const handleName in data) {
1170
+ const values = data[handleName][segmentId];
1171
+ if (!values) continue;
1172
+ const tags = slotTags.get(values);
1173
+ const by = pushedByOf(
1174
+ values,
1175
+ tags,
1176
+ excludeLoaderPushes ? (i) => !tags?.[i]?.loader : undefined,
1177
+ );
1178
+ if (by) (result ??= {})[handleName] = by;
1179
+ }
1180
+ return result;
1181
+ },
1182
+
1072
1183
  getRecordOwners(
1073
1184
  segmentId: string,
1074
1185
  ): Record<string, (string | null)[]> | undefined {
@@ -1117,4 +1228,20 @@ export function createHandleStore(): HandleStore {
1117
1228
  signalEmission();
1118
1229
  },
1119
1230
  };
1231
+ // settled: the handler lane drained; fullySettled: the auxiliary lane too.
1232
+ defineSlot(store, SETTLEMENT_SLOT, (full: boolean): Promise<void> => {
1233
+ if (sealed && inflightCount === 0 && (!full || auxInflightCount === 0)) {
1234
+ return Promise.resolve();
1235
+ }
1236
+ return new Promise<void>((resolve) => {
1237
+ (full ? fullDrainWaiters : drainWaiters).push(resolve);
1238
+ });
1239
+ });
1240
+ Object.defineProperty(store, "settled", SETTLEMENT_ACCESSORS.settled);
1241
+ Object.defineProperty(
1242
+ store,
1243
+ "fullySettled",
1244
+ SETTLEMENT_ACCESSORS.fullySettled,
1245
+ );
1246
+ return store as HandleStore;
1120
1247
  }