@rangojs/router 0.0.0-experimental.6 → 0.0.0-experimental.64e3ae06
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/AGENTS.md +9 -0
- package/README.md +884 -4
- package/dist/bin/rango.js +1606 -0
- package/dist/vite/index.js +4567 -769
- package/package.json +77 -58
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +262 -0
- package/skills/caching/SKILL.md +85 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +167 -0
- package/skills/hooks/SKILL.md +334 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +131 -8
- package/skills/layout/SKILL.md +100 -3
- package/skills/links/SKILL.md +89 -30
- package/skills/loader/SKILL.md +388 -38
- package/skills/middleware/SKILL.md +171 -34
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +78 -1
- package/skills/prerender/SKILL.md +643 -0
- package/skills/rango/SKILL.md +85 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +226 -14
- package/skills/router-setup/SKILL.md +123 -30
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +318 -89
- package/skills/use-cache/SKILL.md +324 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +321 -0
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/event-controller.ts +87 -64
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +282 -557
- package/src/browser/navigation-client.ts +170 -71
- package/src/browser/navigation-store.ts +33 -50
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +292 -310
- package/src/browser/prefetch/cache.ts +206 -0
- package/src/browser/prefetch/fetch.ts +145 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +128 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +193 -73
- package/src/browser/react/NavigationProvider.tsx +78 -11
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +12 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- 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 +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +32 -79
- package/src/browser/react/use-href.tsx +2 -2
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +22 -63
- package/src/browser/react/use-params.ts +65 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +63 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +80 -97
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +167 -55
- package/src/browser/scroll-restoration.ts +117 -44
- package/src/browser/segment-reconciler.ts +216 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +504 -599
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +118 -47
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +36 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +265 -0
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +411 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +479 -0
- package/src/build/route-types/scan-filter.ts +78 -0
- package/src/build/runtime-discovery.ts +231 -0
- 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 +338 -0
- package/src/cache/cache-scope.ts +122 -305
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +101 -72
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +98 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +106 -126
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +86 -0
- package/src/debug.ts +17 -7
- package/src/errors.ts +108 -2
- package/src/handle.ts +15 -29
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +165 -0
- package/src/host/errors.ts +97 -0
- package/src/host/index.ts +53 -0
- package/src/host/pattern-matcher.ts +214 -0
- package/src/host/router.ts +352 -0
- package/src/host/testing.ts +79 -0
- package/src/host/types.ts +146 -0
- package/src/host/utils.ts +25 -0
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +153 -19
- package/src/index.ts +211 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -147
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +37 -0
- package/src/prerender/store.ts +185 -0
- package/src/prerender.ts +463 -0
- package/src/reverse.ts +330 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +934 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +430 -0
- package/src/route-definition/index.ts +52 -0
- package/src/route-definition/redirect.ts +93 -0
- package/src/route-definition.ts +1 -1428
- package/src/route-map-builder.ts +217 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +59 -8
- package/src/router/content-negotiation.ts +116 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +374 -81
- package/src/router/intercept-resolution.ts +397 -0
- package/src/router/lazy-includes.ts +236 -0
- package/src/router/loader-resolution.ts +215 -122
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +150 -35
- package/src/router/match-api.ts +620 -0
- package/src/router/match-context.ts +5 -3
- package/src/router/match-handlers.ts +440 -0
- package/src/router/match-middleware/background-revalidation.ts +80 -93
- package/src/router/match-middleware/cache-lookup.ts +382 -9
- package/src/router/match-middleware/cache-store.ts +51 -22
- package/src/router/match-middleware/intercept-resolution.ts +55 -17
- package/src/router/match-middleware/segment-resolution.ts +25 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +34 -28
- package/src/router/metrics.ts +235 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +222 -0
- package/src/router/middleware.ts +327 -369
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +402 -0
- package/src/router/preview-match.ts +170 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/router-context.ts +40 -21
- package/src/router/router-interfaces.ts +452 -0
- package/src/router/router-options.ts +592 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +570 -0
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +198 -0
- package/src/router/segment-resolution/revalidation.ts +1242 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -0
- package/src/router/segment-wrappers.ts +291 -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 +239 -0
- package/src/router/types.ts +77 -3
- package/src/router.ts +665 -4182
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +764 -754
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +209 -0
- package/src/rsc/manifest-init.ts +86 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +379 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +237 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +348 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +38 -11
- package/src/search-params.ts +230 -0
- package/src/segment-system.tsx +26 -14
- package/src/server/context.ts +182 -51
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +94 -15
- package/src/server/loader-registry.ts +15 -56
- package/src/server/request-context.ts +439 -73
- package/src/server.ts +35 -128
- package/src/ssr/index.tsx +100 -31
- package/src/static-handler.ts +114 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +687 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +183 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +109 -0
- package/src/types/segments.ts +148 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +197 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +339 -0
- package/src/urls/path-helper.ts +329 -0
- package/src/urls/pattern-types.ts +95 -0
- package/src/urls/response-types.ts +106 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -802
- package/src/use-loader.tsx +85 -77
- package/src/vite/discovery/bundle-postprocess.ts +184 -0
- package/src/vite/discovery/discover-routers.ts +344 -0
- package/src/vite/discovery/prerender-collection.ts +385 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +47 -0
- package/src/vite/discovery/state.ts +108 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +11 -782
- package/src/vite/plugin-types.ts +48 -0
- package/src/vite/plugins/cjs-to-esm.ts +93 -0
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +105 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +287 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +179 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +110 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +569 -0
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +323 -0
- package/src/vite/plugins/version-injector.ts +83 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +27 -16
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +445 -0
- package/src/vite/router-discovery.ts +777 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/{package-resolution.ts → utils/package-resolution.ts} +25 -29
- package/src/vite/utils/prerender-utils.ts +189 -0
- package/src/vite/utils/shared-utils.ts +169 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- 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/href.ts +0 -255
- package/src/server/route-manifest-cache.ts +0 -173
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Profile Registry
|
|
3
|
+
*
|
|
4
|
+
* Named cache profiles for "use cache" directive.
|
|
5
|
+
* Profiles define TTL, SWR, and optional default tags.
|
|
6
|
+
* Set by createRouter() at startup, read by registerCachedFunction() at runtime.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface CacheProfile {
|
|
10
|
+
/** Time-to-live in seconds */
|
|
11
|
+
ttl: number;
|
|
12
|
+
/** Stale-while-revalidate window in seconds */
|
|
13
|
+
swr?: number;
|
|
14
|
+
/** Default cache tags for invalidation */
|
|
15
|
+
tags?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
|
|
19
|
+
|
|
20
|
+
let _profiles: Record<string, CacheProfile> = {
|
|
21
|
+
default: DEFAULT_PROFILE,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
25
|
+
|
|
26
|
+
/**
|
|
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.
|
|
32
|
+
*/
|
|
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
|
+
}
|
|
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);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
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.
|
|
70
|
+
*/
|
|
71
|
+
export function getCacheProfile(name: string): CacheProfile | undefined {
|
|
72
|
+
return _profiles[name];
|
|
73
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Segment Codec
|
|
3
|
+
*
|
|
4
|
+
* RSC serialization/deserialization for cached segments.
|
|
5
|
+
* Handles the Flight protocol stream <-> string conversion
|
|
6
|
+
* and the segment-level encode/decode lifecycle.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
10
|
+
|
|
11
|
+
import type { ResolvedSegment } from "../types.js";
|
|
12
|
+
import type { SerializedSegmentData } from "./types.js";
|
|
13
|
+
import {
|
|
14
|
+
renderToReadableStream,
|
|
15
|
+
createTemporaryReferenceSet,
|
|
16
|
+
} from "@vitejs/plugin-rsc/rsc";
|
|
17
|
+
import { createFromReadableStream } from "@vitejs/plugin-rsc/rsc";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Stream Utilities (internal)
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a ReadableStream to a string.
|
|
25
|
+
*/
|
|
26
|
+
export async function streamToString(
|
|
27
|
+
stream: ReadableStream<Uint8Array>,
|
|
28
|
+
): Promise<string> {
|
|
29
|
+
const reader = stream.getReader();
|
|
30
|
+
const decoder = new TextDecoder();
|
|
31
|
+
let result = "";
|
|
32
|
+
|
|
33
|
+
while (true) {
|
|
34
|
+
const { done, value } = await reader.read();
|
|
35
|
+
if (done) break;
|
|
36
|
+
result += decoder.decode(value, { stream: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
result += decoder.decode(); // flush
|
|
40
|
+
return result;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Convert a string to a ReadableStream.
|
|
45
|
+
*/
|
|
46
|
+
export function stringToStream(str: string): ReadableStream<Uint8Array> {
|
|
47
|
+
const encoder = new TextEncoder();
|
|
48
|
+
const uint8 = encoder.encode(str);
|
|
49
|
+
|
|
50
|
+
return new ReadableStream({
|
|
51
|
+
start(controller) {
|
|
52
|
+
controller.enqueue(uint8);
|
|
53
|
+
controller.close();
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// RSC Serialization Primitives (internal)
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* RSC-serialize a value using React Server Components stream.
|
|
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.
|
|
69
|
+
*/
|
|
70
|
+
export async function rscSerialize(
|
|
71
|
+
value: unknown,
|
|
72
|
+
): Promise<string | undefined> {
|
|
73
|
+
if (value === undefined || value === null) return undefined;
|
|
74
|
+
|
|
75
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
76
|
+
const stream = renderToReadableStream(value, { temporaryReferences });
|
|
77
|
+
return streamToString(stream);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* RSC-deserialize a value from a stored string.
|
|
82
|
+
*/
|
|
83
|
+
export async function rscDeserialize<T>(
|
|
84
|
+
encoded: string | undefined,
|
|
85
|
+
): Promise<T | undefined> {
|
|
86
|
+
if (!encoded) return undefined;
|
|
87
|
+
|
|
88
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
89
|
+
const stream = stringToStream(encoded);
|
|
90
|
+
return createFromReadableStream<T>(stream, { temporaryReferences });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// Null-Preserving RSC Serialization (for caching)
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
/**
|
|
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.
|
|
104
|
+
*/
|
|
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> {
|
|
120
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
121
|
+
const stream = stringToStream(encoded);
|
|
122
|
+
return createFromReadableStream<T>(stream, { temporaryReferences });
|
|
123
|
+
}
|
|
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
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Serialize segments for storage.
|
|
139
|
+
* Each segment's component, layout, loading, and loaderData are RSC-serialized.
|
|
140
|
+
* Metadata is preserved as-is.
|
|
141
|
+
*/
|
|
142
|
+
export async function serializeSegments(
|
|
143
|
+
segments: ResolvedSegment[],
|
|
144
|
+
): Promise<SerializedSegmentData[]> {
|
|
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
|
+
]);
|
|
190
|
+
|
|
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
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Deserialize segments from storage.
|
|
219
|
+
* Reconstructs ResolvedSegment objects from RSC-serialized data.
|
|
220
|
+
*/
|
|
221
|
+
export async function deserializeSegments(
|
|
222
|
+
data: SerializedSegmentData[],
|
|
223
|
+
): Promise<ResolvedSegment[]> {
|
|
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
|
+
);
|
|
256
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Taint symbol for request-scoped objects.
|
|
3
|
+
*
|
|
4
|
+
* Objects branded with NOCACHE_SYMBOL (ctx, env, req) are excluded from
|
|
5
|
+
* "use cache" cache keys and trigger handle capture mode so that side
|
|
6
|
+
* effects (breadcrumbs, metadata) are recorded and replayed on cache hit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const NOCACHE_SYMBOL: unique symbol = Symbol.for("rango:nocache") as any;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Check if a value is tainted (request-scoped, should not be in cache key).
|
|
13
|
+
*/
|
|
14
|
+
export function isTainted(value: unknown): boolean {
|
|
15
|
+
return (
|
|
16
|
+
value !== null &&
|
|
17
|
+
value !== undefined &&
|
|
18
|
+
typeof value === "object" &&
|
|
19
|
+
(NOCACHE_SYMBOL as symbol) in (value as Record<symbol, unknown>)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Symbol stamped on tainted ctx during "use cache" function execution.
|
|
25
|
+
* cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
|
|
26
|
+
* throw if present — reads would cache per-request data under a shared key,
|
|
27
|
+
* and side effects would be lost on cache hit.
|
|
28
|
+
*
|
|
29
|
+
* The value is a numeric reference count, not a boolean. Multiple concurrent
|
|
30
|
+
* cached functions sharing the same ctx/requestCtx each increment on entry
|
|
31
|
+
* and decrement on exit. Guards fire when count > 0.
|
|
32
|
+
*/
|
|
33
|
+
export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
|
|
34
|
+
"rango:inside-cache-exec",
|
|
35
|
+
) as any;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Increment the INSIDE_CACHE_EXEC ref count on an object.
|
|
39
|
+
*/
|
|
40
|
+
export function stampCacheExec(obj: object): void {
|
|
41
|
+
const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
|
|
42
|
+
(obj as any)[INSIDE_CACHE_EXEC] = current + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decrement the INSIDE_CACHE_EXEC ref count on an object.
|
|
47
|
+
* Deletes the symbol when the count reaches zero so the `in` check
|
|
48
|
+
* used by guards no longer fires.
|
|
49
|
+
*/
|
|
50
|
+
export function unstampCacheExec(obj: object): void {
|
|
51
|
+
const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
|
|
52
|
+
if (current <= 1) {
|
|
53
|
+
delete (obj as any)[INSIDE_CACHE_EXEC];
|
|
54
|
+
} else {
|
|
55
|
+
(obj as any)[INSIDE_CACHE_EXEC] = current - 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Throw if ctx is inside a "use cache" execution.
|
|
61
|
+
* Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
|
|
62
|
+
*/
|
|
63
|
+
export function assertNotInsideCacheExec(
|
|
64
|
+
ctx: unknown,
|
|
65
|
+
methodName: string,
|
|
66
|
+
): void {
|
|
67
|
+
if (
|
|
68
|
+
ctx !== null &&
|
|
69
|
+
ctx !== undefined &&
|
|
70
|
+
typeof ctx === "object" &&
|
|
71
|
+
(INSIDE_CACHE_EXEC as symbol) in (ctx as Record<symbol, unknown>)
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`ctx.${methodName}() cannot be called inside a "use cache" function. ` +
|
|
75
|
+
`Side effects on the request context are lost on cache hit because ` +
|
|
76
|
+
`the function body is skipped. Extract the data fetch into a separate ` +
|
|
77
|
+
`cached function and call ctx.${methodName}() outside it, or use the ` +
|
|
78
|
+
`route-level cache() DSL which caches all segments (handler + children) ` +
|
|
79
|
+
`together.`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Brand symbol for functions wrapped by registerCachedFunction().
|
|
86
|
+
* Used at runtime to detect when a "use cache" function is misused
|
|
87
|
+
* (e.g., passed as middleware).
|
|
88
|
+
*/
|
|
89
|
+
export const CACHED_FN_SYMBOL: unique symbol = Symbol.for(
|
|
90
|
+
"rango:cached-fn",
|
|
91
|
+
) as any;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if a value is a "use cache" wrapped function.
|
|
95
|
+
*/
|
|
96
|
+
export function isCachedFunction(value: unknown): boolean {
|
|
97
|
+
return typeof value === "function" && (CACHED_FN_SYMBOL as symbol) in value;
|
|
98
|
+
}
|