@rangojs/router 0.0.0-experimental.10

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 (172) hide show
  1. package/CLAUDE.md +43 -0
  2. package/README.md +19 -0
  3. package/dist/bin/rango.js +227 -0
  4. package/dist/vite/index.js +3039 -0
  5. package/package.json +171 -0
  6. package/skills/caching/SKILL.md +191 -0
  7. package/skills/debug-manifest/SKILL.md +108 -0
  8. package/skills/document-cache/SKILL.md +180 -0
  9. package/skills/fonts/SKILL.md +165 -0
  10. package/skills/hooks/SKILL.md +442 -0
  11. package/skills/intercept/SKILL.md +190 -0
  12. package/skills/layout/SKILL.md +213 -0
  13. package/skills/links/SKILL.md +180 -0
  14. package/skills/loader/SKILL.md +246 -0
  15. package/skills/middleware/SKILL.md +202 -0
  16. package/skills/mime-routes/SKILL.md +124 -0
  17. package/skills/parallel/SKILL.md +228 -0
  18. package/skills/prerender/SKILL.md +283 -0
  19. package/skills/rango/SKILL.md +54 -0
  20. package/skills/response-routes/SKILL.md +358 -0
  21. package/skills/route/SKILL.md +173 -0
  22. package/skills/router-setup/SKILL.md +346 -0
  23. package/skills/tailwind/SKILL.md +129 -0
  24. package/skills/theme/SKILL.md +78 -0
  25. package/skills/typesafety/SKILL.md +394 -0
  26. package/src/__internal.ts +175 -0
  27. package/src/bin/rango.ts +24 -0
  28. package/src/browser/event-controller.ts +876 -0
  29. package/src/browser/index.ts +18 -0
  30. package/src/browser/link-interceptor.ts +121 -0
  31. package/src/browser/lru-cache.ts +69 -0
  32. package/src/browser/merge-segment-loaders.ts +126 -0
  33. package/src/browser/navigation-bridge.ts +913 -0
  34. package/src/browser/navigation-client.ts +165 -0
  35. package/src/browser/navigation-store.ts +823 -0
  36. package/src/browser/partial-update.ts +600 -0
  37. package/src/browser/react/Link.tsx +248 -0
  38. package/src/browser/react/NavigationProvider.tsx +346 -0
  39. package/src/browser/react/ScrollRestoration.tsx +94 -0
  40. package/src/browser/react/context.ts +53 -0
  41. package/src/browser/react/index.ts +52 -0
  42. package/src/browser/react/location-state-shared.ts +120 -0
  43. package/src/browser/react/location-state.ts +62 -0
  44. package/src/browser/react/mount-context.ts +32 -0
  45. package/src/browser/react/use-action.ts +240 -0
  46. package/src/browser/react/use-client-cache.ts +56 -0
  47. package/src/browser/react/use-handle.ts +203 -0
  48. package/src/browser/react/use-href.tsx +40 -0
  49. package/src/browser/react/use-link-status.ts +134 -0
  50. package/src/browser/react/use-mount.ts +31 -0
  51. package/src/browser/react/use-navigation.ts +140 -0
  52. package/src/browser/react/use-segments.ts +188 -0
  53. package/src/browser/request-controller.ts +164 -0
  54. package/src/browser/rsc-router.tsx +352 -0
  55. package/src/browser/scroll-restoration.ts +324 -0
  56. package/src/browser/segment-structure-assert.ts +67 -0
  57. package/src/browser/server-action-bridge.ts +762 -0
  58. package/src/browser/shallow.ts +35 -0
  59. package/src/browser/types.ts +478 -0
  60. package/src/build/generate-manifest.ts +377 -0
  61. package/src/build/generate-route-types.ts +828 -0
  62. package/src/build/index.ts +36 -0
  63. package/src/build/route-trie.ts +239 -0
  64. package/src/cache/cache-scope.ts +563 -0
  65. package/src/cache/cf/cf-cache-store.ts +428 -0
  66. package/src/cache/cf/index.ts +19 -0
  67. package/src/cache/document-cache.ts +340 -0
  68. package/src/cache/index.ts +58 -0
  69. package/src/cache/memory-segment-store.ts +150 -0
  70. package/src/cache/memory-store.ts +253 -0
  71. package/src/cache/types.ts +392 -0
  72. package/src/client.rsc.tsx +83 -0
  73. package/src/client.tsx +643 -0
  74. package/src/component-utils.ts +76 -0
  75. package/src/components/DefaultDocument.tsx +23 -0
  76. package/src/debug.ts +233 -0
  77. package/src/default-error-boundary.tsx +88 -0
  78. package/src/deps/browser.ts +8 -0
  79. package/src/deps/html-stream-client.ts +2 -0
  80. package/src/deps/html-stream-server.ts +2 -0
  81. package/src/deps/rsc.ts +10 -0
  82. package/src/deps/ssr.ts +2 -0
  83. package/src/errors.ts +295 -0
  84. package/src/handle.ts +130 -0
  85. package/src/handles/MetaTags.tsx +193 -0
  86. package/src/handles/index.ts +6 -0
  87. package/src/handles/meta.ts +247 -0
  88. package/src/host/cookie-handler.ts +159 -0
  89. package/src/host/errors.ts +97 -0
  90. package/src/host/index.ts +56 -0
  91. package/src/host/pattern-matcher.ts +214 -0
  92. package/src/host/router.ts +330 -0
  93. package/src/host/testing.ts +79 -0
  94. package/src/host/types.ts +138 -0
  95. package/src/host/utils.ts +25 -0
  96. package/src/href-client.ts +202 -0
  97. package/src/href-context.ts +33 -0
  98. package/src/index.rsc.ts +121 -0
  99. package/src/index.ts +165 -0
  100. package/src/loader.rsc.ts +207 -0
  101. package/src/loader.ts +47 -0
  102. package/src/network-error-thrower.tsx +21 -0
  103. package/src/outlet-context.ts +15 -0
  104. package/src/prerender/param-hash.ts +35 -0
  105. package/src/prerender/store.ts +40 -0
  106. package/src/prerender.ts +156 -0
  107. package/src/reverse.ts +267 -0
  108. package/src/root-error-boundary.tsx +277 -0
  109. package/src/route-content-wrapper.tsx +193 -0
  110. package/src/route-definition.ts +1431 -0
  111. package/src/route-map-builder.ts +242 -0
  112. package/src/route-types.ts +220 -0
  113. package/src/router/error-handling.ts +287 -0
  114. package/src/router/handler-context.ts +158 -0
  115. package/src/router/intercept-resolution.ts +387 -0
  116. package/src/router/loader-resolution.ts +327 -0
  117. package/src/router/manifest.ts +216 -0
  118. package/src/router/match-api.ts +621 -0
  119. package/src/router/match-context.ts +264 -0
  120. package/src/router/match-middleware/background-revalidation.ts +236 -0
  121. package/src/router/match-middleware/cache-lookup.ts +382 -0
  122. package/src/router/match-middleware/cache-store.ts +276 -0
  123. package/src/router/match-middleware/index.ts +81 -0
  124. package/src/router/match-middleware/intercept-resolution.ts +281 -0
  125. package/src/router/match-middleware/segment-resolution.ts +184 -0
  126. package/src/router/match-pipelines.ts +214 -0
  127. package/src/router/match-result.ts +213 -0
  128. package/src/router/metrics.ts +62 -0
  129. package/src/router/middleware.ts +791 -0
  130. package/src/router/pattern-matching.ts +407 -0
  131. package/src/router/revalidation.ts +190 -0
  132. package/src/router/router-context.ts +301 -0
  133. package/src/router/segment-resolution.ts +1315 -0
  134. package/src/router/trie-matching.ts +172 -0
  135. package/src/router/types.ts +163 -0
  136. package/src/router.gen.ts +6 -0
  137. package/src/router.ts +2423 -0
  138. package/src/rsc/handler.ts +1443 -0
  139. package/src/rsc/helpers.ts +64 -0
  140. package/src/rsc/index.ts +56 -0
  141. package/src/rsc/nonce.ts +18 -0
  142. package/src/rsc/types.ts +236 -0
  143. package/src/segment-system.tsx +442 -0
  144. package/src/server/context.ts +466 -0
  145. package/src/server/handle-store.ts +229 -0
  146. package/src/server/loader-registry.ts +174 -0
  147. package/src/server/request-context.ts +554 -0
  148. package/src/server/root-layout.tsx +10 -0
  149. package/src/server/tsconfig.json +14 -0
  150. package/src/server.ts +171 -0
  151. package/src/ssr/index.tsx +296 -0
  152. package/src/theme/ThemeProvider.tsx +291 -0
  153. package/src/theme/ThemeScript.tsx +61 -0
  154. package/src/theme/constants.ts +59 -0
  155. package/src/theme/index.ts +58 -0
  156. package/src/theme/theme-context.ts +70 -0
  157. package/src/theme/theme-script.ts +152 -0
  158. package/src/theme/types.ts +182 -0
  159. package/src/theme/use-theme.ts +44 -0
  160. package/src/types.ts +1757 -0
  161. package/src/urls.gen.ts +8 -0
  162. package/src/urls.ts +1282 -0
  163. package/src/use-loader.tsx +346 -0
  164. package/src/vite/expose-action-id.ts +344 -0
  165. package/src/vite/expose-handle-id.ts +209 -0
  166. package/src/vite/expose-loader-id.ts +426 -0
  167. package/src/vite/expose-location-state-id.ts +177 -0
  168. package/src/vite/expose-prerender-handler-id.ts +429 -0
  169. package/src/vite/index.ts +2068 -0
  170. package/src/vite/package-resolution.ts +125 -0
  171. package/src/vite/version.d.ts +12 -0
  172. package/src/vite/virtual-entries.ts +114 -0
