@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
@@ -35,14 +35,14 @@ import {
35
35
 
36
36
  export { isCachedFunction };
37
37
  import { serializeResult, deserializeResult } from "./segment-codec.js";
38
- import { createHandleStore } from "../server/handle-store.js";
39
38
  import {
40
- restoreHandles,
39
+ appendHandles,
41
40
  encodeHandles,
42
41
  decodeHandles,
43
42
  } from "./handle-snapshot.js";
44
- import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
45
- import { sortedSearchString } from "./cache-key-utils.js";
43
+ import { startHandleCapture, useCacheRecordKey } from "./handle-capture.js";
44
+ import { isHandle } from "../handle.js";
45
+ import { cacheKeyBase, sortedSearchString } from "./cache-key-utils.js";
46
46
  import { encodeKV } from "../encode-kv.js";
47
47
  import { runBackground } from "./background-task.js";
48
48
  import { observePhase, PHASES } from "../router/instrument.js";
@@ -51,9 +51,16 @@ import {
51
51
  recordRequestTags,
52
52
  runWithCacheTagScope,
53
53
  } from "./cache-tag.js";
54
- import { runWithCacheExecScope } from "./cache-exec-scope.js";
54
+ import {
55
+ createCacheExecScope,
56
+ isInCacheExecChain,
57
+ runWithCacheExecScope,
58
+ type CacheExecScope,
59
+ } from "./cache-exec-scope.js";
55
60
  import { reportCacheError } from "./cache-error.js";
61
+ import { currentCacheSeq, invalidatedSince } from "./tag-invalidation.js";
56
62
  import type { CacheItemResult } from "./types.js";
63
+ import type { InternalHandlerContext } from "../types.js";
57
64
 
58
65
  /**
59
66
  * DJB2 hash returning an 8-char hex string. Deterministic across runtimes
@@ -125,10 +132,74 @@ export async function replyToCacheKey(
125
132
  return encodeKV(pairs, { sort: true });
126
133
  }
127
134
 
135
+ /**
136
+ * An unescaped temporary-reference token in an encodeReply part. A user
137
+ * string "$T" encodes as "$$T", a quote inside a string is escaped, and an
138
+ * object key "$T" is followed by ":".
139
+ */
140
+ const TEMPORARY_REFERENCE_TOKEN = /(?<!\\)"\$T"(?!:)/g;
141
+
142
+ const REACT_ELEMENT_TYPE = Symbol.for("react.transitional.element");
143
+ const CLIENT_REFERENCE_TYPE = Symbol.for("react.client.reference");
144
+ const SERVER_REFERENCE_TYPE = Symbol.for("react.server.reference");
145
+
146
+ function countTemporaryReferences(encoded: string | FormData): number {
147
+ if (typeof encoded === "string") {
148
+ return encoded.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
149
+ }
150
+ let count = 0;
151
+ encoded.forEach((value) => {
152
+ if (typeof value === "string") {
153
+ count += value.match(TEMPORARY_REFERENCE_TOKEN)?.length ?? 0;
154
+ }
155
+ });
156
+ return count;
157
+ }
158
+
159
+ /** A React element, or a client or server reference function. */
160
+ function isRenderSlot(value: unknown): boolean {
161
+ if (typeof value === "object" && value !== null) {
162
+ return (value as { $$typeof?: unknown }).$$typeof === REACT_ELEMENT_TYPE;
163
+ }
164
+ if (typeof value === "function") {
165
+ const tag = (value as { $$typeof?: unknown }).$$typeof;
166
+ return tag === CLIENT_REFERENCE_TYPE || tag === SERVER_REFERENCE_TYPE;
167
+ }
168
+ return false;
169
+ }
170
+
171
+ /**
172
+ * Whether encodeReply wrote "$T" for a value that cannot be keyed. Given a
173
+ * temporary-reference set it does not throw on a value it cannot serialize:
174
+ * it writes "$T", so distinct arguments would share one key (issue #924).
175
+ * React elements and client/server references are render slots, left out of
176
+ * the key on purpose (the interleave route, e2e/use-cache-streaming.test.ts);
177
+ * a function, a symbol or a class instance is not.
178
+ *
179
+ * The set is the Map encodeReply fills: one entry per "$T" it writes, plus
180
+ * every object it visits. So more tokens than slot entries means a non-slot
181
+ * "$T", with no traversal of the arguments. Runs only on the encoder path.
182
+ */
183
+ function hasUnkeyableReference(
184
+ encoded: string | FormData,
185
+ tempRefs: unknown,
186
+ ): boolean {
187
+ const tokens = countTemporaryReferences(encoded);
188
+ if (tokens === 0) return false;
189
+ let slots = 0;
190
+ for (const value of (tempRefs as Map<string, unknown>).values()) {
191
+ if (isRenderSlot(value)) slots++;
192
+ }
193
+ return tokens > slots;
194
+ }
195
+
128
196
  // Cached-fn ids already warned about running uncached under a test runner, so
129
197
  // the test-ergonomics warning fires once per fn rather than once per call.
130
198
  const warnedUncachedUnderTest = new Set<string>();
131
199
 
200
+ // Cached-fn ids already warned about arguments that cannot be keyed (dev only).
201
+ const warnedUnkeyableArgs = new Set<string>();
202
+
132
203
  /**
133
204
  * Fast-path cache-key builder for JSON-safe key args. Returns a deterministic
134
205
  * string (object keys recursively sorted so insertion order can't change the
@@ -222,7 +293,10 @@ interface CacheEnvelope {
222
293
  */
223
294
  interface InFlightExecution {
224
295
  promise: Promise<CacheEnvelope>;
296
+ /** Date.now() at registration, for IN_FLIGHT_LEADER_MAX_WAIT_MS. */
225
297
  registeredAt: number;
298
+ /** currentCacheSeq() at registration, for invalidatedSince (#973). */
299
+ startSeq: number;
226
300
  }
227
301
 
228
302
  const inFlightExecutions = new Map<string, InFlightExecution>();
@@ -264,6 +338,30 @@ function raceLeader(
264
338
  });
