@rangojs/router 0.18.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 (244) hide show
  1. package/dist/types/browser/event-controller.d.ts +3 -3
  2. package/dist/types/browser/history-state.d.ts +2 -0
  3. package/dist/types/browser/navigation-transaction.d.ts +20 -2
  4. package/dist/types/browser/partial-update.d.ts +16 -1
  5. package/dist/types/browser/prefetch/cache.d.ts +6 -6
  6. package/dist/types/browser/react/Link.d.ts +19 -9
  7. package/dist/types/browser/react/context.d.ts +2 -2
  8. package/dist/types/browser/react/location-state-shared.d.ts +95 -56
  9. package/dist/types/browser/react/location-state.d.ts +1 -1
  10. package/dist/types/browser/transition-when.d.ts +80 -0
  11. package/dist/types/browser/types.d.ts +104 -9
  12. package/dist/types/build/generate-manifest.d.ts +8 -0
  13. package/dist/types/cache/background-task.d.ts +24 -2
  14. package/dist/types/cache/cache-exec-scope.d.ts +31 -0
  15. package/dist/types/cache/cache-key-utils.d.ts +44 -13
  16. package/dist/types/cache/cache-policy.d.ts +1 -1
  17. package/dist/types/cache/cache-runtime.d.ts +7 -0
  18. package/dist/types/cache/cache-scope.d.ts +117 -35
  19. package/dist/types/cache/cache-tag.d.ts +24 -4
  20. package/dist/types/cache/cf/cf-cache-store.d.ts +55 -44
  21. package/dist/types/cache/cf/cf-shell-frame.d.ts +1 -1
  22. package/dist/types/cache/cf/cf-tag-marker-memo.d.ts +0 -15
  23. package/dist/types/cache/document-cache.d.ts +10 -0
  24. package/dist/types/cache/handle-snapshot.d.ts +40 -8
  25. package/dist/types/cache/invalidation-order.d.ts +23 -0
  26. package/dist/types/cache/request-tag-mask.d.ts +54 -0
  27. package/dist/types/cache/shell-snapshot.d.ts +88 -141
  28. package/dist/types/cache/tag-invalidation.d.ts +38 -14
  29. package/dist/types/cache/types.d.ts +68 -61
  30. package/dist/types/cache/vercel/vercel-cache-store.d.ts +23 -16
  31. package/dist/types/client-urls/navigation.d.ts +28 -2
  32. package/dist/types/client-urls/server-projection.d.ts +5 -2
  33. package/dist/types/client-urls/types.d.ts +5 -5
  34. package/dist/types/client.d.ts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/types/index.rsc.d.ts +1 -1
  37. package/dist/types/route-definition/helpers-types.d.ts +11 -11
  38. package/dist/types/route-name.d.ts +7 -0
  39. package/dist/types/router/error-handling.d.ts +1 -1
  40. package/dist/types/router/intercept-resolution.d.ts +2 -0
  41. package/dist/types/router/match-context.d.ts +5 -0
  42. package/dist/types/router/match-middleware/background-revalidation.d.ts +0 -4
  43. package/dist/types/router/router-options.d.ts +4 -1
  44. package/dist/types/router/segment-resolution/loader-cache.d.ts +18 -1
  45. package/dist/types/router/segment-resolution/loader-mask.d.ts +3 -4
  46. package/dist/types/router/segment-resolution/loader-snapshot.d.ts +14 -40
  47. package/dist/types/router/segment-resolution/mask-nested.d.ts +11 -3
  48. package/dist/types/router/segment-resolution/view-transition-default.d.ts +7 -5
  49. package/dist/types/router/transition-when.d.ts +24 -10
  50. package/dist/types/rsc/attach-transition-when.d.ts +18 -0
  51. package/dist/types/rsc/full-payload.d.ts +14 -7
  52. package/dist/types/rsc/response-cache-serve.d.ts +15 -12
  53. package/dist/types/rsc/shell-build-manifest.d.ts +4 -3
  54. package/dist/types/rsc/shell-capture-constants.d.ts +77 -20
  55. package/dist/types/rsc/shell-capture.d.ts +78 -81
  56. package/dist/types/rsc/shell-serve.d.ts +19 -48
  57. package/dist/types/rsc/types.d.ts +2 -0
  58. package/dist/types/segment-system.d.ts +7 -0
  59. package/dist/types/server/context.d.ts +89 -61
  60. package/dist/types/server/cookie-store.d.ts +30 -18
  61. package/dist/types/server/handle-store.d.ts +52 -1
  62. package/dist/types/server/request-context.d.ts +66 -45
  63. package/dist/types/server/request-headers.d.ts +44 -0
  64. package/dist/types/server.d.ts +1 -0
  65. package/dist/types/testing/e2e/index.d.ts +1 -1
  66. package/dist/types/testing/index.d.ts +3 -2
  67. package/dist/types/testing/internal/action-id.d.ts +7 -0
  68. package/dist/types/testing/location-state-key.d.ts +24 -0
  69. package/dist/types/testing/render-route.d.ts +17 -1
  70. package/dist/types/testing/run-transition-when.d.ts +44 -69
  71. package/dist/types/testing/serve-shell-request.d.ts +7 -6
  72. package/dist/types/testing/shell-status.d.ts +34 -22
  73. package/dist/types/transition-when-ref.d.ts +79 -0
  74. package/dist/types/types/cache-types.d.ts +15 -3
  75. package/dist/types/types/index.d.ts +1 -1
  76. package/dist/types/types/segments.d.ts +86 -71
  77. package/dist/types/urls/path-helper-types.d.ts +3 -4
  78. package/dist/types/urls/pattern-types.d.ts +3 -4
  79. package/dist/types/vite/plugins/transition-when-hoist.d.ts +45 -0
  80. package/dist/vite/index.js +960 -97
  81. package/package.json +1 -1
  82. package/skills/cache-guide/SKILL.md +76 -49
  83. package/skills/caching/SKILL.md +167 -69
  84. package/skills/client-urls/SKILL.md +1 -1
  85. package/skills/comparison/references/framework-comparison.md +8 -5
  86. package/skills/document-cache/SKILL.md +24 -0
  87. package/skills/hooks/SKILL.md +3 -1
  88. package/skills/hooks/navigation.md +23 -2
  89. package/skills/hooks/state.md +98 -6
  90. package/skills/intercept/SKILL.md +29 -12
  91. package/skills/loader/SKILL.md +52 -21
  92. package/skills/middleware/SKILL.md +18 -0
  93. package/skills/migrate-nextjs/SKILL.md +3 -3
  94. package/skills/observability/SKILL.md +6 -5
  95. package/skills/parallel/SKILL.md +1 -1
  96. package/skills/ppr/SKILL.md +129 -77
  97. package/skills/prerender/SKILL.md +3 -3
  98. package/skills/rango/SKILL.md +25 -6
  99. package/skills/route/SKILL.md +2 -0
  100. package/skills/testing/SKILL.md +27 -25
  101. package/skills/testing/cache-prerender.md +9 -9
  102. package/skills/testing/client-components.md +45 -3
  103. package/skills/testing/loader.md +3 -2
  104. package/skills/testing/middleware.md +1 -0
  105. package/skills/testing/predicates.md +49 -49
  106. package/skills/testing/render-handler.md +1 -0
  107. package/skills/testing/serve-shell-request.md +14 -0
  108. package/skills/testing/server-actions.md +9 -3
  109. package/skills/theme/SKILL.md +8 -4
  110. package/skills/typesafety/env-and-bindings.md +8 -0
  111. package/skills/use-cache/SKILL.md +64 -19
  112. package/skills/view-transitions/SKILL.md +150 -29
  113. package/src/__augment-tests__/augmented.check.ts +23 -0
  114. package/src/browser/event-controller.ts +4 -4
  115. package/src/browser/history-state.ts +52 -1
  116. package/src/browser/link-interceptor.ts +4 -0
  117. package/src/browser/navigation-bridge.ts +77 -18
  118. package/src/browser/navigation-client.ts +4 -3
  119. package/src/browser/navigation-store.ts +43 -1
  120. package/src/browser/navigation-transaction.ts +31 -11
  121. package/src/browser/partial-update.ts +146 -35
  122. package/src/browser/prefetch/cache.ts +6 -6
  123. package/src/browser/prefetch/fetch.ts +2 -2
  124. package/src/browser/react/Link.tsx +38 -33
  125. package/src/browser/react/NavigationProvider.tsx +2 -2
  126. package/src/browser/react/context.ts +2 -2
  127. package/src/browser/react/location-state-shared.ts +241 -49
  128. package/src/browser/react/location-state.ts +5 -2
  129. package/src/browser/rsc-router.tsx +4 -0
  130. package/src/browser/server-action-bridge.ts +84 -15
  131. package/src/browser/transition-when.ts +205 -0
  132. package/src/browser/types.ts +124 -10
  133. package/src/build/generate-manifest.ts +77 -1
  134. package/src/cache/background-task.ts +55 -2
  135. package/src/cache/cache-exec-scope.ts +48 -1
  136. package/src/cache/cache-key-utils.ts +56 -16
  137. package/src/cache/cache-policy.ts +9 -4
  138. package/src/cache/cache-runtime.ts +197 -144
  139. package/src/cache/cache-scope.ts +304 -112
  140. package/src/cache/cache-tag.ts +105 -13
  141. package/src/cache/cf/cf-cache-store.ts +152 -76
  142. package/src/cache/cf/cf-shell-frame.ts +2 -2
  143. package/src/cache/cf/cf-tag-marker-memo.ts +11 -100
  144. package/src/cache/document-cache.ts +53 -7
  145. package/src/cache/handle-snapshot.ts +52 -9
  146. package/src/cache/invalidation-order.ts +84 -0
  147. package/src/cache/request-tag-mask.ts +147 -0
  148. package/src/cache/shell-snapshot.ts +130 -348
  149. package/src/cache/tag-invalidation.ts +57 -51
  150. package/src/cache/types.ts +71 -67
  151. package/src/cache/vercel/vercel-cache-store.ts +61 -58
  152. package/src/client-urls/client-root.tsx +8 -4
  153. package/src/client-urls/client-urls.ts +8 -6
  154. package/src/client-urls/navigation.ts +55 -4
  155. package/src/client-urls/server-projection.ts +31 -10
  156. package/src/client-urls/types.ts +12 -5
  157. package/src/client.tsx +2 -0
  158. package/src/host/cookie-handler.ts +2 -1
  159. package/src/index.rsc.ts +3 -0
  160. package/src/index.ts +3 -0
  161. package/src/loader-redirect.tsx +7 -3
  162. package/src/prerender/build-shell-capture.ts +155 -146
  163. package/src/route-definition/dsl-helpers.ts +2 -0
  164. package/src/route-definition/helpers-types.ts +11 -11
  165. package/src/route-name.ts +12 -0
  166. package/src/router/content-negotiation.ts +4 -1
  167. package/src/router/error-handling.ts +5 -2
  168. package/src/router/handler-context.ts +10 -7
  169. package/src/router/intercept-resolution.ts +11 -1
  170. package/src/router/loader-resolution.ts +28 -3
  171. package/src/router/logging.ts +5 -3
  172. package/src/router/match-api.ts +62 -34
  173. package/src/router/match-context.ts +5 -0
  174. package/src/router/match-handlers.ts +9 -29
  175. package/src/router/match-middleware/background-revalidation.ts +3 -5
  176. package/src/router/match-middleware/cache-lookup.ts +35 -19
  177. package/src/router/match-middleware/cache-store.ts +15 -9
  178. package/src/router/match-result.ts +1 -0
  179. package/src/router/middleware.ts +2 -5
  180. package/src/router/navigation-snapshot.ts +6 -5
  181. package/src/router/prerender-match.ts +4 -0
  182. package/src/router/request-classification.ts +7 -4
  183. package/src/router/router-options.ts +4 -1
  184. package/src/router/segment-resolution/fresh.ts +0 -4
  185. package/src/router/segment-resolution/loader-cache.ts +150 -25
  186. package/src/router/segment-resolution/loader-mask.ts +3 -4
  187. package/src/router/segment-resolution/loader-snapshot.ts +15 -78
  188. package/src/router/segment-resolution/mask-nested.ts +47 -22
  189. package/src/router/segment-resolution/revalidation.ts +0 -3
  190. package/src/router/segment-resolution/view-transition-default.ts +6 -21
  191. package/src/router/telemetry.ts +6 -3
  192. package/src/router/transition-when.ts +34 -52
  193. package/src/router.ts +2 -1
  194. package/src/rsc/attach-transition-when.ts +47 -0
  195. package/src/rsc/full-payload.ts +30 -10
  196. package/src/rsc/handler.ts +12 -10
  197. package/src/rsc/loader-fetch.ts +2 -1
  198. package/src/rsc/manifest-init.ts +4 -0
  199. package/src/rsc/origin-guard.ts +8 -5
  200. package/src/rsc/progressive-enhancement.ts +7 -30
  201. package/src/rsc/response-cache-serve.ts +68 -80
  202. package/src/rsc/response-route-handler.ts +2 -2
  203. package/src/rsc/rsc-rendering.ts +206 -252
  204. package/src/rsc/server-action.ts +8 -20
  205. package/src/rsc/shell-build-manifest.ts +22 -20
  206. package/src/rsc/shell-capture-constants.ts +136 -20
  207. package/src/rsc/shell-capture.ts +352 -589
  208. package/src/rsc/shell-serve.ts +73 -109
  209. package/src/rsc/ssr-setup.ts +3 -2
  210. package/src/rsc/types.ts +2 -0
  211. package/src/segment-system.tsx +40 -7
  212. package/src/server/context.ts +189 -124
  213. package/src/server/cookie-store.ts +153 -127
  214. package/src/server/handle-store.ts +245 -27
  215. package/src/server/request-context.ts +123 -84
  216. package/src/server/request-headers.ts +134 -0
  217. package/src/server.ts +4 -0
  218. package/src/testing/dispatch.ts +4 -7
  219. package/src/testing/e2e/index.ts +1 -0
  220. package/src/testing/index.ts +5 -0
  221. package/src/testing/internal/action-id.ts +25 -0
  222. package/src/testing/internal/context.ts +1 -1
  223. package/src/testing/location-state-key.ts +45 -0
  224. package/src/testing/render-handler.ts +1 -1
  225. package/src/testing/render-route.tsx +109 -16
  226. package/src/testing/run-client-revalidate.ts +3 -19
  227. package/src/testing/run-loader.ts +5 -1
  228. package/src/testing/run-transition-when.ts +109 -172
  229. package/src/testing/serve-shell-request.ts +10 -7
  230. package/src/testing/shell-status.ts +60 -56
  231. package/src/transition-when-ref.ts +220 -0
  232. package/src/types/cache-types.ts +15 -3
  233. package/src/types/index.ts +3 -0
  234. package/src/types/segments.ts +94 -91
  235. package/src/urls/path-helper-types.ts +3 -4
  236. package/src/urls/pattern-types.ts +3 -4
  237. package/src/vite/discovery/discover-routers.ts +3 -0
  238. package/src/vite/discovery/shell-prerender-phase.ts +0 -2
  239. package/src/vite/discovery/virtual-module-codegen.ts +16 -6
  240. package/src/vite/plugins/transition-when-hoist.ts +1064 -0
  241. package/src/vite/rango.ts +2 -0
  242. package/src/vite/router-discovery.ts +75 -5
  243. package/dist/types/rsc/transition-gate.d.ts +0 -23
  244. package/src/rsc/transition-gate.ts +0 -86
