@rangojs/router 0.0.0-experimental.122 → 0.0.0-experimental.125
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/dist/bin/rango.js +10 -6
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +55 -48
- package/package.json +61 -21
- package/skills/caching/SKILL.md +2 -1
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +16 -2
- package/skills/intercept/SKILL.md +4 -2
- package/skills/layout/SKILL.md +11 -6
- package/skills/loader/SKILL.md +6 -2
- package/skills/middleware/SKILL.md +4 -2
- package/skills/migrate-nextjs/SKILL.md +3 -1
- package/skills/parallel/SKILL.md +9 -4
- package/skills/rango/SKILL.md +12 -0
- package/skills/route/SKILL.md +10 -2
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +98 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +89 -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 +118 -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/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +1 -1
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +1 -83
- package/src/browser/invalidate-client-cache.ts +52 -0
- package/src/browser/navigation-bridge.ts +14 -1
- package/src/browser/navigation-client.ts +14 -1
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +26 -51
- package/src/browser/navigation-transaction.ts +0 -32
- package/src/browser/partial-update.ts +1 -83
- package/src/browser/prefetch/cache.ts +6 -45
- package/src/browser/prefetch/fetch.ts +7 -0
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -99
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +2 -1
- package/src/browser/react/ScrollRestoration.tsx +10 -6
- package/src/browser/react/filter-segment-order.ts +0 -2
- package/src/browser/react/index.ts +0 -51
- package/src/browser/react/location-state-shared.ts +0 -13
- package/src/browser/react/location-state.ts +0 -1
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +0 -5
- package/src/browser/react/use-link-status.ts +0 -4
- package/src/browser/react/use-navigation.ts +0 -3
- package/src/browser/react/use-params.ts +0 -2
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/rsc-router.tsx +12 -4
- package/src/browser/server-action-bridge.ts +77 -15
- package/src/browser/types.ts +7 -2
- package/src/browser/validate-redirect-origin.ts +4 -5
- package/src/build/route-trie.ts +3 -0
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/router-processing.ts +0 -8
- package/src/cache/cache-policy.ts +0 -54
- package/src/cache/cache-runtime.ts +27 -24
- package/src/cache/cache-scope.ts +0 -27
- package/src/cache/cache-tag.ts +0 -37
- package/src/cache/cf/cf-cache-store.ts +94 -46
- package/src/cache/cf/index.ts +0 -24
- package/src/cache/document-cache.ts +11 -36
- package/src/cache/handle-snapshot.ts +0 -40
- package/src/cache/index.ts +0 -27
- package/src/cache/memory-segment-store.ts +2 -48
- package/src/cache/profile-registry.ts +7 -3
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/types.ts +0 -98
- package/src/client.rsc.tsx +1 -22
- package/src/client.tsx +14 -38
- package/src/component-utils.ts +19 -0
- package/src/deps/ssr.ts +0 -1
- package/src/handle.ts +28 -18
- package/src/handles/MetaTags.tsx +0 -14
- 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 +6 -0
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +1 -65
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +6 -2
- package/src/href-client.ts +0 -4
- package/src/index.rsc.ts +42 -3
- package/src/index.ts +31 -1
- package/src/internal-debug.ts +2 -4
- package/src/loader.rsc.ts +19 -9
- package/src/loader.ts +12 -4
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +23 -30
- package/src/prerender.ts +58 -3
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +1 -44
- package/src/route-definition/dsl-helpers.ts +7 -19
- package/src/route-definition/helpers-types.ts +3 -3
- package/src/route-definition/redirect.ts +11 -1
- package/src/route-map-builder.ts +0 -16
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +0 -13
- package/src/router/error-handling.ts +12 -16
- package/src/router/find-match.ts +4 -30
- package/src/router/intercept-resolution.ts +10 -1
- package/src/router/lazy-includes.ts +1 -57
- package/src/router/loader-resolution.ts +3 -2
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +1 -25
- package/src/router/match-api.ts +0 -20
- package/src/router/match-context.ts +0 -22
- package/src/router/match-handlers.ts +57 -58
- package/src/router/match-middleware/background-revalidation.ts +0 -7
- package/src/router/match-middleware/cache-lookup.ts +1 -54
- package/src/router/match-middleware/cache-store.ts +0 -31
- package/src/router/match-middleware/intercept-resolution.ts +0 -22
- package/src/router/match-middleware/segment-resolution.ts +0 -21
- package/src/router/match-pipelines.ts +1 -42
- package/src/router/match-result.ts +1 -52
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-cookies.ts +0 -13
- package/src/router/middleware-types.ts +0 -115
- package/src/router/middleware.ts +7 -30
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +1 -33
- package/src/router/prerender-match.ts +33 -45
- package/src/router/request-classification.ts +1 -38
- package/src/router/revalidation.ts +5 -58
- package/src/router/router-context.ts +0 -26
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +25 -57
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +10 -13
- package/src/router/segment-resolution/revalidation.ts +5 -42
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/state-cookie-name.ts +33 -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 +63 -40
- package/src/router/types.ts +1 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +40 -9
- package/src/rsc/handler.ts +14 -2
- package/src/rsc/helpers.ts +34 -0
- package/src/rsc/origin-guard.ts +0 -12
- package/src/rsc/progressive-enhancement.ts +4 -1
- package/src/rsc/rsc-rendering.ts +4 -7
- package/src/rsc/runtime-warnings.ts +14 -0
- package/src/rsc/server-action.ts +30 -28
- package/src/rsc/types.ts +2 -1
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +0 -16
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +79 -88
- package/src/server/cookie-store.ts +52 -1
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +74 -77
- package/src/ssr/index.tsx +14 -14
- package/src/static-handler.ts +10 -13
- package/src/testing/cache-status.ts +119 -0
- package/src/testing/collect-handle.ts +40 -0
- package/src/testing/dispatch.ts +581 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +127 -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 +186 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +98 -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 +311 -0
- package/src/testing/render-route.tsx +504 -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/error-types.ts +25 -89
- package/src/types/global-namespace.ts +15 -15
- package/src/types/handler-context.ts +16 -13
- package/src/types/index.ts +0 -10
- package/src/types/request-scope.ts +0 -19
- package/src/types/route-config.ts +6 -50
- package/src/types/route-entry.ts +0 -6
- package/src/types/segments.ts +0 -13
- package/src/urls/include-helper.ts +0 -4
- package/src/urls/index.ts +0 -6
- package/src/urls/path-helper-types.ts +2 -2
- package/src/urls/path-helper.ts +0 -54
- package/src/urls/urls-function.ts +0 -13
- package/src/use-loader.tsx +0 -186
- package/src/vite/discovery/bundle-postprocess.ts +2 -1
- package/src/vite/discovery/discover-routers.ts +6 -7
- package/src/vite/discovery/virtual-module-codegen.ts +1 -11
- package/src/vite/plugin-types.ts +3 -1
- package/src/vite/plugins/cjs-to-esm.ts +0 -11
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +0 -10
- package/src/vite/plugins/cloudflare-protocol-stub.ts +0 -20
- package/src/vite/plugins/expose-action-id.ts +2 -73
- package/src/vite/plugins/expose-id-utils.ts +0 -55
- package/src/vite/plugins/expose-ids/export-analysis.ts +0 -38
- package/src/vite/plugins/expose-ids/handler-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/loader-transform.ts +0 -15
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +10 -0
- package/src/vite/plugins/performance-tracks.ts +0 -3
- package/src/vite/plugins/use-cache-transform.ts +0 -36
- package/src/vite/plugins/version-injector.ts +0 -20
- package/src/vite/plugins/version-plugin.ts +1 -49
- package/src/vite/plugins/virtual-entries.ts +0 -15
- package/src/vite/rango.ts +1 -108
- package/src/vite/router-discovery.ts +2 -1
- package/src/vite/utils/ast-handler-extract.ts +0 -16
- package/src/vite/utils/bundle-analysis.ts +6 -13
- package/src/vite/utils/client-chunks.ts +0 -6
- package/src/vite/utils/forward-user-plugins.ts +0 -22
- package/src/vite/utils/manifest-utils.ts +0 -4
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -35
- package/src/vite/utils/shared-utils.ts +3 -35
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
type RequestContext,
|
|
41
41
|
} from "../../server/request-context.js";
|
|
42
42
|
import { VERSION } from "@rangojs/router:version";
|
|
43
|
+
import {
|
|
44
|
+
isPerClientSignalHeader,
|
|
45
|
+
stripPerClientSignals,
|
|
46
|
+
} from "../../browser/cookie-name.js";
|
|
43
47
|
import {
|
|
44
48
|
resolveTtl,
|
|
45
49
|
resolveSwrWindow,
|
|
@@ -214,6 +218,19 @@ function getTagMarkerInflight(
|
|
|
214
218
|
return inflight;
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Per-request memo of the derived cache-key base URL.
|
|
223
|
+
*
|
|
224
|
+
* deriveBaseUrl() is a pure function of the live request URL, but keyToRequest
|
|
225
|
+
* calls it on EVERY cache operation (each segment/item get/set/delete, each
|
|
226
|
+
* KV->L1 promote, each tag-marker read), so a page composed of many cached
|
|
227
|
+
* entries re-parses the same request.url and re-runs the host validation tens
|
|
228
|
+
* of times. Keying by the request-context object collapses that to one derive
|
|
229
|
+
* per request. Keyed by ctx alone (not by store) because the derived value
|
|
230
|
+
* depends only on the request URL, not on which store asked.
|
|
231
|
+
*/
|
|
232
|
+
const derivedBaseUrlMemo = new WeakMap<object, string>();
|
|
233
|
+
|
|
217
234
|
/** KV key byte-length ceiling. Cloudflare KV rejects keys larger than this. */
|
|
218
235
|
const KV_MAX_KEY_BYTES = 512;
|
|
219
236
|
|
|
@@ -319,10 +336,9 @@ function remainingCacheControl(headers: Headers, now: number): string {
|
|
|
319
336
|
// Types
|
|
320
337
|
// ============================================================================
|
|
321
338
|
|
|
322
|
-
//
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
export type { ExecutionContext } from "../../types/request-scope.js";
|
|
339
|
+
// Imported from the canonical home (also publicly exported from src/index.ts /
|
|
340
|
+
// src/index.rsc.ts) so this module shares the one interface rather than
|
|
341
|
+
// declaring a second that could drift.
|
|
326
342
|
import type { ExecutionContext } from "../../types/request-scope.js";
|
|
327
343
|
|
|
328
344
|
/**
|
|
@@ -713,12 +729,6 @@ export interface CFCacheStoreOptions<TEnv = unknown> {
|
|
|
713
729
|
) => string | Promise<string>;
|
|
714
730
|
}
|
|
715
731
|
|
|
716
|
-
/**
|
|
717
|
-
* Cache status values for the x-edge-cache-status header.
|
|
718
|
-
* @internal
|
|
719
|
-
*/
|
|
720
|
-
export type CacheStatus = "HIT" | "REVALIDATING";
|
|
721
|
-
|
|
722
732
|
// ============================================================================
|
|
723
733
|
// CFCacheStore Implementation
|
|
724
734
|
// ============================================================================
|
|
@@ -806,13 +816,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
806
816
|
// tagCacheTtl gates the L1 marker cache via `> 0`. A non-finite value (NaN
|
|
807
817
|
// from `Number(env.UNSET)`) is not null/undefined, so `?? 0` would let it
|
|
808
818
|
// through and silently disable the cache while reading as "configured".
|
|
809
|
-
//
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
options.tagCacheTtl > 0
|
|
814
|
-
? options.tagCacheTtl
|
|
815
|
-
: 0;
|
|
819
|
+
// finiteBudget coerces non-finite/null/undefined to 0; the `> 0` guard then
|
|
820
|
+
// collapses a finite non-positive value to the documented 0 = disabled.
|
|
821
|
+
const tagCacheTtl = finiteBudget(options.tagCacheTtl, 0);
|
|
822
|
+
this.tagCacheTtl = tagCacheTtl > 0 ? tagCacheTtl : 0;
|
|
816
823
|
|
|
817
824
|
// Read-side tag invalidation requires KV: isGloballyInvalidated() compares an
|
|
818
825
|
// entry's taggedAt against the per-tag KV marker and short-circuits to "not
|
|
@@ -905,31 +912,43 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
905
912
|
return fallback;
|
|
906
913
|
}
|
|
907
914
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
915
|
+
// The result is deterministic per request, but keyToRequest calls this on
|
|
916
|
+
// every cache operation; memoize per request context (see derivedBaseUrlMemo).
|
|
917
|
+
const memoized = derivedBaseUrlMemo.get(ctx);
|
|
918
|
+
if (memoized !== undefined) {
|
|
919
|
+
return memoized;
|
|
920
|
+
}
|
|
911
921
|
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
hostname
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
922
|
+
const derived = ((): string => {
|
|
923
|
+
try {
|
|
924
|
+
const url = new URL(ctx.request.url);
|
|
925
|
+
const hostname = url.hostname;
|
|
926
|
+
|
|
927
|
+
// Use fallback for dev/preview environments
|
|
928
|
+
if (
|
|
929
|
+
hostname === "localhost" ||
|
|
930
|
+
hostname === "127.0.0.1" ||
|
|
931
|
+
hostname.endsWith(".workers.dev") ||
|
|
932
|
+
hostname.endsWith(".pages.dev")
|
|
933
|
+
) {
|
|
934
|
+
return fallback;
|
|
935
|
+
}
|
|
921
936
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
937
|
+
// Validate hostname: must be a valid domain (alphanumeric, hyphens, dots)
|
|
938
|
+
// to prevent host header injection into cache keys
|
|
939
|
+
if (!/^[a-zA-Z0-9.-]+$/.test(hostname) || hostname.length > 253) {
|
|
940
|
+
return fallback;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// Use actual hostname for production
|
|
944
|
+
return `https://${hostname}/`;
|
|
945
|
+
} catch {
|
|
925
946
|
return fallback;
|
|
926
947
|
}
|
|
948
|
+
})();
|
|
927
949
|
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
} catch {
|
|
931
|
-
return fallback;
|
|
932
|
-
}
|
|
950
|
+
derivedBaseUrlMemo.set(ctx, derived);
|
|
951
|
+
return derived;
|
|
933
952
|
}
|
|
934
953
|
|
|
935
954
|
/**
|
|
@@ -1611,6 +1630,9 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1611
1630
|
headers.delete(CACHE_STATUS_HEADER);
|
|
1612
1631
|
headers.delete(CACHE_TAGS_HEADER);
|
|
1613
1632
|
headers.delete(CACHE_TAGGED_AT_HEADER);
|
|
1633
|
+
// Finding #3 (read side): strip per-client signals a pre-fix or
|
|
1634
|
+
// pinned-version L1 entry may carry. See the read-side note in the design doc.
|
|
1635
|
+
stripPerClientSignals(headers);
|
|
1614
1636
|
return new Response(response.body, {
|
|
1615
1637
|
status: response.status,
|
|
1616
1638
|
statusText: response.statusText,
|
|
@@ -1651,6 +1673,10 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1651
1673
|
// replaced with a long max-age so the CF Cache API holds the entry across
|
|
1652
1674
|
// the SWR window; getResponse restores the original before serving.
|
|
1653
1675
|
const headers = new Headers(response.headers);
|
|
1676
|
+
// Finding #3: never persist a per-client signal in the shared L1 entry
|
|
1677
|
+
// (the platform's Set-Cookie rejection is unverified and ignores the
|
|
1678
|
+
// directive anyway). See stripPerClientSignals.
|
|
1679
|
+
stripPerClientSignals(headers);
|
|
1654
1680
|
const originalCacheControl = response.headers.get("Cache-Control");
|
|
1655
1681
|
if (originalCacheControl !== null) {
|
|
1656
1682
|
headers.set(CACHE_ORIG_CC_HEADER, originalCacheControl);
|
|
@@ -1658,10 +1684,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1658
1684
|
headers.set("Cache-Control", `public, max-age=${totalTtl}`);
|
|
1659
1685
|
headers.set(CACHE_STALE_AT_HEADER, String(staleAt));
|
|
1660
1686
|
// Internal tag headers (stripped by toClientResponse before serving).
|
|
1661
|
-
|
|
1662
|
-
for (const [name, value] of Object.entries(tagHeaders)) {
|
|
1663
|
-
headers.set(name, value);
|
|
1664
|
-
}
|
|
1687
|
+
this.setTagHeaders(headers, tags, taggedAt);
|
|
1665
1688
|
|
|
1666
1689
|
const toCache = new Response(l1Body, {
|
|
1667
1690
|
status: response.status,
|
|
@@ -1688,8 +1711,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
1688
1711
|
// L2: persist to KV (KV requires expirationTtl >= 60s)
|
|
1689
1712
|
if (this.kv && this.waitUntil && totalTtl >= 60) {
|
|
1690
1713
|
const kvKey = this.toKVKey(`doc:${key}`);
|
|
1714
|
+
// Finding #3: never persist a per-client signal in the KV envelope.
|
|
1691
1715
|
const headersArray: [string, string][] = [];
|
|
1692
|
-
response.headers.forEach((v, k) =>
|
|
1716
|
+
response.headers.forEach((v, k) => {
|
|
1717
|
+
if (isPerClientSignalHeader(k)) return;
|
|
1718
|
+
headersArray.push([k, v]);
|
|
1719
|
+
});
|
|
1693
1720
|
// Read body as ArrayBuffer and encode to base64 to preserve binary payloads
|
|
1694
1721
|
const bodyBuf = kvBody
|
|
1695
1722
|
? await new Response(kvBody).arrayBuffer()
|
|
@@ -2149,6 +2176,24 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2149
2176
|
};
|
|
2150
2177
|
}
|
|
2151
2178
|
|
|
2179
|
+
/**
|
|
2180
|
+
* Merge the internal tag headers onto an existing Headers instance. The
|
|
2181
|
+
* from-scratch paths spread tagHeaderEntries() into an object-literal init;
|
|
2182
|
+
* the document put/promote paths build a Headers first, so they .set() each
|
|
2183
|
+
* entry instead.
|
|
2184
|
+
*/
|
|
2185
|
+
private setTagHeaders(
|
|
2186
|
+
headers: Headers,
|
|
2187
|
+
tags: string[] | undefined,
|
|
2188
|
+
taggedAt: number | undefined,
|
|
2189
|
+
): void {
|
|
2190
|
+
for (const [name, value] of Object.entries(
|
|
2191
|
+
this.tagHeaderEntries(tags, taggedAt),
|
|
2192
|
+
)) {
|
|
2193
|
+
headers.set(name, value);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2152
2197
|
/** Read an entry's tags/taggedAt back from its headers. */
|
|
2153
2198
|
private readTagInfo(headers: Headers): {
|
|
2154
2199
|
tags?: string[];
|
|
@@ -2848,6 +2893,12 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2848
2893
|
// so evict it and miss rather than re-failing every read until TTL.
|
|
2849
2894
|
let response: Response;
|
|
2850
2895
|
try {
|
|
2896
|
+
// Finding #3 (read side): strip per-client signals a stale envelope may
|
|
2897
|
+
// carry. Inside the try so a malformed `hd` evicts (not throws through);
|
|
2898
|
+
// mutates `hd` in place so promoteResponseToL1 re-seeds from it too.
|
|
2899
|
+
envelope.hd = envelope.hd.filter(
|
|
2900
|
+
([name]) => !isPerClientSignalHeader(name),
|
|
2901
|
+
);
|
|
2851
2902
|
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
2852
2903
|
const headers = new Headers(envelope.hd);
|
|
2853
2904
|
response = new Response(bodyBuffer, {
|
|
@@ -2903,10 +2954,7 @@ export class CFCacheStore<TEnv = unknown> implements SegmentCacheStore<TEnv> {
|
|
|
2903
2954
|
// Re-attach the internal tag headers (envelope.hd is client-facing
|
|
2904
2955
|
// and intentionally excludes them) so the promoted entry stays
|
|
2905
2956
|
// invalidatable.
|
|
2906
|
-
|
|
2907
|
-
for (const [name, value] of Object.entries(tagHeaders)) {
|
|
2908
|
-
headers.set(name, value);
|
|
2909
|
-
}
|
|
2957
|
+
this.setTagHeaders(headers, envelope.t, envelope.ta);
|
|
2910
2958
|
|
|
2911
2959
|
const bodyBuffer = base64ToBuffer(envelope.b);
|
|
2912
2960
|
const response = new Response(bodyBuffer, {
|
package/src/cache/cf/index.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cloudflare Cache Store Exports
|
|
3
|
-
*
|
|
4
|
-
* Main export:
|
|
5
|
-
* - CFCacheStore - Production cache store using Cloudflare's Cache API
|
|
6
|
-
*
|
|
7
|
-
* Header constants (for inspection/debugging):
|
|
8
|
-
* - CACHE_STALE_AT_HEADER - Header containing staleness timestamp
|
|
9
|
-
* - CACHE_STATUS_HEADER - Header containing HIT/REVALIDATING status
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
// Public API
|
|
13
1
|
export {
|
|
14
2
|
CFCacheStore,
|
|
15
3
|
type CFCacheStoreOptions,
|
|
@@ -18,26 +6,14 @@ export {
|
|
|
18
6
|
type KVNamespace,
|
|
19
7
|
} from "./cf-cache-store.js";
|
|
20
8
|
|
|
21
|
-
// Header constants for debugging and inspection. The tag headers
|
|
22
|
-
// (x-edge-cache-tags / x-edge-cache-tagged-at) are intentionally NOT re-exported:
|
|
23
|
-
// they are an internal encoding detail of the store's tag-invalidation check, not
|
|
24
|
-
// a consumer-inspectable contract.
|
|
25
9
|
export {
|
|
26
10
|
CACHE_STALE_AT_HEADER,
|
|
27
11
|
CACHE_STATUS_HEADER,
|
|
28
12
|
CACHE_REVALIDATING_AT_HEADER,
|
|
29
13
|
} from "./cf-cache-store.js";
|
|
30
14
|
|
|
31
|
-
// Default latency-budget values, exported so the CFCacheStoreOptions JSDoc
|
|
32
|
-
// {@link}s resolve and consumers can derive margins from the defaults.
|
|
33
15
|
export {
|
|
34
16
|
EDGE_LOOKUP_TIMEOUT_MS,
|
|
35
17
|
EDGE_READ_TIMEOUT_MS,
|
|
36
18
|
KV_READ_TIMEOUT_MS,
|
|
37
19
|
} from "./cf-cache-store.js";
|
|
38
|
-
|
|
39
|
-
// Internal exports (re-exported for backwards compatibility, marked @internal in source)
|
|
40
|
-
export {
|
|
41
|
-
type CacheStatus,
|
|
42
|
-
MAX_REVALIDATION_INTERVAL,
|
|
43
|
-
} from "./cf-cache-store.js";
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
15
|
+
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
15
16
|
import {
|
|
16
17
|
getRequestContext,
|
|
17
18
|
type RequestContext,
|
|
@@ -21,36 +22,8 @@ import { sortedSearchString } from "./cache-key-utils.js";
|
|
|
21
22
|
import { runBackground } from "./background-task.js";
|
|
22
23
|
import { reportCacheError } from "./cache-error.js";
|
|
23
24
|
|
|
24
|
-
// ============================================================================
|
|
25
|
-
// Constants
|
|
26
|
-
// ============================================================================
|
|
27
|
-
|
|
28
|
-
/** Header indicating cache status for debugging */
|
|
29
25
|
const CACHE_STATUS_HEADER = "x-document-cache-status";
|
|
30
26
|
|
|
31
|
-
/**
|
|
32
|
-
* Snapshot the request-scoped tag union for a document cache write. The full-page
|
|
33
|
-
* entry is tagged with every cache tag its content resolved (runtime cacheTag(),
|
|
34
|
-
* "use cache" profile tags, and loader cache tags) so updateTag()/revalidateTag()
|
|
35
|
-
* can invalidate it. Returns undefined when no tags were used, keeping untagged
|
|
36
|
-
* document entries header-free.
|
|
37
|
-
*
|
|
38
|
-
* This is a plain synchronous snapshot. The CALLER must drain the rendered body
|
|
39
|
-
* first (see the cache-write closures): runtime cacheTag()/"use cache" and loader
|
|
40
|
-
* tags are recorded synchronously as each value resolves during render, including
|
|
41
|
-
* Suspense-streamed ones that resolve AFTER the handler-settlement barrier - so
|
|
42
|
-
* the correct barrier is the stream draining (render complete), not _handleStore.
|
|
43
|
-
*
|
|
44
|
-
* Caveat: this applies only to the segment cache WRITE path. When a segment is
|
|
45
|
-
* cached for the first time, its cache({ tags }) DSL tags are recorded inside the
|
|
46
|
-
* deferred cacheRoute waitUntil, which can still run after this snapshot; a
|
|
47
|
-
* document that combines whole-page document caching with first-write segment-DSL
|
|
48
|
-
* tags may miss those (the segment cache entry itself is still correctly tagged
|
|
49
|
-
* and invalidated). On a segment-cache HIT the entry's tags are recorded
|
|
50
|
-
* synchronously during lookupRoute, before this snapshot, so they are captured.
|
|
51
|
-
* Runtime cacheTag()/"use cache" and loader tags are always captured once the
|
|
52
|
-
* body drains.
|
|
53
|
-
*/
|
|
54
27
|
function collectRequestTags(
|
|
55
28
|
requestCtx: RequestContext | undefined,
|
|
56
29
|
): string[] | undefined {
|
|
@@ -58,11 +31,6 @@ function collectRequestTags(
|
|
|
58
31
|
return tags && tags.size > 0 ? [...tags] : undefined;
|
|
59
32
|
}
|
|
60
33
|
|
|
61
|
-
/**
|
|
62
|
-
* Simple hash function for segment IDs.
|
|
63
|
-
* Creates a short, deterministic hash to differentiate cache keys
|
|
64
|
-
* based on which segments the client already has.
|
|
65
|
-
*/
|
|
66
34
|
function hashSegmentIds(segmentIds: string): string {
|
|
67
35
|
if (!segmentIds) return "";
|
|
68
36
|
|
|
@@ -71,12 +39,9 @@ function hashSegmentIds(segmentIds: string): string {
|
|
|
71
39
|
const char = segmentIds.charCodeAt(i);
|
|
72
40
|
hash = ((hash << 5) - hash + char) | 0;
|
|
73
41
|
}
|
|
74
|
-
// Convert to base36 for shorter string, take absolute value
|
|
75
42
|
return Math.abs(hash).toString(36);
|
|
76
43
|
}
|
|
77
44
|
|
|
78
|
-
// ============================================================================
|
|
79
|
-
// Cache Control Parsing
|
|
80
45
|
// ============================================================================
|
|
81
46
|
|
|
82
47
|
interface CacheDirectives {
|
|
@@ -121,6 +86,16 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
|
121
86
|
return null;
|
|
122
87
|
}
|
|
123
88
|
|
|
89
|
+
// Never cache a per-client signal into a SHARED response store. A Set-Cookie
|
|
90
|
+
// (e.g. a rango state rotation from invalidateClientCache(), or any cookie a
|
|
91
|
+
// loader set) would be replayed to every client on a hit — pinning them to
|
|
92
|
+
// one value and even rolling a rotated client back to a prior one. The
|
|
93
|
+
// x-rango-keep-cache directive header is the mirror image: a replayed "keep"
|
|
94
|
+
// would suppress invalidation for every replayed client. Refuse both.
|
|
95
|
+
if (hasPerClientSignal(response.headers)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
124
99
|
const cacheControl = response.headers.get("Cache-Control");
|
|
125
100
|
return parseCacheControl(cacheControl);
|
|
126
101
|
}
|
|
@@ -11,23 +11,10 @@ import type { HandleStore } from "../server/handle-store.js";
|
|
|
11
11
|
import type { SegmentHandleData } from "./types.js";
|
|
12
12
|
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* Bound on the background cache-write encode of handle data. A pushed handle
|
|
16
|
-
* value can be a Promise (request-context push-a-promise) or a Promise<ReactNode>
|
|
17
|
-
* (Breadcrumbs content), which the Flight encoder awaits while draining. The
|
|
18
|
-
* encode runs in waitUntil/runBackground, so a never-resolving handle value
|
|
19
|
-
* would otherwise pin a background slot indefinitely; on timeout the entry's
|
|
20
|
-
* handles coalesce to empty rather than hanging or poisoning the whole write.
|
|
21
|
-
*/
|
|
22
14
|
const HANDLE_ENCODE_TIMEOUT_MS = 5000;
|
|
23
15
|
|
|
24
16
|
type HandleRecord = Record<string, SegmentHandleData>;
|
|
25
17
|
|
|
26
|
-
// captureHandles builds a per-segment map keyed by every cached segment id, even
|
|
27
|
-
// segments that pushed nothing (their entry is an empty object). "No handle data"
|
|
28
|
-
// means no segment has any handle, in which case we skip the Flight encode and
|
|
29
|
-
// store an empty string — so the common handle-free route pays neither an encode
|
|
30
|
-
// on write nor a decode on every cache hit.
|
|
31
18
|
function hasHandleData(handles: HandleRecord): boolean {
|
|
32
19
|
for (const segId in handles) {
|
|
33
20
|
for (const _ in handles[segId]) return true;
|
|
@@ -55,42 +42,15 @@ function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
|
|
|
55
42
|
]);
|
|
56
43
|
}
|
|
57
44
|
|
|
58
|
-
/**
|
|
59
|
-
* Encode a captured handle map to a string for cache storage.
|
|
60
|
-
*
|
|
61
|
-
* Handle values can be Promises or React elements (e.g. Breadcrumbs `content`).
|
|
62
|
-
* JSON.stringify destroys those (Promise -> {}, ReactNode non-representable), so
|
|
63
|
-
* persisting the raw map silently corrupts non-scalar handle values on stores
|
|
64
|
-
* that serialize to JSON (the Cloudflare cache). Routing the map through the same
|
|
65
|
-
* RSC-Flight codec the segments/value already use awaits Promises and serializes
|
|
66
|
-
* React elements, so the stored field is a lossless, JSON-safe string. The
|
|
67
|
-
* in-memory store keeps the same string by reference, so both backends replay
|
|
68
|
-
* identical decoded values.
|
|
69
|
-
*/
|
|
70
45
|
export async function encodeHandles(handles: HandleRecord): Promise<string> {
|
|
71
|
-
// No handle was pushed anywhere — store an empty marker (decoded as "skip").
|
|
72
46
|
if (!hasHandleData(handles)) return "";
|
|
73
47
|
return encodeHandleValue(handles);
|
|
74
48
|
}
|
|
75
49
|
|
|
76
|
-
/**
|
|
77
|
-
* Decode a stored handle string back to a handle map. Returns null on any
|
|
78
|
-
* decode failure (e.g. a cross-version entry read under a pinned static
|
|
79
|
-
* version), so the caller can skip handle restore without discarding the
|
|
80
|
-
* otherwise-valid cached segments alongside it.
|
|
81
|
-
*/
|
|
82
50
|
export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
83
51
|
return decodeHandleValue<HandleRecord>(encoded);
|
|
84
52
|
}
|
|
85
53
|
|
|
86
|
-
/**
|
|
87
|
-
* Encode an arbitrary handle-data value to a Flight string. Used directly by the
|
|
88
|
-
* prerender/static pipeline, whose static path holds a single segment's
|
|
89
|
-
* `SegmentHandleData` (not a segId-keyed map). Bounded by the same timeout as
|
|
90
|
-
* encodeHandles; failure/timeout coalesces to "". The caller owns the empty
|
|
91
|
-
* check (an empty value still encodes to a non-empty Flight string, so skip the
|
|
92
|
-
* call when there is nothing to store).
|
|
93
|
-
*/
|
|
94
54
|
export async function encodeHandleValue(value: unknown): Promise<string> {
|
|
95
55
|
const encoded = await withTimeout(
|
|
96
56
|
serializeResult(value),
|
package/src/cache/index.ts
CHANGED
|
@@ -1,36 +1,15 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Cache Store
|
|
3
|
-
*
|
|
4
|
-
* Server-side caching for RSC segments and loader data.
|
|
5
|
-
*
|
|
6
|
-
* Main exports for users:
|
|
7
|
-
* - SegmentCacheStore - Interface for implementing custom cache stores
|
|
8
|
-
* - MemorySegmentCacheStore - In-memory cache for development/testing
|
|
9
|
-
* - CFCacheStore - Cloudflare edge cache store for production
|
|
10
|
-
* - CacheScope / createCacheScope - Request-scoped cache provider
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// Segment cache store types and implementations
|
|
14
1
|
export type {
|
|
15
2
|
SegmentCacheStore,
|
|
16
|
-
SegmentCacheProvider,
|
|
17
3
|
CachedEntryData,
|
|
18
|
-
CachedEntryResult,
|
|
19
4
|
CacheGetResult,
|
|
20
|
-
// The getItem()/setItem() signature types on SegmentCacheStore. Exported
|
|
21
|
-
// alongside CacheGetResult so a consumer implementing a custom store can name
|
|
22
|
-
// every type its interface methods use, not just the segment-read result.
|
|
23
5
|
CacheItemResult,
|
|
24
6
|
CacheItemOptions,
|
|
25
7
|
SerializedSegmentData,
|
|
26
8
|
SegmentHandleData,
|
|
27
|
-
CacheConfig,
|
|
28
|
-
CacheConfigOrFactory,
|
|
29
9
|
} from "./types.js";
|
|
30
10
|
|
|
31
11
|
export { MemorySegmentCacheStore } from "./memory-segment-store.js";
|
|
32
12
|
|
|
33
|
-
// Cloudflare cache store
|
|
34
13
|
export {
|
|
35
14
|
CFCacheStore,
|
|
36
15
|
type CFCacheStoreOptions,
|
|
@@ -45,17 +24,11 @@ export {
|
|
|
45
24
|
KV_READ_TIMEOUT_MS,
|
|
46
25
|
} from "./cf/index.js";
|
|
47
26
|
|
|
48
|
-
// Cache scope
|
|
49
27
|
export { CacheScope, createCacheScope } from "./cache-scope.js";
|
|
50
28
|
|
|
51
|
-
// Document-level cache middleware
|
|
52
29
|
export {
|
|
53
30
|
createDocumentCacheMiddleware,
|
|
54
31
|
type DocumentCacheOptions,
|
|
55
32
|
} from "./document-cache.js";
|
|
56
33
|
|
|
57
|
-
// Cache error reporting. CacheErrorCategory is the discriminator surfaced to a
|
|
58
|
-
// router's onError callback as `metadata.category` for the `cache` phase, so
|
|
59
|
-
// consumers can branch on the failure kind (e.g. distinguish a transient
|
|
60
|
-
// cache-read outage from cache-corrupt self-heal).
|
|
61
34
|
export type { CacheErrorCategory } from "./cache-error.js";
|
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
CacheItemOptions,
|
|
15
15
|
} from "./types.js";
|
|
16
16
|
import type { RequestContext } from "../server/request-context.js";
|
|
17
|
+
import { isPerClientSignalHeader } from "../browser/cookie-name.js";
|
|
17
18
|
import {
|
|
18
19
|
resolveTtl,
|
|
19
20
|
resolveSwrWindow,
|
|
@@ -58,8 +59,6 @@ interface CachedResponseEntry {
|
|
|
58
59
|
|
|
59
60
|
interface CachedItemEntry {
|
|
60
61
|
value: string;
|
|
61
|
-
/** RSC-encoded handle data (see handle-snapshot.ts encodeHandles). Stored as
|
|
62
|
-
* the encoded string by reference, identical to the JSON-serializing stores. */
|
|
63
62
|
handles?: string;
|
|
64
63
|
expiresAt: number;
|
|
65
64
|
staleAt: number;
|
|
@@ -170,8 +169,6 @@ export class MemorySegmentCacheStore<
|
|
|
170
169
|
|
|
171
170
|
constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
|
|
172
171
|
if (options?.name != null) {
|
|
173
|
-
// Named stores use the globalThis registry so data survives HMR.
|
|
174
|
-
// Each name gets its own isolated Map.
|
|
175
172
|
this.cache = getNamedMap<CachedEntryData>(
|
|
176
173
|
CACHE_REGISTRY_KEY,
|
|
177
174
|
options.name,
|
|
@@ -193,7 +190,6 @@ export class MemorySegmentCacheStore<
|
|
|
193
190
|
options.name,
|
|
194
191
|
);
|
|
195
192
|
} else {
|
|
196
|
-
// Unnamed stores get a plain instance-level Map (no globalThis sharing).
|
|
197
193
|
this.cache = new Map<string, CachedEntryData>();
|
|
198
194
|
this.responseCache = new Map<string, CachedResponseEntry>();
|
|
199
195
|
this.itemCache = new Map<string, CachedItemEntry>();
|
|
@@ -228,15 +224,11 @@ export class MemorySegmentCacheStore<
|
|
|
228
224
|
ttl: number,
|
|
229
225
|
_swr?: number,
|
|
230
226
|
): Promise<void> {
|
|
231
|
-
// Note: Memory store doesn't implement SWR - entries just expire at TTL
|
|
232
|
-
// For SWR support, use CFCacheStore or similar distributed cache
|
|
233
227
|
const entry: CachedEntryData = {
|
|
234
228
|
...data,
|
|
235
229
|
expiresAt: Date.now() + ttl * 1000,
|
|
236
230
|
};
|
|
237
231
|
const prefixedKey = `seg:${key}`;
|
|
238
|
-
// Always drop stale tag mappings before writing so an overwrite with
|
|
239
|
-
// different (or no) tags cannot leave the previous tags pointing here.
|
|
240
232
|
this.unregisterTags(prefixedKey);
|
|
241
233
|
this.cache.set(key, entry);
|
|
242
234
|
if (data.tags && data.tags.length > 0) {
|
|
@@ -288,12 +280,10 @@ export class MemorySegmentCacheStore<
|
|
|
288
280
|
tags?: string[],
|
|
289
281
|
): Promise<void> {
|
|
290
282
|
try {
|
|
291
|
-
// arrayBuffer() can reject (e.g. an already-consumed body). A write
|
|
292
|
-
// failure must degrade to a no-op (entry simply not cached), never throw
|
|
293
|
-
// up and fail the request.
|
|
294
283
|
const body = await response.clone().arrayBuffer();
|
|
295
284
|
const headers: [string, string][] = [];
|
|
296
285
|
response.headers.forEach((value, name) => {
|
|
286
|
+
if (isPerClientSignalHeader(name)) return;
|
|
297
287
|
headers.push([name, value]);
|
|
298
288
|
});
|
|
299
289
|
|
|
@@ -363,19 +353,11 @@ export class MemorySegmentCacheStore<
|
|
|
363
353
|
}
|
|
364
354
|
}
|
|
365
355
|
|
|
366
|
-
/**
|
|
367
|
-
* Invalidate every cache entry (segment, response, item) tagged with any of
|
|
368
|
-
* `tags`. Entries are dropped immediately; the next read is a miss and
|
|
369
|
-
* re-renders fresh. This is the store-level primitive both updateTag() and
|
|
370
|
-
* revalidateTag() delegate to. (In-process, so there is nothing to batch
|
|
371
|
-
* beyond looping the tags.)
|
|
372
|
-
*/
|
|
373
356
|
async invalidateTags(tags: string[]): Promise<void> {
|
|
374
357
|
for (const tag of tags) {
|
|
375
358
|
const keys = this.tagIndex.get(tag);
|
|
376
359
|
if (!keys || keys.size === 0) continue;
|
|
377
360
|
|
|
378
|
-
// Snapshot the keys before mutating the index inside the loop.
|
|
379
361
|
const prefixedKeys = [...keys];
|
|
380
362
|
|
|
381
363
|
for (const prefixedKey of prefixedKeys) {
|
|
@@ -391,18 +373,11 @@ export class MemorySegmentCacheStore<
|
|
|
391
373
|
this.itemCache.delete(rawKey);
|
|
392
374
|
}
|
|
393
375
|
|
|
394
|
-
// Drop this key from every tag set it belonged to, not just `tag`.
|
|
395
376
|
this.unregisterTags(prefixedKey);
|
|
396
377
|
}
|
|
397
378
|
}
|
|
398
379
|
}
|
|
399
380
|
|
|
400
|
-
/**
|
|
401
|
-
* Register `tags` for a prefixed cache key in both the forward
|
|
402
|
-
* (tag -> keys) and reverse (key -> tags) indexes.
|
|
403
|
-
* Callers must call unregisterTags() first to clear stale mappings.
|
|
404
|
-
* @internal
|
|
405
|
-
*/
|
|
406
381
|
private registerTags(tags: string[], prefixedKey: string): void {
|
|
407
382
|
let tagSet = this.keyTags.get(prefixedKey);
|
|
408
383
|
if (!tagSet) {
|
|
@@ -420,11 +395,6 @@ export class MemorySegmentCacheStore<
|
|
|
420
395
|
}
|
|
421
396
|
}
|
|
422
397
|
|
|
423
|
-
/**
|
|
424
|
-
* Remove a prefixed cache key from every tag set it belongs to.
|
|
425
|
-
* Uses the reverse index so this is O(tags-per-key), not O(total-tags).
|
|
426
|
-
* @internal
|
|
427
|
-
*/
|
|
428
398
|
private unregisterTags(prefixedKey: string): void {
|
|
429
399
|
const tagSet = this.keyTags.get(prefixedKey);
|
|
430
400
|
if (!tagSet) return;
|
|
@@ -440,10 +410,6 @@ export class MemorySegmentCacheStore<
|
|
|
440
410
|
this.keyTags.delete(prefixedKey);
|
|
441
411
|
}
|
|
442
412
|
|
|
443
|
-
/**
|
|
444
|
-
* Get cache statistics for debugging purposes.
|
|
445
|
-
* @internal
|
|
446
|
-
*/
|
|
447
413
|
getStats(): { size: number; keys: string[] } {
|
|
448
414
|
return {
|
|
449
415
|
size: this.cache.size,
|
|
@@ -451,18 +417,6 @@ export class MemorySegmentCacheStore<
|
|
|
451
417
|
};
|
|
452
418
|
}
|
|
453
419
|
|
|
454
|
-
/**
|
|
455
|
-
* Reset the global cache registry.
|
|
456
|
-
* Useful for test isolation - call this in beforeEach to ensure
|
|
457
|
-
* tests don't share cache state via globalThis.
|
|
458
|
-
*
|
|
459
|
-
* @example
|
|
460
|
-
* ```typescript
|
|
461
|
-
* beforeEach(() => {
|
|
462
|
-
* MemorySegmentCacheStore.resetGlobalCache();
|
|
463
|
-
* });
|
|
464
|
-
* ```
|
|
465
|
-
*/
|
|
466
420
|
static resetGlobalCache(): void {
|
|
467
421
|
delete (globalThis as any)[CACHE_REGISTRY_KEY];
|
|
468
422
|
delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
|
|
@@ -64,9 +64,13 @@ export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
67
|
+
* Read a profile out of the global registry by name.
|
|
68
|
+
*
|
|
69
|
+
* There is currently no production reader: the "use cache: <profile>" runtime
|
|
70
|
+
* path resolves from the request-scoped _cacheProfiles map (see
|
|
71
|
+
* cache-runtime.ts), and the route DSL has no cache("profileName") form (see
|
|
72
|
+
* dsl-helpers.ts). This accessor exists so tests can assert what
|
|
73
|
+
* setCacheProfiles() wrote into the global registry.
|
|
70
74
|
*/
|
|
71
75
|
export function getCacheProfile(name: string): CacheProfile | undefined {
|
|
72
76
|
return _profiles[name];
|