@rangojs/router 0.0.0-experimental.9 → 0.0.0-experimental.98d9a51b

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 (299) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1532 -155
  4. package/dist/vite/index.js +4444 -2235
  5. package/package.json +69 -62
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +85 -23
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +12 -8
  11. package/skills/document-cache/SKILL.md +18 -16
  12. package/skills/fonts/SKILL.md +6 -4
  13. package/skills/hooks/SKILL.md +333 -71
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +131 -8
  16. package/skills/layout/SKILL.md +100 -3
  17. package/skills/links/SKILL.md +74 -15
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +15 -11
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +405 -45
  23. package/skills/rango/SKILL.md +85 -21
  24. package/skills/response-routes/SKILL.md +144 -91
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/theme/SKILL.md +9 -8
  28. package/skills/typesafety/SKILL.md +316 -87
  29. package/skills/use-cache/SKILL.md +324 -0
  30. package/src/__internal.ts +102 -4
  31. package/src/bin/rango.ts +312 -15
  32. package/src/browser/action-coordinator.ts +97 -0
  33. package/src/browser/action-response-classifier.ts +99 -0
  34. package/src/browser/event-controller.ts +87 -64
  35. package/src/browser/history-state.ts +80 -0
  36. package/src/browser/intercept-utils.ts +52 -0
  37. package/src/browser/link-interceptor.ts +24 -4
  38. package/src/browser/logging.ts +55 -0
  39. package/src/browser/merge-segment-loaders.ts +20 -12
  40. package/src/browser/navigation-bridge.ts +282 -557
  41. package/src/browser/navigation-client.ts +169 -73
  42. package/src/browser/navigation-store.ts +33 -50
  43. package/src/browser/navigation-transaction.ts +297 -0
  44. package/src/browser/network-error-handler.ts +61 -0
  45. package/src/browser/partial-update.ts +292 -310
  46. package/src/browser/prefetch/cache.ts +206 -0
  47. package/src/browser/prefetch/fetch.ts +145 -0
  48. package/src/browser/prefetch/observer.ts +65 -0
  49. package/src/browser/prefetch/policy.ts +48 -0
  50. package/src/browser/prefetch/queue.ts +128 -0
  51. package/src/browser/rango-state.ts +112 -0
  52. package/src/browser/react/Link.tsx +190 -70
  53. package/src/browser/react/NavigationProvider.tsx +78 -11
  54. package/src/browser/react/context.ts +6 -0
  55. package/src/browser/react/filter-segment-order.ts +11 -0
  56. package/src/browser/react/index.ts +12 -12
  57. package/src/browser/react/location-state-shared.ts +95 -53
  58. package/src/browser/react/location-state.ts +60 -15
  59. package/src/browser/react/mount-context.ts +6 -1
  60. package/src/browser/react/nonce-context.ts +23 -0
  61. package/src/browser/react/shallow-equal.ts +27 -0
  62. package/src/browser/react/use-action.ts +29 -51
  63. package/src/browser/react/use-client-cache.ts +5 -3
  64. package/src/browser/react/use-handle.ts +29 -70
  65. package/src/browser/react/use-link-status.ts +6 -5
  66. package/src/browser/react/use-navigation.ts +22 -63
  67. package/src/browser/react/use-params.ts +65 -0
  68. package/src/browser/react/use-pathname.ts +47 -0
  69. package/src/browser/react/use-router.ts +63 -0
  70. package/src/browser/react/use-search-params.ts +56 -0
  71. package/src/browser/react/use-segments.ts +80 -97
  72. package/src/browser/response-adapter.ts +73 -0
  73. package/src/browser/rsc-router.tsx +106 -27
  74. package/src/browser/scroll-restoration.ts +117 -44
  75. package/src/browser/segment-reconciler.ts +216 -0
  76. package/src/browser/segment-structure-assert.ts +16 -0
  77. package/src/browser/server-action-bridge.ts +504 -599
  78. package/src/browser/shallow.ts +6 -1
  79. package/src/browser/types.ts +116 -47
  80. package/src/browser/validate-redirect-origin.ts +29 -0
  81. package/src/build/generate-manifest.ts +82 -21
  82. package/src/build/generate-route-types.ts +36 -752
  83. package/src/build/index.ts +6 -5
  84. package/src/build/route-trie.ts +39 -13
  85. package/src/build/route-types/ast-helpers.ts +25 -0
  86. package/src/build/route-types/ast-route-extraction.ts +98 -0
  87. package/src/build/route-types/codegen.ts +102 -0
  88. package/src/build/route-types/include-resolution.ts +411 -0
  89. package/src/build/route-types/param-extraction.ts +48 -0
  90. package/src/build/route-types/per-module-writer.ts +128 -0
  91. package/src/build/route-types/router-processing.ts +479 -0
  92. package/src/build/route-types/scan-filter.ts +78 -0
  93. package/src/build/runtime-discovery.ts +231 -0
  94. package/src/cache/background-task.ts +34 -0
  95. package/src/cache/cache-key-utils.ts +44 -0
  96. package/src/cache/cache-policy.ts +125 -0
  97. package/src/cache/cache-runtime.ts +338 -0
  98. package/src/cache/cache-scope.ts +122 -303
  99. package/src/cache/cf/cf-cache-store.ts +571 -17
  100. package/src/cache/cf/index.ts +13 -3
  101. package/src/cache/document-cache.ts +101 -72
  102. package/src/cache/handle-capture.ts +81 -0
  103. package/src/cache/handle-snapshot.ts +41 -0
  104. package/src/cache/index.ts +1 -15
  105. package/src/cache/memory-segment-store.ts +191 -13
  106. package/src/cache/profile-registry.ts +73 -0
  107. package/src/cache/read-through-swr.ts +134 -0
  108. package/src/cache/segment-codec.ts +256 -0
  109. package/src/cache/taint.ts +98 -0
  110. package/src/cache/types.ts +72 -122
  111. package/src/client.rsc.tsx +3 -1
  112. package/src/client.tsx +84 -126
  113. package/src/component-utils.ts +4 -4
  114. package/src/components/DefaultDocument.tsx +5 -1
  115. package/src/context-var.ts +86 -0
  116. package/src/debug.ts +17 -7
  117. package/src/errors.ts +77 -7
  118. package/src/handle.ts +15 -10
  119. package/src/handles/MetaTags.tsx +73 -20
  120. package/src/handles/breadcrumbs.ts +66 -0
  121. package/src/handles/index.ts +1 -0
  122. package/src/handles/meta.ts +30 -13
  123. package/src/host/cookie-handler.ts +21 -15
  124. package/src/host/errors.ts +8 -8
  125. package/src/host/index.ts +4 -7
  126. package/src/host/pattern-matcher.ts +27 -27
  127. package/src/host/router.ts +61 -39
  128. package/src/host/testing.ts +8 -8
  129. package/src/host/types.ts +15 -7
  130. package/src/host/utils.ts +1 -1
  131. package/src/href-client.ts +65 -45
  132. package/src/index.rsc.ts +133 -21
  133. package/src/index.ts +164 -52
  134. package/src/internal-debug.ts +11 -0
  135. package/src/loader.rsc.ts +25 -143
  136. package/src/loader.ts +27 -10
  137. package/src/network-error-thrower.tsx +3 -1
  138. package/src/outlet-provider.tsx +45 -0
  139. package/src/prerender/param-hash.ts +4 -2
  140. package/src/prerender/store.ts +158 -13
  141. package/src/prerender.ts +333 -26
  142. package/src/reverse.ts +184 -121
  143. package/src/root-error-boundary.tsx +41 -29
  144. package/src/route-content-wrapper.tsx +7 -4
  145. package/src/route-definition/dsl-helpers.ts +934 -0
  146. package/src/route-definition/helper-factories.ts +200 -0
  147. package/src/route-definition/helpers-types.ts +430 -0
  148. package/src/route-definition/index.ts +52 -0
  149. package/src/route-definition/redirect.ts +93 -0
  150. package/src/route-definition.ts +1 -1431
  151. package/src/route-map-builder.ts +162 -123
  152. package/src/route-name.ts +53 -0
  153. package/src/route-types.ts +48 -9
  154. package/src/router/content-negotiation.ts +116 -0
  155. package/src/router/debug-manifest.ts +72 -0
  156. package/src/router/error-handling.ts +9 -9
  157. package/src/router/find-match.ts +160 -0
  158. package/src/router/handler-context.ts +374 -81
  159. package/src/router/intercept-resolution.ts +26 -16
  160. package/src/router/lazy-includes.ts +236 -0
  161. package/src/router/loader-resolution.ts +215 -122
  162. package/src/router/logging.ts +251 -0
  163. package/src/router/manifest.ts +85 -32
  164. package/src/router/match-api.ts +118 -119
  165. package/src/router/match-context.ts +4 -2
  166. package/src/router/match-handlers.ts +440 -0
  167. package/src/router/match-middleware/background-revalidation.ts +80 -93
  168. package/src/router/match-middleware/cache-lookup.ts +336 -84
  169. package/src/router/match-middleware/cache-store.ts +43 -24
  170. package/src/router/match-middleware/intercept-resolution.ts +45 -20
  171. package/src/router/match-middleware/segment-resolution.ts +17 -8
  172. package/src/router/match-pipelines.ts +10 -45
  173. package/src/router/match-result.ts +34 -28
  174. package/src/router/metrics.ts +235 -15
  175. package/src/router/middleware-cookies.ts +55 -0
  176. package/src/router/middleware-types.ts +222 -0
  177. package/src/router/middleware.ts +327 -369
  178. package/src/router/pattern-matching.ts +197 -41
  179. package/src/router/prerender-match.ts +402 -0
  180. package/src/router/preview-match.ts +170 -0
  181. package/src/router/revalidation.ts +137 -38
  182. package/src/router/router-context.ts +40 -21
  183. package/src/router/router-interfaces.ts +452 -0
  184. package/src/router/router-options.ts +592 -0
  185. package/src/router/router-registry.ts +24 -0
  186. package/src/router/segment-resolution/fresh.ts +570 -0
  187. package/src/router/segment-resolution/helpers.ts +263 -0
  188. package/src/router/segment-resolution/loader-cache.ts +198 -0
  189. package/src/router/segment-resolution/revalidation.ts +1242 -0
  190. package/src/router/segment-resolution/static-store.ts +67 -0
  191. package/src/router/segment-resolution.ts +21 -1315
  192. package/src/router/segment-wrappers.ts +291 -0
  193. package/src/router/telemetry-otel.ts +299 -0
  194. package/src/router/telemetry.ts +300 -0
  195. package/src/router/timeout.ts +148 -0
  196. package/src/router/trie-matching.ts +96 -29
  197. package/src/router/types.ts +16 -9
  198. package/src/router.ts +595 -1984
  199. package/src/rsc/handler-context.ts +45 -0
  200. package/src/rsc/handler.ts +680 -1027
  201. package/src/rsc/helpers.ts +140 -6
  202. package/src/rsc/index.ts +0 -20
  203. package/src/rsc/loader-fetch.ts +209 -0
  204. package/src/rsc/manifest-init.ts +86 -0
  205. package/src/rsc/nonce.ts +14 -0
  206. package/src/rsc/origin-guard.ts +141 -0
  207. package/src/rsc/progressive-enhancement.ts +379 -0
  208. package/src/rsc/response-error.ts +37 -0
  209. package/src/rsc/response-route-handler.ts +347 -0
  210. package/src/rsc/rsc-rendering.ts +237 -0
  211. package/src/rsc/runtime-warnings.ts +42 -0
  212. package/src/rsc/server-action.ts +348 -0
  213. package/src/rsc/ssr-setup.ts +128 -0
  214. package/src/rsc/types.ts +38 -11
  215. package/src/search-params.ts +230 -0
  216. package/src/segment-system.tsx +25 -13
  217. package/src/server/context.ts +173 -48
  218. package/src/server/cookie-store.ts +190 -0
  219. package/src/server/fetchable-loader-store.ts +37 -0
  220. package/src/server/handle-store.ts +94 -15
  221. package/src/server/loader-registry.ts +15 -56
  222. package/src/server/request-context.ts +439 -73
  223. package/src/server.ts +35 -155
  224. package/src/ssr/index.tsx +100 -31
  225. package/src/static-handler.ts +114 -0
  226. package/src/theme/ThemeProvider.tsx +21 -15
  227. package/src/theme/ThemeScript.tsx +5 -5
  228. package/src/theme/constants.ts +5 -2
  229. package/src/theme/index.ts +4 -14
  230. package/src/theme/theme-context.ts +4 -30
  231. package/src/theme/theme-script.ts +21 -18
  232. package/src/types/boundaries.ts +158 -0
  233. package/src/types/cache-types.ts +198 -0
  234. package/src/types/error-types.ts +192 -0
  235. package/src/types/global-namespace.ts +100 -0
  236. package/src/types/handler-context.ts +687 -0
  237. package/src/types/index.ts +88 -0
  238. package/src/types/loader-types.ts +183 -0
  239. package/src/types/route-config.ts +170 -0
  240. package/src/types/route-entry.ts +109 -0
  241. package/src/types/segments.ts +148 -0
  242. package/src/types.ts +1 -1757
  243. package/src/urls/include-helper.ts +197 -0
  244. package/src/urls/index.ts +53 -0
  245. package/src/urls/path-helper-types.ts +339 -0
  246. package/src/urls/path-helper.ts +329 -0
  247. package/src/urls/pattern-types.ts +95 -0
  248. package/src/urls/response-types.ts +106 -0
  249. package/src/urls/type-extraction.ts +372 -0
  250. package/src/urls/urls-function.ts +98 -0
  251. package/src/urls.ts +1 -1282
  252. package/src/use-loader.tsx +85 -77
  253. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  254. package/src/vite/discovery/discover-routers.ts +344 -0
  255. package/src/vite/discovery/prerender-collection.ts +385 -0
  256. package/src/vite/discovery/route-types-writer.ts +258 -0
  257. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  258. package/src/vite/discovery/state.ts +108 -0
  259. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  260. package/src/vite/index.ts +11 -1963
  261. package/src/vite/plugin-types.ts +48 -0
  262. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  263. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  264. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  265. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  266. package/src/vite/plugins/expose-id-utils.ts +287 -0
  267. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  268. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  269. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  270. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  271. package/src/vite/plugins/expose-ids/types.ts +45 -0
  272. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  273. package/src/vite/plugins/refresh-cmd.ts +65 -0
  274. package/src/vite/plugins/use-cache-transform.ts +323 -0
  275. package/src/vite/plugins/version-injector.ts +83 -0
  276. package/src/vite/plugins/version-plugin.ts +254 -0
  277. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -14
  278. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  279. package/src/vite/rango.ts +445 -0
  280. package/src/vite/router-discovery.ts +777 -0
  281. package/src/vite/utils/ast-handler-extract.ts +517 -0
  282. package/src/vite/utils/banner.ts +36 -0
  283. package/src/vite/utils/bundle-analysis.ts +137 -0
  284. package/src/vite/utils/manifest-utils.ts +70 -0
  285. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  286. package/src/vite/utils/prerender-utils.ts +189 -0
  287. package/src/vite/utils/shared-utils.ts +169 -0
  288. package/CLAUDE.md +0 -43
  289. package/src/browser/lru-cache.ts +0 -69
  290. package/src/browser/request-controller.ts +0 -164
  291. package/src/cache/memory-store.ts +0 -253
  292. package/src/href-context.ts +0 -33
  293. package/src/router.gen.ts +0 -6
  294. package/src/urls.gen.ts +0 -8
  295. package/src/vite/expose-handle-id.ts +0 -209
  296. package/src/vite/expose-loader-id.ts +0 -426
  297. package/src/vite/expose-location-state-id.ts +0 -177
  298. package/src/vite/expose-prerender-handler-id.ts +0 -429
  299. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -10,10 +10,20 @@
