@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19

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 (177) hide show
  1. package/README.md +46 -8
  2. package/dist/bin/rango.js +105 -18
  3. package/dist/vite/index.js +227 -93
  4. package/package.json +15 -14
  5. package/skills/hooks/SKILL.md +1 -1
  6. package/skills/intercept/SKILL.md +79 -0
  7. package/skills/layout/SKILL.md +62 -2
  8. package/skills/loader/SKILL.md +94 -1
  9. package/skills/middleware/SKILL.md +81 -0
  10. package/skills/parallel/SKILL.md +57 -2
  11. package/skills/prerender/SKILL.md +187 -17
  12. package/skills/route/SKILL.md +42 -1
  13. package/skills/router-setup/SKILL.md +77 -0
  14. package/src/__internal.ts +1 -1
  15. package/src/bin/rango.ts +38 -19
  16. package/src/browser/action-coordinator.ts +97 -0
  17. package/src/browser/event-controller.ts +25 -27
  18. package/src/browser/history-state.ts +80 -0
  19. package/src/browser/intercept-utils.ts +1 -1
  20. package/src/browser/link-interceptor.ts +0 -3
  21. package/src/browser/merge-segment-loaders.ts +9 -2
  22. package/src/browser/navigation-bridge.ts +46 -13
  23. package/src/browser/navigation-client.ts +32 -61
  24. package/src/browser/navigation-store.ts +1 -31
  25. package/src/browser/navigation-transaction.ts +46 -207
  26. package/src/browser/partial-update.ts +102 -150
  27. package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
  28. package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
  29. package/src/browser/prefetch/policy.ts +42 -0
  30. package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
  31. package/src/browser/react/Link.tsx +28 -23
  32. package/src/browser/react/NavigationProvider.tsx +9 -1
  33. package/src/browser/react/index.ts +2 -6
  34. package/src/browser/react/location-state-shared.ts +1 -1
  35. package/src/browser/react/location-state.ts +2 -0
  36. package/src/browser/react/nonce-context.ts +23 -0
  37. package/src/browser/react/use-action.ts +9 -1
  38. package/src/browser/react/use-handle.ts +3 -25
  39. package/src/browser/react/use-params.ts +2 -4
  40. package/src/browser/react/use-pathname.ts +2 -3
  41. package/src/browser/react/use-router.ts +1 -1
  42. package/src/browser/react/use-search-params.ts +2 -1
  43. package/src/browser/react/use-segments.ts +7 -60
  44. package/src/browser/response-adapter.ts +73 -0
  45. package/src/browser/rsc-router.tsx +29 -23
  46. package/src/browser/scroll-restoration.ts +10 -7
  47. package/src/browser/server-action-bridge.ts +115 -96
  48. package/src/browser/types.ts +1 -31
  49. package/src/browser/validate-redirect-origin.ts +29 -0
  50. package/src/build/generate-manifest.ts +5 -0
  51. package/src/build/generate-route-types.ts +2 -0
  52. package/src/build/route-types/codegen.ts +13 -4
  53. package/src/build/route-types/include-resolution.ts +13 -0
  54. package/src/build/route-types/per-module-writer.ts +15 -3
  55. package/src/build/route-types/router-processing.ts +45 -3
  56. package/src/build/runtime-discovery.ts +13 -1
  57. package/src/cache/background-task.ts +34 -0
  58. package/src/cache/cache-key-utils.ts +44 -0
  59. package/src/cache/cache-policy.ts +125 -0
  60. package/src/cache/cache-runtime.ts +132 -96
  61. package/src/cache/cache-scope.ts +71 -73
  62. package/src/cache/cf/cf-cache-store.ts +9 -4
  63. package/src/cache/document-cache.ts +72 -47
  64. package/src/cache/handle-capture.ts +81 -0
  65. package/src/cache/memory-segment-store.ts +18 -7
  66. package/src/cache/profile-registry.ts +43 -8
  67. package/src/cache/read-through-swr.ts +134 -0
  68. package/src/cache/segment-codec.ts +101 -112
  69. package/src/cache/taint.ts +26 -0
  70. package/src/client.tsx +53 -30
  71. package/src/errors.ts +6 -1
  72. package/src/handle.ts +1 -1
  73. package/src/handles/MetaTags.tsx +5 -2
  74. package/src/host/cookie-handler.ts +8 -3
  75. package/src/host/router.ts +14 -1
  76. package/src/href-client.ts +3 -1
  77. package/src/index.rsc.ts +33 -1
  78. package/src/index.ts +27 -0
  79. package/src/loader.rsc.ts +12 -4
  80. package/src/loader.ts +8 -0
  81. package/src/prerender/store.ts +4 -3
  82. package/src/prerender.ts +76 -18
  83. package/src/reverse.ts +11 -7
  84. package/src/root-error-boundary.tsx +30 -26
  85. package/src/route-definition/dsl-helpers.ts +9 -6
  86. package/src/route-definition/redirect.ts +15 -3
  87. package/src/route-map-builder.ts +38 -2
  88. package/src/route-name.ts +53 -0
  89. package/src/route-types.ts +7 -0
  90. package/src/router/content-negotiation.ts +1 -1
  91. package/src/router/debug-manifest.ts +16 -3
  92. package/src/router/handler-context.ts +94 -15
  93. package/src/router/intercept-resolution.ts +6 -4
  94. package/src/router/lazy-includes.ts +4 -0
  95. package/src/router/loader-resolution.ts +1 -0
  96. package/src/router/logging.ts +100 -3
  97. package/src/router/manifest.ts +32 -3
  98. package/src/router/match-api.ts +61 -7
  99. package/src/router/match-context.ts +3 -0
  100. package/src/router/match-handlers.ts +185 -11
  101. package/src/router/match-middleware/background-revalidation.ts +65 -85
  102. package/src/router/match-middleware/cache-lookup.ts +69 -4
  103. package/src/router/match-middleware/cache-store.ts +2 -0
  104. package/src/router/match-pipelines.ts +8 -43
  105. package/src/router/middleware-types.ts +7 -0
  106. package/src/router/middleware.ts +93 -8
  107. package/src/router/pattern-matching.ts +41 -5
  108. package/src/router/prerender-match.ts +34 -6
  109. package/src/router/preview-match.ts +7 -1
  110. package/src/router/revalidation.ts +61 -2
  111. package/src/router/router-context.ts +15 -0
  112. package/src/router/router-interfaces.ts +34 -0
  113. package/src/router/router-options.ts +200 -0
  114. package/src/router/segment-resolution/fresh.ts +123 -30
  115. package/src/router/segment-resolution/helpers.ts +19 -0
  116. package/src/router/segment-resolution/loader-cache.ts +37 -146
  117. package/src/router/segment-resolution/revalidation.ts +358 -94
  118. package/src/router/segment-wrappers.ts +3 -0
  119. package/src/router/telemetry-otel.ts +299 -0
  120. package/src/router/telemetry.ts +300 -0
  121. package/src/router/timeout.ts +148 -0
  122. package/src/router/types.ts +7 -1
  123. package/src/router.ts +155 -11
  124. package/src/rsc/handler-context.ts +11 -0
  125. package/src/rsc/handler.ts +380 -88
  126. package/src/rsc/helpers.ts +25 -16
  127. package/src/rsc/loader-fetch.ts +84 -42
  128. package/src/rsc/origin-guard.ts +141 -0
  129. package/src/rsc/progressive-enhancement.ts +232 -19
  130. package/src/rsc/response-route-handler.ts +37 -26
  131. package/src/rsc/rsc-rendering.ts +12 -5
  132. package/src/rsc/runtime-warnings.ts +42 -0
  133. package/src/rsc/server-action.ts +134 -58
  134. package/src/rsc/types.ts +8 -0
  135. package/src/search-params.ts +22 -10
  136. package/src/server/context.ts +53 -5
  137. package/src/server/fetchable-loader-store.ts +11 -6
  138. package/src/server/handle-store.ts +66 -9
  139. package/src/server/loader-registry.ts +11 -46
  140. package/src/server/request-context.ts +90 -9
  141. package/src/ssr/index.tsx +63 -27
  142. package/src/static-handler.ts +7 -0
  143. package/src/theme/ThemeProvider.tsx +6 -1
  144. package/src/theme/index.ts +1 -6
  145. package/src/theme/theme-context.ts +1 -28
  146. package/src/theme/theme-script.ts +2 -1
  147. package/src/types/cache-types.ts +5 -0
  148. package/src/types/error-types.ts +3 -0
  149. package/src/types/global-namespace.ts +9 -0
  150. package/src/types/handler-context.ts +35 -13
  151. package/src/types/loader-types.ts +7 -0
  152. package/src/types/route-entry.ts +28 -0
  153. package/src/urls/include-helper.ts +49 -8
  154. package/src/urls/index.ts +1 -0
  155. package/src/urls/path-helper-types.ts +30 -12
  156. package/src/urls/path-helper.ts +17 -2
  157. package/src/urls/pattern-types.ts +21 -1
  158. package/src/urls/response-types.ts +27 -2
  159. package/src/urls/type-extraction.ts +23 -15
  160. package/src/use-loader.tsx +12 -4
  161. package/src/vite/discovery/bundle-postprocess.ts +12 -7
  162. package/src/vite/discovery/discover-routers.ts +30 -18
  163. package/src/vite/discovery/prerender-collection.ts +24 -27
  164. package/src/vite/discovery/route-types-writer.ts +7 -7
  165. package/src/vite/discovery/virtual-module-codegen.ts +5 -2
  166. package/src/vite/plugins/client-ref-hashing.ts +3 -3
  167. package/src/vite/plugins/use-cache-transform.ts +91 -3
  168. package/src/vite/rango.ts +3 -3
  169. package/src/vite/router-discovery.ts +99 -36
  170. package/src/vite/utils/prerender-utils.ts +21 -0
  171. package/src/vite/utils/shared-utils.ts +3 -1
  172. package/src/browser/request-controller.ts +0 -164
  173. package/src/href-context.ts +0 -33
  174. package/src/router.gen.ts +0 -6
  175. package/src/static-handler.gen.ts +0 -5
  176. package/src/urls.gen.ts +0 -8
  177. /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
