@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.8a4d0430

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 (300) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +4474 -867
  5. package/package.json +60 -51
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +50 -21
  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 +167 -0
  13. package/skills/hooks/SKILL.md +334 -72
  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 +89 -30
  18. package/skills/loader/SKILL.md +388 -38
  19. package/skills/middleware/SKILL.md +171 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +78 -1
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +226 -14
  26. package/skills/router-setup/SKILL.md +123 -30
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +318 -89
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +102 -4
  32. package/src/bin/rango.ts +321 -0
  33. package/src/browser/action-coordinator.ts +97 -0
  34. package/src/browser/action-response-classifier.ts +99 -0
  35. package/src/browser/event-controller.ts +87 -64
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/intercept-utils.ts +52 -0
  38. package/src/browser/link-interceptor.ts +24 -4
  39. package/src/browser/logging.ts +55 -0
  40. package/src/browser/merge-segment-loaders.ts +20 -12
  41. package/src/browser/navigation-bridge.ts +285 -553
  42. package/src/browser/navigation-client.ts +124 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +295 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +258 -308
  47. package/src/browser/prefetch/cache.ts +146 -0
  48. package/src/browser/prefetch/fetch.ts +135 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +42 -0
  51. package/src/browser/prefetch/queue.ts +88 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +185 -73
  54. package/src/browser/react/NavigationProvider.tsx +51 -11
  55. package/src/browser/react/context.ts +6 -0
  56. package/src/browser/react/filter-segment-order.ts +11 -0
  57. package/src/browser/react/index.ts +12 -12
  58. package/src/browser/react/location-state-shared.ts +95 -53
  59. package/src/browser/react/location-state.ts +60 -15
  60. package/src/browser/react/mount-context.ts +6 -1
  61. package/src/browser/react/nonce-context.ts +23 -0
  62. package/src/browser/react/shallow-equal.ts +27 -0
  63. package/src/browser/react/use-action.ts +29 -51
  64. package/src/browser/react/use-client-cache.ts +5 -3
  65. package/src/browser/react/use-handle.ts +32 -79
  66. package/src/browser/react/use-href.tsx +2 -2
  67. package/src/browser/react/use-link-status.ts +6 -5
  68. package/src/browser/react/use-navigation.ts +22 -63
  69. package/src/browser/react/use-params.ts +65 -0
  70. package/src/browser/react/use-pathname.ts +47 -0
  71. package/src/browser/react/use-router.ts +63 -0
  72. package/src/browser/react/use-search-params.ts +56 -0
  73. package/src/browser/react/use-segments.ts +80 -97
  74. package/src/browser/response-adapter.ts +73 -0
  75. package/src/browser/rsc-router.tsx +107 -26
  76. package/src/browser/scroll-restoration.ts +92 -16
  77. package/src/browser/segment-reconciler.ts +216 -0
  78. package/src/browser/segment-structure-assert.ts +16 -0
  79. package/src/browser/server-action-bridge.ts +504 -599
  80. package/src/browser/shallow.ts +6 -1
  81. package/src/browser/types.ts +109 -47
  82. package/src/browser/validate-redirect-origin.ts +29 -0
  83. package/src/build/generate-manifest.ts +235 -24
  84. package/src/build/generate-route-types.ts +36 -0
  85. package/src/build/index.ts +13 -0
  86. package/src/build/route-trie.ts +265 -0
  87. package/src/build/route-types/ast-helpers.ts +25 -0
  88. package/src/build/route-types/ast-route-extraction.ts +98 -0
  89. package/src/build/route-types/codegen.ts +102 -0
  90. package/src/build/route-types/include-resolution.ts +411 -0
  91. package/src/build/route-types/param-extraction.ts +48 -0
  92. package/src/build/route-types/per-module-writer.ts +128 -0
  93. package/src/build/route-types/router-processing.ts +469 -0
  94. package/src/build/route-types/scan-filter.ts +78 -0
  95. package/src/build/runtime-discovery.ts +231 -0
  96. package/src/cache/background-task.ts +34 -0
  97. package/src/cache/cache-key-utils.ts +44 -0
  98. package/src/cache/cache-policy.ts +125 -0
  99. package/src/cache/cache-runtime.ts +338 -0
  100. package/src/cache/cache-scope.ts +120 -303
  101. package/src/cache/cf/cf-cache-store.ts +119 -7
  102. package/src/cache/cf/index.ts +8 -2
  103. package/src/cache/document-cache.ts +101 -72
  104. package/src/cache/handle-capture.ts +81 -0
  105. package/src/cache/handle-snapshot.ts +41 -0
  106. package/src/cache/index.ts +0 -15
  107. package/src/cache/memory-segment-store.ts +191 -13
  108. package/src/cache/profile-registry.ts +73 -0
  109. package/src/cache/read-through-swr.ts +134 -0
  110. package/src/cache/segment-codec.ts +256 -0
  111. package/src/cache/taint.ts +98 -0
  112. package/src/cache/types.ts +72 -122
  113. package/src/client.rsc.tsx +3 -1
  114. package/src/client.tsx +106 -126
  115. package/src/component-utils.ts +4 -4
  116. package/src/components/DefaultDocument.tsx +5 -1
  117. package/src/context-var.ts +86 -0
  118. package/src/debug.ts +17 -7
  119. package/src/errors.ts +108 -2
  120. package/src/handle.ts +15 -29
  121. package/src/handles/MetaTags.tsx +73 -20
  122. package/src/handles/breadcrumbs.ts +66 -0
  123. package/src/handles/index.ts +1 -0
  124. package/src/handles/meta.ts +30 -13
  125. package/src/host/cookie-handler.ts +21 -15
  126. package/src/host/errors.ts +8 -8
  127. package/src/host/index.ts +4 -7
  128. package/src/host/pattern-matcher.ts +27 -27
  129. package/src/host/router.ts +61 -39
  130. package/src/host/testing.ts +8 -8
  131. package/src/host/types.ts +15 -7
  132. package/src/host/utils.ts +1 -1
  133. package/src/href-client.ts +119 -29
  134. package/src/index.rsc.ts +153 -19
  135. package/src/index.ts +211 -30
  136. package/src/internal-debug.ts +11 -0
  137. package/src/loader.rsc.ts +26 -157
  138. package/src/loader.ts +27 -10
  139. package/src/network-error-thrower.tsx +3 -1
  140. package/src/outlet-provider.tsx +45 -0
  141. package/src/prerender/param-hash.ts +37 -0
  142. package/src/prerender/store.ts +185 -0
  143. package/src/prerender.ts +463 -0
  144. package/src/reverse.ts +330 -0
  145. package/src/root-error-boundary.tsx +41 -29
  146. package/src/route-content-wrapper.tsx +7 -4
  147. package/src/route-definition/dsl-helpers.ts +934 -0
  148. package/src/route-definition/helper-factories.ts +200 -0
  149. package/src/route-definition/helpers-types.ts +430 -0
  150. package/src/route-definition/index.ts +52 -0
  151. package/src/route-definition/redirect.ts +93 -0
  152. package/src/route-definition.ts +1 -1428
  153. package/src/route-map-builder.ts +211 -123
  154. package/src/route-name.ts +53 -0
  155. package/src/route-types.ts +59 -8
  156. package/src/router/content-negotiation.ts +116 -0
  157. package/src/router/debug-manifest.ts +72 -0
  158. package/src/router/error-handling.ts +9 -9
  159. package/src/router/find-match.ts +158 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +395 -0
  162. package/src/router/lazy-includes.ts +234 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +248 -0
  165. package/src/router/manifest.ts +148 -35
  166. package/src/router/match-api.ts +620 -0
  167. package/src/router/match-context.ts +5 -3
  168. package/src/router/match-handlers.ts +440 -0
  169. package/src/router/match-middleware/background-revalidation.ts +80 -93
  170. package/src/router/match-middleware/cache-lookup.ts +382 -9
  171. package/src/router/match-middleware/cache-store.ts +51 -22
  172. package/src/router/match-middleware/intercept-resolution.ts +55 -17
  173. package/src/router/match-middleware/segment-resolution.ts +24 -6
  174. package/src/router/match-pipelines.ts +10 -45
  175. package/src/router/match-result.ts +34 -28
  176. package/src/router/metrics.ts +235 -15
  177. package/src/router/middleware-cookies.ts +55 -0
  178. package/src/router/middleware-types.ts +222 -0
  179. package/src/router/middleware.ts +324 -367
  180. package/src/router/pattern-matching.ts +211 -43
  181. package/src/router/prerender-match.ts +402 -0
  182. package/src/router/preview-match.ts +170 -0
  183. package/src/router/revalidation.ts +137 -38
  184. package/src/router/router-context.ts +36 -21
  185. package/src/router/router-interfaces.ts +452 -0
  186. package/src/router/router-options.ts +592 -0
  187. package/src/router/router-registry.ts +24 -0
  188. package/src/router/segment-resolution/fresh.ts +570 -0
  189. package/src/router/segment-resolution/helpers.ts +263 -0
  190. package/src/router/segment-resolution/loader-cache.ts +198 -0
  191. package/src/router/segment-resolution/revalidation.ts +1241 -0
  192. package/src/router/segment-resolution/static-store.ts +67 -0
  193. package/src/router/segment-resolution.ts +21 -0
  194. package/src/router/segment-wrappers.ts +289 -0
  195. package/src/router/telemetry-otel.ts +299 -0
  196. package/src/router/telemetry.ts +300 -0
  197. package/src/router/timeout.ts +148 -0
  198. package/src/router/trie-matching.ts +239 -0
  199. package/src/router/types.ts +77 -3
  200. package/src/router.ts +692 -4257
  201. package/src/rsc/handler-context.ts +45 -0
  202. package/src/rsc/handler.ts +764 -754
  203. package/src/rsc/helpers.ts +140 -6
  204. package/src/rsc/index.ts +0 -20
  205. package/src/rsc/loader-fetch.ts +209 -0
  206. package/src/rsc/manifest-init.ts +86 -0
  207. package/src/rsc/nonce.ts +14 -0
  208. package/src/rsc/origin-guard.ts +141 -0
  209. package/src/rsc/progressive-enhancement.ts +379 -0
  210. package/src/rsc/response-error.ts +37 -0
  211. package/src/rsc/response-route-handler.ts +347 -0
  212. package/src/rsc/rsc-rendering.ts +235 -0
  213. package/src/rsc/runtime-warnings.ts +42 -0
  214. package/src/rsc/server-action.ts +348 -0
  215. package/src/rsc/ssr-setup.ts +128 -0
  216. package/src/rsc/types.ts +38 -11
  217. package/src/search-params.ts +230 -0
  218. package/src/segment-system.tsx +25 -13
  219. package/src/server/context.ts +182 -51
  220. package/src/server/cookie-store.ts +190 -0
  221. package/src/server/fetchable-loader-store.ts +37 -0
  222. package/src/server/handle-store.ts +94 -15
  223. package/src/server/loader-registry.ts +15 -56
  224. package/src/server/request-context.ts +430 -70
  225. package/src/server.ts +35 -130
  226. package/src/ssr/index.tsx +100 -31
  227. package/src/static-handler.ts +114 -0
  228. package/src/theme/ThemeProvider.tsx +21 -15
  229. package/src/theme/ThemeScript.tsx +5 -5
  230. package/src/theme/constants.ts +5 -2
  231. package/src/theme/index.ts +4 -14
  232. package/src/theme/theme-context.ts +4 -30
  233. package/src/theme/theme-script.ts +21 -18
  234. package/src/types/boundaries.ts +158 -0
  235. package/src/types/cache-types.ts +198 -0
  236. package/src/types/error-types.ts +192 -0
  237. package/src/types/global-namespace.ts +100 -0
  238. package/src/types/handler-context.ts +687 -0
  239. package/src/types/index.ts +88 -0
  240. package/src/types/loader-types.ts +183 -0
  241. package/src/types/route-config.ts +170 -0
  242. package/src/types/route-entry.ts +102 -0
  243. package/src/types/segments.ts +148 -0
  244. package/src/types.ts +1 -1623
  245. package/src/urls/include-helper.ts +197 -0
  246. package/src/urls/index.ts +53 -0
  247. package/src/urls/path-helper-types.ts +339 -0
  248. package/src/urls/path-helper.ts +329 -0
  249. package/src/urls/pattern-types.ts +95 -0
  250. package/src/urls/response-types.ts +106 -0
  251. package/src/urls/type-extraction.ts +372 -0
  252. package/src/urls/urls-function.ts +98 -0
  253. package/src/urls.ts +1 -802
  254. package/src/use-loader.tsx +85 -77
  255. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  256. package/src/vite/discovery/discover-routers.ts +344 -0
  257. package/src/vite/discovery/prerender-collection.ts +385 -0
  258. package/src/vite/discovery/route-types-writer.ts +258 -0
  259. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  260. package/src/vite/discovery/state.ts +110 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -1133
  263. package/src/vite/plugin-types.ts +131 -0
  264. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  265. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  266. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  267. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -51
  268. package/src/vite/plugins/expose-id-utils.ts +287 -0
  269. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  270. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  271. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  272. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  273. package/src/vite/plugins/expose-ids/types.ts +45 -0
  274. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  275. package/src/vite/plugins/refresh-cmd.ts +65 -0
  276. package/src/vite/plugins/use-cache-transform.ts +323 -0
  277. package/src/vite/plugins/version-injector.ts +83 -0
  278. package/src/vite/plugins/version-plugin.ts +254 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +510 -0
  282. package/src/vite/router-discovery.ts +785 -0
  283. package/src/vite/utils/ast-handler-extract.ts +517 -0
  284. package/src/vite/utils/banner.ts +36 -0
  285. package/src/vite/utils/bundle-analysis.ts +137 -0
  286. package/src/vite/utils/manifest-utils.ts +70 -0
  287. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  288. package/src/vite/utils/prerender-utils.ts +189 -0
  289. package/src/vite/utils/shared-utils.ts +169 -0
  290. package/CLAUDE.md +0 -43
  291. package/src/browser/lru-cache.ts +0 -69
  292. package/src/browser/request-controller.ts +0 -164
  293. package/src/cache/memory-store.ts +0 -253
  294. package/src/href-context.ts +0 -33
  295. package/src/href.ts +0 -255
  296. package/src/server/route-manifest-cache.ts +0 -173
  297. package/src/vite/expose-handle-id.ts +0 -209
  298. package/src/vite/expose-loader-id.ts +0 -426
  299. package/src/vite/expose-location-state-id.ts +0 -177
  300. /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
