@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- 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 +170 -18
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- 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/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- 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/index.ts +0 -3
- 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 +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- 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 +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -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 +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- 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/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- 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 +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/cache/cache-scope.ts
CHANGED
|
@@ -12,24 +12,18 @@ import type { PartialCacheOptions } from "../types.js";
|
|
|
12
12
|
import type { ResolvedSegment } from "../types.js";
|
|
13
13
|
import type { SegmentCacheStore, CachedEntryData } from "./types.js";
|
|
14
14
|
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
getRequestContext,
|
|
17
|
+
_getRequestContext,
|
|
18
|
+
} from "../server/request-context.js";
|
|
16
19
|
import { serializeSegments, deserializeSegments } from "./segment-codec.js";
|
|
17
20
|
import { captureHandles, restoreHandles } from "./handle-snapshot.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
deserializeSegments,
|
|
25
|
-
} from "./segment-codec.js";
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// Constants
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
/** Default TTL when no explicit value or store defaults are configured */
|
|
32
|
-
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";
|
|
33
27
|
|
|
34
28
|
function debugCacheLog(message: string): void {
|
|
35
29
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -42,27 +36,31 @@ function debugCacheLog(message: string): void {
|
|
|
42
36
|
// ============================================================================
|
|
43
37
|
|
|
44
38
|
/**
|
|
45
|
-
* Generate cache key base from pathname and params.
|
|
46
|
-
*
|
|
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.
|
|
47
43
|
* @internal
|
|
48
44
|
*/
|
|
49
45
|
function getCacheKeyBase(
|
|
46
|
+
host: string,
|
|
50
47
|
pathname: string,
|
|
51
48
|
params?: Record<string, string>,
|
|
49
|
+
searchParams?: URLSearchParams,
|
|
52
50
|
): string {
|
|
53
|
-
const paramStr = params
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
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;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* Generate default cache key for a route request.
|
|
65
|
-
*
|
|
62
|
+
* Includes pathname, route params, and user-facing search params for
|
|
63
|
+
* correct scoping. Internal _rsc* params are excluded.
|
|
66
64
|
* Includes request type prefix since they produce different segment sets:
|
|
67
65
|
* - doc: document requests (full page load)
|
|
68
66
|
* - partial: navigation requests (client-side navigation)
|
|
@@ -76,11 +74,13 @@ function getDefaultRouteCacheKey(
|
|
|
76
74
|
): string {
|
|
77
75
|
const ctx = getRequestContext();
|
|
78
76
|
const isPartial = ctx?.url.searchParams.has("_rsc_partial") ?? false;
|
|
77
|
+
const searchParams = ctx?.url.searchParams;
|
|
78
|
+
const host = ctx?.url.host ?? "localhost";
|
|
79
79
|
|
|
80
80
|
// Intercept navigations get their own cache namespace
|
|
81
81
|
const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
|
|
82
82
|
|
|
83
|
-
return `${prefix}:${getCacheKeyBase(pathname, params)}`;
|
|
83
|
+
return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// ============================================================================
|
|
@@ -145,7 +145,7 @@ export class CacheScope {
|
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
// Hardcoded fallback
|
|
148
|
-
return
|
|
148
|
+
return DEFAULT_ROUTE_TTL;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/**
|
|
@@ -170,23 +170,11 @@ export class CacheScope {
|
|
|
170
170
|
* 2. App-level store from request context
|
|
171
171
|
*/
|
|
172
172
|
getStore(): SegmentCacheStore | null {
|
|
173
|
-
|
|
174
|
-
if (this.explicitStore) {
|
|
175
|
-
return this.explicitStore;
|
|
176
|
-
}
|
|
177
|
-
// Fall back to app-level store from request context
|
|
178
|
-
const ctx = getRequestContext();
|
|
179
|
-
return ctx?._cacheStore ?? null;
|
|
173
|
+
return resolveCacheStore(this.explicitStore);
|
|
180
174
|
}
|
|
181
175
|
|
|
182
176
|
/**
|
|
183
|
-
* Resolve the cache key using
|
|
184
|
-
*
|
|
185
|
-
* Resolution priority:
|
|
186
|
-
* 1. Route-level `key` function (full override)
|
|
187
|
-
* 2. Store-level `keyGenerator` (modifies default key)
|
|
188
|
-
* 3. Default key generation (prefix:pathname:params)
|
|
189
|
-
*
|
|
177
|
+
* Resolve the cache key using the shared 3-tier priority.
|
|
190
178
|
* @internal
|
|
191
179
|
*/
|
|
192
180
|
private async resolveKey(
|
|
@@ -194,46 +182,9 @@ export class CacheScope {
|
|
|
194
182
|
params: Record<string, string>,
|
|
195
183
|
isIntercept?: boolean,
|
|
196
184
|
): Promise<string> {
|
|
197
|
-
const requestCtx = getRequestContext();
|
|
198
|
-
if (!requestCtx) {
|
|
199
|
-
// Fallback to default key if no request context
|
|
200
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Priority 1: Route-level key function (full override)
|
|
204
|
-
if (this.config !== false && this.config.key) {
|
|
205
|
-
try {
|
|
206
|
-
const customKey = await this.config.key(requestCtx);
|
|
207
|
-
return customKey;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
console.error(
|
|
210
|
-
`[CacheScope] Custom key function failed, using default:`,
|
|
211
|
-
error,
|
|
212
|
-
);
|
|
213
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Generate default key
|
|
218
185
|
const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const store = this.getStore();
|
|
222
|
-
if (store?.keyGenerator) {
|
|
223
|
-
try {
|
|
224
|
-
const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
|
|
225
|
-
return modifiedKey;
|
|
226
|
-
} catch (error) {
|
|
227
|
-
console.error(
|
|
228
|
-
`[CacheScope] Store keyGenerator failed, using default:`,
|
|
229
|
-
error,
|
|
230
|
-
);
|
|
231
|
-
return defaultKey;
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Priority 3: Default key
|
|
236
|
-
return defaultKey;
|
|
186
|
+
const keyFn = this.config !== false ? this.config.key : undefined;
|
|
187
|
+
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
237
188
|
}
|
|
238
189
|
|
|
239
190
|
/**
|
|
@@ -254,6 +205,27 @@ export class CacheScope {
|
|
|
254
205
|
} | null> {
|
|
255
206
|
if (!this.enabled) return null;
|
|
256
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
|
+
|
|
257
229
|
const store = this.getStore();
|
|
258
230
|
if (!store) return null;
|
|
259
231
|
|
|
@@ -274,7 +246,7 @@ export class CacheScope {
|
|
|
274
246
|
const segments = await deserializeSegments(cached.segments);
|
|
275
247
|
|
|
276
248
|
// Replay handle data
|
|
277
|
-
const handleStore =
|
|
249
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
278
250
|
if (handleStore) {
|
|
279
251
|
restoreHandles(cached.handles, handleStore);
|
|
280
252
|
}
|
|
@@ -313,6 +285,27 @@ export class CacheScope {
|
|
|
313
285
|
): Promise<void> {
|
|
314
286
|
if (!this.enabled || segments.length === 0) return;
|
|
315
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
|
+
|
|
316
309
|
const store = this.getStore();
|
|
317
310
|
if (!store) return;
|
|
318
311
|
|
|
@@ -29,10 +29,15 @@ import type {
|
|
|
29
29
|
CacheItemOptions,
|
|
30
30
|
} from "../types.js";
|
|
31
31
|
import {
|
|
32
|
-
|
|
32
|
+
_getRequestContext,
|
|
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
|
|
@@ -126,7 +131,7 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
126
131
|
* @example Using cookies for locale-aware caching
|
|
127
132
|
* ```typescript
|
|
128
133
|
* keyGenerator: (ctx, defaultKey) => {
|
|
129
|
-
* const locale =
|
|
134
|
+
* const locale = cookies().get('locale')?.value || 'en';
|
|
130
135
|
* return `${locale}:${defaultKey}`;
|
|
131
136
|
* }
|
|
132
137
|
* ```
|
|
@@ -184,7 +189,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
184
189
|
private deriveBaseUrl(): string {
|
|
185
190
|
const fallback = "https://rsc-cache.internal.com/";
|
|
186
191
|
|
|
187
|
-
const ctx =
|
|
192
|
+
const ctx = _getRequestContext();
|
|
188
193
|
if (!ctx?.request) {
|
|
189
194
|
return fallback;
|
|
190
195
|
}
|
|
@@ -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
|
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
|
|
14
14
|
import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
15
15
|
import { getRequestContext } from "../server/request-context.js";
|
|
16
|
+
import { sortedSearchString } from "./cache-key-utils.js";
|
|
17
|
+
import { runBackground } from "./background-task.js";
|
|
16
18
|
|
|
17
19
|
// ============================================================================
|
|
18
20
|
// Constants
|
|
@@ -110,15 +112,24 @@ function addCacheStatusHeader(
|
|
|
110
112
|
}
|
|
111
113
|
|
|
112
114
|
/**
|
|
113
|
-
*
|
|
115
|
+
* Drain and run onResponse callbacks registered on the request context.
|
|
116
|
+
* Mirrors the drain semantics of finalizeResponse() in rsc/helpers.ts:
|
|
117
|
+
* callbacks are spliced out so they fire at most once per request.
|
|
114
118
|
*/
|
|
115
|
-
function
|
|
119
|
+
function drainOnResponseCallbacks(
|
|
116
120
|
response: Response,
|
|
117
|
-
|
|
121
|
+
requestCtx:
|
|
122
|
+
| { _onResponseCallbacks: Array<(r: Response) => Response> }
|
|
123
|
+
| undefined,
|
|
118
124
|
): Response {
|
|
125
|
+
if (!requestCtx || requestCtx._onResponseCallbacks.length === 0) {
|
|
126
|
+
return response;
|
|
127
|
+
}
|
|
128
|
+
const callbacks = requestCtx._onResponseCallbacks;
|
|
129
|
+
requestCtx._onResponseCallbacks = [];
|
|
119
130
|
let result = response;
|
|
120
131
|
for (const callback of callbacks) {
|
|
121
|
-
result = callback(result);
|
|
132
|
+
result = callback(result) ?? result;
|
|
122
133
|
}
|
|
123
134
|
return result;
|
|
124
135
|
}
|
|
@@ -193,6 +204,11 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
193
204
|
): Promise<Response> {
|
|
194
205
|
const url = ctx.url;
|
|
195
206
|
|
|
207
|
+
// Only cache GET requests — mutations and other methods must not be cached
|
|
208
|
+
if (ctx.request.method !== "GET") {
|
|
209
|
+
return next();
|
|
210
|
+
}
|
|
211
|
+
|
|
196
212
|
// Skip RSC action requests (mutations shouldn't be cached)
|
|
197
213
|
if (url.searchParams.has("_rsc_action")) {
|
|
198
214
|
return next();
|
|
@@ -229,18 +245,31 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
229
245
|
const isPartial = url.searchParams.has("_rsc_partial");
|
|
230
246
|
const typeLabel = isPartial ? "RSC" : "HTML";
|
|
231
247
|
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
const clientSegments = url.searchParams.get("_rsc_segments") || "";
|
|
236
|
-
const segmentHash =
|
|
237
|
-
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
238
|
-
const typeSuffix = isPartial ? ":rsc" : ":html";
|
|
239
|
-
const cacheKey = keyGenerator
|
|
240
|
-
? keyGenerator(url) + segmentHash + typeSuffix
|
|
241
|
-
: `${url.pathname}${segmentHash}${typeSuffix}`;
|
|
248
|
+
// Track whether next() has been called so the catch block knows
|
|
249
|
+
// whether it is safe to fall through to the handler.
|
|
250
|
+
let handlerCalled = false;
|
|
242
251
|
|
|
243
252
|
try {
|
|
253
|
+
// Generate cache key inside try so a throwing keyGenerator degrades
|
|
254
|
+
// gracefully to the origin handler instead of rejecting the request.
|
|
255
|
+
// This is a deliberate fail-open-to-origin policy: the fallback is
|
|
256
|
+
// "serve uncached from origin", not "use a different cache key".
|
|
257
|
+
const clientSegments = url.searchParams.get("_rsc_segments") || "";
|
|
258
|
+
const segmentHash =
|
|
259
|
+
isPartial && clientSegments ? `:${hashSegmentIds(clientSegments)}` : "";
|
|
260
|
+
const typeSuffix = isPartial ? ":rsc" : ":html";
|
|
261
|
+
|
|
262
|
+
let searchSuffix = "";
|
|
263
|
+
if (!keyGenerator) {
|
|
264
|
+
const sorted = sortedSearchString(url.searchParams);
|
|
265
|
+
if (sorted) {
|
|
266
|
+
searchSuffix = `?${sorted}`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const cacheKey = keyGenerator
|
|
271
|
+
? keyGenerator(url) + segmentHash + typeSuffix
|
|
272
|
+
: `${url.pathname}${searchSuffix}${segmentHash}${typeSuffix}`;
|
|
244
273
|
// 1. Check cache
|
|
245
274
|
const cached = await store.getResponse(cacheKey);
|
|
246
275
|
|
|
@@ -248,15 +277,10 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
248
277
|
if (!cached.shouldRevalidate) {
|
|
249
278
|
// Fresh hit - return immediately
|
|
250
279
|
log(`[DocumentCache] HIT ${typeLabel}: ${url.pathname}`);
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
response,
|
|
256
|
-
requestCtx._onResponseCallbacks,
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
return response;
|
|
280
|
+
return drainOnResponseCallbacks(
|
|
281
|
+
addCacheStatusHeader(cached.response, "HIT"),
|
|
282
|
+
requestCtx,
|
|
283
|
+
);
|
|
260
284
|
}
|
|
261
285
|
|
|
262
286
|
// Stale hit - return cached response, revalidate in background
|
|
@@ -264,41 +288,33 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
264
288
|
`[DocumentCache] STALE ${typeLabel}: ${url.pathname} (revalidating)`,
|
|
265
289
|
);
|
|
266
290
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
log(
|
|
281
|
-
`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`,
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.error(`[DocumentCache] Revalidation failed:`, error);
|
|
291
|
+
runBackground(requestCtx, async () => {
|
|
292
|
+
try {
|
|
293
|
+
const fresh = await next();
|
|
294
|
+
const directives = shouldCacheResponse(fresh);
|
|
295
|
+
|
|
296
|
+
if (directives) {
|
|
297
|
+
await store.putResponse!(
|
|
298
|
+
cacheKey,
|
|
299
|
+
fresh,
|
|
300
|
+
directives.sMaxAge!,
|
|
301
|
+
directives.staleWhileRevalidate,
|
|
302
|
+
);
|
|
303
|
+
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
286
304
|
}
|
|
287
|
-
})
|
|
288
|
-
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error(`[DocumentCache] Revalidation failed:`, error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
289
309
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
response,
|
|
295
|
-
requestCtx._onResponseCallbacks,
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
return response;
|
|
310
|
+
return drainOnResponseCallbacks(
|
|
311
|
+
addCacheStatusHeader(cached.response, "STALE"),
|
|
312
|
+
requestCtx,
|
|
313
|
+
);
|
|
299
314
|
}
|
|
300
315
|
|
|
301
316
|
// 2. Cache miss - run handler
|
|
317
|
+
handlerCalled = true;
|
|
302
318
|
const originalResponse = await next();
|
|
303
319
|
|
|
304
320
|
// 3. Cache if response has appropriate headers
|
|
@@ -309,24 +325,27 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
309
325
|
`[DocumentCache] MISS ${typeLabel}: ${url.pathname} (caching with s-maxage=${directives.sMaxAge})`,
|
|
310
326
|
);
|
|
311
327
|
|
|
328
|
+
// If the response has no body (e.g., 200 with empty body), skip caching
|
|
329
|
+
if (!originalResponse.body) {
|
|
330
|
+
return originalResponse;
|
|
331
|
+
}
|
|
332
|
+
|
|
312
333
|
// Tee the body so we can return one stream and cache the other
|
|
313
|
-
const [returnStream, cacheStream] = originalResponse.body
|
|
334
|
+
const [returnStream, cacheStream] = originalResponse.body.tee();
|
|
314
335
|
|
|
315
336
|
// Clone response for caching (non-blocking)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
});
|
|
329
|
-
}
|
|
337
|
+
runBackground(requestCtx, async () => {
|
|
338
|
+
try {
|
|
339
|
+
await store.putResponse!(
|
|
340
|
+
cacheKey,
|
|
341
|
+
new Response(cacheStream, originalResponse),
|
|
342
|
+
directives.sMaxAge!,
|
|
343
|
+
directives.staleWhileRevalidate,
|
|
344
|
+
);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
console.error(`[DocumentCache] Cache write failed:`, error);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
330
349
|
|
|
331
350
|
return addCacheStatusHeader(
|
|
332
351
|
new Response(returnStream, originalResponse),
|
|
@@ -338,7 +357,12 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
338
357
|
return originalResponse;
|
|
339
358
|
} catch (error) {
|
|
340
359
|
console.error(`[DocumentCache] Error:`, error);
|
|
341
|
-
|
|
360
|
+
if (handlerCalled) {
|
|
361
|
+
// Post-handler failure (e.g. body.tee()): do not call next() again
|
|
362
|
+
// as that would re-run handler side effects.
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
// Pre-handler failure (cache lookup): degrade gracefully to origin
|
|
342
366
|
return next();
|
|
343
367
|
}
|
|
344
368
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handle Capture
|
|
3
|
+
*
|
|
4
|
+
* Captures handle pushes during cached function execution.
|
|
5
|
+
* Extracted from cache-runtime.ts so tests can import without
|
|
6
|
+
* pulling in @vitejs/plugin-rsc/rsc dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandleStore } from "../server/handle-store.js";
|
|
10
|
+
import type { SegmentHandleData } from "./types.js";
|
|
11
|
+
|
|
12
|
+
export interface HandleCapture {
|
|
13
|
+
data: Record<string, SegmentHandleData>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Active capture tokens per HandleStore.
|
|
18
|
+
*
|
|
19
|
+
* Instead of mutating handleStore.push (which breaks when overlapping
|
|
20
|
+
* captures finish out of order), we install a single interceptor on
|
|
21
|
+
* first use and manage a set of active capture tokens. Each push fans
|
|
22
|
+
* out to every active token. Stopping a capture simply removes the
|
|
23
|
+
* token — order does not matter.
|
|
24
|
+
*/
|
|
25
|
+
const activeCapturesMap = new WeakMap<HandleStore, Set<HandleCapture>>();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* One-time interceptor installation. Wraps the original push so every
|
|
29
|
+
* call fans out to all active capture tokens. Installed once per
|
|
30
|
+
* HandleStore instance; subsequent startHandleCapture calls on the
|
|
31
|
+
* same store just add tokens to the Set.
|
|
32
|
+
*/
|
|
33
|
+
function ensureInterceptorInstalled(handleStore: HandleStore): void {
|
|
34
|
+
if (activeCapturesMap.has(handleStore)) return;
|
|
35
|
+
|
|
36
|
+
const captures = new Set<HandleCapture>();
|
|
37
|
+
activeCapturesMap.set(handleStore, captures);
|
|
38
|
+
|
|
39
|
+
const originalPush = handleStore.push.bind(handleStore);
|
|
40
|
+
handleStore.push = (
|
|
41
|
+
handleName: string,
|
|
42
|
+
segmentId: string,
|
|
43
|
+
value: unknown,
|
|
44
|
+
) => {
|
|
45
|
+
for (const capture of captures) {
|
|
46
|
+
if (!capture.data[segmentId]) {
|
|
47
|
+
capture.data[segmentId] = {};
|
|
48
|
+
}
|
|
49
|
+
if (!capture.data[segmentId][handleName]) {
|
|
50
|
+
capture.data[segmentId][handleName] = [];
|
|
51
|
+
}
|
|
52
|
+
capture.data[segmentId][handleName].push(value);
|
|
53
|
+
}
|
|
54
|
+
originalPush(handleName, segmentId, value);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Start capturing handle pushes for a cached function execution.
|
|
60
|
+
*
|
|
61
|
+
* Concurrency-safe: multiple overlapping captures on the same
|
|
62
|
+
* HandleStore are independent. Each capture registers a token in a
|
|
63
|
+
* Set; stopping removes it. No ordering requirement (LIFO not needed).
|
|
64
|
+
*/
|
|
65
|
+
export function startHandleCapture(handleStore: HandleStore): {
|
|
66
|
+
capture: HandleCapture;
|
|
67
|
+
stop: () => void;
|
|
68
|
+
} {
|
|
69
|
+
ensureInterceptorInstalled(handleStore);
|
|
70
|
+
|
|
71
|
+
const capture: HandleCapture = { data: {} };
|
|
72
|
+
const captures = activeCapturesMap.get(handleStore)!;
|
|
73
|
+
captures.add(capture);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
capture,
|
|
77
|
+
stop() {
|
|
78
|
+
captures.delete(capture);
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
package/src/cache/index.ts
CHANGED
|
@@ -10,21 +10,6 @@
|
|
|
10
10
|
* - CacheScope / createCacheScope - Request-scoped cache provider
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
// Generic cache store types (reserved for future extensibility)
|
|
14
|
-
// These types support caching arbitrary values like Response, Stream, etc.
|
|
15
|
-
// Currently unused - segment caching uses SegmentCacheStore directly.
|
|
16
|
-
export type {
|
|
17
|
-
CacheStore,
|
|
18
|
-
CacheEntry,
|
|
19
|
-
CacheValue,
|
|
20
|
-
CacheValueType,
|
|
21
|
-
CachePutOptions,
|
|
22
|
-
CacheMetadata,
|
|
23
|
-
} from "./types.js";
|
|
24
|
-
|
|
25
|
-
// Generic memory cache (reserved for future extensibility)
|
|
26
|
-
export { MemoryCacheStore } from "./memory-store.js";
|
|
27
|
-
|
|
28
13
|
// Segment cache store types and implementations
|
|
29
14
|
export type {
|
|
30
15
|
SegmentCacheStore,
|