@rangojs/router 0.0.0-experimental.eb0645d3 → 0.0.0-experimental.f1468e3c
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 +8 -0
- package/README.md +126 -16
- package/dist/bin/rango.js +319 -95
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +2724 -1053
- package/package.json +68 -14
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +64 -2
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +224 -32
- package/skills/caching/SKILL.md +279 -17
- package/skills/composability/SKILL.md +27 -3
- package/skills/css/SKILL.md +76 -0
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +11 -9
- package/skills/hooks/SKILL.md +243 -29
- package/skills/host-router/SKILL.md +83 -23
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +68 -19
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +190 -23
- package/skills/loader/SKILL.md +235 -9
- package/skills/middleware/SKILL.md +18 -10
- package/skills/migrate-nextjs/SKILL.md +43 -19
- package/skills/migrate-react-router/SKILL.md +8 -2
- package/skills/mime-routes/SKILL.md +28 -1
- package/skills/observability/SKILL.md +172 -0
- package/skills/parallel/SKILL.md +18 -7
- package/skills/prerender/SKILL.md +65 -60
- package/skills/rango/SKILL.md +251 -24
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +115 -48
- package/skills/route/SKILL.md +46 -5
- package/skills/router-setup/SKILL.md +30 -8
- package/skills/scripts/SKILL.md +179 -0
- package/skills/server-actions/SKILL.md +775 -0
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +130 -0
- package/skills/testing/bindings.md +103 -0
- package/skills/testing/cache-prerender.md +127 -0
- package/skills/testing/client-components.md +124 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +91 -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 +122 -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 +123 -0
- package/skills/typesafety/SKILL.md +322 -29
- package/skills/use-cache/SKILL.md +57 -14
- package/skills/view-transitions/SKILL.md +337 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- 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/connection-warmup.ts +134 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +192 -150
- 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/navigation-bridge.ts +94 -25
- package/src/browser/navigation-client.ts +121 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +115 -67
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/network-error-handler.ts +34 -7
- package/src/browser/partial-update.ts +147 -128
- package/src/browser/prefetch/cache.ts +107 -56
- package/src/browser/prefetch/fetch.ts +204 -34
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +158 -76
- package/src/browser/react/Link.tsx +30 -7
- package/src/browser/react/NavigationProvider.tsx +283 -118
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/deferred-handle-resolution.ts +75 -0
- package/src/browser/react/filter-segment-order.ts +66 -7
- package/src/browser/react/index.ts +0 -48
- package/src/browser/react/location-state-shared.ts +178 -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 +17 -14
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +33 -8
- package/src/browser/react/use-navigation.ts +10 -5
- package/src/browser/react/use-params.ts +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +25 -3
- 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 +99 -8
- package/src/browser/rsc-router.tsx +91 -24
- package/src/browser/scroll-restoration.ts +30 -17
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +214 -55
- package/src/browser/types.ts +80 -9
- package/src/browser/validate-redirect-origin.ts +43 -16
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -1
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +117 -14
- package/src/build/route-types/ast-route-extraction.ts +15 -8
- package/src/build/route-types/codegen.ts +16 -5
- package/src/build/route-types/include-resolution.ts +117 -23
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +22 -6
- package/src/build/route-types/router-processing.ts +55 -28
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +216 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-key-utils.ts +29 -13
- package/src/cache/cache-policy.ts +108 -34
- package/src/cache/cache-runtime.ts +224 -41
- package/src/cache/cache-scope.ts +188 -82
- package/src/cache/cache-tag.ts +103 -0
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +1989 -378
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +89 -21
- package/src/cache/handle-snapshot.ts +70 -0
- package/src/cache/index.ts +10 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +46 -31
- package/src/cache/read-through-swr.ts +56 -12
- package/src/cache/segment-codec.ts +9 -17
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +37 -100
- package/src/client.rsc.tsx +44 -21
- package/src/client.tsx +36 -61
- package/src/cloudflare/index.ts +11 -0
- package/src/cloudflare/tracing.ts +109 -0
- package/src/component-utils.ts +19 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +18 -6
- package/src/decode-loader-results.ts +52 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/encode-kv.ts +49 -0
- package/src/errors.ts +30 -4
- package/src/escape-script.ts +52 -0
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +62 -19
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +37 -8
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +51 -40
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +9 -60
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +23 -52
- 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 +96 -12
- package/src/index.ts +94 -14
- package/src/internal-debug.ts +11 -10
- 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/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +16 -16
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/regex-escape.ts +8 -0
- package/src/render-error-thrower.tsx +20 -0
- package/src/response-utils.ts +34 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +19 -77
- package/src/route-definition/dsl-helpers.ts +304 -309
- package/src/route-definition/helper-factories.ts +28 -140
- package/src/route-definition/helpers-types.ts +82 -55
- package/src/route-definition/index.ts +1 -2
- package/src/route-definition/redirect.ts +44 -11
- package/src/route-definition/resolve-handler-use.ts +12 -1
- package/src/route-definition/use-item-types.ts +29 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -46
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +73 -25
- package/src/router/error-handling.ts +45 -18
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +27 -43
- package/src/router/instrument.ts +350 -0
- package/src/router/intercept-resolution.ts +39 -20
- package/src/router/lazy-includes.ts +10 -47
- package/src/router/loader-resolution.ts +155 -72
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- package/src/router/match-api.ts +9 -24
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +58 -58
- package/src/router/match-middleware/background-revalidation.ts +40 -24
- package/src/router/match-middleware/cache-lookup.ts +159 -285
- package/src/router/match-middleware/cache-store.ts +64 -52
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -22
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +44 -74
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +7 -134
- package/src/router/middleware.ts +247 -166
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +85 -94
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/prerender-match.ts +104 -65
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +123 -73
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +3 -28
- package/src/router/router-interfaces.ts +83 -35
- package/src/router/router-options.ts +136 -5
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +97 -84
- package/src/router/segment-resolution/helpers.ts +86 -6
- package/src/router/segment-resolution/loader-cache.ts +76 -39
- package/src/router/segment-resolution/revalidation.ts +272 -320
- 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 +56 -0
- package/src/router/segment-resolution.ts +5 -1
- package/src/router/segment-wrappers.ts +6 -5
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +161 -199
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/tracing.ts +206 -0
- package/src/router/trie-matching.ts +162 -64
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +110 -55
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +264 -220
- package/src/rsc/helpers.ts +100 -6
- package/src/rsc/index.ts +2 -5
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/loader-fetch.ts +114 -38
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +117 -11
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +88 -188
- package/src/rsc/rsc-rendering.ts +98 -76
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +281 -117
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/transition-gate.ts +89 -0
- package/src/rsc/types.ts +23 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +35 -30
- package/src/segment-loader-promise.ts +31 -4
- package/src/segment-system.tsx +254 -143
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +163 -51
- package/src/server/cookie-parse.ts +32 -0
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +21 -38
- package/src/server/loader-registry.ts +33 -42
- package/src/server/request-context.ts +287 -178
- package/src/ssr/index.tsx +21 -16
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +162 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +701 -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 +257 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +371 -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 +357 -0
- package/src/testing/render-route.tsx +581 -0
- package/src/testing/run-loader.ts +385 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/run-transition-when.ts +164 -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 +20 -58
- package/src/theme/ThemeScript.tsx +7 -9
- package/src/theme/constants.ts +52 -13
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +22 -21
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +110 -62
- package/src/types/index.ts +3 -10
- package/src/types/loader-types.ts +11 -9
- package/src/types/request-scope.ts +112 -0
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +135 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +29 -12
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +22 -29
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -19
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +185 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +126 -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 +96 -68
- 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 +44 -0
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +2 -0
- package/src/vite/inject-client-debug.ts +36 -0
- package/src/vite/plugin-types.ts +126 -8
- package/src/vite/plugins/cjs-to-esm.ts +16 -19
- 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-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +48 -95
- package/src/vite/plugins/expose-id-utils.ts +88 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +101 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +11 -90
- package/src/vite/plugins/expose-ids/loader-transform.ts +14 -24
- package/src/vite/plugins/expose-ids/router-transform.ts +118 -29
- package/src/vite/plugins/expose-internal-ids.ts +505 -486
- package/src/vite/plugins/performance-tracks.ts +26 -25
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +73 -83
- package/src/vite/plugins/version-injector.ts +40 -29
- package/src/vite/plugins/version-plugin.ts +37 -40
- package/src/vite/plugins/virtual-entries.ts +39 -25
- package/src/vite/rango.ts +109 -118
- package/src/vite/router-discovery.ts +718 -119
- package/src/vite/utils/ast-handler-extract.ts +26 -35
- package/src/vite/utils/banner.ts +1 -1
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/directive-prologue.ts +40 -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 +54 -39
- package/src/vite/utils/shared-utils.ts +90 -41
- 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/network-error-thrower.tsx +0 -23
- package/src/router/middleware-cookies.ts +0 -55
package/src/cache/cache-scope.ts
CHANGED
|
@@ -16,14 +16,38 @@ import {
|
|
|
16
16
|
getRequestContext,
|
|
17
17
|
_getRequestContext,
|
|
18
18
|
} from "../server/request-context.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { recordRequestTags } from "./cache-tag.js";
|
|
20
|
+
import { reportCacheError } from "./cache-error.js";
|
|
21
|
+
// segment-codec is the only module on cache-scope's import graph that eagerly
|
|
22
|
+
// pulls @vitejs/plugin-rsc (a virtual: module the plain node/vitest runner cannot
|
|
23
|
+
// resolve). It is imported LAZILY at the two call sites below (deserializeSegments
|
|
24
|
+
// in lookupRoute, serializeSegments in cacheRoute) so that requiring cache-scope —
|
|
25
|
+
// e.g. dispatch's lazy `import("../cache/cache-scope.js")` for the response-route
|
|
26
|
+
// cache path — does not crash a consumer test that never mocks plugin-rsc. Behavior
|
|
27
|
+
// is unchanged: both methods are async and already awaited the codec.
|
|
28
|
+
import {
|
|
29
|
+
captureHandles,
|
|
30
|
+
restoreHandles,
|
|
31
|
+
encodeHandles,
|
|
32
|
+
decodeHandles,
|
|
33
|
+
} from "./handle-snapshot.js";
|
|
21
34
|
import { sortedSearchString, sortedRouteParams } from "./cache-key-utils.js";
|
|
22
35
|
import {
|
|
23
36
|
DEFAULT_ROUTE_TTL,
|
|
37
|
+
isFiniteNonNegativeSeconds,
|
|
24
38
|
resolveCacheKey,
|
|
25
39
|
resolveCacheStore,
|
|
40
|
+
resolveTagsOption,
|
|
26
41
|
} from "./cache-policy.js";
|
|
42
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
43
|
+
|
|
44
|
+
export function resolveCacheTags(
|
|
45
|
+
config: PartialCacheOptions | false,
|
|
46
|
+
ctx: RequestContext | undefined,
|
|
47
|
+
): string[] | undefined {
|
|
48
|
+
if (config === false) return undefined;
|
|
49
|
+
return resolveTagsOption(config.tags, ctx, "CacheScope");
|
|
50
|
+
}
|
|
27
51
|
|
|
28
52
|
function debugCacheLog(message: string): void {
|
|
29
53
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -31,17 +55,36 @@ function debugCacheLog(message: string): void {
|
|
|
31
55
|
}
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
// ============================================================================
|
|
35
|
-
// Key Generation (internal)
|
|
36
|
-
// ============================================================================
|
|
37
|
-
|
|
38
58
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
59
|
+
* A finite, non-negative seconds value? A NaN/Infinity ttl/swr (from a bad
|
|
60
|
+
* cache() option or store defaults) flows into computeExpiration ->
|
|
61
|
+
* staleAt/expiresAt = NaN, where every `now > NaN` is false so the entry never
|
|
62
|
+
* evicts and is served fresh forever; a negative value makes every read a miss.
|
|
63
|
+
* Mirror profile-registry.ts's Number.isFinite + >= 0 check, but the callers
|
|
64
|
+
* degrade to a default (warning in dev) rather than throw — this runs on the
|
|
65
|
+
* foreground render.
|
|
44
66
|
*/
|
|
67
|
+
function isValidCacheSeconds(value: number, label: string): boolean {
|
|
68
|
+
if (isFiniteNonNegativeSeconds(value)) return true;
|
|
69
|
+
if (process.env.NODE_ENV !== "production") {
|
|
70
|
+
console.warn(
|
|
71
|
+
`[CacheScope] Invalid ${label} ${value}; falling back to default`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Coerce a resolved ttl to a finite, non-negative number (default on invalid). */
|
|
78
|
+
function validatedTtl(value: number): number {
|
|
79
|
+
return isValidCacheSeconds(value, "ttl") ? value : DEFAULT_ROUTE_TTL;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Coerce a resolved swr to a finite, non-negative number, or undefined (no SWR window). */
|
|
83
|
+
function validatedSwr(value: number | undefined): number | undefined {
|
|
84
|
+
if (value === undefined) return undefined;
|
|
85
|
+
return isValidCacheSeconds(value, "swr") ? value : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
45
88
|
function getCacheKeyBase(
|
|
46
89
|
host: string,
|
|
47
90
|
pathname: string,
|
|
@@ -57,16 +100,6 @@ function getCacheKeyBase(
|
|
|
57
100
|
return key;
|
|
58
101
|
}
|
|
59
102
|
|
|
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
103
|
function getDefaultRouteCacheKey(
|
|
71
104
|
pathname: string,
|
|
72
105
|
params?: Record<string, string>,
|
|
@@ -128,20 +161,26 @@ export class CacheScope {
|
|
|
128
161
|
}
|
|
129
162
|
|
|
130
163
|
/**
|
|
131
|
-
* Get effective TTL from config or store defaults
|
|
164
|
+
* Get effective TTL from config or store defaults.
|
|
165
|
+
*
|
|
166
|
+
* Unlike profile-registry.ts (which fails fast at config time), the render
|
|
167
|
+
* path must DEGRADE: a non-finite/negative ttl (NaN/Infinity from a bad
|
|
168
|
+
* defaults config) would make computeExpiration produce NaN deadlines so the
|
|
169
|
+
* entry never evicts, or a guaranteed miss for a negative value. Fall back to
|
|
170
|
+
* DEFAULT_ROUTE_TTL instead of throwing in the foreground render.
|
|
132
171
|
*/
|
|
133
172
|
get ttl(): number {
|
|
134
173
|
if (this.config === false) return 0;
|
|
135
174
|
|
|
136
175
|
// Explicit TTL in cache() options
|
|
137
176
|
if (this.config.ttl !== undefined) {
|
|
138
|
-
return this.config.ttl;
|
|
177
|
+
return validatedTtl(this.config.ttl);
|
|
139
178
|
}
|
|
140
179
|
|
|
141
180
|
// Fall back to store defaults (explicit store first, then app-level)
|
|
142
181
|
const store = this.getStore();
|
|
143
182
|
if (store?.defaults?.ttl !== undefined) {
|
|
144
|
-
return store.defaults.ttl;
|
|
183
|
+
return validatedTtl(store.defaults.ttl);
|
|
145
184
|
}
|
|
146
185
|
|
|
147
186
|
// Hardcoded fallback
|
|
@@ -149,19 +188,22 @@ export class CacheScope {
|
|
|
149
188
|
}
|
|
150
189
|
|
|
151
190
|
/**
|
|
152
|
-
* Get SWR window from config or store defaults
|
|
191
|
+
* Get SWR window from config or store defaults.
|
|
192
|
+
*
|
|
193
|
+
* A non-finite/negative swr is degraded to undefined (no SWR window) rather
|
|
194
|
+
* than fed into expiry math; see the ttl getter for the rationale.
|
|
153
195
|
*/
|
|
154
196
|
get swr(): number | undefined {
|
|
155
197
|
if (this.config === false) return undefined;
|
|
156
198
|
|
|
157
199
|
// Explicit SWR in cache() options
|
|
158
200
|
if (this.config.swr !== undefined) {
|
|
159
|
-
return this.config.swr;
|
|
201
|
+
return validatedSwr(this.config.swr);
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
// Fall back to store defaults
|
|
163
205
|
const store = this.getStore();
|
|
164
|
-
return store?.defaults?.swr;
|
|
206
|
+
return validatedSwr(store?.defaults?.swr);
|
|
165
207
|
}
|
|
166
208
|
|
|
167
209
|
/**
|
|
@@ -187,6 +229,32 @@ export class CacheScope {
|
|
|
187
229
|
return resolveCacheKey(keyFn, this.getStore(), defaultKey, "CacheScope");
|
|
188
230
|
}
|
|
189
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Evaluate the cache `condition` predicate. Returns false (skip the cache
|
|
234
|
+
* operation) when the predicate returns false or throws; returns true when
|
|
235
|
+
* there is no condition or no request context to evaluate it against.
|
|
236
|
+
*/
|
|
237
|
+
private conditionAllows(op: "read" | "write"): boolean {
|
|
238
|
+
if (this.config === false || !this.config.condition) return true;
|
|
239
|
+
const requestCtx = getRequestContext();
|
|
240
|
+
if (!requestCtx) return true;
|
|
241
|
+
try {
|
|
242
|
+
if (!this.config.condition(requestCtx)) {
|
|
243
|
+
debugCacheLog(
|
|
244
|
+
`[CacheScope] condition returned false, skipping cache ${op}`,
|
|
245
|
+
);
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error(
|
|
251
|
+
`[CacheScope] condition function threw, skipping cache ${op}:`,
|
|
252
|
+
error,
|
|
253
|
+
);
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
190
258
|
/**
|
|
191
259
|
* Lookup cached segments for a route (single cache entry per request).
|
|
192
260
|
* Returns { segments, shouldRevalidate } or null if cache miss.
|
|
@@ -204,35 +272,21 @@ export class CacheScope {
|
|
|
204
272
|
shouldRevalidate: boolean;
|
|
205
273
|
} | null> {
|
|
206
274
|
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
|
-
}
|
|
275
|
+
if (!this.conditionAllows("read")) return null;
|
|
228
276
|
|
|
229
277
|
const store = this.getStore();
|
|
230
278
|
if (!store) return null;
|
|
231
279
|
|
|
232
|
-
// Resolve cache key
|
|
233
|
-
|
|
234
|
-
|
|
280
|
+
// Resolve cache key INSIDE the try so a throwing consumer key() (or a
|
|
281
|
+
// store.keyGenerator) degrades to a cache miss (return null -> render
|
|
282
|
+
// uncached) instead of crashing the foreground render. resolveCacheKey
|
|
283
|
+
// itself keeps its hard-fail/no-fallback-to-default contract (a throw must
|
|
284
|
+
// not silently collide onto the default slot); the graceful degradation
|
|
285
|
+
// happens here, where a miss is a safe outcome.
|
|
286
|
+
let key: string | undefined;
|
|
235
287
|
try {
|
|
288
|
+
key = await this.resolveKey(pathname, params, isIntercept);
|
|
289
|
+
|
|
236
290
|
const result = await store.get(key);
|
|
237
291
|
|
|
238
292
|
if (!result) {
|
|
@@ -242,13 +296,44 @@ export class CacheScope {
|
|
|
242
296
|
|
|
243
297
|
const { data: cached, shouldRevalidate } = result;
|
|
244
298
|
|
|
245
|
-
// Deserialize segments
|
|
246
|
-
|
|
299
|
+
// Deserialize segments. A failure means the cached segments are corrupt/
|
|
300
|
+
// partial: evict the entry (self-heal - the re-render re-caches under the
|
|
301
|
+
// same key) and report it as corruption, distinct from a transient infra
|
|
302
|
+
// error (handled by the outer catch).
|
|
303
|
+
let segments: ResolvedSegment[];
|
|
304
|
+
try {
|
|
305
|
+
const { deserializeSegments } = await import("./segment-codec.js");
|
|
306
|
+
segments = await deserializeSegments(cached.segments);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
reportCacheError(
|
|
309
|
+
error,
|
|
310
|
+
"cache-corrupt",
|
|
311
|
+
`[CacheScope] ${key}: corrupt cached segments, evicting`,
|
|
312
|
+
);
|
|
313
|
+
await store
|
|
314
|
+
.delete(key)
|
|
315
|
+
.catch((e) =>
|
|
316
|
+
reportCacheError(e, "cache-delete", `[CacheScope] ${key}: evict`),
|
|
317
|
+
);
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// A hit serves content that was tagged at write time, so the document
|
|
322
|
+
// tag union must include this entry's tags for updateTag()/revalidateTag()
|
|
323
|
+
// to invalidate any full-page entry built on top of it. The write path
|
|
324
|
+
// records via cacheRoute (resolveCacheTags); the hit path records here.
|
|
325
|
+
recordRequestTags(cached.tags);
|
|
247
326
|
|
|
248
|
-
// Replay handle data
|
|
327
|
+
// Replay handle data. An empty string means the route pushed no handles —
|
|
328
|
+
// skip the decode entirely (the common case). Otherwise decode the
|
|
329
|
+
// Flight-encoded blob; a decode failure skips handle restore but keeps the
|
|
330
|
+
// valid cached segments.
|
|
249
331
|
const handleStore = _getRequestContext()?._handleStore;
|
|
250
|
-
if (handleStore) {
|
|
251
|
-
|
|
332
|
+
if (handleStore && cached.handles) {
|
|
333
|
+
const handlesRecord = await decodeHandles(cached.handles);
|
|
334
|
+
if (handlesRecord) {
|
|
335
|
+
restoreHandles(handlesRecord, handleStore);
|
|
336
|
+
}
|
|
252
337
|
}
|
|
253
338
|
|
|
254
339
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -262,11 +347,37 @@ export class CacheScope {
|
|
|
262
347
|
|
|
263
348
|
return { segments, shouldRevalidate };
|
|
264
349
|
} catch (error) {
|
|
265
|
-
|
|
350
|
+
// Covers a store.get() failure AND a throwing consumer key()/keyGenerator
|
|
351
|
+
// (resolveKey). Either way degrade to a cache miss so the render proceeds.
|
|
352
|
+
reportCacheError(
|
|
353
|
+
error,
|
|
354
|
+
"cache-read",
|
|
355
|
+
`[CacheScope] lookup ${key ?? "(key resolution failed)"}`,
|
|
356
|
+
);
|
|
266
357
|
return null;
|
|
267
358
|
}
|
|
268
359
|
}
|
|
269
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Record this scope's segment-DSL cache({ tags }) into the request tag union
|
|
363
|
+
* synchronously, under the same gate cacheRoute() uses for a write.
|
|
364
|
+
*
|
|
365
|
+
* cacheRoute() already records these tags, but it is invoked inside
|
|
366
|
+
* requestCtx.waitUntil() by the cache-store middleware (and the proactive path
|
|
367
|
+
* re-resolves the whole tree before calling it), so its recording is deferred
|
|
368
|
+
* and RACES the document cache's post-body-drain snapshot of _requestTags. On a
|
|
369
|
+
* first-write (segment-cache miss) the document tag union could miss these
|
|
370
|
+
* tags, and updateTag()/revalidateTag() would then fail to invalidate the
|
|
371
|
+
* cached document until a later write reseeded it. Calling this synchronously
|
|
372
|
+
* in the request pipeline (before the snapshot) closes that window. Idempotent
|
|
373
|
+
* (the tag union is a Set), so the duplicate record in cacheRoute is harmless.
|
|
374
|
+
*/
|
|
375
|
+
recordTags(requestCtx: RequestContext | undefined): void {
|
|
376
|
+
if (!this.enabled) return;
|
|
377
|
+
if (!this.conditionAllows("write")) return;
|
|
378
|
+
recordRequestTags(resolveCacheTags(this.config, requestCtx), requestCtx);
|
|
379
|
+
}
|
|
380
|
+
|
|
270
381
|
/**
|
|
271
382
|
* Cache all segments for a route (non-blocking via waitUntil)
|
|
272
383
|
* Single cache entry per route request.
|
|
@@ -284,27 +395,7 @@ export class CacheScope {
|
|
|
284
395
|
isIntercept?: boolean,
|
|
285
396
|
): Promise<void> {
|
|
286
397
|
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
|
-
}
|
|
398
|
+
if (!this.conditionAllows("write")) return;
|
|
308
399
|
|
|
309
400
|
const store = this.getStore();
|
|
310
401
|
if (!store) return;
|
|
@@ -325,6 +416,10 @@ export class CacheScope {
|
|
|
325
416
|
// Resolve cache key early (while request context is available)
|
|
326
417
|
const key = await this.resolveKey(pathname, params, isIntercept);
|
|
327
418
|
|
|
419
|
+
// Resolve tags early (while request context is available, before waitUntil)
|
|
420
|
+
const tags = resolveCacheTags(this.config, requestCtx);
|
|
421
|
+
recordRequestTags(tags, requestCtx);
|
|
422
|
+
|
|
328
423
|
// Check if this is a partial request (navigation) vs document request
|
|
329
424
|
const isPartial = requestCtx.originalUrl.searchParams.has("_rsc_partial");
|
|
330
425
|
|
|
@@ -381,13 +476,20 @@ export class CacheScope {
|
|
|
381
476
|
);
|
|
382
477
|
}
|
|
383
478
|
|
|
384
|
-
// Serialize
|
|
385
|
-
|
|
479
|
+
// Serialize segments and Flight-encode handles in parallel. Handles go
|
|
480
|
+
// through the codec (not raw into the entry) so Promise/ReactNode handle
|
|
481
|
+
// values survive a JSON-serializing store — see encodeHandles.
|
|
482
|
+
const { serializeSegments } = await import("./segment-codec.js");
|
|
483
|
+
const [serializedSegments, encodedHandles] = await Promise.all([
|
|
484
|
+
serializeSegments(nonLoaderSegments),
|
|
485
|
+
encodeHandles(handles),
|
|
486
|
+
]);
|
|
386
487
|
|
|
387
488
|
const data: CachedEntryData = {
|
|
388
489
|
segments: serializedSegments,
|
|
389
|
-
handles,
|
|
490
|
+
handles: encodedHandles,
|
|
390
491
|
expiresAt: Date.now() + ttl * 1000,
|
|
492
|
+
tags,
|
|
391
493
|
};
|
|
392
494
|
|
|
393
495
|
if (INTERNAL_RANGO_DEBUG) {
|
|
@@ -405,7 +507,11 @@ export class CacheScope {
|
|
|
405
507
|
);
|
|
406
508
|
}
|
|
407
509
|
} catch (error) {
|
|
408
|
-
|
|
510
|
+
reportCacheError(
|
|
511
|
+
error,
|
|
512
|
+
"cache-write",
|
|
513
|
+
`[CacheScope] Failed to cache ${key}`,
|
|
514
|
+
);
|
|
409
515
|
}
|
|
410
516
|
});
|
|
411
517
|
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Tag API
|
|
3
|
+
*
|
|
4
|
+
* Provides cacheTag() for tagging cached entries at runtime inside "use cache"
|
|
5
|
+
* functions. Tags are scoped via AsyncLocalStorage; calling cacheTag() outside
|
|
6
|
+
* a "use cache" execution throws.
|
|
7
|
+
*
|
|
8
|
+
* The runtime (cache-runtime.ts) wraps "use cache" execution in
|
|
9
|
+
* runWithCacheTagScope(), collects the runtime tags, and merges them with the
|
|
10
|
+
* profile/DSL tags before storing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import {
|
|
15
|
+
_getRequestContext,
|
|
16
|
+
type RequestContext,
|
|
17
|
+
} from "../server/request-context.js";
|
|
18
|
+
|
|
19
|
+
const cacheTagStorage = new AsyncLocalStorage<Set<string>>();
|
|
20
|
+
|
|
21
|
+
export function normalizeTag(tag: string): string | null {
|
|
22
|
+
// Trim and return the canonical (trimmed) form, not the raw tag. Both the
|
|
23
|
+
// write path (cacheTag) and the invalidate path (updateTag/revalidateTag)
|
|
24
|
+
// route through here, and matching is exact-string: returning the untrimmed
|
|
25
|
+
// tag made cacheTag(" products ") and updateTag("products") two different
|
|
26
|
+
// logical tags, a silent failure-to-invalidate (stale data served forever).
|
|
27
|
+
const trimmed = tag?.trim();
|
|
28
|
+
return trimmed ? trimmed : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeTags(tags: Iterable<string>): string[] {
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const tag of tags) {
|
|
34
|
+
const normalized = normalizeTag(tag);
|
|
35
|
+
if (normalized !== null) out.push(normalized);
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Tag the current "use cache" entry for later invalidation via
|
|
42
|
+
* updateTag() / revalidateTag().
|
|
43
|
+
*
|
|
44
|
+
* Must be called inside a function marked with "use cache".
|
|
45
|
+
* Tags are additive - multiple calls accumulate.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* async function getProduct(ctx) {
|
|
50
|
+
* "use cache";
|
|
51
|
+
* cacheTag(`product:${ctx.params.id}`, "products");
|
|
52
|
+
* return db.getProduct(ctx.params.id);
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
export function cacheTag(...tags: string[]): void {
|
|
57
|
+
const store = cacheTagStorage.getStore();
|
|
58
|
+
if (!store) {
|
|
59
|
+
throw new Error('cacheTag() must be called inside a "use cache" function.');
|
|
60
|
+
}
|
|
61
|
+
for (const tag of tags) {
|
|
62
|
+
const normalized = normalizeTag(tag);
|
|
63
|
+
if (normalized === null) {
|
|
64
|
+
if (process.env.NODE_ENV !== "production") {
|
|
65
|
+
console.warn(`[cacheTag] Ignoring empty or whitespace-only tag.`);
|
|
66
|
+
}
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
store.add(normalized);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function recordRequestTags(
|
|
74
|
+
tags: Iterable<string> | undefined,
|
|
75
|
+
ctx: RequestContext | undefined = _getRequestContext(),
|
|
76
|
+
): void {
|
|
77
|
+
if (!tags) return;
|
|
78
|
+
const set = ctx?._requestTags;
|
|
79
|
+
if (!set) return;
|
|
80
|
+
for (const tag of tags) {
|
|
81
|
+
const normalized = normalizeTag(tag);
|
|
82
|
+
if (normalized !== null) set.add(normalized);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Run a function within a cache tag scope. Any cacheTag() calls inside `fn`
|
|
88
|
+
* accumulate into the returned Set.
|
|
89
|
+
*
|
|
90
|
+
* The returned Set is the LIVE reference - the caller must await `result`
|
|
91
|
+
* before reading `tags`, because an async cached function may call cacheTag()
|
|
92
|
+
* after an await boundary.
|
|
93
|
+
*
|
|
94
|
+
* @internal Used by cache-runtime.ts to wrap "use cache" execution.
|
|
95
|
+
*/
|
|
96
|
+
export function runWithCacheTagScope<T>(fn: () => T): {
|
|
97
|
+
result: T;
|
|
98
|
+
tags: Set<string>;
|
|
99
|
+
} {
|
|
100
|
+
const tagSet = new Set<string>();
|
|
101
|
+
const result = cacheTagStorage.run(tagSet, fn);
|
|
102
|
+
return { result, tags: tagSet };
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Base64 Helpers (binary-safe response body encoding for KV)
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
// Chunk size for String.fromCharCode.apply: large enough to amortize the call
|
|
6
|
+
// overhead, small enough to stay well under the JS engine argument-count limit
|
|
7
|
+
// (~65k). 8192 turns a per-byte concat loop into O(n/8192) apply calls.
|
|
8
|
+
const FROM_CHARCODE_CHUNK = 8192;
|
|
9
|
+
|
|
10
|
+
/** Encode ArrayBuffer to base64 string. */
|
|
11
|
+
export function bufferToBase64(buffer: ArrayBuffer): string {
|
|
12
|
+
const bytes = new Uint8Array(buffer);
|
|
13
|
+
// Build the binary (latin1) string in fixed-size chunks instead of one
|
|
14
|
+
// String.fromCharCode per byte. Identical output to the per-byte loop (each
|
|
15
|
+
// byte maps to the same code unit); just far fewer string concatenations for
|
|
16
|
+
// large document payloads.
|
|
17
|
+
let binary = "";
|
|
18
|
+
for (let i = 0; i < bytes.length; i += FROM_CHARCODE_CHUNK) {
|
|
19
|
+
const chunk = bytes.subarray(i, i + FROM_CHARCODE_CHUNK);
|
|
20
|
+
binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Decode base64 string to ArrayBuffer. */
|
|
26
|
+
export function base64ToBuffer(base64: string): ArrayBuffer {
|
|
27
|
+
const binary = atob(base64);
|
|
28
|
+
const bytes = new Uint8Array(binary.length);
|
|
29
|
+
for (let i = 0; i < binary.length; i++) {
|
|
30
|
+
bytes[i] = binary.charCodeAt(i);
|
|
31
|
+
}
|
|
32
|
+
return bytes.buffer;
|
|
33
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Constants
|
|
3
|
+
// ============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Header names, KV prefixes, and timeout/interval defaults for the CF cache
|
|
6
|
+
// store. Extracted from cf-cache-store.ts so the constants can be shared by the
|
|
7
|
+
// store and its sibling collaborator modules without a circular import back to
|
|
8
|
+
// the class. The public ones (CACHE_*_HEADER, TAG_MARKER_PREFIX,
|
|
9
|
+
// MAX_REVALIDATION_INTERVAL, EDGE_*_TIMEOUT_MS, KV_READ_TIMEOUT_MS) are
|
|
10
|
+
// re-exported from cf-cache-store.ts so existing import paths still resolve.
|
|
11
|
+
|
|
12
|
+
/** Header storing timestamp when entry becomes stale */
|
|
13
|
+
export const CACHE_STALE_AT_HEADER = "x-edge-cache-stale-at";
|
|
14
|
+
|
|
15
|
+
/** Header storing cache status: HIT | REVALIDATING */
|
|
16
|
+
export const CACHE_STATUS_HEADER = "x-edge-cache-status";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Header storing this entry's cache tags as a JSON array. JSON-encoded (not the
|
|
20
|
+
* comma-delimited CF `Cache-Tag` format) so tags containing commas round-trip
|
|
21
|
+
* safely; the read paths parse this to run the tag-invalidation check.
|
|
22
|
+
*/
|
|
23
|
+
export const CACHE_TAGS_HEADER = "x-edge-cache-tags";
|
|
24
|
+
|
|
25
|
+
/** Header storing the ms-epoch timestamp when this entry's tags were attached. */
|
|
26
|
+
export const CACHE_TAGGED_AT_HEADER = "x-edge-cache-tagged-at";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* KV key prefix for tag-invalidation markers. A marker stores the ms-epoch
|
|
30
|
+
* timestamp of the most recent invalidation of a tag; reads treat any entry
|
|
31
|
+
* whose taggedAt is older than its tags' latest marker as invalidated. Markers
|
|
32
|
+
* live in the SAME KV namespace as the cached entries - there is no separate
|
|
33
|
+
* tag-invalidation store.
|
|
34
|
+
*/
|
|
35
|
+
export const TAG_MARKER_PREFIX = "__tag__/";
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Header storing the epoch-ms timestamp when an entry was marked REVALIDATING.
|
|
39
|
+
* The SWR thundering-herd guard reads this to decide whether the in-flight
|
|
40
|
+
* revalidation is still recent. It replaces a prior reliance on the HTTP `Age`
|
|
41
|
+
* header: CF's Cache API does not populate `Age` reliably per-colo (and our own
|
|
42
|
+
* unit MockCache never set it), so an absent `Age` defaulted to 0 and made every
|
|
43
|
+
* REVALIDATING entry look "just revalidated" forever -- a dropped/never-finished
|
|
44
|
+
* background revalidation could then pin an entry stale until hard expiry. An
|
|
45
|
+
* explicit timestamp we write ourselves (same pattern as CACHE_STALE_AT_HEADER)
|
|
46
|
+
* is reliable and lets the MAX_REVALIDATION_INTERVAL re-arm actually fire.
|
|
47
|
+
*/
|
|
48
|
+
export const CACHE_REVALIDATING_AT_HEADER = "x-edge-cache-revalidating-at";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Header storing the absolute epoch-ms hard-expiry deadline (staleAt +
|
|
52
|
+
* swrWindow*1000) of an L1 entry. The stale-path REVALIDATING re-put reads this
|
|
53
|
+
* to recompute a SHRINKING Cache-Control max-age instead of copying set()'s
|
|
54
|
+
* original full-window max-age. Without it, every MAX_REVALIDATION_INTERVAL
|
|
55
|
+
* re-arm re-puts the full window and restarts CF's retention clock, pinning a
|
|
56
|
+
* perpetually-stale entry (one whose background revalidation keeps failing) past
|
|
57
|
+
* its intended hard-expiry indefinitely. Mirrors the KVSegmentEnvelope `e`
|
|
58
|
+
* field and the remaining-ttl math in promoteSegmentToL1/promoteItemToL1.
|
|
59
|
+
* @internal
|
|
60
|
+
*/
|
|
61
|
+
export const CACHE_EXPIRES_AT_HEADER = "x-edge-cache-expires-at";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Header stashing the route author's original Cache-Control on L1 document
|
|
65
|
+
* entries. putResponse/promoteResponseToL1 overwrite Cache-Control with a long
|
|
66
|
+
* `max-age` so the CF Cache API retains the entry across the whole SWR window;
|
|
67
|
+
* getResponse restores this original value before serving so the client and any
|
|
68
|
+
* upstream CDN see the author's intended directive, not the internal edge TTL.
|
|
69
|
+
*/
|
|
70
|
+
export const CACHE_ORIG_CC_HEADER = "x-edge-cache-orig-cc";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Maximum age in seconds for REVALIDATING status before allowing new revalidation.
|
|
74
|
+
* After this period, a stale entry in REVALIDATING status will trigger revalidation again.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export const MAX_REVALIDATION_INTERVAL = 30;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Maximum time (ms) to wait for an L1 edge cache (CF Cache API) read before
|
|
81
|
+
* giving up and treating it as a miss. The Cache API is normally sub-millisecond
|
|
82
|
+
* per-colo, so a slow `match` signals a degraded colo; we don't want it adding
|
|
83
|
+
* latency to the request. On timeout the lookup is abandoned, a warning is
|
|
84
|
+
* logged, and the read falls through to its normal miss path (L2/KV or render).
|
|
85
|
+
*
|
|
86
|
+
* This is the default; override per store via
|
|
87
|
+
* `CFCacheStoreOptions.edgeLookupTimeoutMs` (<= 0 disables the budget).
|
|
88
|
+
*/
|
|
89
|
+
export const EDGE_LOOKUP_TIMEOUT_MS = 10;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Maximum time (ms) to wait for the BODY of a matched L1 entry to be read
|
|
93
|
+
* (response.json()) before treating the read as a miss.
|
|
94
|
+
*
|
|
95
|
+
* This is separate from {@link EDGE_LOOKUP_TIMEOUT_MS} on purpose. CF's Cache
|
|
96
|
+
* API resolves `match()` with a lazily-streamed body, so a fast `match` can be
|
|
97
|
+
* followed by a multi-second stall while the body bytes are fetched -- the
|
|
98
|
+
* latency tail lives here, after the match budget has already passed. The
|
|
99
|
+
* default bounds that tail aggressively: a healthy per-colo body read (fetch +
|
|
100
|
+
* JSON parse) settles in low single-digit milliseconds, so 20ms clears a
|
|
101
|
+
* healthy read while still failing fast to L2/KV (or render) on a degraded colo
|
|
102
|
+
* instead of pinning the request behind a seconds-long read. Raise it per store
|
|
103
|
+
* if large Flight payloads legitimately need longer.
|
|
104
|
+
*
|
|
105
|
+
* Override per store via `CFCacheStoreOptions.edgeReadTimeoutMs` (<= 0 disables).
|
|
106
|
+
*/
|
|
107
|
+
export const EDGE_READ_TIMEOUT_MS = 20;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Maximum time (ms) to wait for an L2 (KV) read (`kv.get(key, {type:"json"})`)
|
|
111
|
+
* before treating it as a miss. Unlike the L1 budgets, KV is a GLOBAL store: the
|
|
112
|
+
* file header documents ~50ms healthy reads, and a degraded namespace can tail
|
|
113
|
+
* to seconds. KV is the LAST cache tier before a full render, so an unbounded
|
|
114
|
+
* read here pins the whole request behind a degraded global lookup.
|
|
115
|
+
*
|
|
116
|
+
* The default (170ms) sits a few multiples above the documented ~50ms healthy
|
|
117
|
+
* read, leaving headroom for legitimate latency tails (larger payloads,
|
|
118
|
+
* far-from-colo regions) so a healthy-but-slow read does not false-miss into a
|
|
119
|
+
* render, while still abandoning a genuinely degraded namespace well before its
|
|
120
|
+
* multi-second tail can pin the request. A deployment with a tighter SLA can
|
|
121
|
+
* lower it, and one whose healthy p99 runs higher should raise it: measure the
|
|
122
|
+
* KV read p99 (Workers Analytics) and add margin. It is a degradation
|
|
123
|
+
* guard-rail, not a tuning lever for "slow KV is normal here".
|
|
124
|
+
*
|
|
125
|
+
* Override per store via `CFCacheStoreOptions.kvReadTimeoutMs` (<= 0 disables).
|
|
126
|
+
*/
|
|
127
|
+
export const KV_READ_TIMEOUT_MS = 170;
|