@rangojs/router 0.0.0-experimental.0f44aca1

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 (305) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +899 -0
  3. package/dist/bin/rango.js +1601 -0
  4. package/dist/vite/index.js +5214 -0
  5. package/package.json +176 -0
  6. package/skills/breadcrumbs/SKILL.md +250 -0
  7. package/skills/cache-guide/SKILL.md +262 -0
  8. package/skills/caching/SKILL.md +220 -0
  9. package/skills/composability/SKILL.md +172 -0
  10. package/skills/debug-manifest/SKILL.md +112 -0
  11. package/skills/document-cache/SKILL.md +182 -0
  12. package/skills/fonts/SKILL.md +167 -0
  13. package/skills/hooks/SKILL.md +704 -0
  14. package/skills/host-router/SKILL.md +218 -0
  15. package/skills/intercept/SKILL.md +313 -0
  16. package/skills/layout/SKILL.md +310 -0
  17. package/skills/links/SKILL.md +239 -0
  18. package/skills/loader/SKILL.md +596 -0
  19. package/skills/middleware/SKILL.md +339 -0
  20. package/skills/mime-routes/SKILL.md +128 -0
  21. package/skills/parallel/SKILL.md +305 -0
  22. package/skills/prerender/SKILL.md +643 -0
  23. package/skills/rango/SKILL.md +118 -0
  24. package/skills/response-routes/SKILL.md +411 -0
  25. package/skills/route/SKILL.md +385 -0
  26. package/skills/router-setup/SKILL.md +439 -0
  27. package/skills/tailwind/SKILL.md +129 -0
  28. package/skills/theme/SKILL.md +79 -0
  29. package/skills/typesafety/SKILL.md +623 -0
  30. package/skills/use-cache/SKILL.md +324 -0
  31. package/src/__internal.ts +273 -0
  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 +899 -0
  36. package/src/browser/history-state.ts +80 -0
  37. package/src/browser/index.ts +18 -0
  38. package/src/browser/intercept-utils.ts +52 -0
  39. package/src/browser/link-interceptor.ts +141 -0
  40. package/src/browser/logging.ts +55 -0
  41. package/src/browser/merge-segment-loaders.ts +134 -0
  42. package/src/browser/navigation-bridge.ts +645 -0
  43. package/src/browser/navigation-client.ts +215 -0
  44. package/src/browser/navigation-store.ts +806 -0
  45. package/src/browser/navigation-transaction.ts +295 -0
  46. package/src/browser/network-error-handler.ts +61 -0
  47. package/src/browser/partial-update.ts +550 -0
  48. package/src/browser/prefetch/cache.ts +146 -0
  49. package/src/browser/prefetch/fetch.ts +135 -0
  50. package/src/browser/prefetch/observer.ts +65 -0
  51. package/src/browser/prefetch/policy.ts +42 -0
  52. package/src/browser/prefetch/queue.ts +88 -0
  53. package/src/browser/rango-state.ts +112 -0
  54. package/src/browser/react/Link.tsx +360 -0
  55. package/src/browser/react/NavigationProvider.tsx +386 -0
  56. package/src/browser/react/ScrollRestoration.tsx +94 -0
  57. package/src/browser/react/context.ts +59 -0
  58. package/src/browser/react/filter-segment-order.ts +11 -0
  59. package/src/browser/react/index.ts +52 -0
  60. package/src/browser/react/location-state-shared.ts +162 -0
  61. package/src/browser/react/location-state.ts +107 -0
  62. package/src/browser/react/mount-context.ts +37 -0
  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 +218 -0
  66. package/src/browser/react/use-client-cache.ts +58 -0
  67. package/src/browser/react/use-handle.ts +162 -0
  68. package/src/browser/react/use-href.tsx +40 -0
  69. package/src/browser/react/use-link-status.ts +135 -0
  70. package/src/browser/react/use-mount.ts +31 -0
  71. package/src/browser/react/use-navigation.ts +99 -0
  72. package/src/browser/react/use-params.ts +65 -0
  73. package/src/browser/react/use-pathname.ts +47 -0
  74. package/src/browser/react/use-router.ts +63 -0
  75. package/src/browser/react/use-search-params.ts +56 -0
  76. package/src/browser/react/use-segments.ts +171 -0
  77. package/src/browser/response-adapter.ts +73 -0
  78. package/src/browser/rsc-router.tsx +431 -0
  79. package/src/browser/scroll-restoration.ts +400 -0
  80. package/src/browser/segment-reconciler.ts +216 -0
  81. package/src/browser/segment-structure-assert.ts +83 -0
  82. package/src/browser/server-action-bridge.ts +667 -0
  83. package/src/browser/shallow.ts +40 -0
  84. package/src/browser/types.ts +538 -0
  85. package/src/browser/validate-redirect-origin.ts +29 -0
  86. package/src/build/generate-manifest.ts +438 -0
  87. package/src/build/generate-route-types.ts +36 -0
  88. package/src/build/index.ts +35 -0
  89. package/src/build/route-trie.ts +265 -0
  90. package/src/build/route-types/ast-helpers.ts +25 -0
  91. package/src/build/route-types/ast-route-extraction.ts +98 -0
  92. package/src/build/route-types/codegen.ts +102 -0
  93. package/src/build/route-types/include-resolution.ts +411 -0
  94. package/src/build/route-types/param-extraction.ts +48 -0
  95. package/src/build/route-types/per-module-writer.ts +128 -0
  96. package/src/build/route-types/router-processing.ts +469 -0
  97. package/src/build/route-types/scan-filter.ts +78 -0
  98. package/src/build/runtime-discovery.ts +231 -0
  99. package/src/cache/background-task.ts +34 -0
  100. package/src/cache/cache-key-utils.ts +44 -0
  101. package/src/cache/cache-policy.ts +125 -0
  102. package/src/cache/cache-runtime.ts +338 -0
  103. package/src/cache/cache-scope.ts +382 -0
  104. package/src/cache/cf/cf-cache-store.ts +540 -0
  105. package/src/cache/cf/index.ts +25 -0
  106. package/src/cache/document-cache.ts +369 -0
  107. package/src/cache/handle-capture.ts +81 -0
  108. package/src/cache/handle-snapshot.ts +41 -0
  109. package/src/cache/index.ts +43 -0
  110. package/src/cache/memory-segment-store.ts +328 -0
  111. package/src/cache/profile-registry.ts +73 -0
  112. package/src/cache/read-through-swr.ts +134 -0
  113. package/src/cache/segment-codec.ts +256 -0
  114. package/src/cache/taint.ts +98 -0
  115. package/src/cache/types.ts +342 -0
  116. package/src/client.rsc.tsx +85 -0
  117. package/src/client.tsx +601 -0
  118. package/src/component-utils.ts +76 -0
  119. package/src/components/DefaultDocument.tsx +27 -0
  120. package/src/context-var.ts +86 -0
  121. package/src/debug.ts +243 -0
  122. package/src/default-error-boundary.tsx +88 -0
  123. package/src/deps/browser.ts +8 -0
  124. package/src/deps/html-stream-client.ts +2 -0
  125. package/src/deps/html-stream-server.ts +2 -0
  126. package/src/deps/rsc.ts +10 -0
  127. package/src/deps/ssr.ts +2 -0
  128. package/src/errors.ts +365 -0
  129. package/src/handle.ts +135 -0
  130. package/src/handles/MetaTags.tsx +246 -0
  131. package/src/handles/breadcrumbs.ts +66 -0
  132. package/src/handles/index.ts +7 -0
  133. package/src/handles/meta.ts +264 -0
  134. package/src/host/cookie-handler.ts +165 -0
  135. package/src/host/errors.ts +97 -0
  136. package/src/host/index.ts +53 -0
  137. package/src/host/pattern-matcher.ts +214 -0
  138. package/src/host/router.ts +352 -0
  139. package/src/host/testing.ts +79 -0
  140. package/src/host/types.ts +146 -0
  141. package/src/host/utils.ts +25 -0
  142. package/src/href-client.ts +222 -0
  143. package/src/index.rsc.ts +233 -0
  144. package/src/index.ts +277 -0
  145. package/src/internal-debug.ts +11 -0
  146. package/src/loader.rsc.ts +89 -0
  147. package/src/loader.ts +64 -0
  148. package/src/network-error-thrower.tsx +23 -0
  149. package/src/outlet-context.ts +15 -0
  150. package/src/outlet-provider.tsx +45 -0
  151. package/src/prerender/param-hash.ts +37 -0
  152. package/src/prerender/store.ts +185 -0
  153. package/src/prerender.ts +463 -0
  154. package/src/reverse.ts +330 -0
  155. package/src/root-error-boundary.tsx +289 -0
  156. package/src/route-content-wrapper.tsx +196 -0
  157. package/src/route-definition/dsl-helpers.ts +934 -0
  158. package/src/route-definition/helper-factories.ts +200 -0
  159. package/src/route-definition/helpers-types.ts +430 -0
  160. package/src/route-definition/index.ts +52 -0
  161. package/src/route-definition/redirect.ts +93 -0
  162. package/src/route-definition.ts +1 -0
  163. package/src/route-map-builder.ts +275 -0
  164. package/src/route-name.ts +53 -0
  165. package/src/route-types.ts +259 -0
  166. package/src/router/content-negotiation.ts +116 -0
  167. package/src/router/debug-manifest.ts +72 -0
  168. package/src/router/error-handling.ts +287 -0
  169. package/src/router/find-match.ts +158 -0
  170. package/src/router/handler-context.ts +451 -0
  171. package/src/router/intercept-resolution.ts +395 -0
  172. package/src/router/lazy-includes.ts +234 -0
  173. package/src/router/loader-resolution.ts +420 -0
  174. package/src/router/logging.ts +248 -0
  175. package/src/router/manifest.ts +267 -0
  176. package/src/router/match-api.ts +620 -0
  177. package/src/router/match-context.ts +266 -0
  178. package/src/router/match-handlers.ts +440 -0
  179. package/src/router/match-middleware/background-revalidation.ts +223 -0
  180. package/src/router/match-middleware/cache-lookup.ts +634 -0
  181. package/src/router/match-middleware/cache-store.ts +295 -0
  182. package/src/router/match-middleware/index.ts +81 -0
  183. package/src/router/match-middleware/intercept-resolution.ts +306 -0
  184. package/src/router/match-middleware/segment-resolution.ts +192 -0
  185. package/src/router/match-pipelines.ts +179 -0
  186. package/src/router/match-result.ts +219 -0
  187. package/src/router/metrics.ts +282 -0
  188. package/src/router/middleware-cookies.ts +55 -0
  189. package/src/router/middleware-types.ts +222 -0
  190. package/src/router/middleware.ts +748 -0
  191. package/src/router/pattern-matching.ts +563 -0
  192. package/src/router/prerender-match.ts +402 -0
  193. package/src/router/preview-match.ts +170 -0
  194. package/src/router/revalidation.ts +289 -0
  195. package/src/router/router-context.ts +316 -0
  196. package/src/router/router-interfaces.ts +452 -0
  197. package/src/router/router-options.ts +592 -0
  198. package/src/router/router-registry.ts +24 -0
  199. package/src/router/segment-resolution/fresh.ts +570 -0
  200. package/src/router/segment-resolution/helpers.ts +263 -0
  201. package/src/router/segment-resolution/loader-cache.ts +198 -0
  202. package/src/router/segment-resolution/revalidation.ts +1239 -0
  203. package/src/router/segment-resolution/static-store.ts +67 -0
  204. package/src/router/segment-resolution.ts +21 -0
  205. package/src/router/segment-wrappers.ts +289 -0
  206. package/src/router/telemetry-otel.ts +299 -0
  207. package/src/router/telemetry.ts +300 -0
  208. package/src/router/timeout.ts +148 -0
  209. package/src/router/trie-matching.ts +239 -0
  210. package/src/router/types.ts +170 -0
  211. package/src/router.ts +1002 -0
  212. package/src/rsc/handler-context.ts +45 -0
  213. package/src/rsc/handler.ts +1089 -0
  214. package/src/rsc/helpers.ts +198 -0
  215. package/src/rsc/index.ts +36 -0
  216. package/src/rsc/loader-fetch.ts +209 -0
  217. package/src/rsc/manifest-init.ts +86 -0
  218. package/src/rsc/nonce.ts +32 -0
  219. package/src/rsc/origin-guard.ts +141 -0
  220. package/src/rsc/progressive-enhancement.ts +379 -0
  221. package/src/rsc/response-error.ts +37 -0
  222. package/src/rsc/response-route-handler.ts +347 -0
  223. package/src/rsc/rsc-rendering.ts +235 -0
  224. package/src/rsc/runtime-warnings.ts +42 -0
  225. package/src/rsc/server-action.ts +348 -0
  226. package/src/rsc/ssr-setup.ts +128 -0
  227. package/src/rsc/types.ts +263 -0
  228. package/src/search-params.ts +230 -0
  229. package/src/segment-system.tsx +454 -0
  230. package/src/server/context.ts +591 -0
  231. package/src/server/cookie-store.ts +190 -0
  232. package/src/server/fetchable-loader-store.ts +37 -0
  233. package/src/server/handle-store.ts +308 -0
  234. package/src/server/loader-registry.ts +133 -0
  235. package/src/server/request-context.ts +914 -0
  236. package/src/server/root-layout.tsx +10 -0
  237. package/src/server/tsconfig.json +14 -0
  238. package/src/server.ts +51 -0
  239. package/src/ssr/index.tsx +365 -0
  240. package/src/static-handler.ts +114 -0
  241. package/src/theme/ThemeProvider.tsx +297 -0
  242. package/src/theme/ThemeScript.tsx +61 -0
  243. package/src/theme/constants.ts +62 -0
  244. package/src/theme/index.ts +48 -0
  245. package/src/theme/theme-context.ts +44 -0
  246. package/src/theme/theme-script.ts +155 -0
  247. package/src/theme/types.ts +182 -0
  248. package/src/theme/use-theme.ts +44 -0
  249. package/src/types/boundaries.ts +158 -0
  250. package/src/types/cache-types.ts +198 -0
  251. package/src/types/error-types.ts +192 -0
  252. package/src/types/global-namespace.ts +100 -0
  253. package/src/types/handler-context.ts +687 -0
  254. package/src/types/index.ts +88 -0
  255. package/src/types/loader-types.ts +183 -0
  256. package/src/types/route-config.ts +170 -0
  257. package/src/types/route-entry.ts +102 -0
  258. package/src/types/segments.ts +148 -0
  259. package/src/types.ts +1 -0
  260. package/src/urls/include-helper.ts +197 -0
  261. package/src/urls/index.ts +53 -0
  262. package/src/urls/path-helper-types.ts +339 -0
  263. package/src/urls/path-helper.ts +329 -0
  264. package/src/urls/pattern-types.ts +95 -0
  265. package/src/urls/response-types.ts +106 -0
  266. package/src/urls/type-extraction.ts +372 -0
  267. package/src/urls/urls-function.ts +98 -0
  268. package/src/urls.ts +1 -0
  269. package/src/use-loader.tsx +354 -0
  270. package/src/vite/discovery/bundle-postprocess.ts +184 -0
  271. package/src/vite/discovery/discover-routers.ts +344 -0
  272. package/src/vite/discovery/prerender-collection.ts +385 -0
  273. package/src/vite/discovery/route-types-writer.ts +258 -0
  274. package/src/vite/discovery/self-gen-tracking.ts +47 -0
  275. package/src/vite/discovery/state.ts +110 -0
  276. package/src/vite/discovery/virtual-module-codegen.ts +203 -0
  277. package/src/vite/index.ts +16 -0
  278. package/src/vite/plugin-types.ts +131 -0
  279. package/src/vite/plugins/cjs-to-esm.ts +93 -0
  280. package/src/vite/plugins/client-ref-dedup.ts +115 -0
  281. package/src/vite/plugins/client-ref-hashing.ts +105 -0
  282. package/src/vite/plugins/expose-action-id.ts +365 -0
  283. package/src/vite/plugins/expose-id-utils.ts +287 -0
  284. package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
  285. package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
  286. package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
  287. package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
  288. package/src/vite/plugins/expose-ids/types.ts +45 -0
  289. package/src/vite/plugins/expose-internal-ids.ts +569 -0
  290. package/src/vite/plugins/refresh-cmd.ts +65 -0
  291. package/src/vite/plugins/use-cache-transform.ts +323 -0
  292. package/src/vite/plugins/version-injector.ts +83 -0
  293. package/src/vite/plugins/version-plugin.ts +254 -0
  294. package/src/vite/plugins/version.d.ts +12 -0
  295. package/src/vite/plugins/virtual-entries.ts +123 -0
  296. package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
  297. package/src/vite/rango.ts +510 -0
  298. package/src/vite/router-discovery.ts +785 -0
  299. package/src/vite/utils/ast-handler-extract.ts +517 -0
  300. package/src/vite/utils/banner.ts +36 -0
  301. package/src/vite/utils/bundle-analysis.ts +137 -0
  302. package/src/vite/utils/manifest-utils.ts +70 -0
  303. package/src/vite/utils/package-resolution.ts +121 -0
  304. package/src/vite/utils/prerender-utils.ts +189 -0
  305. package/src/vite/utils/shared-utils.ts +169 -0
