@rangojs/router 0.0.0-experimental.6 → 0.0.0-experimental.64e3ae06

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 +9 -0
  2. package/README.md +884 -4
  3. package/dist/bin/rango.js +1606 -0
  4. package/dist/vite/index.js +4567 -769
  5. package/package.json +77 -58
  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 +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 +282 -557
  42. package/src/browser/navigation-client.ts +170 -71
  43. package/src/browser/navigation-store.ts +33 -50
  44. package/src/browser/navigation-transaction.ts +297 -0
  45. package/src/browser/network-error-handler.ts +61 -0
  46. package/src/browser/partial-update.ts +292 -310
  47. package/src/browser/prefetch/cache.ts +206 -0
  48. package/src/browser/prefetch/fetch.ts +145 -0
  49. package/src/browser/prefetch/observer.ts +65 -0
  50. package/src/browser/prefetch/policy.ts +48 -0
  51. package/src/browser/prefetch/queue.ts +128 -0
  52. package/src/browser/rango-state.ts +112 -0
  53. package/src/browser/react/Link.tsx +193 -73
  54. package/src/browser/react/NavigationProvider.tsx +78 -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 +167 -55
  76. package/src/browser/scroll-restoration.ts +117 -44
  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 +118 -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 +479 -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 +122 -305
  101. package/src/cache/cf/cf-cache-store.ts +571 -17
  102. package/src/cache/cf/index.ts +13 -3
  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 +1 -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 +165 -0
  126. package/src/host/errors.ts +97 -0
  127. package/src/host/index.ts +53 -0
  128. package/src/host/pattern-matcher.ts +214 -0
  129. package/src/host/router.ts +352 -0
  130. package/src/host/testing.ts +79 -0
  131. package/src/host/types.ts +146 -0
  132. package/src/host/utils.ts +25 -0
  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 -147
  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 +217 -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 +160 -0
  160. package/src/router/handler-context.ts +374 -81
  161. package/src/router/intercept-resolution.ts +397 -0
  162. package/src/router/lazy-includes.ts +236 -0
  163. package/src/router/loader-resolution.ts +215 -122
  164. package/src/router/logging.ts +251 -0
  165. package/src/router/manifest.ts +150 -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 +25 -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 +327 -369
  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 +40 -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 +1242 -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 +291 -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 +665 -4182
  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 +237 -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 +26 -14
  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 +439 -73
  225. package/src/server.ts +35 -128
  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 +109 -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 +108 -0
  261. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  262. package/src/vite/index.ts +11 -782
  263. package/src/vite/plugin-types.ts +48 -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 -53
  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 +266 -0
  279. package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -16
  280. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  281. package/src/vite/rango.ts +445 -0
  282. package/src/vite/router-discovery.ts +777 -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
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Cache Profile Registry
3
+ *
4
+ * Named cache profiles for "use cache" directive.
5
+ * Profiles define TTL, SWR, and optional default tags.
6
+ * Set by createRouter() at startup, read by registerCachedFunction() at runtime.
7
+ */
8
+
9
+ export interface CacheProfile {
10
+ /** Time-to-live in seconds */
11
+ ttl: number;
12
+ /** Stale-while-revalidate window in seconds */
13
+ swr?: number;
14
+ /** Default cache tags for invalidation */
15
+ tags?: string[];
16
+ }
17
+
18
+ const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
19
+
20
+ let _profiles: Record<string, CacheProfile> = {
21
+ default: DEFAULT_PROFILE,
22
+ };
23
+
24
+ const PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
25
+
26
+ /**
27
+ * Validate and merge user profiles with the default profile.
28
+ * Returns a new object suitable for both DSL-time and request-scoped use.
29
+ *
30
+ * Used by createRouter() to compute the resolved profile map once,
31
+ * stored on the router instance and passed to every request context.
32
+ */
33
+ export function resolveCacheProfiles(
34
+ profiles?: Record<string, CacheProfile>,
35
+ ): Record<string, CacheProfile> {
36
+ const merged: Record<string, CacheProfile> = {
37
+ default: DEFAULT_PROFILE,
38
+ };
39
+ if (profiles) {
40
+ for (const name of Object.keys(profiles)) {
41
+ if (!PROFILE_NAME_RE.test(name)) {
42
+ throw new Error(
43
+ `Invalid cache profile name "${name}". ` +
44
+ `Profile names must match [a-zA-Z0-9_-]+.`,
45
+ );
46
+ }
47
+ merged[name] = profiles[name];
48
+ }
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ /**
54
+ * Set all cache profiles in the global registry.
55
+ * Called by createRouter() at startup for DSL-time resolution
56
+ * (cache("profileName") reads from this during route definition).
57
+ *
58
+ * WARNING: This is global mutable state. It exists only for DSL-time
59
+ * reads. Runtime resolution (registerCachedFunction) uses request-scoped
60
+ * profiles and does NOT read from this registry.
61
+ */
62
+ export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
63
+ _profiles = resolveCacheProfiles(profiles);
64
+ }
65
+
66
+ /**
67
+ * Get a cache profile by name from the global registry.
68
+ * Used only at DSL-time (cache("profileName") inside urls() evaluation).
69
+ * Runtime code uses request-scoped profiles instead.
70
+ */
71
+ export function getCacheProfile(name: string): CacheProfile | undefined {
72
+ return _profiles[name];
73
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * SWR Read-Through Engine
3
+ *
4
+ * Generic read-through cache with stale-while-revalidate support
5
+ * for item-level caching (getItem/setItem).
6
+ *
7
+ * Flow:
8
+ * 1. Lookup cached item by key
9
+ * 2. Fresh hit → deserialize, return
10
+ * 3. Stale hit → deserialize, return, revalidate in background
11
+ * 4. Miss → execute, cache write (blocking when no waitUntil), return
12
+ */
13
+
14
+ import type { CacheItemResult, CacheItemOptions } from "./types.js";
15
+ import { runBackground } from "./background-task.js";
16
+
17
+ interface WaitUntilHost {
18
+ waitUntil?: (fn: () => Promise<void>) => void;
19
+ }
20
+
21
+ export interface ReadThroughItemConfig<T> {
22
+ /** Retrieve a cached item by key */
23
+ getItem: (key: string) => Promise<CacheItemResult | null>;
24
+ /** Store a serialized item by key */
25
+ setItem: (
26
+ key: string,
27
+ value: string,
28
+ options?: CacheItemOptions,
29
+ ) => Promise<void>;
30
+ /** Cache key */
31
+ key: string;
32
+ /** Execute the underlying function/loader on miss or revalidation */
33
+ execute: () => Promise<T>;
34
+ /** Serialize result for storage. Return null to skip caching. */
35
+ serialize: (data: T) => Promise<string | null>;
36
+ /** Deserialize cached value back to the original type */
37
+ deserialize: (value: string) => Promise<T>;
38
+ /** Options passed to setItem on cache write */
39
+ storeOptions: CacheItemOptions;
40
+ /** Called on fresh cache hit (before returning data) */
41
+ onHit?: (cached: CacheItemResult) => void;
42
+ /** Called on stale cache hit (before scheduling background revalidation) */
43
+ onStale?: (cached: CacheItemResult) => void;
44
+ /** Called on cache miss (before executing) */
45
+ onMiss?: () => void;
46
+ /** Called after successful cache write */
47
+ onCached?: () => void;
48
+ /** Host with optional waitUntil for background tasks */
49
+ host?: WaitUntilHost | null;
50
+ }
51
+
52
+ /**
53
+ * Read-through cache with SWR support for item-level caching.
54
+ *
55
+ * On fresh hit: returns deserialized cached data.
56
+ * On stale hit: returns stale data, schedules background revalidation.
57
+ * On miss: executes, writes to cache (blocking when no waitUntil), returns.
58
+ */
59
+ export async function readThroughItem<T>(
60
+ config: ReadThroughItemConfig<T>,
61
+ ): Promise<T> {
62
+ const {
63
+ getItem,
64
+ setItem,
65
+ key,
66
+ execute,
67
+ serialize,
68
+ deserialize,
69
+ storeOptions,
70
+ onHit,
71
+ onStale,
72
+ onMiss,
73
+ onCached,
74
+ host,
75
+ } = config;
76
+
77
+ // Cache lookup
78
+ try {
79
+ const cached = await getItem(key);
80
+
81
+ if (cached) {
82
+ const data = await deserialize(cached.value);
83
+
84
+ if (!cached.shouldRevalidate) {
85
+ onHit?.(cached);
86
+ return data;
87
+ }
88
+
89
+ // Stale hit — return stale data, revalidate in background
90
+ onStale?.(cached);
91
+ runBackground(
92
+ host,
93
+ async () => {
94
+ try {
95
+ const fresh = await execute();
96
+ const serialized = await serialize(fresh);
97
+ if (serialized !== null) {
98
+ await setItem(key, serialized, storeOptions);
99
+ }
100
+ } catch {
101
+ // Background revalidation failed silently
102
+ }
103
+ },
104
+ true,
105
+ );
106
+ return data;
107
+ }
108
+ } catch {
109
+ // Cache lookup failed, fall through to fresh execution
110
+ }
111
+
112
+ // Cache miss
113
+ onMiss?.();
114
+ const data = await execute();
115
+
116
+ // Non-blocking cache write (blocks when no waitUntil)
117
+ await runBackground(
118
+ host,
119
+ async () => {
120
+ try {
121
+ const serialized = await serialize(data);
122
+ if (serialized !== null) {
123
+ await setItem(key, serialized, storeOptions);
124
+ onCached?.();
125
+ }
126
+ } catch {
127
+ // Cache write failed silently
128
+ }
129
+ },
130
+ true,
131
+ );
132
+
133
+ return data;
134
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Segment Codec
3
+ *
4
+ * RSC serialization/deserialization for cached segments.
5
+ * Handles the Flight protocol stream <-> string conversion
6
+ * and the segment-level encode/decode lifecycle.
7
+ */
8
+
9
+ /// <reference types="@vitejs/plugin-rsc/types" />
10
+
11
+ import type { ResolvedSegment } from "../types.js";
12
+ import type { SerializedSegmentData } from "./types.js";
13
+ import {
14
+ renderToReadableStream,
15
+ createTemporaryReferenceSet,
16
+ } from "@vitejs/plugin-rsc/rsc";
17
+ import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
18
+
19
+ // ============================================================================
20
+ // Stream Utilities (internal)
21
+ // ============================================================================
22
+
23
+ /**
24
+ * Convert a ReadableStream to a string.
25
+ */
26
+ export async function streamToString(
27
+ stream: ReadableStream<Uint8Array>,
28
+ ): Promise<string> {
29
+ const reader = stream.getReader();
30
+ const decoder = new TextDecoder();
31
+ let result = "";
32
+
33
+ while (true) {
34
+ const { done, value } = await reader.read();
35
+ if (done) break;
36
+ result += decoder.decode(value, { stream: true });
37
+ }
38
+
39
+ result += decoder.decode(); // flush
40
+ return result;
41
+ }
42
+
43
+ /**
44
+ * Convert a string to a ReadableStream.
45
+ */
46
+ export function stringToStream(str: string): ReadableStream<Uint8Array> {
47
+ const encoder = new TextEncoder();
48
+ const uint8 = encoder.encode(str);
49
+
50
+ return new ReadableStream({
51
+ start(controller) {
52
+ controller.enqueue(uint8);
53
+ controller.close();
54
+ },
55
+ });
56
+ }
57
+
58
+ // ============================================================================
59
+ // RSC Serialization Primitives (internal)
60
+ // ============================================================================
61
+
62
+ /**
63
+ * RSC-serialize a value using React Server Components stream.
64
+ * Used for serializing loaderData, layout, loading components etc.
65
+ *
66
+ * Returns undefined for null/undefined inputs (component fields that are absent).
67
+ * For contexts where null is a valid result (loader caching, "use cache"),
68
+ * use serializeResult() instead which preserves null through RSC Flight.
69
+ */
70
+ export async function rscSerialize(
71
+ value: unknown,
72
+ ): Promise<string | undefined> {
73
+ if (value === undefined || value === null) return undefined;
74
+
75
+ const temporaryReferences = createTemporaryReferenceSet();
76
+ const stream = renderToReadableStream(value, { temporaryReferences });
77
+ return streamToString(stream);
78
+ }
79
+
80
+ /**
81
+ * RSC-deserialize a value from a stored string.
82
+ */
83
+ export async function rscDeserialize<T>(
84
+ encoded: string | undefined,
85
+ ): Promise<T | undefined> {
86
+ if (!encoded) return undefined;
87
+
88
+ const temporaryReferences = createTemporaryReferenceSet();
89
+ const stream = stringToStream(encoded);
90
+ return createFromReadableStream<T>(stream, { temporaryReferences });
91
+ }
92
+
93
+ // ============================================================================
94
+ // Null-Preserving RSC Serialization (for caching)
95
+ // ============================================================================
96
+
97
+ /**
98
+ * RSC-serialize any value including null.
99
+ * Unlike rscSerialize(), this does NOT skip null — it serializes it through
100
+ * RSC Flight so that a loader returning null produces a valid cached entry
101
+ * rather than a permanent cache miss.
102
+ *
103
+ * Returns null only on serialization failure.
104
+ */
105
+ export async function serializeResult(value: unknown): Promise<string | null> {
106
+ try {
107
+ const temporaryReferences = createTemporaryReferenceSet();
108
+ const stream = renderToReadableStream(value, { temporaryReferences });
109
+ return await streamToString(stream);
110
+ } catch {
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * RSC-deserialize a cached result string.
117
+ * Counterpart to serializeResult() — always receives a non-empty string.
118
+ */
119
+ export async function deserializeResult<T>(encoded: string): Promise<T> {
120
+ const temporaryReferences = createTemporaryReferenceSet();
121
+ const stream = stringToStream(encoded);
122
+ return createFromReadableStream<T>(stream, { temporaryReferences });
123
+ }
124
+
125
+ // ============================================================================
126
+ // Public API
127
+ // ============================================================================
128
+
129
+ /**
130
+ * RSC-deserialize a single encoded component string back to a React element.
131
+ * Used by the static handler runtime to revive pre-rendered components.
132
+ * Identical to deserializeResult<unknown>.
133
+ */
134
+ export const deserializeComponent: (encoded: string) => Promise<unknown> =
135
+ deserializeResult;
136
+
137
+ /**
138
+ * Serialize segments for storage.
139
+ * Each segment's component, layout, loading, and loaderData are RSC-serialized.
140
+ * Metadata is preserved as-is.
141
+ */
142
+ export async function serializeSegments(
143
+ segments: ResolvedSegment[],
144
+ ): Promise<SerializedSegmentData[]> {
145
+ return Promise.all(
146
+ segments.map(async (segment): Promise<SerializedSegmentData> => {
147
+ const temporaryReferences = createTemporaryReferenceSet();
148
+
149
+ // Await component if it's a Promise (intercepts with loading keep component as Promise)
150
+ const componentResolved =
151
+ segment.component instanceof Promise
152
+ ? await segment.component
153
+ : segment.component;
154
+
155
+ // Serialize the component to RSC stream
156
+ const stream = renderToReadableStream(componentResolved, {
157
+ temporaryReferences,
158
+ });
159
+
160
+ // RSC-serialize loading: "null" string distinguishes explicit null from undefined
161
+ const encodedLoading =
162
+ segment.loading !== undefined
163
+ ? segment.loading === null
164
+ ? "null"
165
+ : await rscSerialize(segment.loading)
166
+ : undefined;
167
+
168
+ // Await loaderData / loaderDataPromise if they're Promises
169
+ const loaderDataResolved =
170
+ segment.loaderData instanceof Promise
171
+ ? await segment.loaderData
172
+ : segment.loaderData;
173
+ const loaderDataPromiseResolved =
174
+ segment.loaderDataPromise instanceof Promise
175
+ ? await segment.loaderDataPromise
176
+ : segment.loaderDataPromise;
177
+
178
+ // Parallelize stream-to-string and RSC serialization of sub-fields
179
+ const [
180
+ encoded,
181
+ encodedLayout,
182
+ encodedLoaderData,
183
+ encodedLoaderDataPromise,
184
+ ] = await Promise.all([
185
+ streamToString(stream),
186
+ segment.layout ? rscSerialize(segment.layout) : undefined,
187
+ rscSerialize(loaderDataResolved),
188
+ rscSerialize(loaderDataPromiseResolved),
189
+ ]);
190
+
191
+ return {
192
+ encoded,
193
+ encodedLayout,
194
+ encodedLoading,
195
+ encodedLoaderData,
196
+ encodedLoaderDataPromise,
197
+ metadata: {
198
+ id: segment.id,
199
+ type: segment.type,
200
+ namespace: segment.namespace,
201
+ index: segment.index,
202
+ params: segment.params,
203
+ slot: segment.slot,
204
+ belongsToRoute: segment.belongsToRoute,
205
+ layoutName: segment.layoutName,
206
+ parallelName: segment.parallelName,
207
+ loaderId: segment.loaderId,
208
+ loaderIds: segment.loaderIds,
209
+ transition: segment.transition,
210
+ mountPath: segment.mountPath,
211
+ },
212
+ };
213
+ }),
214
+ );
215
+ }
216
+
217
+ /**
218
+ * Deserialize segments from storage.
219
+ * Reconstructs ResolvedSegment objects from RSC-serialized data.
220
+ */
221
+ export async function deserializeSegments(
222
+ data: SerializedSegmentData[],
223
+ ): Promise<ResolvedSegment[]> {
224
+ return Promise.all(
225
+ data.map(async (item): Promise<ResolvedSegment> => {
226
+ const temporaryReferences = createTemporaryReferenceSet();
227
+
228
+ // Handle the "null" sentinel for loading before RSC deserialization.
229
+ // During serialization, loading: null is stored as the string "null" to
230
+ // distinguish it from undefined.
231
+ const loadingIsNullSentinel = item.encodedLoading === "null";
232
+
233
+ const [component, layout, loaderData, loaderDataPromise, loadingData] =
234
+ await Promise.all([
235
+ createFromReadableStream(stringToStream(item.encoded), {
236
+ temporaryReferences,
237
+ }),
238
+ rscDeserialize(item.encodedLayout),
239
+ rscDeserialize(item.encodedLoaderData),
240
+ rscDeserialize(item.encodedLoaderDataPromise),
241
+ loadingIsNullSentinel
242
+ ? (null as any)
243
+ : rscDeserialize(item.encodedLoading),
244
+ ]);
245
+
246
+ return {
247
+ ...item.metadata,
248
+ component,
249
+ layout,
250
+ loading: loadingData,
251
+ loaderData,
252
+ loaderDataPromise,
253
+ } as ResolvedSegment;
254
+ }),
255
+ );
256
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Taint symbol for request-scoped objects.
3
+ *
4
+ * Objects branded with NOCACHE_SYMBOL (ctx, env, req) are excluded from
5
+ * "use cache" cache keys and trigger handle capture mode so that side
6
+ * effects (breadcrumbs, metadata) are recorded and replayed on cache hit.
7
+ */
8
+
9
+ export const NOCACHE_SYMBOL: unique symbol = Symbol.for("rango:nocache") as any;
10
+
11
+ /**
12
+ * Check if a value is tainted (request-scoped, should not be in cache key).
13
+ */
14
+ export function isTainted(value: unknown): boolean {
15
+ return (
16
+ value !== null &&
17
+ value !== undefined &&
18
+ typeof value === "object" &&
19
+ (NOCACHE_SYMBOL as symbol) in (value as Record<symbol, unknown>)
20
+ );
21
+ }
22
+
23
+ /**
24
+ * Symbol stamped on tainted ctx during "use cache" function execution.
25
+ * cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
26
+ * throw if present — reads would cache per-request data under a shared key,
27
+ * and side effects would be lost on cache hit.
28
+ *
29
+ * The value is a numeric reference count, not a boolean. Multiple concurrent
30
+ * cached functions sharing the same ctx/requestCtx each increment on entry
31
+ * and decrement on exit. Guards fire when count > 0.
32
+ */
33
+ export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
34
+ "rango:inside-cache-exec",
35
+ ) as any;
36
+
37
+ /**
38
+ * Increment the INSIDE_CACHE_EXEC ref count on an object.
39
+ */
40
+ export function stampCacheExec(obj: object): void {
41
+ const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
42
+ (obj as any)[INSIDE_CACHE_EXEC] = current + 1;
43
+ }
44
+
45
+ /**
46
+ * Decrement the INSIDE_CACHE_EXEC ref count on an object.
47
+ * Deletes the symbol when the count reaches zero so the `in` check
48
+ * used by guards no longer fires.
49
+ */
50
+ export function unstampCacheExec(obj: object): void {
51
+ const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
52
+ if (current <= 1) {
53
+ delete (obj as any)[INSIDE_CACHE_EXEC];
54
+ } else {
55
+ (obj as any)[INSIDE_CACHE_EXEC] = current - 1;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Throw if ctx is inside a "use cache" execution.
61
+ * Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
62
+ */
63
+ export function assertNotInsideCacheExec(
64
+ ctx: unknown,
65
+ methodName: string,
66
+ ): void {
67
+ if (
68
+ ctx !== null &&
69
+ ctx !== undefined &&
70
+ typeof ctx === "object" &&
71
+ (INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
72
+ ) {
73
+ throw new Error(
74
+ `ctx.${methodName}() cannot be called inside a "use cache" function. ` +
75
+ `Side effects on the request context are lost on cache hit because ` +
76
+ `the function body is skipped. Extract the data fetch into a separate ` +
77
+ `cached function and call ctx.${methodName}() outside it, or use the ` +
78
+ `route-level cache() DSL which caches all segments (handler + children) ` +
79
+ `together.`,
80
+ );
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Brand symbol for functions wrapped by registerCachedFunction().
86
+ * Used at runtime to detect when a "use cache" function is misused
87
+ * (e.g., passed as middleware).
88
+ */
89
+ export const CACHED_FN_SYMBOL: unique symbol = Symbol.for(
90
+ "rango:cached-fn",
91
+ ) as any;
92
+
93
+ /**
94
+ * Check if a value is a "use cache" wrapped function.
95
+ */
96
+ export function isCachedFunction(value: unknown): boolean {
97
+ return typeof value === "function" && (CACHED_FN_SYMBOL as symbol) in value;
98
+ }