@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650

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 (356) hide show
  1. package/README.md +24 -9
  2. package/dist/bin/rango.js +157 -63
  3. package/dist/testing/vitest.js +82 -0
  4. package/dist/vite/index.js +1584 -639
  5. package/package.json +71 -21
  6. package/skills/api-client/SKILL.md +211 -0
  7. package/skills/breadcrumbs/SKILL.md +60 -0
  8. package/skills/bundle-analysis/SKILL.md +159 -0
  9. package/skills/cache-guide/SKILL.md +222 -30
  10. package/skills/caching/SKILL.md +263 -8
  11. package/skills/composability/SKILL.md +27 -2
  12. package/skills/css/SKILL.md +76 -0
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +3 -1
  15. package/skills/hooks/SKILL.md +235 -28
  16. package/skills/host-router/SKILL.md +122 -22
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +29 -5
  19. package/skills/layout/SKILL.md +13 -9
  20. package/skills/links/SKILL.md +173 -17
  21. package/skills/loader/SKILL.md +170 -23
  22. package/skills/middleware/SKILL.md +16 -10
  23. package/skills/migrate-nextjs/SKILL.md +38 -16
  24. package/skills/mime-routes/SKILL.md +27 -0
  25. package/skills/observability/SKILL.md +137 -0
  26. package/skills/parallel/SKILL.md +11 -7
  27. package/skills/prerender/SKILL.md +14 -33
  28. package/skills/rango/SKILL.md +250 -25
  29. package/skills/react-compiler/SKILL.md +168 -0
  30. package/skills/response-routes/SKILL.md +114 -47
  31. package/skills/route/SKILL.md +42 -5
  32. package/skills/router-setup/SKILL.md +3 -3
  33. package/skills/server-actions/SKILL.md +78 -42
  34. package/skills/tailwind/SKILL.md +27 -3
  35. package/skills/testing/SKILL.md +129 -0
  36. package/skills/testing/bindings.md +89 -0
  37. package/skills/testing/cache-prerender.md +124 -0
  38. package/skills/testing/client-components.md +122 -0
  39. package/skills/testing/e2e-parity.md +125 -0
  40. package/skills/testing/flight.md +92 -0
  41. package/skills/testing/handles.md +129 -0
  42. package/skills/testing/loader.md +128 -0
  43. package/skills/testing/middleware.md +99 -0
  44. package/skills/testing/render-handler.md +121 -0
  45. package/skills/testing/response-routes.md +95 -0
  46. package/skills/testing/reverse-and-types.md +84 -0
  47. package/skills/testing/server-actions.md +107 -0
  48. package/skills/testing/server-tree.md +128 -0
  49. package/skills/testing/setup.md +120 -0
  50. package/skills/typesafety/SKILL.md +316 -26
  51. package/skills/use-cache/SKILL.md +36 -5
  52. package/skills/vercel/SKILL.md +107 -0
  53. package/skills/view-transitions/SKILL.md +294 -0
  54. package/src/__augment-tests__/augment.ts +81 -0
  55. package/src/__augment-tests__/augmented.check.ts +116 -0
  56. package/src/__internal.ts +0 -65
  57. package/src/browser/action-coordinator.ts +53 -36
  58. package/src/browser/action-fence.ts +47 -0
  59. package/src/browser/app-shell.ts +14 -27
  60. package/src/browser/cookie-name.ts +140 -0
  61. package/src/browser/event-controller.ts +37 -143
  62. package/src/browser/history-state.ts +21 -0
  63. package/src/browser/index.ts +3 -3
  64. package/src/browser/invalidate-client-cache.ts +52 -0
  65. package/src/browser/navigation-bridge.ts +30 -59
  66. package/src/browser/navigation-client.ts +96 -84
  67. package/src/browser/navigation-store-handle.ts +38 -0
  68. package/src/browser/navigation-store.ts +32 -82
  69. package/src/browser/navigation-transaction.ts +9 -59
  70. package/src/browser/partial-update.ts +60 -127
  71. package/src/browser/prefetch/cache.ts +82 -72
  72. package/src/browser/prefetch/fetch.ts +108 -33
  73. package/src/browser/prefetch/queue.ts +6 -3
  74. package/src/browser/rango-state.ts +157 -115
  75. package/src/browser/react/Link.tsx +0 -2
  76. package/src/browser/react/NavigationProvider.tsx +41 -48
  77. package/src/browser/react/ScrollRestoration.tsx +10 -6
  78. package/src/browser/react/filter-segment-order.ts +0 -2
  79. package/src/browser/react/index.ts +0 -48
  80. package/src/browser/react/location-state-shared.ts +166 -8
  81. package/src/browser/react/location-state.ts +39 -14
  82. package/src/browser/react/use-action.ts +6 -15
  83. package/src/browser/react/use-handle.ts +17 -14
  84. package/src/browser/react/use-link-status.ts +0 -4
  85. package/src/browser/react/use-navigation.ts +0 -3
  86. package/src/browser/react/use-params.ts +11 -11
  87. package/src/browser/react/use-reverse.ts +106 -0
  88. package/src/browser/react/use-router.ts +20 -5
  89. package/src/browser/react/use-search-params.ts +0 -5
  90. package/src/browser/react/use-segments.ts +0 -13
  91. package/src/browser/response-adapter.ts +52 -1
  92. package/src/browser/rsc-router.tsx +70 -34
  93. package/src/browser/scroll-restoration.ts +22 -14
  94. package/src/browser/segment-structure-assert.ts +2 -2
  95. package/src/browser/server-action-bridge.ts +168 -44
  96. package/src/browser/types.ts +36 -21
  97. package/src/browser/validate-redirect-origin.ts +43 -16
  98. package/src/build/collect-fallback-refs.ts +107 -0
  99. package/src/build/generate-manifest.ts +60 -35
  100. package/src/build/generate-route-types.ts +3 -0
  101. package/src/build/index.ts +8 -2
  102. package/src/build/prefix-tree-utils.ts +123 -0
  103. package/src/build/route-trie.ts +89 -10
  104. package/src/build/route-types/codegen.ts +4 -4
  105. package/src/build/route-types/include-resolution.ts +1 -1
  106. package/src/build/route-types/param-extraction.ts +6 -3
  107. package/src/build/route-types/per-module-writer.ts +7 -4
  108. package/src/build/route-types/router-processing.ts +122 -22
  109. package/src/build/route-types/scan-filter.ts +1 -1
  110. package/src/build/route-types/source-scan.ts +118 -0
  111. package/src/build/runtime-discovery.ts +9 -20
  112. package/src/cache/cache-error.ts +104 -0
  113. package/src/cache/cache-policy.ts +68 -28
  114. package/src/cache/cache-runtime.ts +134 -32
  115. package/src/cache/cache-scope.ts +100 -74
  116. package/src/cache/cache-tag.ts +98 -0
  117. package/src/cache/cf/cf-cache-store.ts +2255 -238
  118. package/src/cache/cf/index.ts +6 -16
  119. package/src/cache/document-cache.ts +61 -20
  120. package/src/cache/handle-snapshot.ts +63 -0
  121. package/src/cache/index.ts +22 -20
  122. package/src/cache/memory-segment-store.ts +136 -37
  123. package/src/cache/profile-registry.ts +6 -30
  124. package/src/cache/read-through-swr.ts +41 -11
  125. package/src/cache/segment-codec.ts +0 -16
  126. package/src/cache/tag-invalidation.ts +230 -0
  127. package/src/cache/types.ts +33 -100
  128. package/src/cache/vercel/index.ts +11 -0
  129. package/src/cache/vercel/vercel-cache-store.ts +799 -0
  130. package/src/client.rsc.tsx +6 -21
  131. package/src/client.tsx +25 -61
  132. package/src/component-utils.ts +19 -0
  133. package/src/context-var.ts +17 -5
  134. package/src/decode-loader-results.ts +36 -0
  135. package/src/defer.ts +196 -0
  136. package/src/deps/ssr.ts +0 -1
  137. package/src/errors.ts +30 -4
  138. package/src/handle.ts +31 -23
  139. package/src/handles/MetaTags.tsx +0 -14
  140. package/src/handles/breadcrumbs.ts +16 -5
  141. package/src/handles/meta.ts +0 -39
  142. package/src/host/cookie-handler.ts +0 -36
  143. package/src/host/errors.ts +0 -24
  144. package/src/host/index.ts +8 -2
  145. package/src/host/pattern-matcher.ts +7 -50
  146. package/src/host/router.ts +107 -99
  147. package/src/host/testing.ts +40 -27
  148. package/src/host/types.ts +37 -4
  149. package/src/host/utils.ts +1 -1
  150. package/src/href-client.ts +137 -22
  151. package/src/index.rsc.ts +63 -9
  152. package/src/index.ts +64 -9
  153. package/src/internal-debug.ts +2 -4
  154. package/src/loader-store.ts +500 -0
  155. package/src/loader.rsc.ts +20 -13
  156. package/src/loader.ts +12 -11
  157. package/src/missing-id-error.ts +68 -0
  158. package/src/network-error-thrower.tsx +1 -6
  159. package/src/outlet-provider.tsx +1 -5
  160. package/src/prerender/param-hash.ts +10 -11
  161. package/src/prerender/store.ts +32 -37
  162. package/src/prerender.ts +61 -6
  163. package/src/redirect-origin.ts +100 -0
  164. package/src/response-utils.ts +9 -0
  165. package/src/reverse.ts +65 -40
  166. package/src/root-error-boundary.tsx +1 -19
  167. package/src/route-content-wrapper.tsx +7 -72
  168. package/src/route-definition/dsl-helpers.ts +244 -281
  169. package/src/route-definition/helper-factories.ts +29 -139
  170. package/src/route-definition/helpers-types.ts +40 -17
  171. package/src/route-definition/redirect.ts +43 -9
  172. package/src/route-definition/resolve-handler-use.ts +6 -0
  173. package/src/route-definition/use-item-types.ts +32 -0
  174. package/src/route-map-builder.ts +0 -16
  175. package/src/route-types.ts +19 -41
  176. package/src/router/basename.ts +14 -0
  177. package/src/router/content-negotiation.ts +15 -15
  178. package/src/router/error-handling.ts +13 -17
  179. package/src/router/find-match.ts +44 -23
  180. package/src/router/handler-context.ts +4 -41
  181. package/src/router/intercept-resolution.ts +14 -19
  182. package/src/router/lazy-includes.ts +9 -46
  183. package/src/router/loader-resolution.ts +91 -46
  184. package/src/router/logging.ts +0 -6
  185. package/src/router/manifest.ts +18 -29
  186. package/src/router/match-api.ts +0 -20
  187. package/src/router/match-context.ts +0 -22
  188. package/src/router/match-handlers.ts +57 -58
  189. package/src/router/match-middleware/background-revalidation.ts +0 -7
  190. package/src/router/match-middleware/cache-lookup.ts +150 -271
  191. package/src/router/match-middleware/cache-store.ts +3 -33
  192. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  193. package/src/router/match-middleware/segment-resolution.ts +0 -22
  194. package/src/router/match-pipelines.ts +1 -42
  195. package/src/router/match-result.ts +31 -80
  196. package/src/router/metrics.ts +0 -34
  197. package/src/router/middleware-types.ts +5 -112
  198. package/src/router/middleware.ts +118 -133
  199. package/src/router/navigation-snapshot.ts +0 -51
  200. package/src/router/params-util.ts +23 -0
  201. package/src/router/pattern-matching.ts +62 -67
  202. package/src/router/prerender-match.ts +99 -63
  203. package/src/router/preview-match.ts +3 -1
  204. package/src/router/request-classification.ts +28 -62
  205. package/src/router/revalidation.ts +50 -56
  206. package/src/router/route-snapshot.ts +0 -1
  207. package/src/router/router-context.ts +0 -27
  208. package/src/router/router-interfaces.ts +68 -35
  209. package/src/router/router-options.ts +55 -1
  210. package/src/router/router-registry.ts +2 -5
  211. package/src/router/segment-resolution/fresh.ts +44 -63
  212. package/src/router/segment-resolution/helpers.ts +34 -0
  213. package/src/router/segment-resolution/loader-cache.ts +40 -37
  214. package/src/router/segment-resolution/revalidation.ts +203 -285
  215. package/src/router/segment-resolution/static-store.ts +19 -5
  216. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  217. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  218. package/src/router/segment-resolution.ts +4 -1
  219. package/src/router/segment-wrappers.ts +0 -3
  220. package/src/router/state-cookie-name.ts +33 -0
  221. package/src/router/substitute-pattern-params.ts +56 -0
  222. package/src/router/telemetry-otel.ts +0 -20
  223. package/src/router/telemetry.ts +96 -19
  224. package/src/router/timeout.ts +0 -20
  225. package/src/router/trie-matching.ts +87 -48
  226. package/src/router/types.ts +9 -63
  227. package/src/router/url-params.ts +0 -5
  228. package/src/router.ts +80 -41
  229. package/src/rsc/handler-context.ts +3 -2
  230. package/src/rsc/handler.ts +83 -78
  231. package/src/rsc/helpers.ts +93 -5
  232. package/src/rsc/index.ts +1 -1
  233. package/src/rsc/json-route-result.ts +38 -0
  234. package/src/rsc/manifest-init.ts +28 -41
  235. package/src/rsc/origin-guard.ts +39 -25
  236. package/src/rsc/progressive-enhancement.ts +12 -1
  237. package/src/rsc/redirect-guard.ts +99 -0
  238. package/src/rsc/response-error.ts +79 -12
  239. package/src/rsc/response-route-handler.ts +76 -62
  240. package/src/rsc/rsc-rendering.ts +41 -60
  241. package/src/rsc/runtime-warnings.ts +23 -10
  242. package/src/rsc/server-action.ts +62 -67
  243. package/src/rsc/ssr-setup.ts +16 -0
  244. package/src/rsc/types.ts +10 -5
  245. package/src/runtime-env.ts +18 -0
  246. package/src/search-params.ts +4 -20
  247. package/src/segment-loader-promise.ts +14 -2
  248. package/src/segment-system.tsx +199 -142
  249. package/src/serialize.ts +243 -0
  250. package/src/server/context.ts +150 -51
  251. package/src/server/cookie-store.ts +80 -5
  252. package/src/server/handle-store.ts +7 -24
  253. package/src/server/loader-registry.ts +5 -24
  254. package/src/server/request-context.ts +165 -87
  255. package/src/ssr/index.tsx +14 -14
  256. package/src/static-handler.ts +10 -13
  257. package/src/testing/cache-status.ts +162 -0
  258. package/src/testing/collect-handle.ts +40 -0
  259. package/src/testing/dispatch.ts +618 -0
  260. package/src/testing/dom.entry.ts +22 -0
  261. package/src/testing/e2e/fixture.ts +188 -0
  262. package/src/testing/e2e/index.ts +128 -0
  263. package/src/testing/e2e/matchers.ts +35 -0
  264. package/src/testing/e2e/page-helpers.ts +272 -0
  265. package/src/testing/e2e/parity.ts +387 -0
  266. package/src/testing/e2e/server.ts +195 -0
  267. package/src/testing/flight-matchers.ts +97 -0
  268. package/src/testing/flight-normalize.ts +11 -0
  269. package/src/testing/flight-runtime.d.ts +57 -0
  270. package/src/testing/flight-tree.ts +682 -0
  271. package/src/testing/flight.entry.ts +52 -0
  272. package/src/testing/flight.ts +232 -0
  273. package/src/testing/generated-routes.ts +183 -0
  274. package/src/testing/index.ts +99 -0
  275. package/src/testing/internal/context.ts +348 -0
  276. package/src/testing/internal/flight-client-globals.ts +30 -0
  277. package/src/testing/internal/seed-vars.ts +54 -0
  278. package/src/testing/render-handler.ts +330 -0
  279. package/src/testing/render-route.tsx +566 -0
  280. package/src/testing/run-loader.ts +378 -0
  281. package/src/testing/run-middleware.ts +205 -0
  282. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  283. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  284. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  285. package/src/testing/vitest-stubs/version.ts +5 -0
  286. package/src/testing/vitest.ts +305 -0
  287. package/src/theme/ThemeProvider.tsx +0 -52
  288. package/src/theme/ThemeScript.tsx +0 -6
  289. package/src/theme/constants.ts +0 -12
  290. package/src/theme/index.ts +0 -7
  291. package/src/theme/theme-context.ts +1 -5
  292. package/src/theme/theme-script.ts +0 -14
  293. package/src/theme/use-theme.ts +0 -3
  294. package/src/types/boundaries.ts +0 -35
  295. package/src/types/cache-types.ts +13 -4
  296. package/src/types/error-types.ts +30 -90
  297. package/src/types/global-namespace.ts +54 -41
  298. package/src/types/handler-context.ts +97 -22
  299. package/src/types/index.ts +1 -10
  300. package/src/types/loader-types.ts +6 -3
  301. package/src/types/request-scope.ts +0 -19
  302. package/src/types/route-config.ts +6 -50
  303. package/src/types/route-entry.ts +0 -6
  304. package/src/types/segments.ts +18 -14
  305. package/src/urls/include-helper.ts +9 -56
  306. package/src/urls/index.ts +1 -11
  307. package/src/urls/path-helper-types.ts +19 -5
  308. package/src/urls/path-helper.ts +17 -106
  309. package/src/urls/pattern-types.ts +36 -19
  310. package/src/urls/response-types.ts +20 -19
  311. package/src/urls/type-extraction.ts +58 -139
  312. package/src/urls/urls-function.ts +1 -18
  313. package/src/use-loader.tsx +292 -107
  314. package/src/vite/debug.ts +1 -0
  315. package/src/vite/discovery/bundle-postprocess.ts +8 -7
  316. package/src/vite/discovery/discover-routers.ts +95 -82
  317. package/src/vite/discovery/discovery-errors.ts +194 -0
  318. package/src/vite/discovery/prerender-collection.ts +26 -34
  319. package/src/vite/discovery/route-types-writer.ts +40 -84
  320. package/src/vite/discovery/state.ts +39 -1
  321. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  322. package/src/vite/index.ts +4 -0
  323. package/src/vite/plugin-types.ts +185 -10
  324. package/src/vite/plugins/cjs-to-esm.ts +3 -18
  325. package/src/vite/plugins/client-ref-dedup.ts +0 -11
  326. package/src/vite/plugins/client-ref-hashing.ts +12 -11
  327. package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
  328. package/src/vite/plugins/expose-action-id.ts +4 -75
  329. package/src/vite/plugins/expose-id-utils.ts +3 -54
  330. package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
  331. package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
  332. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
  333. package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
  334. package/src/vite/plugins/expose-internal-ids.ts +57 -67
  335. package/src/vite/plugins/performance-tracks.ts +9 -16
  336. package/src/vite/plugins/refresh-cmd.ts +1 -1
  337. package/src/vite/plugins/use-cache-transform.ts +26 -49
  338. package/src/vite/plugins/vercel-output.ts +258 -0
  339. package/src/vite/plugins/version-injector.ts +2 -32
  340. package/src/vite/plugins/version-plugin.ts +32 -23
  341. package/src/vite/plugins/virtual-entries.ts +35 -17
  342. package/src/vite/rango.ts +148 -115
  343. package/src/vite/router-discovery.ts +220 -68
  344. package/src/vite/utils/ast-handler-extract.ts +15 -31
  345. package/src/vite/utils/bundle-analysis.ts +10 -15
  346. package/src/vite/utils/client-chunks.ts +184 -0
  347. package/src/vite/utils/forward-user-plugins.ts +171 -0
  348. package/src/vite/utils/manifest-utils.ts +4 -59
  349. package/src/vite/utils/package-resolution.ts +1 -73
  350. package/src/vite/utils/prerender-utils.ts +0 -34
  351. package/src/vite/utils/shared-utils.ts +95 -43
  352. package/src/browser/action-response-classifier.ts +0 -99
  353. package/src/browser/react/use-client-cache.ts +0 -58
  354. package/src/browser/shallow.ts +0 -40
  355. package/src/handles/index.ts +0 -7
  356. package/src/router/middleware-cookies.ts +0 -55