@@ -0,0 +1,540 @@
1
+ /// <reference path="../../vite/plugins/version.d.ts" />
2
+
3
+ // Extend CacheStorage with Cloudflare's default cache property
4
+ declare global {
5
+ interface CacheStorage {
6
+ readonly default: Cache;
7
+ }
8
+ }
9
+
10
+ /**
11
+ * Cloudflare Edge Cache Store
12
+ *
13
+ * Production cache store using Cloudflare's Cache API.
14
+ * Handles SWR atomically - get() checks staleness and marks REVALIDATING in one operation.
15
+ *
16
+ * Features:
17
+ * - Extended TTL for SWR window (max-age = ttl + swr)
18
+ * - Staleness via x-edge-cache-stale-at header
19
+ * - Atomic REVALIDATING status for thundering herd prevention
20
+ * - Non-blocking writes via waitUntil
21
+ */
22
+
23
+ import type {
24
+ SegmentCacheStore,
25
+ CachedEntryData,
26
+ CacheDefaults,
27
+ CacheGetResult,
28
+ CacheItemResult,
29
+ CacheItemOptions,
30
+ } from "../types.js";
31
+ import {
32
+ _getRequestContext,
33
+ type RequestContext,
34
+ } from "../../server/request-context.js";
35
+ import { VERSION } from "@rangojs/router:version";
36
+ import {
37
+ resolveTtl,
38
+ resolveSwrWindow,
39
+ DEFAULT_FUNCTION_TTL,
40
+ } from "../cache-policy.js";
41
+
42
+ // ============================================================================
43
+ // Constants
44
+ // ============================================================================
45
+
46
+ /** Header storing timestamp when entry becomes stale */
47
+ export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
48
+
49
+ /** Header storing cache status: HIT | REVALIDATING */
50
+ export const CACHE_STATUS_HEADER = "x-edge-cache-status";
51
+
52
+ /**
53
+ * Maximum age in seconds for REVALIDATING status before allowing new revalidation.
54
+ * After this period, a stale entry in REVALIDATING status will trigger revalidation again.
55
+ * @internal
56
+ */
57
+ export const MAX_REVALIDATION_INTERVAL = 30;
58
+
59
+ // ============================================================================
60
+ // Types
61
+ // ============================================================================
62
+
63
+ /**
64
+ * Cloudflare Workers ExecutionContext (subset we need)
65
+ */
66
+ export interface ExecutionContext {
67
+ waitUntil(promise: Promise<any>): void;
68
+ passThroughOnException(): void;
69
+ }
70
+
71
+ export interface CFCacheStoreOptions<TEnv = unknown> {
72
+ /**
73
+ * Cache namespace. If not provided, uses caches.default (recommended).
74
+ * Only set this if you need isolated cache storage.
75
+ */
76
+ namespace?: string;
77
+
78
+ /**
79
+ * Base URL for cache keys.
80
+ *
81
+ * If not provided, derives from request hostname via requestContext:
82
+ * - Production domains → uses `https://{hostname}/`
83
+ * - Dev/preview (localhost, workers.dev, pages.dev) → uses internal fallback URL
84
+ */
85
+ baseUrl?: string;
86
+
87
+ /** Default cache options */
88
+ defaults?: CacheDefaults;
89
+
90
+ /**
91
+ * Cloudflare ExecutionContext for non-blocking cache writes.
92
+ * Pass the `ctx` from your worker's fetch handler.
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * new CFCacheStore({ ctx: env.ctx })
97
+ * ```
98
+ */
99
+ ctx: ExecutionContext;
100
+
101
+ /**
102
+ * Cache version string override. When this changes, all cached entries are
103
+ * effectively invalidated (new keys won't match old entries).
104
+ *
105
+ * Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
106
+ * Only set this if you need a custom versioning strategy.
107
+ */
108
+ version?: string;
109
+
110
+ /**
111
+ * Custom key generator applied to all cache operations.
112
+ * Receives the full RequestContext (including env) and the default-generated key.
113
+ * Return value becomes the final cache key (unless route overrides with `key` option).
114
+ *
115
+ * @example Using headers for user segmentation
116
+ * ```typescript
117
+ * keyGenerator: (ctx, defaultKey) => {
118
+ * const segment = ctx.request.headers.get('x-user-segment') || 'default';
119
+ * return `${segment}:${defaultKey}`;
120
+ * }
121
+ * ```
122
+ *
123
+ * @example Using env bindings for multi-region
124
+ * ```typescript
125
+ * keyGenerator: (ctx, defaultKey) => {
126
+ * const region = ctx.env.REGION || 'us';
127
+ * return `${region}:${defaultKey}`;
128
+ * }
129
+ * ```
130
+ *
131
+ * @example Using cookies for locale-aware caching
132
+ * ```typescript
133
+ * keyGenerator: (ctx, defaultKey) => {
134
+ * const locale = cookies().get('locale')?.value || 'en';
135
+ * return `${locale}:${defaultKey}`;
136
+ * }
137
+ * ```
138
+ */
139
+ keyGenerator?: (
140
+ ctx: RequestContext<TEnv>,
141
+ defaultKey: string,
142
+ ) => string | Promise<string>;
143
+ }
144
+
145
+ /**
146
+ * Cache status values for the x-edge-cache-status header.
147
+ * @internal
148
+ */
149
+ export type CacheStatus = "HIT" | "REVALIDATING";
150
+
151
+ // ============================================================================
152
+ // CFCacheStore Implementation
153
+ // ============================================================================
154
+
155
+ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
156
+ readonly defaults?: CacheDefaults;
157
+ readonly keyGenerator?: (
158
+ ctx: RequestContext<TEnv>,
159
+ defaultKey: string,
160
+ ) => string | Promise<string>;
161
+
162
+ private readonly namespace?: string;
163
+ private readonly baseUrl: string;
164
+ private readonly waitUntil?: (fn: () => Promise<void>) => void;
165
+ private readonly version?: string;
166
+
167
+ constructor(options: CFCacheStoreOptions<TEnv>) {
168
+ if (!options.ctx) {
169
+ throw new Error(
170
+ "[CFCacheStore] ExecutionContext (ctx) is required. " +
171
+ "Pass the Cloudflare ExecutionContext from your worker's fetch handler: " +
172
+ "new CFCacheStore({ ctx: env.ctx })",
173
+ );
174
+ }
175
+
176
+ this.namespace = options.namespace;
177
+ this.baseUrl = options.baseUrl ?? this.deriveBaseUrl();
178
+ this.defaults = options.defaults;
179
+ this.version = options.version ?? VERSION;
180
+ this.keyGenerator = options.keyGenerator;
181
+ this.waitUntil = (fn) => options.ctx.waitUntil(fn());
182
+ }
183
+
184
+ /**
185
+ * Derive base URL from request hostname via requestContext.
186
+ * Uses internal fallback for dev/preview environments and untrusted hostnames.
187
+ * @internal
188
+ */
189
+ private deriveBaseUrl(): string {
190
+ const fallback = "https://rsc-cache.internal.com/";
191
+
192
+ const ctx = _getRequestContext();
193
+ if (!ctx?.request) {
194
+ return fallback;
195
+ }
196
+
197
+ try {
198
+ const url = new URL(ctx.request.url);
199
+ const hostname = url.hostname;
200
+
201
+ // Use fallback for dev/preview environments
202
+ if (
203
+ hostname === "localhost" ||
204
+ hostname === "127.0.0.1" ||
205
+ hostname.endsWith(".workers.dev") ||
206
+ hostname.endsWith(".pages.dev")
207
+ ) {
208
+ return fallback;
209
+ }
210
+
211
+ // Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
212
+ // to prevent host header injection into cache keys
213
+ if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
214
+ return fallback;
215
+ }
216
+
217
+ // Use actual hostname for production
218
+ return `https://${hostname}/`;
219
+ } catch {
220
+ return fallback;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get the cache instance - uses caches.default unless namespace is specified.
226
+ * @internal
227
+ */
228
+ private getCache(): Cache | Promise<Cache> {
229
+ if (this.namespace) {
230
+ return caches.open(this.namespace);
231
+ }
232
+ return caches.default;
233
+ }
234
+
235
+ /**
236
+ * Get cached entry data by key.
237
+ *
238
+ * Handles SWR atomically:
239
+ * - If stale and not already revalidating, marks as REVALIDATING and returns shouldRevalidate: true
240
+ * - If already REVALIDATING (and recent), returns shouldRevalidate: false
241
+ * - If fresh, returns shouldRevalidate: false
242
+ *
243
+ * The atomic mark prevents thundering herd - only first request triggers revalidation.
244
+ */
245
+ async get(key: string): Promise<CacheGetResult | null> {
246
+ try {
247
+ const cache = await this.getCache();
248
+ const request = this.keyToRequest(key);
249
+ const response = await cache.match(request);
250
+
251
+ if (!response) {
252
+ return null;
253
+ }
254
+
255
+ // Read status headers
256
+ const status = response.headers.get(CACHE_STATUS_HEADER);
257
+ const age = Number(response.headers.get("age") ?? "0");
258
+ const staleAt = Number(
259
+ response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
260
+ );
261
+
262
+ const isStale = staleAt > 0 && Date.now() > staleAt;
263
+ const isRevalidating =
264
+ status === "REVALIDATING" && age < MAX_REVALIDATION_INTERVAL;
265
+
266
+ // Case 1: Fresh or already being revalidated - just return data
267
+ if (!isStale || isRevalidating) {
268
+ const data = (await response.json()) as CachedEntryData;
269
+ return { data, shouldRevalidate: false };
270
+ }
271
+
272
+ // Case 2: Stale and needs revalidation - atomically mark REVALIDATING
273
+ const [b1, b2] = response.body!.tee();
274
+
275
+ const headers = new Headers(response.headers);
276
+ headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
277
+
278
+ // Blocking write - must complete before returning to prevent race
279
+ await cache.put(
280
+ request,
281
+ new Response(b1, { status: response.status, headers }),
282
+ );
283
+
284
+ const data = (await new Response(b2).json()) as CachedEntryData;
285
+ return { data, shouldRevalidate: true };
286
+ } catch (error) {
287
+ console.error("[CFCacheStore] get failed:", error);
288
+ return null;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Store entry data with TTL and optional SWR window.
294
+ * Uses waitUntil for non-blocking write when available.
295
+ */
296
+ async set(
297
+ key: string,
298
+ data: CachedEntryData,
299
+ ttl: number,
300
+ swr?: number,
301
+ ): Promise<void> {
302
+ try {
303
+ const cache = await this.getCache();
304
+ const request = this.keyToRequest(key);
305
+
306
+ // Extended TTL covers SWR window
307
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
308
+ const totalTtl = ttl + swrWindow;
309
+ const staleAt = Date.now() + ttl * 1000;
310
+
311
+ const response = new Response(JSON.stringify(data), {
312
+ headers: {
313
+ "Content-Type": "application/json",
314
+ "Cache-Control": `public, max-age=${totalTtl}`,
315
+ [CACHE_STALE_AT_HEADER]: String(staleAt),
316
+ [CACHE_STATUS_HEADER]: "HIT",
317
+ },
318
+ });
319
+
320
+ const putPromise = cache.put(request, response);
321
+
322
+ if (this.waitUntil) {
323
+ // Non-blocking write
324
+ this.waitUntil(async () => {
325
+ await putPromise;
326
+ });
327
+ } else {
328
+ // Blocking fallback
329
+ await putPromise;
330
+ }
331
+ } catch (error) {
332
+ console.error("[CFCacheStore] set failed:", error);
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Delete a cached entry
338
+ */
339
+ async delete(key: string): Promise<boolean> {
340
+ try {
341
+ const cache = await this.getCache();
342
+ return await cache.delete(this.keyToRequest(key));
343
+ } catch (error) {
344
+ console.error("[CFCacheStore] delete failed:", error);
345
+ return false;
346
+ }
347
+ }
348
+
349
+ // ============================================================================
350
+ // Document Cache Methods
351
+ // ============================================================================
352
+
353
+ /**
354
+ * Get a cached Response by key (for document-level caching).
355
+ * Returns the response and whether it should be revalidated (SWR).
356
+ */
357
+ async getResponse(
358
+ key: string,
359
+ ): Promise<{ response: Response; shouldRevalidate: boolean } | null> {
360
+ try {
361
+ const cache = await this.getCache();
362
+ const request = this.keyToRequest(`doc:${key}`);
363
+ const response = await cache.match(request);
364
+
365
+ if (!response || response.status !== 200) {
366
+ return null;
367
+ }
368
+
369
+ // Check staleness
370
+ const staleAt = Number(response.headers.get(CACHE_STALE_AT_HEADER) || 0);
371
+ const isStale = staleAt > 0 && Date.now() > staleAt;
372
+
373
+ return {
374
+ response,
375
+ shouldRevalidate: isStale,
376
+ };
377
+ } catch (error) {
378
+ console.error("[CFCacheStore] getResponse failed:", error);
379
+ return null;
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Store a Response with TTL and optional SWR window (for document-level caching).
385
+ */
386
+ async putResponse(
387
+ key: string,
388
+ response: Response,
389
+ ttl: number,
390
+ swr?: number,
391
+ ): Promise<void> {
392
+ try {
393
+ const cache = await this.getCache();
394
+ const request = this.keyToRequest(`doc:${key}`);
395
+
396
+ // Extended TTL covers SWR window
397
+ const swrWindow = resolveSwrWindow(swr, this.defaults);
398
+ const totalTtl = ttl + swrWindow;
399
+ const staleAt = Date.now() + ttl * 1000;
400
+
401
+ // Clone and add cache headers
402
+ const headers = new Headers(response.headers);
403
+ headers.set("Cache-Control", `public, max-age=${totalTtl}`);
404
+ headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
405
+
406
+ const toCache = new Response(response.body, {
407
+ status: response.status,
408
+ statusText: response.statusText,
409
+ headers,
410
+ });
411
+
412
+ const putPromise = cache.put(request, toCache);
413
+
414
+ if (this.waitUntil) {
415
+ // Non-blocking write
416
+ this.waitUntil(async () => {
417
+ await putPromise;
418
+ });
419
+ } else {
420
+ // Blocking fallback
421
+ await putPromise;
422
+ }
423
+ } catch (error) {
424
+ console.error("[CFCacheStore] putResponse failed:", error);
425
+ }
426
+ }
427
+
428
+ // ============================================================================
429
+ // Function Cache Methods (for "use cache" directive)
430
+ // ============================================================================
431
+
432
+ /**
433
+ * Get a cached function result by key.
434
+ * Follows the same SWR pattern as get() for segment caching.
435
+ */
436
+ async getItem(key: string): Promise<CacheItemResult | null> {
437
+ try {
438
+ const cache = await this.getCache();
439
+ const request = this.keyToRequest(`fn:${key}`);
440
+ const response = await cache.match(request);
441
+
442
+ if (!response) return null;
443
+
444
+ const staleAt = Number(
445
+ response.headers.get(CACHE_STALE_AT_HEADER) ?? "0",
446
+ );
447
+ const status = response.headers.get(CACHE_STATUS_HEADER);
448
+ const age = Number(response.headers.get("age") ?? "0");
449
+
450
+ const isStale = staleAt > 0 && Date.now() > staleAt;
451
+ const isRevalidating =
452
+ status === "REVALIDATING" && age < MAX_REVALIDATION_INTERVAL;
453
+
454
+ const data = (await response.json()) as {
455
+ value: string;
456
+ handles?: Record<string, Record<string, unknown[]>>;
457
+ };
458
+
459
+ if (!isStale || isRevalidating) {
460
+ return {
461
+ value: data.value,
462
+ handles: data.handles,
463
+ shouldRevalidate: false,
464
+ };
465
+ }
466
+
467
+ // Stale and needs revalidation — mark REVALIDATING atomically
468
+ const headers = new Headers(response.headers);
469
+ headers.set(CACHE_STATUS_HEADER, "REVALIDATING");
470
+ await cache.put(
471
+ request,
472
+ new Response(JSON.stringify(data), { status: 200, headers }),
473
+ );
474
+
475
+ return {
476
+ value: data.value,
477
+ handles: data.handles,
478
+ shouldRevalidate: true,
479
+ };
480
+ } catch (error) {
481
+ console.error("[CFCacheStore] getItem failed:", error);
482
+ return null;
483
+ }
484
+ }
485
+
486
+ /**
487
+ * Store a function result with TTL and optional SWR window.
488
+ */
489
+ async setItem(
490
+ key: string,
491
+ value: string,
492
+ options?: CacheItemOptions,
493
+ ): Promise<void> {
494
+ try {
495
+ const cache = await this.getCache();
496
+ const request = this.keyToRequest(`fn:${key}`);
497
+
498
+ const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
499
+ const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
500
+ const totalTtl = ttl + swrWindow;
501
+ const staleAt = Date.now() + ttl * 1000;
502
+
503
+ const body = JSON.stringify({ value, handles: options?.handles });
504
+ const response = new Response(body, {
505
+ headers: {
506
+ "Content-Type": "application/json",
507
+ "Cache-Control": `public, max-age=${totalTtl}`,
508
+ [CACHE_STALE_AT_HEADER]: String(staleAt),
509
+ [CACHE_STATUS_HEADER]: "HIT",
510
+ },
511
+ });
512
+
513
+ const putPromise = cache.put(request, response);
514
+
515
+ if (this.waitUntil) {
516
+ this.waitUntil(async () => {
517
+ await putPromise;
518
+ });
519
+ } else {
520
+ await putPromise;
521
+ }
522
+ } catch (error) {
523
+ console.error("[CFCacheStore] setItem failed:", error);
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Convert string key to Request object for CF Cache API.
529
+ * Includes version in URL if specified (for cache invalidation on code changes).
530
+ * @internal
531
+ */
532
+ private keyToRequest(key: string): Request {
533
+ const encodedKey = encodeURIComponent(key);
534
+ // Include version in URL path to invalidate cache when version changes
535
+ const versionPath = this.version ? `v/${this.version}/` : "";
536
+ return new Request(`${this.baseUrl}${versionPath}${encodedKey}`, {
537
+ method: "GET",
538
+ });
539
+ }
540
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Cloudflare Cache Store Exports
3
+ *
4
+ * Main export:
5
+ * - CFCacheStore - Production cache store using Cloudflare's Cache API
6
+ *
7
+ * Header constants (for inspection/debugging):
8
+ * - CACHE_STALE_AT_HEADER - Header containing staleness timestamp
9
+ * - CACHE_STATUS_HEADER - Header containing HIT/REVALIDATING status
10
+ */
11
+
12
+ // Public API
13
+ export { CFCacheStore, type CFCacheStoreOptions } from "./cf-cache-store.js";
14
+
15
+ // Header constants for debugging and inspection
16
+ export {
17
+ CACHE_STALE_AT_HEADER,
18
+ CACHE_STATUS_HEADER,
19
+ } from "./cf-cache-store.js";
20
+
21
+ // Internal exports (re-exported for backwards compatibility, marked @internal in source)
22
+ export {
23
+ type CacheStatus,
24
+ MAX_REVALIDATION_INTERVAL,
25
+ } from "./cf-cache-store.js";