@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26

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 (225) hide show
  1. package/README.md +294 -28
  2. package/dist/bin/rango.js +355 -47
  3. package/dist/vite/index.js +1658 -1239
  4. package/package.json +3 -3
  5. package/skills/cache-guide/SKILL.md +9 -5
  6. package/skills/caching/SKILL.md +4 -4
  7. package/skills/document-cache/SKILL.md +2 -2
  8. package/skills/hooks/SKILL.md +40 -29
  9. package/skills/host-router/SKILL.md +218 -0
  10. package/skills/intercept/SKILL.md +79 -0
  11. package/skills/layout/SKILL.md +62 -2
  12. package/skills/loader/SKILL.md +229 -15
  13. package/skills/middleware/SKILL.md +109 -30
  14. package/skills/parallel/SKILL.md +57 -2
  15. package/skills/prerender/SKILL.md +189 -19
  16. package/skills/rango/SKILL.md +1 -2
  17. package/skills/response-routes/SKILL.md +3 -3
  18. package/skills/route/SKILL.md +44 -3
  19. package/skills/router-setup/SKILL.md +80 -3
  20. package/skills/theme/SKILL.md +5 -4
  21. package/skills/typesafety/SKILL.md +59 -16
  22. package/skills/use-cache/SKILL.md +16 -2
  23. package/src/__internal.ts +1 -1
  24. package/src/bin/rango.ts +56 -19
  25. package/src/browser/action-coordinator.ts +97 -0
  26. package/src/browser/event-controller.ts +29 -48
  27. package/src/browser/history-state.ts +80 -0
  28. package/src/browser/intercept-utils.ts +1 -1
  29. package/src/browser/link-interceptor.ts +19 -3
  30. package/src/browser/merge-segment-loaders.ts +9 -2
  31. package/src/browser/navigation-bridge.ts +66 -443
  32. package/src/browser/navigation-client.ts +34 -62
  33. package/src/browser/navigation-store.ts +4 -33
  34. package/src/browser/navigation-transaction.ts +295 -0
  35. package/src/browser/partial-update.ts +103 -151
  36. package/src/browser/prefetch/cache.ts +67 -0
  37. package/src/browser/prefetch/fetch.ts +137 -0
  38. package/src/browser/prefetch/observer.ts +65 -0
  39. package/src/browser/prefetch/policy.ts +42 -0
  40. package/src/browser/prefetch/queue.ts +88 -0
  41. package/src/browser/rango-state.ts +112 -0
  42. package/src/browser/react/Link.tsx +154 -44
  43. package/src/browser/react/NavigationProvider.tsx +32 -0
  44. package/src/browser/react/context.ts +6 -0
  45. package/src/browser/react/filter-segment-order.ts +11 -0
  46. package/src/browser/react/index.ts +2 -6
  47. package/src/browser/react/location-state-shared.ts +29 -11
  48. package/src/browser/react/location-state.ts +6 -4
  49. package/src/browser/react/nonce-context.ts +23 -0
  50. package/src/browser/react/shallow-equal.ts +27 -0
  51. package/src/browser/react/use-action.ts +23 -45
  52. package/src/browser/react/use-client-cache.ts +5 -3
  53. package/src/browser/react/use-handle.ts +21 -64
  54. package/src/browser/react/use-navigation.ts +7 -32
  55. package/src/browser/react/use-params.ts +5 -34
  56. package/src/browser/react/use-pathname.ts +2 -3
  57. package/src/browser/react/use-router.ts +3 -6
  58. package/src/browser/react/use-search-params.ts +2 -1
  59. package/src/browser/react/use-segments.ts +75 -114
  60. package/src/browser/response-adapter.ts +73 -0
  61. package/src/browser/rsc-router.tsx +46 -22
  62. package/src/browser/scroll-restoration.ts +10 -7
  63. package/src/browser/server-action-bridge.ts +458 -405
  64. package/src/browser/types.ts +21 -35
  65. package/src/browser/validate-redirect-origin.ts +29 -0
  66. package/src/build/generate-manifest.ts +38 -13
  67. package/src/build/generate-route-types.ts +4 -0
  68. package/src/build/index.ts +1 -0
  69. package/src/build/route-trie.ts +19 -3
  70. package/src/build/route-types/codegen.ts +13 -4
  71. package/src/build/route-types/include-resolution.ts +13 -0
  72. package/src/build/route-types/per-module-writer.ts +15 -3
  73. package/src/build/route-types/router-processing.ts +170 -18
  74. package/src/build/runtime-discovery.ts +13 -1
  75. package/src/cache/background-task.ts +34 -0
  76. package/src/cache/cache-key-utils.ts +44 -0
  77. package/src/cache/cache-policy.ts +125 -0
  78. package/src/cache/cache-runtime.ts +136 -123
  79. package/src/cache/cache-scope.ts +76 -83
  80. package/src/cache/cf/cf-cache-store.ts +12 -7
  81. package/src/cache/document-cache.ts +93 -69
  82. package/src/cache/handle-capture.ts +81 -0
  83. package/src/cache/index.ts +0 -15
  84. package/src/cache/memory-segment-store.ts +43 -69
  85. package/src/cache/profile-registry.ts +43 -8
  86. package/src/cache/read-through-swr.ts +134 -0
  87. package/src/cache/segment-codec.ts +140 -117
  88. package/src/cache/taint.ts +30 -3
  89. package/src/cache/types.ts +1 -115
  90. package/src/client.rsc.tsx +0 -1
  91. package/src/client.tsx +53 -76
  92. package/src/errors.ts +6 -1
  93. package/src/handle.ts +1 -1
  94. package/src/handles/MetaTags.tsx +5 -2
  95. package/src/host/cookie-handler.ts +8 -3
  96. package/src/host/index.ts +0 -3
  97. package/src/host/router.ts +14 -1
  98. package/src/href-client.ts +3 -1
  99. package/src/index.rsc.ts +53 -10
  100. package/src/index.ts +73 -43
  101. package/src/loader.rsc.ts +12 -4
  102. package/src/loader.ts +8 -0
  103. package/src/prerender/store.ts +60 -18
  104. package/src/prerender.ts +76 -18
  105. package/src/reverse.ts +11 -7
  106. package/src/root-error-boundary.tsx +30 -26
  107. package/src/route-definition/dsl-helpers.ts +9 -6
  108. package/src/route-definition/index.ts +0 -3
  109. package/src/route-definition/redirect.ts +15 -3
  110. package/src/route-map-builder.ts +38 -2
  111. package/src/route-name.ts +53 -0
  112. package/src/route-types.ts +7 -0
  113. package/src/router/content-negotiation.ts +1 -1
  114. package/src/router/debug-manifest.ts +16 -3
  115. package/src/router/handler-context.ts +96 -17
  116. package/src/router/intercept-resolution.ts +6 -4
  117. package/src/router/lazy-includes.ts +4 -0
  118. package/src/router/loader-resolution.ts +6 -11
  119. package/src/router/logging.ts +100 -3
  120. package/src/router/manifest.ts +32 -3
  121. package/src/router/match-api.ts +62 -54
  122. package/src/router/match-context.ts +3 -0
  123. package/src/router/match-handlers.ts +185 -11
  124. package/src/router/match-middleware/background-revalidation.ts +65 -85
  125. package/src/router/match-middleware/cache-lookup.ts +78 -10
  126. package/src/router/match-middleware/cache-store.ts +2 -0
  127. package/src/router/match-pipelines.ts +8 -43
  128. package/src/router/match-result.ts +0 -9
  129. package/src/router/metrics.ts +233 -13
  130. package/src/router/middleware-types.ts +34 -39
  131. package/src/router/middleware.ts +290 -130
  132. package/src/router/pattern-matching.ts +61 -10
  133. package/src/router/prerender-match.ts +36 -6
  134. package/src/router/preview-match.ts +7 -1
  135. package/src/router/revalidation.ts +61 -2
  136. package/src/router/router-context.ts +15 -0
  137. package/src/router/router-interfaces.ts +158 -40
  138. package/src/router/router-options.ts +223 -1
  139. package/src/router/router-registry.ts +5 -2
  140. package/src/router/segment-resolution/fresh.ts +165 -242
  141. package/src/router/segment-resolution/helpers.ts +263 -0
  142. package/src/router/segment-resolution/loader-cache.ts +102 -98
  143. package/src/router/segment-resolution/revalidation.ts +394 -272
  144. package/src/router/segment-resolution/static-store.ts +2 -2
  145. package/src/router/segment-resolution.ts +1 -3
  146. package/src/router/segment-wrappers.ts +3 -0
  147. package/src/router/telemetry-otel.ts +299 -0
  148. package/src/router/telemetry.ts +300 -0
  149. package/src/router/timeout.ts +148 -0
  150. package/src/router/trie-matching.ts +20 -2
  151. package/src/router/types.ts +7 -1
  152. package/src/router.ts +203 -18
  153. package/src/rsc/handler-context.ts +13 -2
  154. package/src/rsc/handler.ts +489 -438
  155. package/src/rsc/helpers.ts +125 -5
  156. package/src/rsc/index.ts +0 -20
  157. package/src/rsc/loader-fetch.ts +84 -42
  158. package/src/rsc/manifest-init.ts +3 -2
  159. package/src/rsc/origin-guard.ts +141 -0
  160. package/src/rsc/progressive-enhancement.ts +245 -19
  161. package/src/rsc/response-route-handler.ts +347 -0
  162. package/src/rsc/rsc-rendering.ts +47 -43
  163. package/src/rsc/runtime-warnings.ts +42 -0
  164. package/src/rsc/server-action.ts +166 -66
  165. package/src/rsc/ssr-setup.ts +128 -0
  166. package/src/rsc/types.ts +20 -2
  167. package/src/search-params.ts +38 -23
  168. package/src/server/context.ts +61 -7
  169. package/src/server/cookie-store.ts +190 -0
  170. package/src/server/fetchable-loader-store.ts +11 -6
  171. package/src/server/handle-store.ts +84 -12
  172. package/src/server/loader-registry.ts +11 -46
  173. package/src/server/request-context.ts +275 -49
  174. package/src/server.ts +6 -0
  175. package/src/ssr/index.tsx +67 -28
  176. package/src/static-handler.ts +7 -0
  177. package/src/theme/ThemeProvider.tsx +6 -1
  178. package/src/theme/index.ts +4 -18
  179. package/src/theme/theme-context.ts +1 -28
  180. package/src/theme/theme-script.ts +2 -1
  181. package/src/types/cache-types.ts +6 -1
  182. package/src/types/error-types.ts +3 -0
  183. package/src/types/global-namespace.ts +22 -0
  184. package/src/types/handler-context.ts +103 -16
  185. package/src/types/index.ts +1 -1
  186. package/src/types/loader-types.ts +9 -6
  187. package/src/types/route-config.ts +17 -26
  188. package/src/types/route-entry.ts +28 -0
  189. package/src/types/segments.ts +0 -5
  190. package/src/urls/include-helper.ts +49 -8
  191. package/src/urls/index.ts +1 -0
  192. package/src/urls/path-helper-types.ts +30 -12
  193. package/src/urls/path-helper.ts +17 -2
  194. package/src/urls/pattern-types.ts +21 -1
  195. package/src/urls/response-types.ts +29 -7
  196. package/src/urls/type-extraction.ts +23 -15
  197. package/src/use-loader.tsx +27 -9
  198. package/src/vite/discovery/bundle-postprocess.ts +32 -52
  199. package/src/vite/discovery/discover-routers.ts +52 -26
  200. package/src/vite/discovery/prerender-collection.ts +58 -41
  201. package/src/vite/discovery/route-types-writer.ts +7 -7
  202. package/src/vite/discovery/state.ts +7 -7
  203. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  204. package/src/vite/index.ts +10 -51
  205. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  206. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  207. package/src/vite/plugins/expose-internal-ids.ts +4 -3
  208. package/src/vite/plugins/refresh-cmd.ts +65 -0
  209. package/src/vite/plugins/use-cache-transform.ts +91 -3
  210. package/src/vite/plugins/version-plugin.ts +188 -18
  211. package/src/vite/rango.ts +61 -36
  212. package/src/vite/router-discovery.ts +173 -100
  213. package/src/vite/utils/prerender-utils.ts +81 -0
  214. package/src/vite/utils/shared-utils.ts +19 -9
  215. package/skills/testing/SKILL.md +0 -226
  216. package/src/browser/lru-cache.ts +0 -61
  217. package/src/browser/react/prefetch.ts +0 -27
  218. package/src/browser/request-controller.ts +0 -164
  219. package/src/cache/memory-store.ts +0 -253
  220. package/src/href-context.ts +0 -33
  221. package/src/route-definition/route-function.ts +0 -119
  222. package/src/router.gen.ts +0 -6
  223. package/src/static-handler.gen.ts +0 -5
  224. package/src/urls.gen.ts +0 -8
  225. /package/{CLAUDE.md → AGENTS.md} +0 -0
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared Cache Key Utilities
3
+ *
4
+ * Deterministic normalization of search params and route params
5
+ * for cache key generation. Used by cache-runtime, cache-scope,
6
+ * document-cache, and loader-cache.
7
+ */
8
+
9
+ /**
10
+ * Build a sorted, deterministic query string from URLSearchParams,
11
+ * excluding internal _rsc* and __* params.
12
+ *
13
+ * Returns empty string when no user-facing params exist.
14
+ */
15
+ export function sortedSearchString(searchParams: URLSearchParams): string {
16
+ const pairs: [string, string][] = [];
17
+ for (const [k, v] of searchParams) {
18
+ if (!k.startsWith("_rsc") && !k.startsWith("__")) {
19
+ pairs.push([k, v]);
20
+ }
21
+ }
22
+ if (pairs.length === 0) return "";
23
+ pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
24
+ return pairs
25
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
26
+ .join("&");
27
+ }
28
+
29
+ /**
30
+ * Build a sorted, deterministic string from route params.
31
+ *
32
+ * Returns empty string when params is empty or undefined.
33
+ */
34
+ export function sortedRouteParams(
35
+ params: Record<string, string> | undefined,
36
+ ): string {
37
+ if (!params) return "";
38
+ const entries = Object.entries(params);
39
+ if (entries.length === 0) return "";
40
+ return entries
41
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
42
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
43
+ .join("&");
44
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared Cache Policy Utilities
3
+ *
4
+ * Resolution cascades for TTL, SWR, cache key, and cache store.
5
+ * Consolidates the multi-tier resolution pattern:
6
+ * explicit option → store defaults → fallback constant
7
+ */
8
+
9
+ import type { CacheDefaults, SegmentCacheStore } from "./types.js";
10
+ import { _getRequestContext } from "../server/request-context.js";
11
+ import type { RequestContext } from "../server/request-context.js";
12
+
13
+ /**
14
+ * Default TTL for route-level cache() DSL and loader cache.
15
+ * Applied when neither the cache options nor the store defaults specify a TTL.
16
+ */
17
+ export const DEFAULT_ROUTE_TTL = 60;
18
+
19
+ /**
20
+ * Default TTL for function-level "use cache" (setItem).
21
+ * Applied when neither the item options nor the store defaults specify a TTL.
22
+ */
23
+ export const DEFAULT_FUNCTION_TTL = 900;
24
+
25
+ /**
26
+ * Resolve effective TTL from the 3-tier cascade:
27
+ * explicit → store defaults → fallback.
28
+ */
29
+ export function resolveTtl(
30
+ explicit: number | undefined,
31
+ defaults: CacheDefaults | undefined,
32
+ fallback: number,
33
+ ): number {
34
+ if (explicit !== undefined) return explicit;
35
+ if (defaults?.ttl !== undefined) return defaults.ttl;
36
+ return fallback;
37
+ }
38
+
39
+ /**
40
+ * Resolve effective SWR window from the 2-tier cascade:
41
+ * explicit → store defaults.
42
+ * Returns 0 when unset (no SWR window).
43
+ */
44
+ export function resolveSwrWindow(
45
+ explicit: number | undefined,
46
+ defaults: CacheDefaults | undefined,
47
+ ): number {
48
+ if (explicit !== undefined) return explicit;
49
+ if (defaults?.swr !== undefined) return defaults.swr;
50
+ return 0;
51
+ }
52
+
53
+ /**
54
+ * Compute staleAt and expiresAt timestamps from TTL and SWR window.
55
+ *
56
+ * - staleAt: when the entry becomes stale (TTL boundary)
57
+ * - expiresAt: when the entry should be evicted (TTL + SWR)
58
+ *
59
+ * When swrWindow is 0, staleAt === expiresAt (no SWR).
60
+ */
61
+ export function computeExpiration(
62
+ ttlSeconds: number,
63
+ swrSeconds: number = 0,
64
+ ): { staleAt: number; expiresAt: number } {
65
+ const now = Date.now();
66
+ const staleAt = now + ttlSeconds * 1000;
67
+ const expiresAt = staleAt + swrSeconds * 1000;
68
+ return { staleAt, expiresAt };
69
+ }
70
+
71
+ // ============================================================================
72
+ // Cache Key Resolution
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Resolve cache key using the 3-tier priority:
77
+ * 1. keyFn (full override from route/loader cache options)
78
+ * 2. store.keyGenerator (modifies default key)
79
+ * 3. defaultKey (used when neither keyFn nor keyGenerator is provided)
80
+ *
81
+ * Errors from keyFn and store.keyGenerator propagate to the caller.
82
+ * Cache identity is correctness-critical: if explicit key logic throws,
83
+ * silently remapping to a different key could cause cache collisions or
84
+ * serve stale/wrong data. Callers must handle the error or let it surface.
85
+ *
86
+ * Uses _getRequestContext (non-throwing) so that calls outside ALS
87
+ * (e.g. build-time) gracefully fall back to defaultKey.
88
+ */
89
+ export async function resolveCacheKey(
90
+ keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
91
+ store: SegmentCacheStore | null,
92
+ defaultKey: string,
93
+ _label: string,
94
+ ): Promise<string> {
95
+ const requestCtx = _getRequestContext();
96
+
97
+ // Priority 1: Route/loader-level key function (full override)
98
+ if (keyFn && requestCtx) {
99
+ return await keyFn(requestCtx);
100
+ }
101
+
102
+ // Priority 2: Store-level keyGenerator (modifies default key)
103
+ if (store?.keyGenerator && requestCtx) {
104
+ return await store.keyGenerator(requestCtx, defaultKey);
105
+ }
106
+
107
+ // Priority 3: Default key (no custom key logic provided)
108
+ return defaultKey;
109
+ }
110
+
111
+ // ============================================================================
112
+ // Cache Store Resolution
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Resolve cache store from the 2-tier priority:
117
+ * 1. Explicit store from cache options
118
+ * 2. App-level store from request context
119
+ */
120
+ export function resolveCacheStore(
121
+ explicitStore: SegmentCacheStore | undefined,
122
+ ): SegmentCacheStore | null {
123
+ if (explicitStore) return explicitStore;
124
+ return _getRequestContext()?._cacheStore ?? null;
125
+ }
@@ -17,9 +17,6 @@
17
17
  /// <reference types="@vitejs/plugin-rsc/types" />
18
18
 
19
19
  import {
20
- renderToReadableStream,
21
- createFromReadableStream,
22
- createTemporaryReferenceSet,
23
20
  encodeReply,
24
21
  createClientTemporaryReferenceSet,
25
22
  } from "@vitejs/plugin-rsc/rsc";
@@ -28,38 +25,17 @@ import {
28
25
  isTainted,
29
26
  CACHED_FN_SYMBOL,
30
27
  isCachedFunction,
31
- INSIDE_CACHE_EXEC,
28
+ stampCacheExec,
29
+ unstampCacheExec,
32
30
  } from "./taint.js";
33
31
 
34
32
  export { isCachedFunction };
35
- import { getCacheProfile } from "./profile-registry.js";
36
- import { streamToString, stringToStream } from "./segment-codec.js";
37
- import type { SegmentHandleData } from "./types.js";
38
- import type { HandleStore } from "../server/handle-store.js";
39
-
40
- // ============================================================================
41
- // Serialization Helpers
42
- // ============================================================================
43
-
44
- async function serializeResult(value: unknown): Promise<string | null> {
45
- try {
46
- const temporaryReferences = createTemporaryReferenceSet();
47
- const stream = renderToReadableStream(value, { temporaryReferences });
48
- return await streamToString(stream);
49
- } catch {
50
- return null;
51
- }
52
- }
53
-
54
- async function deserializeResult<T>(encoded: string): Promise<T> {
55
- const temporaryReferences = createTemporaryReferenceSet();
56
- const stream = stringToStream(encoded);
57
- return createFromReadableStream<T>(stream, { temporaryReferences });
58
- }
59
-
60
- // ============================================================================
61
- // Cache Key Generation
62
- // ============================================================================
33
+ import { serializeResult, deserializeResult } from "./segment-codec.js";
34
+ import { createHandleStore } from "../server/handle-store.js";
35
+ import { restoreHandles } from "./handle-snapshot.js";
36
+ import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
37
+ import { sortedSearchString } from "./cache-key-utils.js";
38
+ import { runBackground } from "./background-task.js";
63
39
 
64
40
  /**
65
41
  * Convert encodeReply result to a stable string key.
@@ -72,58 +48,6 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
72
48
  return text;
73
49
  }
74
50
 
75
- // ============================================================================
76
- // Handle Capture
77
- // ============================================================================
78
-
79
- interface HandleCapture {
80
- data: Record<string, SegmentHandleData>;
81
- }
82
-
83
- function startHandleCapture(handleStore: HandleStore): HandleCapture {
84
- const capture: HandleCapture = { data: {} };
85
- const originalPush = handleStore.push.bind(handleStore);
86
-
87
- // Intercept push() calls to record them
88
- handleStore.push = (
89
- handleName: string,
90
- segmentId: string,
91
- value: unknown,
92
- ) => {
93
- if (!capture.data[segmentId]) {
94
- capture.data[segmentId] = {};
95
- }
96
- if (!capture.data[segmentId][handleName]) {
97
- capture.data[segmentId][handleName] = [];
98
- }
99
- capture.data[segmentId][handleName].push(value);
100
- // Still call the original so the data flows through normally
101
- originalPush(handleName, segmentId, value);
102
- };
103
-
104
- return capture;
105
- }
106
-
107
- function stopHandleCapture(
108
- handleStore: HandleStore,
109
- _capture: HandleCapture,
110
- ): void {
111
- // Restore original push by deleting the override
112
- // (the original is on the prototype/closure, our override is an own property)
113
- delete (handleStore as any).push;
114
- }
115
-
116
- function restoreHandles(
117
- handles: Record<string, SegmentHandleData>,
118
- handleStore: HandleStore,
119
- ): void {
120
- for (const [segId, segHandles] of Object.entries(handles)) {
121
- if (Object.keys(segHandles).length > 0) {
122
- handleStore.replaySegmentData(segId, segHandles);
123
- }
124
- }
125
- }
126
-
127
51
  // ============================================================================
128
52
  // Core: registerCachedFunction
129
53
  // ============================================================================
@@ -144,17 +68,30 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
144
68
  const wrapped = async function (this: any, ...args: any[]): Promise<any> {
145
69
  const requestCtx = getRequestContext();
146
70
  const store = requestCtx?._cacheStore;
147
- const profile = getCacheProfile(profileName || "default");
71
+ const resolvedProfileName = profileName || "default";
148
72
 
149
- // Bypass: no store, no getItem support, or no profile configured
150
- if (!store?.getItem || !profile) {
73
+ // Bypass: no store or no getItem support
74
+ if (!store?.getItem) {
151
75
  return fn.apply(this, args);
152
76
  }
153
77
 
78
+ // Resolve profile strictly from request-scoped config (set by the
79
+ // active router via createRequestContext). No global fallback —
80
+ // global profile state is only for DSL-time cache("profileName").
81
+ const profile = requestCtx?._cacheProfiles?.[resolvedProfileName];
82
+
83
+ if (!profile) {
84
+ throw new Error(
85
+ `[use cache] "${id}" uses unknown cache profile "${resolvedProfileName}". ` +
86
+ `Define it in createRouter({ cacheProfiles: { "${resolvedProfileName}": { ttl: ... } } }).`,
87
+ );
88
+ }
89
+
154
90
  // Separate tainted args (ctx, env, req) from key-generating args.
155
- // For tainted objects that carry route context (params, pathname),
156
- // extract those serializable values into the key so different routes
157
- // and param combinations produce distinct cache entries.
91
+ // For tainted objects that carry route context (params, pathname,
92
+ // searchParams), extract serializable values into the key so
93
+ // different routes, param combinations, and query variants produce
94
+ // distinct cache entries.
158
95
  const keyArgs: unknown[] = [];
159
96
  let hasTaintedArgs = false;
160
97
  for (const arg of args) {
@@ -162,10 +99,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
162
99
  hasTaintedArgs = true;
163
100
  const ctx = arg as any;
164
101
  if (ctx.params && typeof ctx.params === "object") {
102
+ // Include host to prevent cross-host cache collisions (same
103
+ // pattern as route-level cache-scope.ts key generation).
104
+ if (ctx.url?.host) {
105
+ keyArgs.push(ctx.url.host);
106
+ }
107
+ // Include route name to prevent collisions when the same cached
108
+ // function is reused across routes with identical pathname/params
109
+ // but different local reverse() scope.
110
+ if (ctx._routeName) {
111
+ keyArgs.push(ctx._routeName);
112
+ }
165
113
  keyArgs.push(ctx.pathname, ctx.params);
166
114
  if (ctx._responseType) {
167
115
  keyArgs.push(ctx._responseType);
168
116
  }
117
+ // Include user-facing search params (exclude internal _rsc*/__ params)
118
+ if (ctx.searchParams instanceof URLSearchParams) {
119
+ const normalized = sortedSearchString(ctx.searchParams);
120
+ if (normalized) {
121
+ keyArgs.push(normalized);
122
+ }
123
+ }
169
124
  }
