@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -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 +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- 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 +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -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 -120
- 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 +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- 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 +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- 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 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- 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 +6 -1
- package/src/client.tsx +118 -302
- 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 +77 -7
- package/src/handle.ts +55 -10
- 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 +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -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 +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- 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 +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- 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 +152 -39
- 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 +756 -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 +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -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 +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- 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 +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -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 +187 -38
- package/src/server/context.ts +333 -59
- 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 +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -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 +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- 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 +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -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 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -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} +107 -64
- 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 +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -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 +497 -0
- package/src/vite/router-discovery.ts +1423 -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/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -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/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- 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/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /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,201 +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
|
-
*/
|
|
147
|
-
export async function serializeSegments(
|
|
148
|
-
segments: ResolvedSegment[]
|
|
149
|
-
): Promise<SerializedSegmentData[]> {
|
|
150
|
-
const serialized: SerializedSegmentData[] = [];
|
|
151
|
-
|
|
152
|
-
for (const segment of segments) {
|
|
153
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
154
|
-
|
|
155
|
-
// Await component if it's a Promise (intercepts with loading keep component as Promise)
|
|
156
|
-
const componentResolved =
|
|
157
|
-
segment.component instanceof Promise
|
|
158
|
-
? await segment.component
|
|
159
|
-
: segment.component;
|
|
160
|
-
|
|
161
|
-
// Serialize the component to RSC stream
|
|
162
|
-
const stream = renderToReadableStream(componentResolved, {
|
|
163
|
-
temporaryReferences,
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Convert stream to string
|
|
167
|
-
const encoded = await streamToString(stream);
|
|
168
|
-
|
|
169
|
-
// RSC-serialize layout if present (ReactNode)
|
|
170
|
-
const encodedLayout = segment.layout
|
|
171
|
-
? await rscSerialize(segment.layout)
|
|
172
|
-
: undefined;
|
|
173
|
-
|
|
174
|
-
// RSC-serialize loading if present (ReactNode) - preserves tree structure
|
|
175
|
-
// Use "null" string to distinguish explicit null from undefined
|
|
176
|
-
const encodedLoading =
|
|
177
|
-
segment.loading !== undefined
|
|
178
|
-
? segment.loading === null
|
|
179
|
-
? "null"
|
|
180
|
-
: await rscSerialize(segment.loading)
|
|
181
|
-
: undefined;
|
|
182
|
-
|
|
183
|
-
// Await and RSC-serialize loaderData if present
|
|
184
|
-
const loaderDataResolved =
|
|
185
|
-
segment.loaderData instanceof Promise
|
|
186
|
-
? await segment.loaderData
|
|
187
|
-
: segment.loaderData;
|
|
188
|
-
const encodedLoaderData = await rscSerialize(loaderDataResolved);
|
|
189
|
-
|
|
190
|
-
// Await and RSC-serialize loaderDataPromise if present
|
|
191
|
-
const loaderDataPromiseResolved =
|
|
192
|
-
segment.loaderDataPromise instanceof Promise
|
|
193
|
-
? await segment.loaderDataPromise
|
|
194
|
-
: segment.loaderDataPromise;
|
|
195
|
-
const encodedLoaderDataPromise = await rscSerialize(
|
|
196
|
-
loaderDataPromiseResolved
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
serialized.push({
|
|
200
|
-
encoded,
|
|
201
|
-
encodedLayout,
|
|
202
|
-
encodedLoading,
|
|
203
|
-
encodedLoaderData,
|
|
204
|
-
encodedLoaderDataPromise,
|
|
205
|
-
metadata: {
|
|
206
|
-
id: segment.id,
|
|
207
|
-
type: segment.type,
|
|
208
|
-
namespace: segment.namespace,
|
|
209
|
-
index: segment.index,
|
|
210
|
-
params: segment.params,
|
|
211
|
-
slot: segment.slot,
|
|
212
|
-
belongsToRoute: segment.belongsToRoute,
|
|
213
|
-
layoutName: segment.layoutName,
|
|
214
|
-
parallelName: segment.parallelName,
|
|
215
|
-
loaderId: segment.loaderId,
|
|
216
|
-
loaderIds: segment.loaderIds,
|
|
217
|
-
},
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return serialized;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* Deserialize segments from storage.
|
|
226
|
-
* Reconstructs ResolvedSegment objects from RSC-serialized data.
|
|
227
|
-
*/
|
|
228
|
-
export async function deserializeSegments(
|
|
229
|
-
data: SerializedSegmentData[]
|
|
230
|
-
): Promise<ResolvedSegment[]> {
|
|
231
|
-
const segments: ResolvedSegment[] = [];
|
|
232
|
-
|
|
233
|
-
for (const item of data) {
|
|
234
|
-
const temporaryReferences = createTemporaryReferenceSet();
|
|
235
|
-
|
|
236
|
-
// Revive the component from cached string
|
|
237
|
-
const stream = stringToStream(item.encoded);
|
|
238
|
-
const component = await createFromReadableStream(stream, {
|
|
239
|
-
temporaryReferences,
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
// RSC-deserialize layout, loaderData, loaderDataPromise in parallel
|
|
243
|
-
const [layout, loaderData, loaderDataPromise, loadingData] =
|
|
244
|
-
await Promise.all([
|
|
245
|
-
rscDeserialize(item.encodedLayout),
|
|
246
|
-
rscDeserialize(item.encodedLoaderData),
|
|
247
|
-
rscDeserialize(item.encodedLoaderDataPromise),
|
|
248
|
-
rscDeserialize(item.encodedLoading),
|
|
249
|
-
]);
|
|
250
|
-
|
|
251
|
-
segments.push({
|
|
252
|
-
...item.metadata,
|
|
253
|
-
component: await component,
|
|
254
|
-
layout,
|
|
255
|
-
loading: loadingData,
|
|
256
|
-
loaderData,
|
|
257
|
-
loaderDataPromise,
|
|
258
|
-
} as ResolvedSegment);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return segments;
|
|
83
|
+
return `${prefix}:${getCacheKeyBase(host, pathname, params, searchParams)}`;
|
|
262
84
|
}
|
|
263
85
|
|
|
264
86
|
// ============================================================================
|
|
@@ -269,7 +91,8 @@ export async function deserializeSegments(
|
|
|
269
91
|
* CacheScope represents a cache boundary in the route tree.
|
|
270
92
|
*
|
|
271
93
|
* When withCache encounters an entry with cache config, it creates
|
|
272
|
-
* 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.
|
|
273
96
|
*
|
|
274
97
|
* Store resolution priority:
|
|
275
98
|
* 1. Explicit store in cache() options
|
|
@@ -289,7 +112,7 @@ export class CacheScope {
|
|
|
289
112
|
|
|
290
113
|
constructor(
|
|
291
114
|
config: PartialCacheOptions | false,
|
|
292
|
-
parent: CacheScope | null = null
|
|
115
|
+
parent: CacheScope | null = null,
|
|
293
116
|
) {
|
|
294
117
|
this.config = config;
|
|
295
118
|
this.parent = parent;
|
|
@@ -322,7 +145,7 @@ export class CacheScope {
|
|
|
322
145
|
}
|
|
323
146
|
|
|
324
147
|
// Hardcoded fallback
|
|
325
|
-
return
|
|
148
|
+
return DEFAULT_ROUTE_TTL;
|
|
326
149
|
}
|
|
327
150
|
|
|
328
151
|
/**
|
|
@@ -346,65 +169,22 @@ export class CacheScope {
|
|
|
346
169
|
* 1. Explicit store from cache() options
|
|
347
170
|
* 2. App-level store from request context
|
|
348
171
|
*/
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (this.explicitStore) {
|
|
352
|
-
return this.explicitStore;
|
|
353
|
-
}
|
|
354
|
-
// Fall back to app-level store from request context
|
|
355
|
-
const ctx = getRequestContext();
|
|
356
|
-
return ctx?._cacheStore ?? null;
|
|
172
|
+
getStore(): SegmentCacheStore | null {
|
|
173
|
+
return resolveCacheStore(this.explicitStore);
|
|
357
174
|
}
|
|
358
175
|
|
|
359
176
|
/**
|
|
360
|
-
* Resolve the cache key using
|
|
361
|
-
*
|
|
362
|
-
* Resolution priority:
|
|
363
|
-
* 1. Route-level `key` function (full override)
|
|
364
|
-
* 2. Store-level `keyGenerator` (modifies default key)
|
|
365
|
-
* 3. Default key generation (prefix:pathname:params)
|
|
366
|
-
*
|
|
177
|
+
* Resolve the cache key using the shared 3-tier priority.
|
|
367
178
|
* @internal
|
|
368
179
|
*/
|
|
369
180
|
private async resolveKey(
|
|
370
181
|
pathname: string,
|
|
371
182
|
params: Record<string, string>,
|
|
372
|
-
isIntercept?: boolean
|
|
183
|
+
isIntercept?: boolean,
|
|
373
184
|
): Promise<string> {
|
|
374
|
-
const requestCtx = getRequestContext();
|
|
375
|
-
if (!requestCtx) {
|
|
376
|
-
// Fallback to default key if no request context
|
|
377
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Priority 1: Route-level key function (full override)
|
|
381
|
-
if (this.config !== false && this.config.key) {
|
|
382
|
-
try {
|
|
383
|
-
const customKey = await this.config.key(requestCtx);
|
|
384
|
-
return customKey;
|
|
385
|
-
} catch (error) {
|
|
386
|
-
console.error(`[CacheScope] Custom key function failed, using default:`, error);
|
|
387
|
-
return getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Generate default key
|
|
392
185
|
const defaultKey = getDefaultRouteCacheKey(pathname, params, isIntercept);
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const store = this.getStore();
|
|
396
|
-
if (store?.keyGenerator) {
|
|
397
|
-
try {
|
|
398
|
-
const modifiedKey = await store.keyGenerator(requestCtx, defaultKey);
|
|
399
|
-
return modifiedKey;
|
|
400
|
-
} catch (error) {
|
|
401
|
-
console.error(`[CacheScope] Store keyGenerator failed, using default:`, error);
|
|
402
|
-
return defaultKey;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Priority 3: Default key
|
|
407
|
-
return defaultKey;
|
|
186
|
+
const keyFn = this.config !== false ? this.config.key : undefined;
|
|
187
|
+
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
408
188
|
}
|
|
409
189
|
|
|
410
190
|
/**
|
|
@@ -418,13 +198,34 @@ export class CacheScope {
|
|
|
418
198
|
async lookupRoute(
|
|
419
199
|
pathname: string,
|
|
420
200
|
params: Record<string, string>,
|
|
421
|
-
isIntercept?: boolean
|
|
201
|
+
isIntercept?: boolean,
|
|
422
202
|
): Promise<{
|
|
423
203
|
segments: ResolvedSegment[];
|
|
424
204
|
shouldRevalidate: boolean;
|
|
425
205
|
} | null> {
|
|
426
206
|
if (!this.enabled) return null;
|
|
427
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
|
+
|
|
428
229
|
const store = this.getStore();
|
|
429
230
|
if (!store) return null;
|
|
430
231
|
|
|
@@ -435,7 +236,7 @@ export class CacheScope {
|
|
|
435
236
|
const result = await store.get(key);
|
|
436
237
|
|
|
437
238
|
if (!result) {
|
|
438
|
-
|
|
239
|
+
debugCacheLog(`[CacheScope] MISS: ${key}`);
|
|
439
240
|
return null;
|
|
440
241
|
}
|
|
441
242
|
|
|
@@ -445,21 +246,19 @@ export class CacheScope {
|
|
|
445
246
|
const segments = await deserializeSegments(cached.segments);
|
|
446
247
|
|
|
447
248
|
// Replay handle data
|
|
448
|
-
const handleStore =
|
|
249
|
+
const handleStore = _getRequestContext()?._handleStore;
|
|
449
250
|
if (handleStore) {
|
|
450
|
-
|
|
451
|
-
if (Object.keys(segHandles).length > 0) {
|
|
452
|
-
handleStore.replaySegmentData(segId, segHandles);
|
|
453
|
-
}
|
|
454
|
-
}
|
|
251
|
+
restoreHandles(cached.handles, handleStore);
|
|
455
252
|
}
|
|
456
253
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
+
}
|
|
463
262
|
|
|
464
263
|
return { segments, shouldRevalidate };
|
|
465
264
|
} catch (error) {
|
|
@@ -482,10 +281,31 @@ export class CacheScope {
|
|
|
482
281
|
pathname: string,
|
|
483
282
|
params: Record<string, string>,
|
|
484
283
|
segments: ResolvedSegment[],
|
|
485
|
-
isIntercept?: boolean
|
|
284
|
+
isIntercept?: boolean,
|
|
486
285
|
): Promise<void> {
|
|
487
286
|
if (!this.enabled || segments.length === 0) return;
|
|
488
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
|
+
|
|
489
309
|
const store = this.getStore();
|
|
490
310
|
if (!store) return;
|
|
491
311
|
|
|
@@ -506,27 +326,61 @@ export class CacheScope {
|
|
|
506
326
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
507
327
|
|
|
508
328
|
// Check if this is a partial request (navigation) vs document request
|
|
509
|
-
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
|
+
}
|
|
510
336
|
|
|
511
337
|
requestCtx.waitUntil(async () => {
|
|
338
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
339
|
+
debugCacheLog(
|
|
340
|
+
`[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
512
344
|
await handleStore.settled;
|
|
513
345
|
|
|
514
|
-
|
|
515
|
-
|
|
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).
|
|
516
355
|
if (!isPartial) {
|
|
517
|
-
const
|
|
518
|
-
(s) => s.component
|
|
356
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
357
|
+
(s) => s.component === null && s.type === "layout",
|
|
519
358
|
);
|
|
520
|
-
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
|
+
}
|
|
521
372
|
}
|
|
522
373
|
|
|
523
374
|
// Collect handle data for non-loader segments only
|
|
524
|
-
const handles
|
|
525
|
-
for (const seg of nonLoaderSegments) {
|
|
526
|
-
handles[seg.id] = handleStore.getDataForSegment(seg.id);
|
|
527
|
-
}
|
|
375
|
+
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
528
376
|
|
|
529
377
|
try {
|
|
378
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
379
|
+
debugCacheLog(
|
|
380
|
+
`[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
|
|
530
384
|
// Serialize non-loader segments only
|
|
531
385
|
const serializedSegments = await serializeSegments(nonLoaderSegments);
|
|
532
386
|
|
|
@@ -536,14 +390,20 @@ export class CacheScope {
|
|
|
536
390
|
expiresAt: Date.now() + ttl * 1000,
|
|
537
391
|
};
|
|
538
392
|
|
|
393
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
394
|
+
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
395
|
+
}
|
|
396
|
+
|
|
539
397
|
await store.set(key, data, ttl, swr);
|
|
540
398
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
}
|
|
547
407
|
} catch (error) {
|
|
548
408
|
console.error(`[CacheScope] Failed to cache ${key}:`, error);
|
|
549
409
|
}
|
|
@@ -556,7 +416,7 @@ export class CacheScope {
|
|
|
556
416
|
*/
|
|
557
417
|
export function createCacheScope(
|
|
558
418
|
config: { options: PartialCacheOptions | false } | undefined,
|
|
559
|
-
parent: CacheScope | null = null
|
|
419
|
+
parent: CacheScope | null = null,
|
|
560
420
|
): CacheScope | null {
|
|
561
421
|
if (!config) return parent; // No config, inherit parent
|
|
562
422
|
return new CacheScope(config.options, parent);
|