@@ -0,0 +1,563 @@
1
+ /**
2
+ * CacheScope - Runtime cache scope for iterator-based caching
3
+ *
4
+ * Each cache() boundary in the route tree creates a new CacheScope.
5
+ * The scope owns: config, serialization, and storage operations.
6
+ */
7
+
8
+ /// <reference types="@vitejs/plugin-rsc/types" />
9
+
10
+ import type { PartialCacheOptions } from "../types.js";
11
+ import type { ResolvedSegment } from "../types.js";
12
+ import type {
13
+ SegmentCacheStore,
14
+ SegmentHandleData,
15
+ CachedEntryData,
16
+ SerializedSegmentData,
17
+ } from "./types.js";
18
+ import { getRequestContext } from "../server/request-context.js";
19
+ import {
20
+ renderToReadableStream,
21
+ createTemporaryReferenceSet,
22
+ } from "@vitejs/plugin-rsc/rsc";
23
+ import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
24
+
25
+ // ============================================================================
26
+ // Constants
27
+ // ============================================================================
28
+
29
+ /** Default TTL when no explicit value or store defaults are configured */
30
+ const DEFAULT_TTL_SECONDS = 60;
31
+
32
+ // ============================================================================
33
+ // Serialization Utilities (internal)
34
+ // ============================================================================
35
+
36
+ /**
37
+ * Generate cache key base from pathname and params.
38
+ * Params are sorted alphabetically for consistent key generation.
39
+ * @internal
40
+ */
41
+ function getCacheKeyBase(
42
+ pathname: string,
43
+ params?: Record<string, string>
44
+ ): string {
45
+ const paramStr = params
46
+ ? Object.entries(params)
47
+ .sort(([a], [b]) => a.localeCompare(b))
48
+ .map(([k, v]) => `${k}=${v}`)
49
+ .join("&")
50
+ : "";
51
+
52
+ return paramStr ? `${pathname}:${paramStr}` : pathname;
53
+ }
54
+
55
+ /**
56
+ * Generate default cache key for a route request.
57
+ * Single cache entry per route - uses pathname as the key.
58
+ * Includes request type prefix since they produce different segment sets:
59
+ * - doc: document requests (full page load)
60
+ * - partial: navigation requests (client-side navigation)
61
+ * - intercept: intercept navigation (modal/overlay routes)
62
+ * @internal
63
+ */
64
+ function getDefaultRouteCacheKey(
65
+ pathname: string,
66
+ params?: Record<string, string>,
67
+ isIntercept?: boolean
68
+ ): string {
69
+ const ctx = getRequestContext();
70
+ const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
71
+
72
+ // Intercept navigations get their own cache namespace
73
+ const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
74
+
75
+ return `${prefix}:${getCacheKeyBase(pathname, params)}`;
76
+ }
77
+
78
+ /**
79
+ * Convert a ReadableStream to a string.
80
+ * @internal
81
+ */
82
+ async function streamToString(
83
+ stream: ReadableStream<Uint8Array>
84
+ ): Promise<string> {
85
+ const reader = stream.getReader();
86
+ const decoder = new TextDecoder();
87
+ let result = "";
88
+
89
+ while (true) {
90
+ const { done, value } = await reader.read();
91
+ if (done) break;
92
+ result += decoder.decode(value, { stream: true });
93
+ }
94
+
95
+ result += decoder.decode(); // flush
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Convert a string to a ReadableStream.
101
+ * @internal
102
+ */
103
+ function stringToStream(str: string): ReadableStream<Uint8Array> {
104
+ const encoder = new TextEncoder();
105
+ const uint8 = encoder.encode(str);
106
+
107
+ return new ReadableStream({
108
+ start(controller) {
109
+ controller.enqueue(uint8);
110
+ controller.close();
111
+ },
112
+ });
113
+ }
114
+
115
+ /**
116
+ * RSC-serialize a value using React Server Components stream.
117
+ * Used for serializing loaderData, layout, loading components etc.
118
+ * @internal
119
+ */
120
+ async function rscSerialize(value: unknown): Promise<string | undefined> {
121
+ if (value === undefined || value === null) return undefined;
122
+
123
+ const temporaryReferences = createTemporaryReferenceSet();
124
+ const stream = renderToReadableStream(value, { temporaryReferences });
125
+ return streamToString(stream);
126
+ }
127
+
128
+ /**
129
+ * RSC-deserialize a value from a stored string.
130
+ * @internal
131
+ */
132
+ async function rscDeserialize<T>(
133
+ encoded: string | undefined
134
+ ): Promise<T | undefined> {
135
+ if (!encoded) return undefined;
136
+
137
+ const temporaryReferences = createTemporaryReferenceSet();
138
+ const stream = stringToStream(encoded);
139
+ return createFromReadableStream<T>(stream, { temporaryReferences });
140
+ }
141
+
142
+ /**
143
+ * Serialize segments for storage.
144
+ * Each segment's component, layout, loading, and loaderData are RSC-serialized.
145
+ * Metadata is preserved as-is.
146
+ */
147
+ export async function serializeSegments(
148
+ segments: ResolvedSegment[]
149
+ ): Promise<SerializedSegmentData[]> {
150
+ const serialized: SerializedSegmentData[] = [];
151
+
152
+ for (const segment of segments) {
153
+ const temporaryReferences = createTemporaryReferenceSet();
154
+
155
+ // Await component if it's a Promise (intercepts with loading keep component as Promise)
156
+ const componentResolved =
157
+ segment.component instanceof Promise
158
+ ? await segment.component
159
+ : segment.component;
160
+
161
+ // Serialize the component to RSC stream
162
+ const stream = renderToReadableStream(componentResolved, {
163
+ temporaryReferences,
164
+ });
165
+
166
+ // Convert stream to string
167
+ const encoded = await streamToString(stream);
168
+
169
+ // RSC-serialize layout if present (ReactNode)
170
+ const encodedLayout = segment.layout
171
+ ? await rscSerialize(segment.layout)
172
+ : undefined;
173
+
174
+ // RSC-serialize loading if present (ReactNode) - preserves tree structure
175
+ // Use "null" string to distinguish explicit null from undefined
176
+ const encodedLoading =
177
+ segment.loading !== undefined
178
+ ? segment.loading === null
179
+ ? "null"
180
+ : await rscSerialize(segment.loading)
181
+ : undefined;
182
+
183
+ // Await and RSC-serialize loaderData if present
184
+ const loaderDataResolved =
185
+ segment.loaderData instanceof Promise
186
+ ? await segment.loaderData
187
+ : segment.loaderData;
188
+ const encodedLoaderData = await rscSerialize(loaderDataResolved);
189
+
190
+ // Await and RSC-serialize loaderDataPromise if present
191
+ const loaderDataPromiseResolved =
192
+ segment.loaderDataPromise instanceof Promise
193
+ ? await segment.loaderDataPromise
194
+ : segment.loaderDataPromise;
195
+ const encodedLoaderDataPromise = await rscSerialize(
196
+ loaderDataPromiseResolved
197
+ );
198
+
199
+ serialized.push({
200
+ encoded,
201
+ encodedLayout,
202
+ encodedLoading,
203
+ encodedLoaderData,
204
+ encodedLoaderDataPromise,
205
+ metadata: {
206
+ id: segment.id,
207
+ type: segment.type,
208
+ namespace: segment.namespace,
209
+ index: segment.index,
210
+ params: segment.params,
211
+ slot: segment.slot,
212
+ belongsToRoute: segment.belongsToRoute,
213
+ layoutName: segment.layoutName,
214
+ parallelName: segment.parallelName,
215
+ loaderId: segment.loaderId,
216
+ loaderIds: segment.loaderIds,
217
+ },
218
+ });
219
+ }
220
+
221
+ return serialized;
222
+ }
223
+
224
+ /**
225
+ * Deserialize segments from storage.
226
+ * Reconstructs ResolvedSegment objects from RSC-serialized data.
227
+ */
228
+ export async function deserializeSegments(
229
+ data: SerializedSegmentData[]
230
+ ): Promise<ResolvedSegment[]> {
231
+ const segments: ResolvedSegment[] = [];
232
+
233
+ for (const item of data) {
234
+ const temporaryReferences = createTemporaryReferenceSet();
235
+
236
+ // Revive the component from cached string
237
+ const stream = stringToStream(item.encoded);
238
+ const component = await createFromReadableStream(stream, {
239
+ temporaryReferences,
240
+ });
241
+
242
+ // RSC-deserialize layout, loaderData, loaderDataPromise in parallel
243
+ const [layout, loaderData, loaderDataPromise, loadingData] =
244
+ await Promise.all([
245
+ rscDeserialize(item.encodedLayout),
246
+ rscDeserialize(item.encodedLoaderData),
247
+ rscDeserialize(item.encodedLoaderDataPromise),
248
+ rscDeserialize(item.encodedLoading),
249
+ ]);
250
+
251
+ segments.push({
252
+ ...item.metadata,
253
+ component: await component,
254
+ layout,
255
+ loading: loadingData,
256
+ loaderData,
257
+ loaderDataPromise,
258
+ } as ResolvedSegment);
259
+ }
260
+
261
+ return segments;
262
+ }
263
+
264
+ // ============================================================================
265
+ // CacheScope
266
+ // ============================================================================
267
+
268
+ /**
269
+ * CacheScope represents a cache boundary in the route tree.
270
+ *
271
+ * When withCache encounters an entry with cache config, it creates
272
+ * a new CacheScope. The scope owns serialization, storage, and TTL.
273
+ *
274
+ * Store resolution priority:
275
+ * 1. Explicit store in cache() options
276
+ * 2. App-level store from handler config
277
+ *
278
+ * TTL resolution priority:
279
+ * 1. Explicit value in cache() options
280
+ * 2. Explicit store's defaults (if store specified)
281
+ * 3. App-level store's defaults
282
+ * 4. Hardcoded fallback (60 seconds)
283
+ */
284
+ export class CacheScope {
285
+ readonly config: PartialCacheOptions | false;
286
+ readonly parent: CacheScope | null;
287
+ /** Explicit store from cache() options, if specified */
288
+ private readonly explicitStore: SegmentCacheStore | undefined;
289
+
290
+ constructor(
291
+ config: PartialCacheOptions | false,
292
+ parent: CacheScope | null = null
293
+ ) {
294
+ this.config = config;
295
+ this.parent = parent;
296
+ // Extract and store explicit store reference
297
+ this.explicitStore = config !== false ? config.store : undefined;
298
+ }
299
+
300
+ /**
301
+ * Whether caching is enabled for this scope
302
+ */
303
+ get enabled(): boolean {
304
+ return this.config !== false;
305
+ }
306
+
307
+ /**
308
+ * Get effective TTL from config or store defaults
309
+ */
310
+ get ttl(): number {
311
+ if (this.config === false) return 0;
312
+
313
+ // Explicit TTL in cache() options
314
+ if (this.config.ttl !== undefined) {
315
+ return this.config.ttl;
316
+ }
317
+
318
+ // Fall back to store defaults (explicit store first, then app-level)
319
+ const store = this.getStore();
320
+ if (store?.defaults?.ttl !== undefined) {
321
+ return store.defaults.ttl;
322
+ }
323
+
324
+ // Hardcoded fallback
325
+ return DEFAULT_TTL_SECONDS;
326
+ }
327
+
328
+ /**
329
+ * Get SWR window from config or store defaults
330
+ */
331
+ get swr(): number | undefined {
332
+ if (this.config === false) return undefined;
333
+
334
+ // Explicit SWR in cache() options
335
+ if (this.config.swr !== undefined) {
336
+ return this.config.swr;
337
+ }
338
+
339
+ // Fall back to store defaults
340
+ const store = this.getStore();
341
+ return store?.defaults?.swr;
342
+ }
343
+
344
+ /**
345
+ * Get the cache store - resolution priority:
346
+ * 1. Explicit store from cache() options
347
+ * 2. App-level store from request context
348
+ */
349
+ private getStore(): SegmentCacheStore | null {
350
+ // Explicit store from cache() options takes precedence
351
+ if (this.explicitStore) {
352
+ return this.explicitStore;
353
+ }
354
+ // Fall back to app-level store from request context
355
+ const ctx = getRequestContext();
356
+ return ctx?._cacheStore ?? null;
357
+ }
358
+
359
+ /**
360
+ * Resolve the cache key using custom key functions or default generation.
361
+ *
362
+ * Resolution priority:
363
+ * 1. Route-level `key` function (full override)
364
+ * 2. Store-level `keyGenerator` (modifies default key)
365
+ * 3. Default key generation (prefix:pathname:params)
366
+ *
367
+ * @internal
368
+ */
369
+ private async resolveKey(
370
+ pathname: string,
371
+ params: Record<string, string>,
372
+ isIntercept?: boolean
373
+ ): Promise<string> {
374
+ const requestCtx = getRequestContext();
375
+ if (!requestCtx) {
376
+ // Fallback to default key if no request context
377
+ return getDefaultRouteCacheKey(pathname, params, isIntercept);
378
+ }
379
+
380
+ // Priority 1: Route-level key function (full override)
381
+ if (this.config !== false && this.config.key) {
382
+ try {
383
+ const customKey = await this.config.key(requestCtx);
384
+ return customKey;
385
+ } catch (error) {
386
+ console.error(`[CacheScope] Custom key function failed, using default:`, error);
387
+ return getDefaultRouteCacheKey(pathname, params, isIntercept);
388
+ }
389
+ }
390
+
391
+ // Generate default key
392
+ const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
393
+
394
+ // Priority 2: Store-level keyGenerator (modifies default key)
395
+ const store = this.getStore();
396
+ if (store?.keyGenerator) {
397
+ try {
398
+ const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
399
+ return modifiedKey;
400
+ } catch (error) {
401
+ console.error(`[CacheScope] Store keyGenerator failed, using default:`, error);
402
+ return defaultKey;
403
+ }
404
+ }
405
+
406
+ // Priority 3: Default key
407
+ return defaultKey;
408
+ }
409
+
410
+ /**
411
+ * Lookup cached segments for a route (single cache entry per request).
412
+ * Returns { segments, shouldRevalidate } or null if cache miss.
413
+ *
414
+ * @param pathname - URL pathname for cache key generation
415
+ * @param params - Route params for cache key generation
416
+ * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
417
+ */
418
+ async lookupRoute(
419
+ pathname: string,
420
+ params: Record<string, string>,
421
+ isIntercept?: boolean
422
+ ): Promise<{
423
+ segments: ResolvedSegment[];
424
+ shouldRevalidate: boolean;
425
+ } | null> {
426
+ if (!this.enabled) return null;
427
+
428
+ const store = this.getStore();
429
+ if (!store) return null;
430
+
431
+ // Resolve cache key (may use custom key functions)
432
+ const key = await this.resolveKey(pathname, params, isIntercept);
433
+
434
+ try {
435
+ const result = await store.get(key);
436
+
437
+ if (!result) {
438
+ console.log(`[CacheScope] MISS: ${key}`);
439
+ return null;
440
+ }
441
+
442
+ const { data: cached, shouldRevalidate } = result;
443
+
444
+ // Deserialize segments
445
+ const segments = await deserializeSegments(cached.segments);
446
+
447
+ // Replay handle data
448
+ const handleStore = getRequestContext()?._handleStore;
449
+ if (handleStore) {
450
+ for (const [segId, segHandles] of Object.entries(cached.handles)) {
451
+ if (Object.keys(segHandles).length > 0) {
452
+ handleStore.replaySegmentData(segId, segHandles);
453
+ }
454
+ }
455
+ }
456
+
457
+ const segmentTypes = segments.map((s) =>
458
+ s.type === "parallel" ? s.slot : s.type
459
+ );
460
+ console.log(
461
+ `[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`
462
+ );
463
+
464
+ return { segments, shouldRevalidate };
465
+ } catch (error) {
466
+ console.error(`[CacheScope] Failed to lookup ${key}:`, error);
467
+ return null;
468
+ }
469
+ }
470
+
471
+ /**
472
+ * Cache all segments for a route (non-blocking via waitUntil)
473
+ * Single cache entry per route request.
474
+ * Loaders are excluded - they're always fresh unless they have their own cache() config.
475
+ *
476
+ * @param pathname - URL pathname for cache key generation
477
+ * @param params - Route params for cache key generation
478
+ * @param segments - All resolved segments to cache
479
+ * @param isIntercept - Whether this is an intercept navigation (uses different cache key)
480
+ */
481
+ async cacheRoute(
482
+ pathname: string,
483
+ params: Record<string, string>,
484
+ segments: ResolvedSegment[],
485
+ isIntercept?: boolean
486
+ ): Promise<void> {
487
+ if (!this.enabled || segments.length === 0) return;
488
+
489
+ const store = this.getStore();
490
+ if (!store) return;
491
+
492
+ const requestCtx = getRequestContext();
493
+ const handleStore = requestCtx?._handleStore;
494
+
495
+ if (!handleStore || !requestCtx) return;
496
+
497
+ // Exclude loader segments - loaders are always fresh by default
498
+ // Loaders can opt-in to caching with their own cache() config
499
+ const nonLoaderSegments = segments.filter((s) => s.type !== "loader");
500
+ if (nonLoaderSegments.length === 0) return;
501
+
502
+ const ttl = this.ttl;
503
+ const swr = this.swr;
504
+
505
+ // Resolve cache key early (while request context is available)
506
+ const key = await this.resolveKey(pathname, params, isIntercept);
507
+
508
+ // Check if this is a partial request (navigation) vs document request
509
+ const isPartial = requestCtx.url.searchParams.has("_rsc_partial");
510
+
511
+ requestCtx.waitUntil(async () => {
512
+ await handleStore.settled;
513
+
514
+ // For document requests: only cache if ALL segments have components (complete render)
515
+ // For partial requests: null components are expected (client already has them)
516
+ if (!isPartial) {
517
+ const hasAllComponents = nonLoaderSegments.every(
518
+ (s) => s.component !== null
519
+ );
520
+ if (!hasAllComponents) return;
521
+ }
522
+
523
+ // Collect handle data for non-loader segments only
524
+ const handles: Record<string, SegmentHandleData> = {};
525
+ for (const seg of nonLoaderSegments) {
526
+ handles[seg.id] = handleStore.getDataForSegment(seg.id);
527
+ }
528
+
529
+ try {
530
+ // Serialize non-loader segments only
531
+ const serializedSegments = await serializeSegments(nonLoaderSegments);
532
+
533
+ const data: CachedEntryData = {
534
+ segments: serializedSegments,
535
+ handles,
536
+ expiresAt: Date.now() + ttl * 1000,
537
+ };
538
+
539
+ await store.set(key, data, ttl, swr);
540
+
541
+ const segmentTypes = nonLoaderSegments.map((s) =>
542
+ s.type === "parallel" ? s.slot : s.type
543
+ );
544
+ console.log(
545
+ `[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`
546
+ );
547
+ } catch (error) {
548
+ console.error(`[CacheScope] Failed to cache ${key}:`, error);
549
+ }
550
+ });
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Create a cache scope from entry's cache config
556
+ */
557
+ export function createCacheScope(
558
+ config: { options: PartialCacheOptions | false } | undefined,
559
+ parent: CacheScope | null = null
560
+ ): CacheScope | null {
561
+ if (!config) return parent; // No config, inherit parent
562
+ return new CacheScope(config.options, parent);
563
+ }