@@ -2,6 +2,7 @@ import {
2
2
  extractParamsFromPattern,
3
3
  formatRouteEntry,
4
4
  } from "./param-extraction.js";
5
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
5
6
 
6
7
  // ---------------------------------------------------------------------------
7
8
  // Code generation
@@ -66,15 +67,23 @@ export function generateRouteTypesSource(
66
67
  routeManifest: Record<string, string>,
67
68
  searchSchemas?: Record<string, Record<string, string>>,
68
69
  ): string {
69
- const entries = Object.entries(routeManifest).sort(([a], [b]) =>
70
- a.localeCompare(b),
71
- );
70
+ const entries = Object.entries(routeManifest)
71
+ .filter(([name]) => !isAutoGeneratedRouteName(name))
72
+ .sort(([a], [b]) => a.localeCompare(b));
73
+
74
+ const filteredSearchSchemas = searchSchemas
75
+ ? Object.fromEntries(
76
+ Object.entries(searchSchemas).filter(
77
+ ([name]) => !isAutoGeneratedRouteName(name),
78
+ ),
79
+ )
80
+ : undefined;
72
81
 
73
82
  const objectBody = entries
74
83
  .map(([name, pattern]) => {
75
84
  const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `"${name}"`;
76
85
  const params = extractParamsFromPattern(pattern);
77
- const search = searchSchemas?.[name];
86
+ const search = filteredSearchSchemas?.[name];
78
87
  return formatRouteEntry(key, pattern, params, search);
79
88
  })
80
89
  .join("\n");
@@ -325,6 +325,13 @@ function buildRouteMapFromBlock(
325
325
  );
326
326
  }