@@ -2,59 +2,65 @@
2
2
  * CacheScope - Runtime cache scope for iterator-based caching
3
3
  *
4
4
  * Each cache() boundary in the route tree creates a new CacheScope.
5
- * The scope owns: config, serialization, and storage operations.
5
+ * The scope owns: config, key management, and storage operations.
6
+ *
7
+ * Serialization is delegated to segment-codec.ts.
8
+ * Handle data capture/restore is delegated to handle-snapshot.ts.
6
9
  */
7
10
 
8
- /// <reference types="@vitejs/plugin-rsc/types" />
9
-
10
11
  import type { PartialCacheOptions } from "../types.js";
11
12
  import type { ResolvedSegment } from "../types.js";
12
- import type {
13
- SegmentCacheStore,
14
- SegmentHandleData,
15
- CachedEntryData,
16
- SerializedSegmentData,
17
- } from "./types.js";
18
- import { getRequestContext } from "../server/request-context.js";
13
+ import type { SegmentCacheStore, CachedEntryData } from "./types.js";
14
+ import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
19
15
  import {
20
- renderToReadableStream,
21
- createTemporaryReferenceSet,
22
- } from "@vitejs/plugin-rsc/rsc";
23
- import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
24
-
25
- // ============================================================================
26
- // Constants
27
- // ============================================================================
28
-
29
- /** Default TTL when no explicit value or store defaults are configured */
30
- const DEFAULT_TTL_SECONDS = 60;
16
+ getRequestContext,
17
+ _getRequestContext,
18
+ } from "../server/request-context.js";
19
+ import { serializeSegments, deserializeSegments } from "./segment-codec.js";
20
+ import { captureHandles, restoreHandles } from "./handle-snapshot.js";
21
+ import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
22
+ import {
23
+ DEFAULT_ROUTE_TTL,
24
+ resolveCacheKey,
25
+ resolveCacheStore,
26
+ } from "./cache-policy.js";
27
+
28
+ function debugCacheLog(message: string): void {
29
+ if (INTERNAL_RANGO_DEBUG) {
30
+ console.log(message);
31
+ }
32
+ }
31
33
 
