@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
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
-
createTemporaryReferenceSet,
|
|
21
20
|
encodeReply,
|
|
22
21
|
createClientTemporaryReferenceSet,
|
|
23
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
@@ -26,19 +25,17 @@ import {
|
|
|
26
25
|
isTainted,
|
|
27
26
|
CACHED_FN_SYMBOL,
|
|
28
27
|
isCachedFunction,
|
|
29
|
-
|
|
28
|
+
stampCacheExec,
|
|
29
|
+
unstampCacheExec,
|
|
30
30
|
} from "./taint.js";
|
|
31
31
|
|
|
32
32
|
export { isCachedFunction };
|
|
33
|
-
import { getCacheProfile } from "./profile-registry.js";
|
|
34
33
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
35
|
-
import
|
|
36
|
-
import type { HandleStore } from "../server/handle-store.js";
|
|
34
|
+
import { createHandleStore } from "../server/handle-store.js";
|
|
37
35
|
import { restoreHandles } from "./handle-snapshot.js";
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// ============================================================================
|
|
36
|
+
import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
|
|
37
|
+
import { sortedSearchString } from "./cache-key-utils.js";
|
|
38
|
+
import { runBackground } from "./background-task.js";
|
|
42
39
|
|
|
43
40
|
/**
|
|
44
41
|
* Convert encodeReply result to a stable string key.
|
|
@@ -51,47 +48,6 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
|
|
|
51
48
|
return text;
|
|
52
49
|
}
|
|
53
50
|
|
|
54
|
-
// ============================================================================
|
|
55
|
-
// Handle Capture
|
|
56
|
-
// ============================================================================
|
|
57
|
-
|
|
58
|
-
interface HandleCapture {
|
|
59
|
-
data: Record<string, SegmentHandleData>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function startHandleCapture(handleStore: HandleStore): HandleCapture {
|
|
63
|
-
const capture: HandleCapture = { data: {} };
|
|
64
|
-
const originalPush = handleStore.push.bind(handleStore);
|
|
65
|
-
|
|
66
|
-
// Intercept push() calls to record them
|
|
67
|
-
handleStore.push = (
|
|
68
|
-
handleName: string,
|
|
69
|
-
segmentId: string,
|
|
70
|
-
value: unknown,
|
|
71
|
-
) => {
|
|
72
|
-
if (!capture.data[segmentId]) {
|
|
73
|
-
capture.data[segmentId] = {};
|
|
74
|
-
}
|
|
75
|
-
if (!capture.data[segmentId][handleName]) {
|
|
76
|
-
capture.data[segmentId][handleName] = [];
|
|
77
|
-
}
|
|
78
|
-
capture.data[segmentId][handleName].push(value);
|
|
79
|
-
// Still call the original so the data flows through normally
|
|
80
|
-
originalPush(handleName, segmentId, value);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
return capture;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function stopHandleCapture(
|
|
87
|
-
handleStore: HandleStore,
|
|
88
|
-
_capture: HandleCapture,
|
|
89
|
-
): void {
|
|
90
|
-
// Restore original push by deleting the override
|
|
91
|
-
// (the original is on the prototype/closure, our override is an own property)
|
|
92
|
-
delete (handleStore as any).push;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
51
|
// ============================================================================
|
|
96
52
|
// Core: registerCachedFunction
|
|
97
53
|
// ============================================================================
|
|
@@ -112,17 +68,30 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
112
68
|
const wrapped = async function (this: any, ...args: any[]): Promise<any> {
|
|
113
69
|
const requestCtx = getRequestContext();
|
|
114
70
|
const store = requestCtx?._cacheStore;
|
|
115
|
-
const
|
|
71
|
+
const resolvedProfileName = profileName || "default";
|
|
116
72
|
|
|
117
|
-
// Bypass: no store
|
|
118
|
-
if (!store?.getItem
|
|
73
|
+
// Bypass: no store or no getItem support
|
|
74
|
+
if (!store?.getItem) {
|
|
119
75
|
return fn.apply(this, args);
|
|
120
76
|
}
|
|
121
77
|
|
|
78
|
+
// Resolve profile strictly from request-scoped config (set by the
|
|
79
|
+
// active router via createRequestContext). No global fallback —
|
|
80
|
+
// global profile state is only for DSL-time cache("profileName").
|
|
81
|
+
const profile = requestCtx?._cacheProfiles?.[resolvedProfileName];
|
|
82
|
+
|
|
83
|
+
if (!profile) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[use cache] "${id}" uses unknown cache profile "${resolvedProfileName}". ` +
|
|
86
|
+
`Define it in createRouter({ cacheProfiles: { "${resolvedProfileName}": { ttl: ... } } }).`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
122
90
|
// Separate tainted args (ctx, env, req) from key-generating args.
|
|
123
|
-
// For tainted objects that carry route context (params, pathname
|
|
124
|
-
// extract
|
|
125
|
-
//
|
|
91
|
+
// For tainted objects that carry route context (params, pathname,
|
|
92
|
+
// searchParams), extract serializable values into the key so
|
|
93
|
+
// different routes, param combinations, and query variants produce
|
|
94
|
+
// distinct cache entries.
|
|
126
95
|
const keyArgs: unknown[] = [];
|
|
127
96
|
let hasTaintedArgs = false;
|
|
128
97
|
for (const arg of args) {
|
|
@@ -130,10 +99,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
130
99
|
hasTaintedArgs = true;
|
|
131
100
|
const ctx = arg as any;
|
|
132
101
|
if (ctx.params && typeof ctx.params === "object") {
|
|
102
|
+
// Include host to prevent cross-host cache collisions (same
|
|
103
|
+
// pattern as route-level cache-scope.ts key generation).
|
|
104
|
+
if (ctx.url?.host) {
|
|
105
|
+
keyArgs.push(ctx.url.host);
|
|
106
|
+
}
|
|
107
|
+
// Include route name to prevent collisions when the same cached
|
|
108
|
+
// function is reused across routes with identical pathname/params
|
|
109
|
+
// but different local reverse() scope.
|
|
110
|
+
if (ctx._routeName) {
|
|
111
|
+
keyArgs.push(ctx._routeName);
|
|
112
|
+
}
|
|
133
113
|
keyArgs.push(ctx.pathname, ctx.params);
|
|
134
114
|
if (ctx._responseType) {
|
|
135
115
|
keyArgs.push(ctx._responseType);
|
|
136
116
|
}
|
|
117
|
+
// Include user-facing search params (exclude internal _rsc*/__ params)
|
|
118
|
+
if (ctx.searchParams instanceof URLSearchParams) {
|
|
119
|
+
const normalized = sortedSearchString(ctx.searchParams);
|
|
120
|
+
if (normalized) {
|
|
121
|
+
keyArgs.push(normalized);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
137
124
|
}
|
|
138
125
|
} else {
|
|
139
126
|
keyArgs.push(arg);
|
|
@@ -202,24 +189,75 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
202
189
|
restoreHandles(cached.handles, handleStore);
|
|
203
190
|
}
|
|
204
191
|
}
|
|
205
|
-
// Background revalidation
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
192
|
+
// Background revalidation — must capture handles if tainted args present.
|
|
193
|
+
// Use an isolated handle store so background pushes don't pollute the
|
|
194
|
+
// live response or throw LateHandlePushError on the completed store.
|
|
195
|
+
// Same isolation pattern as route-level background-revalidation.ts.
|
|
196
|
+
runBackground(requestCtx, async () => {
|
|
197
|
+
// Reuse closure-captured requestCtx instead of calling
|
|
198
|
+
// getRequestContext() — ALS context may be gone inside waitUntil.
|
|
199
|
+
let originalHandleStore:
|
|
200
|
+
| ReturnType<typeof createHandleStore>
|
|
201
|
+
| undefined;
|
|
202
|
+
if (hasTaintedArgs && requestCtx) {
|
|
203
|
+
originalHandleStore = requestCtx._handleStore;
|
|
204
|
+
requestCtx._handleStore = createHandleStore();
|
|
205
|
+
}
|
|
206
|
+
const bgHandleStore = hasTaintedArgs
|
|
207
|
+
? requestCtx?._handleStore
|
|
208
|
+
: undefined;
|
|
209
|
+
let bgCapture: HandleCapture | undefined;
|
|
210
|
+
let bgStopCapture: (() => void) | undefined;
|
|
211
|
+
if (bgHandleStore) {
|
|
212
|
+
const c = startHandleCapture(bgHandleStore);
|
|
213
|
+
bgCapture = c.capture;
|
|
214
|
+
bgStopCapture = c.stop;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Stamp tainted args and RequestContext so request-scoped
|
|
218
|
+
// reads (cookies, headers) and side effects (ctx.set, etc.)
|
|
219
|
+
// throw inside background revalidation, same as the miss path.
|
|
220
|
+
// Uses ref-counted stamp/unstamp so overlapping executions
|
|
221
|
+
// sharing the same ctx don't clear each other's guards.
|
|
222
|
+
const bgTaintedArgs: unknown[] = [];
|
|
223
|
+
for (const arg of args) {
|
|
224
|
+
if (isTainted(arg)) {
|
|
225
|
+
stampCacheExec(arg as object);
|
|
226
|
+
bgTaintedArgs.push(arg);
|
|
220
227
|
}
|
|
221
|
-
}
|
|
222
|
-
|
|
228
|
+
}
|
|
229
|
+
if (requestCtx) {
|
|
230
|
+
stampCacheExec(requestCtx as object);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const freshResult = await fn.apply(this, args);
|
|
235
|
+
bgStopCapture?.();
|
|
236
|
+
const serialized = await serializeResult(freshResult);
|
|
237
|
+
if (serialized !== null) {
|
|
238
|
+
await store.setItem!(cacheKey, serialized, {
|
|
239
|
+
handles: bgCapture?.data,
|
|
240
|
+
ttl: profile.ttl,
|
|
241
|
+
swr: profile.swr,
|
|
242
|
+
tags: profile.tags,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
} catch (bgError) {
|
|
246
|
+
bgStopCapture?.();
|
|
247
|
+
requestCtx?._reportBackgroundError?.(bgError, "stale-revalidation");
|
|
248
|
+
} finally {
|
|
249
|
+
for (const arg of bgTaintedArgs) {
|
|
250
|
+
unstampCacheExec(arg as object);
|
|
251
|
+
}
|
|
252
|
+
if (requestCtx) {
|
|
253
|
+
unstampCacheExec(requestCtx as object);
|
|
254
|
+
}
|
|
255
|
+
// Restore original handle store
|
|
256
|
+
if (originalHandleStore && requestCtx) {
|
|
257
|
+
requestCtx._handleStore = originalHandleStore;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
});
|
|
223
261
|
return result;
|
|
224
262
|
} catch {
|
|
225
263
|
// Deserialization of stale value failed, fall through
|
|
@@ -229,41 +267,44 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
229
267
|
// Cache miss: execute, serialize, store
|
|
230
268
|
const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
231
269
|
let capture: HandleCapture | undefined;
|
|
270
|
+
let stopCapture: (() => void) | undefined;
|
|
232
271
|
if (handleStore && hasTaintedArgs) {
|
|
233
|
-
|
|
272
|
+
const c = startHandleCapture(handleStore);
|
|
273
|
+
capture = c.capture;
|
|
274
|
+
stopCapture = c.stop;
|
|
234
275
|
}
|
|
235
276
|
|
|
236
277
|
// Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
|
|
237
278
|
// inside the cached function body (those side effects are lost on hit).
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
// the HandlerContext/ResponseHandlerContext passed as args).
|
|
279
|
+
// Uses ref-counted stamp/unstamp so overlapping executions
|
|
280
|
+
// sharing the same ctx don't clear each other's guards.
|
|
241
281
|
const taintedArgs: unknown[] = [];
|
|
242
282
|
for (const arg of args) {
|
|
243
283
|
if (isTainted(arg)) {
|
|
244
|
-
(arg as
|
|
284
|
+
stampCacheExec(arg as object);
|
|
245
285
|
taintedArgs.push(arg);
|
|
246
286
|
}
|
|
247
287
|
}
|
|
248
|
-
|
|
249
|
-
|
|
288
|
+
// Always stamp the ALS RequestContext so cookies()/headers() guards fire
|
|
289
|
+
// even when the cached function receives no tainted args. The guard in
|
|
290
|
+
// cookie-store.ts checks RequestContext, not function args.
|
|
291
|
+
if (requestCtx) {
|
|
292
|
+
stampCacheExec(requestCtx as object);
|
|
250
293
|
}
|
|
251
294
|
|
|
252
295
|
let result: any;
|
|
253
296
|
try {
|
|
254
297
|
result = await fn.apply(this, args);
|
|
255
298
|
} finally {
|
|
256
|
-
//
|
|
299
|
+
// Decrement ref count; symbol is deleted when it reaches zero
|
|
257
300
|
for (const arg of taintedArgs) {
|
|
258
|
-
|
|
301
|
+
unstampCacheExec(arg as object);
|
|
259
302
|
}
|
|
260
|
-
if (
|
|
261
|
-
|
|
303
|
+
if (requestCtx) {
|
|
304
|
+
unstampCacheExec(requestCtx as object);
|
|
262
305
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (capture && handleStore) {
|
|
266
|
-
stopHandleCapture(handleStore, capture);
|
|
306
|
+
// Remove this capture token (order-independent, safe for concurrent use)
|
|
307
|
+
stopCapture?.();
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
// Serialize and store — fully non-blocking when waitUntil is available.
|
|
@@ -279,17 +320,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
279
320
|
tags: profile.tags,
|
|
280
321
|
});
|
|
281
322
|
}
|
|
282
|
-
} catch {
|
|
283
|
-
|
|
323
|
+
} catch (writeError) {
|
|
324
|
+
requestCtx?._reportBackgroundError?.(writeError, "cache-write");
|
|
284
325
|
}
|
|
285
326
|
};
|
|
286
327
|
|
|
287
|
-
|
|
288
|
-
requestCtx.waitUntil(cacheWrite);
|
|
289
|
-
} else {
|
|
290
|
-
// No waitUntil (e.g. Node.js dev server): run inline as best-effort
|
|
291
|
-
await cacheWrite();
|
|
292
|
-
}
|
|
328
|
+
await runBackground(requestCtx, cacheWrite, true);
|
|
293
329
|
|
|
294
330
|
return result;
|
|
295
331
|
};
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -18,13 +18,12 @@ import {
|
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
19
|
import { serializeSegments, deserializeSegments } from "./segment-codec.js";
|
|
20
20
|
import { captureHandles, restoreHandles } from "./handle-snapshot.js";
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const DEFAULT_TTL_SECONDS = 60;
|
|
21
|
+
import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_ROUTE_TTL,
|
|
24
|
+
resolveCacheKey,
|
|
25
|
+
resolveCacheStore,
|
|
26
|
+
} from "./cache-policy.js";
|
|
28
27
|
|
|
29
28
|
function debugCacheLog(message: string): void {
|
|
30
29
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -37,27 +36,31 @@ function debugCacheLog(message: string): void {
|
|
|
37
36
|
// ============================================================================
|
|
38
37
|
|
|
39
38
|
/**
|
|
40
|
-
* Generate cache key base from pathname and params.
|
|
41
|
-
*
|
|
39
|
+
* Generate cache key base from host, pathname, route params, and search params.
|
|
40
|
+
* Host is included to prevent cross-host cache collisions on shared stores.
|
|
41
|
+
* Route params and search params are sorted alphabetically for deterministic keys.
|
|
42
|
+
* Internal _rsc* and __* query params are excluded.
|
|
42
43
|
* @internal
|
|
43
44
|
*/
|
|
44
45
|
function getCacheKeyBase(
|
|
46
|
+
host: string,
|
|
45
47
|
pathname: string,
|
|
46
48
|
params?: Record<string, string>,
|
|
49
|
+
searchParams?: URLSearchParams,
|
|
47
50
|
): string {
|
|
48
|
-
const paramStr = params
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
51
|
+
const paramStr = sortedRouteParams(params);
|
|
52
|
+
const searchStr = searchParams ? sortedSearchString(searchParams) : "";
|
|
53
|
+
|
|
54
|
+
let key = `${host}${pathname}`;
|
|
55
|
+
if (paramStr) key += `:${paramStr}`;
|
|
56
|
+
if (searchStr) key += `?${searchStr}`;
|
|
57
|
+
return key;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
/**
|
|
59
61
|
* Generate default cache key for a route request.
|
|
60
|
-
*
|
|
62
|
+
* Includes pathname, route params, and user-facing search params for
|
|
63
|
+
* correct scoping. Internal _rsc* params are excluded.
|
|
61
64
|
* Includes request type prefix since they produce different segment sets:
|
|
62
65
|
* - doc: document requests (full page load)
|
|
63
66
|
* - partial: navigation requests (client-side navigation)
|
|
@@ -71,11 +74,13 @@ function getDefaultRouteCacheKey(
|
|
|
71
74
|
): string {
|
|
72
75
|
const ctx = getRequestContext();
|
|
73
76
|
const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
|
|
77
|
+
const searchParams = ctx?.url.searchParams;
|
|
78
|
+
const host = ctx?.url.host ?? "localhost";
|
|
74
79
|
|
|
75
80
|
// Intercept navigations get their own cache namespace
|
|
76
81
|
const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
|
|
77
82
|
|
|
78
|
-
return `${prefix}:${getCacheKeyBase(pathname, params)}`;
|
|
83
|
+
return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
|
|
79
84
|
}
|
|
80
85
|
|
|
81
86
|
// ============================================================================
|
|
@@ -140,7 +145,7 @@ export class CacheScope {
|
|
|
140
145
|
}
|
|
141
146
|
|
|
142
147
|
// Hardcoded fallback
|
|
143
|
-
return
|
|
148
|
+
return DEFAULT_ROUTE_TTL;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
/**
|
|
@@ -165,23 +170,11 @@ export class CacheScope {
|
|
|
165
170
|
* 2. App-level store from request context
|
|
166
171
|
*/
|
|
167
172
|
getStore(): SegmentCacheStore | null {
|
|
168
|
-
|
|
169
|
-
if (this.explicitStore) {
|
|
170
|
-
return this.explicitStore;
|
|
171
|
-
}
|
|
172
|
-
// Fall back to app-level store from request context
|
|
173
|
-
const ctx = getRequestContext();
|
|
174
|
-
return ctx?._cacheStore ?? null;
|
|
173
|
+
return resolveCacheStore(this.explicitStore);
|
|
175
174
|
}
|
|
176
175
|
|
|
177
176
|
/**
|
|
178
|
-
* Resolve the cache key using
|
|
179
|
-
*
|
|
180
|
-
* Resolution priority:
|
|
181
|
-
* 1. Route-level `key` function (full override)
|
|
182
|
-
* 2. Store-level `keyGenerator` (modifies default key)
|
|
183
|
-
* 3. Default key generation (prefix:pathname:params)
|
|
184
|
-
*
|
|
177
|
+
* Resolve the cache key using the shared 3-tier priority.
|
|
185
178
|
* @internal
|
|
186
179
|
*/
|
|
187
180
|
private async resolveKey(
|
|
@@ -189,46 +182,9 @@ export class CacheScope {
|
|
|
189
182
|
params: Record<string, string>,
|
|
190
183
|
isIntercept?: boolean,
|
|
191
184
|
): Promise<string> {
|
|
192
|
-
const requestCtx = getRequestContext();
|
|
193
|
-
if (!requestCtx) {
|
|
194
|
-
// Fallback to default key if no request context
|
|
195
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Priority 1: Route-level key function (full override)
|
|
199
|
-
if (this.config !== false && this.config.key) {
|
|
200
|
-
try {
|
|
201
|
-
const customKey = await this.config.key(requestCtx);
|
|
202
|
-
return customKey;
|
|
203
|
-
} catch (error) {
|
|
204
|
-
console.error(
|
|
205
|
-
`[CacheScope] Custom key function failed, using default:`,
|
|
206
|
-
error,
|
|
207
|
-
);
|
|
208
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Generate default key
|
|
213
185
|
const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const store = this.getStore();
|
|
217
|
-
if (store?.keyGenerator) {
|
|
218
|
-
try {
|
|
219
|
-
const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
|
|
220
|
-
return modifiedKey;
|
|
221
|
-
} catch (error) {
|
|
222
|
-
console.error(
|
|
223
|
-
`[CacheScope] Store keyGenerator failed, using default:`,
|
|
224
|
-
error,
|
|
225
|
-
);
|
|
226
|
-
return defaultKey;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// Priority 3: Default key
|
|
231
|
-
return defaultKey;
|
|
186
|
+
const keyFn = this.config !== false ? this.config.key : undefined;
|
|
187
|
+
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
232
188
|
}
|
|
233
189
|
|
|
234
190
|
/**
|
|
@@ -249,6 +205,27 @@ export class CacheScope {
|
|
|
249
205
|
} | null> {
|
|
250
206
|
if (!this.enabled) return null;
|
|
251
207
|
|
|
208
|
+
// Evaluate condition — skip cache read when condition returns false
|
|
209
|
+
if (this.config !== false && this.config.condition) {
|
|
210
|
+
const requestCtx = getRequestContext();
|
|
211
|
+
if (requestCtx) {
|
|
212
|
+
try {
|
|
213
|
+
if (!this.config.condition(requestCtx)) {
|
|
214
|
+
debugCacheLog(
|
|
215
|
+
`[CacheScope] condition returned false, skipping cache read`,
|
|
216
|
+
);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(
|
|
221
|
+
`[CacheScope] condition function threw, skipping cache read:`,
|
|
222
|
+
error,
|
|
223
|
+
);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
252
229
|
const store = this.getStore();
|
|
253
230
|
if (!store) return null;
|
|
254
231
|
|
|
@@ -308,6 +285,27 @@ export class CacheScope {
|
|
|
308
285
|
): Promise<void> {
|
|
309
286
|
if (!this.enabled || segments.length === 0) return;
|
|
310
287
|
|
|
288
|
+
// Evaluate condition — skip cache write when condition returns false
|
|
289
|
+
if (this.config !== false && this.config.condition) {
|
|
290
|
+
const conditionCtx = getRequestContext();
|
|
291
|
+
if (conditionCtx) {
|
|
292
|
+
try {
|
|
293
|
+
if (!this.config.condition(conditionCtx)) {
|
|
294
|
+
debugCacheLog(
|
|
295
|
+
`[CacheScope] condition returned false, skipping cache write`,
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(
|
|
301
|
+
`[CacheScope] condition function threw, skipping cache write:`,
|
|
302
|
+
error,
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
311
309
|
const store = this.getStore();
|
|
312
310
|
if (!store) return;
|
|
313
311
|
|
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
type RequestContext,
|
|
34
34
|
} from "../../server/request-context.js";
|
|
35
35
|
import { VERSION } from "@rangojs/router:version";
|
|
36
|
+
import {
|
|
37
|
+
resolveTtl,
|
|
38
|
+
resolveSwrWindow,
|
|
39
|
+
DEFAULT_FUNCTION_TTL,
|
|
40
|
+
} from "../cache-policy.js";
|
|
36
41
|
|
|
37
42
|
// ============================================================================
|
|
38
43
|
// Constants
|
|
@@ -299,7 +304,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
299
304
|
const request = this.keyToRequest(key);
|
|
300
305
|
|
|
301
306
|
// Extended TTL covers SWR window
|
|
302
|
-
const swrWindow = swr
|
|
307
|
+
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
303
308
|
const totalTtl = ttl + swrWindow;
|
|
304
309
|
const staleAt = Date.now() + ttl * 1000;
|
|
305
310
|
|
|
@@ -389,7 +394,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
389
394
|
const request = this.keyToRequest(`doc:${key}`);
|
|
390
395
|
|
|
391
396
|
// Extended TTL covers SWR window
|
|
392
|
-
const swrWindow = swr
|
|
397
|
+
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
393
398
|
const totalTtl = ttl + swrWindow;
|
|
394
399
|
const staleAt = Date.now() + ttl * 1000;
|
|
395
400
|
|
|
@@ -490,8 +495,8 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
490
495
|
const cache = await this.getCache();
|
|
491
496
|
const request = this.keyToRequest(`fn:${key}`);
|
|
492
497
|
|
|
493
|
-
const ttl = options?.ttl
|
|
494
|
-
const swrWindow = options?.swr
|
|
498
|
+
const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
499
|
+
const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
|
|
495
500
|
const totalTtl = ttl + swrWindow;
|
|
496
501
|
const staleAt = Date.now() + ttl * 1000;
|
|
497
502
|
|