@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
@@ -15,9 +15,221 @@ import {
15
15
  _getRequestContext,
16
16
  type RequestContext,
17
17
  } from "../server/request-context.js";
18
+ import {
19
+ getCurrentLoaderBodyId,
20
+ getLoaderBodyTags,
21
+ isInsideLoaderScope,
22
+ type LoaderIdentityRead,
23
+ type LoaderIdentityReadVerb,
24
+ } from "../server/context.js";
25
+ import type { ExecutionStart } from "./tag-invalidation.js";
18
26
 
19
27
  const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
20
28
 
29
+ /**
30
+ * Each "use cache" tag scope's enclosing scope when it opened (#980). A tag
31
+ * recorded in a scope goes to every scope above it: an intermediate call can
32
+ * return a value holding a nested call still running, whose tags land after
33
+ * the intermediate call reported its own to its caller. Its caller's encode
34
+ * awaits the same promise, so the tags are in before it reads its own.
35
+ */
36
+ const scopeParents = new WeakMap<Set<string>, Set<string>>();
37
+
38
+ /** Add `tag` to `scope` and every scope enclosing it. */
39
+ function addToScopes(scope: Set<string> | undefined, tag: string): void {
40
+ for (let s = scope; s; s = scopeParents.get(s)) s.add(tag);
41
+ }
42
+
43
+ /**
44
+ * Call `fn` outside any "use cache" tag scope: detached work a cached body
45
+ * schedules (a stale entry's background refresh) records nothing onto the
46
+ * entry being filled.
47
+ */
48
+ export function outsideCacheTagScope<T>(fn: () => T): T {
49
+ return cacheTagStorage.exit(fn);
50
+ }
51
+
52
+ /**
53
+ * Tag owners (#957). A route cache() record replays its segments without
54
+ * running their handlers or re-rendering their server components, so the
55
+ * tags that content recorded must ride on the record, or a HIT (and the PPR
56
+ * capture that replays it) loses them.
57
+ *
58
+ * Every tag records onto its owner: the running loader body
59
+ * (getCurrentLoaderBodyId, server/context.ts), else the segment whose handler
60
+ * or record serialization is running (runInSegmentTagScope). No segment scope
61
+ * opens inside a loader body, so this is innermost-wins. A loader's value
62
+ * reaches a record only through the handler (or loader) that reads it, so
63
+ * ctx.use() links the reader to the loader (linkLoaderTags) whoever started
64
+ * it, and getSegmentTags follows those links. A loader nobody reads
65
+ * server-side (useLoader under loading()) stays off every record.
66
+ *
67
+ * The sets are keyed by the request context a tag records on, so a shell
68
+ * capture's derived context (Object.create(reqCtx)) never reads or feeds the
69
+ * foreground's.
70
+ *
71
+ * All of it runs only for a request that can write a record
72
+ * (RequestContext._recordTagOwners, armed by armRecordTagOwners at match
73
+ * time): elsewhere a handler call takes no scope, closure or allocation.
74
+ */
75
+ const segmentOwnerStorage = new AsyncLocalStorage<string>();
76
+
77
+ /**
78
+ * Arm the record tag owners for `ctx`: its match resolved a cache scope, so
79
+ * it can write a route cache() record (match-api.ts). A capture's or
80
+ * background re-render's derived context inherits the flag.
81
+ */
82
+ export function armRecordTagOwners(
83
+ ctx: RequestContext | undefined = _getRequestContext(),
84
+ ): void {
85
+ if (ctx) ctx._recordTagOwners = true;
86
+ }
87
+
88
+ /** Whether `ctx` records tag owners (see armRecordTagOwners). */
89
+ export function recordTagOwnersArmed(
90
+ ctx: RequestContext | undefined = _getRequestContext(),
91
+ ): boolean {
92
+ return ctx?._recordTagOwners === true;
93
+ }
94
+
95
+ interface RequestTagOwners {
96
+ /** owner key -> tags recorded while it ran */
97
+ tags: Map<string, Set<string>>;
98
+ /** owner key -> loader owner keys whose values it consumed */
99
+ uses: Map<string, Set<string>>;
100
+ }
101
+ const tagOwnersByRequest = new WeakMap<RequestContext, RequestTagOwners>();
102
+
103
+ const segmentOwner = (segmentId: string): string => `s:${segmentId}`;
104
+ const loaderOwner = (loaderId: string): string => `l:${loaderId}`;
105
+
106
+ function ownersFor(ctx: RequestContext): RequestTagOwners {
107
+ let owners = tagOwnersByRequest.get(ctx);
108
+ if (!owners) {
109
+ owners = { tags: new Map(), uses: new Map() };
110
+ tagOwnersByRequest.set(ctx, owners);
111
+ }
112
+ return owners;
113
+ }
114
+
115
+ function setFor(map: Map<string, Set<string>>, key: string): Set<string> {
116
+ let set = map.get(key);
117
+ if (!set) {
118
+ set = new Set();
119
+ map.set(key, set);
120
+ }
121
+ return set;
122
+ }
123
+
124
+ function currentOwner(): string | undefined {
125
+ const loaderId = getCurrentLoaderBodyId();
126
+ return loaderId !== undefined
127
+ ? loaderOwner(loaderId)
128
+ : segmentOwnerStorage.getStore();
129
+ }
130
+
131
+ /**
132
+ * Call `fn(arg)` inside segment `segmentId`'s tag scope: tags recorded while
133
+ * it or its async continuations run (outside a loader body) are owned by that
134
+ * segment. Unarmed, it is a plain call.
135
+ */
136
+ export function runInSegmentTagScope<R, A = undefined>(
137
+ segmentId: string,
138
+ fn: (arg: A) => R,
139
+ arg?: A,
140
+ ): R {
141
+ if (!recordTagOwnersArmed()) return fn(arg as A);
142
+ return segmentOwnerStorage.run(segmentOwner(segmentId), fn, arg as A);
143
+ }
144
+
145
+ /**
146
+ * Record that the current owner reads loader `loaderId`'s value (ctx.use),
147
+ * so the loader's tags follow the reader onto a record. The DSL funnel
148
+ * starting a loader (resolveLoaderData, inside the DSL loader scope but no
149
+ * loader body) is not a reader.
150
+ */
151
+ export function linkLoaderTags(
152
+ loaderId: string,
153
+ ctx: RequestContext | undefined = _getRequestContext(),
154
+ ): void {
155
+ if (!ctx || !recordTagOwnersArmed(ctx)) return;
156
+ const owner = currentOwner();
157
+ if (owner === undefined) return;
158
+ if (owner.startsWith("s:") && isInsideLoaderScope()) return;
159
+ setFor(ownersFor(ctx).uses, owner).add(loaderOwner(loaderId));
160
+ }
161
+
162
+ /**
163
+ * Record `tags` as loader `loaderId`'s own, read outside its body (its
164
+ * cache() config tags, or a loader-cache HIT's stored tags):
165
+ * recordRequestTags with an explicit owner.
166
+ */
167
+ export function recordLoaderTags(
168
+ loaderId: string,
169
+ tags: Iterable<string> | undefined,
170
+ ctx: RequestContext | undefined = _getRequestContext(),
171
+ ): void {
172
+ recordOwnedTags(
173
+ tags,
174
+ ctx,
175
+ recordTagOwnersArmed(ctx) ? loaderOwner(loaderId) : undefined,
176
+ );
177
+ }
178
+
179
+ /**
180
+ * The tags segment `segmentId`'s content recorded in `ctx`: its own, plus
181
+ * those of every loader it consumed, transitively.
182
+ */
183
+ export function getSegmentTags(
184
+ ctx: RequestContext,
185
+ segmentId: string,
186
+ ): ReadonlySet<string> {
187
+ const owners = tagOwnersByRequest.get(ctx);
188
+ const out = new Set<string>();
189
+ if (!owners) return out;
190
+ const seen = new Set<string>();
191
+ const pending = [segmentOwner(segmentId)];
192
+ for (let owner = pending.pop(); owner !== undefined; owner = pending.pop()) {
193
+ if (seen.has(owner)) continue;
194
+ seen.add(owner);
195
+ for (const tag of owners.tags.get(owner) ?? []) out.add(tag);
196
+ for (const used of owners.uses.get(owner) ?? []) pending.push(used);
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /**
202
+ * Record `tags` onto each of `segmentIds` (a route cache() HIT replaying
203
+ * those segments): a record written from the replay, such as a shell
204
+ * capture's doc record (match-middleware/cache-store.ts), then carries them
205
+ * as a fresh render's record would. Unarmed, a no-op.
206
+ */
207
+ export function recordSegmentTags(
208
+ segmentIds: readonly string[],
209
+ tags: Iterable<string> | undefined,
210
+ ctx: RequestContext | undefined = _getRequestContext(),
211
+ ): void {
212
+ if (!tags || !ctx || !recordTagOwnersArmed(ctx)) return;
213
+ for (const id of segmentIds) recordOwnedTags(tags, ctx, segmentOwner(id));
214
+ }
215
+
216
+ /**
217
+ * The owner of the loaders a shell capture bakes (loader-cache.ts bake
218
+ * lane): their data is shell material that no handler reads, so their tags
219
+ * reach the shell through this owner (shell-capture.ts), not a record.
220
+ */
221
+ export const SHELL_BAKE_TAG_OWNER: string = "#shell-bake";
222
+
223
+ /** Link loader `loaderId`'s tags to `ownerId` (see SHELL_BAKE_TAG_OWNER). */
224
+ export function linkLoaderTagsTo(
225
+ ownerId: string,
226
+ loaderId: string,
227
+ ctx: RequestContext | undefined = _getRequestContext(),
228
+ ): void {
229
+ if (!ctx || !recordTagOwnersArmed(ctx)) return;
230
+ setFor(ownersFor(ctx).uses, segmentOwner(ownerId)).add(loaderOwner(loaderId));
231
+ }
232
+
21
233
  export function normalizeTag(tag: string): string | null {
22
234
  // Trim and return the canonical (trimmed) form, not the raw tag. Both the
23
235
  // write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
@@ -57,14 +269,15 @@ export function normalizeTags(tags: Iterable<string>): string[] {
57
269
  * cache" in its tree. This is PPR's DERIVATIVE invalidation: PPR is
58
270
  * execution-PRESERVING (everything still runs underneath; only document bytes
59
271
  * are shortcut), so its tags ride this existing instrument rather than a
60
- * first-class ppr key/tag API. The shell-expiry invariant holds by
61
- * construction — baked ⇒ evicts (bake-lane loaders execute during capture and
62
- * record here), hole ⇒ fresh (masked loaders behind a renderable loading()
63
- * never execute during capture, so nothing under a hole can tag the shell).
64
- *
65
- * Inside a cache() DSL segment the render-callable form records at the DOCUMENT
66
- * level, not the segment (only the "use cache" runtime enters the tag scope) — a
67
- * documented semantic, not a filtered one. An empty/whitespace-only tag is
272
+ * first-class ppr key/tag API. Whatever renders into the shell records here,
273
+ * at capture or on the route's cache() record the capture replays. A masked
274
+ * live-lane loader never executes at capture, so its own tags reach a shell
275
+ * only through a handler that reads it (ctx.use).
276
+ *
277
+ * Inside a route cache() boundary the render-callable form also tags that
278
+ * route's cache() record, which re-records it on every HIT (#957). A loader's
279
+ * tags reach the record only when a handler consumes its value (ctx.use);
280
+ * see runInSegmentTagScope / linkLoaderTags. An empty/whitespace-only tag is
68
281
  * dropped in both forms (the render-callable form silently, via normalizeTags in
69
282
  * recordRequestTags; the scope form with a dev warning).
70
283
  *
@@ -98,7 +311,7 @@ export function cacheTag(...tags: string[]): void {
98
311
  }
99
312
  continue;
100
313
  }
101
- store.add(normalized);
314
+ addToScopes(store, normalized);
102
315
  }
103
316
  return;
104
317
  }
@@ -120,30 +333,273 @@ export function recordRequestTags(
120
333
  tags: Iterable<string> | undefined,
121
334
  ctx: RequestContext | undefined = _getRequestContext(),
122
335
  ): void {
336
+ recordOwnedTags(
337
+ tags,
338
+ ctx,
339
+ recordTagOwnersArmed(ctx) ? currentOwner() : undefined,
340
+ );
341
+ }
342
+
343
+ /**
344
+ * Recorded-tag sets (#964). A loader with its own cache() skips its body on a
345
+ * HIT, so its entry stores what its execution recorded, including the tags of
346
+ * every loader value it read, whoever ran that loader.
347
+ *
348
+ * Each loader execution records into its own set, held on its body scope
349
+ * (runInsideLoaderBodyScope); a loader-cache MISS opens one around its
350
+ * execution and value encodes (captureRecordedTags). The innermost wins. A
351
+ * read of a loader's value links the reader's set to the value's
352
+ * (readValueTags), and the entry write flattens the links
353
+ * (flattenRecordedTags) after its value settled.
354
+ *
355
+ * The per-execution sets and read links run only for a request that resolves
356
+ * a loader-cache binding (armLoaderTagSets); elsewhere a loader execution
357
+ * allocates no set and a read links nothing.
358
+ *
359
+ * The same sets carry the execution's request-identity reads (#972,
360
+ * recordLoaderIdentityRead in server/context.ts): an entry with no declared
361
+ * key refuses an execution whose links reach one (recordedIdentityRead).
362
+ */
363
+ interface TagCapture {
364
+ into: Set<string>;
365
+ /** The loader body set active when the capture opened. */
366
+ outer: Set<string> | undefined;
367
+ }
368
+ // On globalThis like the loader scopes (server/context.ts): the identity-read
369
+ // recorder is installed there too, so a second evaluated copy of this module
370
+ // (a duplicated package, a dev re-evaluation) must share the capture scope and
371
+ // the recorded reads, or its recorder writes where no fill looks.
372
+ const recordedTagCapture: AsyncLocalStorage<TagCapture> = ((globalThis as any)[
373
+ Symbol.for("rangojs-router:recorded-tag-capture")
374
+ ] ??= new AsyncLocalStorage<TagCapture>());
375
+ const tagLinks = new WeakMap<Set<string>, Set<Set<string>>>();
376
+ const loaderValueTags = new WeakMap<object, Set<string>>();
377
+ const identityReads: WeakMap<Set<string>, LoaderIdentityRead> = ((
378
+ globalThis as any
379
+ )[Symbol.for("rangojs-router:identity-reads")] ??= new WeakMap());
380
+
381
+ /**
382
+ * Arm the per-execution loader tag sets for `ctx`: its match resolved a
383
+ * loader bound with its own cache() (match-api.ts, bindsLoaderCache), or such
384
+ * a binding started (loader-cache.ts). A loader-cache MISS captures its own
385
+ * execution and encodes either way.
386
+ */
387
+ export function armLoaderTagSets(
388
+ ctx: RequestContext | undefined = _getRequestContext(),
389
+ ): void {
390
+ if (ctx) ctx._recordLoaderTags = true;
391
+ }
392
+
393
+ /** Whether `ctx` records per-execution loader tag sets (armLoaderTagSets). */
394
+ export function loaderTagSetsArmed(ctx: RequestContext | undefined): boolean {
395
+ return ctx?._recordLoaderTags === true;
396
+ }
397
+
398
+ function activeTagSet(): Set<string> | undefined {
399
+ const capture = recordedTagCapture.getStore();
400
+ const body = getLoaderBodyTags();
401
+ if (capture && capture.outer === body) return capture.into;
402
+ return body ?? capture?.into;
403
+ }
404
+
405
+ /**
406
+ * Call `fn`, adding every tag recorded in its async chain
407
+ * (recordRequestTags, recordLoaderTags) to `into`, until a loader body
408
+ * started inside it takes over with its own set.
409
+ */
410
+ export function captureRecordedTags<T>(into: Set<string>, fn: () => T): T {
411
+ return recordedTagCapture.run({ into, outer: getLoaderBodyTags() }, fn);
412
+ }
413
+
414
+ /** `into` also answers for every tag `from` records. */
415
+ export function linkRecordedTags(into: Set<string>, from: Set<string>): void {
416
+ if (into === from) return;
417
+ let links = tagLinks.get(into);
418
+ if (!links) tagLinks.set(into, (links = new Set()));
419
+ links.add(from);
420
+ }
421
+
422
+ /** Mark a loader's value promise as carrying `tags` (see readValueTags). */
423
+ export function tagLoaderValue(value: object, tags: Set<string>): void {
424
+ loaderValueTags.set(value, tags);
425
+ }
426
+
427
+ /** A loader value's tags also answer for `tags` (tagLoaderValue). */
428
+ export function linkValueTags(value: object, tags: Set<string>): void {
429
+ const own = loaderValueTags.get(value);
430
+ if (own) linkRecordedTags(own, tags);
431
+ }
432
+
433
+ /** The current execution reads a loader's value: it takes on its tags. */
434
+ export function readValueTags(value: object): void {
435
+ const tags = loaderValueTags.get(value);
123
436
  if (!tags) return;
124
- const set = ctx?._requestTags;
125
- if (!set) return;
437
+ const into = activeTagSet();
438
+ if (into) linkRecordedTags(into, tags);
439
+ }
440
+
441
+ // recordLoaderIdentityRead (server/context.ts) calls this through the shared
442
+ // key, not an import: a test mocking context.ts would break module init. First
443
+ // read per execution: the fill's error names it.
444
+ (globalThis as Record<symbol, unknown>)[
445
+ Symbol.for("rangojs-router:identity-read-recorder")
446
+ ] = (surface: string, verb: LoaderIdentityReadVerb): void => {
447
+ const set = activeTagSet();
448
+ if (set === undefined || identityReads.has(set)) return;
449
+ identityReads.set(set, { surface, verb, bodyId: getCurrentLoaderBodyId() });
450
+ };
451
+
452
+ /**
453
+ * A loader-cache HIT restores the read its entry's MISS recorded onto the
454
+ * value's set, so an unkeyed reader of the value refuses it as it would on
455
+ * the MISS (loader-cache.ts identity mark).
456
+ */
457
+ export function markIdentityRead(
458
+ set: Set<string>,
459
+ read: LoaderIdentityRead,
460
+ ): void {
461
+ if (!identityReads.has(set)) identityReads.set(set, read);
462
+ }
463
+
464
+ /**
465
+ * The identity read `root`'s execution recorded, or one a set it links to
466
+ * recorded, nearest first (breadth-first, so the execution's own read wins).
467
+ */
468
+ export function recordedIdentityRead(
469
+ root: Set<string>,
470
+ ): LoaderIdentityRead | undefined {
471
+ const seen = new Set<Set<string>>();
472
+ const queue = [root];
473
+ for (let i = 0; i < queue.length; i++) {
474
+ const set = queue[i]!;
475
+ if (seen.has(set)) continue;
476
+ seen.add(set);
477
+ const read = identityReads.get(set);
478
+ if (read) return read;
479
+ for (const linked of tagLinks.get(set) ?? []) queue.push(linked);
480
+ }
481
+ return undefined;
482
+ }
483
+
484
+ /** `root` and every set it links to, transitively. */
485
+ function linkedSets(root: Set<string>): Set<Set<string>> {
486
+ const seen = new Set<Set<string>>();
487
+ const pending = [root];
488
+ for (let set = pending.pop(); set !== undefined; set = pending.pop()) {
489
+ if (seen.has(set)) continue;
490
+ seen.add(set);
491
+ for (const linked of tagLinks.get(set) ?? []) pending.push(linked);
492
+ }
493
+ return seen;
494
+ }
495
+
496
+ /** `root`'s tags plus those of every set it links to, transitively. */
497
+ export function flattenRecordedTags(root: Set<string>): Set<string> {
498
+ const out = new Set<string>();
499
+ for (const set of linkedSets(root)) {
500
+ for (const tag of set) out.add(tag);
501
+ }
502
+ return out;
503
+ }
504
+
505
+ /**
506
+ * Where each execution that records into a set started (#977): a loader
507
+ * execution (createLoaderExecutor), a loader-cache MISS or refresh, a
508
+ * loader-cache binding's value.
509
+ */
510
+ const tagSetStarts = new WeakMap<Set<string>, ExecutionStart>();
511
+
512
+ /** Record where the execution recording into `set` started. */
513
+ export function markTagSetStart(set: Set<string>, start: ExecutionStart): void {
514
+ tagSetStarts.set(set, start);
515
+ }
516
+
517
+ /**
518
+ * The earliest start among `root` and every set it links to: a value built
519
+ * from another execution's value is as old as the oldest of them. A
520
+ * loader-cache write gates on it (loader-cache.ts), because the loader value
521
+ * it stores can be a run a reader started before the binding executed, and
522
+ * the values that run read can be older still.
523
+ */
524
+ export function earliestRecordedStart(
525
+ root: Set<string>,
526
+ ): ExecutionStart | undefined {
527
+ let earliest: ExecutionStart | undefined;
528
+ for (const set of linkedSets(root)) {
529
+ const start = tagSetStarts.get(set);
530
+ if (!start) continue;
531
+ earliest = earliest
532
+ ? {
533
+ seq: Math.min(earliest.seq, start.seq),
534
+ at: Math.min(earliest.at, start.at),
535
+ }
536
+ : start;
537
+ }
538
+ return earliest;
539
+ }
540
+
541
+ /**
542
+ * recordRequestTags, also recording onto `owner`'s set (#957), the active
543
+ * loader tag set (#964), and the enclosing "use cache" execution's tag
544
+ * scope (#980).
545
+ *
546
+ * The last one is how a nested "use cache" call's tags reach the entry that
547
+ * bakes its value: the inner wrapper records its tags (a miss's, a hit's
548
+ * stored ones, a joined execution's) after its own scope closed, so the
549
+ * scope current then is the caller's. Without it an outer entry kept the
550
+ * inner value past updateTag() of an inner tag, and the same-request gate
551
+ * (tag-invalidation.ts invalidatedSince) did not see the outer execution
552
+ * as tagged by it.
553
+ */
554
+ function recordOwnedTags(
555
+ tags: Iterable<string> | undefined,
556
+ ctx: RequestContext | undefined,
557
+ owner: string | undefined,
558
+ ): void {
559
+ if (!tags || !ctx?._requestTags) return;
560
+ const set = ctx._requestTags;
561
+ const captured = activeTagSet();
562
+ const enclosing = cacheTagStorage.getStore();
563
+ let owned: Set<string> | undefined;
126
564
  for (const tag of tags) {
127
565
  const normalized = normalizeTag(tag);
128
- if (normalized !== null) set.add(normalized);
566
+ if (normalized === null) continue;
567
+ set.add(normalized);
568
+ captured?.add(normalized);
569
+ addToScopes(enclosing, normalized);
570
+ if (owner !== undefined) {
571
+ owned ??= setFor(ownersFor(ctx).tags, owner);
572
+ owned.add(normalized);
573
+ }
129
574
  }
130
575
  }
131
576
 
132
577
  /**
133
- * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
134
- * accumulate into the returned Set.
578
+ * Run a function within a cache tag scope. Any cacheTag() calls inside `fn`,
579
+ * and the tags a nested "use cache" call records (recordOwnedTags, #980),
580
+ * accumulate into the returned Set and every scope enclosing it
581
+ * (scopeParents).
135
582
  *
136
583
  * The returned Set is the LIVE reference - the caller must await `result`
137
584
  * before reading `tags`, because an async cached function may call cacheTag()
138
- * after an await boundary.
585
+ * after an await boundary. Pass `tagSet` to re-enter an execution's scope
586
+ * (its result's Flight encode, #980); it keeps the parent it opened under.
139
587
  *
140
588
  * @internal Used by cache-runtime.ts to wrap "use cache" execution.
141
589
  */
142
- export function runWithCacheTagScope<T>(fn: () => T): {
590
+ export function runWithCacheTagScope<T>(
591
+ fn: () => T,
592
+ tagSet?: Set<string>,
593
+ ): {
143
594
  result: T;
144
595
  tags: Set<string>;
145
596
  } {
146
- const tagSet = new Set<string>();
147
- const result = cacheTagStorage.run(tagSet, fn);
148
- return { result, tags: tagSet };
597
+ let scope = tagSet;
598
+ if (!scope) {
599
+ scope = new Set<string>();
600
+ const parent = cacheTagStorage.getStore();
601
+ if (parent) scopeParents.set(scope, parent);
602
+ }
603
+ const result = cacheTagStorage.run(scope, fn);
604
+ return { result, tags: scope };
149
605
  }
@@ -7,9 +7,28 @@
7
7
  // (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
8
8
  const FROM_CHARCODE_CHUNK = 8192;
9
9
 
10
+ /**
11
+ * TC39 Uint8Array base64 methods (workerd, V8 >= 14; not Node 22/24). Same
12
+ * alphabet, padding, and invalid-character throw as btoa/atob, and about 15x
13
+ * faster than the per-byte loop on a 629 KB PPR prelude in workerd (0.05 ms vs
14
+ * 0.75 ms, issue #941). Looked up per call, not captured at import, so a
15
+ * polyfill or test spy installed later is the one that runs.
16
+ */
17
+ type NativeUint8ArrayBase64 = {
18
+ fromBase64?: (b64: string) => Uint8Array;
19
+ prototype: { toBase64?: (this: Uint8Array) => string };
20
+ };
21
+ const NativeUint8Array = Uint8Array as unknown as NativeUint8ArrayBase64;
22
+
10
23
  /** Encode ArrayBuffer to base64 string. */
11
24
  export function bufferToBase64(buffer: ArrayBuffer): string {
12
- const bytes = new Uint8Array(buffer);
25
+ return bytesToBase64(new Uint8Array(buffer));
26
+ }
27
+
28
+ /** Encode bytes (a view's own region only) to a base64 string. */
29
+ export function bytesToBase64(bytes: Uint8Array): string {
30
+ const nativeToBase64 = NativeUint8Array.prototype.toBase64;
31
+ if (nativeToBase64) return nativeToBase64.call(bytes);
13
32
  // Build the binary (latin1) string in fixed-size chunks instead of one
14
33
  // String.fromCharCode per byte. Identical output to the per-byte loop (each
15
34
  // byte maps to the same code unit); just far fewer string concatenations for
@@ -22,12 +41,25 @@ export function bufferToBase64(buffer: ArrayBuffer): string {
22
41
  return btoa(binary);
23
42
  }
24
43
 
25
- /** Decode base64 string to ArrayBuffer. */
26
- export function base64ToBuffer(base64: string): ArrayBuffer {
44
+ /** Decode a base64 string to bytes. Throws on a character outside the alphabet. */
45
+ export function base64ToBytes(base64: string): Uint8Array {
46
+ if (NativeUint8Array.fromBase64) return NativeUint8Array.fromBase64(base64);
27
47
  const binary = atob(base64);
28
48
  const bytes = new Uint8Array(binary.length);
29
49
  for (let i = 0; i < binary.length; i++) {
30
50
  bytes[i] = binary.charCodeAt(i);
31
51
  }
32
- return bytes.buffer;
52
+ return bytes;
53
+ }
54
+
55
+ /** The byte length `base64` decodes to (padded or not), without decoding. */
56
+ export function base64ByteLength(base64: string): number {
57
+ let length = base64.length;
58
+ while (length > 0 && base64.charCodeAt(length - 1) === 61 /* = */) length--;
59
+ return Math.floor((length * 3) / 4);
60
+ }
61
+
62
+ /** Decode base64 string to ArrayBuffer. */
63
+ export function base64ToBuffer(base64: string): ArrayBuffer {
64
+ return base64ToBytes(base64).buffer as ArrayBuffer;
33
65
  }
@@ -132,3 +132,15 @@ export const EDGE_READ_TIMEOUT_MS = 20;
132
132
  * Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
133
133
  */
134
134
  export const KV_READ_TIMEOUT_MS = 170;
135
+
136
+ /**
137
+ * Floor (ms) on the budget for reading a PPR shell's snapshot, the rest of a
138
+ * frame whose head and prelude were already read. A document HIT has sent
139
+ * its prelude by then and cannot render the rest without the snapshot's doc
140
+ * record, so a timed-out read degrades the HIT (the entry is replaced, the
141
+ * page reloads once into a MISS; rsc-rendering.ts degradeUnreplayableShell).
142
+ * That costs far more than waiting, so a slow body read gets at least this
143
+ * long; `kvReadTimeoutMs` above it wins, and `kvReadTimeoutMs <= 0` still
144
+ * disables the bound.
145
+ */
146
+ export const SHELL_SNAPSHOT_READ_MIN_TIMEOUT_MS = 1000;