10
10
  */
11
11
 
12
12
  // Public API
13
- export { CFCacheStore, type CFCacheStoreOptions } from "./cf-cache-store.js";
13
+ export {
14
+ CFCacheStore,
15
+ type CFCacheStoreOptions,
16
+ type KVNamespace,
17
+ } from "./cf-cache-store.js";
14
18
 
15
19
  // Header constants for debugging and inspection
16
- export { CACHE_STALE_AT_HEADER, CACHE_STATUS_HEADER } from "./cf-cache-store.js";
20
+ export {
21
+ CACHE_STALE_AT_HEADER,
22
+ CACHE_STATUS_HEADER,
23
+ } from "./cf-cache-store.js";
17
24
 
18
25
  // Internal exports (re-exported for backwards compatibility, marked @internal in source)
19
- export { type CacheStatus, MAX_REVALIDATION_INTERVAL } from "./cf-cache-store.js";
26
+ export {
27
+ type CacheStatus,
28
+ MAX_REVALIDATION_INTERVAL,
29
+ } from "./cf-cache-store.js";
@@ -13,6 +13,8 @@
13
13
 
14
14
  import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
15
15
  import { getRequestContext } from "../server/request-context.js";
16
+ import { sortedSearchString } from "./cache-key-utils.js";
17
+ import { runBackground } from "./background-task.js";
16
18
 
