@rangojs/router 0.0.0-experimental.9c9afef3 → 0.0.0-experimental.a014d2b7

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 (402) hide show
  1. package/AGENTS.md +8 -0
  2. package/README.md +245 -49
  3. package/dist/bin/rango.js +440 -133
  4. package/dist/testing/vitest.js +82 -0
  5. package/dist/vite/index.js +3373 -1176
  6. package/dist/vite/index.js.bak +5448 -0
  7. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  8. package/package.json +68 -14
  9. package/skills/api-client/SKILL.md +211 -0
  10. package/skills/breadcrumbs/SKILL.md +64 -2
  11. package/skills/bundle-analysis/SKILL.md +159 -0
  12. package/skills/cache-guide/SKILL.md +224 -32
  13. package/skills/caching/SKILL.md +279 -17
  14. package/skills/composability/SKILL.md +27 -3
  15. package/skills/css/SKILL.md +76 -0
  16. package/skills/debug-manifest/SKILL.md +4 -2
  17. package/skills/document-cache/SKILL.md +78 -55
  18. package/skills/handler-use/SKILL.md +364 -0
  19. package/skills/hooks/SKILL.md +250 -30
  20. package/skills/host-router/SKILL.md +83 -23
  21. package/skills/i18n/SKILL.md +276 -0
  22. package/skills/intercept/SKILL.md +87 -18
  23. package/skills/layout/SKILL.md +35 -9
  24. package/skills/links/SKILL.md +249 -17
  25. package/skills/loader/SKILL.md +235 -9
  26. package/skills/middleware/SKILL.md +52 -13
  27. package/skills/migrate-nextjs/SKILL.md +584 -0
  28. package/skills/migrate-react-router/SKILL.md +771 -0
  29. package/skills/mime-routes/SKILL.md +28 -1
  30. package/skills/observability/SKILL.md +172 -0
  31. package/skills/parallel/SKILL.md +77 -7
  32. package/skills/prerender/SKILL.md +172 -125
  33. package/skills/rango/SKILL.md +251 -22
  34. package/skills/react-compiler/SKILL.md +168 -0
  35. package/skills/response-routes/SKILL.md +123 -48
  36. package/skills/route/SKILL.md +70 -5
  37. package/skills/router-setup/SKILL.md +65 -8
  38. package/skills/scripts/SKILL.md +179 -0
  39. package/skills/server-actions/SKILL.md +775 -0
  40. package/skills/streams-and-websockets/SKILL.md +283 -0
  41. package/skills/tailwind/SKILL.md +27 -3
  42. package/skills/testing/SKILL.md +130 -0
  43. package/skills/testing/bindings.md +103 -0
  44. package/skills/testing/cache-prerender.md +127 -0
  45. package/skills/testing/client-components.md +124 -0
  46. package/skills/testing/e2e-parity.md +125 -0
  47. package/skills/testing/flight.md +91 -0
  48. package/skills/testing/handles.md +129 -0
  49. package/skills/testing/loader.md +128 -0
  50. package/skills/testing/middleware.md +99 -0
  51. package/skills/testing/render-handler.md +122 -0
  52. package/skills/testing/response-routes.md +95 -0
  53. package/skills/testing/reverse-and-types.md +84 -0
  54. package/skills/testing/server-actions.md +107 -0
  55. package/skills/testing/server-tree.md +128 -0
  56. package/skills/testing/setup.md +123 -0
  57. package/skills/typesafety/SKILL.md +322 -29
  58. package/skills/use-cache/SKILL.md +57 -14
  59. package/skills/view-transitions/SKILL.md +337 -0
  60. package/src/__augment-tests__/augment.ts +81 -0
  61. package/src/__augment-tests__/augmented.check.ts +116 -0
  62. package/src/__internal.ts +1 -66
  63. package/src/browser/action-coordinator.ts +53 -36
  64. package/src/browser/action-fence.ts +47 -0
  65. package/src/browser/app-shell.ts +39 -0
  66. package/src/browser/app-version.ts +14 -0
  67. package/src/browser/connection-warmup.ts +134 -0
  68. package/src/browser/cookie-name.ts +140 -0
  69. package/src/browser/event-controller.ts +192 -150
  70. package/src/browser/history-state.ts +21 -0
  71. package/src/browser/index.ts +3 -3
  72. package/src/browser/invalidate-client-cache.ts +52 -0
  73. package/src/browser/navigation-bridge.ts +131 -30
  74. package/src/browser/navigation-client.ts +186 -100
  75. package/src/browser/navigation-store-handle.ts +38 -0
  76. package/src/browser/navigation-store.ts +157 -74
  77. package/src/browser/navigation-transaction.ts +9 -59
  78. package/src/browser/network-error-handler.ts +34 -7
  79. package/src/browser/partial-update.ts +165 -112
  80. package/src/browser/prefetch/cache.ts +205 -62
  81. package/src/browser/prefetch/fetch.ts +347 -39
  82. package/src/browser/prefetch/queue.ts +42 -8
  83. package/src/browser/rango-state.ts +158 -76
  84. package/src/browser/react/Link.tsx +102 -15
  85. package/src/browser/react/NavigationProvider.tsx +295 -119
  86. package/src/browser/react/ScrollRestoration.tsx +10 -6
  87. package/src/browser/react/context.ts +7 -2
  88. package/src/browser/react/deferred-handle-resolution.ts +75 -0
  89. package/src/browser/react/filter-segment-order.ts +66 -7
  90. package/src/browser/react/index.ts +0 -48
  91. package/src/browser/react/location-state-shared.ts +178 -8
  92. package/src/browser/react/location-state.ts +39 -14
  93. package/src/browser/react/use-action.ts +6 -15
  94. package/src/browser/react/use-handle.ts +23 -69
  95. package/src/browser/react/use-href.tsx +8 -1
  96. package/src/browser/react/use-link-status.ts +33 -8
  97. package/src/browser/react/use-navigation.ts +32 -7
  98. package/src/browser/react/use-params.ts +20 -10
  99. package/src/browser/react/use-reverse.ts +106 -0
  100. package/src/browser/react/use-router.ts +46 -11
  101. package/src/browser/react/use-search-params.ts +0 -5
  102. package/src/browser/react/use-segments.ts +11 -21
  103. package/src/browser/response-adapter.ts +99 -8
  104. package/src/browser/rsc-router.tsx +114 -24
  105. package/src/browser/scroll-restoration.ts +37 -22
  106. package/src/browser/segment-reconciler.ts +36 -14
  107. package/src/browser/segment-structure-assert.ts +2 -2
  108. package/src/browser/server-action-bridge.ts +222 -72
  109. package/src/browser/types.ts +102 -12
  110. package/src/browser/validate-redirect-origin.ts +43 -16
  111. package/src/build/collect-fallback-refs.ts +107 -0
  112. package/src/build/generate-manifest.ts +65 -40
  113. package/src/build/generate-route-types.ts +5 -1
  114. package/src/build/index.ts +8 -2
  115. package/src/build/prefix-tree-utils.ts +123 -0
  116. package/src/build/route-trie.ts +165 -36
  117. package/src/build/route-types/ast-route-extraction.ts +15 -8
  118. package/src/build/route-types/codegen.ts +16 -5
  119. package/src/build/route-types/include-resolution.ts +125 -24
  120. package/src/build/route-types/param-extraction.ts +6 -3
  121. package/src/build/route-types/per-module-writer.ts +22 -6
  122. package/src/build/route-types/router-processing.ts +260 -94
  123. package/src/build/route-types/scan-filter.ts +9 -2
  124. package/src/build/route-types/source-scan.ts +216 -0
  125. package/src/build/runtime-discovery.ts +9 -20
  126. package/src/cache/cache-error.ts +104 -0
  127. package/src/cache/cache-key-utils.ts +29 -13
  128. package/src/cache/cache-policy.ts +108 -34
  129. package/src/cache/cache-runtime.ts +224 -41
  130. package/src/cache/cache-scope.ts +188 -82
  131. package/src/cache/cache-tag.ts +103 -0
  132. package/src/cache/cf/cf-base64.ts +33 -0
  133. package/src/cache/cf/cf-cache-constants.ts +127 -0
  134. package/src/cache/cf/cf-cache-store.ts +1989 -378
  135. package/src/cache/cf/cf-cache-types.ts +349 -0
  136. package/src/cache/cf/cf-kv-utils.ts +46 -0
  137. package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
  138. package/src/cache/cf/index.ts +6 -16
  139. package/src/cache/document-cache.ts +89 -21
  140. package/src/cache/handle-snapshot.ts +70 -0
  141. package/src/cache/index.ts +10 -20
  142. package/src/cache/memory-segment-store.ts +136 -37
  143. package/src/cache/profile-registry.ts +46 -31
  144. package/src/cache/read-through-swr.ts +56 -12
  145. package/src/cache/segment-codec.ts +9 -17
  146. package/src/cache/tag-invalidation.ts +230 -0
  147. package/src/cache/types.ts +37 -100
  148. package/src/client.rsc.tsx +44 -21
  149. package/src/client.tsx +119 -290
  150. package/src/cloudflare/index.ts +11 -0
  151. package/src/cloudflare/tracing.ts +109 -0
  152. package/src/component-utils.ts +19 -0
  153. package/src/components/DefaultDocument.tsx +8 -2
  154. package/src/context-var.ts +18 -6
  155. package/src/decode-loader-results.ts +52 -0
  156. package/src/defer.ts +196 -0
  157. package/src/deps/ssr.ts +0 -1
  158. package/src/encode-kv.ts +49 -0
  159. package/src/errors.ts +30 -4
  160. package/src/escape-script.ts +52 -0
  161. package/src/handle.ts +70 -22
  162. package/src/handles/MetaTags.tsx +62 -19
  163. package/src/handles/Scripts.tsx +183 -0
  164. package/src/handles/breadcrumbs.ts +37 -8
  165. package/src/handles/is-thenable.ts +19 -0
  166. package/src/handles/meta.ts +51 -40
  167. package/src/handles/script.ts +244 -0
  168. package/src/host/cookie-handler.ts +9 -60
  169. package/src/host/errors.ts +0 -24
  170. package/src/host/index.ts +8 -2
  171. package/src/host/pattern-matcher.ts +23 -52
  172. package/src/host/router.ts +107 -99
  173. package/src/host/testing.ts +40 -27
  174. package/src/host/types.ts +37 -4
  175. package/src/host/utils.ts +1 -1
  176. package/src/href-client.ts +137 -22
  177. package/src/index.rsc.ts +99 -13
  178. package/src/index.ts +139 -19
  179. package/src/internal-debug.ts +11 -10
  180. package/src/loader-store.ts +500 -0
  181. package/src/loader.rsc.ts +20 -13
  182. package/src/loader.ts +12 -11
  183. package/src/missing-id-error.ts +68 -0
  184. package/src/outlet-context.ts +1 -1
  185. package/src/outlet-provider.tsx +1 -5
  186. package/src/prerender/param-hash.ts +16 -16
  187. package/src/prerender/store.ts +37 -41
  188. package/src/prerender.ts +198 -82
  189. package/src/redirect-origin.ts +100 -0
  190. package/src/regex-escape.ts +8 -0
  191. package/src/render-error-thrower.tsx +20 -0
  192. package/src/response-utils.ts +62 -0
  193. package/src/reverse.ts +65 -15
  194. package/src/root-error-boundary.tsx +1 -19
  195. package/src/route-content-wrapper.tsx +19 -77
  196. package/src/route-definition/dsl-helpers.ts +461 -304
  197. package/src/route-definition/helper-factories.ts +28 -140
  198. package/src/route-definition/helpers-types.ts +143 -69
  199. package/src/route-definition/index.ts +4 -2
  200. package/src/route-definition/redirect.ts +51 -10
  201. package/src/route-definition/resolve-handler-use.ts +160 -0
  202. package/src/route-definition/use-item-types.ts +29 -0
  203. package/src/route-map-builder.ts +0 -16
  204. package/src/route-types.ts +37 -46
  205. package/src/router/basename.ts +14 -0
  206. package/src/router/content-negotiation.ts +164 -17
  207. package/src/router/error-handling.ts +45 -18
  208. package/src/router/find-match.ts +44 -23
  209. package/src/router/handler-context.ts +52 -31
  210. package/src/router/instrument.ts +350 -0
  211. package/src/router/intercept-resolution.ts +48 -24
  212. package/src/router/lazy-includes.ts +15 -52
  213. package/src/router/loader-resolution.ts +268 -56
  214. package/src/router/logging.ts +0 -6
  215. package/src/router/manifest.ts +40 -42
  216. package/src/router/match-api.ts +124 -204
  217. package/src/router/match-context.ts +0 -22
  218. package/src/router/match-handlers.ts +58 -58
  219. package/src/router/match-middleware/background-revalidation.ts +40 -24
  220. package/src/router/match-middleware/cache-lookup.ts +170 -276
  221. package/src/router/match-middleware/cache-store.ts +64 -52
  222. package/src/router/match-middleware/intercept-resolution.ts +0 -22
  223. package/src/router/match-middleware/segment-resolution.ts +45 -14
  224. package/src/router/match-pipelines.ts +1 -42
  225. package/src/router/match-result.ts +87 -39
  226. package/src/router/metrics.ts +0 -34
  227. package/src/router/middleware-types.ts +7 -140
  228. package/src/router/middleware.ts +266 -169
  229. package/src/router/navigation-snapshot.ts +131 -0
  230. package/src/router/params-util.ts +23 -0
  231. package/src/router/pattern-matching.ts +132 -90
  232. package/src/router/prefetch-cache-ttl.ts +51 -0
  233. package/src/router/prerender-match.ts +195 -56
  234. package/src/router/preview-match.ts +32 -102
  235. package/src/router/request-classification.ts +276 -0
  236. package/src/router/revalidation.ts +123 -73
  237. package/src/router/route-snapshot.ts +244 -0
  238. package/src/router/router-context.ts +3 -28
  239. package/src/router/router-interfaces.ts +115 -35
  240. package/src/router/router-options.ts +172 -15
  241. package/src/router/router-registry.ts +2 -5
  242. package/src/router/segment-resolution/fresh.ts +162 -84
  243. package/src/router/segment-resolution/helpers.ts +86 -6
  244. package/src/router/segment-resolution/loader-cache.ts +76 -39
  245. package/src/router/segment-resolution/revalidation.ts +351 -321
  246. package/src/router/segment-resolution/static-store.ts +19 -5
  247. package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
  248. package/src/router/segment-resolution/view-transition-default.ts +56 -0
  249. package/src/router/segment-resolution.ts +5 -1
  250. package/src/router/segment-wrappers.ts +6 -5
  251. package/src/router/state-cookie-name.ts +33 -0
  252. package/src/router/substitute-pattern-params.ts +56 -0
  253. package/src/router/telemetry-otel.ts +161 -199
  254. package/src/router/telemetry.ts +96 -19
  255. package/src/router/timeout.ts +0 -20
  256. package/src/router/tracing.ts +206 -0
  257. package/src/router/trie-matching.ts +163 -59
  258. package/src/router/types.ts +9 -63
  259. package/src/router/url-params.ts +44 -0
  260. package/src/router.ts +157 -54
  261. package/src/rsc/handler-context.ts +3 -2
  262. package/src/rsc/handler.ts +655 -529
  263. package/src/rsc/helpers.ts +168 -46
  264. package/src/rsc/index.ts +2 -5
  265. package/src/rsc/json-route-result.ts +38 -0
  266. package/src/rsc/loader-fetch.ts +122 -31
  267. package/src/rsc/manifest-init.ts +33 -42
  268. package/src/rsc/origin-guard.ts +39 -25
  269. package/src/rsc/progressive-enhancement.ts +131 -14
  270. package/src/rsc/redirect-guard.ts +99 -0
  271. package/src/rsc/response-cache-serve.ts +238 -0
  272. package/src/rsc/response-error.ts +79 -12
  273. package/src/rsc/response-route-handler.ts +99 -189
  274. package/src/rsc/rsc-rendering.ts +109 -74
  275. package/src/rsc/runtime-warnings.ts +23 -10
  276. package/src/rsc/server-action.ts +287 -115
  277. package/src/rsc/ssr-setup.ts +18 -2
  278. package/src/rsc/transition-gate.ts +89 -0
  279. package/src/rsc/types.ts +29 -9
  280. package/src/runtime-env.ts +18 -0
  281. package/src/search-params.ts +35 -30
  282. package/src/segment-content-promise.ts +67 -0
  283. package/src/segment-loader-promise.ts +149 -0
  284. package/src/segment-system.tsx +236 -202
  285. package/src/serialize.ts +243 -0
  286. package/src/server/context.ts +224 -52
  287. package/src/server/cookie-parse.ts +32 -0
  288. package/src/server/cookie-store.ts +80 -5
  289. package/src/server/handle-store.ts +40 -38
  290. package/src/server/loader-registry.ts +38 -46
  291. package/src/server/request-context.ts +401 -173
  292. package/src/ssr/index.tsx +24 -16
  293. package/src/static-handler.ts +27 -18
  294. package/src/testing/cache-status.ts +162 -0
  295. package/src/testing/collect-handle.ts +40 -0
  296. package/src/testing/dispatch.ts +701 -0
  297. package/src/testing/dom.entry.ts +22 -0
  298. package/src/testing/e2e/fixture.ts +188 -0
  299. package/src/testing/e2e/index.ts +128 -0
  300. package/src/testing/e2e/matchers.ts +35 -0
  301. package/src/testing/e2e/page-helpers.ts +272 -0
  302. package/src/testing/e2e/parity.ts +387 -0
  303. package/src/testing/e2e/server.ts +195 -0
  304. package/src/testing/flight-matchers.ts +97 -0
  305. package/src/testing/flight-normalize.ts +11 -0
  306. package/src/testing/flight-runtime.d.ts +57 -0
  307. package/src/testing/flight-tree.ts +682 -0
  308. package/src/testing/flight.entry.ts +52 -0
  309. package/src/testing/flight.ts +257 -0
  310. package/src/testing/generated-routes.ts +183 -0
  311. package/src/testing/index.ts +105 -0
  312. package/src/testing/internal/context.ts +371 -0
  313. package/src/testing/internal/flight-client-globals.ts +30 -0
  314. package/src/testing/internal/seed-vars.ts +54 -0
  315. package/src/testing/render-handler.ts +357 -0
  316. package/src/testing/render-route.tsx +581 -0
  317. package/src/testing/run-loader.ts +385 -0
  318. package/src/testing/run-middleware.ts +205 -0
  319. package/src/testing/run-transition-when.ts +164 -0
  320. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  321. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  322. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  323. package/src/testing/vitest-stubs/version.ts +5 -0
  324. package/src/testing/vitest.ts +305 -0
  325. package/src/theme/ThemeProvider.tsx +20 -58
  326. package/src/theme/ThemeScript.tsx +7 -9
  327. package/src/theme/constants.ts +52 -13
  328. package/src/theme/index.ts +0 -7
  329. package/src/theme/theme-context.ts +1 -5
  330. package/src/theme/theme-script.ts +22 -21
  331. package/src/theme/use-theme.ts +0 -3
  332. package/src/types/boundaries.ts +0 -35
  333. package/src/types/cache-types.ts +17 -8
  334. package/src/types/error-types.ts +30 -90
  335. package/src/types/global-namespace.ts +54 -41
  336. package/src/types/handler-context.ts +125 -71
  337. package/src/types/index.ts +3 -10
  338. package/src/types/loader-types.ts +40 -11
  339. package/src/types/request-scope.ts +112 -0
  340. package/src/types/route-config.ts +6 -50
  341. package/src/types/route-entry.ts +12 -7
  342. package/src/types/segments.ts +136 -15
  343. package/src/urls/include-helper.ts +33 -70
  344. package/src/urls/index.ts +1 -11
  345. package/src/urls/path-helper-types.ts +68 -18
  346. package/src/urls/path-helper.ts +57 -111
  347. package/src/urls/pattern-types.ts +48 -19
  348. package/src/urls/response-types.ts +25 -22
  349. package/src/urls/type-extraction.ts +58 -139
  350. package/src/urls/urls-function.ts +1 -19
  351. package/src/use-loader.tsx +346 -89
  352. package/src/vite/debug.ts +185 -0
  353. package/src/vite/discovery/bundle-postprocess.ts +36 -38
  354. package/src/vite/discovery/discover-routers.ts +130 -85
  355. package/src/vite/discovery/discovery-errors.ts +194 -0
  356. package/src/vite/discovery/gate-state.ts +171 -0
  357. package/src/vite/discovery/prerender-collection.ts +214 -132
  358. package/src/vite/discovery/route-types-writer.ts +40 -84
  359. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  360. package/src/vite/discovery/state.ts +57 -4
  361. package/src/vite/discovery/virtual-module-codegen.ts +14 -34
  362. package/src/vite/index.ts +6 -0
  363. package/src/vite/inject-client-debug.ts +36 -0
  364. package/src/vite/plugin-types.ts +178 -5
  365. package/src/vite/plugins/cjs-to-esm.ts +16 -19
  366. package/src/vite/plugins/client-ref-dedup.ts +16 -11
  367. package/src/vite/plugins/client-ref-hashing.ts +28 -15
  368. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  369. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  370. package/src/vite/plugins/cloudflare-protocol-stub.ts +194 -0
  371. package/src/vite/plugins/expose-action-id.ts +48 -95
  372. package/src/vite/plugins/expose-id-utils.ts +96 -51
  373. package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
  374. package/src/vite/plugins/expose-ids/handler-transform.ts +15 -64
  375. package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
  376. package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
  377. package/src/vite/plugins/expose-internal-ids.ts +553 -317
  378. package/src/vite/plugins/performance-tracks.ts +64 -170
  379. package/src/vite/plugins/refresh-cmd.ts +89 -27
  380. package/src/vite/plugins/use-cache-transform.ts +73 -83
  381. package/src/vite/plugins/version-injector.ts +40 -29
  382. package/src/vite/plugins/version-plugin.ts +37 -40
  383. package/src/vite/plugins/virtual-entries.ts +39 -25
  384. package/src/vite/rango.ts +118 -114
  385. package/src/vite/router-discovery.ts +941 -142
  386. package/src/vite/utils/ast-handler-extract.ts +26 -35
  387. package/src/vite/utils/banner.ts +1 -1
  388. package/src/vite/utils/bundle-analysis.ts +10 -15
  389. package/src/vite/utils/client-chunks.ts +184 -0
  390. package/src/vite/utils/directive-prologue.ts +40 -0
  391. package/src/vite/utils/forward-user-plugins.ts +171 -0
  392. package/src/vite/utils/manifest-utils.ts +4 -59
  393. package/src/vite/utils/package-resolution.ts +20 -52
  394. package/src/vite/utils/prerender-utils.ts +81 -34
  395. package/src/vite/utils/shared-utils.ts +92 -42
  396. package/src/browser/action-response-classifier.ts +0 -99
  397. package/src/browser/debug-channel.ts +0 -93
  398. package/src/browser/react/use-client-cache.ts +0 -58
  399. package/src/browser/shallow.ts +0 -40
  400. package/src/handles/index.ts +0 -7
  401. package/src/network-error-thrower.tsx +0 -23
  402. package/src/router/middleware-cookies.ts +0 -55