@@ -11,7 +11,14 @@
11
11
  */
12
12
 
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
+ import type { CacheErrorCategory } from "../cache/cache-error.js";
14
15
  import type { CookieOptions } from "../router/middleware.js";
16
+ import {
17
+ KEEP_CACHE_HEADER,
18
+ getRawCookieValue,
19
+ mintStateValue,
20
+ serializeStateCookie,
21
+ } from "../browser/cookie-name.js";
15
22
  import type { LoaderDefinition, LoaderContext } from "../types.js";
16
23
  import type { ScopedReverseFunction } from "../reverse.js";
17
24
  import type {
@@ -33,6 +40,7 @@ import {
33
40
  type HandleData,
34
41
  } from "./handle-store.js";
35
42
  import { isHandle } from "../handle.js";
43
+ import { withDefer } from "../defer.js";
36
44
  import { track, type MetricsStore } from "./context.js";
37
45
  import { getFetchableLoader } from "./fetchable-loader-store.js";
38
46
  import type { SegmentCacheStore } from "../cache/types.js";
@@ -102,6 +110,10 @@ export interface RequestContext<
102
110
  setStatus(status: number): void;
103
111
  /** @internal Set status bypassing cache-exec guard (for framework error handling) */
104
112
  _setStatus(status: number): void;
113
+ /** @internal Rotate the rango state cookie (server seat of invalidateClientCache). */
114
+ _rotateStateCookie(): void;
115
+ /** @internal Set the keepClientCache() directive header on the response. */
116
+ _setKeepCacheDirective(): void;
105
117
 
106
118
  /**
107
119
  * Access loader data or push handle data.
@@ -140,6 +152,25 @@ export interface RequestContext<
140
152
  /** @internal Cache store for segment caching (optional, used by CacheScope) */
141
153
  _cacheStore?: SegmentCacheStore;
142
154
 
155
+ /**
156
+ * @internal Handler-owned registry of explicit per-scope stores from
157
+ * cache({ store }). Created once per createRSCHandler() and threaded into
158
+ * every request context, so it accumulates every explicit store the handler
159
+ * resolves. updateTag()/revalidateTag() iterate this set plus _cacheStore to
160
+ * reach every store that may hold tagged entries. The app-level store is not
161
+ * added here (it is always reachable via _cacheStore).
162
+ */
163
+ _explicitTaggedStores?: Set<SegmentCacheStore>;
164
+
165
+ /**
166
+ * @internal Union of every cache tag resolved while producing this request's
167
+ * response (from cache({ tags }), runtime cacheTag(), and loader cache tags).
168
+ * Populated at the tag-resolution sites via recordRequestTags(). Read by the
169
+ * document cache middleware so a full-page entry is tagged with everything its
170
+ * content used and can therefore be invalidated by updateTag()/revalidateTag().
171
+ */
172
+ _requestTags: Set<string>;
173
+
143
174
  /** @internal Cache profiles for "use cache" profile resolution (per-router) */
144
175
  _cacheProfiles?: Record<
145
176
  string,
@@ -273,7 +304,9 @@ export interface RequestContext<
273
304
 
274
305
  /**
275
306
  * @internal Set to true when the matched entry tree contains any `loading()`
276
- * entries (streaming). Used by rendered() to fail fast.
307
+ * entries (streaming). On a streaming tree rendered() waits for the streaming
308
+ * handlers to settle (via handleStore.settled) before resolving, and the
309
+ * deadlock guard state is kept live until that wait completes.
277
310
  */
278
311
  _treeHasStreaming?: boolean;
279
312
 
@@ -297,6 +330,18 @@ export interface RequestContext<
297
330
  */
298
331
  _renderBarrierHandleSnapshot?: HandleData;
299
332
 
333
+ /**
334
+ * @internal The deadlock guard window is closed (no further handler-awaits-
335
+ * loader cycle is possible). For non-streaming trees this is set when the
336
+ * barrier resolves. For streaming trees the window stays open until
337
+ * handleStore.settled — rendered() keeps waiting past the barrier and a
338
+ * loading() handler can still resume and await a still-waiting loader — so it
339
+ * is set only after settled. The guard (loader-resolution `setupLoaderAccess`)
340
+ * reads this instead of `_renderBarrierSegmentOrder` so it does not go blind
341
+ * during the streaming settle wait.
342
+ */
343
+ _renderBarrierGuardClosed?: boolean;
344
+
300
345
  /** @internal Per-request error dedup set for onError reporting */
301
346
  _reportedErrors: WeakSet<object>;
302
347
 
@@ -304,9 +349,13 @@ export interface RequestContext<
304
349
  * @internal Report a non-fatal background error through the router's
305
350
  * onError callback. Wired by the RSC handler / router during request
306
351
  * creation. Cache-runtime and other subsystems call this to surface
307
- * errors without failing the response.
352
+ * errors without failing the response. `category` is surfaced to consumers as
353
+ * `metadata.category` on the onError context (phase `cache`).
308
354
  */
309
- _reportBackgroundError?: (error: unknown, category: string) => void;
355
+ _reportBackgroundError?: (
356
+ error: unknown,
357
+ category: CacheErrorCategory,
358
+ ) => void;
310
359
 
311
360
  /** @internal Per-request debug performance override (set via ctx.debugPerformance()) */
312
361
  _debugPerformance?: boolean;
@@ -322,6 +371,15 @@ export interface RequestContext<
322
371
  * to avoid a second resolveRoute call. Cleared on HMR invalidation.
323
372
  */
324
373
  _classifiedRoute?: import("../router/route-snapshot.js").RouteSnapshot;
374
+
375
+ /**
376
+ * @internal Coarse route-level cache signal for the X-Rango-Cache debug
377
+ * header. Populated by match/matchPartial only when the debug cache signal
378
+ * gate is enabled (debugCacheSignal option or RANGO_TEST_SIGNALS=1). Read by
379
+ * the response-finalization path (createResponseWithMergedHeaders). Undefined
380
+ * when the gate is off, so no header is emitted.
381
+ */
382
+ _cacheSignal?: import("../router/telemetry.js").CacheSegmentSignal[];
325
383
  }