265
339
  }
266
340
 
341
+ /**
342
+ * A stale refresh's view of a handler ctx: loader reads run on their own
343
+ * executor (setupLoaderAccess _runLoaderIsolated), and an inner HIT replays in
344
+ * full without claiming. The refresh diverts its pushes, so the request's
345
+ * memoized run of a loader would take that loader's live pushes off the page,
346
+ * and a claim would starve the page's own replay of it.
347
+ */
348
+ function refreshView(arg: unknown): unknown {
349
+ const ctx = arg as InternalHandlerContext<any, any>;
350
+ const runIsolated = isTainted(arg) ? ctx._runLoaderIsolated : undefined;
351
+ if (!runIsolated) return arg;
352
+ const runs = new Map<string, Promise<any>>();
353
+ const use = (item: any) => {
354
+ if (isHandle(item)) return ctx.use(item);
355
+ let run = runs.get(item.$$id);
356
+ if (!run) runs.set(item.$$id, (run = runIsolated(item)));
357
+ return run;
358
+ };
359
+ return Object.create(ctx, {
360
+ use: { value: use },
361
+ _claimLoaderPushes: { value: undefined },
362
+ });
363
+ }
364
+
267
365
  // ============================================================================
268
366
  // Core: registerCachedFunction
269
367
  // ============================================================================
@@ -333,17 +431,33 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
333
431
  );
334
432
  }
335
433
 
336
- // Separate tainted args (ctx, env, req) from key-generating args.
337
- // For tainted objects that carry route context (params, pathname,
338
- // searchParams), extract serializable values into the key so
339
- // different routes, param combinations, and query variants produce
340
- // distinct cache entries.
434
+ // Separate request-scoped args (ctx, a Request, the request's env) from
435
+ // key-generating args. For those that carry route context (ctx params,
436
+ // pathname, searchParams; a Request's URL), extract serializable values
437
+ // into the key so different routes, param combinations, and query
438
+ // variants produce distinct cache entries.
341
439
  const keyArgs: unknown[] = [];
342
440
  let hasTaintedArgs = false;
