@rangojs/router 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/README.md +3 -2
  2. package/dist/types/browser/cookie-name.d.ts +6 -0
  3. package/dist/types/browser/event-controller.d.ts +3 -3
  4. package/dist/types/browser/history-state.d.ts +18 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  6. package/dist/types/browser/partial-update.d.ts +16 -1
  7. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  8. package/dist/types/browser/react/Link.d.ts +19 -9
  9. package/dist/types/browser/react/context.d.ts +2 -2
  10. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  11. package/dist/types/browser/react/location-state.d.ts +1 -1
  12. package/dist/types/browser/transition-when.d.ts +80 -0
  13. package/dist/types/browser/types.d.ts +104 -9
  14. package/dist/types/build/generate-manifest.d.ts +8 -0
  15. package/dist/types/cache/background-task.d.ts +24 -2
  16. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  17. package/dist/types/cache/cache-key-utils.d.ts +48 -2
  18. package/dist/types/cache/cache-policy.d.ts +18 -1
  19. package/dist/types/cache/cache-runtime.d.ts +7 -0
  20. package/dist/types/cache/cache-scope.d.ts +192 -26
  21. package/dist/types/cache/cache-tag.d.ts +116 -11
  22. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  23. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  24. package/dist/types/cache/cf/cf-cache-store.d.ts +224 -72
  25. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  26. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  27. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +19 -0
  28. package/dist/types/cache/document-cache.d.ts +10 -0
  29. package/dist/types/cache/handle-snapshot.d.ts +41 -8
  30. package/dist/types/cache/index.d.ts +1 -0
  31. package/dist/types/cache/invalidation-order.d.ts +23 -0
  32. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  33. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  34. package/dist/types/cache/shell-memo.d.ts +178 -0
  35. package/dist/types/cache/shell-snapshot.d.ts +141 -105
  36. package/dist/types/cache/tag-invalidation.d.ts +54 -3
  37. package/dist/types/cache/types.d.ts +267 -96
  38. package/dist/types/cache/vercel/vercel-cache-store.d.ts +123 -18
  39. package/dist/types/client-urls/navigation.d.ts +28 -2
  40. package/dist/types/client-urls/server-projection.d.ts +5 -2
  41. package/dist/types/client-urls/types.d.ts +5 -5
  42. package/dist/types/client.d.ts +1 -1
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +1 -1
  45. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  46. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  47. package/dist/types/route-name.d.ts +7 -0
  48. package/dist/types/router/error-handling.d.ts +1 -1
  49. package/dist/types/router/instrument.d.ts +8 -0
  50. package/dist/types/router/intercept-resolution.d.ts +2 -0
  51. package/dist/types/router/match-context.d.ts +5 -0
  52. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  53. package/dist/types/router/middleware-types.d.ts +6 -1
  54. package/dist/types/router/router-options.d.ts +4 -1
  55. package/dist/types/router/segment-resolution/loader-cache.d.ts +58 -12
  56. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  57. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +33 -48
  58. package/dist/types/router/segment-resolution/mask-nested.d.ts +73 -24
  59. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  60. package/dist/types/router/transition-when.d.ts +24 -10
  61. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  62. package/dist/types/rsc/full-payload.d.ts +21 -0
  63. package/dist/types/rsc/handler.d.ts +1 -1
  64. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  65. package/dist/types/rsc/shell-build-manifest.d.ts +12 -3
  66. package/dist/types/rsc/shell-capture-constants.d.ts +116 -12
  67. package/dist/types/rsc/shell-capture.d.ts +202 -74
  68. package/dist/types/rsc/shell-serve.d.ts +144 -50
  69. package/dist/types/rsc/types.d.ts +9 -0
  70. package/dist/types/segment-system.d.ts +7 -0
  71. package/dist/types/server/context.d.ts +155 -33
  72. package/dist/types/server/cookie-store.d.ts +33 -0
  73. package/dist/types/server/handle-store.d.ts +52 -1
  74. package/dist/types/server/request-context.d.ts +210 -63
  75. package/dist/types/server/request-headers.d.ts +44 -0
  76. package/dist/types/server.d.ts +1 -0
  77. package/dist/types/ssr/index.d.ts +9 -0
  78. package/dist/types/ssr/ssr-root.d.ts +9 -8
  79. package/dist/types/testing/e2e/index.d.ts +1 -1
  80. package/dist/types/testing/flight.entry.d.ts +7 -0
  81. package/dist/types/testing/index.d.ts +3 -2
  82. package/dist/types/testing/internal/action-id.d.ts +7 -0
  83. package/dist/types/testing/location-state-key.d.ts +24 -0
  84. package/dist/types/testing/render-route.d.ts +17 -1
  85. package/dist/types/testing/run-loader.d.ts +18 -3
  86. package/dist/types/testing/run-transition-when.d.ts +44 -69
  87. package/dist/types/testing/serve-shell-request.d.ts +133 -0
  88. package/dist/types/testing/shell-status.d.ts +44 -19
  89. package/dist/types/transition-when-ref.d.ts +79 -0
  90. package/dist/types/types/cache-types.d.ts +15 -3
  91. package/dist/types/types/handler-context.d.ts +14 -1
  92. package/dist/types/types/index.d.ts +1 -1
  93. package/dist/types/types/segments.d.ts +86 -71
  94. package/dist/types/urls/path-helper-types.d.ts +3 -4
  95. package/dist/types/urls/pattern-types.d.ts +21 -18
  96. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  97. package/dist/types/vite/router-discovery.d.ts +29 -2
  98. package/dist/vite/index.js +1005 -105
  99. package/package.json +1 -1
  100. package/skills/breadcrumbs/SKILL.md +17 -7
  101. package/skills/cache-guide/SKILL.md +175 -76
  102. package/skills/caching/SKILL.md +361 -33
  103. package/skills/catalog.json +1 -1
  104. package/skills/client-urls/SKILL.md +12 -7
  105. package/skills/cloudflare/SKILL.md +15 -0
  106. package/skills/comparison/references/framework-comparison.md +52 -37
  107. package/skills/defer-hydration/SKILL.md +50 -30
  108. package/skills/deployment-caching/SKILL.md +5 -2
  109. package/skills/document-cache/SKILL.md +32 -2
  110. package/skills/handler-use/SKILL.md +2 -0
  111. package/skills/hooks/SKILL.md +3 -1
  112. package/skills/hooks/handle-and-actions.md +3 -1
  113. package/skills/hooks/navigation.md +23 -2
  114. package/skills/hooks/state.md +101 -7
  115. package/skills/intercept/SKILL.md +29 -12
  116. package/skills/loader/SKILL.md +197 -44
  117. package/skills/middleware/SKILL.md +34 -9
  118. package/skills/migrate-nextjs/SKILL.md +68 -37
  119. package/skills/migrate-react-router/SKILL.md +2 -1
  120. package/skills/migrate-react-router/component-migration.md +3 -1
  121. package/skills/migrate-react-router/data-and-actions.md +3 -1
  122. package/skills/migrate-react-router/route-mapping.md +7 -4
  123. package/skills/observability/SKILL.md +65 -0
  124. package/skills/parallel/SKILL.md +13 -8
  125. package/skills/ppr/SKILL.md +715 -371
  126. package/skills/prerender/SKILL.md +30 -17
  127. package/skills/rango/SKILL.md +72 -34
  128. package/skills/route/SKILL.md +14 -7
  129. package/skills/router-setup/SKILL.md +4 -2
  130. package/skills/scripts/SKILL.md +8 -1
  131. package/skills/server-actions/SKILL.md +11 -0
  132. package/skills/shell-manifest/SKILL.md +21 -12
  133. package/skills/testing/SKILL.md +49 -43
  134. package/skills/testing/cache-prerender.md +15 -14
  135. package/skills/testing/client-components.md +45 -3
  136. package/skills/testing/loader.md +19 -1
  137. package/skills/testing/middleware.md +1 -0
  138. package/skills/testing/predicates.md +49 -49
  139. package/skills/testing/render-handler.md +1 -0
  140. package/skills/testing/serve-shell-request.md +202 -0
  141. package/skills/testing/server-actions.md +9 -3
  142. package/skills/theme/SKILL.md +56 -4
  143. package/skills/typesafety/env-and-bindings.md +18 -2
  144. package/skills/use-cache/SKILL.md +91 -23
  145. package/skills/vercel/SKILL.md +14 -2
  146. package/skills/view-transitions/SKILL.md +150 -29
  147. package/src/__augment-tests__/augmented.check.ts +23 -0
  148. package/src/browser/cookie-name.ts +23 -0
  149. package/src/browser/event-controller.ts +4 -4
  150. package/src/browser/history-state.ts +75 -1
  151. package/src/browser/link-interceptor.ts +4 -0
  152. package/src/browser/navigation-bridge.ts +77 -18
  153. package/src/browser/navigation-client.ts +4 -3
  154. package/src/browser/navigation-store.ts +43 -1
  155. package/src/browser/navigation-transaction.ts +31 -11
  156. package/src/browser/partial-update.ts +146 -35
  157. package/src/browser/prefetch/cache.ts +6 -6
  158. package/src/browser/prefetch/fetch.ts +2 -2
  159. package/src/browser/react/Link.tsx +38 -33
  160. package/src/browser/react/NavigationProvider.tsx +2 -2
  161. package/src/browser/react/context.ts +2 -2
  162. package/src/browser/react/location-state-shared.ts +241 -49
  163. package/src/browser/react/location-state.ts +5 -2
  164. package/src/browser/rsc-router.tsx +9 -0
  165. package/src/browser/server-action-bridge.ts +84 -15
  166. package/src/browser/transition-when.ts +205 -0
  167. package/src/browser/types.ts +124 -10
  168. package/src/build/generate-manifest.ts +77 -1
  169. package/src/cache/background-task.ts +55 -2
  170. package/src/cache/cache-exec-scope.ts +48 -1
  171. package/src/cache/cache-key-utils.ts +62 -2
  172. package/src/cache/cache-policy.ts +31 -4
  173. package/src/cache/cache-runtime.ts +209 -129
  174. package/src/cache/cache-scope.ts +573 -69
  175. package/src/cache/cache-tag.ts +475 -19
  176. package/src/cache/cf/cf-base64.ts +36 -4
  177. package/src/cache/cf/cf-cache-constants.ts +12 -0
  178. package/src/cache/cf/cf-cache-store.ts +1094 -376
  179. package/src/cache/cf/cf-cache-types.ts +32 -4
  180. package/src/cache/cf/cf-shell-frame.ts +258 -0
  181. package/src/cache/cf/cf-tag-marker-memo.ts +55 -40
  182. package/src/cache/document-cache.ts +53 -7
  183. package/src/cache/handle-snapshot.ts +53 -9
  184. package/src/cache/index.ts +2 -0
  185. package/src/cache/invalidation-order.ts +84 -0
  186. package/src/cache/isolate-tag-memo.ts +308 -0
  187. package/src/cache/request-tag-mask.ts +147 -0
  188. package/src/cache/shell-memo.ts +341 -0
  189. package/src/cache/shell-snapshot.ts +249 -277
  190. package/src/cache/tag-invalidation.ts +109 -18
  191. package/src/cache/types.ts +274 -102
  192. package/src/cache/vercel/vercel-cache-store.ts +561 -74
  193. package/src/client-urls/client-root.tsx +8 -4
  194. package/src/client-urls/client-urls.ts +8 -6
  195. package/src/client-urls/navigation.ts +55 -4
  196. package/src/client-urls/server-projection.ts +31 -10
  197. package/src/client-urls/types.ts +12 -5
  198. package/src/client.tsx +2 -0
  199. package/src/host/cookie-handler.ts +2 -1
  200. package/src/index.rsc.ts +3 -0
  201. package/src/index.ts +3 -0
  202. package/src/loader-redirect.tsx +7 -3
  203. package/src/prerender/build-shell-capture.ts +173 -110
  204. package/src/route-definition/dsl-helpers.ts +6 -3
  205. package/src/route-definition/helpers-types.ts +11 -11
  206. package/src/route-name.ts +12 -0
  207. package/src/router/content-negotiation.ts +4 -1
  208. package/src/router/error-handling.ts +5 -2
  209. package/src/router/handler-context.ts +42 -5
  210. package/src/router/instrument.ts +54 -2
  211. package/src/router/intercept-resolution.ts +30 -8
  212. package/src/router/loader-resolution.ts +89 -33
  213. package/src/router/logging.ts +5 -3
  214. package/src/router/match-api.ts +89 -42
  215. package/src/router/match-context.ts +5 -0
  216. package/src/router/match-handlers.ts +14 -29
  217. package/src/router/match-middleware/background-revalidation.ts +3 -5
  218. package/src/router/match-middleware/cache-lookup.ts +52 -19
  219. package/src/router/match-middleware/cache-store.ts +36 -10
  220. package/src/router/match-result.ts +1 -0
  221. package/src/router/middleware-types.ts +6 -1
  222. package/src/router/middleware.ts +20 -4
  223. package/src/router/navigation-snapshot.ts +6 -5
  224. package/src/router/prerender-match.ts +10 -0
  225. package/src/router/request-classification.ts +7 -4
  226. package/src/router/router-options.ts +4 -1
  227. package/src/router/segment-resolution/fresh.ts +18 -13
  228. package/src/router/segment-resolution/helpers.ts +4 -2
  229. package/src/router/segment-resolution/loader-cache.ts +366 -70
  230. package/src/router/segment-resolution/loader-mask.ts +3 -4
  231. package/src/router/segment-resolution/loader-snapshot.ts +215 -126
  232. package/src/router/segment-resolution/mask-nested.ts +161 -42
  233. package/src/router/segment-resolution/revalidation.ts +27 -13
  234. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  235. package/src/router/telemetry.ts +6 -3
  236. package/src/router/transition-when.ts +34 -52
  237. package/src/router.ts +2 -1
  238. package/src/rsc/attach-transition-when.ts +47 -0
  239. package/src/rsc/full-payload.ts +47 -3
  240. package/src/rsc/handler.ts +24 -10
  241. package/src/rsc/loader-fetch.ts +2 -1
  242. package/src/rsc/manifest-init.ts +4 -0
  243. package/src/rsc/origin-guard.ts +8 -5
  244. package/src/rsc/progressive-enhancement.ts +10 -32
  245. package/src/rsc/response-cache-serve.ts +69 -78
  246. package/src/rsc/response-route-handler.ts +2 -2
  247. package/src/rsc/rsc-rendering.ts +712 -338
  248. package/src/rsc/server-action.ts +8 -20
  249. package/src/rsc/shell-build-manifest.ts +49 -29
  250. package/src/rsc/shell-capture-constants.ts +182 -12
  251. package/src/rsc/shell-capture.ts +1126 -612
  252. package/src/rsc/shell-serve.ts +331 -85
  253. package/src/rsc/ssr-setup.ts +3 -2
  254. package/src/rsc/types.ts +9 -0
  255. package/src/segment-system.tsx +40 -7
  256. package/src/server/context.ts +328 -72
  257. package/src/server/cookie-store.ts +184 -133
  258. package/src/server/handle-store.ts +245 -27
  259. package/src/server/request-context.ts +364 -100
  260. package/src/server/request-headers.ts +134 -0
  261. package/src/server.ts +4 -0
  262. package/src/ssr/index.tsx +96 -37
  263. package/src/ssr/ssr-root.tsx +9 -8
  264. package/src/testing/dispatch.ts +4 -7
  265. package/src/testing/e2e/index.ts +1 -0
  266. package/src/testing/flight.entry.ts +14 -0
  267. package/src/testing/index.ts +5 -0
  268. package/src/testing/internal/action-id.ts +25 -0
  269. package/src/testing/internal/context.ts +1 -1
  270. package/src/testing/location-state-key.ts +45 -0
  271. package/src/testing/render-handler.ts +1 -1
  272. package/src/testing/render-route.tsx +109 -16
  273. package/src/testing/run-client-revalidate.ts +3 -19
  274. package/src/testing/run-loader.ts +67 -5
  275. package/src/testing/run-transition-when.ts +109 -172
  276. package/src/testing/serve-shell-request.ts +381 -0
  277. package/src/testing/shell-status.ts +73 -47
  278. package/src/theme/ThemeProvider.tsx +7 -6
  279. package/src/transition-when-ref.ts +220 -0
  280. package/src/types/cache-types.ts +15 -3
  281. package/src/types/handler-context.ts +14 -1
  282. package/src/types/index.ts +3 -0
  283. package/src/types/segments.ts +94 -91
  284. package/src/urls/path-helper-types.ts +3 -4
  285. package/src/urls/pattern-types.ts +21 -18
  286. package/src/vite/discovery/discover-routers.ts +3 -0
  287. package/src/vite/discovery/shell-prerender-phase.ts +9 -2
  288. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  289. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  290. package/src/vite/rango.ts +2 -0
  291. package/src/vite/router-discovery.ts +89 -9
  292. package/dist/types/rsc/transition-gate.d.ts +0 -23
  293. package/src/rsc/transition-gate.ts +0 -86