@@ -11,12 +11,20 @@
11
11
  import type { UrlPatterns } from "../urls.js";
12
12
  import type { AllUseItems } from "../route-types.js";
13
13
  import { extractStaticPrefix } from "../router/pattern-matching.js";
14
- import { RangoContext, runWithPrefixes } from "../server/context.js";
14
+ import {
15
+ getParallelSlotEntries,
16
+ RangoContext,
17
+ runWithPrefixes,
18
+ } from "../server/context.js";
15
19
  import type { EntryData, TrackedInclude } from "../server/context.js";
16
20
  import type { TrailingSlashMode } from "../types.js";
17
21
  import { createRouteHelpers } from "../route-definition.js";
18
22
  import MapRootLayout from "../server/root-layout.js";
19
23
  import { collectFallbackClientRefs } from "./collect-fallback-refs.js";
24
+ import {
25
+ createTransitionWhenError,
26
+ transitionWhenProblem,
27
+ } from "../transition-when-ref.js";
20
28
  import {
21
29
  isIncludeProvider,
22
30
  resolveIncludeModule,
@@ -59,6 +67,57 @@ export interface GeneratedManifest {
59
67
  routeSearchSchemas?: Record<string, Record<string, string>>;
60
68
  }
61
69
 
70
+ /**
71
+ * Fail discovery on an invalid transition({ when }) anywhere in the entry
72
+ * chains of the evaluated routes: the route itself, its ancestors, their
73
+ * orphan layouts, parallel slots and intercepts. Strict: a plain function is
74
+ * rejected, because discovery evaluates the tree in the react-server graph
75
+ * where a "use client" import is a client reference (transition-when-ref.ts).
76
+ * The error names the first route found through the entry.
77
+ */
78
+ function validateTransitionWhens(
79
+ manifest: Map<string, EntryData>,
80
+ patterns: Map<string, string>,
81
+ ): void {
82
+ const seen = new Set<object>();
83
+ for (const [routeName, routeEntry] of manifest) {
84
+ const route = {
85
+ routeName,
86
+ pattern:
87
+ patterns.get(routeName) ??
88
+ (routeEntry.type === "route" ? routeEntry.pattern : undefined),
89
+ };
90
+ const check = (when: unknown, entryType: string): void => {
91
+ const problem = transitionWhenProblem(when, true);
92
+ if (problem) {
93
+ throw createTransitionWhenError(when, problem, {
94
+ ...route,
95
+ entryType,
96
+ });
97
+ }
98
+ };
99
+ const visit = (entry: EntryData): void => {
100
+ if (seen.has(entry)) return;
101
+ seen.add(entry);
102
+ check(entry.transition?.when, entry.type);
103
+ for (const orphan of entry.layout) visit(orphan);
104
+ for (const { entry: slot } of getParallelSlotEntries(entry.parallel)) {
105
+ visit(slot);
106
+ }
107
+ for (const intercept of entry.intercept) {
108
+ check(intercept.transition?.when, "intercept");
109
+ }
110
+ };
111
+ for (
112
+ let entry: EntryData | null | undefined = routeEntry;
113
+ entry;
114
+ entry = entry.parent ?? entry.orphanOwner
115
+ ) {
116
+ visit(entry);
117
+ }
118
+ }
119
+ }
120
+
62
121
  /**
63
122
  * Build prefix tree node by running the patterns with proper context.
64
123
  * Uses a visited set to detect circular includes and prevent infinite recursion.
@@ -116,6 +175,7 @@ async function buildPrefixTreeNode(
116
175
  responseTypeRoutes?: Record<string, string>,
117
176
  routeSearchSchemas?: Record<string, Record<string, string>>,
118
177
  routerId?: string,
178
+ validateTransitionWhen?: boolean,
119
179
  ): Promise<PrefixTreeNode> {
120
180
  // Resolve an async include provider (`() => import("./routes")`) so its routes
121
181
  // are walked into the build-time manifest/types/href. Runtime matching still
@@ -174,6 +234,8 @@ async function buildPrefixTreeNode(
174
234
  },
175
235
  );
176
236
 
237
+ if (validateTransitionWhen) validateTransitionWhens(manifest, patternsMap);
238
+
177
239
  // Collect route names defined in this include (routes have prefixes applied)
178
240
  const routes = [...patternsMap.keys()];
179
241
  Object.assign(routeManifest, Object.fromEntries(patternsMap));
@@ -230,6 +292,7 @@ async function buildPrefixTreeNode(
230
292
  responseTypeRoutes,
231
293
  routeSearchSchemas,
232
294
  routerId,
295
+ validateTransitionWhen,
233
296
  ),
234
297
  );
235
298
 
@@ -313,6 +376,14 @@ export async function generateManifestFull<TEnv>(
313
376
  * EntryData map built below is local; this is the only seam that surfaces it.
314
377
  */
315
378
  collectClientFallbackRef?: (refKey: string) => void;
379
+ /**
380
+ * Fail on an invalid transition({ when }): a non-function, or a plain
381
+ * (server) function where a client reference is required. Only the Vite
382
+ * plugin's discovery passes it: it evaluates the tree in the react-server
383
+ * graph, the one place a "use client" import is told apart from a server
384
+ * function (transition-when-ref.ts).
385
+ */
386
+ validateTransitionWhen?: boolean;
316
387
  },
