@rangojs/router 0.16.0 → 0.18.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 (309) hide show
  1. package/README.md +255 -89
  2. package/dist/testing/vitest.js +252 -0
  3. package/dist/types/browser/cookie-name.d.ts +6 -0
  4. package/dist/types/browser/history-state.d.ts +16 -0
  5. package/dist/types/browser/navigation-transaction.d.ts +6 -0
  6. package/dist/types/browser/partial-update.d.ts +11 -0
  7. package/dist/types/browser/react/ScrollRestoration.d.ts +6 -6
  8. package/dist/types/browser/rsc-router.d.ts +1 -1
  9. package/dist/types/browser/scroll-restoration.d.ts +7 -0
  10. package/dist/types/cache/cache-exec-scope.d.ts +18 -1
  11. package/dist/types/cache/cache-key-utils.d.ts +17 -2
  12. package/dist/types/cache/cache-policy.d.ts +17 -0
  13. package/dist/types/cache/cache-scope.d.ts +132 -19
  14. package/dist/types/cache/cache-tag.d.ts +92 -7
  15. package/dist/types/cache/cf/cf-base64.d.ts +6 -0
  16. package/dist/types/cache/cf/cf-cache-constants.d.ts +11 -0
  17. package/dist/types/cache/cf/cf-cache-store.d.ts +185 -44
  18. package/dist/types/cache/cf/cf-cache-types.d.ts +31 -4
  19. package/dist/types/cache/cf/cf-shell-frame.d.ts +95 -0
  20. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +34 -0
  21. package/dist/types/cache/handle-capture.d.ts +41 -3
  22. package/dist/types/cache/handle-snapshot.d.ts +54 -13
  23. package/dist/types/cache/index.d.ts +1 -0
  24. package/dist/types/cache/isolate-tag-memo.d.ts +149 -0
  25. package/dist/types/cache/segment-codec.d.ts +10 -3
  26. package/dist/types/cache/shell-memo.d.ts +178 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +110 -21
  28. package/dist/types/cache/tag-invalidation.d.ts +30 -3
  29. package/dist/types/cache/taint.d.ts +1 -1
  30. package/dist/types/cache/types.d.ts +220 -44
  31. package/dist/types/cache/vercel/vercel-cache-store.d.ts +111 -13
  32. package/dist/types/client.d.ts +4 -4
  33. package/dist/types/client.rsc.d.ts +4 -5
  34. package/dist/types/components/DefaultDocument.d.ts +5 -2
  35. package/dist/types/context-var.d.ts +6 -5
  36. package/dist/types/escape-script.d.ts +1 -1
  37. package/dist/types/handles/MetaTags.d.ts +5 -4
  38. package/dist/types/handles/Scripts.d.ts +8 -7
  39. package/dist/types/handles/meta.d.ts +2 -2
  40. package/dist/types/handles/script.d.ts +17 -16
  41. package/dist/types/href-client.d.ts +1 -1
  42. package/dist/types/html.d.ts +12 -0
  43. package/dist/types/index.d.ts +1 -1
  44. package/dist/types/index.rsc.d.ts +3 -2
  45. package/dist/types/loader-store.d.ts +7 -4
  46. package/dist/types/prerender/build-shell-capture.d.ts +11 -4
  47. package/dist/types/reverse.d.ts +36 -4
  48. package/dist/types/route-definition/dsl-helpers.d.ts +3 -3
  49. package/dist/types/route-definition/helpers-types.d.ts +33 -9
  50. package/dist/types/route-definition/resolve-handler-use.d.ts +23 -1
  51. package/dist/types/route-types.d.ts +1 -1
  52. package/dist/types/router/debug-manifest.d.ts +3 -2
  53. package/dist/types/router/error-handling.d.ts +20 -0
  54. package/dist/types/router/handler-context.d.ts +6 -0
  55. package/dist/types/router/instrument.d.ts +8 -0
  56. package/dist/types/router/match-middleware/background-revalidation.d.ts +46 -5
  57. package/dist/types/router/middleware-types.d.ts +9 -3
  58. package/dist/types/router/router-context.d.ts +3 -1
  59. package/dist/types/router/router-interfaces.d.ts +24 -2
  60. package/dist/types/router/router-options.d.ts +8 -5
  61. package/dist/types/router/segment-resolution/fresh.d.ts +10 -0
  62. package/dist/types/router/segment-resolution/loader-cache.d.ts +66 -1
  63. package/dist/types/router/segment-resolution/loader-mask.d.ts +9 -19
  64. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +23 -12
  65. package/dist/types/router/segment-resolution/mask-nested.d.ts +67 -26
  66. package/dist/types/router/segment-resolution/revalidation.d.ts +6 -2
  67. package/dist/types/router/segment-wrappers.d.ts +1 -1
  68. package/dist/types/rsc/full-payload.d.ts +14 -0
  69. package/dist/types/rsc/handler.d.ts +1 -1
  70. package/dist/types/rsc/response-route-handler.d.ts +1 -1
  71. package/dist/types/rsc/rsc-rendering.d.ts +1 -2
  72. package/dist/types/rsc/shell-build-manifest.d.ts +8 -0
  73. package/dist/types/rsc/shell-capture-constants.d.ts +61 -6
  74. package/dist/types/rsc/shell-capture.d.ts +151 -19
  75. package/dist/types/rsc/shell-serve.d.ts +140 -17
  76. package/dist/types/rsc/ssr-setup.d.ts +6 -6
  77. package/dist/types/rsc/types.d.ts +17 -0
  78. package/dist/types/server/context.d.ts +139 -15
  79. package/dist/types/server/cookie-store.d.ts +21 -0
  80. package/dist/types/server/handle-store.d.ts +26 -2
  81. package/dist/types/server/request-context.d.ts +182 -53
  82. package/dist/types/ssr/index.d.ts +24 -0
  83. package/dist/types/ssr/ssr-root.d.ts +9 -8
  84. package/dist/types/static-handler.d.ts +4 -1
  85. package/dist/types/testing/flight.entry.d.ts +7 -0
  86. package/dist/types/testing/generated-routes.d.ts +2 -2
  87. package/dist/types/testing/internal/context.d.ts +1 -1
  88. package/dist/types/testing/render-route.d.ts +32 -6
  89. package/dist/types/testing/run-loader.d.ts +18 -3
  90. package/dist/types/testing/serve-shell-request.d.ts +132 -0
  91. package/dist/types/testing/shell-status.d.ts +23 -10
  92. package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +28 -5
  93. package/dist/types/testing/vitest.d.ts +35 -1
  94. package/dist/types/theme/ThemeScript.d.ts +3 -3
  95. package/dist/types/types/cache-types.d.ts +14 -10
  96. package/dist/types/types/handler-context.d.ts +28 -1
  97. package/dist/types/types/loader-types.d.ts +8 -6
  98. package/dist/types/urls/pattern-types.d.ts +22 -18
  99. package/dist/types/urls/response-types.d.ts +3 -12
  100. package/dist/types/use-loader.d.ts +12 -3
  101. package/dist/types/vite/plugins/use-cache-transform.d.ts +33 -2
  102. package/dist/types/vite/rango.d.ts +1 -1
  103. package/dist/types/vite/router-discovery.d.ts +29 -2
  104. package/dist/vite/index.js +1420 -1348
  105. package/package.json +2 -2
  106. package/skills/api-client/SKILL.md +22 -10
  107. package/skills/breadcrumbs/SKILL.md +75 -22
  108. package/skills/bundle-analysis/SKILL.md +22 -22
  109. package/skills/cache-guide/SKILL.md +306 -124
  110. package/skills/caching/SKILL.md +530 -98
  111. package/skills/catalog.json +16 -16
  112. package/skills/client-urls/SKILL.md +65 -38
  113. package/skills/cloudflare/SKILL.md +87 -20
  114. package/skills/cloudflare/references/d1-and-local-dev.md +15 -2
  115. package/skills/cloudflare/references/streaming-and-deploy.md +10 -3
  116. package/skills/cloudflare/references/webhooks-and-crypto.md +9 -2
  117. package/skills/comparison/SKILL.md +5 -0
  118. package/skills/comparison/references/framework-comparison.md +75 -57
  119. package/skills/composability/SKILL.md +38 -29
  120. package/skills/css/SKILL.md +10 -13
  121. package/skills/debug-manifest/SKILL.md +74 -31
  122. package/skills/defer-hydration/SKILL.md +52 -31
  123. package/skills/deployment-caching/SKILL.md +11 -3
  124. package/skills/document-cache/SKILL.md +98 -28
  125. package/skills/fonts/SKILL.md +16 -8
  126. package/skills/handler-use/SKILL.md +55 -17
  127. package/skills/hooks/SKILL.md +28 -20
  128. package/skills/hooks/data.md +23 -3
  129. package/skills/hooks/handle-and-actions.md +34 -17
  130. package/skills/hooks/navigation.md +39 -10
  131. package/skills/hooks/outlets.md +13 -6
  132. package/skills/hooks/state.md +39 -15
  133. package/skills/hooks/urls.md +7 -5
  134. package/skills/host-router/SKILL.md +64 -12
  135. package/skills/i18n/SKILL.md +60 -34
  136. package/skills/intercept/SKILL.md +161 -26
  137. package/skills/layout/SKILL.md +106 -50
  138. package/skills/links/SKILL.md +51 -19
  139. package/skills/loader/SKILL.md +386 -128
  140. package/skills/middleware/SKILL.md +129 -26
  141. package/skills/migrate-nextjs/SKILL.md +156 -70
  142. package/skills/migrate-nextjs/backend-host-swap.md +5 -2
  143. package/skills/migrate-react-router/SKILL.md +51 -39
  144. package/skills/migrate-react-router/component-migration.md +28 -7
  145. package/skills/migrate-react-router/data-and-actions.md +21 -6
  146. package/skills/migrate-react-router/route-mapping.md +19 -10
  147. package/skills/mime-routes/SKILL.md +12 -7
  148. package/skills/observability/SKILL.md +92 -14
  149. package/skills/parallel/SKILL.md +88 -47
  150. package/skills/ppr/SKILL.md +866 -379
  151. package/skills/prerender/SKILL.md +210 -108
  152. package/skills/rango/SKILL.md +115 -65
  153. package/skills/react-compiler/SKILL.md +6 -5
  154. package/skills/response-routes/SKILL.md +133 -25
  155. package/skills/route/SKILL.md +170 -60
  156. package/skills/router-setup/SKILL.md +420 -240
  157. package/skills/scripts/SKILL.md +39 -21
  158. package/skills/server-actions/SKILL.md +231 -59
  159. package/skills/shell-manifest/SKILL.md +28 -14
  160. package/skills/streams-and-websockets/SKILL.md +32 -14
  161. package/skills/tailwind/SKILL.md +37 -33
  162. package/skills/testing/SKILL.md +73 -33
  163. package/skills/testing/cache-prerender.md +60 -33
  164. package/skills/testing/client-components.md +59 -6
  165. package/skills/testing/e2e-parity.md +12 -2
  166. package/skills/testing/flight.md +19 -11
  167. package/skills/testing/handles.md +8 -5
  168. package/skills/testing/loader.md +26 -6
  169. package/skills/testing/middleware.md +8 -3
  170. package/skills/testing/predicates.md +120 -0
  171. package/skills/testing/render-handler.md +70 -22
  172. package/skills/testing/response-routes.md +11 -4
  173. package/skills/testing/reverse-and-types.md +20 -14
  174. package/skills/testing/serve-shell-request.md +188 -0
  175. package/skills/testing/server-actions.md +12 -3
  176. package/skills/testing/server-tree.md +9 -0
  177. package/skills/testing/setup.md +9 -5
  178. package/skills/theme/SKILL.md +155 -30
  179. package/skills/typesafety/SKILL.md +20 -9
  180. package/skills/typesafety/env-and-bindings.md +137 -50
  181. package/skills/typesafety/generated-files-and-cli.md +26 -14
  182. package/skills/typesafety/params-and-search.md +44 -19
  183. package/skills/typesafety/route-types.md +50 -14
  184. package/skills/use-cache/SKILL.md +224 -83
  185. package/skills/vercel/SKILL.md +35 -13
  186. package/skills/view-transitions/SKILL.md +42 -21
  187. package/src/__augment-tests__/augmented.check.ts +26 -1
  188. package/src/browser/cookie-name.ts +23 -0
  189. package/src/browser/history-state.ts +23 -0
  190. package/src/browser/index.ts +2 -2
  191. package/src/browser/navigation-bridge.ts +3 -4
  192. package/src/browser/navigation-transaction.ts +22 -2
  193. package/src/browser/partial-update.ts +35 -25
  194. package/src/browser/react/ScrollRestoration.tsx +6 -6
  195. package/src/browser/rsc-router.tsx +6 -1
  196. package/src/browser/scroll-restoration.ts +53 -6
  197. package/src/cache/cache-exec-scope.ts +37 -4
  198. package/src/cache/cache-key-utils.ts +23 -8
  199. package/src/cache/cache-policy.ts +22 -0
  200. package/src/cache/cache-runtime.ts +264 -68
  201. package/src/cache/cache-scope.ts +419 -43
  202. package/src/cache/cache-tag.ts +375 -11
  203. package/src/cache/cf/cf-base64.ts +36 -4
  204. package/src/cache/cf/cf-cache-constants.ts +12 -0
  205. package/src/cache/cf/cf-cache-store.ts +990 -348
  206. package/src/cache/cf/cf-cache-types.ts +32 -4
  207. package/src/cache/cf/cf-shell-frame.ts +258 -0
  208. package/src/cache/cf/cf-tag-marker-memo.ts +143 -39
  209. package/src/cache/document-cache.ts +15 -0
  210. package/src/cache/handle-capture.ts +90 -10
  211. package/src/cache/handle-snapshot.ts +133 -28
  212. package/src/cache/index.ts +2 -0
  213. package/src/cache/isolate-tag-memo.ts +308 -0
  214. package/src/cache/segment-codec.ts +26 -7
  215. package/src/cache/shell-memo.ts +341 -0
  216. package/src/cache/shell-snapshot.ts +217 -27
  217. package/src/cache/tag-invalidation.ts +108 -22
  218. package/src/cache/taint.ts +1 -1
  219. package/src/cache/types.ts +225 -44
  220. package/src/cache/vercel/vercel-cache-store.ts +545 -61
  221. package/src/client.rsc.tsx +10 -9
  222. package/src/client.tsx +5 -4
  223. package/src/components/DefaultDocument.tsx +7 -2
  224. package/src/context-var.ts +6 -5
  225. package/src/escape-script.ts +1 -1
  226. package/src/handles/MetaTags.tsx +13 -11
  227. package/src/handles/Scripts.tsx +11 -10
  228. package/src/handles/meta.ts +2 -2
  229. package/src/handles/script.ts +17 -16
  230. package/src/href-client.ts +1 -1
  231. package/src/html.ts +12 -0
  232. package/src/index.rsc.ts +5 -0
  233. package/src/index.ts +1 -0
  234. package/src/loader-store.ts +7 -4
  235. package/src/prerender/build-shell-capture.ts +73 -22
  236. package/src/reverse.ts +44 -4
  237. package/src/route-definition/dsl-helpers.ts +121 -33
  238. package/src/route-definition/helpers-types.ts +33 -9
  239. package/src/route-definition/resolve-handler-use.ts +121 -6
  240. package/src/route-types.ts +0 -3
  241. package/src/router/debug-manifest.ts +10 -2
  242. package/src/router/error-handling.ts +51 -15
  243. package/src/router/handler-context.ts +71 -28
  244. package/src/router/instrument.ts +54 -2
  245. package/src/router/intercept-resolution.ts +141 -43
  246. package/src/router/loader-resolution.ts +115 -39
  247. package/src/router/match-api.ts +40 -26
  248. package/src/router/match-handlers.ts +5 -0
  249. package/src/router/match-middleware/background-revalidation.ts +161 -96
  250. package/src/router/match-middleware/cache-lookup.ts +110 -10
  251. package/src/router/match-middleware/cache-store.ts +48 -101
  252. package/src/router/middleware-types.ts +9 -6
  253. package/src/router/middleware.ts +75 -13
  254. package/src/router/prerender-match.ts +59 -19
  255. package/src/router/route-snapshot.ts +11 -1
  256. package/src/router/router-context.ts +2 -0
  257. package/src/router/router-interfaces.ts +25 -2
  258. package/src/router/router-options.ts +8 -5
  259. package/src/router/segment-resolution/fresh.ts +62 -21
  260. package/src/router/segment-resolution/helpers.ts +4 -2
  261. package/src/router/segment-resolution/loader-cache.ts +407 -76
  262. package/src/router/segment-resolution/loader-mask.ts +12 -22
  263. package/src/router/segment-resolution/loader-snapshot.ts +208 -56
  264. package/src/router/segment-resolution/mask-nested.ts +138 -44
  265. package/src/router/segment-resolution/revalidation.ts +61 -16
  266. package/src/router/segment-wrappers.ts +4 -0
  267. package/src/rsc/full-payload.ts +25 -1
  268. package/src/rsc/handler.ts +14 -2
  269. package/src/rsc/progressive-enhancement.ts +3 -2
  270. package/src/rsc/render-pipeline.ts +3 -0
  271. package/src/rsc/response-cache-serve.ts +9 -6
  272. package/src/rsc/response-route-handler.ts +11 -3
  273. package/src/rsc/rsc-rendering.ts +608 -225
  274. package/src/rsc/shell-build-manifest.ts +27 -9
  275. package/src/rsc/shell-capture-constants.ts +73 -6
  276. package/src/rsc/shell-capture.ts +1068 -296
  277. package/src/rsc/shell-serve.ts +316 -34
  278. package/src/rsc/ssr-setup.ts +14 -8
  279. package/src/rsc/types.ts +18 -0
  280. package/src/server/context.ts +300 -32
  281. package/src/server/cookie-store.ts +87 -61
  282. package/src/server/handle-store.ts +232 -7
  283. package/src/server/request-context.ts +297 -67
  284. package/src/ssr/index.tsx +136 -44
  285. package/src/ssr/ssr-root.tsx +9 -8
  286. package/src/static-handler.ts +4 -1
  287. package/src/testing/flight-runtime.d.ts +21 -0
  288. package/src/testing/flight.entry.ts +14 -0
  289. package/src/testing/generated-routes.ts +2 -2
  290. package/src/testing/internal/context.ts +1 -1
  291. package/src/testing/internal/flight-client-globals.ts +7 -5
  292. package/src/testing/render-route.tsx +85 -24
  293. package/src/testing/run-loader.ts +68 -6
  294. package/src/testing/serve-shell-request.ts +378 -0
  295. package/src/testing/shell-status.ts +34 -12
  296. package/src/testing/vitest-stubs/plugin-rsc.ts +123 -20
  297. package/src/testing/vitest.ts +62 -1
  298. package/src/theme/ThemeProvider.tsx +7 -6
  299. package/src/theme/ThemeScript.tsx +3 -3
  300. package/src/types/cache-types.ts +14 -10
  301. package/src/types/handler-context.ts +28 -1
  302. package/src/types/loader-types.ts +8 -6
  303. package/src/urls/pattern-types.ts +22 -18
  304. package/src/urls/response-types.ts +3 -23
  305. package/src/use-loader.tsx +12 -3
  306. package/src/vite/discovery/shell-prerender-phase.ts +9 -0
  307. package/src/vite/plugins/use-cache-transform.ts +118 -74
  308. package/src/vite/rango.ts +7 -3
  309. package/src/vite/router-discovery.ts +14 -4