441
+ // The calling segment, read synchronously as ctx.use(Handle) does: a HIT
442
+ // replays into it. A route's layouts and page share one handler ctx and
443
+ // so one key; the recorded ids are the first caller's. Its ctx's claim
444
+ // keeps a replayed loader's pushes to one copy per request.
445
+ let callerSegmentId: string | undefined;
446
+ let claimLoaderPushes: ((loaderId: string) => boolean) | undefined;
447
+ // A fetchable loader's request body (loader-fetch.ts) is an input the
448
+ // route fields leave out: such a call runs uncached (#940).
449
+ let hasBodyCtx = false;
343
450
  for (const arg of args) {
344
451
  if (isTainted(arg)) {
345
452
  hasTaintedArgs = true;
346
453
  const ctx = arg as any;
454
+ if (ctx.body !== undefined || ctx.formData !== undefined) {
455
+ hasBodyCtx = true;
456
+ }
457
+ if (callerSegmentId === undefined) {
458
+ callerSegmentId = ctx._currentSegmentId;
459
+ claimLoaderPushes = ctx._claimLoaderPushes;
460
+ }
347
461
  if (ctx.params && typeof ctx.params === "object") {
348
462
  // Include host to prevent cross-host cache collisions (same
349
463
  // pattern as route-level cache-scope.ts key generation).
@@ -373,6 +487,22 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
373
487
  }
374
488
  }
375
489
  }
490
+ } else if (arg instanceof Request) {
491
+ // A raw Request (ctx.request) is request-scoped like ctx: fold in its
492
+ // URL with the same host-namespacing and search normalization.
493
+ const url = new URL(arg.url);
494
+ keyArgs.push(
495
+ cacheKeyBase(
496
+ url.host,
497
+ url.pathname,
498
+ url.searchParams,
499
+ undefined,
500
+ requestCtx?._searchParamsFilter,
501
+ ),
502
+ );
503
+ } else if (arg != null && arg === requestCtx?.env) {
504
+ // The request's env is left out: constant per deployment, and its
505
+ // bindings are not serializable.
376
506
  } else {
377
507
  keyArgs.push(arg);
378
508
  }
@@ -383,7 +513,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
383
513
  // gone. Throw early rather than silently dropping handle side effects.
384
514
  if (hasTaintedArgs && !requestCtx?._handleStore) {
385
515
  throw new Error(
386
- `[use cache] "${id}" receives a tainted argument (ctx/env/req) but the ` +
516
+ `[use cache] "${id}" receives a ctx argument but the ` +
387
517
  `HandleStore is not available. This typically happens when a "use cache" ` +
388
518
  `function with ctx runs outside the request context (e.g., during late ` +
389
519
  `streaming after AsyncLocalStorage context is lost). Move the "use cache" ` +
@@ -391,10 +521,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
391
521
  `use the route-level cache() DSL instead.`,
392
522
  );
393
523
  }
524
+ // Records group pushes by owning loader (useCacheRecordKey) and replay
525
+ // into the caller's segment; a caller without one records segment ids.
526
+ const ownerKeyed = callerSegmentId !== undefined;
394
527
 
395
528
  // Generate cache key
396
529
  let cacheKey: string;
397
530
  try {
531
+ if (hasBodyCtx) {
532
+ throw new Error("ctx carries a request body");
533
+ }
398
534
  if (keyArgs.length > 0) {
399
535
  // Fast path: when every key arg is JSON-safe, build the key with a
400
536
  // deterministic stable-stringify and skip encodeReply (the Flight reply
@@ -410,6 +546,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
410
546
  const encoded = await encodeReply(keyArgs as unknown[], {
411
547
  temporaryReferences: tempRefs,
412
548
  });
549
+ if (hasUnkeyableReference(encoded, tempRefs)) {
550
+ throw new Error("unserializable key argument");
551
+ }
413
552
  const argsKey = await replyToCacheKey(encoded);
414
553
  cacheKey = `use-cache:${id}:${argsKey}`;
415
554
  }
@@ -420,6 +559,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
420
559
  // Non-serializable args: run uncached (within a tag scope so cacheTag()
421
560
  // still does not throw). Record runtime tags so the document union still
422
561
  // sees them even though this call is not itself cached.
562
+ if (
563
+ process.env.NODE_ENV !== "production" &&
564
+ !warnedUnkeyableArgs.has(id)
565
+ ) {
566
+ warnedUnkeyableArgs.add(id);
567
+ console.warn(
568
+ `[use cache] "${id}" ran uncached: an argument, or a value nested in ` +
569
+ `one, cannot be serialized into the cache key (a function, class ` +
570
+ `instance or symbol, or a loader ctx carrying a request body). ` +
571
+ `Pass serializable values; ctx, a Request, the request's env and ` +
572
+ `React elements are handled.`,
573
+ );
574
+ }
423
575
  const scoped = runWithCacheTagScope(() => fn.apply(this, args));
424
576
  const result = await scoped.result;
425
577
  recordRequestTags(scoped.tags, requestCtx);
@@ -441,7 +593,9 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
441
593
  const handleStore = requestCtx?._handleStore;
442
594
  if (handleStore) {
443
595
  const r = await decodeHandles(entry.handles);
444
- if (r) restoreHandles(r, handleStore);
596
+ if (r) {
597
+ appendHandles(r, handleStore, callerSegmentId, claimLoaderPushes);
598
+ }
445
599
  }
446
600
  }