317
388
  ): Promise<FullManifest> {
318
389
  const routeManifest: Record<string, string> = {};
@@ -351,6 +422,10 @@ export async function generateManifestFull<TEnv>(
351
422
  },
352
423
  );
353
424
 
425
+ if (options?.validateTransitionWhen) {
426
+ validateTransitionWhens(manifest, patternsMap);
427
+ }
428
+
354
429
  // Surface the "use client" components registered as error/notFound fallbacks
355
430
  // (route-tree errorBoundary()/notFoundBoundary() helpers, stored on EntryData).
356
431
  // The boundary may be a handler function and/or wrap the client boundary in
@@ -413,6 +488,7 @@ export async function generateManifestFull<TEnv>(
413
488
  responseTypeRoutes,
414
489
  routeSearchSchemas,
415
490
  options?.routerId,
491
+ options?.validateTransitionWhen,
416
492
  ),
417
493
  );
418
494
 
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * Background Task Runner
3
3
  *
4
- * Unified helper for scheduling async work via waitUntil.
5
- * When waitUntil is unavailable, falls back to blocking or skipping.
4
+ * Unified helper for scheduling async work via waitUntil, and the two bounded
5
+ * waits the PPR shell capture puts on background work (raceDeadline,
6
+ * settleGrowing). When waitUntil is unavailable, falls back to blocking or
7
+ * skipping.
6
8
  */