17
19
  // ============================================================================
18
20
  // Constants
@@ -110,15 +112,24 @@ function addCacheStatusHeader(
110
112
  }
111
113
 
112
114
  /**
113
- * Run onResponse callbacks registered on the request context
115
+ * Drain and run onResponse callbacks registered on the request context.
116
+ * Mirrors the drain semantics of finalizeResponse() in rsc/helpers.ts:
117
+ * callbacks are spliced out so they fire at most once per request.
114
118
  */
115
- function runOnResponseCallbacks(
119
+ function drainOnResponseCallbacks(
116
120
  response: Response,
117
- callbacks: Array<(response: Response) => Response>,
121
+ requestCtx:
122
+ | { _onResponseCallbacks: Array<(r: Response) => Response> }
123
+ | undefined,
118
124
  ): Response {
125
+ if (!requestCtx || requestCtx._onResponseCallbacks.length === 0) {
126
+ return response;
127
+ }
128
+ const callbacks = requestCtx._onResponseCallbacks;
129
+ requestCtx._onResponseCallbacks = [];
119
130
  let result = response;
120
131
  for (const callback of callbacks) {
121
- result = callback(result);
132
+ result = callback(result) ?? result;
122
133
  }
123
134
  return result;
124
135
  }
@@ -185,9 +196,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
185
196
  ): MiddlewareFn<TEnv> {
186
197
  const { skipPaths = [], keyGenerator, isEnabled, debug = false } = options;
187
198
 
188
- const log = debug
189
- ? (message: string) => console.log(message)
190
- : () => {};
199
+ const log = debug ? (message: string) => console.log(message) : () => {};
191
200
 
192
201
  return async function documentCacheMiddleware(
193
202
  ctx: MiddlewareContext<TEnv>,
@@ -195,6 +204,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
195
204
  ): Promise<Response> {
196
205
  const url = ctx.url;
197
206
 
207
+ // Only cache GET requests — mutations and other methods must not be cached
208
+ if (ctx.request.method !== "GET") {
209
+ return next();
210
+ }
211
+
198
212
  // Skip RSC action requests (mutations shouldn't be cached)
199
213
  if (url.searchParams.has("_rsc_action")) {
200
214
  return next();
@@ -231,17 +245,31 @@ export function createDocumentCacheMiddleware<TEnv = any>(
231
245
  const isPartial = url.searchParams.has("_rsc_partial");
232
246
  const typeLabel = isPartial ? "RSC" : "HTML";
233
247
 
234
- // Generate cache key
235
- // For partial requests, include hash of client segments to prevent serving
236
- // wrong cached response when navigating from different pages with different layouts
237
- const clientSegments = url.searchParams.get("_rsc_segments") || "";
238
- const segmentHash = isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
239
- const typeSuffix = isPartial ? ":rsc" : ":html";
240
- const cacheKey = keyGenerator
241
- ? keyGenerator(url) + segmentHash + typeSuffix
242
- : `${url.pathname}${segmentHash}${typeSuffix}`;
248
+ // Track whether next() has been called so the catch block knows
249
+ // whether it is safe to fall through to the handler.
250
+ let handlerCalled = false;
243
251
 
244
252
  try {
253
+ // Generate cache key inside try so a throwing keyGenerator degrades
254
+ // gracefully to the origin handler instead of rejecting the request.
255
+ // This is a deliberate fail-open-to-origin policy: the fallback is
256
+ // "serve uncached from origin", not "use a different cache key".
257
+ const clientSegments = url.searchParams.get("_rsc_segments") || "";
258
+ const segmentHash =
259
+ isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
260
+ const typeSuffix = isPartial ? ":rsc" : ":html";
261
+
262
+ let searchSuffix = "";
263
+ if (!keyGenerator) {
264
+ const sorted = sortedSearchString(url.searchParams);
265
+ if (sorted) {
266
+ searchSuffix = `?${sorted}`;
267
+ }
268
+ }
269
+
270
+ const cacheKey = keyGenerator
271
+ ? keyGenerator(url) + segmentHash + typeSuffix
272
+ : `${url.pathname}${searchSuffix}${segmentHash}${typeSuffix}`;
245
273
  // 1. Check cache
246
274
  const cached = await store.getResponse(cacheKey);
247
275
 
@@ -249,79 +277,75 @@ export function createDocumentCacheMiddleware<TEnv = any>(
249
277
  if (!cached.shouldRevalidate) {
250
278
  // Fresh hit - return immediately
251
279
  log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
252
- let response = addCacheStatusHeader(cached.response, "HIT");
253
- // Run onResponse callbacks even for cache hits
254
- if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
255
- response = runOnResponseCallbacks(
256
- response,
257
- requestCtx._onResponseCallbacks,
258
- );
259
- }
260
- return response;
280
+ return drainOnResponseCallbacks(
281
+ addCacheStatusHeader(cached.response, "HIT"),
282
+ requestCtx,
283
+ );
261
284
  }
262
285
 
263
286
  // Stale hit - return cached response, revalidate in background
264
- log(`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`);
265
-
266
- if (requestCtx) {
267
- requestCtx.waitUntil(async () => {
268
- try {
269
- const fresh = await next();
270
- const directives = shouldCacheResponse(fresh);
271
-
272
- if (directives) {
273
- await store.putResponse!(
274
- cacheKey,
275
- fresh,
276
- directives.sMaxAge!,
277
- directives.staleWhileRevalidate,
278
- );
279
- log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
280
- }
281
- } catch (error) {
282
- console.error(`[DocumentCache] Revalidation failed:`, error);
287
+ log(
288
+ `[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
289
+ );
290
+
291
+ runBackground(requestCtx, async () => {
292
+ try {
293
+ const fresh = await next();
294
+ const directives = shouldCacheResponse(fresh);
295
+
296
+ if (directives) {
297
+ await store.putResponse!(
298
+ cacheKey,
299
+ fresh,
300
+ directives.sMaxAge!,
301
+ directives.staleWhileRevalidate,
302
+ );
303
+ log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
283
304
  }
284
- });
285
- }
305
+ } catch (error) {
306
+ console.error(`[DocumentCache] Revalidation failed:`, error);
307
+ }
308
+ });
286
309
 
287
- let response = addCacheStatusHeader(cached.response, "STALE");
288
- // Run onResponse callbacks even for stale cache hits
289
- if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
290
- response = runOnResponseCallbacks(
291
- response,
292
- requestCtx._onResponseCallbacks,
293
- );
294
- }
295
- return response;
310
+ return drainOnResponseCallbacks(
311
+ addCacheStatusHeader(cached.response, "STALE"),
312
+ requestCtx,
313
+ );
296
314
  }
297
315
 
298
316
  // 2. Cache miss - run handler
317
+ handlerCalled = true;
299
318
  const originalResponse = await next();
300
319
 
301
320
  // 3. Cache if response has appropriate headers
302
321
  const directives = shouldCacheResponse(originalResponse);
303
322
 
304
323
  if (directives) {
305
- log(`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`);
324
+ log(
325
+ `[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`,
326
+ );
327
+
328
+ // If the response has no body (e.g., 200 with empty body), skip caching
329
+ if (!originalResponse.body) {
330
+ return originalResponse;
331
+ }
306
332
 
307
333
  // Tee the body so we can return one stream and cache the other
308
- const [returnStream, cacheStream] = originalResponse.body!.tee();
334
+ const [returnStream, cacheStream] = originalResponse.body.tee();
309
335
 
310
336
  // Clone response for caching (non-blocking)
311
- if (requestCtx) {
312
- requestCtx.waitUntil(async () => {
313
- try {
314
- await store.putResponse!(
315
- cacheKey,
316
- new Response(cacheStream, originalResponse),
317
- directives.sMaxAge!,
318
- directives.staleWhileRevalidate,
319
- );
320
- } catch (error) {
321
- console.error(`[DocumentCache] Cache write failed:`, error);
322
- }
323
- });
324
- }
337
+ runBackground(requestCtx, async () => {
338
+ try {
339
+ await store.putResponse!(
340
+ cacheKey,
341
+ new Response(cacheStream, originalResponse),
342
+ directives.sMaxAge!,
343
+ directives.staleWhileRevalidate,
344
+ );
345
+ } catch (error) {
346
+ console.error(`[DocumentCache] Cache write failed:`, error);
347
+ }
348
+ });
325
349
 
326
350
  return addCacheStatusHeader(
327
351
  new Response(returnStream, originalResponse),
@@ -333,7 +357,12 @@ export function createDocumentCacheMiddleware<TEnv = any>(
333
357
  return originalResponse;
334
358
  } catch (error) {
335
359
  console.error(`[DocumentCache] Error:`, error);
336
- // On any cache error, fall through to handler
360
+ if (handlerCalled) {
361
+ // Post-handler failure (e.g. body.tee()): do not call next() again
362
+ // as that would re-run handler side effects.
363
+ throw error;
364
+ }
365
+ // Pre-handler failure (cache lookup): degrade gracefully to origin
337
366
  return next();
338
367
  }
339
368
  };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Handle Capture
3
+ *
4
+ * Captures handle pushes during cached function execution.
5
+ * Extracted from cache-runtime.ts so tests can import without
6
+ * pulling in @vitejs/plugin-rsc/rsc dependencies.
7
+ */
8
+
9
+ import type { HandleStore } from "../server/handle-store.js";
10
+ import type { SegmentHandleData } from "./types.js";
11
+
12
+ export interface HandleCapture {
13
+ data: Record<string, SegmentHandleData>;
14
+ }
15
+
16
+ /**
17
+ * Active capture tokens per HandleStore.
18
+ *
19
+ * Instead of mutating handleStore.push (which breaks when overlapping
20
+ * captures finish out of order), we install a single interceptor on
21
+ * first use and manage a set of active capture tokens. Each push fans
22
+ * out to every active token. Stopping a capture simply removes the
23
+ * token — order does not matter.
24
+ */
25
+ const activeCapturesMap = new WeakMap<HandleStore, Set<HandleCapture>>();
26
+
27
+ /**
28
+ * One-time interceptor installation. Wraps the original push so every
29
+ * call fans out to all active capture tokens. Installed once per
30
+ * HandleStore instance; subsequent startHandleCapture calls on the
31
+ * same store just add tokens to the Set.
32
+ */
33
+ function ensureInterceptorInstalled(handleStore: HandleStore): void {
34
+ if (activeCapturesMap.has(handleStore)) return;
35
+
36
+ const captures = new Set<HandleCapture>();
37
+ activeCapturesMap.set(handleStore, captures);
38
+
39
+ const originalPush = handleStore.push.bind(handleStore);
40
+ handleStore.push = (
41
+ handleName: string,
42
+ segmentId: string,
43
+ value: unknown,
44
+ ) => {
45
+ for (const capture of captures) {
46
+ if (!capture.data[segmentId]) {
47
+ capture.data[segmentId] = {};
48
+ }
49
+ if (!capture.data[segmentId][handleName]) {
50
+ capture.data[segmentId][handleName] = [];
51
+ }
52
+ capture.data[segmentId][handleName].push(value);
53
+ }
54
+ originalPush(handleName, segmentId, value);
55
+ };
56
+ }
57
+
58
+ /**
59
+ * Start capturing handle pushes for a cached function execution.
60
+ *
61
+ * Concurrency-safe: multiple overlapping captures on the same
62
+ * HandleStore are independent. Each capture registers a token in a
63
+ * Set; stopping removes it. No ordering requirement (LIFO not needed).
64
+ */
65
+ export function startHandleCapture(handleStore: HandleStore): {
66
+ capture: HandleCapture;
67
+ stop: () => void;
68
+ } {
69
+ ensureInterceptorInstalled(handleStore);
70
+
71
+ const capture: HandleCapture = { data: {} };
72
+ const captures = activeCapturesMap.get(handleStore)!;
73
+ captures.add(capture);
74
+
75
+ return {
76
+ capture,
77
+ stop() {
78
+ captures.delete(capture);
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Handle Snapshot
3
+ *
4
+ * Capture and restore handle data for cached segments.
5
+ * Handle data (breadcrumbs, metadata from ctx.use(Handle)) is collected
6
+ * during segment resolution and stored alongside cached segments.
7
+ */
8
+
9
+ import type { ResolvedSegment } from "../types.js";
10
+ import type { HandleStore } from "../server/handle-store.js";
11
+ import type { SegmentHandleData } from "./types.js";
12
+
13
+ /**
14
+ * Capture handle data for a set of segments from the handle store.
15
+ * Used when caching segments to preserve their handle data.
16
+ */
17
+ export function captureHandles(
18
+ segments: ResolvedSegment[],
19
+ handleStore: HandleStore,
20
+ ): Record<string, SegmentHandleData> {
21
+ const handles: Record<string, SegmentHandleData> = {};
22
+ for (const seg of segments) {
23
+ handles[seg.id] = handleStore.getDataForSegment(seg.id);
24
+ }
25
+ return handles;
26
+ }
27
+
28
+ /**
29
+ * Restore handle data from a cached snapshot into the handle store.
30
+ * Used when serving cached segments to replay their handle data.
31
+ */
32
+ export function restoreHandles(
33
+ handles: Record<string, SegmentHandleData>,
34
+ handleStore: HandleStore,
35
+ ): void {
36
+ for (const [segId, segHandles] of Object.entries(handles)) {
37
+ if (Object.keys(segHandles).length > 0) {
38
+ handleStore.replaySegmentData(segId, segHandles);
39
+ }
40
+ }
41
+ }
@@ -10,21 +10,6 @@
10
10
  * - CacheScope / createCacheScope - Request-scoped cache provider
11
11
  */
12
12
 
13
- // Generic cache store types (reserved for future extensibility)
14
- // These types support caching arbitrary values like Response, Stream, etc.
15
- // Currently unused - segment caching uses SegmentCacheStore directly.
16
- export type {
17
- CacheStore,
18
- CacheEntry,
19
- CacheValue,
20
- CacheValueType,
21
- CachePutOptions,
22
- CacheMetadata,
23
- } from "./types.js";
24
-
25
- // Generic memory cache (reserved for future extensibility)
26
- export { MemoryCacheStore } from "./memory-store.js";
27
-
28
13
  // Segment cache store types and implementations
29
14
  export type {
30
15
  SegmentCacheStore,
@@ -44,6 +29,7 @@ export { MemorySegmentCacheStore } from "./memory-segment-store.js";
44
29
  export {
45
30
  CFCacheStore,
46
31
  type CFCacheStoreOptions,
32
+ type KVNamespace,
47
33
  CACHE_STALE_AT_HEADER,
48
34
  CACHE_STATUS_HEADER,
49
35
  } from "./cf/index.js";
@@ -5,15 +5,83 @@
5
5
  * Uses globalThis to survive HMR in development.
6
6
  */
7
7
 
8
- import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult } from "./types.js";
8
+ import type {
9
+ SegmentCacheStore,
10
+ CachedEntryData,
11
+ CacheDefaults,
12
+ CacheGetResult,
13
+ CacheItemResult,
14
+ CacheItemOptions,
15
+ SegmentHandleData,
16
+ } from "./types.js";
9
17
  import type { RequestContext } from "../server/request-context.js";
18
+ import {
19
+ resolveTtl,
20
+ resolveSwrWindow,
21
+ computeExpiration,
22
+ DEFAULT_FUNCTION_TTL,
23
+ } from "./cache-policy.js";
10
24
 
11
- const CACHE_GLOBAL_KEY = "__rsc_router_segment_cache_store__";
25
+ const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
26
+ const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
27
+ const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
28
+
29
+ /**
30
+ * Get or create a named Map from a globalThis-backed registry.
31
+ * The registry survives HMR; individual stores are keyed by name.
32
+ */
33
+ function getNamedMap<V>(registryKey: string, name: string): Map<string, V> {
34
+ let registry = (globalThis as any)[registryKey] as
35
+ | Map<string, Map<string, V>>
36
+ | undefined;
37
+ if (!registry) {
38
+ registry = new Map();
39
+ (globalThis as any)[registryKey] = registry;
40
+ }
41
+ let map = registry.get(name);
42
+ if (!map) {
43
+ map = new Map<string, V>();
44
+ registry.set(name, map);
45
+ }
46
+ return map;
47
+ }
48
+
49
+ interface CachedResponseEntry {
50
+ body: ArrayBuffer;
51
+ status: number;
52
+ headers: [string, string][];
53
+ expiresAt: number;
54
+ staleAt: number;
55
+ }
56
+
57
+ interface CachedItemEntry {
58
+ value: string;
59
+ handles?: Record<string, SegmentHandleData>;
60
+ expiresAt: number;
61
+ staleAt: number;
62
+ }
12
63
 
13
64
  /**
14
65
  * Options for MemorySegmentCacheStore
15
66
  */
16
67
  export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
68
+ /**
69
+ * Optional name for this store instance. Named stores persist their
70
+ * backing Map on globalThis so data survives Vite HMR module reloads.
71
+ * Stores with different names get separate Maps.
72
+ *
73
+ * When omitted, the store uses a plain instance-level Map with no
74
+ * globalThis sharing, which is the safest default for isolation.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * // Two named stores are isolated from each other
79
+ * const fast = new MemorySegmentCacheStore({ name: "fast", defaults: { ttl: 10 } });
80
+ * const slow = new MemorySegmentCacheStore({ name: "slow", defaults: { ttl: 300 } });
81
+ * ```
82
+ */
83
+ name?: string;
84
+
17
85
  /**
18
86
  * Default cache options for cache() boundaries.
19
87
  * When cache() is called without explicit ttl/swr,
@@ -35,14 +103,14 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
35
103
  * @example
36
104
  * ```typescript
37
105
  * keyGenerator: (ctx, defaultKey) => {
38
- * const locale = ctx.cookie('locale') || 'en';
106
+ * const locale = cookies().get('locale')?.value || 'en';
39
107
  * return `${locale}:${defaultKey}`;
40
108
  * }
41
109
  * ```
42
110
  */
43
111
  keyGenerator?: (
44
112
  ctx: RequestContext<TEnv>,
45
- defaultKey: string
113
+ defaultKey: string,
46
114
  ) => string | Promise<string>;
47
115
  }
48
116
 
@@ -69,19 +137,40 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
69
137
  * })
70
138
  * ```
71
139
  */
72
- export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
140
+ export class MemorySegmentCacheStore<
141
+ TEnv = unknown,
142
+ > implements SegmentCacheStore<TEnv> {
73
143
  private cache: Map<string, CachedEntryData>;
144
+ private responseCache: Map<string, CachedResponseEntry>;
145
+ private itemCache: Map<string, CachedItemEntry>;
74
146
  readonly defaults?: CacheDefaults;
75
147
  readonly keyGenerator?: (
76
148
  ctx: RequestContext<TEnv>,
77
- defaultKey: string
149
+ defaultKey: string,
78
150
  ) => string | Promise<string>;
79
151
 
80
152
  constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
81
- // Use globalThis to survive HMR in development
82
- this.cache =
83
- (globalThis as any)[CACHE_GLOBAL_KEY] ??
84
- ((globalThis as any)[CACHE_GLOBAL_KEY] = new Map<string, CachedEntryData>());
153
+ if (options?.name != null) {
154
+ // Named stores use the globalThis registry so data survives HMR.
155
+ // Each name gets its own isolated Map.
156
+ this.cache = getNamedMap<CachedEntryData>(
157
+ CACHE_REGISTRY_KEY,
158
+ options.name,
159
+ );
160
+ this.responseCache = getNamedMap<CachedResponseEntry>(
161
+ RESPONSE_CACHE_REGISTRY_KEY,
162
+ options.name,
163
+ );
164
+ this.itemCache = getNamedMap<CachedItemEntry>(
165
+ ITEM_CACHE_REGISTRY_KEY,
166
+ options.name,
167
+ );
168
+ } else {
169
+ // Unnamed stores get a plain instance-level Map (no globalThis sharing).
170
+ this.cache = new Map<string, CachedEntryData>();
171
+ this.responseCache = new Map<string, CachedResponseEntry>();
172
+ this.itemCache = new Map<string, CachedItemEntry>();
173
+ }
85
174
  this.defaults = options?.defaults;
86
175
  this.keyGenerator = options?.keyGenerator;
87
176
  }
@@ -103,7 +192,12 @@ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStor
103
192
  return { data: cached, shouldRevalidate: false };
104
193
  }
105
194
 
106
- async set(key: string, data: CachedEntryData, ttl: number, _swr?: number): Promise<void> {
195
+ async set(
196
+ key: string,
197
+ data: CachedEntryData,
198
+ ttl: number,
199
+ _swr?: number,
200
+ ): Promise<void> {
107
201
  // Note: Memory store doesn't implement SWR - entries just expire at TTL
108
202
  // For SWR support, use CFCacheStore or similar distributed cache
109
203
  const entry: CachedEntryData = {
@@ -119,6 +213,88 @@ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStor
119
213
 
120
214
  async clear(): Promise<void> {
121
215
  this.cache.clear();
216
+ this.responseCache.clear();
217
+ this.itemCache.clear();
218
+ }
219
+
220
+ async getResponse(
221
+ key: string,
222
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
223
+ const cached = this.responseCache.get(key);
224
+ if (!cached) return null;
225
+
226
+ if (Date.now() > cached.expiresAt) {
227
+ this.responseCache.delete(key);
228
+ return null;
229
+ }
230
+
231
+ const isStale = cached.staleAt > 0 && Date.now() > cached.staleAt;
232
+ const headers = new Headers(cached.headers);
233
+ return {
234
+ response: new Response(cached.body, {
235
+ status: cached.status,
236
+ headers,
237
+ }),
238
+ shouldRevalidate: isStale,
239
+ };
240
+ }
241
+
242
+ async putResponse(
243
+ key: string,
244
+ response: Response,
245
+ ttl: number,
246
+ swr?: number,
247
+ ): Promise<void> {
248
+ const body = await response.clone().arrayBuffer();
249
+ const headers: [string, string][] = [];
250
+ response.headers.forEach((value, name) => {
251
+ headers.push([name, value]);
252
+ });
253
+
254
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
255
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
256
+
257
+ this.responseCache.set(key, {
258
+ body,
259
+ status: response.status,
260
+ headers,
261
+ expiresAt,
262
+ staleAt,
263
+ });
264
+ }
265
+
266
+ async getItem(key: string): Promise<CacheItemResult | null> {
267
+ const cached = this.itemCache.get(key);
268
+ if (!cached) return null;
269
+
270
+ const now = Date.now();
271
+ if (now > cached.expiresAt) {
272
+ this.itemCache.delete(key);
273
+ return null;
274
+ }
275
+
276
+ const isStale = now > cached.staleAt;
277
+ return {
278
+ value: cached.value,
279
+ handles: cached.handles,
280
+ shouldRevalidate: isStale,
281
+ };
282
+ }
283
+
284
+ async setItem(
285
+ key: string,
286
+ value: string,
287
+ options?: CacheItemOptions,
288
+ ): Promise<void> {
289
+ const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
290
+ const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
291
+ const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
292
+ this.itemCache.set(key, {
293
+ value,
294
+ handles: options?.handles,
295
+ expiresAt,
296
+ staleAt,
297
+ });
122
298
  }
123
299
 
124
300
  /**
@@ -133,7 +309,7 @@ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStor
133
309
  }
134
310
 
135
311
  /**
136
- * Reset the global cache state.
312
+ * Reset the global cache registry.
137
313
  * Useful for test isolation - call this in beforeEach to ensure
138
314
  * tests don't share cache state via globalThis.
139
315
  *
@@ -145,6 +321,8 @@ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStor
145
321
  * ```
146
322
  */
147
323
  static resetGlobalCache(): void {
148
- delete (globalThis as any)[CACHE_GLOBAL_KEY];
324
+ delete (globalThis as any)[CACHE_REGISTRY_KEY];
325
+ delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
326
+ delete (globalThis as any)[ITEM_CACHE_REGISTRY_KEY];
149
327
  }
150
328
  }