170
125
  } else {
171
126
  keyArgs.push(arg);
@@ -234,24 +189,75 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
234
189
  restoreHandles(cached.handles, handleStore);
235
190
  }
236
191
  }
237
- // Background revalidation
238
- if (requestCtx?.waitUntil) {
239
- requestCtx.waitUntil(async () => {
240
- try {
241
- const freshResult = await fn.apply(this, args);
242
- const serialized = await serializeResult(freshResult);
243
- if (serialized !== null) {
244
- await store.setItem!(cacheKey, serialized, {
245
- ttl: profile.ttl,
246
- swr: profile.swr,
247
- tags: profile.tags,
248
- });
249
- }
250
- } catch {
251
- // Background revalidation failed silently
192
+ // Background revalidation — must capture handles if tainted args present.
193
+ // Use an isolated handle store so background pushes don't pollute the
194
+ // live response or throw LateHandlePushError on the completed store.
195
+ // Same isolation pattern as route-level background-revalidation.ts.
196
+ runBackground(requestCtx, async () => {
197
+ // Reuse closure-captured requestCtx instead of calling
198
+ // getRequestContext() ALS context may be gone inside waitUntil.
199
+ let originalHandleStore:
200
+ | ReturnType<typeof createHandleStore>
201
+ | undefined;
202
+ if (hasTaintedArgs && requestCtx) {
203
+ originalHandleStore = requestCtx._handleStore;
204
+ requestCtx._handleStore = createHandleStore();
205
+ }
206
+ const bgHandleStore = hasTaintedArgs
207
+ ? requestCtx?._handleStore
208
+ : undefined;
209
+ let bgCapture: HandleCapture | undefined;
210
+ let bgStopCapture: (() => void) | undefined;
211
+ if (bgHandleStore) {
212
+ const c = startHandleCapture(bgHandleStore);
213
+ bgCapture = c.capture;
214
+ bgStopCapture = c.stop;
215
+ }
216
+
217
+ // Stamp tainted args and RequestContext so request-scoped
218
+ // reads (cookies, headers) and side effects (ctx.set, etc.)
219
+ // throw inside background revalidation, same as the miss path.
220
+ // Uses ref-counted stamp/unstamp so overlapping executions
221
+ // sharing the same ctx don't clear each other's guards.
222
+ const bgTaintedArgs: unknown[] = [];
223
+ for (const arg of args) {
224
+ if (isTainted(arg)) {
225
+ stampCacheExec(arg as object);
226
+ bgTaintedArgs.push(arg);
252
227
  }
253
- });
254
- }
228
+ }
229
+ if (requestCtx) {
230
+ stampCacheExec(requestCtx as object);
231
+ }
232
+
233
+ try {
234
+ const freshResult = await fn.apply(this, args);
235
+ bgStopCapture?.();
236
+ const serialized = await serializeResult(freshResult);
237
+ if (serialized !== null) {
238
+ await store.setItem!(cacheKey, serialized, {
239
+ handles: bgCapture?.data,
240
+ ttl: profile.ttl,
241
+ swr: profile.swr,
242
+ tags: profile.tags,
243
+ });
244
+ }
245
+ } catch (bgError) {
246
+ bgStopCapture?.();
247
+ requestCtx?._reportBackgroundError?.(bgError, "stale-revalidation");
248
+ } finally {
249
+ for (const arg of bgTaintedArgs) {
250
+ unstampCacheExec(arg as object);
251
+ }
252
+ if (requestCtx) {
253
+ unstampCacheExec(requestCtx as object);
254
+ }
255
+ // Restore original handle store
256
+ if (originalHandleStore && requestCtx) {
257
+ requestCtx._handleStore = originalHandleStore;
258
+ }
259
+ }
260
+ });
255
261
  return result;
256
262
  } catch {
257
263
  // Deserialization of stale value failed, fall through
@@ -261,32 +267,44 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
261
267
  // Cache miss: execute, serialize, store
262
268
  const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
263
269
  let capture: HandleCapture | undefined;
270
+ let stopCapture: (() => void) | undefined;
264
271
  if (handleStore && hasTaintedArgs) {
265
- capture = startHandleCapture(handleStore);
272
+ const c = startHandleCapture(handleStore);
273
+ capture = c.capture;
274
+ stopCapture = c.stop;
266
275
  }
267
276
 
268
277
  // Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
269
278
  // inside the cached function body (those side effects are lost on hit).
279
+ // Uses ref-counted stamp/unstamp so overlapping executions
280
+ // sharing the same ctx don't clear each other's guards.
270
281
  const taintedArgs: unknown[] = [];
271
282
  for (const arg of args) {
272
283
  if (isTainted(arg)) {
273
- (arg as any)[INSIDE_CACHE_EXEC] = true;
284
+ stampCacheExec(arg as object);
274
285
  taintedArgs.push(arg);
275
286
  }
276
287
  }
288
+ // Always stamp the ALS RequestContext so cookies()/headers() guards fire
289
+ // even when the cached function receives no tainted args. The guard in
290
+ // cookie-store.ts checks RequestContext, not function args.
291
+ if (requestCtx) {
292
+ stampCacheExec(requestCtx as object);
293
+ }
277
294
 
278
295
  let result: any;
279
296
  try {
280
297
  result = await fn.apply(this, args);
281
298
  } finally {
282
- // Always remove the flag, even if the function throws
299
+ // Decrement ref count; symbol is deleted when it reaches zero
283
300
  for (const arg of taintedArgs) {
284
- delete (arg as any)[INSIDE_CACHE_EXEC];
301
+ unstampCacheExec(arg as object);
285
302
  }
286
- }
287
-
288
- if (capture && handleStore) {
289
- stopHandleCapture(handleStore, capture);
303
+ if (requestCtx) {
304
+ unstampCacheExec(requestCtx as object);
305
+ }
306
+ // Remove this capture token (order-independent, safe for concurrent use)
307
+ stopCapture?.();
290
308
  }
291
309
 
292
310
  // Serialize and store — fully non-blocking when waitUntil is available.
@@ -302,17 +320,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
302
320
  tags: profile.tags,
303
321
  });
304
322
  }
305
- } catch {
306
- // Serialization or store write failed silently
323
+ } catch (writeError) {
324
+ requestCtx?._reportBackgroundError?.(writeError, "cache-write");
307
325
  }
308
326
  };
309
327
 
310
- if (requestCtx?.waitUntil) {
311
- requestCtx.waitUntil(cacheWrite);
312
- } else {
313
- // No waitUntil (e.g. Node.js dev server): run inline as best-effort
314
- await cacheWrite();
315
- }
328
+ await runBackground(requestCtx, cacheWrite, true);
316
329
 
317
330
  return result;
318
331
  };