@@ -10,7 +10,7 @@ import type {
10
10
  ShouldRevalidateFn,
11
11
  TransitionConfig,
12
12
  } from "../types";
13
- import { invariant } from "../errors";
13
+ import { invariant, DslContextError } from "../errors";
14
14
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
15
 
16
16
  // ============================================================================
@@ -40,7 +40,7 @@ export interface MetricsStore {
40
40
  metrics: PerformanceMetric[];
41
41
  }
42
42
  // ============================================================================
43
- // RSC Router Context
43
+ // Rango Context
44
44
  // ============================================================================
45
45
 
46
46
  /**
@@ -71,6 +71,10 @@ export type EntryPropCommon = {
71
71
  };
72
72
 
73
73
  /**
74
+ * Attachments resolved by walking the parent chain, not owned by the entry:
75
+ * middleware composes downward; revalidate and the error/notFound boundaries are
76
+ * resolved by nearest-ancestor lookup. Inherited, not a single execution chain.
77
+ *
74
78
  * @internal This type is an implementation detail and may change without notice.
75
79
  */
76
80
  export type EntryPropDatas = {
@@ -80,6 +84,16 @@ export type EntryPropDatas = {
80
84
  notFoundBoundary: (ReactNode | NotFoundBoundaryHandler)[];
81
85
  };
82
86
 
87
+ /**
88
+ * Render-time presentation fields shared by every entry variant.
89
+ *
90
+ * @internal This type is an implementation detail and may change without notice.
91
+ */
92
+ export type EntryPropRender = {
93
+ loading?: ReactNode | false;
94
+ transition?: TransitionConfig;
95
+ };
96
+
83
97
  /**
84
98
  * Loader entry stored in EntryData
85
99
  * Contains the loader definition and its revalidation rules
@@ -136,6 +150,19 @@ export type InterceptWhenFn<TEnv = any> = (
136
150
  ctx: InterceptSelectorContext<TEnv>,
137
151
  ) => boolean;
138
152
 
153
+ /**
154
+ * Config object passed to intercept() (its 4th argument). `when` gates whether
155
+ * the intercept activates on a soft navigation — a single match-time selector or
156
+ * an array of them (ALL must return true; omit to always activate). This is the
157
+ * intercept counterpart to transition({ when }); both express conditional
158
+ * behavior as a config field rather than a separate DSL helper.
159
+ *
160
+ * @internal This type is an implementation detail and may change without notice.
161
+ */
162
+ export interface InterceptConfig<TEnv = any> {
163
+ when?: InterceptWhenFn<TEnv> | InterceptWhenFn<TEnv>[];
164
+ }
165
+
139
166
  /**
140
167
  * Intercept entry stored in EntryData
141
168
  * Contains the slot name, route to intercept, and handler
@@ -158,11 +185,9 @@ export type InterceptEntry = {
158
185
  };
159
186
 
160
187
  export interface ParallelEntryData
161
- extends EntryPropCommon, EntryPropDatas, EntryPropSegments {
188
+ extends EntryPropCommon, EntryPropDatas, EntryPropSegments, EntryPropRender {
162
189
  type: "parallel";
163
190
  handler: Record<`@${string}`, Handler<any, any, any> | ReactNode>;
164
- loading?: ReactNode | false;
165
- transition?: TransitionConfig;
166
191
  /** Set when any parallel slot is a Static definition */
167
192
  isStaticPrerender?: true;
168
193
  /** Per-slot static handler $$ids for build-time store lookup */
@@ -171,6 +196,13 @@ export interface ParallelEntryData
171
196
 
172
197
  export type ParallelEntries = Partial<Record<`@${string}`, ParallelEntryData>>;
173
198
 
199
+ /**
200
+ * This entry's own structural children plus its owned loaders. `loader` lives
201
+ * here (not in EntryPropDatas) because loaders are owned by the entry, not
202
+ * inherited from ancestors.
203
+ *
204
+ * @internal This type is an implementation detail and may change without notice.
205
+ */
174
206
  export type EntryPropSegments = {
175
207
  loader: LoaderEntry[];
176
208
  layout: EntryData[];
@@ -182,8 +214,6 @@ export type EntryData =
182
214
  | ({
183
215
  type: "route";
184
216
  handler: Handler<any, any, any>;
185
- loading?: ReactNode | false;
186
- transition?: TransitionConfig;
187
217
  /** URL pattern for this route (used by path() in urls()) */
188
218
  pattern?: string;
189
219
  /** Set when handler is a Prerender definition */
@@ -191,8 +221,12 @@ export type EntryData =
191
221
  /** Original PrerenderHandlerDefinition (for build-time getParams access) */
192
222
  prerenderDef?: {
193
223
  getParams?: (ctx: any) => Promise<any[]> | any[];
194
- options?: { passthrough?: boolean };
224
+ options?: { concurrency?: number };
195
225
  };
226
+ /** Set when route is wrapped with Passthrough() — has a separate live handler */
227
+ isPassthrough?: true;
228
+ /** Live handler for runtime fallback (only set on Passthrough routes) */
229
+ liveHandler?: Handler<any, any, any>;
196
230
  /** Set when handler is a Static definition (build-time only) */
197
231
  isStaticPrerender?: true;
198
232
  /** Static handler $$id for build-time store lookup */
@@ -201,29 +235,28 @@ export type EntryData =
201
235
  responseType?: string;
202
236
  } & EntryPropCommon &
203
237
  EntryPropDatas &
204
- EntryPropSegments)
238
+ EntryPropSegments &
239
+ EntryPropRender)
205
240
  | ({
206
241
  type: "layout";
207
242
  handler: ReactNode | Handler<any, any, any>;
208
- loading?: ReactNode | false;
209
- transition?: TransitionConfig;
210
243
  /** Set when handler is a Static definition (build-time only) */
211
244
  isStaticPrerender?: true;
212
245
  /** Static handler $$id for build-time store lookup */
213
246
  staticHandlerId?: string;
214
247
  } & EntryPropCommon &
215
248
  EntryPropDatas &
216
- EntryPropSegments)
249
+ EntryPropSegments &
250
+ EntryPropRender)
217
251
  | ParallelEntryData