32
34
  // ============================================================================
33
- // Serialization Utilities (internal)
35
+ // Key Generation (internal)
34
36
  // ============================================================================
35
37
 
36
38
  /**
37
- * Generate cache key base from pathname and params.
38
- * Params are sorted alphabetically for consistent key generation.
39
+ * Generate cache key base from host, pathname, route params, and search params.
40
+ * Host is included to prevent cross-host cache collisions on shared stores.
41
+ * Route params and search params are sorted alphabetically for deterministic keys.
42
+ * Internal _rsc* and __* query params are excluded.
39
43
  * @internal
40
44
  */
41
45
  function getCacheKeyBase(
46
+ host: string,
42
47
  pathname: string,
43
- params?: Record<string, string>
48
+ params?: Record<string, string>,
49
+ searchParams?: URLSearchParams,
44
50
  ): string {
45
- const paramStr = params
46
- ? Object.entries(params)
47
- .sort(([a], [b]) => a.localeCompare(b))
48
- .map(([k, v]) => `${k}=${v}`)
49
- .join("&")
50
- : "";
51
-
52
- return paramStr ? `${pathname}:${paramStr}` : pathname;
51
+ const paramStr = sortedRouteParams(params);
52
+ const searchStr = searchParams ? sortedSearchString(searchParams) : "";
53
+
54
+ let key = `${host}${pathname}`;
55
+ if (paramStr) key += `:${paramStr}`;
56
+ if (searchStr) key += `?${searchStr}`;
57
+ return key;
53
58
  }