@@ -7,7 +7,16 @@
7
7
  import type { ReactNode } from "react";
8
8
  import type { EntryData } from "../server/context";
9
9
  import { observePhase, PHASES } from "./instrument.js";
10
- import { contextGet } from "../context-var.js";
10
+ import { contextGet, isNonCacheable } from "../context-var.js";
11
+ import { NOCACHE_SYMBOL } from "../cache/taint.js";
12
+ import {
13
+ linkLoaderTags,
14
+ linkRecordedTags,
15
+ linkValueTags,
16
+ loaderTagSetsArmed,
17
+ readValueTags,
18
+ tagLoaderValue,
19
+ } from "../cache/cache-tag.js";
11
20
  import type {
12
21
  ResolvedSegment,
13
22
  HandlerContext,
@@ -43,6 +52,7 @@ import {
43
52
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
44
53
  import { _getRequestContext } from "../server/request-context.js";
45
54
  import {
55
+ assertNonCacheableReadAllowed,
46
56
  isInsideLoaderScope,
47
57
  runInsideLoaderBodyScope,
48
58
  isInsidePushCallbackScope,
@@ -386,6 +396,12 @@ function createLoaderExecutor<TEnv>(
386
396
  * payload's handle-stream lifetime.
387
397
  */
388
398
  silentHandles?: boolean;
399
+ /**
400
+ * loaderId -> the cache() tags of that loader's binding in this request
401
+ * (setupLoaderAccess _bindLoaderCacheTags): an execution of it outside
402
+ * the binding answers for them too (#964).
403
+ */
404
+ bindingTags?: ReadonlyMap<string, Set<string>>;
389
405
  },
390
406
  ): (
391
407
  loader: LoaderDefinition<any, any>,
@@ -394,6 +410,7 @@ function createLoaderExecutor<TEnv>(
394
410
  // Capture RequestContext eagerly for cookie access (ALS protection on Cloudflare)
395
411
  const reqCtxRef = _getRequestContext();
396
412
  const silentHandles = executorOptions?.silentHandles === true;
413
+ const internal = ctx as InternalHandlerContext<any, TEnv>;
397
414
 
398
415
  // Dependency graph: loaderId -> set of loader IDs it directly depends on.
399
416
  const dependsOn = new Map<string, Set<string>>();
@@ -429,9 +446,30 @@ function createLoaderExecutor<TEnv>(
429
446
  deps.add(loader.$$id);
430
447
  }
431
448
 
432
- // Return cached promise if already started
433
- if (loaderPromises.has(loader.$$id)) {
434
- return loaderPromises.get(loader.$$id)!;
449
+ // Every loader execution funnels here (handler and loader-to-loader
450
+ // reads, uncached DSL bindings, cache misses): memoized per request, so a
451
+ // loader runs at most once however many reads, with no store cache. A
452
+ // loader's cache() read-through lives in loader-cache.ts
453
+ // (executeLoaderData), whose ctx.use override serves a later handler read
454
+ // the binding's result.
455
+ //
456
+ // A read is a consumption: the value bakes into the reader's output, so
457
+ // the loader's tags follow the reader onto a cache() record (#957) and
458
+ // into the reader's own loader-cache entry (#964, readValueTags; armed
459
+ // only where a loader cache() can read them).
460
+ const reqCtx = reqCtxRef ?? _getRequestContext();
461
+ linkLoaderTags(loader.$$id, reqCtx);
462
+ const recordTags = loaderTagSetsArmed(reqCtx);
463
+ // A loader body reads a cache()-bound loader through its binding too, so
464
+ // every reader in the request gets the one value a HIT served. The
465
+ // binding's own MISS (callerLoaderId null) runs the loader below.
466
+ const bound =
467
+ callerLoaderId !== null &&
468
+ internal._loaderCacheOverrides?.get(loader.$$id);
469
+ const memo = bound || loaderPromises.get(loader.$$id);
470
+ if (memo) {
471
+ if (recordTags) readValueTags(memo);
472
+ return memo;
435
473
  }
436
474
 
437
475
  // Get loader function - either from loader object or fetchable registry
@@ -452,7 +490,7 @@ function createLoaderExecutor<TEnv>(
452
490
  pendingLoaders.add(loader.$$id);
453
491
 
454
492
  const currentLoaderId = loader.$$id;
455
- const variables = (ctx as InternalHandlerContext<any, TEnv>)._variables;
493
+ const variables = internal._variables;
456
494
 
457
495
  // Capture whether this loader is being started from a DSL loader scope
458
496
  // (runInsideLoaderScope in fresh.ts). Handler-invoked loaders are NOT
@@ -466,18 +504,26 @@ function createLoaderExecutor<TEnv>(
466
504
  // stale. Attributing to the owning route/layout segment gives handle
467
505
  // pushes the same granularity handler pushes have (segmentOrder-correct
468
506
  // for breadcrumb accumulation).
469
- const owningSegmentId = (ctx as InternalHandlerContext<any, TEnv>)
470
- ._currentSegmentId;
507
+ const owningSegmentId = internal._currentSegmentId;
471
508
 
472
509
  let renderedResolved = false;
473
510
  let renderedPromise: Promise<void> | null = null;
474
511
 
475
- // Loader functions are always fresh (never cached), so they get an
476
- // unguarded get that bypasses non-cacheable read guards. This applies
477
- // to ALL loaders — DSL and handler-called — because the loader
478
- // function itself always re-executes. Also handles nested deps
479
- // (loaderA → use(loaderB)) since all share this unguarded get.
480
- const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> = {
512
+ // Request-scoped like the handler ctx it derives from (issue #940): the
513
+ // NOCACHE_SYMBOL brand below makes a "use cache" function taking it key by
514
+ // the route fields (_routeName, _responseType) and replay its handle
515
+ // pushes into the owning segment, claimed per loader like a handler ctx
516
+ // (_claimLoaderPushes, _runLoaderIsolated). Non-cacheable ctx.get reads
517
+ // run the shared guard; the body itself stays exempt (loader body scope).
518
+ const loaderCtx: LoaderContext<Record<string, string | undefined>, TEnv> &
519
+ Pick<
520
+ InternalHandlerContext<any, TEnv>,
521
+ | "_routeName"
522
+ | "_responseType"
523
+ | "_currentSegmentId"
524
+ | "_claimLoaderPushes"
525
+ | "_runLoaderIsolated"
526
+ > = {
481
527
  params: ctx.params,
482
528
  routeParams: (ctx.params ?? {}) as Record<string, string>,
483
529
  request: ctx.request,
@@ -517,6 +563,9 @@ function createLoaderExecutor<TEnv>(
517
563
  buildHandleSnapshot(reqCtx._handleStore, segmentOrder);
518
564
  return collectHandleData(keyOrVar, snapshot, segmentOrder);
519
565
  }
566
+ if (isNonCacheable(variables, keyOrVar)) {
567
+ assertNonCacheableReadAllowed(keyOrVar, _getRequestContext());
568
+ }
520
569
  return contextGet(variables, keyOrVar);
521
570
  }) as typeof ctx.get,
522
571
  use: ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
@@ -552,6 +601,11 @@ function createLoaderExecutor<TEnv>(
552
601
  method: "GET",
553
602
  body: undefined,
554
603
  reverse: ctx.reverse as LoaderContext["reverse"],
604
+ _routeName: internal._routeName,
605
+ _responseType: internal._responseType,
606
+ _currentSegmentId: owningSegmentId,
607
+ _claimLoaderPushes: internal._claimLoaderPushes,
608
+ _runLoaderIsolated: internal._runLoaderIsolated,
555
609
  rendered: (): Promise<void> => {
556
610
  // Guard: only DSL loaders may use rendered()
557
611
  if (!isDslLoader) {
@@ -639,6 +693,7 @@ function createLoaderExecutor<TEnv>(
639
693
  return renderedPromise;
640
694
  },
641
695
  };
696
+ (loaderCtx as any)[NOCACHE_SYMBOL] = true;
642
697
 
643
698
  // Meter this loader once via observePhase (loader:<id> perf metric +
644
699
  // rango.loader span); loaderFn runs inside the span callback so its KV/D1/
@@ -655,42 +710,29 @@ function createLoaderExecutor<TEnv>(
655
710
  // does not grant rendered() to handler-invoked loaders. Uses a body-only
656
711
  // scope, so isInsideLoaderScope() / barrier / deadlock gating is unchanged.
657
712
  //
658
- // `handlerInvoked` (!isDslLoader) rides on the scope for the CONSUMPTION-
659
- // LANE RULE: a handler-consumed loader's value is a BAKED copy in every
660
- // shared artifact (cache(), "use cache", the PPR shell), so its identity
661
- // reads are exempt from the shell-capture guard — same allowance the
662
- // cache-purity guards give it. DSL segment loaders keep their lane
663
- // machinery (live = masked at capture, bake = guarded). A DSL loader's
664
- // nested deps inherit isDslLoader=false only when the CHAIN started in a
665
- // handler; a chain started by the segment funnel stays DSL (the loader
666
- // scope ALS survives the body's awaits).
667
- // Shell fast path eligibility: a HANDLER-invoked loader executing during a
668
- // capture is handler-layer dynamism — on a handler-free (replayed) HIT it
669
- // would never re-run, freezing its consumption-lane value (#672's "fresh
670
- // per serve" slot shape). Mark the capture; the entry declines the fast
671
- // path and keeps the full tail. DSL loaders re-run on every HIT and never
672
- // set this.
673
- if (!isDslLoader) {
674
- const captureCtx = _getRequestContext();
675
- if (
676
- captureCtx?._shellCaptureRun &&
677
- captureCtx._shellCaptureHandleLiveness
678
- ) {
679
- captureCtx._shellCaptureHandleLiveness.handlerInvokedLoader = true;
680
- }
681
- }
713
+ // The CONSUMPTION-LANE RULE: a handler-consumed loader's value is a
714
+ // BAKED copy in every shared artifact (cache(), "use cache", the PPR
715
+ // shell). The cache-purity guards permit its identity reads; the PPR
716
+ // capture guard does not (server/context.ts assertNotInsideShellCapture).
682
717
 
718
+ const recordedTags = recordTags ? new Set<string>() : undefined;
683
719
  const promise = observePhase(PHASES.loader(loader.$$id), () =>
684
720
  Promise.resolve(
685
721
  runInsideLoaderBodyScope(
686
722
  () => loaderFn(loaderCtx as LoaderContext<any, TEnv>),
687
723
  loader.$$id,
688
- !isDslLoader,
724
+ recordedTags,
689
725
  ),
690
726
  ).finally(() => {
691
727
  pendingLoaders.delete(loader.$$id);
692
728
  }),
693
729
  );
730
+ if (recordedTags) {
731
+ tagLoaderValue(promise, recordedTags);
732
+ readValueTags(promise);
733
+ const bindingTags = executorOptions?.bindingTags?.get(loader.$$id);
734
+ if (bindingTags) linkRecordedTags(recordedTags, bindingTags);
735
+ }
694
736
 
695
737
  // Auxiliary-lane tracking: keeps the handle store open for this body's
696
738
  // ctx.handle() pushes without joining `settled` (the handler barrier that
@@ -734,7 +776,41 @@ export function setupLoaderAccess<TEnv>(
734
776
  const reqCtxRef = _getRequestContext();
735
777
  const handleStoreRef = reqCtxRef?._handleStore;
736
778
 
737
- const useLoader = createLoaderExecutor(ctx, loaderPromises);
779
+ // Loader-cache HIT replay (loader-cache.ts replayLoaderHandles): a cached
780
+ // entry records its ctx.use dependencies' pushes too, and a dependency is
781
+ // memoized here and shared with live readers. A loader that already ran
782
+ // here keeps its live pushes; otherwise one replay delivers its recorded
783
+ // pushes, and a later run here replaces them (HandleStore.pushReplayed).
784
+ const claimed = new Set<string>();
785
+ const internal = ctx as InternalHandlerContext<any, TEnv>;
786
+ internal._claimLoaderPushes = (loaderId) => {
787
+ if (loaderPromises.has(loaderId) || claimed.has(loaderId)) return false;
788
+ claimed.add(loaderId);
789
+ return true;
790
+ };
791
+ // A loader cache() binding's tags reach every execution of that loader
792
+ // outside the binding: one a reader started before the binding (linked
793
+ // here), a later one, or a refresh's (bindingTags).
794
+ const executor: { bindingTags?: Map<string, Set<string>> } = {};
795
+ internal._bindLoaderCacheTags = (loaderId, tags) => {
796
+ (executor.bindingTags ??= new Map()).set(loaderId, tags);
797
+ const started = loaderPromises.get(loaderId);
798
+ if (started) linkValueTags(started, tags);
799
+ };
800
+ const useLoader = createLoaderExecutor(ctx, loaderPromises, executor);
801
+ // A refresh's pushes are diverted, so its loader ctxs never claim
802
+ // (cache-runtime.ts refreshView). It reads each loader fresh, not through
803
+ // the page's bindings: a stale refresh must not rebuild its entry from
804
+ // another stale entry.
805
+ internal._runLoaderIsolated = (loader) =>
806
+ createLoaderExecutor(
807
+ Object.create(ctx, {
808
+ _claimLoaderPushes: { value: undefined },
809
+ _loaderCacheOverrides: { value: undefined },
810
+ }),
811
+ new Map(),
812
+ executor,
813
+ )(loader, null);
738
814
 
739
815
  ctx.use = ((item: LoaderDefinition<any, any> | Handle<any, any>) => {
740
816
  if (isHandle(item)) {
@@ -3,11 +3,15 @@ import {
3
3
  createCacheScope,
4
4
  resolveShellImplicitCacheScope,
5
5
  } from "../cache/cache-scope.js";
6
+ import { armLoaderTagSets, armRecordTagOwners } from "../cache/cache-tag.js";
7
+ import { bindsLoaderCache } from "./segment-resolution/loader-cache.js";
6
8
  import { RouteNotFoundError } from "../errors";
7
9
  import {
8
10
  createErrorInfo,
9
11
  createErrorSegment,
12
+ findInOrphans,
10
13
  findNearestErrorBoundary as findErrorBoundary,
14
+ withErrorBoundary,
11
15
  } from "./error-handling.js";
12
16
  import {
13
17
  createHandlerContext,
@@ -148,6 +152,20 @@ export async function createMatchContextForFull<TEnv>(
148
152
  Store.metrics = metricsStore;
149
153
  }
150
154
 
155
+ // A shell capture's implicit doc scope, or a document HIT tail's (which
156
+ // replaces any route-derived scope: the tail replays the shell's doc
157
+ // record), or else the route's own scope.
158
+ const cacheScope = resolveShellImplicitCacheScope(snapshot.cacheScope);
159
+ // A scope means a route cache() record may be written: arm its tag owners
160
+ // before any handler runs (#957). A document HIT tail writes no record and
161
+ // runs no handler, so it arms nothing. A loader bound with its own cache()
162
+ // needs the per-execution loader tag sets before any loader runs (#964):
163
+ // loaders still run on a HIT.
164
+ if (cacheScope && !_getRequestContext()?._shellImplicitCache?.docTail) {
165
+ armRecordTagOwners();
166
+ }
167
+ if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
168
+
151
169
  return {
152
170
  request,
153
171
  url: cleanUrl,
@@ -184,9 +202,7 @@ export async function createMatchContextForFull<TEnv>(
184
202
  },
185
203
  isSameRouteNavigation: false,
186
204
  interceptResult: null,
187
- // Shell fast path: a capture or an eligible document/navigation HIT may
188
- // substitute an implicit doc-level scope (a route-derived scope wins).
189
- cacheScope: resolveShellImplicitCacheScope(snapshot.cacheScope),
205
+ cacheScope,
190
206
  isIntercept: false,
191
207
  actionContext: undefined,
192
208
  isAction: false,
@@ -399,6 +415,16 @@ export async function createMatchContextForPartial<TEnv>(
399
415
  }
400
416
 
401
417
  const isIntercept = !!interceptResult;
418
+ // A normal-route navigation may replay a PPR shell's canonical segment
419
+ // snapshot. Intercepts remain source-dependent and always use their normal
420
+ // cache path.
421
+ const cacheScope = isIntercept
422
+ ? snapshot.cacheScope
423
+ : resolveShellImplicitCacheScope(snapshot.cacheScope);
424
+ // A scope means a route cache() record may be written: arm its tag owners
425
+ // before any handler runs (#957); see the full-match site for #964's arm.
426
+ if (cacheScope) armRecordTagOwners();
427
+ if (bindsLoaderCache(snapshot.entries)) armLoaderTagSets();
402
428
 
403
429
  return {
404
430
  request,
@@ -425,12 +451,7 @@ export async function createMatchContextForPartial<TEnv>(
425
451
  interceptSelectorContext,
426
452
  isSameRouteNavigation: nav.isSameRouteNavigation,
427
453
  interceptResult,
428
- // A normal-route navigation may replay a PPR shell's canonical segment
429
- // snapshot. Intercepts remain source-dependent and always use their normal
430
- // cache path.
431
- cacheScope: isIntercept
432
- ? snapshot.cacheScope
433
- : resolveShellImplicitCacheScope(snapshot.cacheScope),
454
+ cacheScope,
434
455
  isIntercept,
435
456
  actionContext,
436
457
  isAction,
@@ -481,24 +502,21 @@ export async function matchError<TEnv>(
481
502
  segmentType,
482
503
  );
483
504
 
505
+ // Same search order as findNearestErrorBoundary, so the fallback above and
506
+ // the entry that positions it agree.
484
507
  let entryWithBoundary: EntryData | null = null;
485
508
  let current: EntryData | null = manifestEntry;
509
+ let prev: EntryData | null = null;
486
510
  while (current) {
487
511
  if (current.errorBoundary && current.errorBoundary.length > 0) {
488
512
  entryWithBoundary = current;
489
513
  break;
490
514
  }
491
515
 
492
- if (current.layout && current.layout.length > 0) {
493
- for (const orphan of current.layout) {
494
- if (orphan.errorBoundary && orphan.errorBoundary.length > 0) {
495
- entryWithBoundary = orphan;
496
- break;
497
- }
498
- }
499
- if (entryWithBoundary) break;
500
- }
516
+ entryWithBoundary = findInOrphans(current, withErrorBoundary, prev) ?? null;
517
+ if (entryWithBoundary) break;
501
518
 
519
+ prev = current;
502
520
  current = current.parent;
503
521
  }
504
522
 
@@ -507,23 +525,19 @@ export async function matchError<TEnv>(
507
525
 
508
526
  if (entryWithBoundary) {
509
527
  boundaryEntry = entryWithBoundary;
528
+ // The chain entry holding the boundary, itself or in its orphan tree: the
529
+ // error segment takes the place of its child in the chain.
530
+ const boundaryOwner = current;
510
531
 
511
532
  outletEntry = manifestEntry;
512
533
  current = manifestEntry;
513
534
 
514
535
  while (current) {
515
- if (current.parent === boundaryEntry) {
536
+ if (current.parent === boundaryOwner) {
516
537
  outletEntry = current;
517
538
  break;
518
539
  }
519
540
 
520
- if (current.parent && current.parent.layout) {
521
- if (current.parent.layout.includes(boundaryEntry)) {
522
- outletEntry = current;
523
- break;
524
- }
525
- }
526
-
527
541
  current = current.parent;
528
542
  }
529
543
  } else {
@@ -47,6 +47,7 @@ import {
47
47
  buildCacheSignalSegments,
48
48
  } from "./telemetry.js";
49
49
  import { _getRequestContext } from "../server/request-context.js";
50
+ import { ShellRecordUnavailableError } from "../cache/shell-snapshot.js";
50
51
  import { evaluatePprTransitionWhen } from "./transition-when.js";
51
52
 
52
53
  /**
@@ -380,6 +381,10 @@ export function createMatchHandlers<TEnv = any>(
380
381
  emitter.end(0, false, error.status);
381
382
  throw error;
382
383
  }
384
+ // A document shell HIT tail's doc record could not supply the
385
+ // handler layer: a serve-path signal serveShellHit degrades, not a
386
+ // routing error (the record's decode failure was already reported).
387
+ if (error instanceof ShellRecordUnavailableError) throw error;
383
388
  emitter.error(
384
389
  error instanceof Error ? error : new Error(String(error)),
385
390
  "routing",