218
252
  | ({
219
253
  type: "cache";
220
254
  /** Cache entries create cache boundaries and render like layouts (with Outlet) */
221
255
  handler: ReactNode | Handler<any, any, any>;
222
- loading?: ReactNode | false;
223
- transition?: TransitionConfig;
224
256
  } & EntryPropCommon &
225
257
  EntryPropDatas &
226
- EntryPropSegments);
258
+ EntryPropSegments &
259
+ EntryPropRender);
227
260
 
228
261
  /**
229
262
  * Tracked include info for build-time manifest generation
@@ -276,6 +309,22 @@ interface HelperContext {
276
309
  /** True when resolving handlers inside a cache() DSL boundary.
277
310
  * Read by ctx.get() to guard non-cacheable variable reads. */
278
311
  insideCacheScope?: boolean;
312
+ /**
313
+ * Include scope string applied to direct-descendant shortCodes.
314
+ *
315
+ * Each `include(...)` call allocates a sibling-positional token like `I0`,
316
+ * `I1` from its parent's include counter and stores the composed scope
317
+ * (`${parentScope}I${idx}`) in its lazyContext. When the include's handler
318
+ * evaluates lazily, the store's `includeScope` is set from that context so
319
+ * every direct-descendant shortCode is generated as
320
+ * `${parent.shortCode}${includeScope}${prefix}${index}` — preventing
321
+ * collisions with siblings declared outside the include.
322
+ *
323
+ * The scope is NOT propagated through `store.run(...)`, so layouts /
324
+ * parallels / caches inside the include absorb the scope into their own
325
+ * shortCodes and their children start fresh.
326
+ */
327
+ includeScope?: string;
279
328
  }