327
327
 
328
+ // Includes without a name keep their child names private to the mounted
329
+ // module. They remain active at runtime via an internal scope prefix, but
330
+ // they are intentionally omitted from generated public route maps.
331
+ if (namePrefix === null) {
332
+ continue;
333
+ }
334
+
328
335
  // Apply prefixes
329
336
  for (const [name, pattern] of Object.entries(childResult.routes)) {
330
337
  const prefixedName = namePrefix ? `${namePrefix}.${name}` : name;
@@ -394,5 +401,11 @@ export function buildCombinedRouteMapWithSearch(
394
401
  searchSchemas,
395
402
  diagnosticsOut,
396
403
  );
404
+
405
+ // Remove from visited so sibling branches can include the same variable
406
+ // without false circular-include detection. Only ancestors in the current
407
+ // recursion path should trigger the cycle guard.
408
+ visited.delete(key);
409
+
397
410
  return { routes, searchSchemas };
398
411
  }
@@ -26,7 +26,7 @@ export function writePerModuleRouteTypes(
26
26
  * Find all variable names assigned to urls() calls in source code.
27
27
  * e.g. `export const patterns = urls(...)` -> ["patterns"]
28
28
  */
29
- function findUrlsVariableNames(code: string): string[] {
29
+ export function findUrlsVariableNames(code: string): string[] {
30
30
  const sourceFile = ts.createSourceFile(
31
31
  "input.tsx",
32
32
  code,
@@ -97,9 +97,21 @@ export function writePerModuleRouteTypesForFile(filePath: string): void {
97
97
  routes = extractRoutesFromSource(source);
98
98
  }
99
99
 
100
- if (routes.length === 0) return;
101
-
102
100
  const genPath = filePath.replace(/\.(tsx?)$/, ".gen.ts");
101
+
102
+ // When a urls() variable was found but static resolution yields zero
103
+ // routes, write an empty placeholder so generated imports stay
104
+ // resolvable until runtime discovery fills them in.
105
+ if (routes.length === 0) {
106
+ if (varNames.length > 0 && !existsSync(genPath)) {
107
+ writeFileSync(genPath, generatePerModuleTypesSource([]));
108
+ console.log(
109
+ `[rsc-router] Generated route types (placeholder) -> ${genPath}`,
110
+ );
111
+ }
112
+ return;
113
+ }
114
+
103
115
  const genSource = generatePerModuleTypesSource(routes);
104
116
  const existing = existsSync(genPath)
105
117
  ? readFileSync(genPath, "utf-8")
@@ -10,6 +10,21 @@ import {
10
10
  buildCombinedRouteMapWithSearch,
11
11
  type UnresolvableInclude,
12
12
  } from "./include-resolution.js";
13
+ import { findUrlsVariableNames } from "./per-module-writer.js";
14
+ import { isAutoGeneratedRouteName } from "../../route-name.js";
15
+
16
+ function countPublicRouteEntries(source: string): number {
17
+ const matches =
18
+ source.matchAll(/^\s+(?:"([^"]+)"|([a-zA-Z_$][^:]*)):\s*["{]/gm) ?? [];
19
+ let count = 0;
20
+ for (const match of matches) {
21
+ const routeName = match[1] || match[2];
22
+ if (routeName && !isAutoGeneratedRouteName(routeName.trim())) {
23
+ count++;
24
+ }
25
+ }
26
+ return count;
27
+ }
13
28
 
14
29
  // ---------------------------------------------------------------------------
15
30
  // Router file URL extraction
@@ -184,6 +199,33 @@ export function detectUnresolvableIncludes(
184
199
  return diagnostics;
185
200
  }
186
201
 
202
+ /**
203
+ * Walk the include tree for a standalone urls() module file and detect
204
+ * all unresolvable includes. Mirrors detectUnresolvableIncludes() but
205
+ * operates on urls() variable declarations instead of going through
206
+ * createRouter().
207
+ */
208
+ export function detectUnresolvableIncludesForUrlsFile(
209
+ filePath: string,
210
+ ): UnresolvableInclude[] {
211
+ const realPath = resolve(filePath);
212
+ let source: string;
213
+ try {
214
+ source = readFileSync(realPath, "utf-8");
215
+ } catch {
216
+ return [];
217
+ }
218
+
219
+ const varNames = findUrlsVariableNames(source);
220
+ if (varNames.length === 0) return [];
221
+
222
+ const diagnostics: UnresolvableInclude[] = [];
223
+ for (const varName of varNames) {
224
+ buildCombinedRouteMapWithSearch(realPath, varName, new Set(), diagnostics);
225
+ }
226
+ return diagnostics;
227
+ }
228
+
187
229
  // ---------------------------------------------------------------------------
188
230
  // Per-router named-routes.gen.ts writer
189
231
  // ---------------------------------------------------------------------------
@@ -300,10 +342,10 @@ export function writeCombinedRouteTypes(
300
342
  // or other dynamic code. During HMR (file watcher), always write so
301
343
  // newly added routes appear immediately.
302
344
  if (opts?.preserveIfLarger && existing) {
303
- const existingCount = (
304
- existing.match(/^\s+["a-zA-Z_$][^:]*:\s*["{]/gm) || []
345
+ const existingCount = countPublicRouteEntries(existing);
346
+ const newCount = Object.keys(result.routes).filter(
347
+ (name) => !isAutoGeneratedRouteName(name),
305
348
  ).length;
306
- const newCount = Object.keys(result.routes).length;
307
349
  if (existingCount > newCount) {
308
350
  continue;
309
351
  }
@@ -4,6 +4,7 @@ import {
4
4
  generateRouteTypesSource,
5
5
  buildCombinedRouteMapForRouterFile,
6
6
  } from "./generate-route-types.ts";
7
+ import { isAutoGeneratedRouteName } from "../route-name.js";
7
8
 
8
9
  export interface RuntimeDiscoveryOptions {
9
10
  /** Project root directory (where package.json / node_modules live). */
@@ -140,7 +141,18 @@ export async function discoverAndWriteRouteTypes(
140
141
  const manifest = generateManifest(router.urlpatterns, routerMountIndex);
141
142
  routerMountIndex++;
142
143
 
143
- const routeManifest: Record<string, string> = manifest.routeManifest;
144
+ // Filter out auto-generated route names that the runtime creates for
145
+ // unnamed routes (path() with no name option). These get names like
146
+ // "$path__health" at root level or "docs.$path__health" under include().
147
+ // Match the Vite discovery writer's predicate: any name starting with "$"
148
+ // is internal. For prefixed names, check each dot-separated segment.
149
+ const rawManifest: Record<string, string> = manifest.routeManifest;
150
+ const routeManifest: Record<string, string> = {};
151
+ for (const [name, pattern] of Object.entries(rawManifest)) {
152
+ if (!isAutoGeneratedRouteName(name)) {
153
+ routeManifest[name] = pattern;
154
+ }
155
+ }
144
156
  let routeSearchSchemas:
145
157
  | Record<string, Record<string, string>>
146
158
  | undefined = manifest.routeSearchSchemas;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Background Task Runner
3
+ *
4
+ * Unified helper for scheduling async work via waitUntil.
5
+ * When waitUntil is unavailable, falls back to blocking or skipping.
6
+ */
7
+
8
+ interface WaitUntilHost {
9
+ waitUntil?: (fn: () => Promise<void>) => void;
10
+ }
11
+
12
+ /**
13
+ * Schedule an async task in the background via waitUntil.
14
+ *
15
+ * @param host - Object with optional waitUntil (request context or similar)
16
+ * @param task - Async function to execute
17
+ * @param blockWhenNoWaitUntil - If true, awaits the task when waitUntil is
18
+ * unavailable (e.g., Node.js dev server). If false (default), the task
19
+ * is silently skipped when waitUntil is unavailable.
20
+ * @returns A promise when blocking fallback is used, void otherwise.
21
+ */
22
+ export function runBackground(
23
+ host: WaitUntilHost | null | undefined,
24
+ task: () => Promise<void>,
25
+ blockWhenNoWaitUntil = false,
26
+ ): Promise<void> | void {
27
+ if (host?.waitUntil) {
28
+ host.waitUntil(task);
29
+ return;
30
+ }
31
+ if (blockWhenNoWaitUntil) {
32
+ return task();
33
+ }
34
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared Cache Key Utilities
3
+ *
4
+ * Deterministic normalization of search params and route params
5
+ * for cache key generation. Used by cache-runtime, cache-scope,
6
+ * document-cache, and loader-cache.
7
+ */
8
+
9
+ /**
10
+ * Build a sorted, deterministic query string from URLSearchParams,
11
+ * excluding internal _rsc* and __* params.
12
+ *
13
+ * Returns empty string when no user-facing params exist.
14
+ */
15
+ export function sortedSearchString(searchParams: URLSearchParams): string {
16
+ const pairs: [string, string][] = [];
17
+ for (const [k, v] of searchParams) {
18
+ if (!k.startsWith("_rsc") && !k.startsWith("__")) {
19
+ pairs.push([k, v]);
20
+ }
21
+ }
22
+ if (pairs.length === 0) return "";
23
+ pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
24
+ return pairs
25
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
26
+ .join("&");
27
+ }
28
+
29
+ /**
30
+ * Build a sorted, deterministic string from route params.
31
+ *
32
+ * Returns empty string when params is empty or undefined.
33
+ */
34
+ export function sortedRouteParams(
35
+ params: Record<string, string> | undefined,
36
+ ): string {
37
+ if (!params) return "";
38
+ const entries = Object.entries(params);
39
+ if (entries.length === 0) return "";
40
+ return entries
41
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
42
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
43
+ .join("&");
44
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared Cache Policy Utilities
3
+ *
4
+ * Resolution cascades for TTL, SWR, cache key, and cache store.
5
+ * Consolidates the multi-tier resolution pattern:
6
+ * explicit option → store defaults → fallback constant
7
+ */
8
+
9
+ import type { CacheDefaults, SegmentCacheStore } from "./types.js";
10
+ import { _getRequestContext } from "../server/request-context.js";
11
+ import type { RequestContext } from "../server/request-context.js";
12
+
13
+ /**
14
+ * Default TTL for route-level cache() DSL and loader cache.
15
+ * Applied when neither the cache options nor the store defaults specify a TTL.
16
+ */
17
+ export const DEFAULT_ROUTE_TTL = 60;
18
+
19
+ /**
20
+ * Default TTL for function-level "use cache" (setItem).
21
+ * Applied when neither the item options nor the store defaults specify a TTL.
22
+ */
23
+ export const DEFAULT_FUNCTION_TTL = 900;
24
+
25
+ /**
26
+ * Resolve effective TTL from the 3-tier cascade:
27
+ * explicit → store defaults → fallback.
28
+ */
29
+ export function resolveTtl(
30
+ explicit: number | undefined,
31
+ defaults: CacheDefaults | undefined,
32
+ fallback: number,
33
+ ): number {
34
+ if (explicit !== undefined) return explicit;
35
+ if (defaults?.ttl !== undefined) return defaults.ttl;
36
+ return fallback;
37
+ }
38
+
39
+ /**
40
+ * Resolve effective SWR window from the 2-tier cascade:
41
+ * explicit → store defaults.
42
+ * Returns 0 when unset (no SWR window).
43
+ */
44
+ export function resolveSwrWindow(
45
+ explicit: number | undefined,
46
+ defaults: CacheDefaults | undefined,
47
+ ): number {
48
+ if (explicit !== undefined) return explicit;
49
+ if (defaults?.swr !== undefined) return defaults.swr;
50
+ return 0;
51
+ }
52
+
53
+ /**
54
+ * Compute staleAt and expiresAt timestamps from TTL and SWR window.
55
+ *
56
+ * - staleAt: when the entry becomes stale (TTL boundary)
57
+ * - expiresAt: when the entry should be evicted (TTL + SWR)
58
+ *
59
+ * When swrWindow is 0, staleAt === expiresAt (no SWR).
60
+ */
61
+ export function computeExpiration(
62
+ ttlSeconds: number,
63
+ swrSeconds: number = 0,
64
+ ): { staleAt: number; expiresAt: number } {
65
+ const now = Date.now();
66
+ const staleAt = now + ttlSeconds * 1000;
67
+ const expiresAt = staleAt + swrSeconds * 1000;
68
+ return { staleAt, expiresAt };
69
+ }
70
+
71
+ // ============================================================================
72
+ // Cache Key Resolution
73
+ // ============================================================================
74
+
75
+ /**
76
+ * Resolve cache key using the 3-tier priority:
77
+ * 1. keyFn (full override from route/loader cache options)
78
+ * 2. store.keyGenerator (modifies default key)
79
+ * 3. defaultKey (used when neither keyFn nor keyGenerator is provided)
80
+ *
81
+ * Errors from keyFn and store.keyGenerator propagate to the caller.
82
+ * Cache identity is correctness-critical: if explicit key logic throws,
83
+ * silently remapping to a different key could cause cache collisions or
84
+ * serve stale/wrong data. Callers must handle the error or let it surface.
85
+ *
86
+ * Uses _getRequestContext (non-throwing) so that calls outside ALS
87
+ * (e.g. build-time) gracefully fall back to defaultKey.
88
+ */
89
+ export async function resolveCacheKey(
90
+ keyFn: ((ctx: RequestContext) => string | Promise<string>) | undefined,
91
+ store: SegmentCacheStore | null,
92
+ defaultKey: string,
93
+ _label: string,
94
+ ): Promise<string> {
95
+ const requestCtx = _getRequestContext();
96
+
97
+ // Priority 1: Route/loader-level key function (full override)
98
+ if (keyFn && requestCtx) {
99
+ return await keyFn(requestCtx);
100
+ }
101
+
102
+ // Priority 2: Store-level keyGenerator (modifies default key)
103
+ if (store?.keyGenerator && requestCtx) {
104
+ return await store.keyGenerator(requestCtx, defaultKey);
105
+ }
106
+
107
+ // Priority 3: Default key (no custom key logic provided)
108
+ return defaultKey;
109
+ }
110
+
111
+ // ============================================================================
112
+ // Cache Store Resolution
113
+ // ============================================================================
114
+
115
+ /**
116
+ * Resolve cache store from the 2-tier priority:
117
+ * 1. Explicit store from cache options
118
+ * 2. App-level store from request context
119
+ */
120
+ export function resolveCacheStore(
121
+ explicitStore: SegmentCacheStore | undefined,
122
+ ): SegmentCacheStore | null {
123
+ if (explicitStore) return explicitStore;
124
+ return _getRequestContext()?._cacheStore ?? null;
125
+ }