54
59
 
55
60
  /**
56
61
  * Generate default cache key for a route request.
57
- * Single cache entry per route - uses pathname as the key.
62
+ * Includes pathname, route params, and user-facing search params for
63
+ * correct scoping. Internal _rsc* params are excluded.
58
64
  * Includes request type prefix since they produce different segment sets:
59
65
  * - doc: document requests (full page load)
60
66
  * - partial: navigation requests (client-side navigation)
@@ -64,203 +70,17 @@ function getCacheKeyBase(
64
70
  function getDefaultRouteCacheKey(
65
71
  pathname: string,
66
72
  params?: Record<string, string>,
67
- isIntercept?: boolean
73
+ isIntercept?: boolean,
68
74
  ): string {
69
75
  const ctx = getRequestContext();
70
76
  const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
77
+ const searchParams = ctx?.url.searchParams;
78
+ const host = ctx?.url.host ?? "localhost";
71
79
 
72
80
  // Intercept navigations get their own cache namespace
73
81
  const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
74
82
 
75
- return `${prefix}:${getCacheKeyBase(pathname, params)}`;
76
- }
77
-
78
- /**
79
- * Convert a ReadableStream to a string.
80
- * @internal
81
- */
82
- async function streamToString(
83
- stream: ReadableStream<Uint8Array>
84
- ): Promise<string> {
85
- const reader = stream.getReader();
86
- const decoder = new TextDecoder();
87
- let result = "";
88
-
89
- while (true) {
90
- const { done, value } = await reader.read();
91
- if (done) break;
92
- result += decoder.decode(value, { stream: true });
93
- }
94
-
95
- result += decoder.decode(); // flush
96
- return result;
97
- }
98
-
99
- /**
100
- * Convert a string to a ReadableStream.
101
- * @internal
102
- */
103
- function stringToStream(str: string): ReadableStream<Uint8Array> {
104
- const encoder = new TextEncoder();
105
- const uint8 = encoder.encode(str);
106
-
107
- return new ReadableStream({
108
- start(controller) {
109
- controller.enqueue(uint8);
110
- controller.close();
111
- },
112
- });
113
- }
114
-
115
- /**
116
- * RSC-serialize a value using React Server Components stream.
117
- * Used for serializing loaderData, layout, loading components etc.
118
- * @internal
119
- */
120
- async function rscSerialize(value: unknown): Promise<string | undefined> {
121
- if (value === undefined || value === null) return undefined;
122
-
123
- const temporaryReferences = createTemporaryReferenceSet();
124
- const stream = renderToReadableStream(value, { temporaryReferences });
125
- return streamToString(stream);
126
- }
127
-
128
- /**
129
- * RSC-deserialize a value from a stored string.
130
- * @internal
131
- */
132
- async function rscDeserialize<T>(
133
- encoded: string | undefined
134
- ): Promise<T | undefined> {
135
- if (!encoded) return undefined;
136
-
137
- const temporaryReferences = createTemporaryReferenceSet();
138
- const stream = stringToStream(encoded);
139
- return createFromReadableStream<T>(stream, { temporaryReferences });
140
- }
141
-
142
- /**
143
- * Serialize segments for storage.
144
- * Each segment's component, layout, loading, and loaderData are RSC-serialized.
145
- * Metadata is preserved as-is.
146
- * @internal
147
- */
148
- async function serializeSegments(
149
- segments: ResolvedSegment[]
150
- ): Promise<SerializedSegmentData[]> {
151
- const serialized: SerializedSegmentData[] = [];
152
-
153
- for (const segment of segments) {
154
- const temporaryReferences = createTemporaryReferenceSet();
155
-
156
- // Await component if it's a Promise (intercepts with loading keep component as Promise)
157
- const componentResolved =
158
- segment.component instanceof Promise
159
- ? await segment.component
160
- : segment.component;
161
-
162
- // Serialize the component to RSC stream
163
- const stream = renderToReadableStream(componentResolved, {
164
- temporaryReferences,
165
- });
166
-
167
- // Convert stream to string
168
- const encoded = await streamToString(stream);
169
-
170
- // RSC-serialize layout if present (ReactNode)
171
- const encodedLayout = segment.layout
172
- ? await rscSerialize(segment.layout)
173
- : undefined;
174
-
175
- // RSC-serialize loading if present (ReactNode) - preserves tree structure
176
- // Use "null" string to distinguish explicit null from undefined
177
- const encodedLoading =
178
- segment.loading !== undefined
179
- ? segment.loading === null
180
- ? "null"
181
- : await rscSerialize(segment.loading)
182
- : undefined;
183
-
184
- // Await and RSC-serialize loaderData if present
185
- const loaderDataResolved =
186
- segment.loaderData instanceof Promise
187
- ? await segment.loaderData
188
- : segment.loaderData;
189
- const encodedLoaderData = await rscSerialize(loaderDataResolved);
190
-
191
- // Await and RSC-serialize loaderDataPromise if present
192
- const loaderDataPromiseResolved =
193
- segment.loaderDataPromise instanceof Promise
194
- ? await segment.loaderDataPromise
195
- : segment.loaderDataPromise;
196
- const encodedLoaderDataPromise = await rscSerialize(
197
- loaderDataPromiseResolved
198
- );
199
-
200
- serialized.push({
201
- encoded,
202
- encodedLayout,
203
- encodedLoading,
204
- encodedLoaderData,
205
- encodedLoaderDataPromise,
206
- metadata: {
207
- id: segment.id,
208
- type: segment.type,
209
- namespace: segment.namespace,
210
- index: segment.index,
211
- params: segment.params,
212
- slot: segment.slot,
213
- belongsToRoute: segment.belongsToRoute,
214
- layoutName: segment.layoutName,
215
- parallelName: segment.parallelName,
216
- loaderId: segment.loaderId,
217
- loaderIds: segment.loaderIds,
218
- },
219
- });
220
- }
221
-
222
- return serialized;
223
- }
224
-
225
- /**
226
- * Deserialize segments from storage.
227
- * Reconstructs ResolvedSegment objects from RSC-serialized data.
228
- * @internal
229
- */
230
- async function deserializeSegments(
231
- data: SerializedSegmentData[]
232
- ): Promise<ResolvedSegment[]> {
233
- const segments: ResolvedSegment[] = [];
234
-
235
- for (const item of data) {
236
- const temporaryReferences = createTemporaryReferenceSet();
237
-
238
- // Revive the component from cached string
239
- const stream = stringToStream(item.encoded);
240
- const component = await createFromReadableStream(stream, {
241
- temporaryReferences,
242
- });
243
-
244
- // RSC-deserialize layout, loaderData, loaderDataPromise in parallel
245
- const [layout, loaderData, loaderDataPromise, loadingData] =
246
- await Promise.all([
247
- rscDeserialize(item.encodedLayout),
248
- rscDeserialize(item.encodedLoaderData),
249
- rscDeserialize(item.encodedLoaderDataPromise),
250
- rscDeserialize(item.encodedLoading),
251
- ]);
252
-
253
- segments.push({
254
- ...item.metadata,
255
- component: await component,
256
- layout,
257
- loading: loadingData,
258
- loaderData,
259
- loaderDataPromise,
260
- } as ResolvedSegment);
261
- }
262
-
263
- return segments;
83
+ return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
264
84
  }