7
9
 
8
10
  interface WaitUntilHost {
@@ -32,3 +34,54 @@ export function runBackground(
32
34
  return task();
33
35
  }
34
36
  }
37
+
38
+ /**
39
+ * Race `work` against an absolute deadline (epoch ms), keeping its value. The
40
+ * losing work is not cancelled (a router match has no abort signal) and its
41
+ * late rejection is swallowed. The timer is unref'd, so a pending deadline
42
+ * never keeps a Node dev process alive.
43
+ */
44
+ export async function raceDeadline<T>(
45
+ work: Promise<T>,
46
+ deadline: number,
47
+ ): Promise<{ done: true; value: T } | { done: false }> {
48
+ work.catch(() => {});
49
+ let timer: ReturnType<typeof setTimeout> | undefined;
50
+ const expired = new Promise<{ done: false }>((resolve) => {
51
+ timer = setTimeout(
52
+ () => resolve({ done: false }),
53
+ Math.max(0, deadline - Date.now()),
54
+ );
55
+ (timer as { unref?: () => void }).unref?.();
56
+ });
57
+ try {
58
+ return await Promise.race([
59
+ work.then((value) => ({ done: true as const, value })),
60
+ expired,
61
+ ]);
62
+ } finally {
63
+ if (timer) clearTimeout(timer);
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Wait until every promise in `list` has settled, including ones appended
69
+ * while waiting (a settled write can schedule a nested one: cacheRoute
70
+ * schedules its store.set in a second waitUntil), or until `deadline` (epoch
71
+ * ms). Resolves true when the list drained, false at the deadline. Never
72
+ * mutates `list`: a later call waits for what is still pending.
73
+ */
74
+ export async function settleGrowing(
75
+ list: readonly Promise<unknown>[],
76
+ deadline: number,
77
+ ): Promise<boolean> {
78
+ let seen = 0;
79
+ while (list.length > seen) {
80
+ if (Date.now() >= deadline) return false;
81
+ const batch = list.slice(seen);
82
+ seen = list.length;
83
+ const settled = await raceDeadline(Promise.allSettled(batch), deadline);
84
+ if (!settled.done) return false;
85
+ }
86
+ return true;
87
+ }
@@ -38,6 +38,53 @@ export interface CacheExecScope {
38
38
 
39
39
  const cacheExecStorage = new AsyncLocalStorage<CacheExecScope>();
40
40
 
41
+ // Global key: the value must survive HMR re-evaluation of this module, like
42
+ // the other request-scoped ALS instances (server/context.ts RangoContext).
43
+ const IDENTITY_EXEMPT_KEY = Symbol.for("rangojs-router:identity-exempt");
44
+ const identityExemptStorage: AsyncLocalStorage<boolean> = ((globalThis as any)[
45
+ IDENTITY_EXEMPT_KEY
46
+ ] ??= new AsyncLocalStorage<boolean>());
47
+
48
+ /**
49
+ * Run a callback whose identity reads never reach a shared entry, so
50
+ * guardIdentityRead (server/context.ts) lets them through and records
51
+ * nothing (#976):
52
+ *
53
+ * - a cache's own decisions and metadata: a `key()`, a store `keyGenerator`
54
+ * (cache-policy.ts resolveCacheKey), a `condition()` (cache-scope.ts
55
+ * conditionAllows, loader-cache.ts) and a `tags()` function
56
+ * (cache-policy.ts resolveTagsOption). The value picks the entry, whether
57
+ * one is used, or how it is invalidated; it is never rendered. Reading the
58
+ * request there is the documented way to partition an entry;
59
+ * - the router's `onError` (router/error-handling.ts invokeOnError): it
60
+ * observes, and a render error can reach it inside a capture or a cached
61
+ * scope.
62
+ *
63
+ * Scar: a ppr capture resolves its doc record key, and the record's tags,
64
+ * again under the capture context; once `ctx.request.headers` was guarded, a
65
+ * keyGenerator or tags() reading it refused every capture of the route.
66
+ *
67
+ * The exemption is async-local, so it would follow everything the callback
68
+ * starts. A "use cache" body (runWithCacheExecScope), a loader body
69
+ * (server/context.ts runInsideLoaderBodyScope) and a segment funnel
70
+ * (getContext().runWithStore) end it on entry (endIdentityExempt). Scar: a
71
+ * key() that awaited a "use cache" function reading cookies() stored the
72
+ * first visitor's cookie and served it to the next.
73
+ */
74
+ export function runIdentityExempt<T>(fn: () => T): T {
75
+ return identityExemptStorage.run(true, fn);
76
+ }
77
+
78
+ /** True inside runIdentityExempt, and not in a scope started from it. */
79
+ export function isInsideIdentityExempt(): boolean {
80
+ return identityExemptStorage.getStore() === true;
81
+ }
82
+
83
+ /** Run `fn` with the identity exemption ended (see runIdentityExempt). */
84
+ export function endIdentityExempt<T>(fn: () => T): T {
85
+ return isInsideIdentityExempt() ? identityExemptStorage.run(false, fn) : fn();
86
+ }
87
+
41
88
  /**
42
89
  * Run fn with the "use cache" execution scope active. Continuations spawned
43
90
  * from fn's synchronous kickoff inherit the scope; parallel chains do not.
@@ -48,7 +95,7 @@ export function runWithCacheExecScope<T>(
48
95
  fn: () => T,
49
96
  scope: CacheExecScope = createCacheExecScope(),
50
97
  ): T {
51
- return cacheExecStorage.run(scope, fn);
98
+ return endIdentityExempt(() => cacheExecStorage.run(scope, fn));
52
99
  }
53
100
 
54
101
  /** A scope nested in the calling chain's current one, if any. */
@@ -97,21 +97,61 @@ export function cacheKeyBase(
97
97
  }
98
98
 
99
99
  /**
100
- * The record key, or shell partition, of nested keyed cache() scopes (#970)
101
- * from its parts, outermost first: the chain's `key()` results and, for an
102
- * inner scope without its own `key()`, that scope's default key
103
- * (CacheScope.resolveKeyFrom). One part is returned raw, so a single keyed
104
- * scope keeps its key byte for byte. Two or more are joined by `|`, each
105
- * URI-encoded (like partitionShellKey, rsc/shell-capture-constants.ts): an
106
- * encoded part holds no `|`, so every tuple maps to its own key, of any
107
- * depth. Raw, `("a|b", "c")` and `("a", "b|c")` named one record. A composed
108
- * key holds no `:` either, so a single raw result with one (a prefixed
109
- * `tier:...`, a `doc:` default key) never equals it; an unprefixed raw result
110
- * can. Shared with the testing helper `shellCacheKey`, which composes a
111
- * nested partition the same way.
100
+ * The prefix of a `key()` result's part in a record key (#975), and of a
101
+ * loader's own `key()` result after its `loader:<id>:` namespace (#1009,
102
+ * loader-cache.ts loaderKeyFromResult).
112
103
  */
113
- export function composeCacheKeys(parts: readonly string[]): string {
114
- return parts.length === 1
115
- ? parts[0]
116
- : parts.map((part) => encodeURIComponent(part)).join("|");
104
+ export const KEY_PART_PREFIX = "key:";
105
+
106
+ /**
107
+ * A cache() record key, or ppr shell partition, from its parts
108
+ * (CacheScope.resolveKeyFrom, CacheScope.resolvePartition): the `key()`
109
+ * results of the scope's cache() chain, outermost first, then its default
110
+ * parts: the keyGenerator results of enclosing scopes on another store
111
+ * (#974) and, for a record key under a scope without its own `key()`, its
112
+ * default key (or its store's keyGenerator result, #970).
113
+ *
114
+ * Key scheme invariant (#975). A `key()` result is often request input,
115
+ * and a record key has no route or scope discriminator (the store reads
116
+ * `store.get(key)`), so a result that equals another record's key writes
117
+ * one route's content under the other's entry. Before #975 a single
118
+ * `key()` result was stored raw: the header value `doc:localhost/pricing`
119
+ * named /pricing's default-keyed record, and `gold|doc%3Alocalhost%2Fpricing`
120
+ * named gold's nested /pricing record (#970). So every part is encoded:
121
+ *
122
+ * - a `key()` part is `key:` plus `encodeURIComponent(result)`: it starts
123
+ * with `key:` and holds no `|` and no other `:`;
124
+ * - a default part is `encodeURIComponent(value)`: it holds no `:` or `|`;
125
+ * an enclosing store's keyGenerator result equal to the default key is an
126
+ * empty part, so each store keeps its position (CacheScope
127
+ * positionalParts);
128
+ * - one `key()` part is the key; two or more parts are joined by `|`,
129
+ * `key()` parts first;
130
+ * - a lone default part stays raw: with no `key()` on the chain the record
131
+ * key is the default key or the store's keyGenerator result, unchanged.
132
+ *
133
+ * Why nothing collides: the router's default keys (`doc:`, `partial:`,
134
+ * `intercept:`, `response:<type>:`) never start with `key:` and hold a `:`.
135
+ * A namespaced `key()` result starts with `key:` and holds no `|`. A
136
+ * composed key holds a `|` and either starts with `key:` or holds no `:`.
137
+ * Among composed keys, splitting on `|` recovers each part, a part's kind
138
+ * (a `key:` prefix, or no `:` at all) and its value. Pinned by the collision
139
+ * probe in src/cache/__tests__/cache-scope-chain.test.ts. A keyGenerator
140
+ * result is the store's own namespace, as before.
141
+ *
142
+ * Shared with the testing helper `shellCacheKey`, so a test builds the
143
+ * production partition.
144
+ */
145
+ export function composeCacheKeys(
146
+ keyResults: readonly string[],
147
+ defaultParts: readonly string[] = [],
148
+ ): string {
149
+ if (keyResults.length === 0 && defaultParts.length === 1) {
150
+ return defaultParts[0];
151
+ }
152
+ const parts = keyResults.map(
153
+ (result) => KEY_PART_PREFIX + encodeURIComponent(result),
154
+ );
155
+ for (const part of defaultParts) parts.push(encodeURIComponent(part));
156
+ return parts.join("|");
117
157
  }
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { CacheDefaults, SegmentCacheStore } from "./types.js";
10
10
  import { _getRequestContext } from "../server/request-context.js";
11
+ import { runIdentityExempt } from "./cache-exec-scope.js";
11
12
  import type { RequestContext } from "../server/request-context.js";
12
13
  import { normalizeTags } from "./cache-tag.js";
13
14
  import { reportCacheError } from "./cache-error.js";
@@ -127,16 +128,18 @@ export async function resolveCacheKey(
127
128
  keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
128
129
  store: SegmentCacheStore | null,
129
130
  defaultKey: string,
130
- _label: string,
131
131
  ): Promise<string> {
132
132
  const requestCtx = _getRequestContext();
133
133
 
134
+ // A key reads the request to partition the entry (runIdentityExempt): the
135
+ // identity guards let it through, a ppr capture included.
134
136
  if (keyFn && requestCtx) {
135
- return await keyFn(requestCtx);
137
+ return await runIdentityExempt(() => keyFn(requestCtx));
136
138
  }
137
139
 
138
140
  if (store?.keyGenerator && requestCtx) {
139
- return await store.keyGenerator(requestCtx, defaultKey);
141
+ const keyGenerator = store.keyGenerator;
142
+ return await runIdentityExempt(() => keyGenerator(requestCtx, defaultKey));
140
143
  }
141
144
 
142
145
  return defaultKey;
@@ -157,7 +160,9 @@ export function resolveTagsOption<TEnv>(
157
160
  return undefined;
158
161
  }
159
162
  try {
160
- return normalizeTagList(tags(ctx));
163
+ // Tags label the entry, never render: identity reads pass
164
+ // (runIdentityExempt), a ppr capture's record write included.
165
+ return normalizeTagList(runIdentityExempt(() => tags(ctx)));
161
166
  } catch (error) {
162
167
  reportCacheError(
163
168
  error,