326
384
 
327
385
  /**
@@ -341,6 +399,8 @@ export type PublicRequestContext<
341
399
  | "deleteCookie"
342
400
  | "_handleStore"
343
401
  | "_cacheStore"
402
+ | "_explicitTaggedStores"
403
+ | "_requestTags"
344
404
  | "_cacheProfiles"
345
405
  | "_onResponseCallbacks"
346
406
  | "_themeConfig"
@@ -355,13 +415,17 @@ export type PublicRequestContext<
355
415
  | "_renderBarrierWaiters"
356
416
  | "_handlerLoaderDeps"
357
417
  | "_renderBarrierHandleSnapshot"
418
+ | "_renderBarrierGuardClosed"
358
419
  | "_reportBackgroundError"
359
420
  | "_debugPerformance"
360
421
  | "_metricsStore"
361
422
  | "_basename"
362
423
  | "_setStatus"
424
+ | "_rotateStateCookie"
425
+ | "_setKeepCacheDirective"
363
426
  | "_variables"
364
427
  | "_classifiedRoute"
428
+ | "_cacheSignal"
365
429
  | "res"
366
430
  >;
367
431
 
@@ -413,6 +477,7 @@ export function _getRequestContext<TEnv = DefaultEnv>():
413
477
  export function setRequestContextParams(
414
478
  params: Record<string, string>,
415
479
  routeName?: string,
480
+ routeMap?: Record<string, string>,
416
481
  ): void {
417
482
  const ctx = requestContextStorage.getStore();
418
483
  if (ctx) {
@@ -425,9 +490,13 @@ export function setRequestContextParams(
425
490
  : undefined
426
491
  ) as DefaultRouteName | undefined;
427
492
  }
428
- // Update reverse with scoped resolution now that route is known
493
+ // Update reverse with scoped resolution now that route is known. Production
494
+ // omits routeMap and uses the global map (routes are registered globally);
495
+ // the testing primitives (renderToFlightString/renderServerTree) pass a
496
+ // scoped routeMap so `ctx.reverse` is not order-dependent on whatever router
497
+ // registered last.
429
498
  ctx.reverse = createReverseFunction(
430
- getGlobalRouteMap(),
499
+ routeMap ?? getGlobalRouteMap(),
431
500
  routeName,
432
501
  params,
433
502
  routeName ? isRouteRootScoped(routeName) : undefined,
@@ -485,6 +554,11 @@ export interface CreateRequestContextOptions<TEnv> {
485
554
  initialResponse?: Response;
486
555
  /** Optional cache store for segment caching (used by CacheScope) */
