@rangojs/router 0.0.0-experimental.8 → 0.0.0-experimental.81
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 +942 -4
- package/dist/bin/rango.js +1689 -0
- package/dist/vite/index.js +5091 -941
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +61 -52
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -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/handler-use/SKILL.md +362 -0
- package/skills/hooks/SKILL.md +340 -72
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +151 -8
- package/skills/layout/SKILL.md +122 -3
- package/skills/links/SKILL.md +92 -31
- package/skills/loader/SKILL.md +404 -44
- package/skills/middleware/SKILL.md +205 -37
- package/skills/migrate-nextjs/SKILL.md +560 -0
- package/skills/migrate-react-router/SKILL.md +765 -0
- package/skills/mime-routes/SKILL.md +128 -0
- package/skills/parallel/SKILL.md +263 -1
- package/skills/prerender/SKILL.md +685 -0
- package/skills/rango/SKILL.md +87 -16
- package/skills/response-routes/SKILL.md +411 -0
- package/skills/route/SKILL.md +281 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/tailwind/SKILL.md +129 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +328 -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/app-version.ts +14 -0
- package/src/browser/event-controller.ts +92 -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 +317 -560
- package/src/browser/navigation-client.ts +206 -68
- package/src/browser/navigation-store.ts +73 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +343 -316
- package/src/browser/prefetch/cache.ts +216 -0
- package/src/browser/prefetch/fetch.ts +206 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +160 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +253 -74
- package/src/browser/react/NavigationProvider.tsx +91 -11
- package/src/browser/react/context.ts +11 -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 +30 -126
- 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 +44 -65
- package/src/browser/react/use-params.ts +75 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-router.ts +76 -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 +214 -58
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +141 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +235 -24
- package/src/build/generate-route-types.ts +39 -0
- package/src/build/index.ts +13 -0
- package/src/build/route-trie.ts +291 -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 +418 -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 +618 -0
- package/src/build/route-types/scan-filter.ts +85 -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 +342 -0
- package/src/cache/cache-scope.ts +167 -309
- package/src/cache/cf/cf-cache-store.ts +571 -17
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- 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 +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +3 -1
- package/src/client.tsx +135 -301
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +108 -2
- package/src/handle.ts +55 -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 +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +119 -29
- package/src/index.rsc.ts +155 -19
- package/src/index.ts +251 -30
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +26 -157
- 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 +186 -0
- package/src/prerender.ts +524 -0
- package/src/reverse.ts +354 -0
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1121 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +478 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +149 -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 +77 -8
- package/src/router/content-negotiation.ts +215 -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 +438 -86
- package/src/router/intercept-resolution.ts +402 -0
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +356 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +163 -35
- package/src/router/match-api.ts +555 -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 +108 -93
- package/src/router/match-middleware/cache-lookup.ts +460 -10
- package/src/router/match-middleware/cache-store.ts +98 -26
- package/src/router/match-middleware/intercept-resolution.ts +57 -17
- package/src/router/match-middleware/segment-resolution.ts +80 -6
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +135 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +220 -0
- package/src/router/middleware.ts +324 -369
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +211 -43
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +137 -38
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +748 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1379 -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 +78 -3
- package/src/router.ts +740 -4252
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +907 -797
- package/src/rsc/helpers.ts +140 -6
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +393 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +246 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +358 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +46 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +134 -36
- package/src/server/context.ts +341 -61
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +607 -81
- package/src/server.ts +35 -130
- package/src/ssr/index.tsx +103 -30
- package/src/static-handler.ts +126 -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 +791 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +210 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +150 -0
- package/src/types.ts +1 -1623
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +116 -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 +161 -81
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +348 -0
- package/src/vite/discovery/prerender-collection.ts +439 -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 +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -1133
- package/src/vite/plugin-types.ts +103 -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/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +72 -53
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -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 +786 -0
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -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} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +462 -0
- package/src/vite/router-discovery.ts +977 -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 +221 -0
- package/src/vite/utils/shared-utils.ts +170 -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
package/src/cache/cache-scope.ts
CHANGED
|
@@ -2,59 +2,65 @@
|
|
|
2
2
|
* CacheScope - Runtime cache scope for iterator-based caching
|
|
3
3
|
*
|
|
4
4
|
* Each cache() boundary in the route tree creates a new CacheScope.
|
|
5
|
-
* The scope owns: config,
|
|
5
|
+
* The scope owns: config, key management, and storage operations.
|
|
6
|
+
*
|
|
7
|
+
* Serialization is delegated to segment-codec.ts.
|
|
8
|
+
* Handle data capture/restore is delegated to handle-snapshot.ts.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
|
-
/// <reference types="@vitejs/plugin-rsc/types" />
|
|
9
|
-
|
|
10
11
|
import type { PartialCacheOptions } from "../types.js";
|
|
11
12
|
import type { ResolvedSegment } from "../types.js";
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
SegmentHandleData,
|
|
15
|
-
CachedEntryData,
|
|
16
|
-
SerializedSegmentData,
|
|
17
|
-
} from "./types.js";
|
|
18
|
-
import { getRequestContext } from "../server/request-context.js";
|
|
13
|
+
import type { SegmentCacheStore, CachedEntryData } from "./types.js";
|
|
14
|
+
import { INTERNAL_RANGO_DEBUG } from "../internal-debug.js";
|
|
19
15
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
} from "
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
getRequestContext,
|
|
17
|
+
_getRequestContext,
|
|
18
|
+
} from "../server/request-context.js";
|
|
19
|
+
import { serializeSegments, deserializeSegments } from "./segment-codec.js";
|
|
20
|
+
import { captureHandles, restoreHandles } from "./handle-snapshot.js";
|
|
21
|
+
import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
|
|
22
|
+
import {
|
|
23
|
+
DEFAULT_ROUTE_TTL,
|
|
24
|
+
resolveCacheKey,
|
|
25
|
+
resolveCacheStore,
|
|
26
|
+
} from "./cache-policy.js";
|
|
27
|
+
|
|
28
|
+
function debugCacheLog(message: string): void {
|
|
29
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
30
|
+
console.log(message);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
31
33
|
|
|
32
34
|
// ============================================================================
|
|
33
|
-
//
|
|
35
|
+
// Key Generation (internal)
|
|
34
36
|
// ============================================================================
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
* Generate cache key base from pathname and params.
|
|
38
|
-
*
|
|
39
|
+
* Generate cache key base from host, pathname, route params, and search params.
|
|
40
|
+
* Host is included to prevent cross-host cache collisions on shared stores.
|
|
41
|
+
* Route params and search params are sorted alphabetically for deterministic keys.
|
|
42
|
+
* Internal _rsc* and __* query params are excluded.
|
|
39
43
|
* @internal
|
|
40
44
|
*/
|
|
41
45
|
function getCacheKeyBase(
|
|
46
|
+
host: string,
|
|
42
47
|
pathname: string,
|
|
43
|
-
params?: Record<string, string
|
|
48
|
+
params?: Record<string, string>,
|
|
49
|
+
searchParams?: URLSearchParams,
|
|
44
50
|
): string {
|
|
45
|
-
const paramStr = params
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
return paramStr ? `${pathname}:${paramStr}` : pathname;
|
|
51
|
+
const paramStr = sortedRouteParams(params);
|
|
52
|
+
const searchStr = searchParams ? sortedSearchString(searchParams) : "";
|
|
53
|
+
|
|
54
|
+
let key = `${host}${pathname}`;
|
|
55
|
+
if (paramStr) key += `:${paramStr}`;
|
|
56
|
+
if (searchStr) key += `?${searchStr}`;
|
|
57
|
+
return key;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Generate default cache key for a route request.
|
|
57
|
-
*
|
|
62
|
+
* Includes pathname, route params, and user-facing search params for
|
|
63
|
+
* correct scoping. Internal _rsc* params are excluded.
|
|
58
64
|
* Includes request type prefix since they produce different segment sets:
|
|
59
65
|
* - doc: document requests (full page load)
|
|
60
66
|
* - partial: navigation requests (client-side navigation)
|
|
@@ -64,203 +70,17 @@ function getCacheKeyBase(
|
|
|
64
70
|
function getDefaultRouteCacheKey(
|
|
65
71
|
pathname: string,
|
|
66
72
|
params?: Record<string, string>,
|
|
67
|
-
isIntercept?: boolean
|
|
73
|
+
isIntercept?: boolean,
|
|
68
74
|
): string {
|
|
69
75
|
const ctx = getRequestContext();
|
|
70
|
-
const isPartial = ctx?.
|
|
76
|
+
const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
|
|
77
|
+
const searchParams = ctx?.url.searchParams;
|
|
78
|
+
const host = ctx?.url.host ?? "localhost";
|
|
71
79
|
|
|
72
80
|
// Intercept navigations get their own cache namespace
|
|
73
81
|
const prefix = isIntercept ? "intercept" : isPartial ? "partial" : "doc";
|
|
74
82
|
|
|
75
|
-
return `${prefix}:${getCacheKeyBase(pathname, params)}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Convert a ReadableStream to a string.
|
|
80
|
-
* @internal
|
|
81
|
-
*/
|
|
82
|
-
async function streamToString(
|
|
83
|
-
stream: ReadableStream<Uint8Array>
|
|
84
|
-
): Promise<string> {
|
|
85
|
-
const reader = stream.getReader();
|
|
86
|
-
const decoder = new TextDecoder();
|
|
87
|
-
let result = "";
|
|
88
|
-
|
|
89
|
-
while (true) {
|
|
90
|
-
const { done, value } = await reader.read();
|
|
91
|
-
if (done) break;
|
|
92
|
-
result += decoder.decode(value, { stream: true });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
result += decoder.decode(); // flush
|
|
96
|
-
return result;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Convert a string to a ReadableStream.
|
|
101
|
-
* @internal
|
|
102
|
-
*/
|
|
103
|
-
function stringToStream(str: string): ReadableStream<Uint8Array> {
|
|
104
|
-
const encoder = new TextEncoder();
|
|
105
|
-
const uint8 = encoder.encode(str);
|
|
106
|
-
|
|
107
|
-
return new ReadableStream({
|
|
108
|
-
start(controller) {
|
|
109
|
-
controller.enqueue(uint8);
|
|
110
|
-
controller.close();
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* RSC-serialize a value using React Server Components stream.
|
|
117
|
-
* Used for serializing loaderData, layout, loading components etc.
|
|
118
|
-
* @internal
|
|
119
|
-
*/
|
|
120
|
-
async function rscSerialize(value: unknown): Promise<string | undefined> {
|
|
121
|
-
if (value === undefined || value === null) return undefined;
|
|
122
|
-
|
|
123
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
124
|
-
const stream = renderToReadableStream(value, { temporaryReferences });
|
|
125
|
-
return streamToString(stream);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* RSC-deserialize a value from a stored string.
|
|
130
|
-
* @internal
|
|
131
|
-
*/
|
|
132
|
-
async function rscDeserialize<T>(
|
|
133
|
-
encoded: string | undefined
|
|
134
|
-
): Promise<T | undefined> {
|
|
135
|
-
if (!encoded) return undefined;
|
|
136
|
-
|
|
137
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
138
|
-
const stream = stringToStream(encoded);
|
|
139
|
-
return createFromReadableStream<T>(stream, { temporaryReferences });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Serialize segments for storage.
|
|
144
|
-
* Each segment's component, layout, loading, and loaderData are RSC-serialized.
|
|
145
|
-
* Metadata is preserved as-is.
|
|
146
|
-
* @internal
|
|
147
|
-
*/
|
|
148
|
-
async function serializeSegments(
|
|
149
|
-
segments: ResolvedSegment[]
|
|
150
|
-
): Promise<SerializedSegmentData[]> {
|
|
151
|
-
const serialized: SerializedSegmentData[] = [];
|
|
152
|
-
|
|
153
|
-
for (const segment of segments) {
|
|
154
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
155
|
-
|
|
156
|
-
// Await component if it's a Promise (intercepts with loading keep component as Promise)
|
|
157
|
-
const componentResolved =
|
|
158
|
-
segment.component instanceof Promise
|
|
159
|
-
? await segment.component
|
|
160
|
-
: segment.component;
|
|
161
|
-
|
|
162
|
-
// Serialize the component to RSC stream
|
|
163
|
-
const stream = renderToReadableStream(componentResolved, {
|
|
164
|
-
temporaryReferences,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
// Convert stream to string
|
|
168
|
-
const encoded = await streamToString(stream);
|
|
169
|
-
|
|
170
|
-
// RSC-serialize layout if present (ReactNode)
|
|
171
|
-
const encodedLayout = segment.layout
|
|
172
|
-
? await rscSerialize(segment.layout)
|
|
173
|
-
: undefined;
|
|
174
|
-
|
|
175
|
-
// RSC-serialize loading if present (ReactNode) - preserves tree structure
|
|
176
|
-
// Use "null" string to distinguish explicit null from undefined
|
|
177
|
-
const encodedLoading =
|
|
178
|
-
segment.loading !== undefined
|
|
179
|
-
? segment.loading === null
|
|
180
|
-
? "null"
|
|
181
|
-
: await rscSerialize(segment.loading)
|
|
182
|
-
: undefined;
|
|
183
|
-
|
|
184
|
-
// Await and RSC-serialize loaderData if present
|
|
185
|
-
const loaderDataResolved =
|
|
186
|
-
segment.loaderData instanceof Promise
|
|
187
|
-
? await segment.loaderData
|
|
188
|
-
: segment.loaderData;
|
|
189
|
-
const encodedLoaderData = await rscSerialize(loaderDataResolved);
|
|
190
|
-
|
|
191
|
-
// Await and RSC-serialize loaderDataPromise if present
|
|
192
|
-
const loaderDataPromiseResolved =
|
|
193
|
-
segment.loaderDataPromise instanceof Promise
|
|
194
|
-
? await segment.loaderDataPromise
|
|
195
|
-
: segment.loaderDataPromise;
|
|
196
|
-
const encodedLoaderDataPromise = await rscSerialize(
|
|
197
|
-
loaderDataPromiseResolved
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
serialized.push({
|
|
201
|
-
encoded,
|
|
202
|
-
encodedLayout,
|
|
203
|
-
encodedLoading,
|
|
204
|
-
encodedLoaderData,
|
|
205
|
-
encodedLoaderDataPromise,
|
|
206
|
-
metadata: {
|
|
207
|
-
id: segment.id,
|
|
208
|
-
type: segment.type,
|
|
209
|
-
namespace: segment.namespace,
|
|
210
|
-
index: segment.index,
|
|
211
|
-
params: segment.params,
|
|
212
|
-
slot: segment.slot,
|
|
213
|
-
belongsToRoute: segment.belongsToRoute,
|
|
214
|
-
layoutName: segment.layoutName,
|
|
215
|
-
parallelName: segment.parallelName,
|
|
216
|
-
loaderId: segment.loaderId,
|
|
217
|
-
loaderIds: segment.loaderIds,
|
|
218
|
-
},
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return serialized;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Deserialize segments from storage.
|
|
227
|
-
* Reconstructs ResolvedSegment objects from RSC-serialized data.
|
|
228
|
-
* @internal
|
|
229
|
-
*/
|
|
230
|
-
async function deserializeSegments(
|
|
231
|
-
data: SerializedSegmentData[]
|
|
232
|
-
): Promise<ResolvedSegment[]> {
|
|
233
|
-
const segments: ResolvedSegment[] = [];
|
|
234
|
-
|
|
235
|
-
for (const item of data) {
|
|
236
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
237
|
-
|
|
238
|
-
// Revive the component from cached string
|
|
239
|
-
const stream = stringToStream(item.encoded);
|
|
240
|
-
const component = await createFromReadableStream(stream, {
|
|
241
|
-
temporaryReferences,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
// RSC-deserialize layout, loaderData, loaderDataPromise in parallel
|
|
245
|
-
const [layout, loaderData, loaderDataPromise, loadingData] =
|
|
246
|
-
await Promise.all([
|
|
247
|
-
rscDeserialize(item.encodedLayout),
|
|
248
|
-
rscDeserialize(item.encodedLoaderData),
|
|
249
|
-
rscDeserialize(item.encodedLoaderDataPromise),
|
|
250
|
-
rscDeserialize(item.encodedLoading),
|
|
251
|
-
]);
|
|
252
|
-
|
|
253
|
-
segments.push({
|
|
254
|
-
...item.metadata,
|
|
255
|
-
component: await component,
|
|
256
|
-
layout,
|
|
257
|
-
loading: loadingData,
|
|
258
|
-
loaderData,
|
|
259
|
-
loaderDataPromise,
|
|
260
|
-
} as ResolvedSegment);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return segments;
|
|
83
|
+
return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
|
|
264
84
|
}
|
|
265
85
|
|
|
266
86
|
// ============================================================================
|
|
@@ -271,7 +91,8 @@ async function deserializeSegments(
|
|
|
271
91
|
* CacheScope represents a cache boundary in the route tree.
|
|
272
92
|
*
|
|
273
93
|
* When withCache encounters an entry with cache config, it creates
|
|
274
|
-
* a new CacheScope. The scope owns
|
|
94
|
+
* a new CacheScope. The scope owns key management, TTL resolution,
|
|
95
|
+
* and storage operations. Serialization is handled by segment-codec.ts.
|
|
275
96
|
*
|
|
276
97
|
* Store resolution priority:
|
|
277
98
|
* 1. Explicit store in cache() options
|
|
@@ -291,7 +112,7 @@ export class CacheScope {
|
|
|
291
112
|
|
|
292
113
|
constructor(
|
|
293
114
|
config: PartialCacheOptions | false,
|
|
294
|
-
parent: CacheScope | null = null
|
|
115
|
+
parent: CacheScope | null = null,
|
|
295
116
|
) {
|
|
296
117
|
this.config = config;
|
|
297
118
|
this.parent = parent;
|
|
@@ -324,7 +145,7 @@ export class CacheScope {
|
|
|
324
145
|
}
|
|
325
146
|
|
|
326
147
|
// Hardcoded fallback
|
|
327
|
-
return
|
|
148
|
+
return DEFAULT_ROUTE_TTL;
|
|
328
149
|
}
|
|
329
150
|
|
|
330
151
|
/**
|
|
@@ -348,65 +169,22 @@ export class CacheScope {
|
|
|
348
169
|
* 1. Explicit store from cache() options
|
|
349
170
|
* 2. App-level store from request context
|
|
350
171
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
if (this.explicitStore) {
|
|
354
|
-
return this.explicitStore;
|
|
355
|
-
}
|
|
356
|
-
// Fall back to app-level store from request context
|
|
357
|
-
const ctx = getRequestContext();
|
|
358
|
-
return ctx?._cacheStore ?? null;
|
|
172
|
+
getStore(): SegmentCacheStore | null {
|
|
173
|
+
return resolveCacheStore(this.explicitStore);
|
|
359
174
|
}
|
|
360
175
|
|
|
361
176
|
/**
|
|
362
|
-
* Resolve the cache key using
|
|
363
|
-
*
|
|
364
|
-
* Resolution priority:
|
|
365
|
-
* 1. Route-level `key` function (full override)
|
|
366
|
-
* 2. Store-level `keyGenerator` (modifies default key)
|
|
367
|
-
* 3. Default key generation (prefix:pathname:params)
|
|
368
|
-
*
|
|
177
|
+
* Resolve the cache key using the shared 3-tier priority.
|
|
369
178
|
* @internal
|
|
370
179
|
*/
|
|
371
180
|
private async resolveKey(
|
|
372
181
|
pathname: string,
|
|
373
182
|
params: Record<string, string>,
|
|
374
|
-
isIntercept?: boolean
|
|
183
|
+
isIntercept?: boolean,
|
|
375
184
|
): Promise<string> {
|
|
376
|
-
const requestCtx = getRequestContext();
|
|
377
|
-
if (!requestCtx) {
|
|
378
|
-
// Fallback to default key if no request context
|
|
379
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Priority 1: Route-level key function (full override)
|
|
383
|
-
if (this.config !== false && this.config.key) {
|
|
384
|
-
try {
|
|
385
|
-
const customKey = await this.config.key(requestCtx);
|
|
386
|
-
return customKey;
|
|
387
|
-
} catch (error) {
|
|
388
|
-
console.error(`[CacheScope] Custom key function failed, using default:`, error);
|
|
389
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
// Generate default key
|
|
394
185
|
const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const store = this.getStore();
|
|
398
|
-
if (store?.keyGenerator) {
|
|
399
|
-
try {
|
|
400
|
-
const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
|
|
401
|
-
return modifiedKey;
|
|
402
|
-
} catch (error) {
|
|
403
|
-
console.error(`[CacheScope] Store keyGenerator failed, using default:`, error);
|
|
404
|
-
return defaultKey;
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Priority 3: Default key
|
|
409
|
-
return defaultKey;
|
|
186
|
+
const keyFn = this.config !== false ? this.config.key : undefined;
|
|
187
|
+
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
410
188
|
}
|
|
411
189
|
|
|
412
190
|
/**
|
|
@@ -420,13 +198,34 @@ export class CacheScope {
|
|
|
420
198
|
async lookupRoute(
|
|
421
199
|
pathname: string,
|
|
422
200
|
params: Record<string, string>,
|
|
423
|
-
isIntercept?: boolean
|
|
201
|
+
isIntercept?: boolean,
|
|
424
202
|
): Promise<{
|
|
425
203
|
segments: ResolvedSegment[];
|
|
426
204
|
shouldRevalidate: boolean;
|
|
427
205
|
} | null> {
|
|
428
206
|
if (!this.enabled) return null;
|
|
429
207
|
|
|
208
|
+
// Evaluate condition — skip cache read when condition returns false
|
|
209
|
+
if (this.config !== false && this.config.condition) {
|
|
210
|
+
const requestCtx = getRequestContext();
|
|
211
|
+
if (requestCtx) {
|
|
212
|
+
try {
|
|
213
|
+
if (!this.config.condition(requestCtx)) {
|
|
214
|
+
debugCacheLog(
|
|
215
|
+
`[CacheScope] condition returned false, skipping cache read`,
|
|
216
|
+
);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
} catch (error) {
|
|
220
|
+
console.error(
|
|
221
|
+
`[CacheScope] condition function threw, skipping cache read:`,
|
|
222
|
+
error,
|
|
223
|
+
);
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
430
229
|
const store = this.getStore();
|
|
431
230
|
if (!store) return null;
|
|
432
231
|
|
|
@@ -437,7 +236,7 @@ export class CacheScope {
|
|
|
437
236
|
const result = await store.get(key);
|
|
438
237
|
|
|
439
238
|
if (!result) {
|
|
440
|
-
|
|
239
|
+
debugCacheLog(`[CacheScope] MISS: ${key}`);
|
|
441
240
|
return null;
|
|
442
241
|
}
|
|
443
242
|
|
|
@@ -447,21 +246,19 @@ export class CacheScope {
|
|
|
447
246
|
const segments = await deserializeSegments(cached.segments);
|
|
448
247
|
|
|
449
248
|
// Replay handle data
|
|
450
|
-
const handleStore =
|
|
249
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
451
250
|
if (handleStore) {
|
|
452
|
-
|
|
453
|
-
if (Object.keys(segHandles).length > 0) {
|
|
454
|
-
handleStore.replaySegmentData(segId, segHandles);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
251
|
+
restoreHandles(cached.handles, handleStore);
|
|
457
252
|
}
|
|
458
253
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
254
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
255
|
+
const segmentTypes = segments.map((s) =>
|
|
256
|
+
s.type === "parallel" ? s.slot : s.type,
|
|
257
|
+
);
|
|
258
|
+
debugCacheLog(
|
|
259
|
+
`[CacheScope] ${shouldRevalidate ? "STALE" : "HIT"}: ${key} (${segmentTypes.join(", ")})`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
465
262
|
|
|
466
263
|
return { segments, shouldRevalidate };
|
|
467
264
|
} catch (error) {
|
|
@@ -484,10 +281,31 @@ export class CacheScope {
|
|
|
484
281
|
pathname: string,
|
|
485
282
|
params: Record<string, string>,
|
|
486
283
|
segments: ResolvedSegment[],
|
|
487
|
-
isIntercept?: boolean
|
|
284
|
+
isIntercept?: boolean,
|
|
488
285
|
): Promise<void> {
|
|
489
286
|
if (!this.enabled || segments.length === 0) return;
|
|
490
287
|
|
|
288
|
+
// Evaluate condition — skip cache write when condition returns false
|
|
289
|
+
if (this.config !== false && this.config.condition) {
|
|
290
|
+
const conditionCtx = getRequestContext();
|
|
291
|
+
if (conditionCtx) {
|
|
292
|
+
try {
|
|
293
|
+
if (!this.config.condition(conditionCtx)) {
|
|
294
|
+
debugCacheLog(
|
|
295
|
+
`[CacheScope] condition returned false, skipping cache write`,
|
|
296
|
+
);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error(
|
|
301
|
+
`[CacheScope] condition function threw, skipping cache write:`,
|
|
302
|
+
error,
|
|
303
|
+
);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
491
309
|
const store = this.getStore();
|
|
492
310
|
if (!store) return;
|
|
493
311
|
|
|
@@ -508,27 +326,61 @@ export class CacheScope {
|
|
|
508
326
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
509
327
|
|
|
510
328
|
// Check if this is a partial request (navigation) vs document request
|
|
511
|
-
const isPartial = requestCtx.
|
|
329
|
+
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
330
|
+
|
|
331
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
332
|
+
debugCacheLog(
|
|
333
|
+
`[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
512
336
|
|
|
513
337
|
requestCtx.waitUntil(async () => {
|
|
338
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
339
|
+
debugCacheLog(
|
|
340
|
+
`[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
514
344
|
await handleStore.settled;
|
|
515
345
|
|
|
516
|
-
|
|
517
|
-
|
|
346
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
347
|
+
debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// For document requests: only cache if layout segments have components
|
|
351
|
+
// (complete render). Parallel and route segments may legitimately have
|
|
352
|
+
// null components — UI-less @meta parallels return null, and void route
|
|
353
|
+
// handlers produce null when the UI lives in parallel slots/layouts.
|
|
354
|
+
// Partial requests always allow null components (client already has them).
|
|
518
355
|
if (!isPartial) {
|
|
519
|
-
const
|
|
520
|
-
(s) => s.component
|
|
356
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
357
|
+
(s) => s.component === null && s.type === "layout",
|
|
521
358
|
);
|
|
522
|
-
if (
|
|
359
|
+
if (hasIncompleteLayouts) {
|
|
360
|
+
const nullSegments = nonLoaderSegments
|
|
361
|
+
.filter((s) => s.component === null && s.type === "layout")
|
|
362
|
+
.map((s) => s.id);
|
|
363
|
+
const error = new Error(
|
|
364
|
+
`[CacheScope] Cache write skipped: layout segments have null components ` +
|
|
365
|
+
`(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
|
|
366
|
+
`layout handlers must return JSX for document requests to be cacheable.`,
|
|
367
|
+
);
|
|
368
|
+
error.name = "CacheScopeInvariantError";
|
|
369
|
+
console.error(error.message);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
523
372
|
}
|
|
524
373
|
|
|
525
374
|
// Collect handle data for non-loader segments only
|
|
526
|
-
const handles
|
|
527
|
-
for (const seg of nonLoaderSegments) {
|
|
528
|
-
handles[seg.id] = handleStore.getDataForSegment(seg.id);
|
|
529
|
-
}
|
|
375
|
+
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
530
376
|
|
|
531
377
|
try {
|
|
378
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
379
|
+
debugCacheLog(
|
|
380
|
+
`[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
532
384
|
// Serialize non-loader segments only
|
|
533
385
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
534
386
|
|
|
@@ -538,14 +390,20 @@ export class CacheScope {
|
|
|
538
390
|
expiresAt: Date.now() + ttl * 1000,
|
|
539
391
|
};
|
|
540
392
|
|
|
393
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
394
|
+
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
541
397
|
await store.set(key, data, ttl, swr);
|
|
542
398
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
399
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
400
|
+
const segmentTypes = nonLoaderSegments.map((s) =>
|
|
401
|
+
s.type === "parallel" ? s.slot : s.type,
|
|
402
|
+
);
|
|
403
|
+
debugCacheLog(
|
|
404
|
+
`[CacheScope] Cached: ${key} (${segmentTypes.join(", ")}) ttl=${ttl}s [loaders excluded]`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
549
407
|
} catch (error) {
|
|
550
408
|
console.error(`[CacheScope] Failed to cache ${key}:`, error);
|
|
551
409
|
}
|
|
@@ -558,7 +416,7 @@ export class CacheScope {
|
|
|
558
416
|
*/
|
|
559
417
|
export function createCacheScope(
|
|
560
418
|
config: { options: PartialCacheOptions | false } | undefined,
|
|
561
|
-
parent: CacheScope | null = null
|
|
419
|
+
parent: CacheScope | null = null,
|
|
562
420
|
): CacheScope | null {
|
|
563
421
|
if (!config) return parent; // No config, inherit parent
|
|
564
422
|
return new CacheScope(config.options, parent);
|