@@ -1,3 +1,4 @@
1
+ import type { ServerRouteLocation } from "../types/segments.js";
1
2
  import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import type { ReactNode } from "react";
3
4
  import type {
@@ -13,9 +14,11 @@ import type {
13
14
  import { invariant, DslContextError } from "../errors";
14
15
  import type { DefaultRouteName } from "../types/global-namespace.js";
15
16
  import type { ContextVar } from "../context-var.js";
17
+ import { PPR_LANE_HINT } from "../rsc/shell-capture-constants.js";
16
18
  import {
17
- getCacheExecScope,
18
- type CacheExecScope,
19
+ endIdentityExempt,
20
+ isInsideCacheExecScope,
21
+ isInsideIdentityExempt,
19
22
  } from "../cache/cache-exec-scope.js";
20
23
 
21
24
  // ============================================================================
@@ -134,6 +137,15 @@ export type LoaderEntry = {
134
137
  * appears on navigation-lane entries.
135
138
  */
136
139
  awaitBeforeFlush?: true;
140
+ /**
141
+ * loader(Def, { ssr: false }) on every DSL evaluation, document and
142
+ * navigation: the loader's PPR bake lane (loader-cache.ts
143
+ * resolveLoaderData). The bake/seed key rides on it, so a client
144
+ * navigation's shell replay pins exactly the loaders a document HIT pins,
145
+ * whatever the entry's loading(). awaitBeforeFlush cannot carry that: a
146
+ * navigation evaluation never has it.
147
+ */
148
+ bake?: true;
137
149
  };
138
150
 
139
151
  /**
@@ -153,20 +165,22 @@ export type InterceptSegmentsState = {
153
165
  * Context passed to intercept selector functions (when())
154
166
  * Contains navigation context to determine if interception should occur.
155
167
  *
168
+ * `from` / `to` have the shape transition({ when }) sees, without `state`:
169
+ * history state never reaches the server.
170
+ *
156
171
  * Note: when() is evaluated during route matching, BEFORE middleware runs.
157
172
  * So ctx.get()/ctx.use() are not available, but env (platform bindings) is.
158
173
  *
159
174
  * @internal This type is an implementation detail and may change without notice.
160
175
  */
161
176
  export type InterceptSelectorContext<TEnv = any> = {
162
- from: URL; // Source URL (where user is coming from)
163
- to: URL; // Destination URL (where user is navigating to)
164
- params: Record<string, string>; // Matched route params
177
+ /** Where the navigation comes from: the intercept source while one is open. */
178
+ from: ServerRouteLocation;
179
+ /** The navigation target. */
180
+ to: ServerRouteLocation;
165
181
  request: Request; // The HTTP request object
166
182
  env: TEnv; // Platform bindings (Cloudflare env, etc.)
167
183
  segments: InterceptSegmentsState; // Client's current segments (where navigating FROM)
168
- fromRouteName?: DefaultRouteName; // Named route being navigated away from (undefined for unnamed routes)
169
- toRouteName?: DefaultRouteName; // Named route being navigated to (undefined for unnamed routes)
170
184
  };
171
185
 
172
186
  /**
@@ -536,7 +550,8 @@ export const getContext = (): {
536
550
  // the header guard's middleware exemption depends on the latch dying
537
551
  // with the funnel scope (see assertCachedHeaderWriteAllowed).
538
552
  },
539
- callback,
553
+ // A funnel (its cache() scope) never runs exempt (runIdentityExempt).
554
+ () => endIdentityExempt(callback),
540
555
  );
541
556
  },
542
557
  run: <T>(
@@ -846,20 +861,17 @@ const loaderScopeALS: AsyncLocalStorage<{ active: true }> = ((
846
861
  // Purity-only scope: marks that a loader FUNCTION BODY is executing, regardless
847
862
  // of how the loader was invoked (DSL via runInsideLoaderScope, or handler-
848
863
  // invoked via ctx.use). Consulted by isInsideCacheScope() to exempt
849
- // request-scoped reads, by getCurrentLoaderBodyId() for guard-warning
850
- // attribution, and by isInsideHandlerInvokedLoaderBody() for the
851
- // consumption-lane rule (the shell-capture guard exemption). It deliberately
852
- // does NOT affect isInsideLoaderScope(), so rendered()/barrier/deadlock
853
- // gating (which must distinguish DSL from handler-invoked loaders) is
854
- // unchanged.
864
+ // request-scoped reads, and by getCurrentLoaderBodyId() for guard-warning
865
+ // attribution. It deliberately does NOT affect isInsideLoaderScope(), so
866
+ // rendered()/barrier/deadlock gating (which must distinguish DSL from
867
+ // handler-invoked loaders) is unchanged.
855
868
  interface LoaderBodyScope {
856
869
  active: true;
857
870
  loaderId?: string;
858
- handlerInvoked?: boolean;
859
871
  /** The body scope this one was entered from (a ctx.use(Loader) chain). */
860
872
  parent?: LoaderBodyScope;
861
- /** The "use cache" scope the body was entered in (assertNonCacheableReadAllowed). */
862
- execScope?: CacheExecScope;
873
+ /** The tags this execution records (#964, cache-tag.ts "Recorded-tag sets"). */
874
+ tags?: Set<string>;
863
875
  }
864
876
  const LOADER_BODY_SCOPE_KEY = Symbol.for("rangojs-router:loader-body-scope");
865
877
  const loaderBodyScopeALS: AsyncLocalStorage<LoaderBodyScope> = ((
@@ -868,8 +880,9 @@ const loaderBodyScopeALS: AsyncLocalStorage<LoaderBodyScope> = ((
868
880
 
869
881
  /**
870
882
  * Check if the current execution is inside a cache() DSL boundary.
871
- * Returns false inside loader execution — loaders are always fresh
872
- * (never cached), so non-cacheable reads are safe.
883
+ * Returns false inside loader execution: a route cache() does not store
884
+ * loader values, so non-cacheable reads are safe. A loader bound with its own
885
+ * cache() stores its value; recordLoaderIdentityRead guards that.
873
886
  */
874
887
  export function isInsideCacheScope(): boolean {
875
888
  if (RangoContext.getStore()?.insideCacheScope !== true) return false;
@@ -886,44 +899,273 @@ export function isInsideCacheScope(): boolean {
886
899
  }
887
900
 
888
901
  /**
889
- * Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
890
- * `ctx.set(..., { cache: false })` write). Callers check isNonCacheable() first
891
- * so ordinary reads never reach the scope lookups.
902
+ * What a refused identity read says (guardIdentityRead): its `verb`, and per
903
+ * refusing scope the text after "<surface> cannot be <verb> ...": why the
904
+ * value must not reach that scope, and what to do instead. `fix.warning` is
905
+ * the fix the capture's refusal warning gives (shell-capture.ts
906
+ * refuseOnCaptureGuard).
907
+ */
908
+ export interface IdentityReadWording {
909
+ /** "called" for a function (cookies(), ctx.get()), "read" for a property (the theme getters). */
910
+ verb: LoaderIdentityReadVerb;
911
+ fix: {
912
+ useCache: string;
913
+ cacheScope: string;
914
+ capture: string;
915
+ warning: string;
916
+ };
917
+ /**
918
+ * False when the read is recorded later, by the returned view's read
919
+ * methods (cookies(), headers()): a view taken outside a loader and read
920
+ * inside one still counts, and a cookie write alone is not a read.
921
+ */
922
+ record?: false;
923
+ }
924
+
925
+ /**
926
+ * The one guard every request-identity read goes through: cookies(),
927
+ * headers(), the theme reads (cookie-store.ts readGuardedTheme), a
928
+ * non-cacheable ctx.get() (assertNonCacheableReadAllowed), and the raw reads
929
+ * `ctx.request.headers` and `getRequestContext().cookie()` / `.cookies()`
930
+ * (cookie-store.ts guardRequestHeaders, guardRawCookieRead; #976). Each surface keeps
931
+ * its own wording; the ladder and its exemptions are shared, so the surfaces
932
+ * refuse in exactly the same places:
892
933
  *
893
- * Throws inside a "use cache" body: the key does not include the value, so the
894
- * first caller's value would be stored and served to later callers (#925). A
895
- * loader body entered INSIDE the cached function is exempt, the same baked-copy
896
- * trade isInsideCacheScope() makes for cache(). A cached function called FROM a
897
- * loader is not: the loader re-runs, the cached body does not. The body scope
898
- * records the exec scope it was entered in, so the exemption holds only while
899
- * that is still the innermost one.
934
+ * 0. A cache's own key(), store keyGenerator, condition() and tags(), and
935
+ * onError, read freely and record nothing (runIdentityExempt,
936
+ * cache/cache-exec-scope.ts): the value picks or labels the entry, or is
937
+ * only observed; it is never rendered. A cached body, loader body or
938
+ * funnel they start is guarded again.
939
+ * 1. A PPR shell capture (`ctx` is the capture's derived context,
940
+ * `_shellCaptureRun`) trips first: the capture context is flagged (so a
941
+ * caught throw still refuses the capture) and the read throws. The shell is
942
+ * shared per host+URL, and every HIT replays what the capture read. There
943
+ * is no loader-body exemption here: a bake-lane loader and a loader a
944
+ * handler awaits both bake.
945
+ * 2. A "use cache" body throws: the key does not include the value, so the
946
+ * first caller's would be stored and served to later callers. That holds
947
+ * for a loader body entered inside the cached function too (`await
948
+ * ctx.use(Loader)` there): its value is part of what the function returns.
949
+ * Before, a non-cacheable ctx.get() there was exempt while cookies() threw,
950
+ * and the entry stored the first request's value.
951
+ * 3. A cache() boundary throws, except inside a loader body
952
+ * (isInsideCacheScope): a route cache() never stores loader values.
953
+ * 4. The read is allowed, and recorded on the current loader execution for a
954
+ * loader cache() fill without key() (#972, recordLoaderIdentityRead)
955
+ * unless `wording.record` defers it to the returned view.
956
+ *
957
+ * Outside these scopes every read is allowed, a live loader's included. The
958
+ * response directives (invalidateClientCache(), keepClientCache()) record
959
+ * nothing and are not captured reads: they take refuseInCacheScope alone.
900
960
  */
901
- export function assertNonCacheableReadAllowed(
902
- keyOrVar: string | ContextVar<unknown>,
961
+ export function guardIdentityRead(
962
+ ctx: unknown,
963
+ surface: string,
964
+ wording: IdentityReadWording,
903
965
  ): void {
904
- const execScope = getCacheExecScope();
905
- if (
906
- execScope !== undefined &&
907
- loaderBodyScopeALS.getStore()?.execScope !== execScope
908
- ) {
909
- const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
966
+ if (isInsideIdentityExempt()) return;
967
+ const { verb, fix } = wording;
968
+ if (tripShellCaptureGuard(ctx, surface, fix.warning)) {
910
969
  throw new Error(
911
- `ctx.get() for a non-cacheable variable${name} cannot be called inside a "use cache" function. ` +
912
- `The variable was created with { cache: false } or set with { cache: false }, ` +
913
- `and the cache key does not include its value, so the first caller's value ` +
914
- `would be served to later callers. Read it before calling the cached function ` +
915
- `and pass the value in as an argument so it becomes part of the cache key.`,
970
+ `${surface} cannot be ${verb} while capturing a shared shell ` +
971
+ `(ppr shell capture). ${fix.capture}`,
972
+ );
973
+ }
974
+ refuseInCacheScope(surface, wording);
975
+ if (wording.record !== false) recordLoaderIdentityRead(surface, verb);
976
+ }
977
+
978
+ /**
979
+ * Steps 2 and 3 of guardIdentityRead: throw when a "use cache" body or a
980
+ * cache() boundary would store what `surface` produces.
981
+ */
982
+ export function refuseInCacheScope(
983
+ surface: string,
984
+ { verb, fix }: IdentityReadWording,
985
+ ): void {
986
+ if (isInsideCacheExecScope()) {
987
+ throw new Error(
988
+ `${surface} cannot be ${verb} inside a "use cache" function. ${fix.useCache}`,
916
989
  );
917
990
  }
918
991
  if (isInsideCacheScope()) {
919
992
  throw new Error(
920
- `ctx.get() for a non-cacheable variable cannot be called inside a cache() boundary. ` +
921
- `The variable was created with { cache: false } or set with { cache: false }, ` +
922
- `and its value would be stale on cache hit. Move the read outside the cached scope.`,
993
+ `${surface} cannot be ${verb} inside a cache() boundary. ${fix.cacheScope}`,
923
994
  );
924
995
  }
925
996
  }
926
997
 
998
+ /**
999
+ * The capture refusal for a request-scoped read (cookies(), headers(), a
1000
+ * { cache: false } variable): the message after the surface, and the fix the
1001
+ * refusal warning gives. `what` names the data that would leak.
1002
+ */
1003
+ export function requestReadCaptureFix(
1004
+ what: string,
1005
+ ): Pick<IdentityReadWording["fix"], "capture" | "warning"> {
1006
+ return {
1007
+ capture:
1008
+ `The captured shell is served to every user of this URL, so ` +
1009
+ `request-scoped data read here would leak one user's ${what} to ` +
1010
+ `others. Read it inside a loader without ssr: false and consume it ` +
1011
+ `with useLoader, e.g. createLoader(async () => ` +
1012
+ `getUser(cookies().get("session")?.value)). ${PPR_LANE_HINT}`,
1013
+ warning:
1014
+ "Read it in a loader without ssr: false and consume it with useLoader " +
1015
+ "under loading() or an inline <Suspense> (a live hole). A promise the " +
1016
+ "handler passes or pushes does not help: the capture waits for it.",
1017
+ };
1018
+ }
1019
+
1020
+ /**
1021
+ * True when `ctx` is the active capture render: the derived request context
1022
+ * shell-capture.ts builds (`_shellCaptureRun`), which only the capture sets,
1023
+ * so the foreground render reads identity normally to serve the real user.
1024
+ * On true the capture context is flagged with the read (`surface`, e.g.
1025
+ * "cookies()", "ctx.theme"), the fix the refusal warning gives, and the loader
1026
+ * body (if any) that made the read, and the caller throws.
1027
+ */
1028
+ function tripShellCaptureGuard(
1029
+ ctx: unknown,
1030
+ surface: string,
1031
+ fix: string,
1032
+ ): boolean {
1033
+ if (
1034
+ ctx === null ||
1035
+ typeof ctx !== "object" ||
1036
+ (ctx as { _shellCaptureRun?: unknown })._shellCaptureRun !== true
1037
+ ) {
1038
+ return false;
1039
+ }
1040
+ // Record WHICH loader body (if any) made the read, so the refusal warning
1041
+ // can name the real source instead of hardcoding a lane (issue #672).
1042
+ const flagged = ctx as {
1043
+ _shellCaptureGuardTripped?: { surface: string; fix: string };
1044
+ _shellCaptureGuardTrippedLoaderId?: string;
1045
+ };
1046
+ flagged._shellCaptureGuardTripped = { surface, fix };
1047
+ flagged._shellCaptureGuardTrippedLoaderId = getCurrentLoaderBodyId();
1048
+ return true;
1049
+ }
1050
+
1051
+ const NON_CACHEABLE_READ: IdentityReadWording = {
1052
+ verb: "called",
1053
+ fix: {
1054
+ useCache:
1055
+ "The variable was created with { cache: false } or set with " +
1056
+ "{ cache: false }, and the cache key does not include its value, so " +
1057
+ "the first caller's value would be served to later callers. Read it " +
1058
+ "before calling the cached function and pass the value in as an " +
1059
+ "argument so it becomes part of the cache key.",
1060
+ cacheScope:
1061
+ "The variable was created with { cache: false } or set with " +
1062
+ "{ cache: false }, and its value would be stale on cache hit. Move the " +
1063
+ "read outside the cached scope.",
1064
+ ...requestReadCaptureFix("per-request variables"),
1065
+ },
1066
+ };
1067
+
1068
+ /**
1069
+ * Read guard for a non-cacheable variable (`createVar({ cache: false })` or a
1070
+ * `ctx.set(..., { cache: false })` write), through guardIdentityRead.
1071
+ * Callers check isNonCacheable() first so ordinary reads never reach the
1072
+ * scope lookups. `requestCtx` is the ambient request context, whose capture
1073
+ * flag the guard reads.
1074
+ */
1075
+ export function assertNonCacheableReadAllowed(
1076
+ keyOrVar: string | ContextVar<unknown>,
1077
+ requestCtx?: unknown,
1078
+ ): void {
1079
+ const name = typeof keyOrVar === "string" ? ` "${keyOrVar}"` : "";
1080
+ guardIdentityRead(
1081
+ requestCtx,
1082
+ `ctx.get() for a non-cacheable variable${name}`,
1083
+ NON_CACHEABLE_READ,
1084
+ );
1085
+ }
1086
+
1087
+ /** How the identity error words a read: a function call or a property read. */
1088
+ export type LoaderIdentityReadVerb = "called" | "read";
1089
+
1090
+ /** A request-identity read a loader execution made (recordLoaderIdentityRead). */
1091
+ export interface LoaderIdentityRead {
1092
+ /**
1093
+ * The read as the error names it: "cookies()", "headers()",
1094
+ * `ctx.get() for a non-cacheable variable "x"`, "ctx.theme" or
1095
+ * "getRequestContext().theme".
1096
+ */
1097
+ surface: string;
1098
+ /** "called" for a function, "read" for a property (the theme getters). */
1099
+ verb: LoaderIdentityReadVerb;
1100
+ /** The loader body that made the read. */
1101
+ bodyId: string | undefined;
1102
+ /**
1103
+ * The cached loader whose value carries a read another loader made
1104
+ * (loader-cache.ts identity mark): its readers read it through `via`.
1105
+ */
1106
+ via?: string;
1107
+ }
1108
+
1109
+ /**
1110
+ * Where cache-tag.ts installs its recorder at module init (same key there):
1111
+ * it owns the per-execution recorded sets, and importing it here would be a
1112
+ * cycle.
1113
+ */
1114
+ const IDENTITY_READ_RECORDER_KEY = Symbol.for(
1115
+ "rangojs-router:identity-read-recorder",
1116
+ );
1117
+
1118
+ /**
1119
+ * Record a request-identity read (cookies(), headers(), a non-cacheable
1120
+ * ctx.get()) on the current loader execution (#972).
1121
+ *
1122
+ * isInsideCacheScope() exempts loader bodies because a route cache() never
1123
+ * stores their values. A loader bound with its own cache() does: with no key()
1124
+ * and no store keyGenerator its entry is keyed by loader, host, path and
1125
+ * params only, and an enclosing route cache() key does not partition it
1126
+ * (#974). A cookies() read there stored the first visitor's session and served
1127
+ * it to everyone for the TTL. The read is recorded on the execution, not
1128
+ * thrown at the call: a reader that starts the loader before its binding does
1129
+ * runs it outside any fill, and the binding's MISS then reuses that run. The
1130
+ * fill checks what its execution recorded, through the same links as its tags
1131
+ * (cache-tag.ts recordedIdentityRead), so both orders fail the same way.
1132
+ * Response directives do not record: a key cannot make a skipped body's side
1133
+ * effect reach a HIT.
1134
+ */
1135
+ export function recordLoaderIdentityRead(
1136
+ surface: string,
1137
+ verb: LoaderIdentityReadVerb = "called",
1138
+ ): void {
1139
+ if (isInsideIdentityExempt()) return;
1140
+ const recorder = (globalThis as Record<symbol, unknown>)[
1141
+ IDENTITY_READ_RECORDER_KEY
1142
+ ] as ((surface: string, verb: LoaderIdentityReadVerb) => void) | undefined;
1143
+ recorder?.(surface, verb);
1144
+ }
1145
+
1146
+ /** The error a loader cache() fill with no declared key fails with (#972). */
1147
+ export function loaderCacheIdentityError(
1148
+ read: LoaderIdentityRead,
1149
+ cachedLoaderId: string,
1150
+ ): Error {
1151
+ const filling = `while filling its own cache() entry with no key()`;
1152
+ const where =
1153
+ read.via !== undefined && read.via !== cachedLoaderId
1154
+ ? `inside loader "${read.bodyId}", which loader "${cachedLoaderId}" reads through another loader ("${read.via}") ${filling}`
1155
+ : read.bodyId !== undefined && read.bodyId !== cachedLoaderId
1156
+ ? `inside loader "${read.bodyId}", which loader "${cachedLoaderId}" reads ${filling}`
1157
+ : `inside loader "${cachedLoaderId}", whose own cache() has no key()`;
1158
+ return new Error(
1159
+ `${read.surface} cannot be ${read.verb} ${where}. ` +
1160
+ `The entry is keyed by loader, host, path and params only, so it is ` +
1161
+ `shared across users: request-scoped data (cookies, headers, ` +
1162
+ `non-cacheable variables) read here would be stored from one request ` +
1163
+ `and served to everyone. Add a key that includes the value, or drop ` +
1164
+ `the loader's cache():\n\n` +
1165
+ ` cache({ ttl: 60, key: (ctx) => \`session:\${cookies().get("session")?.value}\` })`,
1166
+ );
1167
+ }
1168
+
927
1169
  /**
928
1170
  * Check if the current execution is inside a DSL loader scope
929
1171
  * (wrapped by runInsideLoaderScope). Used by rendered() barrier
@@ -952,7 +1194,7 @@ export function latchCachedHeaderScope(
952
1194
  /** True inside ANY loader execution — DSL loader scope or a loader body
953
1195
  * (however invoked). The "loaders always re-run fresh" exemptions key off
954
1196
  * this. */
955
- function isInsideAnyLoaderScope(): boolean {
1197
+ export function isInsideAnyLoaderScope(): boolean {
956
1198
  return (
957
1199
  loaderScopeALS.getStore()?.active === true ||
958
1200
  loaderBodyScopeALS.getStore()?.active === true
@@ -1097,32 +1339,41 @@ export function runInsideLoaderScope<T>(fn: () => T): T {
1097
1339
 
1098
1340
  /**
1099
1341
  * Run `fn` inside a loader BODY scope. Marks loader-function execution for the
1100
- * cache-purity guard only (isInsideCacheScope), WITHOUT affecting
1342
+ * cache() purity guard only (isInsideCacheScope), WITHOUT affecting
1101
1343
  * isInsideLoaderScope()/rendered() gating. Applied to every loader body (DSL
1102
1344
  * and handler-invoked via ctx.use) so request-scoped reads inside a loader
1103
- * never trip the cache-scope guards — loaders always run fresh.
1345
+ * never trip the cache() guard — a route cache() never stores loader values.
1346
+ * A "use cache" body the loader was entered in still refuses them
1347
+ * (guardIdentityRead).
1104
1348
  */
1105
1349
  export function runInsideLoaderBodyScope<T>(
1106
1350
  fn: () => T,
1107
1351
  loaderId?: string,
1108
- handlerInvoked?: boolean,
1352
+ tags?: Set<string>,
1109
1353
  ): T {
1110
- return loaderBodyScopeALS.run(
1111
- {
1112
- active: true,
1113
- loaderId,
1114
- handlerInvoked,
1115
- parent: loaderBodyScopeALS.getStore(),
1116
- execScope: getCacheExecScope(),
1117
- },
1118
- fn,
1354
+ // A loader a key() starts with ctx.use() runs guarded (runIdentityExempt).
1355
+ return endIdentityExempt(() =>
1356
+ loaderBodyScopeALS.run(
1357
+ {
1358
+ active: true,
1359
+ loaderId,
1360
+ parent: loaderBodyScopeALS.getStore(),
1361
+ tags,
1362
+ },
1363
+ fn,
1364
+ ),
1119
1365
  );
1120
1366
  }
1121
1367
 
1368
+ /** The innermost loader body's execution tag set (runInsideLoaderBodyScope). */
1369
+ export function getLoaderBodyTags(): Set<string> | undefined {
1370
+ return loaderBodyScopeALS.getStore()?.tags;
1371
+ }
1372
+
1122
1373
  /**
1123
1374
  * The $$id of the loader whose body is currently executing, or undefined
1124
1375
  * outside any loader body. Used by the shell-capture identity guard
1125
- * (cookie-store.ts) so its refusal warning can name the loader that read
1376
+ * (guardIdentityRead) so its refusal warning can name the loader that read
1126
1377
  * cookies()/headers() instead of blaming a lane it cannot see — the old
1127
1378
  * hardcoded "bake-lane loader" text misled a live-lane debugging session
1128
1379
  * (issue #672, secondary).
@@ -1131,6 +1382,24 @@ export function getCurrentLoaderBodyId(): string | undefined {
1131
1382
  return loaderBodyScopeALS.getStore()?.loaderId;
1132
1383
  }
1133
1384
 
1385
+ /**
1386
+ * The innermost loader body running here, or around it through the
1387
+ * ctx.use(Loader) chain, whose id `match` accepts (HandleStore.push, the
1388
+ * shell capture's push funnel). A body `stop` accepts, and `match` does not,
1389
+ * ends the walk: nothing around it is returned.
1390
+ */
1391
+ export function findEnclosingLoaderBody(
1392
+ match: (loaderId: string) => boolean,
1393
+ stop?: (loaderId: string) => boolean,
1394
+ ): string | undefined {
1395
+ for (let s = loaderBodyScopeALS.getStore(); s; s = s.parent) {
1396
+ if (s.loaderId === undefined) continue;
1397
+ if (match(s.loaderId)) return s.loaderId;
1398
+ if (stop?.(s.loaderId)) return undefined;
1399
+ }
1400
+ return undefined;
1401
+ }
1402
+
1134
1403
  /**
1135
1404
  * True while `loaderId`'s body, or a loader it awaits via ctx.use at any
1136
1405
  * depth, is executing. The loader-level cache records these pushes
@@ -1144,19 +1413,6 @@ export function isInsideLoaderBody(loaderId: string): boolean {
1144
1413
  return false;
1145
1414
  }
1146
1415
 
1147
- /**
1148
- * True while a HANDLER-invoked loader body (`await ctx.use(Loader)` from a
1149
- * handler, not the DSL segment funnel) is executing. The consumption-lane
1150
- * rule keys off this: handler consumption yields a BAKED copy in every shared
1151
- * artifact — cache(), "use cache", and the PPR shell — so the shell-capture
1152
- * identity guard (cookie-store.ts) permits cookies()/headers() here, exactly
1153
- * like the cache-purity guards do. DSL segment loaders (live lane masked at
1154
- * capture, bake lane guarded) never set the flag.
1155
- */
1156
- export function isInsideHandlerInvokedLoaderBody(): boolean {
1157
- return loaderBodyScopeALS.getStore()?.handlerInvoked === true;
1158
- }
1159
-
1160
1416
  // Scope for handle PUSH CALLBACKS (push(() => ...), including async ones).
1161
1417
  // A push callback's value is stored as-is; if it is a promise it is NOT tracked
1162
1418
  // by handleStore.settled and does not block segment resolution, so a