@rangojs/router 0.0.0-experimental.7 → 0.0.0-experimental.70

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 (307) hide show
  1. package/AGENTS.md +9 -0
  2. package/README.md +942 -4
  3. package/dist/bin/rango.js +1689 -0
  4. package/dist/vite/index.js +4951 -930
  5. package/package.json +70 -60
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +294 -0
  8. package/skills/caching/SKILL.md +93 -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 +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 +92 -31
  18. package/skills/loader/SKILL.md +404 -44
  19. package/skills/middleware/SKILL.md +173 -34
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +204 -1
  22. package/skills/prerender/SKILL.md +685 -0
  23. package/skills/rango/SKILL.md +85 -16
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +257 -14
  26. package/skills/router-setup/SKILL.md +210 -32
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +9 -8
  29. package/skills/typesafety/SKILL.md +328 -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/app-version.ts +14 -0
  36. package/src/browser/event-controller.ts +92 -64
  37. package/src/browser/history-state.ts +80 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +24 -4
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +20 -12
  42. package/src/browser/navigation-bridge.ts +296 -558
  43. package/src/browser/navigation-client.ts +179 -69
  44. package/src/browser/navigation-store.ts +73 -55
  45. package/src/browser/navigation-transaction.ts +297 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +328 -313
  48. package/src/browser/prefetch/cache.ts +206 -0
  49. package/src/browser/prefetch/fetch.ts +150 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +48 -0
  52. package/src/browser/prefetch/queue.ts +160 -0
  53. package/src/browser/prefetch/resource-ready.ts +77 -0
  54. package/src/browser/rango-state.ts +112 -0
  55. package/src/browser/react/Link.tsx +230 -74
  56. package/src/browser/react/NavigationProvider.tsx +87 -11
  57. package/src/browser/react/context.ts +11 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +12 -12
  60. package/src/browser/react/location-state-shared.ts +95 -53
  61. package/src/browser/react/location-state.ts +60 -15
  62. package/src/browser/react/mount-context.ts +6 -1
  63. package/src/browser/react/nonce-context.ts +23 -0
  64. package/src/browser/react/shallow-equal.ts +27 -0
  65. package/src/browser/react/use-action.ts +29 -51
  66. package/src/browser/react/use-client-cache.ts +5 -3
  67. package/src/browser/react/use-handle.ts +30 -126
  68. package/src/browser/react/use-href.tsx +2 -2
  69. package/src/browser/react/use-link-status.ts +6 -5
  70. package/src/browser/react/use-navigation.ts +22 -63
  71. package/src/browser/react/use-params.ts +65 -0
  72. package/src/browser/react/use-pathname.ts +47 -0
  73. package/src/browser/react/use-router.ts +76 -0
  74. package/src/browser/react/use-search-params.ts +56 -0
  75. package/src/browser/react/use-segments.ts +80 -97
  76. package/src/browser/response-adapter.ts +73 -0
  77. package/src/browser/rsc-router.tsx +214 -58
  78. package/src/browser/scroll-restoration.ts +127 -52
  79. package/src/browser/segment-reconciler.ts +221 -0
  80. package/src/browser/segment-structure-assert.ts +16 -0
  81. package/src/browser/server-action-bridge.ts +510 -603
  82. package/src/browser/shallow.ts +6 -1
  83. package/src/browser/types.ts +141 -48
  84. package/src/browser/validate-redirect-origin.ts +29 -0
  85. package/src/build/generate-manifest.ts +235 -24
  86. package/src/build/generate-route-types.ts +39 -0
  87. package/src/build/index.ts +13 -0
  88. package/src/build/route-trie.ts +265 -0
  89. package/src/build/route-types/ast-helpers.ts +25 -0
  90. package/src/build/route-types/ast-route-extraction.ts +98 -0
  91. package/src/build/route-types/codegen.ts +102 -0
  92. package/src/build/route-types/include-resolution.ts +418 -0
  93. package/src/build/route-types/param-extraction.ts +48 -0
  94. package/src/build/route-types/per-module-writer.ts +128 -0
  95. package/src/build/route-types/router-processing.ts +618 -0
  96. package/src/build/route-types/scan-filter.ts +85 -0
  97. package/src/build/runtime-discovery.ts +231 -0
  98. package/src/cache/background-task.ts +34 -0
  99. package/src/cache/cache-key-utils.ts +44 -0
  100. package/src/cache/cache-policy.ts +125 -0
  101. package/src/cache/cache-runtime.ts +342 -0
  102. package/src/cache/cache-scope.ts +167 -309
  103. package/src/cache/cf/cf-cache-store.ts +571 -17
  104. package/src/cache/cf/index.ts +13 -3
  105. package/src/cache/document-cache.ts +116 -77
  106. package/src/cache/handle-capture.ts +81 -0
  107. package/src/cache/handle-snapshot.ts +41 -0
  108. package/src/cache/index.ts +1 -15
  109. package/src/cache/memory-segment-store.ts +191 -13
  110. package/src/cache/profile-registry.ts +73 -0
  111. package/src/cache/read-through-swr.ts +134 -0
  112. package/src/cache/segment-codec.ts +256 -0
  113. package/src/cache/taint.ts +153 -0
  114. package/src/cache/types.ts +72 -122
  115. package/src/client.rsc.tsx +3 -1
  116. package/src/client.tsx +105 -179
  117. package/src/component-utils.ts +4 -4
  118. package/src/components/DefaultDocument.tsx +5 -1
  119. package/src/context-var.ts +156 -0
  120. package/src/debug.ts +19 -9
  121. package/src/errors.ts +108 -2
  122. package/src/handle.ts +55 -29
  123. package/src/handles/MetaTags.tsx +73 -20
  124. package/src/handles/breadcrumbs.ts +66 -0
  125. package/src/handles/index.ts +1 -0
  126. package/src/handles/meta.ts +30 -13
  127. package/src/host/cookie-handler.ts +21 -15
  128. package/src/host/errors.ts +8 -8
  129. package/src/host/index.ts +4 -7
  130. package/src/host/pattern-matcher.ts +27 -27
  131. package/src/host/router.ts +61 -39
  132. package/src/host/testing.ts +8 -8
  133. package/src/host/types.ts +15 -7
  134. package/src/host/utils.ts +1 -1
  135. package/src/href-client.ts +119 -29
  136. package/src/index.rsc.ts +155 -19
  137. package/src/index.ts +223 -30
  138. package/src/internal-debug.ts +11 -0
  139. package/src/loader.rsc.ts +26 -157
  140. package/src/loader.ts +27 -10
  141. package/src/network-error-thrower.tsx +3 -1
  142. package/src/outlet-provider.tsx +45 -0
  143. package/src/prerender/param-hash.ts +37 -0
  144. package/src/prerender/store.ts +186 -0
  145. package/src/prerender.ts +524 -0
  146. package/src/reverse.ts +351 -0
  147. package/src/root-error-boundary.tsx +41 -29
  148. package/src/route-content-wrapper.tsx +7 -4
  149. package/src/route-definition/dsl-helpers.ts +982 -0
  150. package/src/route-definition/helper-factories.ts +200 -0
  151. package/src/route-definition/helpers-types.ts +434 -0
  152. package/src/route-definition/index.ts +55 -0
  153. package/src/route-definition/redirect.ts +101 -0
  154. package/src/route-definition/resolve-handler-use.ts +149 -0
  155. package/src/route-definition.ts +1 -1428
  156. package/src/route-map-builder.ts +217 -123
  157. package/src/route-name.ts +53 -0
  158. package/src/route-types.ts +70 -8
  159. package/src/router/content-negotiation.ts +215 -0
  160. package/src/router/debug-manifest.ts +72 -0
  161. package/src/router/error-handling.ts +9 -9
  162. package/src/router/find-match.ts +160 -0
  163. package/src/router/handler-context.ts +435 -86
  164. package/src/router/intercept-resolution.ts +402 -0
  165. package/src/router/lazy-includes.ts +237 -0
  166. package/src/router/loader-resolution.ts +356 -128
  167. package/src/router/logging.ts +251 -0
  168. package/src/router/manifest.ts +154 -35
  169. package/src/router/match-api.ts +555 -0
  170. package/src/router/match-context.ts +5 -3
  171. package/src/router/match-handlers.ts +440 -0
  172. package/src/router/match-middleware/background-revalidation.ts +108 -93
  173. package/src/router/match-middleware/cache-lookup.ts +459 -10
  174. package/src/router/match-middleware/cache-store.ts +98 -26
  175. package/src/router/match-middleware/intercept-resolution.ts +57 -17
  176. package/src/router/match-middleware/segment-resolution.ts +80 -6
  177. package/src/router/match-pipelines.ts +10 -45
  178. package/src/router/match-result.ts +55 -33
  179. package/src/router/metrics.ts +240 -15
  180. package/src/router/middleware-cookies.ts +55 -0
  181. package/src/router/middleware-types.ts +220 -0
  182. package/src/router/middleware.ts +324 -369
  183. package/src/router/navigation-snapshot.ts +182 -0
  184. package/src/router/pattern-matching.ts +211 -43
  185. package/src/router/prerender-match.ts +502 -0
  186. package/src/router/preview-match.ts +98 -0
  187. package/src/router/request-classification.ts +310 -0
  188. package/src/router/revalidation.ts +137 -38
  189. package/src/router/route-snapshot.ts +245 -0
  190. package/src/router/router-context.ts +41 -21
  191. package/src/router/router-interfaces.ts +484 -0
  192. package/src/router/router-options.ts +618 -0
  193. package/src/router/router-registry.ts +24 -0
  194. package/src/router/segment-resolution/fresh.ts +743 -0
  195. package/src/router/segment-resolution/helpers.ts +268 -0
  196. package/src/router/segment-resolution/loader-cache.ts +199 -0
  197. package/src/router/segment-resolution/revalidation.ts +1373 -0
  198. package/src/router/segment-resolution/static-store.ts +67 -0
  199. package/src/router/segment-resolution.ts +21 -0
  200. package/src/router/segment-wrappers.ts +291 -0
  201. package/src/router/telemetry-otel.ts +299 -0
  202. package/src/router/telemetry.ts +300 -0
  203. package/src/router/timeout.ts +148 -0
  204. package/src/router/trie-matching.ts +239 -0
  205. package/src/router/types.ts +78 -3
  206. package/src/router.ts +740 -4252
  207. package/src/rsc/handler-context.ts +45 -0
  208. package/src/rsc/handler.ts +907 -797
  209. package/src/rsc/helpers.ts +140 -6
  210. package/src/rsc/index.ts +0 -20
  211. package/src/rsc/loader-fetch.ts +229 -0
  212. package/src/rsc/manifest-init.ts +90 -0
  213. package/src/rsc/nonce.ts +14 -0
  214. package/src/rsc/origin-guard.ts +141 -0
  215. package/src/rsc/progressive-enhancement.ts +391 -0
  216. package/src/rsc/response-error.ts +37 -0
  217. package/src/rsc/response-route-handler.ts +347 -0
  218. package/src/rsc/rsc-rendering.ts +246 -0
  219. package/src/rsc/runtime-warnings.ts +42 -0
  220. package/src/rsc/server-action.ts +356 -0
  221. package/src/rsc/ssr-setup.ts +128 -0
  222. package/src/rsc/types.ts +46 -11
  223. package/src/search-params.ts +230 -0
  224. package/src/segment-system.tsx +165 -17
  225. package/src/server/context.ts +315 -58
  226. package/src/server/cookie-store.ts +190 -0
  227. package/src/server/fetchable-loader-store.ts +37 -0
  228. package/src/server/handle-store.ts +113 -15
  229. package/src/server/loader-registry.ts +24 -64
  230. package/src/server/request-context.ts +607 -81
  231. package/src/server.ts +35 -130
  232. package/src/ssr/index.tsx +103 -30
  233. package/src/static-handler.ts +126 -0
  234. package/src/theme/ThemeProvider.tsx +21 -15
  235. package/src/theme/ThemeScript.tsx +5 -5
  236. package/src/theme/constants.ts +5 -2
  237. package/src/theme/index.ts +4 -14
  238. package/src/theme/theme-context.ts +4 -30
  239. package/src/theme/theme-script.ts +21 -18
  240. package/src/types/boundaries.ts +158 -0
  241. package/src/types/cache-types.ts +198 -0
  242. package/src/types/error-types.ts +192 -0
  243. package/src/types/global-namespace.ts +100 -0
  244. package/src/types/handler-context.ts +791 -0
  245. package/src/types/index.ts +88 -0
  246. package/src/types/loader-types.ts +210 -0
  247. package/src/types/route-config.ts +170 -0
  248. package/src/types/route-entry.ts +109 -0
  249. package/src/types/segments.ts +150 -0
  250. package/src/types.ts +1 -1623
  251. package/src/urls/include-helper.ts +197 -0
  252. package/src/urls/index.ts +53 -0
  253. package/src/urls/path-helper-types.ts +346 -0
  254. package/src/urls/path-helper.ts +364 -0
  255. package/src/urls/pattern-types.ts +107 -0
  256. package/src/urls/response-types.ts +116 -0
  257. package/src/urls/type-extraction.ts +372 -0
  258. package/src/urls/urls-function.ts +98 -0
  259. package/src/urls.ts +1 -802
  260. package/src/use-loader.tsx +161 -81
  261. package/src/vite/discovery/bundle-postprocess.ts +181 -0
  262. package/src/vite/discovery/discover-routers.ts +348 -0
  263. package/src/vite/discovery/prerender-collection.ts +439 -0
  264. package/src/vite/discovery/route-types-writer.ts +258 -0
  265. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  266. package/src/vite/discovery/state.ts +117 -0
  267. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  268. package/src/vite/index.ts +15 -1129
  269. package/src/vite/plugin-types.ts +103 -0
  270. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  271. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  272. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  273. package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
  274. package/src/vite/plugins/expose-id-utils.ts +299 -0
  275. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  276. package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
  277. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  278. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  279. package/src/vite/plugins/expose-ids/types.ts +45 -0
  280. package/src/vite/plugins/expose-internal-ids.ts +786 -0
  281. package/src/vite/plugins/performance-tracks.ts +88 -0
  282. package/src/vite/plugins/refresh-cmd.ts +127 -0
  283. package/src/vite/plugins/use-cache-transform.ts +323 -0
  284. package/src/vite/plugins/version-injector.ts +83 -0
  285. package/src/vite/plugins/version-plugin.ts +266 -0
  286. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
  287. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  288. package/src/vite/rango.ts +462 -0
  289. package/src/vite/router-discovery.ts +918 -0
  290. package/src/vite/utils/ast-handler-extract.ts +517 -0
  291. package/src/vite/utils/banner.ts +36 -0
  292. package/src/vite/utils/bundle-analysis.ts +137 -0
  293. package/src/vite/utils/manifest-utils.ts +70 -0
  294. package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
  295. package/src/vite/utils/prerender-utils.ts +207 -0
  296. package/src/vite/utils/shared-utils.ts +170 -0
  297. package/CLAUDE.md +0 -43
  298. package/src/browser/lru-cache.ts +0 -69
  299. package/src/browser/request-controller.ts +0 -164
  300. package/src/cache/memory-store.ts +0 -253
  301. package/src/href-context.ts +0 -33
  302. package/src/href.ts +0 -255
  303. package/src/server/route-manifest-cache.ts +0 -173
  304. package/src/vite/expose-handle-id.ts +0 -209
  305. package/src/vite/expose-loader-id.ts +0 -426
  306. package/src/vite/expose-location-state-id.ts +0 -177
  307. /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
