@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
|
@@ -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
|
+
}
|
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
|
-
renderToReadableStream,
|
|
21
|
-
createFromReadableStream,
|
|
22
|
-
createTemporaryReferenceSet,
|
|
23
20
|
encodeReply,
|
|
24
21
|
createClientTemporaryReferenceSet,
|
|
25
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
@@ -28,38 +25,17 @@ import {
|
|
|
28
25
|
isTainted,
|
|
29
26
|
CACHED_FN_SYMBOL,
|
|
30
27
|
isCachedFunction,
|
|
31
|
-
|
|
28
|
+
stampCacheExec,
|
|
29
|
+
unstampCacheExec,
|
|
32
30
|
} from "./taint.js";
|
|
33
31
|
|
|
34
32
|
export { isCachedFunction };
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import
|
|
38
|
-
import type
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Serialization Helpers
|
|
42
|
-
// ============================================================================
|
|
43
|
-
|
|
44
|
-
async function serializeResult(value: unknown): Promise<string | null> {
|
|
45
|
-
try {
|
|
46
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
47
|
-
const stream = renderToReadableStream(value, { temporaryReferences });
|
|
48
|
-
return await streamToString(stream);
|
|
49
|
-
} catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function deserializeResult<T>(encoded: string): Promise<T> {
|
|
55
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
56
|
-
const stream = stringToStream(encoded);
|
|
57
|
-
return createFromReadableStream<T>(stream, { temporaryReferences });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ============================================================================
|
|
61
|
-
// Cache Key Generation
|
|
62
|
-
// ============================================================================
|
|
33
|
+
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
34
|
+
import { createHandleStore } from "../server/handle-store.js";
|
|
35
|
+
import { restoreHandles } from "./handle-snapshot.js";
|
|
36
|
+
import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
|
|
37
|
+
import { sortedSearchString } from "./cache-key-utils.js";
|
|
38
|
+
import { runBackground } from "./background-task.js";
|
|
63
39
|
|
|
64
40
|
/**
|
|
65
41
|
* Convert encodeReply result to a stable string key.
|
|
@@ -72,58 +48,6 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
|
|
|
72
48
|
return text;
|
|
73
49
|
}
|
|
74
50
|
|
|
75
|
-
// ============================================================================
|
|
76
|
-
// Handle Capture
|
|
77
|
-
// ============================================================================
|
|
78
|
-
|
|
79
|
-
interface HandleCapture {
|
|
80
|
-
data: Record<string, SegmentHandleData>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function startHandleCapture(handleStore: HandleStore): HandleCapture {
|
|
84
|
-
const capture: HandleCapture = { data: {} };
|
|
85
|
-
const originalPush = handleStore.push.bind(handleStore);
|
|
86
|
-
|
|
87
|
-
// Intercept push() calls to record them
|
|
88
|
-
handleStore.push = (
|
|
89
|
-
handleName: string,
|
|
90
|
-
segmentId: string,
|
|
91
|
-
value: unknown,
|
|
92
|
-
) => {
|
|
93
|
-
if (!capture.data[segmentId]) {
|
|
94
|
-
capture.data[segmentId] = {};
|
|
95
|
-
}
|
|
96
|
-
if (!capture.data[segmentId][handleName]) {
|
|
97
|
-
capture.data[segmentId][handleName] = [];
|
|
98
|
-
}
|
|
99
|
-
capture.data[segmentId][handleName].push(value);
|
|
100
|
-
// Still call the original so the data flows through normally
|
|
101
|
-
originalPush(handleName, segmentId, value);
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
return capture;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function stopHandleCapture(
|
|
108
|
-
handleStore: HandleStore,
|
|
109
|
-
_capture: HandleCapture,
|
|
110
|
-
): void {
|
|
111
|
-
// Restore original push by deleting the override
|
|
112
|
-
// (the original is on the prototype/closure, our override is an own property)
|
|
113
|
-
delete (handleStore as any).push;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function restoreHandles(
|
|
117
|
-
handles: Record<string, SegmentHandleData>,
|
|
118
|
-
handleStore: HandleStore,
|
|
119
|
-
): void {
|
|
120
|
-
for (const [segId, segHandles] of Object.entries(handles)) {
|
|
121
|
-
if (Object.keys(segHandles).length > 0) {
|
|
122
|
-
handleStore.replaySegmentData(segId, segHandles);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
51
|
// ============================================================================
|
|
128
52
|
// Core: registerCachedFunction
|
|
129
53
|
// ============================================================================
|
|
@@ -144,17 +68,30 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
144
68
|
const wrapped = async function (this: any, ...args: any[]): Promise<any> {
|
|
145
69
|
const requestCtx = getRequestContext();
|
|
146
70
|
const store = requestCtx?._cacheStore;
|
|
147
|
-
const
|
|
71
|
+
const resolvedProfileName = profileName || "default";
|
|
148
72
|
|
|
149
|
-
// Bypass: no store
|
|
150
|
-
if (!store?.getItem
|
|
73
|
+
// Bypass: no store or no getItem support
|
|
74
|
+
if (!store?.getItem) {
|
|
151
75
|
return fn.apply(this, args);
|
|
152
76
|
}
|
|
153
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
|
+
|
|
154
90
|
// Separate tainted args (ctx, env, req) from key-generating args.
|
|
155
|
-
// For tainted objects that carry route context (params, pathname
|
|
156
|
-
// extract
|
|
157
|
-
//
|
|
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.
|
|
158
95
|
const keyArgs: unknown[] = [];
|
|
159
96
|
let hasTaintedArgs = false;
|
|
160
97
|
for (const arg of args) {
|
|
@@ -162,10 +99,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
162
99
|
hasTaintedArgs = true;
|
|
163
100
|
const ctx = arg as any;
|
|
164
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
|
+
}
|
|
165
113
|
keyArgs.push(ctx.pathname, ctx.params);
|
|
166
114
|
if (ctx._responseType) {
|
|
167
115
|
keyArgs.push(ctx._responseType);
|
|
168
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
|
+
}
|
|
169
124
|
}
|
|
170
125
|
} else {
|
|
171
126
|
keyArgs.push(arg);
|
|
@@ -234,24 +189,75 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
234
189
|
restoreHandles(cached.handles, handleStore);
|
|
235
190
|
}
|
|
236
191
|
}
|
|
237
|
-
// Background revalidation
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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);
|
|
252
227
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
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
|
+
});
|
|
255
261
|
return result;
|
|
256
262
|
} catch {
|
|
257
263
|
// Deserialization of stale value failed, fall through
|
|
@@ -261,32 +267,44 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
261
267
|
// Cache miss: execute, serialize, store
|
|
262
268
|
const handleStore = hasTaintedArgs ? requestCtx?._handleStore : undefined;
|
|
263
269
|
let capture: HandleCapture | undefined;
|
|
270
|
+
let stopCapture: (() => void) | undefined;
|
|
264
271
|
if (handleStore && hasTaintedArgs) {
|
|
265
|
-
|
|
272
|
+
const c = startHandleCapture(handleStore);
|
|
273
|
+
capture = c.capture;
|
|
274
|
+
stopCapture = c.stop;
|
|
266
275
|
}
|
|
267
276
|
|
|
268
277
|
// Stamp tainted args so ctx.set(), ctx.header(), etc. throw if called
|
|
269
278
|
// inside the cached function body (those side effects are lost on hit).
|
|
279
|
+
// Uses ref-counted stamp/unstamp so overlapping executions
|
|
280
|
+
// sharing the same ctx don't clear each other's guards.
|
|
270
281
|
const taintedArgs: unknown[] = [];
|
|
271
282
|
for (const arg of args) {
|
|
272
283
|
if (isTainted(arg)) {
|
|
273
|
-
(arg as
|
|
284
|
+
stampCacheExec(arg as object);
|
|
274
285
|
taintedArgs.push(arg);
|
|
275
286
|
}
|
|
276
287
|
}
|
|
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);
|
|
293
|
+
}
|
|
277
294
|
|
|
278
295
|
let result: any;
|
|
279
296
|
try {
|
|
280
297
|
result = await fn.apply(this, args);
|
|
281
298
|
} finally {
|
|
282
|
-
//
|
|
299
|
+
// Decrement ref count; symbol is deleted when it reaches zero
|
|
283
300
|
for (const arg of taintedArgs) {
|
|
284
|
-
|
|
301
|
+
unstampCacheExec(arg as object);
|
|
285
302
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
303
|
+
if (requestCtx) {
|
|
304
|
+
unstampCacheExec(requestCtx as object);
|
|
305
|
+
}
|
|
306
|
+
// Remove this capture token (order-independent, safe for concurrent use)
|
|
307
|
+
stopCapture?.();
|
|
290
308
|
}
|
|
291
309
|
|
|
292
310
|
// Serialize and store — fully non-blocking when waitUntil is available.
|
|
@@ -302,17 +320,12 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
302
320
|
tags: profile.tags,
|
|
303
321
|
});
|
|
304
322
|
}
|
|
305
|
-
} catch {
|
|
306
|
-
|
|
323
|
+
} catch (writeError) {
|
|
324
|
+
requestCtx?._reportBackgroundError?.(writeError, "cache-write");
|
|
307
325
|
}
|
|
308
326
|
};
|
|
309
327
|
|
|
310
|
-
|
|
311
|
-
requestCtx.waitUntil(cacheWrite);
|
|
312
|
-
} else {
|
|
313
|
-
// No waitUntil (e.g. Node.js dev server): run inline as best-effort
|
|
314
|
-
await cacheWrite();
|
|
315
|
-
}
|
|
328
|
+
await runBackground(requestCtx, cacheWrite, true);
|
|
316
329
|
|
|
317
330
|
return result;
|
|
318
331
|
};
|