280
329
  // Use a global symbol key so the AsyncLocalStorage instance survives HMR
281
330
  // module re-evaluation. Without this, Vite's RSC module runner may create
@@ -283,10 +332,28 @@ interface HelperContext {
283
332
  // hold references to the old instance — causing getStore() to return
284
333
  // undefined even inside a run() callback.
285
334
  const RSC_CONTEXT_KEY = Symbol.for("rangojs-router:rsc-context");
286
- export const RSCRouterContext: AsyncLocalStorage<HelperContext> = ((
335
+ export const RangoContext: AsyncLocalStorage<HelperContext> = ((
287
336
  globalThis as any
288
337
  )[RSC_CONTEXT_KEY] ??= new AsyncLocalStorage<HelperContext>());
289
338
 
339
+ /** shortCode prefix letter per entry type (e.g. "L0", "R2", "M1C0"). */
340
+ const SHORT_CODE_PREFIX: Record<
341
+ "layout" | "parallel" | "route" | "loader" | "cache",
342
+ string
343
+ > = {
344
+ layout: "L",
345
+ parallel: "P",
346
+ route: "R",
347
+ loader: "D",
348
+ cache: "C",
349
+ };
350
+
351
+ /** Post-increment a named per-store counter, returning the prior value. */
352
+ function bumpCounter(store: HelperContext, key: string): number {
353
+ store.counters[key] ??= 0;
354
+ return store.counters[key]++;
355
+ }
356
+
290
357
  export const getContext = (): {
291
358
  context: AsyncLocalStorage<HelperContext>;
292
359
  getStore: () => HelperContext;
@@ -310,12 +377,12 @@ export const getContext = (): {
310
377
  callback: (...args: any[]) => T,
311
378
  ) => T;
312
379
  } => {
313
- const context = RSCRouterContext;
380
+ const context = RangoContext;
314
381
 
315
382
  return {
316
383
  context,
317
384
  getOrCreateStore: (forRoute?: string): HelperContext => {
318
- let store = RSCRouterContext.getStore();
385
+ let store = RangoContext.getStore();
319
386
  if (!store) {
320
387
  store = {
321
388
  manifest: new Map<string, EntryData>(),
@@ -335,7 +402,7 @@ export const getContext = (): {
335
402
  const store = context.getStore();
336
403
  if (!store) {
337
404
  throw new Error(
338
- "RSC Router context store is not available. Make sure to run within RSC Router context.",
405
+ "Rango context store is not available. Make sure to run within Rango context.",
339
406
  );
340
407
  }
341
408
  return store;
@@ -352,48 +419,36 @@ export const getContext = (): {
352
419
  type: (string & {}) | "layout" | "parallel" | "middleware" | "revalidate",
353
420
  ) => {
354
421
  const store = context.getStore();
355
- invariant(store, "No context RSCRouterContext available");
356
- store.counters[type] ??= 0;
357
- const index = store.counters[type];
358
- store.counters[type] = index + 1;
359
- return `$${type}.${index}`;
422
+ invariant(store, "No context RangoContext available");
423
+ return `$${type}.${bumpCounter(store, type)}`;
360
424
  },
361
425
  getShortCode: (
362
426
  type: "layout" | "parallel" | "route" | "loader" | "cache",
363
427
  ) => {
364
428
  const store = context.getStore();
365
- invariant(store, "No context RSCRouterContext available");
429
+ invariant(store, "No context RangoContext available");
366
430
 
367
431
  const parent = store.parent;
368
- const prefix =
369
- type === "layout"
370
- ? "L"
371
- : type === "parallel"
372
- ? "P"
373
- : type === "loader"
374
- ? "D"
375
- : type === "cache"
376
- ? "C"
377
- : "R";
432
+ const prefix = SHORT_CODE_PREFIX[type];
378
433
  const mountPrefix =
379
434
  store.mountIndex !== undefined ? `M${store.mountIndex}` : "";
380
435
 
436
+ const includeScope = store.includeScope ?? "";
437
+
381
438
  if (!parent) {
382
439
  // Root entry: prefix with mount index and use mount-scoped counter
383
440
  const counterKey = mountPrefix
384
441
  ? `${mountPrefix}_root_${type}`
385
442
  : `root_${type}`;
386
- store.counters[counterKey] ??= 0;
387
- const index = store.counters[counterKey];
388
- store.counters[counterKey] = index + 1;
389
- return `${mountPrefix}${prefix}${index}`;
443
+ return `${mountPrefix}${prefix}${bumpCounter(store, counterKey)}`;
390
444
  } else {
391
- // Child entry: use parent-scoped counter (parent already has M prefix)
392
- const counterKey = `${parent.shortCode}_${type}`;
393
- store.counters[counterKey] ??= 0;
394
- const index = store.counters[counterKey];
395
- store.counters[counterKey] = index + 1;
396
- return `${parent.shortCode}${prefix}${index}`;
445
+ // Child entry: use parent-scoped counter with includeScope appended.
446
+ // When we're evaluating a lazy include's direct children, includeScope
447
+ // is a per-include token like "I0" / "I1I0" that partitions the
448
+ // parent's counter namespace so routes inside one include cannot
449
+ // collide with siblings declared outside it.
450
+ const counterKey = `${parent.shortCode}${includeScope}_${type}`;
451
+ return `${parent.shortCode}${includeScope}${prefix}${bumpCounter(store, counterKey)}`;
397
452
  }
398
453
  },
399
454
  runWithStore: <T>(
@@ -420,6 +475,7 @@ export const getContext = (): {
420
475
  rootScoped: store.rootScoped,
421
476
  trackedIncludes: store.trackedIncludes,
422
477
  cacheProfiles: store.cacheProfiles,
478
+ includeScope: store.includeScope,
423
479
  },
424
480
  callback,
425
481
  );
@@ -466,6 +522,31 @@ export const getContext = (): {
466
522
  };
467
523
  };
468
524
 
525
+ /**
526
+ * Acquire the active DSL build context, throwing `message` if a helper was
527
+ * called outside a urls()/map() builder. Returns the store API and the live
528
+ * HelperContext so callers avoid a second getContext() lookup.
529
+ */
530
+ export function requireDslContext(message: string): {
531
+ store: ReturnType<typeof getContext>;
532
+ ctx: HelperContext;
533
+ } {
534
+ const store = getContext();
535
+ const ctx = store.context.getStore();
536
+ if (!ctx) {
537
+ // The only reason the store is absent here is that a route-definition helper
538
+ // ran with no active RangoContext — i.e. outside a urls()/map() builder.
539
+ // Record that as the cause so the throw is self-explanatory, not a bare
540
+ // "must be called inside urls()" with no indication of the mechanism.
541
+ throw new DslContextError(message, {
542
+ cause:
543
+ "RangoContext store is undefined: a route-definition helper was called " +
544
+ "outside an active urls()/map() builder.",
545
+ });
546
+ }
547
+ return { store, ctx };
548
+ }
549
+
469
550
  /**
470
551
  * Run a callback with specific URL and name prefixes
471
552
  * Used by include() to apply prefixes to nested patterns
@@ -475,7 +556,7 @@ export function runWithPrefixes<T>(
475
556
  namePrefix: string | undefined,
476
557
  callback: () => T,
477
558
  ): T {
478
- const store = RSCRouterContext.getStore();
559
+ const store = RangoContext.getStore();
479
560
  if (!store) {
480
561
  throw new Error("runWithPrefixes must be called within router context");
481
562
  }
@@ -520,7 +601,7 @@ export function runWithPrefixes<T>(
520
601
  ? (store.rootScoped ?? false)
521
602
  : store.rootScoped;
522
603
 
523
- return RSCRouterContext.run(
604
+ return RangoContext.run(
524
605
  {
525
606
  ...store,
526
607
  urlPrefix: combinedUrlPrefix,
@@ -535,7 +616,7 @@ export function runWithPrefixes<T>(
535
616
  * Get current URL prefix from context
536
617
  */
537
618
  export function getUrlPrefix(): string {
538
- const store = RSCRouterContext.getStore();
619
+ const store = RangoContext.getStore();
539
620
  return store?.urlPrefix || "";
540
621
  }
541
622
 
@@ -543,7 +624,7 @@ export function getUrlPrefix(): string {
543
624
  * Get current name prefix from context
544
625
  */
545
626
  export function getNamePrefix(): string | undefined {
546
- const store = RSCRouterContext.getStore();
627
+ const store = RangoContext.getStore();
547
628
  return store?.namePrefix;
548
629
  }
549
630
 
@@ -552,7 +633,7 @@ export function getNamePrefix(): string | undefined {
552
633
  * Returns true at root or inside { name: "" } includes, false inside named includes.
553
634
  */
554
635
  export function getRootScoped(): boolean {
555
- const store = RSCRouterContext.getStore();
636
+ const store = RangoContext.getStore();
556
637
  return store?.rootScoped ?? true;
557
638
  }
558
639
 
@@ -649,7 +730,7 @@ export function getParallelSlotCount(
649
730
  * ```
650
731
  */
651
732
  export function track(label: string, depth?: number): () => void {
652
- const store = RSCRouterContext.getStore();
733
+ const store = RangoContext.getStore();
653
734
 
654
735
  // No-op if context unavailable or metrics not enabled
655
736
  if (!store?.metrics?.enabled) {
@@ -670,11 +751,102 @@ export function track(label: string, depth?: number): () => void {
670
751
  };
671
752
  }
672
753
 
754
+ /**
755
+ * Separate ALS for tracking loader execution scope.
756
+ * Uses a dedicated ALS (not RangoContext) to avoid issues with
757
+ * nested RangoContext.run() calls in Vite's module runner.
758
+ */
759
+ const LOADER_SCOPE_KEY = Symbol.for("rangojs-router:loader-scope");
760
+ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
761
+ globalThis as any
762
+ )[LOADER_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
763
+
764
+ // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
765
+ // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
766
+ // invoked via ctx.use). Consulted ONLY by isInsideCacheScope() to exempt
767
+ // request-scoped reads. It deliberately does NOT affect isInsideLoaderScope(),
768
+ // so rendered()/barrier/deadlock gating (which must distinguish DSL from
769
+ // handler-invoked loaders) is unchanged.
770
+ const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
771
+ const loaderBodyScopeALS: AsyncLocalStorage<{ active: true }> = ((
772
+ globalThis as any
773
+ )[LOADER_BODY_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
774
+
673
775
  /**
674
776
  * Check if the current execution is inside a cache() DSL boundary.
675
777
  * Returns false inside loader execution — loaders are always fresh
676
778
  * (never cached), so non-cacheable reads are safe.
677
779
  */
678
780
  export function isInsideCacheScope(): boolean {
679
- return RSCRouterContext.getStore()?.insideCacheScope === true;
781
+ if (RangoContext.getStore()?.insideCacheScope !== true) return false;
782
+ // Loaders are always fresh — even inside a cache() boundary, the loader
783
+ // function re-executes on every request. Skip the guard when running
784
+ // inside a loader.
785
+ if (loaderScopeALS.getStore()?.active) return false;
786
+ // Also exempt handler-invoked loaders: their bodies run in a loader-body
787
+ // scope (not the DSL loader scope above), so request-scoped reads inside any
788
+ // loader — however invoked — are safe (loaders always re-run fresh).
789
+ if (loaderBodyScopeALS.getStore()?.active) return false;
790
+ return true;
791
+ }
792
+
793
+ /**
794
+ * Check if the current execution is inside a DSL loader scope
795
+ * (wrapped by runInsideLoaderScope). Used by rendered() barrier
796
+ * to distinguish DSL loaders from handler-invoked loaders.
797
+ */
798
+ export function isInsideLoaderScope(): boolean {
799
+ return loaderScopeALS.getStore()?.active === true;
800
+ }
801
+
802
+ /**
803
+ * Run `fn` inside a loader scope. While active, cache-scope guards
804
+ * are bypassed because loaders are always fresh (never cached) and
805
+ * their side effects (setCookie, header, etc.) are safe.
806
+ */
807
+ export function runInsideLoaderScope<T>(fn: () => T): T {
808
+ return loaderScopeALS.run({ active: true }, fn);
809
+ }
810
+
811
+ /**
812
+ * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
813
+ * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
814
+ * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
815
+ * and handler-invoked via ctx.use) so request-scoped reads inside a loader
816
+ * never trip the cache-scope guards — loaders always run fresh.
817
+ */
818
+ export function runInsideLoaderBodyScope<T>(fn: () => T): T {
819
+ return loaderBodyScopeALS.run({ active: true }, fn);
820
+ }
821
+
822
+ // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
823
+ // A push callback's value is stored as-is; if it is a promise it is NOT tracked
824
+ // by handleStore.settled and does not block segment resolution, so a
825
+ // ctx.use(loader) made from inside such a callback can never form a rendered()
826
+ // deadlock. This is an ALS (not a plain boolean) so the exemption survives the
827
+ // callback's own awaits — an async push callback that resumes after `await`
828
+ // still reads as "inside a push callback" and stays out of the deadlock guard.
829
+ const PUSH_CALLBACK_SCOPE_KEY = Symbol.for(
830
+ "rangojs-router:push-callback-scope",
831
+ );
832
+ const pushCallbackScopeALS: AsyncLocalStorage<{ active: true }> = ((
833
+ globalThis as any
834
+ )[PUSH_CALLBACK_SCOPE_KEY] ??= new AsyncLocalStorage<{ active: true }>());
835
+
836
+ /**
837
+ * Check if the current execution is inside a handle push callback (sync or an
838
+ * async callback's continuation). Used by the handler-to-loader deadlock guard
839
+ * to exempt push-callback continuations.
840
+ */
841
+ export function isInsidePushCallbackScope(): boolean {
842
+ return pushCallbackScopeALS.getStore()?.active === true;
843
+ }
844
+
845
+ /**
846
+ * Run `fn` inside a push-callback scope. Wraps the invocation of a handle push
847
+ * callback so that any ctx.use(loader) it makes — including after one of its own
848
+ * awaits — is exempt from the deadlock guard.
849
+ */
850
+ export function runInsidePushCallbackScope<T>(fn: () => T): T {
851
+ return pushCallbackScopeALS.run({ active: true }, fn);
680
852
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Canonical inbound-Cookie-header parser.
3
+ *
4
+ * Kept as a dependency-free leaf so any consumer (request-context, the host
5
+ * dispatcher, tests) can share one implementation without pulling a heavier
6
+ * module's graph. A duplicate copy in middleware-cookies.ts was removed; the
7
+ * host copy in cookie-handler.ts was collapsed onto this one. Not part of the
8
+ * public export surface.
9
+ */
10
+ export function parseCookiesFromHeader(
11
+ cookieHeader: string | null,
12
+ ): Record<string, string> {
13
+ if (!cookieHeader) return {};
14
+
15
+ const cookies: Record<string, string> = {};
16
+ const pairs = cookieHeader.split(";");
17
+
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;
27
+ }
28
+ }
29
+ }
30
+
31
+ return cookies;
32
+ }
@@ -8,7 +8,8 @@
8
8
  */
9
9
 
10
10
  import type { CookieOptions } from "../router/middleware-types.js";
11
- import { getRequestContext } from "./request-context.js";
11
+ import { getRequestContext, _getRequestContext } from "./request-context.js";
12
+ import { isInsideCacheScope } from "./context.js";
12
13
  import { INSIDE_CACHE_EXEC } from "../cache/taint.js";
13
14
 
14
15
  /**
@@ -84,10 +85,23 @@ export interface ReadonlyHeaders {
84
85
  type HeadersIterator<T> = IterableIterator<T>;
85
86
 
86
87
  /**
87
- * Throw if called inside a "use cache" function.
88
- * Reading request-scoped data (cookies, headers) inside a cached function
89
- * produces results that vary per request but the cache key does not include
90
- * those values, leading to one user's data being served to another.
88
+ * Throw if called inside a cache boundary — either a "use cache" function
89
+ * (`INSIDE_CACHE_EXEC` stamped on ctx by the cache runtime) or a `cache()`
90
+ * DSL boundary (`isInsideCacheScope()` the render-store flag set while
91
+ * resolving a `type: "cache"` route entry).
92
+ *
93
+ * Reading request-scoped data (cookies, headers) inside a cached scope
94
+ * produces per-request values that are NOT reflected in the cache key, so
95
+ * they would be frozen into the shared cache entry and served to the wrong
96
+ * users. This is the same hazard for both scopes: a `cache()` boundary caches
97
+ * everything except loaders (it is the document-level "PPR shell"), so a read
98
+ * here is baked into the shell exactly like a `"use cache"` return value is
99
+ * baked into its cache entry.
100
+ *
101
+ * `isInsideCacheScope()` returns false inside loaders (loaders always run
102
+ * fresh on every request, even on a cache hit), so reading cookies()/headers()
103
+ * from a loader is allowed — loaders are the dynamic "holes" of a cached
104
+ * document.
91
105
  */
92
106
  function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
93
107
  if (
@@ -106,6 +120,16 @@ function assertNotInsideCacheContext(ctx: unknown, fnName: string): void {
106
120
  ` const data = await getCachedData(locale); // locale is now in the cache key`,
107
121
  );
108
122
  }
123
+ if (isInsideCacheScope()) {
124
+ throw new Error(
125
+ `${fnName}() cannot be called inside a cache() boundary. ` +
126
+ `A cache() scope caches everything except loaders, so request-scoped ` +
127
+ `data (cookies, headers) read here would be frozen into the shared ` +
128
+ `cached shell and served to other users. Read it inside a loader ` +
129
+ `instead — loaders always run fresh on every request, even on a cache hit:\n\n` +
130
+ ` loader("user", () => getUser(cookies().get("session")?.value));`,
131
+ );
132
+ }
109
133
  }
110
134
 
111
135
  const HEADERS_MUTATION_METHODS = new Set(["set", "append", "delete"]);
@@ -144,6 +168,57 @@ export function headers(): ReadonlyHeaders {
144
168
  }) as unknown as ReadonlyHeaders;
145
169
  }
146
170
 
171
+ /**
172
+ * Force the calling client's caches to miss from now on, from the server seat:
173
+ * write a rotated `Set-Cookie` for the rango state. The responding client
174
+ * applies it on receipt, and its history cache is marked stale by the
175
+ * jar-divergence observer at its next read. Per-client and lazy — it rotates
176
+ * only the client that receives this response, not every client.
177
+ *
178
+ * Idempotent within a request (one `Set-Cookie`). Inert (a dev warning) when
179
+ * called outside a request context. Like `cookies()`, it throws inside a
180
+ * `"use cache"` / `cache()` boundary, but is allowed from a loader (loaders are
181
+ * the dynamic holes of a cached document).
182
+ */
183
+ export function invalidateClientCache(): void {
184
+ const ctx = _getRequestContext();
185
+ if (!ctx) {
186
+ if (process.env.NODE_ENV !== "production") {
187
+ console.warn(
188
+ "[rango] invalidateClientCache() was called outside a request context; ignored.",
189
+ );
190
+ }
191
+ return;
192
+ }
193
+ assertNotInsideCacheContext(ctx, "invalidateClientCache");
194
+ ctx._rotateStateCookie();
195
+ }
196
+
197
+ /**
198
+ * Suppress a server action's automatic client-cache invalidation: tell the
199
+ * action bridge this action changed nothing a route renders, so it should leave
200
+ * the client's state and caches alone (no rotation, no prefetch wipe, no
201
+ * broadcast, no revalidation refetch). Per-response, not per-action-definition —
202
+ * only the execution knows whether anything changed.
203
+ *
204
+ * Sets an internal response header the bridge reads. Idempotent within a
205
+ * request. Inert (a dev warning) outside a request context — there is no
206
+ * automatic invalidation to suppress.
207
+ */
208
+ export function keepClientCache(): void {
209
+ const ctx = _getRequestContext();
210
+ if (!ctx) {
211
+ if (process.env.NODE_ENV !== "production") {
212
+ console.warn(
213
+ "[rango] keepClientCache() was called outside a request context; ignored.",
214
+ );
215
+ }
216
+ return;
217
+ }
218
+ assertNotInsideCacheContext(ctx, "keepClientCache");
219
+ ctx._setKeepCacheDirective();
220
+ }
221
+
147
222
  /**
148
223
  * Create a CookieStore backed by a RequestContext.
149
224
  * @internal Shared between cookies() shorthand and context methods.