447
601
  recordRequestTags(entry.tags, requestCtx);
@@ -474,34 +628,29 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
474
628
  // Stale hit: return stale value, revalidate in background
475
629
  try {
476
630
  const result = await serveCached(cached);
631
+ const liveStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
632
+ const recordKey = ownerKeyed ? useCacheRecordKey() : undefined;
477
633
  // Background revalidation — must capture handles if tainted args present.
478
634
  runBackground(requestCtx, async () => {
479
- // The background body runs under a DERIVED context with an OWN
480
- // _handleStore (the shell-capture isolation pattern —
481
- // shell-capture.ts attemptCapture): its handle pushes land in the
482
- // isolated store (captured below, persisted with the entry) while
483
- // the foreground keeps pushing into the ORIGINAL store, untouched.
484
- // Derivation matters because the foreground is STILL RENDERING here
485
- // — runBackground/waitUntil starts the task on the next microtask,
486
- // not after the response. The previous shape swapped
487
- // requestCtx._handleStore in place (restore in finally), which
488
- // routed the whole overlap window's foreground pushes into the
489
- // background store: lost from the live document AND persisted into
490
- // the revalidated entry (issue #684, plan 010).
491
- const bgHandleStore =
492
- hasTaintedArgs && requestCtx ? createHandleStore() : undefined;
493
- const bgCtx: typeof requestCtx = bgHandleStore
494
- ? Object.assign(Object.create(requestCtx), {
495
- _handleStore: bgHandleStore,
635
+ // The background body's handle pushes belong to the refreshed entry
636
+ // only: the foreground already replayed the stale entry's. They
637
+ // reach the REQUEST's store (a handler ctx.use(Handle) binds it at
638
+ // setupLoaderAccess, so a derived context with its own store never
639
+ // sees them), so the capture accepts only this body's chain and
640
+ // diverts what it records away from the live store. The foreground
641
+ // is still rendering here (runBackground starts on the next
642
+ // microtask), and its concurrent pushes are neither recorded nor
643
+ // diverted (issue #684, plan 010). A root scope: the task is
644
+ // detached, so an outer "use cache" body that scheduled it must not
645
+ // record the refresh's pushes.
646
+ const bgScope: CacheExecScope = {};
647
+ const bgCapture = liveStore
648
+ ? startHandleCapture(liveStore, {
649
+ accept: () => isInCacheExecChain(bgScope),
650
+ divert: true,
651
+ key: recordKey,
496
652
  })
497
- : requestCtx;
498
- let bgCapture: HandleCapture | undefined;
499
- let bgStopCapture: (() => void) | undefined;
500
- if (bgHandleStore) {
501
- const c = startHandleCapture(bgHandleStore);
502
- bgCapture = c.capture;
503
- bgStopCapture = c.stop;
504
- }
653
+ : undefined;
505
654
 
506
655
  // Tainted args are NOT stamped here, in contrast to the foreground
507
656
  // miss path below. The args include the live HandlerContext the
@@ -522,23 +671,27 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
522
671
  // getRequestContext().env.ApiKey) resolves during the background
523
672
  // revalidation instead of throwing "called outside of a request
524
673
  // context". runWithRequestContext sets the store for fn's
525
- // synchronous kickoff; its async continuations inherit it. The
526
- // DERIVED context goes in, so ambient _handleStore reads inside
527
- // the body resolve to the isolated store.
674
+ // synchronous kickoff; its async continuations inherit it.
528
675
  //
529
676
  // The span opens inside the request ALS and wraps the WHOLE task —
530
677
  // fn kickoff through the store write — so its platform spans nest
531
678
  // under rango.background and a slow or failing setItem is part of
532
679
  // the traced revalidation, not an untraced tail.
533
- await runWithRequestContext(bgCtx, () =>
680
+ await runWithRequestContext(requestCtx, () =>
534
681
  observePhase(
535
682
  PHASES.background("use-cache-revalidation"),
536
683
  async () => {
684
+ const refreshArgs = args.map(refreshView);
685
+ // The refresh reads its data from here on (#973).
686
+ const startSeq = currentCacheSeq();
537
687
  const scoped = runWithCacheTagScope(() =>
538
- runWithCacheExecScope(() => fn.apply(this, args)),
688
+ runWithCacheExecScope(
689
+ () => fn.apply(this, refreshArgs),
690
+ bgScope,
691
+ ),
539
692
  );
540
693
  const freshResult = await scoped.result;
541
- bgStopCapture?.();
694
+ bgCapture?.stop();
542
695
  // Merge profile/DSL tags with runtime cacheTag() tags, read
543
696
  // after awaiting so post-await cacheTag() calls are included.
544
697
  // Normalize (drops empty profile tags, matching the
@@ -549,11 +702,30 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
549
702
  ),
550
703
  ];
551
704
  recordRequestTags(freshTags, requestCtx);
552
- const serialized = await serializeResult(freshResult);
705
+ const flightErrors: unknown[] = [];
706
+ const onFlightError = (error: unknown): void => {
707
+ flightErrors.push(error);
708
+ };
709
+ const serialized = await serializeResult(
710
+ freshResult,
711
+ onFlightError,
712
+ );
553
713
  if (serialized !== null) {
554
- const encodedHandles = bgCapture?.data
555
- ? await encodeHandles(bgCapture.data)
714
+ const encodedHandles = bgCapture
715
+ ? await encodeHandles(
716
+ bgCapture.capture.data,
717
+ onFlightError,
718
+ )
556
719
  : undefined;
720
+ // An error row would replace the stale entry; the catch
721
+ // below reports it and the stale entry keeps serving.
722
+ if (flightErrors.length > 0) throw flightErrors[0];
723
+ // The request invalidated one of the refreshed value's
724
+ // tags after the refresh started: the value may predate
725
+ // it, and written now it would outlive it.
726
+ if (invalidatedSince(requestCtx, freshTags, startSeq)) {
727
+ return;
728
+ }
557
729
  await store.setItem!(cacheKey, serialized, {
558
730
  handles: encodedHandles,
559
731
  ttl: profile.ttl,
@@ -565,7 +737,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
565
737
  ),
566
738
  );
567
739
  } catch (bgError) {
568
- bgStopCapture?.();
740
+ bgCapture?.stop();
569
741
  // Pass requestCtx explicitly: this runs in a detached background
570
742
  // task where the ALS context is gone, so onError can only fire if
571
743
  // we hand it the context captured up front.
@@ -576,8 +748,6 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
576
748
  requestCtx,
577
749
  );
578
750
  }
579
- // No finally: nothing shared was mutated — the derived context and
580
- // its handle store are garbage after the task settles.
581
751
  });
582
752
  return result;
583
753
  } catch (error) {
@@ -599,8 +769,8 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
599
769
  // against its OWN handle store (gated on ITS hasTaintedArgs), and records
600
770
  // tags into its OWN request — no deserialized result is shared across
601
771
  // requests. The store write stays exactly once (the leader's).
602
- const existing = inFlightExecutions.get(cacheKey);
603
- if (existing) {
772
+ let existing = inFlightExecutions.get(cacheKey);
773
+ while (existing) {
604
774
  const remainingMs =
605
775
  IN_FLIGHT_LEADER_MAX_WAIT_MS - (Date.now() - existing.registeredAt);
606
776
  const raced =
@@ -622,8 +792,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
622
792
  "cache-read",
623
793
  `[use cache] "${id}" inflight-timeout`,
624
794
  );
625
- // Fall through to a fresh execution below (this call becomes leader).
626
- } else if (raced) {
795
+ break; // This call becomes the leader below.
796
+ }
797
+ // Leader rejected: its map entry is already cleared; run fresh.
798
+ if (!raced) break;
799
+ // Checked after the wait, so an invalidation made meanwhile counts too.
800
+ if (!invalidatedSince(requestCtx, raced.tags, existing.startSeq)) {
627
801
  try {
628
802
  return await serveCached({
629
803
  value: raced.serialized,
@@ -637,11 +811,16 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
637
811
  "cache-corrupt",
638
812
  `[use cache] "${id}" inflight-hit`,
639
813
  );
640
- // Fall through to a fresh execution below.
814
+ break; // Run fresh below.
641
815
  }
642
816
  }
643
- // raced === undefined: leader rejected; its map entry is already
644
- // cleared, so fall through to a fresh run.
817
+ // This request invalidated one of the leader's tags after it started
818
+ // (#973): its value may predate the invalidation. The leader cleared
819
+ // its entry before resolving, so an entry now is a newer execution
820
+ // (another caller that made this same decision): join it rather than
821
+ // start one more.
822
+ const next = inFlightExecutions.get(cacheKey);
823
+ existing = next !== existing ? next : undefined;
645
824
  }
646
825
 
647
826
  // This call becomes the leader. Register a deferred envelope so concurrent
@@ -658,9 +837,11 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
658
837
  // Followers attach their own catch; guard the map's own reference so a
659
838
  // rejected envelope with no waiter is not an unhandled rejection.
660
839
  envelopePromise.catch(() => {});
840
+ const startSeq = currentCacheSeq();
661
841
  inFlightExecutions.set(cacheKey, {
662
842
  promise: envelopePromise,
663
843
  registeredAt: Date.now(),
844
+ startSeq,
664
845
  });
665
846
  const clearSelf = (): void => {
666
847
  if (inFlightExecutions.get(cacheKey)?.promise === envelopePromise) {
@@ -669,14 +850,17 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
669
850
  };
670
851
 
671
852
  // execute, serialize, store
853
+ // Record only this execution's pushes (and cached functions it calls):
854
+ // the handler and loaders push into the same store concurrently, and they
855
+ // re-run on a hit.
672
856
  const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
673
- let capture: HandleCapture | undefined;
674
- let stopCapture: (() => void) | undefined;
675
- if (handleStore && hasTaintedArgs) {
676
- const c = startHandleCapture(handleStore);
677
- capture = c.capture;
678
- stopCapture = c.stop;
679
- }
857
+ const execScope = createCacheExecScope();
858
+ const capture = handleStore
859
+ ? startHandleCapture(handleStore, {
860
+ accept: () => isInCacheExecChain(execScope),
861
+ key: ownerKeyed ? useCacheRecordKey() : undefined,
862
+ })
863
+ : undefined;
680
864
 
681
865
  // Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
682
866
  // inside the cached function body (those side effects are lost on hit).
@@ -702,7 +886,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
702
886
  let scoped: ReturnType<typeof runWithCacheTagScope>;
703
887
  try {
704
888
  scoped = runWithCacheTagScope(() =>
705
- runWithCacheExecScope(() => fn.apply(this, args)),
889
+ runWithCacheExecScope(() => fn.apply(this, args), execScope),
706
890
  );
707
891
  result = await scoped.result;
708
892
  } catch (execError) {
@@ -717,7 +901,7 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
717
901
  unstampCacheExec(arg as object);
718
902
  }
719
903
  // Remove this capture token (order-independent, safe for concurrent use)
720
- stopCapture?.();
904
+ capture?.stop();
721
905
  }
722
906
 
723
907
  // Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
@@ -735,11 +919,19 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
735
919
  const finalizeAndWrite = async (): Promise<void> => {
736
920
  let serialized: string | null;
737
921
  let encodedHandles: string | undefined;
922
+ const flightErrors: unknown[] = [];
923
+ const onFlightError = (error: unknown): void => {
924
+ flightErrors.push(error);
925
+ };
738
926
  try {
739
- serialized = await serializeResult(result);
740
- encodedHandles = capture?.data
741
- ? await encodeHandles(capture.data)
927
+ serialized = await serializeResult(result, onFlightError);
928
+ encodedHandles = capture
929
+ ? await encodeHandles(capture.capture.data, onFlightError)
742
930
  : undefined;
931
+ // Flight encodes an async component that throws, or a rejected
932
+ // promise in the result or a handle value, as an error row and
933
+ // completes normally; stored, every hit would serve that error.
934
+ if (flightErrors.length > 0) throw flightErrors[0];
743
935
  } catch (buildError) {
744
936
  // Serialize/handle-encode failed: no envelope for followers (they run
745
937
  // fresh) and nothing to write.
@@ -762,6 +954,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
762
954
  // Hand followers the envelope before the store write so a slow/failed
763
955
  // write never stalls them.
764
956
  resolveEnvelope({ serialized, tags: allTags, handles: encodedHandles });
957
+ // This request invalidated one of the value's tags after the execution
958
+ // started (#973): the value may predate the invalidation, and written
959
+ // now it would outlive it. Skip the write; the next read runs fresh.
960
+ if (invalidatedSince(requestCtx, allTags, startSeq)) return;
765
961
  try {
766
962
  await store.setItem!(cacheKey, serialized, {
767
963
  handles: encodedHandles,