@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Document-Level Cache Middleware
3
+ *
4
+ * Caches full HTTP responses at the edge based on Cache-Control headers.
5
+ * Routes opt-in to caching by setting s-maxage or stale-while-revalidate headers.
6
+ *
7
+ * Flow:
8
+ * 1. Check cache for existing response
9
+ * 2. If fresh hit → return cached response
10
+ * 3. If stale hit (within SWR window) → return cached, revalidate in background
11
+ * 4. If miss → run handler, cache if response has cache headers
12
+ */
13
+
14
+ import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
15
+ import { getRequestContext } from "../server/request-context.js";
16
+
17
+ // ============================================================================
18
+ // Constants
19
+ // ============================================================================
20
+
21
+ /** Header indicating cache status for debugging */
22
+ const CACHE_STATUS_HEADER = "x-document-cache-status";
23
+
24
+ /**
25
+ * Simple hash function for segment IDs.
26
+ * Creates a short, deterministic hash to differentiate cache keys
27
+ * based on which segments the client already has.
28
+ */
29
+ function hashSegmentIds(segmentIds: string): string {
30
+ if (!segmentIds) return "";
31
+
32
+ let hash = 0;
33
+ for (let i = 0; i < segmentIds.length; i++) {
34
+ const char = segmentIds.charCodeAt(i);
35
+ hash = ((hash << 5) - hash + char) | 0;
36
+ }
37
+ // Convert to base36 for shorter string, take absolute value
38
+ return Math.abs(hash).toString(36);
39
+ }
40
+
41
+ // ============================================================================
42
+ // Cache Control Parsing
43
+ // ============================================================================
44
+
45
+ interface CacheDirectives {
46
+ sMaxAge?: number;
47
+ staleWhileRevalidate?: number;
48
+ }
49
+
50
+ /**
51
+ * Parse Cache-Control header for s-maxage and stale-while-revalidate directives
52
+ */
53
+ function parseCacheControl(header: string | null): CacheDirectives | null {
54
+ if (!header) return null;
55
+
56
+ const directives: CacheDirectives = {};
57
+
58
+ // Parse s-maxage
59
+ const sMaxAgeMatch = header.match(/s-maxage\s*=\s*(\d+)/i);
60
+ if (sMaxAgeMatch) {
61
+ directives.sMaxAge = parseInt(sMaxAgeMatch[1], 10);
62
+ }
63
+
64
+ // Parse stale-while-revalidate
65
+ const swrMatch = header.match(/stale-while-revalidate\s*=\s*(\d+)/i);
66
+ if (swrMatch) {
67
+ directives.staleWhileRevalidate = parseInt(swrMatch[1], 10);
68
+ }
69
+
70
+ // Only return if we have at least s-maxage (required for document caching)
71
+ if (directives.sMaxAge !== undefined) {
72
+ return directives;
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * Check if response should be cached based on Cache-Control headers
80
+ */
81
+ function shouldCacheResponse(response: Response): CacheDirectives | null {
82
+ // Only cache successful responses
83
+ if (response.status !== 200) {
84
+ return null;
85
+ }
86
+
87
+ const cacheControl = response.headers.get("Cache-Control");
88
+ return parseCacheControl(cacheControl);
89
+ }
90
+
91
+ // ============================================================================
92
+ // Response Helpers
93
+ // ============================================================================
94
+
95
+ /**
96
+ * Add cache status header to response for debugging
97
+ */
98
+ function addCacheStatusHeader(
99
+ response: Response,
100
+ status: "HIT" | "STALE" | "MISS",
101
+ ): Response {
102
+ const headers = new Headers(response.headers);
103
+ headers.set(CACHE_STATUS_HEADER, status);
104
+
105
+ return new Response(response.body, {
106
+ status: response.status,
107
+ statusText: response.statusText,
108
+ headers,
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Run onResponse callbacks registered on the request context
114
+ */
115
+ function runOnResponseCallbacks(
116
+ response: Response,
117
+ callbacks: Array<(response: Response) => Response>,
118
+ ): Response {
119
+ let result = response;
120
+ for (const callback of callbacks) {
121
+ result = callback(result);
122
+ }
123
+ return result;
124
+ }
125
+
126
+ // ============================================================================
127
+ // Document Cache Middleware
128
+ // ============================================================================
129
+
130
+ export interface DocumentCacheOptions<TEnv = any> {
131
+ /**
132
+ * Skip caching for specific paths (e.g., API routes)
133
+ */
134
+ skipPaths?: string[];
135
+
136
+ /**
137
+ * Custom cache key generator
138
+ */
139
+ keyGenerator?: (url: URL) => string;
140
+
141
+ /**
142
+ * Callback to determine if caching should be enabled for this request.
143
+ * Receives the middleware context and returns true to enable caching.
144
+ * If not provided, caching is enabled by default.
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * createDocumentCacheMiddleware({
149
+ * isEnabled: (ctx) => !ctx.request.headers.has('x-skip-cache'),
150
+ * })
151
+ * ```
152
+ */
153
+ isEnabled?: (ctx: MiddlewareContext<TEnv>) => boolean | Promise<boolean>;
154
+
155
+ /**
156
+ * Enable debug logging for cache operations.
157
+ * Logs HIT, MISS, STALE, and REVALIDATED events.
158
+ * Defaults to false.
159
+ */
160
+ debug?: boolean;
161
+ }
162
+
163
+ /**
164
+ * Create document cache middleware
165
+ *
166
+ * Add this middleware to your router to enable document-level caching.
167
+ * It uses the cache store's getResponse/putResponse methods for caching.
168
+ * Routes opt-in by setting Cache-Control headers with s-maxage.
169
+ *
170
+ * @example
171
+ * ```typescript
172
+ * // Add middleware to router
173
+ * const router = createRouter<AppEnv>()
174
+ * .use(createDocumentCacheMiddleware({
175
+ * isEnabled: (ctx) => ctx.url.pathname !== '/admin',
176
+ * }))
177
+ * .route("home", (ctx) => {
178
+ * ctx.headers.set("Cache-Control", "s-maxage=60, stale-while-revalidate=300");
179
+ * return <HomePage />;
180
+ * });
181
+ * ```
182
+ */
183
+ export function createDocumentCacheMiddleware<TEnv = any>(
184
+ options: DocumentCacheOptions<TEnv> = {},
185
+ ): MiddlewareFn<TEnv> {
186
+ const { skipPaths = [], keyGenerator, isEnabled, debug = false } = options;
187
+
188
+ const log = debug
189
+ ? (message: string) => console.log(message)
190
+ : () => {};
191
+
192
+ return async function documentCacheMiddleware(
193
+ ctx: MiddlewareContext<TEnv>,
194
+ next: () => Promise<Response>,
195
+ ): Promise<Response> {
196
+ const url = ctx.url;
197
+
198
+ // Skip RSC action requests (mutations shouldn't be cached)
199
+ if (url.searchParams.has("_rsc_action")) {
200
+ return next();
201
+ }
202
+
203
+ // Skip loader requests (have their own caching)
204
+ if (url.searchParams.has("_rsc_loader")) {
205
+ return next();
206
+ }
207
+
208
+ // Skip configured paths
209
+ if (skipPaths.some((path) => url.pathname.startsWith(path))) {
210
+ return next();
211
+ }
212
+
213
+ // Check if caching is enabled for this request
214
+ if (isEnabled) {
215
+ const enabled = await isEnabled(ctx);
216
+ if (!enabled) {
217
+ return next();
218
+ }
219
+ }
220
+
221
+ // Get request context and cache store
222
+ const requestCtx = getRequestContext();
223
+ const store = requestCtx?._cacheStore;
224
+
225
+ // Skip if no cache store or store doesn't support response caching
226
+ if (!store?.getResponse || !store?.putResponse) {
227
+ return next();
228
+ }
229
+
230
+ // Determine request type for cache key differentiation
231
+ const isPartial = url.searchParams.has("_rsc_partial");
232
+ const typeLabel = isPartial ? "RSC" : "HTML";
233
+
234
+ // Generate cache key
235
+ // For partial requests, include hash of client segments to prevent serving
236
+ // wrong cached response when navigating from different pages with different layouts
237
+ const clientSegments = url.searchParams.get("_rsc_segments") || "";
238
+ const segmentHash = isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
239
+ const typeSuffix = isPartial ? ":rsc" : ":html";
240
+ const cacheKey = keyGenerator
241
+ ? keyGenerator(url) + segmentHash + typeSuffix
242
+ : `${url.pathname}${segmentHash}${typeSuffix}`;
243
+
244
+ try {
245
+ // 1. Check cache
246
+ const cached = await store.getResponse(cacheKey);
247
+
248
+ if (cached && cached.response.status === 200) {
249
+ if (!cached.shouldRevalidate) {
250
+ // Fresh hit - return immediately
251
+ log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
252
+ let response = addCacheStatusHeader(cached.response, "HIT");
253
+ // Run onResponse callbacks even for cache hits
254
+ if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
255
+ response = runOnResponseCallbacks(
256
+ response,
257
+ requestCtx._onResponseCallbacks,
258
+ );
259
+ }
260
+ return response;
261
+ }
262
+
263
+ // Stale hit - return cached response, revalidate in background
264
+ log(`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`);
265
+
266
+ if (requestCtx) {
267
+ requestCtx.waitUntil(async () => {
268
+ try {
269
+ const fresh = await next();
270
+ const directives = shouldCacheResponse(fresh);
271
+
272
+ if (directives) {
273
+ await store.putResponse!(
274
+ cacheKey,
275
+ fresh,
276
+ directives.sMaxAge!,
277
+ directives.staleWhileRevalidate,
278
+ );
279
+ log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
280
+ }
281
+ } catch (error) {
282
+ console.error(`[DocumentCache] Revalidation failed:`, error);
283
+ }
284
+ });
285
+ }
286
+
287
+ let response = addCacheStatusHeader(cached.response, "STALE");
288
+ // Run onResponse callbacks even for stale cache hits
289
+ if (requestCtx && requestCtx._onResponseCallbacks.length > 0) {
290
+ response = runOnResponseCallbacks(
291
+ response,
292
+ requestCtx._onResponseCallbacks,
293
+ );
294
+ }
295
+ return response;
296
+ }
297
+
298
+ // 2. Cache miss - run handler
299
+ const originalResponse = await next();
300
+
301
+ // 3. Cache if response has appropriate headers
302
+ const directives = shouldCacheResponse(originalResponse);
303
+
304
+ if (directives) {
305
+ log(`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`);
306
+
307
+ // Tee the body so we can return one stream and cache the other
308
+ const [returnStream, cacheStream] = originalResponse.body!.tee();
309
+
310
+ // Clone response for caching (non-blocking)
311
+ if (requestCtx) {
312
+ requestCtx.waitUntil(async () => {
313
+ try {
314
+ await store.putResponse!(
315
+ cacheKey,
316
+ new Response(cacheStream, originalResponse),
317
+ directives.sMaxAge!,
318
+ directives.staleWhileRevalidate,
319
+ );
320
+ } catch (error) {
321
+ console.error(`[DocumentCache] Cache write failed:`, error);
322
+ }
323
+ });
324
+ }
325
+
326
+ return addCacheStatusHeader(
327
+ new Response(returnStream, originalResponse),
328
+ "MISS",
329
+ );
330
+ }
331
+
332
+ // No cache headers - pass through
333
+ return originalResponse;
334
+ } catch (error) {
335
+ console.error(`[DocumentCache] Error:`, error);
336
+ // On any cache error, fall through to handler
337
+ return next();
338
+ }
339
+ };
340
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Cache Store
3
+ *
4
+ * Server-side caching for RSC segments and loader data.
5
+ *
6
+ * Main exports for users:
7
+ * - SegmentCacheStore - Interface for implementing custom cache stores
8
+ * - MemorySegmentCacheStore - In-memory cache for development/testing
9
+ * - CFCacheStore - Cloudflare edge cache store for production
10
+ * - CacheScope / createCacheScope - Request-scoped cache provider
11
+ */
12
+
13
+ // Generic cache store types (reserved for future extensibility)
14
+ // These types support caching arbitrary values like Response, Stream, etc.
15
+ // Currently unused - segment caching uses SegmentCacheStore directly.
16
+ export type {
17
+ CacheStore,
18
+ CacheEntry,
19
+ CacheValue,
20
+ CacheValueType,
21
+ CachePutOptions,
22
+ CacheMetadata,
23
+ } from "./types.js";
24
+
25
+ // Generic memory cache (reserved for future extensibility)
26
+ export { MemoryCacheStore } from "./memory-store.js";
27
+
28
+ // Segment cache store types and implementations
29
+ export type {
30
+ SegmentCacheStore,
31
+ SegmentCacheProvider,
32
+ CachedEntryData,
33
+ CachedEntryResult,
34
+ CacheGetResult,
35
+ SerializedSegmentData,
36
+ SegmentHandleData,
37
+ CacheConfig,
38
+ CacheConfigOrFactory,
39
+ } from "./types.js";
40
+
41
+ export { MemorySegmentCacheStore } from "./memory-segment-store.js";
42
+
43
+ // Cloudflare cache store
44
+ export {
45
+ CFCacheStore,
46
+ type CFCacheStoreOptions,
47
+ CACHE_STALE_AT_HEADER,
48
+ CACHE_STATUS_HEADER,
49
+ } from "./cf/index.js";
50
+
51
+ // Cache scope
52
+ export { CacheScope, createCacheScope } from "./cache-scope.js";
53
+
54
+ // Document-level cache middleware
55
+ export {
56
+ createDocumentCacheMiddleware,
57
+ type DocumentCacheOptions,
58
+ } from "./document-cache.js";
@@ -0,0 +1,150 @@
1
+ /**
2
+ * In-Memory Segment Cache Store
3
+ *
4
+ * Simple in-memory implementation of SegmentCacheStore.
5
+ * Uses globalThis to survive HMR in development.
6
+ */
7
+
8
+ import type { SegmentCacheStore, CachedEntryData, CacheDefaults, CacheGetResult } from "./types.js";
9
+ import type { RequestContext } from "../server/request-context.js";
10
+
11
+ const CACHE_GLOBAL_KEY = "__rsc_router_segment_cache_store__";
12
+
13
+ /**
14
+ * Options for MemorySegmentCacheStore
15
+ */
16
+ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
17
+ /**
18
+ * Default cache options for cache() boundaries.
19
+ * When cache() is called without explicit ttl/swr,
20
+ * these defaults are used.
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const store = new MemorySegmentCacheStore({
25
+ * defaults: { ttl: 60, swr: 300 }
26
+ * });
27
+ * ```
28
+ */
29
+ defaults?: CacheDefaults;
30
+
31
+ /**
32
+ * Custom key generator applied to all cache operations.
33
+ * Receives the full RequestContext and the default-generated key.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * keyGenerator: (ctx, defaultKey) => {
38
+ * const locale = ctx.cookie('locale') || 'en';
39
+ * return `${locale}:${defaultKey}`;
40
+ * }
41
+ * ```
42
+ */
43
+ keyGenerator?: (
44
+ ctx: RequestContext<TEnv>,
45
+ defaultKey: string
46
+ ) => string | Promise<string>;
47
+ }
48
+
49
+ /**
50
+ * In-memory segment cache store.
51
+ *
52
+ * Suitable for development and single-instance deployments.
53
+ * For production with multiple instances, use a distributed store
54
+ * like Cloudflare KV or Redis.
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * // Basic usage
59
+ * const store = new MemorySegmentCacheStore();
60
+ *
61
+ * // With defaults for cache() boundaries
62
+ * const store = new MemorySegmentCacheStore({
63
+ * defaults: { ttl: 60 }
64
+ * });
65
+ *
66
+ * createRSCHandler({
67
+ * router,
68
+ * cache: { store }
69
+ * })
70
+ * ```
71
+ */
72
+ export class MemorySegmentCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
73
+ private cache: Map<string, CachedEntryData>;
74
+ readonly defaults?: CacheDefaults;
75
+ readonly keyGenerator?: (
76
+ ctx: RequestContext<TEnv>,
77
+ defaultKey: string
78
+ ) => string | Promise<string>;
79
+
80
+ constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
81
+ // Use globalThis to survive HMR in development
82
+ this.cache =
83
+ (globalThis as any)[CACHE_GLOBAL_KEY] ??
84
+ ((globalThis as any)[CACHE_GLOBAL_KEY] = new Map<string, CachedEntryData>());
85
+ this.defaults = options?.defaults;
86
+ this.keyGenerator = options?.keyGenerator;
87
+ }
88
+
89
+ async get(key: string): Promise<CacheGetResult | null> {
90
+ const cached = this.cache.get(key);
91
+
92
+ if (!cached) {
93
+ return null;
94
+ }
95
+
96
+ // Check expiration
97
+ if (Date.now() > cached.expiresAt) {
98
+ this.cache.delete(key);
99
+ return null;
100
+ }
101
+
102
+ // Memory store doesn't support SWR - never triggers revalidation
103
+ return { data: cached, shouldRevalidate: false };
104
+ }
105
+
106
+ async set(key: string, data: CachedEntryData, ttl: number, _swr?: number): Promise<void> {
107
+ // Note: Memory store doesn't implement SWR - entries just expire at TTL
108
+ // For SWR support, use CFCacheStore or similar distributed cache
109
+ const entry: CachedEntryData = {
110
+ ...data,
111
+ expiresAt: Date.now() + ttl * 1000,
112
+ };
113
+ this.cache.set(key, entry);
114
+ }
115
+
116
+ async delete(key: string): Promise<boolean> {
117
+ return this.cache.delete(key);
118
+ }
119
+
120
+ async clear(): Promise<void> {
121
+ this.cache.clear();
122
+ }
123
+
124
+ /**
125
+ * Get cache statistics for debugging purposes.
126
+ * @internal
127
+ */
128
+ getStats(): { size: number; keys: string[] } {
129
+ return {
130
+ size: this.cache.size,
131
+ keys: Array.from(this.cache.keys()),
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Reset the global cache state.
137
+ * Useful for test isolation - call this in beforeEach to ensure
138
+ * tests don't share cache state via globalThis.
139
+ *
140
+ * @example
141
+ * ```typescript
142
+ * beforeEach(() => {
143
+ * MemorySegmentCacheStore.resetGlobalCache();
144
+ * });
145
+ * ```
146
+ */
147
+ static resetGlobalCache(): void {
148
+ delete (globalThis as any)[CACHE_GLOBAL_KEY];
149
+ }
150
+ }