@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
|
@@ -15,24 +15,35 @@ import type {
|
|
|
15
15
|
SegmentHandleData,
|
|
16
16
|
} from "./types.js";
|
|
17
17
|
import type { RequestContext } from "../server/request-context.js";
|
|
18
|
+
import {
|
|
19
|
+
resolveTtl,
|
|
20
|
+
resolveSwrWindow,
|
|
21
|
+
computeExpiration,
|
|
22
|
+
DEFAULT_FUNCTION_TTL,
|
|
23
|
+
} from "./cache-policy.js";
|
|
18
24
|
|
|
19
25
|
const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
|
|
20
26
|
const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
|
|
21
27
|
const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
|
-
*
|
|
25
|
-
* The registry
|
|
30
|
+
* Get or create a named Map from a globalThis-backed registry.
|
|
31
|
+
* The registry survives HMR; individual stores are keyed by name.
|
|
26
32
|
*/
|
|
27
|
-
function
|
|
28
|
-
let registry = (globalThis as any)[
|
|
29
|
-
| Map<string, Map<string,
|
|
33
|
+
function getNamedMap<V>(registryKey: string, name: string): Map<string, V> {
|
|
34
|
+
let registry = (globalThis as any)[registryKey] as
|
|
35
|
+
| Map<string, Map<string, V>>
|
|
30
36
|
| undefined;
|
|
31
37
|
if (!registry) {
|
|
32
38
|
registry = new Map();
|
|
33
|
-
(globalThis as any)[
|
|
39
|
+
(globalThis as any)[registryKey] = registry;
|
|
34
40
|
}
|
|
35
|
-
|
|
41
|
+
let map = registry.get(name);
|
|
42
|
+
if (!map) {
|
|
43
|
+
map = new Map<string, V>();
|
|
44
|
+
registry.set(name, map);
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
36
47
|
}
|
|
37
48
|
|
|
38
49
|
interface CachedResponseEntry {
|
|
@@ -47,37 +58,7 @@ interface CachedItemEntry {
|
|
|
47
58
|
value: string;
|
|
48
59
|
handles?: Record<string, SegmentHandleData>;
|
|
49
60
|
expiresAt: number;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Returns the globalThis-backed registry of named item cache Maps (for "use cache").
|
|
54
|
-
*/
|
|
55
|
-
function getItemCacheRegistry(): Map<string, Map<string, CachedItemEntry>> {
|
|
56
|
-
let registry = (globalThis as any)[ITEM_CACHE_REGISTRY_KEY] as
|
|
57
|
-
| Map<string, Map<string, CachedItemEntry>>
|
|
58
|
-
| undefined;
|
|
59
|
-
if (!registry) {
|
|
60
|
-
registry = new Map();
|
|
61
|
-
(globalThis as any)[ITEM_CACHE_REGISTRY_KEY] = registry;
|
|
62
|
-
}
|
|
63
|
-
return registry;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Returns the globalThis-backed registry of named response cache Maps.
|
|
68
|
-
*/
|
|
69
|
-
function getResponseCacheRegistry(): Map<
|
|
70
|
-
string,
|
|
71
|
-
Map<string, CachedResponseEntry>
|
|
72
|
-
> {
|
|
73
|
-
let registry = (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY] as
|
|
74
|
-
| Map<string, Map<string, CachedResponseEntry>>
|
|
75
|
-
| undefined;
|
|
76
|
-
if (!registry) {
|
|
77
|
-
registry = new Map();
|
|
78
|
-
(globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY] = registry;
|
|
79
|
-
}
|
|
80
|
-
return registry;
|
|
61
|
+
staleAt: number;
|
|
81
62
|
}
|
|
82
63
|
|
|
83
64
|
/**
|
|
@@ -122,7 +103,7 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
|
122
103
|
* @example
|
|
123
104
|
* ```typescript
|
|
124
105
|
* keyGenerator: (ctx, defaultKey) => {
|
|
125
|
-
* const locale =
|
|
106
|
+
* const locale = cookies().get('locale')?.value || 'en';
|
|
126
107
|
* return `${locale}:${defaultKey}`;
|
|
127
108
|
* }
|
|
128
109
|
* ```
|
|
@@ -172,29 +153,18 @@ export class MemorySegmentCacheStore<
|
|
|
172
153
|
if (options?.name != null) {
|
|
173
154
|
// Named stores use the globalThis registry so data survives HMR.
|
|
174
155
|
// Each name gets its own isolated Map.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
responseRegistry.set(options.name, responseMap);
|
|
188
|
-
}
|
|
189
|
-
this.responseCache = responseMap;
|
|
190
|
-
|
|
191
|
-
const itemRegistry = getItemCacheRegistry();
|
|
192
|
-
let itemMap = itemRegistry.get(options.name);
|
|
193
|
-
if (!itemMap) {
|
|
194
|
-
itemMap = new Map<string, CachedItemEntry>();
|
|
195
|
-
itemRegistry.set(options.name, itemMap);
|
|
196
|
-
}
|
|
197
|
-
this.itemCache = itemMap;
|
|
156
|
+
this.cache = getNamedMap<CachedEntryData>(
|
|
157
|
+
CACHE_REGISTRY_KEY,
|
|
158
|
+
options.name,
|
|
159
|
+
);
|
|
160
|
+
this.responseCache = getNamedMap<CachedResponseEntry>(
|
|
161
|
+
RESPONSE_CACHE_REGISTRY_KEY,
|
|
162
|
+
options.name,
|
|
163
|
+
);
|
|
164
|
+
this.itemCache = getNamedMap<CachedItemEntry>(
|
|
165
|
+
ITEM_CACHE_REGISTRY_KEY,
|
|
166
|
+
options.name,
|
|
167
|
+
);
|
|
198
168
|
} else {
|
|
199
169
|
// Unnamed stores get a plain instance-level Map (no globalThis sharing).
|
|
200
170
|
this.cache = new Map<string, CachedEntryData>();
|
|
@@ -281,9 +251,8 @@ export class MemorySegmentCacheStore<
|
|
|
281
251
|
headers.push([name, value]);
|
|
282
252
|
});
|
|
283
253
|
|
|
284
|
-
const swrWindow = swr
|
|
285
|
-
const staleAt =
|
|
286
|
-
const expiresAt = staleAt + swrWindow * 1000;
|
|
254
|
+
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
255
|
+
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
287
256
|
|
|
288
257
|
this.responseCache.set(key, {
|
|
289
258
|
body,
|
|
@@ -298,15 +267,17 @@ export class MemorySegmentCacheStore<
|
|
|
298
267
|
const cached = this.itemCache.get(key);
|
|
299
268
|
if (!cached) return null;
|
|
300
269
|
|
|
301
|
-
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
if (now > cached.expiresAt) {
|
|
302
272
|
this.itemCache.delete(key);
|
|
303
273
|
return null;
|
|
304
274
|
}
|
|
305
275
|
|
|
276
|
+
const isStale = now > cached.staleAt;
|
|
306
277
|
return {
|
|
307
278
|
value: cached.value,
|
|
308
279
|
handles: cached.handles,
|
|
309
|
-
shouldRevalidate:
|
|
280
|
+
shouldRevalidate: isStale,
|
|
310
281
|
};
|
|
311
282
|
}
|
|
312
283
|
|
|
@@ -315,11 +286,14 @@ export class MemorySegmentCacheStore<
|
|
|
315
286
|
value: string,
|
|
316
287
|
options?: CacheItemOptions,
|
|
317
288
|
): Promise<void> {
|
|
318
|
-
const ttl = options?.ttl
|
|
289
|
+
const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
290
|
+
const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
|
|
291
|
+
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
319
292
|
this.itemCache.set(key, {
|
|
320
293
|
value,
|
|
321
294
|
handles: options?.handles,
|
|
322
|
-
expiresAt
|
|
295
|
+
expiresAt,
|
|
296
|
+
staleAt,
|
|
323
297
|
});
|
|
324
298
|
}
|
|
325
299
|
|
|
@@ -15,23 +15,58 @@ export interface CacheProfile {
|
|
|
15
15
|
tags?: string[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
|
|
19
|
+
|
|
18
20
|
let _profiles: Record<string, CacheProfile> = {
|
|
19
|
-
default:
|
|
21
|
+
default: DEFAULT_PROFILE,
|
|
20
22
|
};
|
|
21
23
|
|
|
24
|
+
const PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
25
|
+
|
|
22
26
|
/**
|
|
23
|
-
*
|
|
27
|
+
* Validate and merge user profiles with the default profile.
|
|
28
|
+
* Returns a new object suitable for both DSL-time and request-scoped use.
|
|
29
|
+
*
|
|
30
|
+
* Used by createRouter() to compute the resolved profile map once,
|
|
31
|
+
* stored on the router instance and passed to every request context.
|
|
24
32
|
*/
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
33
|
+
export function resolveCacheProfiles(
|
|
34
|
+
profiles?: Record<string, CacheProfile>,
|
|
35
|
+
): Record<string, CacheProfile> {
|
|
36
|
+
const merged: Record<string, CacheProfile> = {
|
|
37
|
+
default: DEFAULT_PROFILE,
|
|
38
|
+
};
|
|
39
|
+
if (profiles) {
|
|
40
|
+
for (const name of Object.keys(profiles)) {
|
|
41
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Invalid cache profile name "${name}". ` +
|
|
44
|
+
`Profile names must match [a-zA-Z0-9_-]+.`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
merged[name] = profiles[name];
|
|
48
|
+
}
|
|
30
49
|
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Set all cache profiles in the global registry.
|
|
55
|
+
* Called by createRouter() at startup for DSL-time resolution
|
|
56
|
+
* (cache("profileName") reads from this during route definition).
|
|
57
|
+
*
|
|
58
|
+
* WARNING: This is global mutable state. It exists only for DSL-time
|
|
59
|
+
* reads. Runtime resolution (registerCachedFunction) uses request-scoped
|
|
60
|
+
* profiles and does NOT read from this registry.
|
|
61
|
+
*/
|
|
62
|
+
export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
|
|
63
|
+
_profiles = resolveCacheProfiles(profiles);
|
|
31
64
|
}
|
|
32
65
|
|
|
33
66
|
/**
|
|
34
|
-
* Get a cache profile by name
|
|
67
|
+
* Get a cache profile by name from the global registry.
|
|
68
|
+
* Used only at DSL-time (cache("profileName") inside urls() evaluation).
|
|
69
|
+
* Runtime code uses request-scoped profiles instead.
|
|
35
70
|
*/
|
|
36
71
|
export function getCacheProfile(name: string): CacheProfile | undefined {
|
|
37
72
|
return _profiles[name];
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SWR Read-Through Engine
|
|
3
|
+
*
|
|
4
|
+
* Generic read-through cache with stale-while-revalidate support
|
|
5
|
+
* for item-level caching (getItem/setItem).
|
|
6
|
+
*
|
|
7
|
+
* Flow:
|
|
8
|
+
* 1. Lookup cached item by key
|
|
9
|
+
* 2. Fresh hit → deserialize, return
|
|
10
|
+
* 3. Stale hit → deserialize, return, revalidate in background
|
|
11
|
+
* 4. Miss → execute, cache write (blocking when no waitUntil), return
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { CacheItemResult, CacheItemOptions } from "./types.js";
|
|
15
|
+
import { runBackground } from "./background-task.js";
|
|
16
|
+
|
|
17
|
+
interface WaitUntilHost {
|
|
18
|
+
waitUntil?: (fn: () => Promise<void>) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ReadThroughItemConfig<T> {
|
|
22
|
+
/** Retrieve a cached item by key */
|
|
23
|
+
getItem: (key: string) => Promise<CacheItemResult | null>;
|
|
24
|
+
/** Store a serialized item by key */
|
|
25
|
+
setItem: (
|
|
26
|
+
key: string,
|
|
27
|
+
value: string,
|
|
28
|
+
options?: CacheItemOptions,
|
|
29
|
+
) => Promise<void>;
|
|
30
|
+
/** Cache key */
|
|
31
|
+
key: string;
|
|
32
|
+
/** Execute the underlying function/loader on miss or revalidation */
|
|
33
|
+
execute: () => Promise<T>;
|
|
34
|
+
/** Serialize result for storage. Return null to skip caching. */
|
|
35
|
+
serialize: (data: T) => Promise<string | null>;
|
|
36
|
+
/** Deserialize cached value back to the original type */
|
|
37
|
+
deserialize: (value: string) => Promise<T>;
|
|
38
|
+
/** Options passed to setItem on cache write */
|
|
39
|
+
storeOptions: CacheItemOptions;
|
|
40
|
+
/** Called on fresh cache hit (before returning data) */
|
|
41
|
+
onHit?: (cached: CacheItemResult) => void;
|
|
42
|
+
/** Called on stale cache hit (before scheduling background revalidation) */
|
|
43
|
+
onStale?: (cached: CacheItemResult) => void;
|
|
44
|
+
/** Called on cache miss (before executing) */
|
|
45
|
+
onMiss?: () => void;
|
|
46
|
+
/** Called after successful cache write */
|
|
47
|
+
onCached?: () => void;
|
|
48
|
+
/** Host with optional waitUntil for background tasks */
|
|
49
|
+
host?: WaitUntilHost | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read-through cache with SWR support for item-level caching.
|
|
54
|
+
*
|
|
55
|
+
* On fresh hit: returns deserialized cached data.
|
|
56
|
+
* On stale hit: returns stale data, schedules background revalidation.
|
|
57
|
+
* On miss: executes, writes to cache (blocking when no waitUntil), returns.
|
|
58
|
+
*/
|
|
59
|
+
export async function readThroughItem<T>(
|
|
60
|
+
config: ReadThroughItemConfig<T>,
|
|
61
|
+
): Promise<T> {
|
|
62
|
+
const {
|
|
63
|
+
getItem,
|
|
64
|
+
setItem,
|
|
65
|
+
key,
|
|
66
|
+
execute,
|
|
67
|
+
serialize,
|
|
68
|
+
deserialize,
|
|
69
|
+
storeOptions,
|
|
70
|
+
onHit,
|
|
71
|
+
onStale,
|
|
72
|
+
onMiss,
|
|
73
|
+
onCached,
|
|
74
|
+
host,
|
|
75
|
+
} = config;
|
|
76
|
+
|
|
77
|
+
// Cache lookup
|
|
78
|
+
try {
|
|
79
|
+
const cached = await getItem(key);
|
|
80
|
+
|
|
81
|
+
if (cached) {
|
|
82
|
+
const data = await deserialize(cached.value);
|
|
83
|
+
|
|
84
|
+
if (!cached.shouldRevalidate) {
|
|
85
|
+
onHit?.(cached);
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Stale hit — return stale data, revalidate in background
|
|
90
|
+
onStale?.(cached);
|
|
91
|
+
runBackground(
|
|
92
|
+
host,
|
|
93
|
+
async () => {
|
|
94
|
+
try {
|
|
95
|
+
const fresh = await execute();
|
|
96
|
+
const serialized = await serialize(fresh);
|
|
97
|
+
if (serialized !== null) {
|
|
98
|
+
await setItem(key, serialized, storeOptions);
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Background revalidation failed silently
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
true,
|
|
105
|
+
);
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// Cache lookup failed, fall through to fresh execution
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cache miss
|
|
113
|
+
onMiss?.();
|
|
114
|
+
const data = await execute();
|
|
115
|
+
|
|
116
|
+
// Non-blocking cache write (blocks when no waitUntil)
|
|
117
|
+
await runBackground(
|
|
118
|
+
host,
|
|
119
|
+
async () => {
|
|
120
|
+
try {
|
|
121
|
+
const serialized = await serialize(data);
|
|
122
|
+
if (serialized !== null) {
|
|
123
|
+
await setItem(key, serialized, storeOptions);
|
|
124
|
+
onCached?.();
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Cache write failed silently
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
true,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
return data;
|
|
134
|
+
}
|
|
@@ -62,6 +62,10 @@ export function stringToStream(str: string): ReadableStream<Uint8Array> {
|
|
|
62
62
|
/**
|
|
63
63
|
* RSC-serialize a value using React Server Components stream.
|
|
64
64
|
* Used for serializing loaderData, layout, loading components etc.
|
|
65
|
+
*
|
|
66
|
+
* Returns undefined for null/undefined inputs (component fields that are absent).
|
|
67
|
+
* For contexts where null is a valid result (loader caching, "use cache"),
|
|
68
|
+
* use serializeResult() instead which preserves null through RSC Flight.
|
|
65
69
|
*/
|
|
66
70
|
export async function rscSerialize(
|
|
67
71
|
value: unknown,
|
|
@@ -87,19 +91,49 @@ export async function rscDeserialize<T>(
|
|
|
87
91
|
}
|
|
88
92
|
|
|
89
93
|
// ============================================================================
|
|
90
|
-
//
|
|
94
|
+
// Null-Preserving RSC Serialization (for caching)
|
|
91
95
|
// ============================================================================
|
|
92
96
|
|
|
93
97
|
/**
|
|
94
|
-
* RSC-
|
|
95
|
-
*
|
|
98
|
+
* RSC-serialize any value including null.
|
|
99
|
+
* Unlike rscSerialize(), this does NOT skip null — it serializes it through
|
|
100
|
+
* RSC Flight so that a loader returning null produces a valid cached entry
|
|
101
|
+
* rather than a permanent cache miss.
|
|
102
|
+
*
|
|
103
|
+
* Returns null only on serialization failure.
|
|
96
104
|
*/
|
|
97
|
-
export async function
|
|
105
|
+
export async function serializeResult(value: unknown): Promise<string | null> {
|
|
106
|
+
try {
|
|
107
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
108
|
+
const stream = renderToReadableStream(value, { temporaryReferences });
|
|
109
|
+
return await streamToString(stream);
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* RSC-deserialize a cached result string.
|
|
117
|
+
* Counterpart to serializeResult() — always receives a non-empty string.
|
|
118
|
+
*/
|
|
119
|
+
export async function deserializeResult<T>(encoded: string): Promise<T> {
|
|
98
120
|
const temporaryReferences = createTemporaryReferenceSet();
|
|
99
121
|
const stream = stringToStream(encoded);
|
|
100
|
-
return createFromReadableStream(stream, { temporaryReferences });
|
|
122
|
+
return createFromReadableStream<T>(stream, { temporaryReferences });
|
|
101
123
|
}
|
|
102
124
|
|
|
125
|
+
// ============================================================================
|
|
126
|
+
// Public API
|
|
127
|
+
// ============================================================================
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* RSC-deserialize a single encoded component string back to a React element.
|
|
131
|
+
* Used by the static handler runtime to revive pre-rendered components.
|
|
132
|
+
* Identical to deserializeResult<unknown>.
|
|
133
|
+
*/
|
|
134
|
+
export const deserializeComponent: (encoded: string) => Promise<unknown> =
|
|
135
|
+
deserializeResult;
|
|
136
|
+
|
|
103
137
|
/**
|
|
104
138
|
* Serialize segments for storage.
|
|
105
139
|
* Each segment's component, layout, loading, and loaderData are RSC-serialized.
|
|
@@ -108,79 +142,76 @@ export async function deserializeComponent(encoded: string): Promise<unknown> {
|
|
|
108
142
|
export async function serializeSegments(
|
|
109
143
|
segments: ResolvedSegment[],
|
|
110
144
|
): Promise<SerializedSegmentData[]> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const encodedLoaderDataPromise = await rscSerialize(
|
|
157
|
-
loaderDataPromiseResolved,
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
serialized.push({
|
|
161
|
-
encoded,
|
|
162
|
-
encodedLayout,
|
|
163
|
-
encodedLoading,
|
|
164
|
-
encodedLoaderData,
|
|
165
|
-
encodedLoaderDataPromise,
|
|
166
|
-
metadata: {
|
|
167
|
-
id: segment.id,
|
|
168
|
-
type: segment.type,
|
|
169
|
-
namespace: segment.namespace,
|
|
170
|
-
index: segment.index,
|
|
171
|
-
params: segment.params,
|
|
172
|
-
slot: segment.slot,
|
|
173
|
-
belongsToRoute: segment.belongsToRoute,
|
|
174
|
-
layoutName: segment.layoutName,
|
|
175
|
-
parallelName: segment.parallelName,
|
|
176
|
-
loaderId: segment.loaderId,
|
|
177
|
-
loaderIds: segment.loaderIds,
|
|
178
|
-
transition: segment.transition,
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
}
|
|
145
|
+
return Promise.all(
|
|
146
|
+
segments.map(async (segment): Promise<SerializedSegmentData> => {
|
|
147
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
148
|
+
|
|
149
|
+
// Await component if it's a Promise (intercepts with loading keep component as Promise)
|
|
150
|
+
const componentResolved =
|
|
151
|
+
segment.component instanceof Promise
|
|
152
|
+
? await segment.component
|
|
153
|
+
: segment.component;
|
|
154
|
+
|
|
155
|
+
// Serialize the component to RSC stream
|
|
156
|
+
const stream = renderToReadableStream(componentResolved, {
|
|
157
|
+
temporaryReferences,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// RSC-serialize loading: "null" string distinguishes explicit null from undefined
|
|
161
|
+
const encodedLoading =
|
|
162
|
+
segment.loading !== undefined
|
|
163
|
+
? segment.loading === null
|
|
164
|
+
? "null"
|
|
165
|
+
: await rscSerialize(segment.loading)
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
168
|
+
// Await loaderData / loaderDataPromise if they're Promises
|
|
169
|
+
const loaderDataResolved =
|
|
170
|
+
segment.loaderData instanceof Promise
|
|
171
|
+
? await segment.loaderData
|
|
172
|
+
: segment.loaderData;
|
|
173
|
+
const loaderDataPromiseResolved =
|
|
174
|
+
segment.loaderDataPromise instanceof Promise
|
|
175
|
+
? await segment.loaderDataPromise
|
|
176
|
+
: segment.loaderDataPromise;
|
|
177
|
+
|
|
178
|
+
// Parallelize stream-to-string and RSC serialization of sub-fields
|
|
179
|
+
const [
|
|
180
|
+
encoded,
|
|
181
|
+
encodedLayout,
|
|
182
|
+
encodedLoaderData,
|
|
183
|
+
encodedLoaderDataPromise,
|
|
184
|
+
] = await Promise.all([
|
|
185
|
+
streamToString(stream),
|
|
186
|
+
segment.layout ? rscSerialize(segment.layout) : undefined,
|
|
187
|
+
rscSerialize(loaderDataResolved),
|
|
188
|
+
rscSerialize(loaderDataPromiseResolved),
|
|
189
|
+
]);
|
|
182
190
|
|
|
183
|
-
|
|
191
|
+
return {
|
|
192
|
+
encoded,
|
|
193
|
+
encodedLayout,
|
|
194
|
+
encodedLoading,
|
|
195
|
+
encodedLoaderData,
|
|
196
|
+
encodedLoaderDataPromise,
|
|
197
|
+
metadata: {
|
|
198
|
+
id: segment.id,
|
|
199
|
+
type: segment.type,
|
|
200
|
+
namespace: segment.namespace,
|
|
201
|
+
index: segment.index,
|
|
202
|
+
params: segment.params,
|
|
203
|
+
slot: segment.slot,
|
|
204
|
+
belongsToRoute: segment.belongsToRoute,
|
|
205
|
+
layoutName: segment.layoutName,
|
|
206
|
+
parallelName: segment.parallelName,
|
|
207
|
+
loaderId: segment.loaderId,
|
|
208
|
+
loaderIds: segment.loaderIds,
|
|
209
|
+
transition: segment.transition,
|
|
210
|
+
mountPath: segment.mountPath,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
184
215
|
}
|
|
185
216
|
|
|
186
217
|
/**
|
|
@@ -190,44 +221,36 @@ export async function serializeSegments(
|
|
|
190
221
|
export async function deserializeSegments(
|
|
191
222
|
data: SerializedSegmentData[],
|
|
192
223
|
): Promise<ResolvedSegment[]> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
layout,
|
|
226
|
-
loading: loadingData,
|
|
227
|
-
loaderData,
|
|
228
|
-
loaderDataPromise,
|
|
229
|
-
} as ResolvedSegment);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return segments;
|
|
224
|
+
return Promise.all(
|
|
225
|
+
data.map(async (item): Promise<ResolvedSegment> => {
|
|
226
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
227
|
+
|
|
228
|
+
// Handle the "null" sentinel for loading before RSC deserialization.
|
|
229
|
+
// During serialization, loading: null is stored as the string "null" to
|
|
230
|
+
// distinguish it from undefined.
|
|
231
|
+
const loadingIsNullSentinel = item.encodedLoading === "null";
|
|
232
|
+
|
|
233
|
+
const [component, layout, loaderData, loaderDataPromise, loadingData] =
|
|
234
|
+
await Promise.all([
|
|
235
|
+
createFromReadableStream(stringToStream(item.encoded), {
|
|
236
|
+
temporaryReferences,
|
|
237
|
+
}),
|
|
238
|
+
rscDeserialize(item.encodedLayout),
|
|
239
|
+
rscDeserialize(item.encodedLoaderData),
|
|
240
|
+
rscDeserialize(item.encodedLoaderDataPromise),
|
|
241
|
+
loadingIsNullSentinel
|
|
242
|
+
? (null as any)
|
|
243
|
+
: rscDeserialize(item.encodedLoading),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
...item.metadata,
|
|
248
|
+
component,
|
|
249
|
+
layout,
|
|
250
|
+
loading: loadingData,
|
|
251
|
+
loaderData,
|
|
252
|
+
loaderDataPromise,
|
|
253
|
+
} as ResolvedSegment;
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
233
256
|
}
|