265
85
 
266
86
  // ============================================================================
@@ -271,7 +91,8 @@ async function deserializeSegments(
271
91
  * CacheScope represents a cache boundary in the route tree.
272
92
  *
273
93
  * When withCache encounters an entry with cache config, it creates
274
- * a new CacheScope. The scope owns serialization, storage, and TTL.
94
+ * a new CacheScope. The scope owns key management, TTL resolution,
95
+ * and storage operations. Serialization is handled by segment-codec.ts.
275
96
  *
276
97
  * Store resolution priority:
277
98
  * 1. Explicit store in cache() options
@@ -291,7 +112,7 @@ export class CacheScope {
291
112
 
292
113
  constructor(
293
114
  config: PartialCacheOptions | false,
294
- parent: CacheScope | null = null
115
+ parent: CacheScope | null = null,
295
116
  ) {
296
117
  this.config = config;
297
118
  this.parent = parent;
@@ -324,7 +145,7 @@ export class CacheScope {
324
145
  }
325
146
 
326
147
  // Hardcoded fallback
327
- return DEFAULT_TTL_SECONDS;
148
+ return DEFAULT_ROUTE_TTL;
328
149
  }
329
150
 
330
151
  /**
@@ -348,65 +169,22 @@ export class CacheScope {
348
169
  * 1. Explicit store from cache() options
349
170
  * 2. App-level store from request context
350
171
  */
351
- private getStore(): SegmentCacheStore | null {
352
- // Explicit store from cache() options takes precedence
353
- if (this.explicitStore) {
354
- return this.explicitStore;
355
- }
356
- // Fall back to app-level store from request context
357
- const ctx = getRequestContext();
358
- return ctx?._cacheStore ?? null;
172
+ getStore(): SegmentCacheStore | null {
173
+ return resolveCacheStore(this.explicitStore);
359
174
  }
360
175
 
361
176
  /**
362
- * Resolve the cache key using custom key functions or default generation.
363
- *
364
- * Resolution priority:
365
- * 1. Route-level `key` function (full override)
366
- * 2. Store-level `keyGenerator` (modifies default key)
367
- * 3. Default key generation (prefix:pathname:params)
368
- *
177
+ * Resolve the cache key using the shared 3-tier priority.
369
178
  * @internal
370
179
  */
371
180
  private async resolveKey(
372
181
  pathname: string,
373
182
  params: Record<string, string>,
374
- isIntercept?: boolean
183
+ isIntercept?: boolean,
375
184
  ): Promise<string> {
376
- const requestCtx = getRequestContext();
377
- if (!requestCtx) {
378
- // Fallback to default key if no request context
379
- return getDefaultRouteCacheKey(pathname, params, isIntercept);
380
- }
381
-
382
- // Priority 1: Route-level key function (full override)
383
- if (this.config !== false && this.config.key) {
384
- try {
385
- const customKey = await this.config.key(requestCtx);
386
- return customKey;
387
- } catch (error) {
388
- console.error(`[CacheScope] Custom key function failed, using default:`, error);
389
- return getDefaultRouteCacheKey(pathname, params, isIntercept);
390
- }
391
- }
392
-
393
- // Generate default key
394
185
  const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
395
-
396
- // Priority 2: Store-level keyGenerator (modifies default key)
397
- const store = this.getStore();
398
- if (store?.keyGenerator) {
399
- try {
400
- const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
401
- return modifiedKey;
402
- } catch (error) {
403
- console.error(`[CacheScope] Store keyGenerator failed, using default:`, error);
404
- return defaultKey;
405
- }
406
- }
407
-
408
- // Priority 3: Default key
409
- return defaultKey;
186
+ const keyFn = this.config !== false ? this.config.key : undefined;
187
+ return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
410
188
  }
411
189
 
412
190
  /**
@@ -420,13 +198,34 @@ export class CacheScope {
420
198
  async lookupRoute(
421
199
  pathname: string,
422
200
  params: Record<string, string>,
423
- isIntercept?: boolean
201
+ isIntercept?: boolean,
424
202
  ): Promise<{
425
203
  segments: ResolvedSegment[];
426
204
  shouldRevalidate: boolean;
427
205
  } | null> {
428
206
  if (!this.enabled) return null;
429
207
 
208
+ // Evaluate condition — skip cache read when condition returns false
209
+ if (this.config !== false && this.config.condition) {
210
+ const requestCtx = getRequestContext();
211
+ if (requestCtx) {
212
+ try {
213
+ if (!this.config.condition(requestCtx)) {
214
+ debugCacheLog(
215
+ `[CacheScope] condition returned false, skipping cache read`,
216
+ );
217
+ return null;
218
+ }
219
+ } catch (error) {
220
+ console.error(
221
+ `[CacheScope] condition function threw, skipping cache read:`,
222
+ error,
223
+ );
224
+ return null;
225
+ }
226
+ }
227
+ }
228
+
430
229
  const store = this.getStore();
431
230
  if (!store) return null;
432
231
 
@@ -437,7 +236,7 @@ export class CacheScope {
437
236
  const result = await store.get(key);
438
237
 
439
238
  if (!result) {
440
- console.log(`[CacheScope] MISS: ${key}`);
239
+ debugCacheLog(`[CacheScope] MISS: ${key}`);
441
240
  return null;
442
241
  }
443
242
 
@@ -447,21 +246,19 @@ export class CacheScope {
447
246
  const segments = await deserializeSegments(cached.segments);
448
247
 
449
248
  // Replay handle data
450
- const handleStore = getRequestContext()?._handleStore;
249
+ const handleStore = _getRequestContext()?._handleStore;
451
250
  if (handleStore) {
452
- for (const [segId, segHandles] of Object.entries(cached.handles)) {
453
- if (Object.keys(segHandles).length > 0) {
454
- handleStore.replaySegmentData(segId, segHandles);
455
- }
456
- }
251
+ restoreHandles(cached.handles, handleStore);
457
252
  }
458
253
 
459
- const segmentTypes = segments.map((s) =>
460
- s.type === "parallel" ? s.slot : s.type
461
- );
462
- console.log(
463
- `[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`
464
- );
254
+ if (INTERNAL_RANGO_DEBUG) {
255
+ const segmentTypes = segments.map((s) =>
256
+ s.type === "parallel" ? s.slot : s.type,
257
+ );
258
+ debugCacheLog(
259
+ `[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`,
260
+ );
261
+ }
465
262
 
466
263
  return { segments, shouldRevalidate };
467
264
  } catch (error) {
@@ -484,10 +281,31 @@ export class CacheScope {
484
281
  pathname: string,
485
282
  params: Record<string, string>,
486
283
  segments: ResolvedSegment[],
487
- isIntercept?: boolean
284
+ isIntercept?: boolean,
488
285
  ): Promise<void> {
489
286
  if (!this.enabled || segments.length === 0) return;
490
287
 
288
+ // Evaluate condition — skip cache write when condition returns false
289
+ if (this.config !== false && this.config.condition) {
290
+ const conditionCtx = getRequestContext();
291
+ if (conditionCtx) {
292
+ try {
293
+ if (!this.config.condition(conditionCtx)) {
294
+ debugCacheLog(
295
+ `[CacheScope] condition returned false, skipping cache write`,
296
+ );
297
+ return;
298
+ }
299
+ } catch (error) {
300
+ console.error(
301
+ `[CacheScope] condition function threw, skipping cache write:`,
302
+ error,
303
+ );
304
+ return;
305
+ }
306
+ }
307
+ }
308
+
491
309
  const store = this.getStore();
492
310
  if (!store) return;
493
311
 
@@ -517,16 +335,13 @@ export class CacheScope {
517
335
  // For partial requests: null components are expected (client already has them)
518
336
  if (!isPartial) {
519
337
  const hasAllComponents = nonLoaderSegments.every(
520
- (s) => s.component !== null
338
+ (s) => s.component !== null,
521
339
  );
522
340
  if (!hasAllComponents) return;
523
341
  }
524
342
 
525
343
  // Collect handle data for non-loader segments only
526
- const handles: Record<string, SegmentHandleData> = {};
527
- for (const seg of nonLoaderSegments) {
528
- handles[seg.id] = handleStore.getDataForSegment(seg.id);
529
- }
344
+ const handles = captureHandles(nonLoaderSegments, handleStore);
530
345
 
531
346
  try {
532
347
  // Serialize non-loader segments only
@@ -540,12 +355,14 @@ export class CacheScope {
540
355
 
541
356
  await store.set(key, data, ttl, swr);
542
357
 
543
- const segmentTypes = nonLoaderSegments.map((s) =>
544
- s.type === "parallel" ? s.slot : s.type
545
- );
546
- console.log(
547
- `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`
548
- );
358
+ if (INTERNAL_RANGO_DEBUG) {
359
+ const segmentTypes = nonLoaderSegments.map((s) =>
360
+ s.type === "parallel" ? s.slot : s.type,
361
+ );
362
+ debugCacheLog(
363
+ `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`,
364
+ );
365
+ }
549
366
  } catch (error) {
550
367
  console.error(`[CacheScope] Failed to cache ${key}:`, error);
551
368
  }
@@ -558,7 +375,7 @@ export class CacheScope {
558
375
  */
559
376
  export function createCacheScope(
560
377
  config: { options: PartialCacheOptions | false } | undefined,
561
- parent: CacheScope | null = null
378
+ parent: CacheScope | null = null,
562
379
  ): CacheScope | null {
563
380
  if (!config) return parent; // No config, inherit parent
564
381
  return new CacheScope(config.options, parent);