- const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
76
+ const isPartial = ctx?.originalUrl?.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
 
@@ -508,27 +326,61 @@ export class CacheScope {
508
326
  const key = await this.resolveKey(pathname, params, isIntercept);
509
327
 
510
328
  // Check if this is a partial request (navigation) vs document request
511
- const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
329
+ const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
330
+
331
+ if (INTERNAL_RANGO_DEBUG) {
332
+ debugCacheLog(
333
+ `[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
334
+ );
335
+ }
512
336
 
513
337
  requestCtx.waitUntil(async () => {
338
+ if (INTERNAL_RANGO_DEBUG) {
339
+ debugCacheLog(
340
+ `[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
341
+ );
342
+ }
343
+
514
344
  await handleStore.settled;
515
345
 
516
- // For document requests: only cache if ALL segments have components (complete render)
517
- // For partial requests: null components are expected (client already has them)
346
+ if (INTERNAL_RANGO_DEBUG) {
347
+ debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
348
+ }
349
+
350
+ // For document requests: only cache if layout segments have components
351
+ // (complete render). Parallel and route segments may legitimately have
352
+ // null components — UI-less @meta parallels return null, and void route
353
+ // handlers produce null when the UI lives in parallel slots/layouts.
354
+ // Partial requests always allow null components (client already has them).
518
355
  if (!isPartial) {
519
- const hasAllComponents = nonLoaderSegments.every(
520
- (s) => s.component !== null
356
+ const hasIncompleteLayouts = nonLoaderSegments.some(
357
+ (s) => s.component === null && s.type === "layout",
521
358
  );
522
- if (!hasAllComponents) return;
359
+ if (hasIncompleteLayouts) {
360
+ const nullSegments = nonLoaderSegments
361
+ .filter((s) => s.component === null && s.type === "layout")
362
+ .map((s) => s.id);
363
+ const error = new Error(
364
+ `[CacheScope] Cache write skipped: layout segments have null components ` +
365
+ `(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
366
+ `layout handlers must return JSX for document requests to be cacheable.`,
367
+ );
368
+ error.name = "CacheScopeInvariantError";
369
+ console.error(error.message);
370
+ return;
371
+ }
523
372
  }
524
373
 
525
374
  // 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
- }
375
+ const handles = captureHandles(nonLoaderSegments, handleStore);
530
376
 
531
377
  try {
378
+ if (INTERNAL_RANGO_DEBUG) {
379
+ debugCacheLog(
380
+ `[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
381
+ );
382
+ }
383
+
532
384
  // Serialize non-loader segments only
533
385
  const serializedSegments = await serializeSegments(nonLoaderSegments);
534
386
 
@@ -538,14 +390,20 @@ export class CacheScope {
538
390
  expiresAt: Date.now() + ttl * 1000,
539
391
  };
540
392
 
393
+ if (INTERNAL_RANGO_DEBUG) {
394
+ debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
395
+ }
396
+
541
397
  await store.set(key, data, ttl, swr);
542
398
 
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
- );
399
+ if (INTERNAL_RANGO_DEBUG) {
400
+ const segmentTypes = nonLoaderSegments.map((s) =>
401
+ s.type === "parallel" ? s.slot : s.type,
402
+ );
403
+ debugCacheLog(
404
+ `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`,
405
+ );
406
+ }
549
407
  } catch (error) {
550
408
  console.error(`[CacheScope] Failed to cache ${key}:`, error);
551
409
  }
@@ -558,7 +416,7 @@ export class CacheScope {
558
416
  */
559
417
  export function createCacheScope(
560
418
  config: { options: PartialCacheOptions | false } | undefined,
561
- parent: CacheScope | null = null
419
+ parent: CacheScope | null = null,
562
420
  ): CacheScope | null {
563
421
  if (!config) return parent; // No config, inherit parent
564
422
  return new CacheScope(config.options, parent);