@rangojs/router 0.17.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 (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -4,37 +4,40 @@
4
4
  *
5
5
  * A bake-lane loader (lane rule: see resolveLoaderData, loader-cache.ts)
6
6
  * executes during shell capture; its settled container bakes into the prelude
7
- * while nested promises postpone as holes. To keep a HIT's fresh
8
- * payload byte-identical to that frozen prelude, the capture pins the container
9
- * in the shell snapshot:
7
+ * while nested promises postpone as holes. To keep a HIT's fresh payload in
8
+ * agreement with that frozen prelude, the capture pins the container in the
9
+ * shell snapshot. Everything outside a hole is pinned; the one exception is a
10
+ * server-component element holding a promise, which is a hole as a whole (see
11
+ * elideLoaderContainer):
10
12
  *
11
- * - elide: deep-walk the settled container; a PENDING nested promise is a
12
- * hole, replaced by {@link LOADER_HOLE_KEY} — and since
13
- * loader-cache masks every nested thenable at capture
14
- * ({@link maskNestedContainerThenables}), nested promises are
15
- * ALWAYS pending here for new captures. The SETTLED-marker branch
16
- * below is legacy: it fires only if a settled thenable reaches
17
- * elide anyway, and the overlay keeps decoding SETTLED markers
18
- * from pre-mask snapshots. The result is promise-free and
19
- * Flight-serializable.
13
+ * - elide: deep-walk the settled container; a nested promise is a hole,
14
+ * replaced by {@link LOADER_HOLE_KEY}. loader-cache masks every
15
+ * nested thenable at capture ({@link maskNestedContainerThenables},
16
+ * a never-settling promise), so the promise shape alone decides:
17
+ * a value that settled in time does not bake (#692). The result
18
+ * is promise-free and Flight-serializable.
20
19
  * - overlay: on a HIT the loader runs fresh (only the loader body can mint
21
20
  * the live nested promises), then the recorded container is laid
22
21
  * over it: recorded paths win (they are what the prelude froze),
23
22
  * hole-marker paths take the fresh run's value (the live hole),
24
- * SETTLED-marker paths become Promise.resolve(pinned) — consumers
25
- * wrote use(data.x) against a promise-shaped container, and
26
- * handing them the raw value throws React #438 on every HIT (the
27
- * storefront PDP regression) — and fresh-only paths pass through
28
- * (they cannot contradict prelude bytes that never rendered them).
23
+ * and fresh-only paths pass through (they cannot contradict
24
+ * prelude bytes that never rendered them).
29
25
  */
30
26
 
27
+ import { isValidElement, type ReactElement } from "react";
31
28
  import { isThenable } from "../../handles/is-thenable.js";
29
+ import {
30
+ cloneElementWithProps,
31
+ isPlainDataObject,
32
+ maskNestedContainerThenables,
33
+ type MaskReport,
34
+ } from "./mask-nested.js";
32
35
 
33
36
  // Capture-side nested-thenable masking lives in the LEAF module mask-nested.ts
34
37
  // (shared with the capture handle-store push wrap in rsc/shell-capture.ts).
35
38
  // Re-exported here so loader-cache and the unit tests keep one import site for
36
39
  // the snapshot family.
37
- export { maskNestedContainerThenables } from "./mask-nested.js";
40
+ export { maskNestedContainerThenables };
38
41
 
39
42
  /**
40
43
  * Marker object standing in for a pending nested promise in a recorded loader
@@ -55,43 +58,6 @@ export function isLoaderHoleMarker(value: unknown): value is LoaderHoleMarker {
55
58
  );
56
59
  }
57
60
 
58
- /**
59
- * Marker wrapping the inlined value of a NESTED promise that settled during
60
- * capture. The value baked (physics), but the container key was a promise —
61
- * the overlay must hand consumers a Promise.resolve(value), not the raw value,
62
- * or an unconditional use(data.x) throws React #438 on every HIT. The ROOT
63
- * container is never wrapped: loader-cache overlays against the awaited fresh
64
- * container value.
65
- */
66
- export const LOADER_SETTLED_KEY = "$rangoLoaderSettled" as const;
67
-
68
- export interface LoaderSettledMarker {
69
- [LOADER_SETTLED_KEY]: 1;
70
- value: unknown;
71
- /**
72
- * Present when the pinned subtree contains hole markers. Computed once at
73
- * capture (elide already visits every node) so the per-HIT overlay never
74
- * rescans the pinned structure to pick its rehydration path.
75
- */
76
- holes?: 1;
77
- }
78
-
79
- export function isLoaderSettledMarker(
80
- value: unknown,
81
- ): value is LoaderSettledMarker {
82
- return (
83
- typeof value === "object" &&
84
- value !== null &&
85
- (value as Record<string, unknown>)[LOADER_SETTLED_KEY] === 1
86
- );
87
- }
88
-
89
- function isPlainObject(value: unknown): value is Record<string, unknown> {
90
- if (typeof value !== "object" || value === null) return false;
91
- const proto = Object.getPrototypeOf(value);
92
- return proto === Object.prototype || proto === null;
93
- }
94
-
95
61
  /**
96
62
  * Probe whether a promise is already settled without waiting for it: races it
97
63
  * against an immediately-resolved sentinel across two microtask hops (then
@@ -125,14 +91,18 @@ export type ElideResult =
125
91
  | { state: "rejected" };
126
92
 
127
93
  /**
128
- * Deep-elide a settled bake-lane container for recording. Settled nested
129
- * promises pin their value behind a settled marker (they baked, but consumers
130
- * hold a promise-shaped key); pending ones become hole markers; a REJECTED
131
- * nested promise poisons the record (error UI must never bake into a shared
132
- * shell) — the caller refuses the capture. Only plain objects/arrays are
133
- * traversed; anything else (Date, Map, class instance) is a pinned leaf.
134
- * Cycles are cut as pinned references (best effort — Flight rejects true
135
- * cycles later regardless).
94
+ * Deep-elide a settled bake-lane container for recording. Nested promises
95
+ * become hole markers: the capture masked each of them (never settles), so
96
+ * it postponed as a hole. A REJECTED container poisons the record (error UI
97
+ * must never bake into a shared shell) — the caller refuses the capture.
98
+ * Plain data objects, arrays and the props of host, Suspense/Fragment and
99
+ * client-reference elements are
100
+ * traversed copy-on-write (an element through cloneElementWithProps); a shared
101
+ * subtree or a cycle resolves to the same elided result. An element Flight
102
+ * renders by calling user code (see rendersOnServer) is a leaf, or ONE hole
103
+ * marker when a thenable sits anywhere in its props: the pin encode calls that
104
+ * component, so a marker inside its props would reach it. Anything else
105
+ * (Date, Map, class instance, lazy node) is a pinned leaf.
136
106
  *
137
107
  * The ROOT container promise-chain unwraps with NO marker: loader-cache
138
108
  * overlays against the AWAITED fresh container, so the recorded root must be
@@ -140,7 +110,7 @@ export type ElideResult =
140
110
  */
141
111
  export async function elideLoaderContainer(
142
112
  value: unknown,
143
- seen: Set<object> = new Set(),
113
+ seen: Map<object, ElideResult> = new Map(),
144
114
  ): Promise<ElideResult> {
145
115
  if (isThenable(value)) {
146
116
  const probed = await probeSettled(value);
@@ -155,59 +125,151 @@ export async function elideLoaderContainer(
155
125
 
156
126
  async function elideNested(
157
127
  value: unknown,
158
- seen: Set<object>,
128
+ seen: Map<object, ElideResult>,
159
129
  ): Promise<ElideResult> {
160
130
  if (isThenable(value)) {
161
- const probed = await probeSettled(value);
162
- if (probed.state === "pending") {
163
- return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
164
- }
165
- if (probed.state === "rejected") return { state: "rejected" };
166
- const inner = await elideNested(probed.value, seen);
167
- if (inner.state === "rejected") return inner;
168
- const marker: LoaderSettledMarker = inner.hasHole
169
- ? { [LOADER_SETTLED_KEY]: 1, value: inner.value, holes: 1 }
170
- : { [LOADER_SETTLED_KEY]: 1, value: inner.value };
171
- return { state: "ok", value: marker, hasHole: inner.hasHole };
131
+ return { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true };
172
132
  }
133
+ if (typeof value !== "object" || value === null) {
134
+ return { state: "ok", value, hasHole: false };
135
+ }
136
+ const cached = seen.get(value);
137
+ if (cached) return cached;
173
138
 
174
- if (Array.isArray(value)) {
175
- if (seen.has(value)) return { state: "ok", value, hasHole: false };
176
- seen.add(value);
177
- const out: unknown[] = new Array(value.length);
178
- let hasHole = false;
179
- for (let i = 0; i < value.length; i++) {
180
- const r = await elideNested(value[i], seen);
139
+ if (isValidElement<Record<string, unknown>>(value)) {
140
+ if (rendersOnServer(value.type)) {
141
+ return elementHoldsThenable(value.props)
142
+ ? { state: "ok", value: { [LOADER_HOLE_KEY]: 1 }, hasHole: true }
143
+ : { state: "ok", value, hasHole: false };
144
+ }
145
+ // Cached before the walk: a cycle back here pins the original.
146
+ const result: ElideResult & { state: "ok" } = {
147
+ state: "ok",
148
+ value,
149
+ hasHole: false,
150
+ };
151
+ seen.set(value, result);
152
+ const props = value.props;
153
+ let changed: Record<string, unknown> | undefined;
154
+ for (const key of Object.keys(props)) {
155
+ const r = await elideNested(props[key], seen);
181
156
  if (r.state === "rejected") return r;
182
- out[i] = r.value;
183
- hasHole ||= r.hasHole;
157
+ if (r.value !== props[key]) (changed ??= {})[key] = r.value;
158
+ result.hasHole ||= r.hasHole;
184
159
  }
185
- return { state: "ok", value: out, hasHole };
160
+ if (changed) result.value = cloneElementWithProps(value, changed);
161
+ return result;
186
162
  }
187
163
 
188
- if (isPlainObject(value)) {
189
- if (seen.has(value)) return { state: "ok", value, hasHole: false };
190
- seen.add(value);
191
- const out: Record<string, unknown> = {};
192
- let hasHole = false;
193
- for (const key of Object.keys(value)) {
194
- const r = await elideNested(value[key], seen);
164
+ if (Array.isArray(value) || isPlainDataObject(value)) {
165
+ const source = value as Record<string, unknown>;
166
+ const out = (Array.isArray(value) ? new Array(value.length) : {}) as Record<
167
+ string,
168
+ unknown
169
+ >;
170
+ // Cached before the walk: a cycle back here pins the copy.
171
+ const result: ElideResult & { state: "ok" } = {
172
+ state: "ok",
173
+ value: out,
174
+ hasHole: false,
175
+ };
176
+ seen.set(value, result);
177
+ let changed = false;
178
+ for (const key of Object.keys(source)) {
179
+ const r = await elideNested(source[key], seen);
195
180
  if (r.state === "rejected") return r;
196
181
  out[key] = r.value;
197
- hasHole ||= r.hasHole;
182
+ if (r.value !== source[key]) changed = true;
183
+ result.hasHole ||= r.hasHole;
198
184
  }
199
- return { state: "ok", value: out, hasHole };
185
+ if (!changed) result.value = value;
186
+ return result;
200
187
  }
201
188
 
202
189
  return { state: "ok", value, hasHole: false };
203
190
  }
204
191
 
192
+ const CLIENT_REFERENCE = Symbol.for("react.client.reference");
193
+ const TEMPORARY_REFERENCE = Symbol.for("react.temporary.reference");
194
+ const REACT_MEMO = Symbol.for("react.memo");
195
+ const REACT_FORWARD_REF = Symbol.for("react.forward_ref");
196
+ const REACT_LAZY = Symbol.for("react.lazy");
197
+
198
+ /**
199
+ * Whether Flight calls user code to encode an element of this type (its
200
+ * renderElement): a function component, or a memo/forwardRef/lazy wrapper
201
+ * around one. Host (string), Suspense/Fragment (symbol) and client-reference
202
+ * types are encoded with their props as data, so a hole marker in those props
203
+ * reaches no component. A lazy type is resolved the way Flight resolves it:
204
+ * the Flight decode (a cache() hit, a "use cache" result) hands every client
205
+ * component back as a lazy around its client reference, and the capture render
206
+ * already resolved it. One that cannot resolve yet counts as a server
207
+ * component.
208
+ */
209
+ function rendersOnServer(type: unknown): boolean {
210
+ if (typeof type !== "function" && (typeof type !== "object" || !type)) {
211
+ return false;
212
+ }
213
+ const tag = (type as { $$typeof?: unknown }).$$typeof;
214
+ if (tag === CLIENT_REFERENCE || tag === TEMPORARY_REFERENCE) return false;
215
+ if (typeof type === "function") return true;
216
+ if (tag === REACT_MEMO) {
217
+ return rendersOnServer((type as { type?: unknown }).type);
218
+ }
219
+ if (tag === REACT_LAZY) {
220
+ const resolved = readLazyNode(type);
221
+ return resolved === UNRESOLVED || rendersOnServer(resolved);
222
+ }
223
+ return tag === REACT_FORWARD_REF;
224
+ }
225
+
226
+ /**
227
+ * Whether the mask found a thenable in an element's props: the mask's own
228
+ * walk, so elide and the mask share one traversal rule.
229
+ */
230
+ function elementHoldsThenable(props: unknown): boolean {
231
+ const report: MaskReport = { thenable: false };
232
+ maskNestedContainerThenables(props, undefined, report);
233
+ return report.thenable;
234
+ }
235
+
236
+ const UNRESOLVED: unique symbol = Symbol("unresolved");
237
+
238
+ function isLazyNode(value: unknown): boolean {
239
+ return (
240
+ typeof value === "object" &&
241
+ value !== null &&
242
+ (value as { $$typeof?: unknown }).$$typeof === REACT_LAZY
243
+ );
244
+ }
245
+
246
+ /**
247
+ * The value behind a lazy node, or UNRESOLVED. Flight moves an element past
248
+ * ~3.2 KB of its row into a row of its own (`$L`), which the decode wraps in a
249
+ * lazy node, so a hole marker inside that element sits behind one. The pinned
250
+ * record is fully decoded when a HIT overlays it, so the read is synchronous;
251
+ * a node that is still pending throws and stays a leaf.
252
+ */
253
+ function readLazyNode(value: unknown): unknown {
254
+ if (!isLazyNode(value)) return UNRESOLVED;
255
+ const lazy = value as { _payload: unknown; _init: (p: unknown) => unknown };
256
+ try {
257
+ return lazy._init(lazy._payload);
258
+ } catch {
259
+ return UNRESOLVED;
260
+ }
261
+ }
262
+
205
263
  /**
206
264
  * Overlay a recorded (elided) container onto the fresh run's container for a
207
265
  * shell HIT. Recorded wins per path; hole markers take the fresh value (the
208
266
  * live nested promise); fresh-only object keys pass through. Where the shapes
209
267
  * disagree structurally, recorded wins wholesale — it is what the prelude
210
- * froze, and parity beats freshness inside the shell.
268
+ * froze, and parity beats freshness inside the shell. Copy-on-write: a
269
+ * recorded subtree without markers comes back by identity. A recorded element
270
+ * is rebuilt through cloneElementWithProps only at its marker paths, so its
271
+ * other props stay pinned; props the fresh element adds do not pass through
272
+ * (the prelude rendered the element without them).
211
273
  */
212
274
  export function overlayLoaderContainer(
213
275
  fresh: unknown,
@@ -215,45 +277,72 @@ export function overlayLoaderContainer(
215
277
  ): unknown {
216
278
  if (isLoaderHoleMarker(recorded)) return fresh;
217
279
 
218
- if (isLoaderSettledMarker(recorded)) {
219
- const pinned = recorded.value;
220
- // Deep holes inside a settled container (capture-computed `holes` bit)
221
- // need the fresh promise's resolved value to fill them; a fully-pinned
222
- // container resolves immediately (the prelude already shows it — never
223
- // gate it on fresh latency). A rejecting fresh run degrades holes to
224
- // undefined instead of poisoning the pin.
225
- if (recorded.holes === 1) {
226
- return Promise.resolve(fresh).then(
227
- (freshValue) => overlayLoaderContainer(freshValue, pinned),
228
- () => overlayLoaderContainer(undefined, pinned),
229
- );
280
+ if (Array.isArray(recorded)) {
281
+ const freshArr = Array.isArray(fresh) ? fresh : [];
282
+ let out: unknown[] | undefined;
283
+ for (let i = 0; i < recorded.length; i++) {
284
+ const next = overlayLoaderContainer(freshArr[i], recorded[i]);
285
+ if (next !== recorded[i]) (out ??= recorded.slice())[i] = next;
230
286
  }
231
- return Promise.resolve(overlayLoaderContainer(undefined, pinned));
287
+ return out ?? recorded;
232
288
  }
233
289
 
234
- if (Array.isArray(recorded)) {
235
- const freshArr = Array.isArray(fresh) ? fresh : [];
236
- return recorded.map((item, i) => overlayLoaderContainer(freshArr[i], item));
290
+ if (isValidElement<Record<string, unknown>>(recorded)) {
291
+ return overlayElement(fresh, recorded);
237
292
  }
238
293
 
239
- if (isPlainObject(recorded)) {
240
- if (!isPlainObject(fresh)) {
241
- // Shape drift: still honor markers (holes fall back to undefined).
242
- const out: Record<string, unknown> = {};
243
- for (const key of Object.keys(recorded)) {
244
- out[key] = overlayLoaderContainer(undefined, recorded[key]);
245
- }
246
- return out;
247
- }
248
- const out: Record<string, unknown> = {};
294
+ const lazyValue = readLazyNode(recorded);
295
+ if (lazyValue !== UNRESOLVED) {
296
+ const next = overlayLoaderContainer(fresh, lazyValue);
297
+ return next === lazyValue ? recorded : next;
298
+ }
299
+
300
+ if (isPlainDataObject(recorded)) {
301
+ // Shape drift (fresh not a plain object): markers fall back to undefined.
302
+ const freshObj = isPlainDataObject(fresh) ? fresh : undefined;
303
+ let out: Record<string, unknown> | undefined;
249
304
  for (const key of Object.keys(recorded)) {
250
- out[key] = overlayLoaderContainer(fresh[key], recorded[key]);
305
+ const next = overlayLoaderContainer(freshObj?.[key], recorded[key]);
306
+ if (next !== recorded[key]) (out ??= { ...recorded })[key] = next;
251
307
  }
252
- for (const key of Object.keys(fresh)) {
253
- if (!(key in out)) out[key] = fresh[key];
308
+ if (freshObj) {
309
+ for (const key of Object.keys(freshObj)) {
310
+ if (!(key in recorded)) (out ??= { ...recorded })[key] = freshObj[key];
311
+ }
254
312
  }
255
- return out;
313
+ return out ?? recorded;
256
314
  }
257
315
 
258
316
  return recorded;
259
317
  }
318
+
319
+ function overlayElement(
320
+ fresh: unknown,
321
+ recorded: ReactElement<Record<string, unknown>>,
322
+ ): unknown {
323
+ if (isLazyNode(fresh)) {
324
+ // A fresh lazy node is read only when a recorded path needs the fresh
325
+ // value: reading it parses a decoded row or calls a React.lazy factory
326
+ // the HIT render never asks for otherwise.
327
+ if (overlayProps(undefined, recorded) === recorded) return recorded;
328
+ const read = readLazyNode(fresh);
329
+ fresh = read === UNRESOLVED ? undefined : read;
330
+ }
331
+ return overlayProps(
332
+ isValidElement<Record<string, unknown>>(fresh) ? fresh.props : undefined,
333
+ recorded,
334
+ );
335
+ }
336
+
337
+ function overlayProps(
338
+ freshProps: Record<string, unknown> | undefined,
339
+ recorded: ReactElement<Record<string, unknown>>,
340
+ ): unknown {
341
+ const props = recorded.props;
342
+ let changed: Record<string, unknown> | undefined;
343
+ for (const key of Object.keys(props)) {
344
+ const next = overlayLoaderContainer(freshProps?.[key], props[key]);
345
+ if (next !== props[key]) (changed ??= {})[key] = next;
346
+ }
347
+ return changed ? cloneElementWithProps(recorded, changed) : recorded;
348
+ }