@rangojs/router 0.0.0-experimental.97 → 0.0.0-experimental.98914650
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/README.md +24 -9
- package/dist/bin/rango.js +157 -63
- package/dist/testing/vitest.js +82 -0
- package/dist/vite/index.js +1584 -639
- package/package.json +71 -21
- package/skills/api-client/SKILL.md +211 -0
- package/skills/breadcrumbs/SKILL.md +60 -0
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +222 -30
- package/skills/caching/SKILL.md +263 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/css/SKILL.md +76 -0
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +235 -28
- package/skills/host-router/SKILL.md +122 -22
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +29 -5
- package/skills/layout/SKILL.md +13 -9
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +170 -23
- package/skills/middleware/SKILL.md +16 -10
- package/skills/migrate-nextjs/SKILL.md +38 -16
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +11 -7
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +250 -25
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +114 -47
- package/skills/route/SKILL.md +42 -5
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +78 -42
- package/skills/tailwind/SKILL.md +27 -3
- package/skills/testing/SKILL.md +129 -0
- package/skills/testing/bindings.md +89 -0
- package/skills/testing/cache-prerender.md +124 -0
- package/skills/testing/client-components.md +122 -0
- package/skills/testing/e2e-parity.md +125 -0
- package/skills/testing/flight.md +92 -0
- package/skills/testing/handles.md +129 -0
- package/skills/testing/loader.md +128 -0
- package/skills/testing/middleware.md +99 -0
- package/skills/testing/render-handler.md +121 -0
- package/skills/testing/response-routes.md +95 -0
- package/skills/testing/reverse-and-types.md +84 -0
- package/skills/testing/server-actions.md +107 -0
- package/skills/testing/server-tree.md +128 -0
- package/skills/testing/setup.md +120 -0
- package/skills/typesafety/SKILL.md +316 -26
- package/skills/use-cache/SKILL.md +36 -5
- package/skills/vercel/SKILL.md +107 -0
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +116 -0
- package/src/__internal.ts +0 -65
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/action-fence.ts +47 -0
- package/src/browser/app-shell.ts +14 -27
- package/src/browser/cookie-name.ts +140 -0
- package/src/browser/event-controller.ts +37 -143
- 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 +30 -59
- package/src/browser/navigation-client.ts +96 -84
- package/src/browser/navigation-store-handle.ts +38 -0
- package/src/browser/navigation-store.ts +32 -82
- package/src/browser/navigation-transaction.ts +9 -59
- package/src/browser/partial-update.ts +60 -127
- package/src/browser/prefetch/cache.ts +82 -72
- package/src/browser/prefetch/fetch.ts +108 -33
- package/src/browser/prefetch/queue.ts +6 -3
- package/src/browser/rango-state.ts +157 -115
- package/src/browser/react/Link.tsx +0 -2
- package/src/browser/react/NavigationProvider.tsx +41 -48
- 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 -48
- package/src/browser/react/location-state-shared.ts +166 -8
- package/src/browser/react/location-state.ts +39 -14
- package/src/browser/react/use-action.ts +6 -15
- package/src/browser/react/use-handle.ts +17 -14
- 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 +11 -11
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +20 -5
- package/src/browser/react/use-search-params.ts +0 -5
- package/src/browser/react/use-segments.ts +0 -13
- package/src/browser/response-adapter.ts +52 -1
- package/src/browser/rsc-router.tsx +70 -34
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +168 -44
- package/src/browser/types.ts +36 -21
- 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 +3 -0
- package/src/build/index.ts +8 -2
- package/src/build/prefix-tree-utils.ts +123 -0
- package/src/build/route-trie.ts +89 -10
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/param-extraction.ts +6 -3
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +122 -22
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-error.ts +104 -0
- package/src/cache/cache-policy.ts +68 -28
- package/src/cache/cache-runtime.ts +134 -32
- package/src/cache/cache-scope.ts +100 -74
- package/src/cache/cache-tag.ts +98 -0
- package/src/cache/cf/cf-cache-store.ts +2255 -238
- package/src/cache/cf/index.ts +6 -16
- package/src/cache/document-cache.ts +61 -20
- package/src/cache/handle-snapshot.ts +63 -0
- package/src/cache/index.ts +22 -20
- package/src/cache/memory-segment-store.ts +136 -37
- package/src/cache/profile-registry.ts +6 -30
- package/src/cache/read-through-swr.ts +41 -11
- package/src/cache/segment-codec.ts +0 -16
- package/src/cache/tag-invalidation.ts +230 -0
- package/src/cache/types.ts +33 -100
- package/src/cache/vercel/index.ts +11 -0
- package/src/cache/vercel/vercel-cache-store.ts +799 -0
- package/src/client.rsc.tsx +6 -21
- package/src/client.tsx +25 -61
- package/src/component-utils.ts +19 -0
- package/src/context-var.ts +17 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/defer.ts +196 -0
- package/src/deps/ssr.ts +0 -1
- package/src/errors.ts +30 -4
- package/src/handle.ts +31 -23
- package/src/handles/MetaTags.tsx +0 -14
- package/src/handles/breadcrumbs.ts +16 -5
- package/src/handles/meta.ts +0 -39
- package/src/host/cookie-handler.ts +0 -36
- package/src/host/errors.ts +0 -24
- package/src/host/index.ts +8 -2
- package/src/host/pattern-matcher.ts +7 -50
- package/src/host/router.ts +107 -99
- package/src/host/testing.ts +40 -27
- package/src/host/types.ts +37 -4
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +137 -22
- package/src/index.rsc.ts +63 -9
- package/src/index.ts +64 -9
- package/src/internal-debug.ts +2 -4
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +20 -13
- package/src/loader.ts +12 -11
- package/src/missing-id-error.ts +68 -0
- package/src/network-error-thrower.tsx +1 -6
- package/src/outlet-provider.tsx +1 -5
- package/src/prerender/param-hash.ts +10 -11
- package/src/prerender/store.ts +32 -37
- package/src/prerender.ts +61 -6
- package/src/redirect-origin.ts +100 -0
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -40
- package/src/root-error-boundary.tsx +1 -19
- package/src/route-content-wrapper.tsx +7 -72
- package/src/route-definition/dsl-helpers.ts +244 -281
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +40 -17
- package/src/route-definition/redirect.ts +43 -9
- package/src/route-definition/resolve-handler-use.ts +6 -0
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-map-builder.ts +0 -16
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -15
- package/src/router/error-handling.ts +13 -17
- package/src/router/find-match.ts +44 -23
- package/src/router/handler-context.ts +4 -41
- package/src/router/intercept-resolution.ts +14 -19
- package/src/router/lazy-includes.ts +9 -46
- package/src/router/loader-resolution.ts +91 -46
- package/src/router/logging.ts +0 -6
- package/src/router/manifest.ts +18 -29
- 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 +150 -271
- package/src/router/match-middleware/cache-store.ts +3 -33
- 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 +31 -80
- package/src/router/metrics.ts +0 -34
- package/src/router/middleware-types.ts +5 -112
- package/src/router/middleware.ts +118 -133
- package/src/router/navigation-snapshot.ts +0 -51
- package/src/router/params-util.ts +23 -0
- package/src/router/pattern-matching.ts +62 -67
- package/src/router/prerender-match.ts +99 -63
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +28 -62
- package/src/router/revalidation.ts +50 -56
- package/src/router/route-snapshot.ts +0 -1
- package/src/router/router-context.ts +0 -27
- package/src/router/router-interfaces.ts +68 -35
- package/src/router/router-options.ts +55 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +44 -63
- package/src/router/segment-resolution/helpers.ts +34 -0
- package/src/router/segment-resolution/loader-cache.ts +40 -37
- package/src/router/segment-resolution/revalidation.ts +203 -285
- package/src/router/segment-resolution/static-store.ts +19 -5
- package/src/router/segment-resolution/streamed-handler-telemetry.ts +52 -0
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/segment-resolution.ts +4 -1
- package/src/router/segment-wrappers.ts +0 -3
- package/src/router/state-cookie-name.ts +33 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +0 -20
- package/src/router/telemetry.ts +96 -19
- package/src/router/timeout.ts +0 -20
- package/src/router/trie-matching.ts +87 -48
- package/src/router/types.ts +9 -63
- package/src/router/url-params.ts +0 -5
- package/src/router.ts +80 -41
- package/src/rsc/handler-context.ts +3 -2
- package/src/rsc/handler.ts +83 -78
- package/src/rsc/helpers.ts +93 -5
- package/src/rsc/index.ts +1 -1
- package/src/rsc/json-route-result.ts +38 -0
- package/src/rsc/manifest-init.ts +28 -41
- package/src/rsc/origin-guard.ts +39 -25
- package/src/rsc/progressive-enhancement.ts +12 -1
- package/src/rsc/redirect-guard.ts +99 -0
- package/src/rsc/response-error.ts +79 -12
- package/src/rsc/response-route-handler.ts +76 -62
- package/src/rsc/rsc-rendering.ts +41 -60
- package/src/rsc/runtime-warnings.ts +23 -10
- package/src/rsc/server-action.ts +62 -67
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +10 -5
- package/src/runtime-env.ts +18 -0
- package/src/search-params.ts +4 -20
- package/src/segment-loader-promise.ts +14 -2
- package/src/segment-system.tsx +199 -142
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +150 -51
- package/src/server/cookie-store.ts +80 -5
- package/src/server/handle-store.ts +7 -24
- package/src/server/loader-registry.ts +5 -24
- package/src/server/request-context.ts +165 -87
- package/src/ssr/index.tsx +14 -14
- 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 +618 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +188 -0
- package/src/testing/e2e/index.ts +128 -0
- package/src/testing/e2e/matchers.ts +35 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +387 -0
- package/src/testing/e2e/server.ts +195 -0
- package/src/testing/flight-matchers.ts +97 -0
- package/src/testing/flight-normalize.ts +11 -0
- package/src/testing/flight-runtime.d.ts +57 -0
- package/src/testing/flight-tree.ts +682 -0
- package/src/testing/flight.entry.ts +52 -0
- package/src/testing/flight.ts +232 -0
- package/src/testing/generated-routes.ts +183 -0
- package/src/testing/index.ts +99 -0
- package/src/testing/internal/context.ts +348 -0
- package/src/testing/internal/flight-client-globals.ts +30 -0
- package/src/testing/internal/seed-vars.ts +54 -0
- package/src/testing/render-handler.ts +330 -0
- package/src/testing/render-route.tsx +566 -0
- package/src/testing/run-loader.ts +378 -0
- package/src/testing/run-middleware.ts +205 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +305 -0
- package/src/theme/ThemeProvider.tsx +0 -52
- package/src/theme/ThemeScript.tsx +0 -6
- package/src/theme/constants.ts +0 -12
- package/src/theme/index.ts +0 -7
- package/src/theme/theme-context.ts +1 -5
- package/src/theme/theme-script.ts +0 -14
- package/src/theme/use-theme.ts +0 -3
- package/src/types/boundaries.ts +0 -35
- package/src/types/cache-types.ts +13 -4
- package/src/types/error-types.ts +30 -90
- package/src/types/global-namespace.ts +54 -41
- package/src/types/handler-context.ts +97 -22
- package/src/types/index.ts +1 -10
- package/src/types/loader-types.ts +6 -3
- 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 +18 -14
- package/src/urls/include-helper.ts +9 -56
- package/src/urls/index.ts +1 -11
- package/src/urls/path-helper-types.ts +19 -5
- package/src/urls/path-helper.ts +17 -106
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +58 -139
- package/src/urls/urls-function.ts +1 -18
- package/src/use-loader.tsx +292 -107
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +8 -7
- package/src/vite/discovery/discover-routers.ts +95 -82
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +26 -34
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +39 -1
- package/src/vite/discovery/virtual-module-codegen.ts +14 -34
- package/src/vite/index.ts +4 -0
- package/src/vite/plugin-types.ts +185 -10
- package/src/vite/plugins/cjs-to-esm.ts +3 -18
- package/src/vite/plugins/client-ref-dedup.ts +0 -11
- package/src/vite/plugins/client-ref-hashing.ts +12 -11
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -21
- package/src/vite/plugins/expose-action-id.ts +4 -75
- package/src/vite/plugins/expose-id-utils.ts +3 -54
- package/src/vite/plugins/expose-ids/export-analysis.ts +76 -34
- package/src/vite/plugins/expose-ids/handler-transform.ts +6 -74
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -20
- package/src/vite/plugins/expose-ids/router-transform.ts +0 -13
- package/src/vite/plugins/expose-internal-ids.ts +57 -67
- package/src/vite/plugins/performance-tracks.ts +9 -16
- package/src/vite/plugins/refresh-cmd.ts +1 -1
- package/src/vite/plugins/use-cache-transform.ts +26 -49
- package/src/vite/plugins/vercel-output.ts +258 -0
- package/src/vite/plugins/version-injector.ts +2 -32
- package/src/vite/plugins/version-plugin.ts +32 -23
- package/src/vite/plugins/virtual-entries.ts +35 -17
- package/src/vite/rango.ts +148 -115
- package/src/vite/router-discovery.ts +220 -68
- package/src/vite/utils/ast-handler-extract.ts +15 -31
- package/src/vite/utils/bundle-analysis.ts +10 -15
- package/src/vite/utils/client-chunks.ts +184 -0
- package/src/vite/utils/forward-user-plugins.ts +171 -0
- package/src/vite/utils/manifest-utils.ts +4 -59
- package/src/vite/utils/package-resolution.ts +1 -73
- package/src/vite/utils/prerender-utils.ts +0 -34
- package/src/vite/utils/shared-utils.ts +95 -43
- package/src/browser/action-response-classifier.ts +0 -99
- package/src/browser/react/use-client-cache.ts +0 -58
- package/src/browser/shallow.ts +0 -40
- package/src/handles/index.ts +0 -7
- package/src/router/middleware-cookies.ts +0 -55
package/src/cache/cf/index.ts
CHANGED
|
@@ -1,29 +1,19 @@
|
|
|
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,
|
|
4
|
+
type CFCacheDebug,
|
|
5
|
+
type CFCacheReadDebugEvent,
|
|
16
6
|
type KVNamespace,
|
|
17
7
|
} from "./cf-cache-store.js";
|
|
18
8
|
|
|
19
|
-
// Header constants for debugging and inspection
|
|
20
9
|
export {
|
|
21
10
|
CACHE_STALE_AT_HEADER,
|
|
22
11
|
CACHE_STATUS_HEADER,
|
|
12
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
23
13
|
} from "./cf-cache-store.js";
|
|
24
14
|
|
|
25
|
-
// Internal exports (re-exported for backwards compatibility, marked @internal in source)
|
|
26
15
|
export {
|
|
27
|
-
|
|
28
|
-
|
|
16
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
17
|
+
EDGE_READ_TIMEOUT_MS,
|
|
18
|
+
KV_READ_TIMEOUT_MS,
|
|
29
19
|
} from "./cf-cache-store.js";
|
|
@@ -12,23 +12,25 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { MiddlewareFn, MiddlewareContext } from "../router/middleware.js";
|
|
15
|
-
import {
|
|
15
|
+
import { hasPerClientSignal } from "../browser/cookie-name.js";
|
|
16
|
+
import {
|
|
17
|
+
getRequestContext,
|
|
18
|
+
type RequestContext,
|
|
19
|
+
} from "../server/request-context.js";
|
|
16
20
|
import { mayNeedSSR } from "../rsc/ssr-setup.js";
|
|
17
21
|
import { sortedSearchString } from "./cache-key-utils.js";
|
|
18
22
|
import { runBackground } from "./background-task.js";
|
|
23
|
+
import { reportCacheError } from "./cache-error.js";
|
|
19
24
|
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Constants
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
/** Header indicating cache status for debugging */
|
|
25
25
|
const CACHE_STATUS_HEADER = "x-document-cache-status";
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
function collectRequestTags(
|
|
28
|
+
requestCtx: RequestContext | undefined,
|
|
29
|
+
): string[] | undefined {
|
|
30
|
+
const tags = requestCtx?._requestTags;
|
|
31
|
+
return tags && tags.size > 0 ? [...tags] : undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
32
34
|
function hashSegmentIds(segmentIds: string): string {
|
|
33
35
|
if (!segmentIds) return "";
|
|
34
36
|
|
|
@@ -37,12 +39,9 @@ function hashSegmentIds(segmentIds: string): string {
|
|
|
37
39
|
const char = segmentIds.charCodeAt(i);
|
|
38
40
|
hash = ((hash << 5) - hash + char) | 0;
|
|
39
41
|
}
|
|
40
|
-
// Convert to base36 for shorter string, take absolute value
|
|
41
42
|
return Math.abs(hash).toString(36);
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
// ============================================================================
|
|
45
|
-
// Cache Control Parsing
|
|
46
45
|
// ============================================================================
|
|
47
46
|
|
|
48
47
|
interface CacheDirectives {
|
|
@@ -56,6 +55,16 @@ interface CacheDirectives {
|
|
|
56
55
|
function parseCacheControl(header: string | null): CacheDirectives | null {
|
|
57
56
|
if (!header) return null;
|
|
58
57
|
|
|
58
|
+
// RFC 7234: in a SHARED cache, `private` and `no-store` forbid storage and
|
|
59
|
+
// MUST win over `s-maxage` even though `private, s-maxage` is contradictory.
|
|
60
|
+
// The document cache is a shared edge store, so refuse both regardless of any
|
|
61
|
+
// s-maxage / stale-while-revalidate also present. Match standalone directive
|
|
62
|
+
// tokens (start/end, whitespace, comma, semicolon, or `=` bounded), not a
|
|
63
|
+
// substring, so a value containing "private" cannot false-veto.
|
|
64
|
+
if (/(^|[\s,;])(private|no-store)(?=$|[\s,;=])/i.test(header)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
const directives: CacheDirectives = {};
|
|
60
69
|
|
|
61
70
|
// Parse s-maxage
|
|
@@ -87,6 +96,16 @@ function shouldCacheResponse(response: Response): CacheDirectives | null {
|
|
|
87
96
|
return null;
|
|
88
97
|
}
|
|
89
98
|
|
|
99
|
+
// Never cache a per-client signal into a SHARED response store. A Set-Cookie
|
|
100
|
+
// (e.g. a rango state rotation from invalidateClientCache(), or any cookie a
|
|
101
|
+
// loader set) would be replayed to every client on a hit — pinning them to
|
|
102
|
+
// one value and even rolling a rotated client back to a prior one. The
|
|
103
|
+
// x-rango-keep-cache directive header is the mirror image: a replayed "keep"
|
|
104
|
+
// would suppress invalidation for every replayed client. Refuse both.
|
|
105
|
+
if (hasPerClientSignal(response.headers)) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
90
109
|
const cacheControl = response.headers.get("Cache-Control");
|
|
91
110
|
return parseCacheControl(cacheControl);
|
|
92
111
|
}
|
|
@@ -303,17 +322,26 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
303
322
|
const fresh = await next();
|
|
304
323
|
const directives = shouldCacheResponse(fresh);
|
|
305
324
|
|
|
306
|
-
if (directives) {
|
|
325
|
+
if (directives && fresh.body) {
|
|
326
|
+
// Background revalidation: nothing streams to a client, so drain
|
|
327
|
+
// the fresh render fully before snapshotting tags (same
|
|
328
|
+
// render-complete barrier as the miss path).
|
|
329
|
+
const body = await new Response(fresh.body).arrayBuffer();
|
|
307
330
|
await store.putResponse!(
|
|
308
331
|
cacheKey,
|
|
309
|
-
fresh,
|
|
332
|
+
new Response(body, fresh),
|
|
310
333
|
directives.sMaxAge!,
|
|
311
334
|
directives.staleWhileRevalidate,
|
|
335
|
+
collectRequestTags(requestCtx),
|
|
312
336
|
);
|
|
313
337
|
log(`[DocumentCache] REVALIDATED ${typeLabel}: ${url.pathname}`);
|
|
314
338
|
}
|
|
315
339
|
} catch (error) {
|
|
316
|
-
|
|
340
|
+
reportCacheError(
|
|
341
|
+
error,
|
|
342
|
+
"cache-write",
|
|
343
|
+
"[DocumentCache] revalidation",
|
|
344
|
+
);
|
|
317
345
|
}
|
|
318
346
|
});
|
|
319
347
|
|
|
@@ -346,14 +374,27 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
346
374
|
// Clone response for caching (non-blocking)
|
|
347
375
|
runBackground(requestCtx, async () => {
|
|
348
376
|
try {
|
|
377
|
+
// Drain the cache copy fully BEFORE snapshotting tags. Tags from
|
|
378
|
+
// Suspense-streamed "use cache"/cacheTag and loaders are recorded as
|
|
379
|
+
// each value resolves during the RSC/HTML render, which completes
|
|
380
|
+
// only when the stream ends - the handler-settlement barrier is too
|
|
381
|
+
// early. Buffering the body (the client streams the other tee branch,
|
|
382
|
+
// unaffected) is the render-complete barrier that keeps the cached
|
|
383
|
+
// body and its tag set consistent.
|
|
384
|
+
const body = await new Response(cacheStream).arrayBuffer();
|
|
349
385
|
await store.putResponse!(
|
|
350
386
|
cacheKey,
|
|
351
|
-
new Response(
|
|
387
|
+
new Response(body, originalResponse),
|
|
352
388
|
directives.sMaxAge!,
|
|
353
389
|
directives.staleWhileRevalidate,
|
|
390
|
+
collectRequestTags(requestCtx),
|
|
354
391
|
);
|
|
355
392
|
} catch (error) {
|
|
356
|
-
|
|
393
|
+
reportCacheError(
|
|
394
|
+
error,
|
|
395
|
+
"cache-write",
|
|
396
|
+
"[DocumentCache] cache write",
|
|
397
|
+
);
|
|
357
398
|
}
|
|
358
399
|
});
|
|
359
400
|
|
|
@@ -366,7 +407,7 @@ export function createDocumentCacheMiddleware<TEnv = any>(
|
|
|
366
407
|
// No cache headers - pass through
|
|
367
408
|
return originalResponse;
|
|
368
409
|
} catch (error) {
|
|
369
|
-
|
|
410
|
+
reportCacheError(error, "cache-read", "[DocumentCache] middleware");
|
|
370
411
|
if (handlerCalled) {
|
|
371
412
|
// Post-handler failure (e.g. body.tee()): do not call next() again
|
|
372
413
|
// as that would re-run handler side effects.
|
|
@@ -9,6 +9,69 @@
|
|
|
9
9
|
import type { ResolvedSegment } from "../types.js";
|
|
10
10
|
import type { HandleStore } from "../server/handle-store.js";
|
|
11
11
|
import type { SegmentHandleData } from "./types.js";
|
|
12
|
+
import { serializeResult, deserializeResult } from "./segment-codec.js";
|
|
13
|
+
|
|
14
|
+
const HANDLE_ENCODE_TIMEOUT_MS = 5000;
|
|
15
|
+
|
|
16
|
+
type HandleRecord = Record<string, SegmentHandleData>;
|
|
17
|
+
|
|
18
|
+
function hasHandleData(handles: HandleRecord): boolean {
|
|
19
|
+
for (const segId in handles) {
|
|
20
|
+
for (const _ in handles[segId]) return true;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function withTimeout<T>(p: Promise<T>, ms: number, onTimeout: T): Promise<T> {
|
|
26
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
27
|
+
const timeout = new Promise<T>((resolve) => {
|
|
28
|
+
timer = setTimeout(() => resolve(onTimeout), ms);
|
|
29
|
+
});
|
|
30
|
+
return Promise.race([
|
|
31
|
+
p.then(
|
|
32
|
+
(v) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
return v;
|
|
35
|
+
},
|
|
36
|
+
(e) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
throw e;
|
|
39
|
+
},
|
|
40
|
+
),
|
|
41
|
+
timeout,
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function encodeHandles(handles: HandleRecord): Promise<string> {
|
|
46
|
+
if (!hasHandleData(handles)) return "";
|
|
47
|
+
return encodeHandleValue(handles);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function decodeHandles(encoded: string): Promise<HandleRecord | null> {
|
|
51
|
+
return decodeHandleValue<HandleRecord>(encoded);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function encodeHandleValue(value: unknown): Promise<string> {
|
|
55
|
+
const encoded = await withTimeout(
|
|
56
|
+
serializeResult(value),
|
|
57
|
+
HANDLE_ENCODE_TIMEOUT_MS,
|
|
58
|
+
null,
|
|
59
|
+
);
|
|
60
|
+
return encoded ?? "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Decode a Flight-encoded handle-data string. Returns null on any decode
|
|
65
|
+
* failure so the caller can skip handle restore without discarding valid
|
|
66
|
+
* cached/prerendered segments.
|
|
67
|
+
*/
|
|
68
|
+
export async function decodeHandleValue<T>(encoded: string): Promise<T | null> {
|
|
69
|
+
try {
|
|
70
|
+
return await deserializeResult<T>(encoded);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
12
75
|
|
|
13
76
|
/**
|
|
14
77
|
* Capture handle data for a set of segments from the handle store.
|
package/src/cache/index.ts
CHANGED
|
@@ -1,44 +1,46 @@
|
|
|
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,
|
|
5
|
+
CacheItemResult,
|
|
6
|
+
CacheItemOptions,
|
|
20
7
|
SerializedSegmentData,
|
|
21
8
|
SegmentHandleData,
|
|
22
|
-
CacheConfig,
|
|
23
|
-
CacheConfigOrFactory,
|
|
24
9
|
} from "./types.js";
|
|
25
10
|
|
|
26
11
|
export { MemorySegmentCacheStore } from "./memory-segment-store.js";
|
|
27
12
|
|
|
28
|
-
// Cloudflare cache store
|
|
29
13
|
export {
|
|
30
14
|
CFCacheStore,
|
|
31
15
|
type CFCacheStoreOptions,
|
|
16
|
+
type CFCacheDebug,
|
|
17
|
+
type CFCacheReadDebugEvent,
|
|
32
18
|
type KVNamespace,
|
|
33
19
|
CACHE_STALE_AT_HEADER,
|
|
34
20
|
CACHE_STATUS_HEADER,
|
|
21
|
+
CACHE_REVALIDATING_AT_HEADER,
|
|
22
|
+
EDGE_LOOKUP_TIMEOUT_MS,
|
|
23
|
+
EDGE_READ_TIMEOUT_MS,
|
|
24
|
+
KV_READ_TIMEOUT_MS,
|
|
35
25
|
} from "./cf/index.js";
|
|
36
26
|
|
|
37
|
-
|
|
27
|
+
export {
|
|
28
|
+
VercelCacheStore,
|
|
29
|
+
type VercelCacheStoreOptions,
|
|
30
|
+
type VercelRuntimeCache,
|
|
31
|
+
type VercelCacheDebug,
|
|
32
|
+
type VercelCacheReadDebugEvent,
|
|
33
|
+
type VercelCacheReadOutcome,
|
|
34
|
+
VERCEL_MAX_ITEM_BYTES,
|
|
35
|
+
VERCEL_MAX_TAGS_PER_ITEM,
|
|
36
|
+
VERCEL_MAX_TAG_BYTES,
|
|
37
|
+
} from "./vercel/index.js";
|
|
38
|
+
|
|
38
39
|
export { CacheScope, createCacheScope } from "./cache-scope.js";
|
|
39
40
|
|
|
40
|
-
// Document-level cache middleware
|
|
41
41
|
export {
|
|
42
42
|
createDocumentCacheMiddleware,
|
|
43
43
|
type DocumentCacheOptions,
|
|
44
44
|
} from "./document-cache.js";
|
|
45
|
+
|
|
46
|
+
export type { CacheErrorCategory } from "./cache-error.js";
|
|
@@ -12,19 +12,22 @@ import type {
|
|
|
12
12
|
CacheGetResult,
|
|
13
13
|
CacheItemResult,
|
|
14
14
|
CacheItemOptions,
|
|
15
|
-
SegmentHandleData,
|
|
16
15
|
} from "./types.js";
|
|
17
16
|
import type { RequestContext } from "../server/request-context.js";
|
|
17
|
+
import { isPerClientSignalHeader } from "../browser/cookie-name.js";
|
|
18
18
|
import {
|
|
19
19
|
resolveTtl,
|
|
20
20
|
resolveSwrWindow,
|
|
21
21
|
computeExpiration,
|
|
22
22
|
DEFAULT_FUNCTION_TTL,
|
|
23
23
|
} from "./cache-policy.js";
|
|
24
|
+
import { reportCacheError } from "./cache-error.js";
|
|
24
25
|
|
|
25
26
|
const CACHE_REGISTRY_KEY = "__rsc_router_segment_cache_registry__";
|
|
26
27
|
const RESPONSE_CACHE_REGISTRY_KEY = "__rsc_router_response_cache_registry__";
|
|
27
28
|
const ITEM_CACHE_REGISTRY_KEY = "__rsc_router_item_cache_registry__";
|
|
29
|
+
const TAG_INDEX_REGISTRY_KEY = "__rsc_router_tag_index_registry__";
|
|
30
|
+
const KEY_TAGS_REGISTRY_KEY = "__rsc_router_key_tags_registry__";
|
|
28
31
|
|
|
29
32
|
/**
|
|
30
33
|
* Get or create a named Map from a globalThis-backed registry.
|
|
@@ -56,9 +59,10 @@ interface CachedResponseEntry {
|
|
|
56
59
|
|
|
57
60
|
interface CachedItemEntry {
|
|
58
61
|
value: string;
|
|
59
|
-
handles?:
|
|
62
|
+
handles?: string;
|
|
60
63
|
expiresAt: number;
|
|
61
64
|
staleAt: number;
|
|
65
|
+
tags?: string[];
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/**
|
|
@@ -73,6 +77,11 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
|
73
77
|
* When omitted, the store uses a plain instance-level Map with no
|
|
74
78
|
* globalThis sharing, which is the safest default for isolation.
|
|
75
79
|
*
|
|
80
|
+
* Caveat: two instances constructed with the SAME name share all backing maps
|
|
81
|
+
* (data + tag index), but each keeps its OWN `defaults` and `keyGenerator` from
|
|
82
|
+
* its options - those are not shared. Use one instance per name, or keep the
|
|
83
|
+
* options identical, to avoid surprising divergence.
|
|
84
|
+
*
|
|
76
85
|
* @example
|
|
77
86
|
* ```typescript
|
|
78
87
|
* // Two named stores are isolated from each other
|
|
@@ -121,6 +130,11 @@ export interface MemorySegmentCacheStoreOptions<TEnv = unknown> {
|
|
|
121
130
|
* For production with multiple instances, use a distributed store
|
|
122
131
|
* like Cloudflare KV or Redis.
|
|
123
132
|
*
|
|
133
|
+
* Tag-index cleanup is lazy, mirroring the data maps: a tagged entry that
|
|
134
|
+
* expires but is never re-read or invalidated leaves its forward+reverse index
|
|
135
|
+
* entries resident until the key is reused or invalidated. This is bounded by
|
|
136
|
+
* the distinct-tag count and acceptable for a dev/single-instance store.
|
|
137
|
+
*
|
|
124
138
|
* @example
|
|
125
139
|
* ```typescript
|
|
126
140
|
* // Basic usage
|
|
@@ -143,6 +157,10 @@ export class MemorySegmentCacheStore<
|
|
|
143
157
|
private cache: Map<string, CachedEntryData>;
|
|
144
158
|
private responseCache: Map<string, CachedResponseEntry>;
|
|
145
159
|
private itemCache: Map<string, CachedItemEntry>;
|
|
160
|
+
/** tag -> set of prefixed cache keys (seg:key, res:key, item:key) */
|
|
161
|
+
private tagIndex: Map<string, Set<string>>;
|
|
162
|
+
/** prefixed cache key -> set of tags (reverse index for O(tags) unregister) */
|
|
163
|
+
private keyTags: Map<string, Set<string>>;
|
|
146
164
|
readonly defaults?: CacheDefaults;
|
|
147
165
|
readonly keyGenerator?: (
|
|
148
166
|
ctx: RequestContext<TEnv>,
|
|
@@ -151,8 +169,6 @@ export class MemorySegmentCacheStore<
|
|
|
151
169
|
|
|
152
170
|
constructor(options?: MemorySegmentCacheStoreOptions<TEnv>) {
|
|
153
171
|
if (options?.name != null) {
|
|
154
|
-
// Named stores use the globalThis registry so data survives HMR.
|
|
155
|
-
// Each name gets its own isolated Map.
|
|
156
172
|
this.cache = getNamedMap<CachedEntryData>(
|
|
157
173
|
CACHE_REGISTRY_KEY,
|
|
158
174
|
options.name,
|
|
@@ -165,11 +181,20 @@ export class MemorySegmentCacheStore<
|
|
|
165
181
|
ITEM_CACHE_REGISTRY_KEY,
|
|
166
182
|
options.name,
|
|
167
183
|
);
|
|
184
|
+
this.tagIndex = getNamedMap<Set<string>>(
|
|
185
|
+
TAG_INDEX_REGISTRY_KEY,
|
|
186
|
+
options.name,
|
|
187
|
+
);
|
|
188
|
+
this.keyTags = getNamedMap<Set<string>>(
|
|
189
|
+
KEY_TAGS_REGISTRY_KEY,
|
|
190
|
+
options.name,
|
|
191
|
+
);
|
|
168
192
|
} else {
|
|
169
|
-
// Unnamed stores get a plain instance-level Map (no globalThis sharing).
|
|
170
193
|
this.cache = new Map<string, CachedEntryData>();
|
|
171
194
|
this.responseCache = new Map<string, CachedResponseEntry>();
|
|
172
195
|
this.itemCache = new Map<string, CachedItemEntry>();
|
|
196
|
+
this.tagIndex = new Map<string, Set<string>>();
|
|
197
|
+
this.keyTags = new Map<string, Set<string>>();
|
|
173
198
|
}
|
|
174
199
|
this.defaults = options?.defaults;
|
|
175
200
|
this.keyGenerator = options?.keyGenerator;
|
|
@@ -184,6 +209,7 @@ export class MemorySegmentCacheStore<
|
|
|
184
209
|
|
|
185
210
|
// Check expiration
|
|
186
211
|
if (Date.now() > cached.expiresAt) {
|
|
212
|
+
this.unregisterTags(`seg:${key}`);
|
|
187
213
|
this.cache.delete(key);
|
|
188
214
|
return null;
|
|
189
215
|
}
|
|
@@ -198,16 +224,20 @@ export class MemorySegmentCacheStore<
|
|
|
198
224
|
ttl: number,
|
|
199
225
|
_swr?: number,
|
|
200
226
|
): Promise<void> {
|
|
201
|
-
// Note: Memory store doesn't implement SWR - entries just expire at TTL
|
|
202
|
-
// For SWR support, use CFCacheStore or similar distributed cache
|
|
203
227
|
const entry: CachedEntryData = {
|
|
204
228
|
...data,
|
|
205
229
|
expiresAt: Date.now() + ttl * 1000,
|
|
206
230
|
};
|
|
231
|
+
const prefixedKey = `seg:${key}`;
|
|
232
|
+
this.unregisterTags(prefixedKey);
|
|
207
233
|
this.cache.set(key, entry);
|
|
234
|
+
if (data.tags && data.tags.length > 0) {
|
|
235
|
+
this.registerTags(data.tags, prefixedKey);
|
|
236
|
+
}
|
|
208
237
|
}
|
|
209
238
|
|
|
210
239
|
async delete(key: string): Promise<boolean> {
|
|
240
|
+
this.unregisterTags(`seg:${key}`);
|
|
211
241
|
return this.cache.delete(key);
|
|
212
242
|
}
|
|
213
243
|
|
|
@@ -215,6 +245,8 @@ export class MemorySegmentCacheStore<
|
|
|
215
245
|
this.cache.clear();
|
|
216
246
|
this.responseCache.clear();
|
|
217
247
|
this.itemCache.clear();
|
|
248
|
+
this.tagIndex.clear();
|
|
249
|
+
this.keyTags.clear();
|
|
218
250
|
}
|
|
219
251
|
|
|
220
252
|
async getResponse(
|
|
@@ -224,6 +256,7 @@ export class MemorySegmentCacheStore<
|
|
|
224
256
|
if (!cached) return null;
|
|
225
257
|
|
|
226
258
|
if (Date.now() > cached.expiresAt) {
|
|
259
|
+
this.unregisterTags(`res:${key}`);
|
|
227
260
|
this.responseCache.delete(key);
|
|
228
261
|
return null;
|
|
229
262
|
}
|
|
@@ -244,23 +277,38 @@ export class MemorySegmentCacheStore<
|
|
|
244
277
|
response: Response,
|
|
245
278
|
ttl: number,
|
|
246
279
|
swr?: number,
|
|
280
|
+
tags?: string[],
|
|
247
281
|
): Promise<void> {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
headers.
|
|
252
|
-
|
|
282
|
+
try {
|
|
283
|
+
const body = await response.clone().arrayBuffer();
|
|
284
|
+
const headers: [string, string][] = [];
|
|
285
|
+
response.headers.forEach((value, name) => {
|
|
286
|
+
if (isPerClientSignalHeader(name)) return;
|
|
287
|
+
headers.push([name, value]);
|
|
288
|
+
});
|
|
253
289
|
|
|
254
|
-
|
|
255
|
-
|
|
290
|
+
const swrWindow = resolveSwrWindow(swr, this.defaults);
|
|
291
|
+
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
256
292
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
293
|
+
const prefixedKey = `res:${key}`;
|
|
294
|
+
this.unregisterTags(prefixedKey);
|
|
295
|
+
this.responseCache.set(key, {
|
|
296
|
+
body,
|
|
297
|
+
status: response.status,
|
|
298
|
+
headers,
|
|
299
|
+
expiresAt,
|
|
300
|
+
staleAt,
|
|
301
|
+
});
|
|
302
|
+
if (tags && tags.length > 0) {
|
|
303
|
+
this.registerTags(tags, prefixedKey);
|
|
304
|
+
}
|
|
305
|
+
} catch (error) {
|
|
306
|
+
reportCacheError(
|
|
307
|
+
error,
|
|
308
|
+
"cache-write",
|
|
309
|
+
"[MemorySegmentCacheStore] putResponse",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
264
312
|
}
|
|
265
313
|
|
|
266
314
|
async getItem(key: string): Promise<CacheItemResult | null> {
|
|
@@ -269,6 +317,7 @@ export class MemorySegmentCacheStore<
|
|
|
269
317
|
|
|
270
318
|
const now = Date.now();
|
|
271
319
|
if (now > cached.expiresAt) {
|
|
320
|
+
this.unregisterTags(`item:${key}`);
|
|
272
321
|
this.itemCache.delete(key);
|
|
273
322
|
return null;
|
|
274
323
|
}
|
|
@@ -278,6 +327,7 @@ export class MemorySegmentCacheStore<
|
|
|
278
327
|
value: cached.value,
|
|
279
328
|
handles: cached.handles,
|
|
280
329
|
shouldRevalidate: isStale,
|
|
330
|
+
tags: cached.tags,
|
|
281
331
|
};
|
|
282
332
|
}
|
|
283
333
|
|
|
@@ -289,18 +339,77 @@ export class MemorySegmentCacheStore<
|
|
|
289
339
|
const ttl = resolveTtl(options?.ttl, this.defaults, DEFAULT_FUNCTION_TTL);
|
|
290
340
|
const swrWindow = resolveSwrWindow(options?.swr, this.defaults);
|
|
291
341
|
const { staleAt, expiresAt } = computeExpiration(ttl, swrWindow);
|
|
342
|
+
const prefixedKey = `item:${key}`;
|
|
343
|
+
this.unregisterTags(prefixedKey);
|
|
292
344
|
this.itemCache.set(key, {
|
|
293
345
|
value,
|
|
294
346
|
handles: options?.handles,
|
|
295
347
|
expiresAt,
|
|
296
348
|
staleAt,
|
|
349
|
+
tags: options?.tags,
|
|
297
350
|
});
|
|
351
|
+
if (options?.tags && options.tags.length > 0) {
|
|
352
|
+
this.registerTags(options.tags, prefixedKey);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async invalidateTags(tags: string[]): Promise<void> {
|
|
357
|
+
for (const tag of tags) {
|
|
358
|
+
const keys = this.tagIndex.get(tag);
|
|
359
|
+
if (!keys || keys.size === 0) continue;
|
|
360
|
+
|
|
361
|
+
const prefixedKeys = [...keys];
|
|
362
|
+
|
|
363
|
+
for (const prefixedKey of prefixedKeys) {
|
|
364
|
+
const colonIdx = prefixedKey.indexOf(":");
|
|
365
|
+
const prefix = prefixedKey.slice(0, colonIdx);
|
|
366
|
+
const rawKey = prefixedKey.slice(colonIdx + 1);
|
|
367
|
+
|
|
368
|
+
if (prefix === "seg") {
|
|
369
|
+
this.cache.delete(rawKey);
|
|
370
|
+
} else if (prefix === "res") {
|
|
371
|
+
this.responseCache.delete(rawKey);
|
|
372
|
+
} else if (prefix === "item") {
|
|
373
|
+
this.itemCache.delete(rawKey);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
this.unregisterTags(prefixedKey);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private registerTags(tags: string[], prefixedKey: string): void {
|
|
382
|
+
let tagSet = this.keyTags.get(prefixedKey);
|
|
383
|
+
if (!tagSet) {
|
|
384
|
+
tagSet = new Set();
|
|
385
|
+
this.keyTags.set(prefixedKey, tagSet);
|
|
386
|
+
}
|
|
387
|
+
for (const tag of tags) {
|
|
388
|
+
tagSet.add(tag);
|
|
389
|
+
let keys = this.tagIndex.get(tag);
|
|
390
|
+
if (!keys) {
|
|
391
|
+
keys = new Set();
|
|
392
|
+
this.tagIndex.set(tag, keys);
|
|
393
|
+
}
|
|
394
|
+
keys.add(prefixedKey);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private unregisterTags(prefixedKey: string): void {
|
|
399
|
+
const tagSet = this.keyTags.get(prefixedKey);
|
|
400
|
+
if (!tagSet) return;
|
|
401
|
+
for (const tag of tagSet) {
|
|
402
|
+
const keys = this.tagIndex.get(tag);
|
|
403
|
+
if (keys) {
|
|
404
|
+
keys.delete(prefixedKey);
|
|
405
|
+
if (keys.size === 0) {
|
|
406
|
+
this.tagIndex.delete(tag);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
this.keyTags.delete(prefixedKey);
|
|
298
411
|
}
|
|
299
412
|
|
|
300
|
-
/**
|
|
301
|
-
* Get cache statistics for debugging purposes.
|
|
302
|
-
* @internal
|
|
303
|
-
*/
|
|
304
413
|
getStats(): { size: number; keys: string[] } {
|
|
305
414
|
return {
|
|
306
415
|
size: this.cache.size,
|
|
@@ -308,21 +417,11 @@ export class MemorySegmentCacheStore<
|
|
|
308
417
|
};
|
|
309
418
|
}
|
|
310
419
|
|
|
311
|
-
/**
|
|
312
|
-
* Reset the global cache registry.
|
|
313
|
-
* Useful for test isolation - call this in beforeEach to ensure
|
|
314
|
-
* tests don't share cache state via globalThis.
|
|
315
|
-
*
|
|
316
|
-
* @example
|
|
317
|
-
* ```typescript
|
|
318
|
-
* beforeEach(() => {
|
|
319
|
-
* MemorySegmentCacheStore.resetGlobalCache();
|
|
320
|
-
* });
|
|
321
|
-
* ```
|
|
322
|
-
*/
|
|
323
420
|
static resetGlobalCache(): void {
|
|
324
421
|
delete (globalThis as any)[CACHE_REGISTRY_KEY];
|
|
325
422
|
delete (globalThis as any)[RESPONSE_CACHE_REGISTRY_KEY];
|
|
326
423
|
delete (globalThis as any)[ITEM_CACHE_REGISTRY_KEY];
|
|
424
|
+
delete (globalThis as any)[TAG_INDEX_REGISTRY_KEY];
|
|
425
|
+
delete (globalThis as any)[KEY_TAGS_REGISTRY_KEY];
|
|
327
426
|
}
|
|
328
427
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cache
|
|
2
|
+
* Cache profile resolution.
|
|
3
3
|
*
|
|
4
|
-
* Named cache profiles for "use cache" directive
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Named cache profiles for the "use cache" directive define TTL, SWR, and
|
|
5
|
+
* optional default tags. createRouter() resolves the user's profiles once via
|
|
6
|
+
* resolveCacheProfiles() and threads the resulting map onto each request
|
|
7
|
+
* context; the "use cache: <profile>" runtime path reads it from there
|
|
8
|
+
* (request-scoped) — there is no global registry.
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
export interface CacheProfile {
|
|
@@ -17,10 +19,6 @@ export interface CacheProfile {
|
|
|
17
19
|
|
|
18
20
|
const DEFAULT_PROFILE: CacheProfile = { ttl: 900, swr: 1800 };
|
|
19
21
|
|
|
20
|
-
let _profiles: Record<string, CacheProfile> = {
|
|
21
|
-
default: DEFAULT_PROFILE,
|
|
22
|
-
};
|
|
23
|
-
|
|
24
22
|
const PROFILE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
25
23
|
|
|
26
24
|
/**
|
|
@@ -49,25 +47,3 @@ export function resolveCacheProfiles(
|
|
|
49
47
|
}
|
|
50
48
|
return merged;
|
|
51
49
|
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Set all cache profiles in the global registry.
|
|
55
|
-
* Called by createRouter() at startup for DSL-time resolution
|
|
56
|
-
* (cache("profileName") reads from this during route definition).
|
|
57
|
-
*
|
|
58
|
-
* WARNING: This is global mutable state. It exists only for DSL-time
|
|
59
|
-
* reads. Runtime resolution (registerCachedFunction) uses request-scoped
|
|
60
|
-
* profiles and does NOT read from this registry.
|
|
61
|
-
*/
|
|
62
|
-
export function setCacheProfiles(profiles: Record<string, CacheProfile>): void {
|
|
63
|
-
_profiles = resolveCacheProfiles(profiles);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Get a cache profile by name from the global registry.
|
|
68
|
-
* Used only at DSL-time (cache("profileName") inside urls() evaluation).
|
|
69
|
-
* Runtime code uses request-scoped profiles instead.
|
|
70
|
-
*/
|
|
71
|
-
export function getCacheProfile(name: string): CacheProfile | undefined {
|
|
72
|
-
return _profiles[name];
|
|
73
|
-
}
|