@rangojs/router 0.0.0-experimental.31 → 0.0.0-experimental.3232cd17
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 +4 -0
- package/README.md +198 -44
- package/dist/bin/rango.js +287 -105
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +3248 -1117
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +73 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +107 -1
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +245 -21
- package/skills/caching/SKILL.md +302 -6
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +270 -30
- package/skills/host-router/SKILL.md +82 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +49 -5
- package/skills/layout/SKILL.md +35 -9
- package/skills/links/SKILL.md +249 -17
- package/skills/loader/SKILL.md +294 -30
- package/skills/middleware/SKILL.md +52 -13
- package/skills/migrate-nextjs/SKILL.md +584 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +203 -7
- package/skills/prerender/SKILL.md +123 -100
- package/skills/rango/SKILL.md +250 -22
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +122 -47
- package/skills/route/SKILL.md +97 -5
- package/skills/router-setup/SKILL.md +90 -5
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +329 -27
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +67 -40
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +39 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +86 -147
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +148 -19
- package/src/browser/navigation-client.ts +187 -67
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +76 -67
- package/src/browser/navigation-transaction.ts +18 -66
- package/src/browser/partial-update.ts +123 -94
- package/src/browser/prefetch/cache.ts +214 -36
- package/src/browser/prefetch/fetch.ts +260 -38
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +126 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +93 -11
- package/src/browser/react/NavigationProvider.tsx +115 -34
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/filter-segment-order.ts +49 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +23 -69
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +22 -5
- package/src/browser/react/use-params.ts +20 -10
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +46 -11
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +11 -21
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +215 -76
- package/src/browser/scroll-restoration.ts +46 -39
- package/src/browser/segment-reconciler.ts +36 -9
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +176 -50
- package/src/browser/types.ts +95 -11
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +65 -40
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +137 -32
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +9 -2
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +278 -96
- package/src/build/route-types/scan-filter.ts +9 -2
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +149 -43
- package/src/cache/cache-scope.ts +148 -81
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2550 -93
- package/src/cache/cf/index.ts +11 -17
- package/src/cache/document-cache.ts +78 -27
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +23 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/taint.ts +55 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +108 -290
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +84 -2
- package/src/debug.ts +2 -2
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +70 -22
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +52 -26
- package/src/index.ts +100 -38
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +37 -41
- package/src/prerender.ts +198 -82
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +37 -0
- package/src/reverse.ts +65 -15
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +437 -274
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +113 -37
- package/src/route-definition/index.ts +3 -0
- package/src/route-definition/redirect.ts +52 -10
- package/src/route-definition/resolve-handler-use.ts +161 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +7 -17
- package/src/route-types.ts +37 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +108 -9
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +45 -22
- package/src/router/handler-context.ts +83 -41
- package/src/router/intercept-resolution.ts +25 -23
- package/src/router/lazy-includes.ts +19 -53
- package/src/router/loader-resolution.ts +213 -30
- package/src/router/logging.ts +5 -8
- package/src/router/manifest.ts +49 -45
- package/src/router/match-api.ts +121 -205
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +27 -6
- package/src/router/match-middleware/cache-lookup.ts +205 -249
- package/src/router/match-middleware/cache-store.ts +45 -32
- package/src/router/match-middleware/intercept-resolution.ts +8 -28
- package/src/router/match-middleware/segment-resolution.ts +52 -18
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +104 -40
- package/src/router/metrics.ts +5 -34
- package/src/router/middleware-types.ts +13 -142
- package/src/router/middleware.ts +173 -143
- package/src/router/navigation-snapshot.ts +131 -0
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +109 -63
- package/src/router/prerender-match.ts +192 -54
- package/src/router/preview-match.ts +32 -102
- package/src/router/request-classification.ts +276 -0
- package/src/router/revalidation.ts +63 -55
- package/src/router/route-snapshot.ts +244 -0
- package/src/router/router-context.ts +6 -28
- package/src/router/router-interfaces.ts +100 -35
- package/src/router/router-options.ts +91 -11
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +242 -75
- package/src/router/segment-resolution/helpers.ts +64 -25
- package/src/router/segment-resolution/loader-cache.ts +41 -37
- package/src/router/segment-resolution/revalidation.ts +456 -372
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +2 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +91 -46
- package/src/router/types.ts +10 -63
- package/src/router/url-params.ts +44 -0
- package/src/router.ts +134 -43
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +492 -383
- package/src/rsc/helpers.ts +162 -46
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +23 -3
- package/src/rsc/manifest-init.ts +33 -42
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +30 -3
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +90 -63
- package/src/rsc/rsc-rendering.ts +56 -54
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +74 -67
- package/src/rsc/ssr-setup.ts +18 -2
- package/src/rsc/types.ts +25 -6
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +134 -0
- package/src/segment-system.tsx +272 -129
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +309 -61
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +26 -24
- package/src/server/loader-registry.ts +10 -28
- package/src/server/request-context.ts +348 -128
- package/src/ssr/index.tsx +23 -15
- package/src/static-handler.ts +27 -18
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +17 -8
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +233 -81
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +44 -15
- package/src/types/request-scope.ts +107 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +19 -7
- package/src/types/segments.ts +37 -14
- package/src/urls/include-helper.ts +33 -70
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +58 -11
- package/src/urls/path-helper.ts +57 -111
- package/src/urls/pattern-types.ts +48 -19
- package/src/urls/response-types.ts +25 -22
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +346 -89
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +36 -38
- package/src/vite/discovery/discover-routers.ts +130 -85
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +192 -99
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/self-gen-tracking.ts +27 -1
- package/src/vite/discovery/state.ts +51 -6
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +8 -0
- package/src/vite/plugin-types.ts +187 -69
- package/src/vite/plugins/cjs-to-esm.ts +8 -18
- package/src/vite/plugins/client-ref-dedup.ts +16 -11
- package/src/vite/plugins/client-ref-hashing.ts +28 -15
- 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 +194 -0
- package/src/vite/plugins/expose-action-id.ts +49 -98
- package/src/vite/plugins/expose-id-utils.ts +11 -50
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +10 -48
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +20 -16
- package/src/vite/plugins/expose-internal-ids.ts +554 -317
- package/src/vite/plugins/performance-tracks.ts +89 -0
- package/src/vite/plugins/refresh-cmd.ts +89 -27
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +21 -25
- package/src/vite/plugins/version-plugin.ts +41 -20
- package/src/vite/plugins/virtual-entries.ts +2 -17
- package/src/vite/rango.ts +257 -289
- package/src/vite/router-discovery.ts +930 -140
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/banner.ts +4 -4
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +20 -52
- package/src/vite/utils/prerender-utils.ts +27 -29
- package/src/vite/utils/shared-utils.ts +92 -42
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
createClientTemporaryReferenceSet,
|
|
22
22
|
} from "@vitejs/plugin-rsc/rsc";
|
|
23
23
|
import { getRequestContext } from "../server/request-context.js";
|
|
24
|
+
import { isUnderTestRunner } from "../runtime-env.js";
|
|
24
25
|
import {
|
|
25
26
|
isTainted,
|
|
26
27
|
CACHED_FN_SYMBOL,
|
|
@@ -32,10 +33,21 @@ import {
|
|
|
32
33
|
export { isCachedFunction };
|
|
33
34
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
34
35
|
import { createHandleStore } from "../server/handle-store.js";
|
|
35
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
restoreHandles,
|
|
38
|
+
encodeHandles,
|
|
39
|
+
decodeHandles,
|
|
40
|
+
} from "./handle-snapshot.js";
|
|
36
41
|
import { startHandleCapture, type HandleCapture } from "./handle-capture.js";
|
|
37
42
|
import { sortedSearchString } from "./cache-key-utils.js";
|
|
38
43
|
import { runBackground } from "./background-task.js";
|
|
44
|
+
import {
|
|
45
|
+
normalizeTags,
|
|
46
|
+
recordRequestTags,
|
|
47
|
+
runWithCacheTagScope,
|
|
48
|
+
} from "./cache-tag.js";
|
|
49
|
+
import { reportCacheError } from "./cache-error.js";
|
|
50
|
+
import type { CacheItemResult } from "./types.js";
|
|
39
51
|
|
|
40
52
|
/**
|
|
41
53
|
* Convert encodeReply result to a stable string key.
|
|
@@ -48,6 +60,10 @@ async function replyToCacheKey(encoded: string | FormData): Promise<string> {
|
|
|
48
60
|
return text;
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
// Cached-fn ids already warned about running uncached under a test runner, so
|
|
64
|
+
// the test-ergonomics warning fires once per fn rather than once per call.
|
|
65
|
+
const warnedUncachedUnderTest = new Set<string>();
|
|
66
|
+
|
|
51
67
|
// ============================================================================
|
|
52
68
|
// Core: registerCachedFunction
|
|
53
69
|
// ============================================================================
|
|
@@ -70,9 +86,38 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
70
86
|
const store = requestCtx?._cacheStore;
|
|
71
87
|
const resolvedProfileName = profileName || "default";
|
|
72
88
|
|
|
73
|
-
// Bypass: no store or no getItem support
|
|
89
|
+
// Bypass: no store or no getItem support. Still run inside a tag scope so a
|
|
90
|
+
// cacheTag() call inside the function degrades to a no-op rather than
|
|
91
|
+
// throwing "must be called inside a use cache function" - adopting cacheTag()
|
|
92
|
+
// must not hard-fail in apps/tests without an item-capable cache configured.
|
|
93
|
+
// Note: the INSIDE_CACHE_EXEC guard (cookies()/headers()/ctx.set() rejection)
|
|
94
|
+
// is intentionally NOT stamped here. It is a cached-path-only check; in the
|
|
95
|
+
// bypass the body actually executes, so the guarded side effects take effect
|
|
96
|
+
// and nothing is lost on a (non-existent) hit. Same applies to the
|
|
97
|
+
// non-serializable-args bypass below.
|
|
74
98
|
if (!store?.getItem) {
|
|
75
|
-
|
|
99
|
+
// Test-ergonomics guard: under a test runner, a "use cache" function that
|
|
100
|
+
// executes with no item-capable store seeded is exercising the UNCACHED
|
|
101
|
+
// path — a green test that proves nothing about caching. Warn once per fn
|
|
102
|
+
// id so the author knows to seed a cacheStore. Advisory (never throws), so
|
|
103
|
+
// a test that DELIBERATELY runs uncached is unaffected. Gated on the test
|
|
104
|
+
// runner (process.env.VITEST, not folded) so production never evaluates it.
|
|
105
|
+
if (isUnderTestRunner() && !warnedUncachedUnderTest.has(id)) {
|
|
106
|
+
warnedUncachedUnderTest.add(id);
|
|
107
|
+
console.warn(
|
|
108
|
+
`[rango] "use cache" function "${id}" executed but no cacheStore was ` +
|
|
109
|
+
`seeded; the cached path is NOT under test (it ran uncached). Pass ` +
|
|
110
|
+
`{ cacheStore, cacheProfiles } to runLoader/runMiddleware/renderHandler/` +
|
|
111
|
+
`runInRequestContext (or configure createRouter({ cache }) for dispatch) ` +
|
|
112
|
+
`to exercise it.`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
116
|
+
const result = await scoped.result;
|
|
117
|
+
// Still record the runtime tags into the request set so a cacheTag() in an
|
|
118
|
+
// uncached function tags the document, even with no item-capable store.
|
|
119
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
120
|
+
return result;
|
|
76
121
|
}
|
|
77
122
|
|
|
78
123
|
// Resolve profile strictly from request-scoped config (set by the
|
|
@@ -155,40 +200,57 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
155
200
|
cacheKey = `use-cache:${id}`;
|
|
156
201
|
}
|
|
157
202
|
} catch {
|
|
158
|
-
// Non-serializable args: run uncached
|
|
159
|
-
|
|
203
|
+
// Non-serializable args: run uncached (within a tag scope so cacheTag()
|
|
204
|
+
// still does not throw). Record runtime tags so the document union still
|
|
205
|
+
// sees them even though this call is not itself cached.
|
|
206
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
207
|
+
const result = await scoped.result;
|
|
208
|
+
recordRequestTags(scoped.tags, requestCtx);
|
|
209
|
+
return result;
|
|
160
210
|
}
|
|
161
211
|
|
|
162
212
|
// Cache lookup
|
|
163
213
|
const cached = await store.getItem(cacheKey);
|
|
164
214
|
|
|
215
|
+
// Serve a cached entry on the hit path: deserialize the stored value,
|
|
216
|
+
// replay handle data (gated on tainted args), and surface the entry's tags
|
|
217
|
+
// to the request set (the function did not re-run, so its runtime cacheTag()
|
|
218
|
+
// tags are only available from the stored entry). Shared by the fresh-hit
|
|
219
|
+
// and stale-hit branches; the only divergence is the stale branch scheduling
|
|
220
|
+
// background revalidation, which it does after this returns.
|
|
221
|
+
const serveCached = async (entry: CacheItemResult): Promise<any> => {
|
|
222
|
+
const result = await deserializeResult(entry.value);
|
|
223
|
+
if (entry.handles && hasTaintedArgs) {
|
|
224
|
+
const handleStore = requestCtx?._handleStore;
|
|
225
|
+
if (handleStore) {
|
|
226
|
+
const r = await decodeHandles(entry.handles);
|
|
227
|
+
if (r) restoreHandles(r, handleStore);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
recordRequestTags(entry.tags, requestCtx);
|
|
231
|
+
return result;
|
|
232
|
+
};
|
|
233
|
+
|
|
165
234
|
if (cached && !cached.shouldRevalidate) {
|
|
166
235
|
// Fresh hit: deserialize and return
|
|
167
236
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
// Deserialization failed, fall through to fresh execution
|
|
237
|
+
return await serveCached(cached);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
// The stored value is corrupt/partial (failed RSC deserialize). Report
|
|
240
|
+
// it, then fall through to fresh execution - the miss path below re-runs
|
|
241
|
+
// and setItem() overwrites the faulty entry under the same key (self-heal).
|
|
242
|
+
reportCacheError(
|
|
243
|
+
error,
|
|
244
|
+
"cache-corrupt",
|
|
245
|
+
`[use cache] "${id}" fresh-hit`,
|
|
246
|
+
);
|
|
179
247
|
}
|
|
180
248
|
}
|
|
181
249
|
|
|
182
250
|
if (cached?.shouldRevalidate) {
|
|
183
251
|
// Stale hit: return stale value, revalidate in background
|
|
184
252
|
try {
|
|
185
|
-
const result = await
|
|
186
|
-
if (cached.handles && hasTaintedArgs) {
|
|
187
|
-
const handleStore = requestCtx?._handleStore;
|
|
188
|
-
if (handleStore) {
|
|
189
|
-
restoreHandles(cached.handles, handleStore);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
253
|
+
const result = await serveCached(cached);
|
|
192
254
|
// Background revalidation — must capture handles if tainted args present.
|
|
193
255
|
// Use an isolated handle store so background pushes don't pollute the
|
|
194
256
|
// live response or throw LateHandlePushError on the completed store.
|
|
@@ -214,11 +276,21 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
214
276
|
bgStopCapture = c.stop;
|
|
215
277
|
}
|
|
216
278
|
|
|
217
|
-
// Stamp tainted
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
//
|
|
279
|
+
// Stamp tainted ARGS only — not requestCtx. The args stamp guards
|
|
280
|
+
// direct ctx method calls (ctx.set, ctx.header, ctx.onResponse, etc.)
|
|
281
|
+
// which is sufficient for correctness.
|
|
282
|
+
//
|
|
283
|
+
// We intentionally skip stamping requestCtx here because:
|
|
284
|
+
// 1. runBackground starts the async task synchronously (before the
|
|
285
|
+
// first await), so stampCacheExec would pollute the shared
|
|
286
|
+
// requestCtx while the foreground pipeline is still running.
|
|
287
|
+
// This causes assertNotInsideCacheExec to fire when cache-store
|
|
288
|
+
// later calls requestCtx.onResponse().
|
|
289
|
+
// 2. requestCtx methods are closure-bound to the original ctx, so
|
|
290
|
+
// neither Object.create() nor a proxy can isolate the stamp.
|
|
291
|
+
// 3. The foreground miss path already stamps requestCtx and catches
|
|
292
|
+
// cookies()/headers() misuse on first execution. The background
|
|
293
|
+
// re-runs the same function with the same request.
|
|
222
294
|
const bgTaintedArgs: unknown[] = [];
|
|
223
295
|
for (const arg of args) {
|
|
224
296
|
if (isTainted(arg)) {
|
|
@@ -226,32 +298,47 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
226
298
|
bgTaintedArgs.push(arg);
|
|
227
299
|
}
|
|
228
300
|
}
|
|
229
|
-
if (requestCtx) {
|
|
230
|
-
stampCacheExec(requestCtx as object);
|
|
231
|
-
}
|
|
232
301
|
|
|
233
302
|
try {
|
|
234
|
-
const
|
|
303
|
+
const scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
304
|
+
const freshResult = await scoped.result;
|
|
235
305
|
bgStopCapture?.();
|
|
306
|
+
// Merge profile/DSL tags with runtime cacheTag() tags, read after
|
|
307
|
+
// awaiting so post-await cacheTag() calls are included. Normalize
|
|
308
|
+
// (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
309
|
+
const freshTags = [
|
|
310
|
+
...new Set(
|
|
311
|
+
normalizeTags([...(profile.tags ?? []), ...scoped.tags]),
|
|
312
|
+
),
|
|
313
|
+
];
|
|
314
|
+
recordRequestTags(freshTags, requestCtx);
|
|
236
315
|
const serialized = await serializeResult(freshResult);
|
|
237
316
|
if (serialized !== null) {
|
|
317
|
+
const encodedHandles = bgCapture?.data
|
|
318
|
+
? await encodeHandles(bgCapture.data)
|
|
319
|
+
: undefined;
|
|
238
320
|
await store.setItem!(cacheKey, serialized, {
|
|
239
|
-
handles:
|
|
321
|
+
handles: encodedHandles,
|
|
240
322
|
ttl: profile.ttl,
|
|
241
323
|
swr: profile.swr,
|
|
242
|
-
tags:
|
|
324
|
+
tags: freshTags.length > 0 ? freshTags : undefined,
|
|
243
325
|
});
|
|
244
326
|
}
|
|
245
327
|
} catch (bgError) {
|
|
246
328
|
bgStopCapture?.();
|
|
247
|
-
requestCtx
|
|
329
|
+
// Pass requestCtx explicitly: this runs in a detached background
|
|
330
|
+
// task where the ALS context is gone, so onError can only fire if
|
|
331
|
+
// we hand it the context captured up front.
|
|
332
|
+
reportCacheError(
|
|
333
|
+
bgError,
|
|
334
|
+
"stale-revalidation",
|
|
335
|
+
"[use cache] background revalidation failed",
|
|
336
|
+
requestCtx,
|
|
337
|
+
);
|
|
248
338
|
} finally {
|
|
249
339
|
for (const arg of bgTaintedArgs) {
|
|
250
340
|
unstampCacheExec(arg as object);
|
|
251
341
|
}
|
|
252
|
-
if (requestCtx) {
|
|
253
|
-
unstampCacheExec(requestCtx as object);
|
|
254
|
-
}
|
|
255
342
|
// Restore original handle store
|
|
256
343
|
if (originalHandleStore && requestCtx) {
|
|
257
344
|
requestCtx._handleStore = originalHandleStore;
|
|
@@ -259,8 +346,14 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
259
346
|
}
|
|
260
347
|
});
|
|
261
348
|
return result;
|
|
262
|
-
} catch {
|
|
263
|
-
//
|
|
349
|
+
} catch (error) {
|
|
350
|
+
// Stale value is corrupt/partial; report and fall through to a fresh
|
|
351
|
+
// execution, which overwrites the faulty entry under the same key.
|
|
352
|
+
reportCacheError(
|
|
353
|
+
error,
|
|
354
|
+
"cache-corrupt",
|
|
355
|
+
`[use cache] "${id}" stale-hit`,
|
|
356
|
+
);
|
|
264
357
|
}
|
|
265
358
|
}
|
|
266
359
|
|
|
@@ -293,8 +386,10 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
293
386
|
}
|
|
294
387
|
|
|
295
388
|
let result: any;
|
|
389
|
+
let scoped: ReturnType<typeof runWithCacheTagScope>;
|
|
296
390
|
try {
|
|
297
|
-
|
|
391
|
+
scoped = runWithCacheTagScope(() => fn.apply(this, args));
|
|
392
|
+
result = await scoped.result;
|
|
298
393
|
} finally {
|
|
299
394
|
// Decrement ref count; symbol is deleted when it reaches zero
|
|
300
395
|
for (const arg of taintedArgs) {
|
|
@@ -307,17 +402,28 @@ export function registerCachedFunction<T extends (...args: any[]) => any>(
|
|
|
307
402
|
stopCapture?.();
|
|
308
403
|
}
|
|
309
404
|
|
|
405
|
+
// Merge profile/DSL tags with runtime cacheTag() tags. Read scoped.tags
|
|
406
|
+
// after awaiting result so post-await cacheTag() calls are included.
|
|
407
|
+
// Normalize (drops empty profile tags, matching the invalidate path) + dedupe.
|
|
408
|
+
const allTags = [
|
|
409
|
+
...new Set(normalizeTags([...(profile.tags ?? []), ...scoped!.tags])),
|
|
410
|
+
];
|
|
411
|
+
recordRequestTags(allTags, requestCtx);
|
|
412
|
+
|
|
310
413
|
// Serialize and store — fully non-blocking when waitUntil is available.
|
|
311
414
|
// The response does not need to wait for serialization or the store write.
|
|
312
415
|
const cacheWrite = async () => {
|
|
313
416
|
try {
|
|
314
417
|
const serialized = await serializeResult(result);
|
|
315
418
|
if (serialized !== null) {
|
|
419
|
+
const encodedHandles = capture?.data
|
|
420
|
+
? await encodeHandles(capture.data)
|
|
421
|
+
: undefined;
|
|
316
422
|
await store.setItem!(cacheKey, serialized, {
|
|
317
|
-
handles:
|
|
423
|
+
handles: encodedHandles,
|
|
318
424
|
ttl: profile.ttl,
|
|
319
425
|
swr: profile.swr,
|
|
320
|
-
tags:
|
|
426
|
+
tags: allTags.length > 0 ? allTags : undefined,
|
|
321
427
|
});
|
|
322
428
|
}
|
|
323
429
|
} catch (writeError) {
|
package/src/cache/cache-scope.ts
CHANGED
|
@@ -16,14 +16,31 @@ import {
|
|
|
16
16
|
getRequestContext,
|
|
17
17
|
_getRequestContext,
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
|
+
import { recordRequestTags } from "./cache-tag.js";
|
|
20
|
+
import { reportCacheError } from "./cache-error.js";
|
|
19
21
|
import { serializeSegments, deserializeSegments } from "./segment-codec.js";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
captureHandles,
|
|
24
|
+
restoreHandles,
|
|
25
|
+
encodeHandles,
|
|
26
|
+
decodeHandles,
|
|
27
|
+
} from "./handle-snapshot.js";
|
|
21
28
|
import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
|
|
22
29
|
import {
|
|
23
30
|
DEFAULT_ROUTE_TTL,
|
|
24
31
|
resolveCacheKey,
|
|
25
32
|
resolveCacheStore,
|
|
33
|
+
resolveTagsOption,
|
|
26
34
|
} from "./cache-policy.js";
|
|
35
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
36
|
+
|
|
37
|
+
export function resolveCacheTags(
|
|
38
|
+
config: PartialCacheOptions | false,
|
|
39
|
+
ctx: RequestContext | undefined,
|
|
40
|
+
): string[] | undefined {
|
|
41
|
+
if (config === false) return undefined;
|
|
42
|
+
return resolveTagsOption(config.tags, ctx, "CacheScope");
|
|
43
|
+
}
|
|
27
44
|
|
|
28
45
|
function debugCacheLog(message: string): void {
|
|
29
46
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -31,17 +48,6 @@ function debugCacheLog(message: string): void {
|
|
|
31
48
|
}
|
|
32
49
|
}
|
|
33
50
|
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// Key Generation (internal)
|
|
36
|
-
// ============================================================================
|
|
37
|
-
|
|
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.
|
|
43
|
-
* @internal
|
|
44
|
-
*/
|
|
45
51
|
function getCacheKeyBase(
|
|
46
52
|
host: string,
|
|
47
53
|
pathname: string,
|
|
@@ -57,23 +63,13 @@ function getCacheKeyBase(
|
|
|
57
63
|
return key;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
/**
|
|
61
|
-
* Generate default cache key for a route request.
|
|
62
|
-
* Includes pathname, route params, and user-facing search params for
|
|
63
|
-
* correct scoping. Internal _rsc* params are excluded.
|
|
64
|
-
* Includes request type prefix since they produce different segment sets:
|
|
65
|
-
* - doc: document requests (full page load)
|
|
66
|
-
* - partial: navigation requests (client-side navigation)
|
|
67
|
-
* - intercept: intercept navigation (modal/overlay routes)
|
|
68
|
-
* @internal
|
|
69
|
-
*/
|
|
70
66
|
function getDefaultRouteCacheKey(
|
|
71
67
|
pathname: string,
|
|
72
68
|
params?: Record<string, string>,
|
|
73
69
|
isIntercept?: boolean,
|
|
74
70
|
): string {
|
|
75
71
|
const ctx = getRequestContext();
|
|
76
|
-
const isPartial = ctx?.
|
|
72
|
+
const isPartial = ctx?.originalUrl?.searchParams.has("_rsc_partial") ?? false;
|
|
77
73
|
const searchParams = ctx?.url.searchParams;
|
|
78
74
|
const host = ctx?.url.host ?? "localhost";
|
|
79
75
|
|
|
@@ -187,6 +183,32 @@ export class CacheScope {
|
|
|
187
183
|
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
188
184
|
}
|
|
189
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Evaluate the cache `condition` predicate. Returns false (skip the cache
|
|
188
|
+
* operation) when the predicate returns false or throws; returns true when
|
|
189
|
+
* there is no condition or no request context to evaluate it against.
|
|
190
|
+
*/
|
|
191
|
+
private conditionAllows(op: "read" | "write"): boolean {
|
|
192
|
+
if (this.config === false || !this.config.condition) return true;
|
|
193
|
+
const requestCtx = getRequestContext();
|
|
194
|
+
if (!requestCtx) return true;
|
|
195
|
+
try {
|
|
196
|
+
if (!this.config.condition(requestCtx)) {
|
|
197
|
+
debugCacheLog(
|
|
198
|
+
`[CacheScope] condition returned false, skipping cache ${op}`,
|
|
199
|
+
);
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
console.error(
|
|
205
|
+
`[CacheScope] condition function threw, skipping cache ${op}:`,
|
|
206
|
+
error,
|
|
207
|
+
);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
190
212
|
/**
|
|
191
213
|
* Lookup cached segments for a route (single cache entry per request).
|
|
192
214
|
* Returns { segments, shouldRevalidate } or null if cache miss.
|
|
@@ -204,27 +226,7 @@ export class CacheScope {
|
|
|
204
226
|
shouldRevalidate: boolean;
|
|
205
227
|
} | null> {
|
|
206
228
|
if (!this.enabled) return null;
|
|
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
|
-
}
|
|
229
|
+
if (!this.conditionAllows("read")) return null;
|
|
228
230
|
|
|
229
231
|
const store = this.getStore();
|
|
230
232
|
if (!store) return null;
|
|
@@ -242,13 +244,43 @@ export class CacheScope {
|
|
|
242
244
|
|
|
243
245
|
const { data: cached, shouldRevalidate } = result;
|
|
244
246
|
|
|
245
|
-
// Deserialize segments
|
|
246
|
-
|
|
247
|
+
// Deserialize segments. A failure means the cached segments are corrupt/
|
|
248
|
+
// partial: evict the entry (self-heal - the re-render re-caches under the
|
|
249
|
+
// same key) and report it as corruption, distinct from a transient infra
|
|
250
|
+
// error (handled by the outer catch).
|
|
251
|
+
let segments: ResolvedSegment[];
|
|
252
|
+
try {
|
|
253
|
+
segments = await deserializeSegments(cached.segments);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
reportCacheError(
|
|
256
|
+
error,
|
|
257
|
+
"cache-corrupt",
|
|
258
|
+
`[CacheScope] ${key}: corrupt cached segments, evicting`,
|
|
259
|
+
);
|
|
260
|
+
await store
|
|
261
|
+
.delete(key)
|
|
262
|
+
.catch((e) =>
|
|
263
|
+
reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
|
|
264
|
+
);
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// A hit serves content that was tagged at write time, so the document
|
|
269
|
+
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
270
|
+
// to invalidate any full-page entry built on top of it. The write path
|
|
271
|
+
// records via cacheRoute (resolveCacheTags); the hit path records here.
|
|
272
|
+
recordRequestTags(cached.tags);
|
|
247
273
|
|
|
248
|
-
// Replay handle data
|
|
274
|
+
// Replay handle data. An empty string means the route pushed no handles —
|
|
275
|
+
// skip the decode entirely (the common case). Otherwise decode the
|
|
276
|
+
// Flight-encoded blob; a decode failure skips handle restore but keeps the
|
|
277
|
+
// valid cached segments.
|
|
249
278
|
const handleStore = _getRequestContext()?._handleStore;
|
|
250
|
-
if (handleStore) {
|
|
251
|
-
|
|
279
|
+
if (handleStore && cached.handles) {
|
|
280
|
+
const handlesRecord = await decodeHandles(cached.handles);
|
|
281
|
+
if (handlesRecord) {
|
|
282
|
+
restoreHandles(handlesRecord, handleStore);
|
|
283
|
+
}
|
|
252
284
|
}
|
|
253
285
|
|
|
254
286
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -262,7 +294,7 @@ export class CacheScope {
|
|
|
262
294
|
|
|
263
295
|
return { segments, shouldRevalidate };
|
|
264
296
|
} catch (error) {
|
|
265
|
-
|
|
297
|
+
reportCacheError(error, "cache-read", `[CacheScope] lookup ${key}`);
|
|
266
298
|
return null;
|
|
267
299
|
}
|
|
268
300
|
}
|
|
@@ -284,27 +316,7 @@ export class CacheScope {
|
|
|
284
316
|
isIntercept?: boolean,
|
|
285
317
|
): Promise<void> {
|
|
286
318
|
if (!this.enabled || segments.length === 0) return;
|
|
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
|
-
}
|
|
319
|
+
if (!this.conditionAllows("write")) return;
|
|
308
320
|
|
|
309
321
|
const store = this.getStore();
|
|
310
322
|
if (!store) return;
|
|
@@ -325,34 +337,85 @@ export class CacheScope {
|
|
|
325
337
|
// Resolve cache key early (while request context is available)
|
|
326
338
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
327
339
|
|
|
340
|
+
// Resolve tags early (while request context is available, before waitUntil)
|
|
341
|
+
const tags = resolveCacheTags(this.config, requestCtx);
|
|
342
|
+
recordRequestTags(tags, requestCtx);
|
|
343
|
+
|
|
328
344
|
// Check if this is a partial request (navigation) vs document request
|
|
329
|
-
const isPartial = requestCtx.
|
|
345
|
+
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
346
|
+
|
|
347
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
348
|
+
debugCacheLog(
|
|
349
|
+
`[CacheScope] cacheRoute: scheduling waitUntil for ${key} (${nonLoaderSegments.length} segments, isPartial=${isPartial})`,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
330
352
|
|
|
331
353
|
requestCtx.waitUntil(async () => {
|
|
354
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
355
|
+
debugCacheLog(
|
|
356
|
+
`[CacheScope] waitUntil: awaiting handleStore.settled for ${key}`,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
332
360
|
await handleStore.settled;
|
|
333
361
|
|
|
334
|
-
|
|
335
|
-
|
|
362
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
363
|
+
debugCacheLog(`[CacheScope] waitUntil: handleStore settled for ${key}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// For document requests: only cache if layout segments have components
|
|
367
|
+
// (complete render). Parallel and route segments may legitimately have
|
|
368
|
+
// null components — UI-less @meta parallels return null, and void route
|
|
369
|
+
// handlers produce null when the UI lives in parallel slots/layouts.
|
|
370
|
+
// Partial requests always allow null components (client already has them).
|
|
336
371
|
if (!isPartial) {
|
|
337
|
-
const
|
|
338
|
-
(s) => s.component
|
|
372
|
+
const hasIncompleteLayouts = nonLoaderSegments.some(
|
|
373
|
+
(s) => s.component === null && s.type === "layout",
|
|
339
374
|
);
|
|
340
|
-
if (
|
|
375
|
+
if (hasIncompleteLayouts) {
|
|
376
|
+
const nullSegments = nonLoaderSegments
|
|
377
|
+
.filter((s) => s.component === null && s.type === "layout")
|
|
378
|
+
.map((s) => s.id);
|
|
379
|
+
const error = new Error(
|
|
380
|
+
`[CacheScope] Cache write skipped: layout segments have null components ` +
|
|
381
|
+
`(${nullSegments.join(", ")}). This indicates an incomplete render — ` +
|
|
382
|
+
`layout handlers must return JSX for document requests to be cacheable.`,
|
|
383
|
+
);
|
|
384
|
+
error.name = "CacheScopeInvariantError";
|
|
385
|
+
console.error(error.message);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
341
388
|
}
|
|
342
389
|
|
|
343
390
|
// Collect handle data for non-loader segments only
|
|
344
391
|
const handles = captureHandles(nonLoaderSegments, handleStore);
|
|
345
392
|
|
|
346
393
|
try {
|
|
347
|
-
|
|
348
|
-
|
|
394
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
395
|
+
debugCacheLog(
|
|
396
|
+
`[CacheScope] waitUntil: serializing ${nonLoaderSegments.length} segments for ${key}`,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Serialize segments and Flight-encode handles in parallel. Handles go
|
|
401
|
+
// through the codec (not raw into the entry) so Promise/ReactNode handle
|
|
402
|
+
// values survive a JSON-serializing store — see encodeHandles.
|
|
403
|
+
const [serializedSegments, encodedHandles] = await Promise.all([
|
|
404
|
+
serializeSegments(nonLoaderSegments),
|
|
405
|
+
encodeHandles(handles),
|
|
406
|
+
]);
|
|
349
407
|
|
|
350
408
|
const data: CachedEntryData = {
|
|
351
409
|
segments: serializedSegments,
|
|
352
|
-
handles,
|
|
410
|
+
handles: encodedHandles,
|
|
353
411
|
expiresAt: Date.now() + ttl * 1000,
|
|
412
|
+
tags,
|
|
354
413
|
};
|
|
355
414
|
|
|
415
|
+
if (INTERNAL_RANGO_DEBUG) {
|
|
416
|
+
debugCacheLog(`[CacheScope] waitUntil: calling store.set for ${key}`);
|
|
417
|
+
}
|
|
418
|
+
|
|
356
419
|
await store.set(key, data, ttl, swr);
|
|
357
420
|
|
|
358
421
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -364,7 +427,11 @@ export class CacheScope {
|
|
|
364
427
|
);
|
|
365
428
|
}
|
|
366
429
|
} catch (error) {
|
|
367
|
-
|
|
430
|
+
reportCacheError(
|
|
431
|
+
error,
|
|
432
|
+
"cache-write",
|
|
433
|
+
`[CacheScope] Failed to cache ${key}`,
|
|
434
|
+
);
|
|
368
435
|
}
|
|
369
436
|
});
|
|
370
437
|
}
|