487
556
  cacheStore?: SegmentCacheStore;
557
+ /**
558
+ * Handler-owned registry of explicit per-scope stores for cross-store tag
559
+ * invalidation. Created once per handler, reused across requests.
560
+ */
561
+ explicitTaggedStores?: Set<SegmentCacheStore>;
488
562
  /** Optional cache profiles for "use cache" resolution (per-router) */
489
563
  cacheProfiles?: Record<
490
564
  string,
@@ -494,6 +568,10 @@ export interface CreateRequestContextOptions<TEnv> {
494
568
  executionContext?: ExecutionContext;
495
569
  /** Optional theme configuration (enables ctx.theme and ctx.setTheme) */
496
570
  themeConfig?: ResolvedThemeConfig | null;
571
+ /** Resolved rango state cookie name, for the server seat of invalidateClientCache(). */
572
+ stateCookieName?: string;
573
+ /** Build version, used as the prefix of a server-rotated rango state value. */
574
+ version?: string;
497
575
  }
498
576
 
499
577
  /**
@@ -514,15 +592,17 @@ export function createRequestContext<TEnv>(
514
592
  variables,
515
593
  initialResponse,
516
594
  cacheStore,
595
+ explicitTaggedStores,
517
596
  cacheProfiles,
518
597
  executionContext,
519
598
  themeConfig,
599
+ stateCookieName,
600
+ version: stateVersion,
520
601
  } = options;
521
602
  const cookieHeader = request.headers.get("Cookie");
603
+ let rangoStateRotated = false;
522
604
  let parsedCookies: Record<string, string> | null = null;
523
605
 
524
- // Create stub response for collecting headers/cookies.
525
- // All cookie/header mutations go here; cookie reads derive from it.
526
606
  let stubResponse = initialResponse
527
607
  ? new Response(null, {
528
608
  status: initialResponse.status,
@@ -531,11 +611,9 @@ export function createRequestContext<TEnv>(
531
611
  })
532
612
  : new Response(null, { status: 200 });
533
613
 
534
- // Create handle store and loader memoization for this request
535
614
  const handleStore = createHandleStore();
536
615
  const loaderPromises = new Map<string, Promise<any>>();
537
616
 
538
- // Lazy parse cookies from the original Cookie header
539
617
  const getParsedCookies = (): Record<string, string> => {
540
618
  if (!parsedCookies) {
541
619
  parsedCookies = parseCookiesFromHeader(cookieHeader);
@@ -543,7 +621,6 @@ export function createRequestContext<TEnv>(
543
621
  return parsedCookies;
544
622
  };
545
623
 
546
- // Cached response cookie mutations — invalidated on setCookie/deleteCookie/setTheme
547
624
  let responseCookieCache: Map<string, string | null> | null = null;
548
625
  const getResponseCookies = (): Map<string, string | null> => {
549
626
  if (!responseCookieCache) {
@@ -555,8 +632,6 @@ export function createRequestContext<TEnv>(
555
632
  responseCookieCache = null;
556
633
  };
557
634
 
558
- // Guard: throw if a response-level side effect is called inside a cache() scope.
559
- // Uses ALS to detect the scope (set during segment resolution).
560
635
  function assertNotInsideCacheScopeALS(methodName: string): void {
561
636
  if (isInsideCacheScope()) {
562
637
  throw new Error(
@@ -567,8 +642,7 @@ export function createRequestContext<TEnv>(
567
642
  }
568
643
  }
569
644
 
570
- // Effective cookie read: response stub Set-Cookie wins, then original header.
571
- // The stub IS the source of truth for same-request mutations.
645
+ // Response stub Set-Cookie wins, then original header (source of truth for mutations).
572
646
  const effectiveCookie = (name: string): string | undefined => {
573
647
  const mutations = getResponseCookies();
574
648
  if (mutations.has(name)) {
@@ -578,14 +652,11 @@ export function createRequestContext<TEnv>(
578
652
  return getParsedCookies()[name];
579
653
  };
580
654
 
581
- // Theme helpers (only used when themeConfig is provided)
582
655
  const getTheme = (): Theme | undefined => {
583
656
  if (!themeConfig) return undefined;
584
657
 
585
- // Use overlay-aware read so setTheme() in the same request is reflected
586
658
  const stored = effectiveCookie(themeConfig.storageKey);
587
659
  if (stored) {
588
- // Validate stored value
589
660
  if (stored === "system" && themeConfig.enableSystem) {
590
661
  return "system";
591
662
  }
@@ -599,7 +670,6 @@ export function createRequestContext<TEnv>(
599
670
  const setTheme = (theme: Theme): void => {
600
671
  if (!themeConfig) return;
601
672
 
602
- // Validate theme value
603
673
  if (theme !== "system" && !themeConfig.themes.includes(theme)) {
604
674
  console.warn(
605
675
  `[Theme] Invalid theme value: "${theme}". Valid values: system, ${themeConfig.themes.join(", ")}`,
@@ -607,7 +677,6 @@ export function createRequestContext<TEnv>(
607
677
  return;
608
678
  }
609
679
 
610
- // Write to stub — effectiveCookie() will pick it up on next read
611
680
  stubResponse.headers.append(
612
681
  "Set-Cookie",
613
682
  serializeCookieValue(themeConfig.storageKey, theme, {
@@ -619,10 +688,8 @@ export function createRequestContext<TEnv>(
619
688
  invalidateResponseCookieCache();
620
689
  };
621
690
 
622
- // Strip internal _rsc* params so userland sees a clean URL.
623
691
  const cleanUrl = stripInternalParams(url);
624
692
 
625
- // Build the context object first (without use), then add use
626
693
  const ctx: RequestContext<TEnv> = {
627
694
  env,
628
695
  request,
@@ -708,6 +775,45 @@ export function createRequestContext<TEnv>(
708
775
  stubResponse.headers.set(name, value);
709
776
  },
710
777
 
778
+ // Rotate the rango state cookie for the responding client (the server seat
779
+ // of invalidateClientCache). Writes ONE Set-Cookie per request with the
780
+ // value {version}:{timestamp}; the `:` stays raw (the cookie-name.ts
781
+ // serializer), not the URL-encoded form serializeCookieValue would produce.
782
+ // The timestamp is strictly greater than the client's current one (inbound
783
+ // X-Rango-State), so a same-millisecond server rotation still differs from
784
+ // the client value and the divergence observer fires.
785
+ _rotateStateCookie(): void {
786
+ if (rangoStateRotated) return;
787
+ rangoStateRotated = true;
788
+ if (!stateCookieName) return;
789
+ // The client's current value, for the monotonic guard: prefer the
790
+ // X-Rango-State header (router navigation/prefetch fetches send it), but
791
+ // fall back to the request's rango state cookie — action POSTs / plain
792
+ // app fetch()s carry no router header yet DO send the cookie. Without the
793
+ // fallback, prevTs stays 0 and a same-ms mint can equal the client value,
794
+ // leaving the divergence observer silent. `|| null` so an empty header
795
+ // ('' from proxy normalization) falls through instead of short-circuiting.
796
+ // getRawCookieValue reads the cookie undecoded (the wire value
797
+ // decodeStateValue decodes exactly once) AND is the same parser the client
798
+ // mirror uses, so both seats read the same jar entry.
799
+ const prevRaw =
800
+ (request.headers.get("x-rango-state") || null) ??
801
+ getRawCookieValue(cookieHeader, stateCookieName);
802
+ const value = mintStateValue(stateVersion ?? "0", prevRaw);
803
+ stubResponse.headers.append(
804
+ "Set-Cookie",
805
+ serializeStateCookie(stateCookieName, value, url.protocol === "https:"),
806
+ );
807
+ invalidateResponseCookieCache();
808
+ },
809
+
810
+ // Set the keepClientCache() directive header. The action bridge reads it on
811
+ // the response and suppresses its automatic invalidation. `.set` makes this
812
+ // idempotent (one header regardless of call count).
813
+ _setKeepCacheDirective(): void {
814
+ stubResponse.headers.set(KEEP_CACHE_HEADER, "1");
815
+ },
816
+
711
817
  setStatus(status: number): void {
712
818
  assertNotInsideCacheExec(ctx, "setStatus");
713
819
  assertNotInsideCacheScopeALS("setStatus");
@@ -724,13 +830,14 @@ export function createRequestContext<TEnv>(
724
830
  });
725
831
  },
726
832
 
727
- // Placeholder - will be replaced below
728
833
  use: null as any,
729
834
 
730
835
  method: request.method,
731
836
 
732
837
  _handleStore: handleStore,
733
838
  _cacheStore: cacheStore,
839
+ _explicitTaggedStores: explicitTaggedStores,
840
+ _requestTags: new Set<string>(),
734
841
  _cacheProfiles: cacheProfiles,
735
842
 
736
843
  waitUntil(fn: () => Promise<void>): void {
@@ -751,7 +858,6 @@ export function createRequestContext<TEnv>(
751
858
  this._onResponseCallbacks.push(callback);
752
859
  },
753
860
 
754
- // Theme properties (only set when themeConfig is provided)
755
861
  get theme() {
756
862
  return themeConfig ? getTheme() : undefined;
757
863
  },
@@ -775,19 +881,17 @@ export function createRequestContext<TEnv>(
775
881
  _reportedErrors: new WeakSet<object>(),
776
882
  _metricsStore: undefined,
777
883
 
778
- // Render barrier: deferred promise resolved after non-loader segments settle.
779
- _renderBarrier: null as any, // set below
780
- _resolveRenderBarrier: null as any, // set below
884
+ _renderBarrier: null as any,
885
+ _resolveRenderBarrier: null as any,
781
886
  _renderBarrierSegmentOrder: undefined,
782
887
 
783
888
  reverse: createReverseFunction(getGlobalRouteMap(), undefined, {}),
784
889
  };
785
890
 
786
- // Lazy render barrier: only allocate the Promise when a loader actually
787
- // calls rendered(). Requests that don't use rendered() pay zero cost.
891
+ // Lazy allocation: only create Promise when a loader calls rendered().
788
892
  let barrierResolved = false;
789
893
  let resolveBarrier: (() => void) | undefined;
790
- ctx._renderBarrier = null as any; // lazy — created on first access
894
+ ctx._renderBarrier = null as any;
791
895
  ctx._resolveRenderBarrier = (
792
896
  segments: Array<{ type: string; id: string }>,
793
897
  ) => {
@@ -797,21 +901,26 @@ export function createRequestContext<TEnv>(
797
901
  .filter((s) => s.type !== "loader")
798
902
  .map((s) => s.id);
799
903
  ctx._renderBarrierSegmentOrder = segOrder;
800
- // Build and cache handle snapshot so loader ctx.use(handle) calls
801
- // don't rebuild it on every invocation.
802
- ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
803
- handleStore,
804
- segOrder,
805
- );
806
- ctx._renderBarrierWaiters = undefined;
807
- ctx._handlerLoaderDeps = undefined;
904
+
905
+ const closeGuard = () => {
906
+ ctx._renderBarrierWaiters = undefined;
907
+ ctx._handlerLoaderDeps = undefined;
908
+ ctx._renderBarrierGuardClosed = true;
909
+ };
910
+
911
+ if (ctx._treeHasStreaming) {
912
+ handleStore.settled.then(closeGuard);
913
+ } else {
914
+ ctx._renderBarrierHandleSnapshot = buildHandleSnapshot(
915
+ handleStore,
916
+ segOrder,
917
+ );
918
+ closeGuard();
919
+ }
808
920
  if (resolveBarrier) resolveBarrier();
809
921
  };
810
922
  Object.defineProperty(ctx, "_renderBarrier", {
811
923
  get() {
812
- // Barrier already resolved (cache/prerender hit) or first lazy access.
813
- // Either way, replace the getter with a concrete value to avoid
814
- // repeated Promise.resolve() allocations on subsequent reads.
815
924
  const p = barrierResolved
816
925
  ? Promise.resolve()
817
926
  : new Promise<void>((resolve) => {
@@ -827,24 +936,16 @@ export function createRequestContext<TEnv>(
827
936
  configurable: true,
828
937
  });
829
938
 
830
- // Now create use() with access to ctx
831
939
  ctx.use = createUseFunction({
832
940
  handleStore,
833
941
  loaderPromises,
834
942
  getContext: () => ctx,
835
943
  });
836
944
 
837
- // Brand with taint symbol so "use cache" excludes ctx from cache keys
838
945
  (ctx as any)[NOCACHE_SYMBOL] = true;
839
946
  return ctx;
840
947
  }
841
948
 
842
- /**
843
- * Parse Set-Cookie headers from a response into effective cookie state.
844
- * Returns a map of cookie name -> value (string) or name -> null (deleted).
845
- * Last-write-wins: later Set-Cookie entries for the same name overwrite earlier ones.
846
- * Max-Age=0 is treated as a delete.
847
- */
848
949
  const MAX_AGE_ZERO_RE = /;\s*Max-Age\s*=\s*0/i;
