@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.
- package/README.md +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /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)
|
|
70
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|