849
950
 
850
951
  function parseResponseCookies(response: Response): Map<string, string | null> {
@@ -852,7 +953,6 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
852
953
  const setCookies = response.headers.getSetCookie();
853
954
 
854
955
  for (const header of setCookies) {
855
- // First segment before ';' is the name=value pair
856
956
  const semiIdx = header.indexOf(";");
857
957
  const pair = semiIdx === -1 ? header : header.substring(0, semiIdx);
858
958
  const eqIdx = pair.indexOf("=");
@@ -864,11 +964,9 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
864
964
  name = decodeURIComponent(pair.substring(0, eqIdx).trim());
865
965
  value = decodeURIComponent(pair.substring(eqIdx + 1).trim());
866
966
  } catch {
867
- // Malformed encoding — skip this entry
868
967
  continue;
869
968
  }
870
969
 
871
- // Max-Age=0 means the cookie is being deleted
872
970
  const isDeleted = MAX_AGE_ZERO_RE.test(header);
873
971
  result.set(name, isDeleted ? null : value);
874
972
  }
@@ -876,10 +974,10 @@ function parseResponseCookies(response: Response): Map<string, string | null> {
876
974
  return result;
877
975
  }
878
976
 
879
- /**
880
- * Parse cookies from Cookie header
881
- */
882
- function parseCookiesFromHeader(
977
+ // Exported for unit tests; the canonical cookie parse/serialize lives here
978
+ // (a duplicate copy in middleware-cookies.ts was removed). Not part of the
979
+ // public export surface.
980
+ export function parseCookiesFromHeader(
883
981
  cookieHeader: string | null,
884
982
  ): Record<string, string> {
885
983
  if (!cookieHeader) return {};
@@ -894,7 +992,7 @@ function parseCookiesFromHeader(
894
992
  try {
895
993
  cookies[name] = decodeURIComponent(raw);
896
994
  } catch {
897
- // Malformed percent-encoded value (e.g. %zz, %2) - fall back to raw value
995
+ // Malformed percent-encoding: fall back to raw value
898
996
  cookies[name] = raw;
899
997
  }
900
998
  }
@@ -903,10 +1001,7 @@ function parseCookiesFromHeader(
903
1001
  return cookies;
904
1002
  }
905
1003
 
906
- /**
907
- * Serialize a cookie for Set-Cookie header
908
- */
909
- function serializeCookieValue(
1004
+ export function serializeCookieValue(
910
1005
  name: string,
911
1006
  value: string,
912
1007
  options: CookieOptions = {},
@@ -933,20 +1028,12 @@ export interface CreateUseFunctionOptions<TEnv> {
933
1028
  getContext: () => RequestContext<TEnv>;
934
1029
  }
935
1030
 
936
- /**
937
- * Create the use() function for loader and handle composition.
938
- *
939
- * This is the unified implementation used by both RequestContext and HandlerContext.
940
- * - For loaders: executes and memoizes loader functions
941
- * - For handles: returns a push function to add handle data
942
- */
943
1031
  export function createUseFunction<TEnv>(
944
1032
  options: CreateUseFunctionOptions<TEnv>,
945
1033
  ): RequestContext["use"] {
946
1034
  const { handleStore, loaderPromises, getContext } = options;
947
1035
 
948
1036
  return ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
949
- // Handle case: return a push function
950
1037
  if (isHandle(item)) {
951
1038
  const handle = item;
952
1039
  const ctx = getContext();
@@ -959,30 +1046,24 @@ export function createUseFunction<TEnv>(
959
1046
  );
960
1047
  }
961
1048
 
962
- // Return a push function bound to this handle and segment
963
- return (
964
- dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>),
965
- ) => {
966
- // If it's a function, call it immediately to get the promise
967
- const valueOrPromise =
968
- typeof dataOrFn === "function"
969
- ? (dataOrFn as () => Promise<unknown>)()
970
- : dataOrFn;
971
-
972
- // Push directly - promises will be serialized by RSC and streamed
973
- handleStore.push(handle.$$id, segmentId, valueOrPromise);
974
- };
1049
+ return withDefer(
1050
+ (dataOrFn: unknown | Promise<unknown> | (() => Promise<unknown>)) => {
1051
+ const valueOrPromise =
1052
+ typeof dataOrFn === "function"
1053
+ ? (dataOrFn as () => Promise<unknown>)()
1054
+ : dataOrFn;
1055
+
1056
+ handleStore.push(handle.$$id, segmentId, valueOrPromise);
1057
+ },
1058
+ );
975
1059
  }
976
1060
 
977
- // Loader case
978
1061
  const loader = item as LoaderDefinition<any, any>;
979
1062
 
980
- // Return cached promise if already started
981
1063
  if (loaderPromises.has(loader.$$id)) {
982
1064
  return loaderPromises.get(loader.$$id);
983
1065
  }
984
1066
 
985
- // Get loader function - either from loader object or fetchable registry
986
1067
  let loaderFn = loader.fn;
987
1068
  if (!loaderFn) {
988
1069
  const fetchable = getFetchableLoader(loader.$$id);
@@ -999,7 +1080,6 @@ export function createUseFunction<TEnv>(
999
1080
 
1000
1081
  const ctx = getContext();
1001
1082
 
1002
- // Create loader context with recursive use() support
1003
1083
  const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
1004
1084
  params: ctx.params,
1005
1085
  routeParams: (ctx.params ?? {}) as Record<string, string>,
@@ -1016,7 +1096,6 @@ export function createUseFunction<TEnv>(
1016
1096
  use: (<TDep, TDepParams = any>(
1017
1097
  dep: LoaderDefinition<TDep, TDepParams>,
1018
1098
  ): Promise<TDep> => {
1019
- // Recursive call - will start dep loader if not already started
1020
1099
  return ctx.use(dep);
1021
1100
  }) as LoaderContext["use"],
1022
1101
  method: "GET",
@@ -1040,7 +1119,6 @@ export function createUseFunction<TEnv>(
1040
1119
  doneLoader();
1041
1120
  });
1042
1121
 
1043
- // Memoize for subsequent calls
1044
1122
  loaderPromises.set(loader.$$id, promise);
1045
1123
 
1046
1124
  return promise;
package/src/ssr/index.tsx CHANGED
@@ -71,7 +71,7 @@ export interface SSRRenderOptions {
71
71
  */
72
72
  export interface SSRDependencies<TEnv = unknown> {
73
73
  /**
74
- * createFromReadableStream from @vitejs/plugin-rsc/ssr
74
+ * createFromReadableStream from @rangojs/router/internal/deps/ssr
75
75
  */
76
76
  createFromReadableStream: <T>(
77
77
  stream: ReadableStream<Uint8Array>,
@@ -86,7 +86,7 @@ export interface SSRDependencies<TEnv = unknown> {
86
86
  ) => Promise<ReactDOMReadableStream>;
87
87
 
88
88
  /**
89
- * injectRSCPayload from rsc-html-stream/server
89
+ * injectRSCPayload from @rangojs/router/internal/deps/html-stream-server
90
90
  */
91
91
  injectRSCPayload: (
92
92
  rscStream: ReadableStream<Uint8Array>,
@@ -218,10 +218,10 @@ function createSsrEventController(opts: {
218
218
  *
219
219
  * @example
220
220
  * ```tsx
221
- * import { createSSRHandler } from "rsc-router/ssr";
222
- * import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
221
+ * import { createSSRHandler } from "@rangojs/router/ssr";
222
+ * import { createFromReadableStream } from "@rangojs/router/internal/deps/ssr";
223
223
  * import { renderToReadableStream } from "react-dom/server.edge";
224
- * import { injectRSCPayload } from "rsc-html-stream/server";
224
+ * import { injectRSCPayload } from "@rangojs/router/internal/deps/html-stream-server";
225
225
  *
226
226
  * export const renderHTML = createSSRHandler({
227
227
  * createFromReadableStream,
@@ -263,6 +263,7 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
263
263
  let payload: Promise<RscPayload> | undefined;
264
264
  let handlesPromise: Promise<HandleData> | undefined;
265
265
  let ssrContextValue: NavigationStoreContextValue | undefined;
266
+ let rootPromise: Promise<React.ReactNode> | undefined;
266
267
  function SsrRoot() {
267
268
  payload ??= createFromReadableStream<RscPayload>(rscStream1);
268
269
  const resolved = React.use(payload);
@@ -296,17 +297,16 @@ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
296
297
  };
297
298
 
298
299
  // Build content tree from segments.
299
- // Order must match NavigationProvider: NavigationStoreContext > ThemeProvider > content
300
- const reconstructedRoot = renderSegments(
301
- resolved.metadata?.segments ?? [],
302
- {
300
+ // Order must match NavigationProvider: NavigationStoreContext > NonceContext > ThemeProvider > content
301
+ // Memoize like payload/handles above: renderSegments is async, so
302
+ // React.use() on a fresh promise suspends and replays SsrRoot, which
303
+ // would re-run the entire segment-tree build on every initial render.
304
+ rootPromise ??= Promise.resolve(
305
+ renderSegments(resolved.metadata?.segments ?? [], {
303
306
  rootLayout: resolved.metadata?.rootLayout,
304
- },
307
+ }),
305
308
  );
306
- let content: React.ReactNode =
307
- reconstructedRoot instanceof Promise
308
- ? React.use(reconstructedRoot)
309
- : reconstructedRoot;
309
+ let content: React.ReactNode = React.use(rootPromise);
310
310
 
311
311
  // Wrap content with ThemeProvider if theme is enabled
312
312
  if (themeConfig) {