Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 47 additions & 4 deletions packages/vinext/src/cache/cache-adapters-virtual.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,31 @@
* guarantee is present. URL-only caches retain the contextual `_rsc` digest.
*/
responseVary?: "verbatim";
/**
* Rewrites and other request routing run before the shared response stage,
* and the resolved response-stage invocation participates in cache identity.
*
* Warm planners may therefore request rewrite source paths: conditional and
* default route resolutions cannot reuse one another's cached response.
*/
requestRouting?: "uncached-stage";
/**
* Cacheable App Page responses require a build-bound probe manifest before
* the adapter may emit public shared-cache policy. The adapter's deployment
* integration is responsible for carrying that manifest into the runtime
* that serves the corresponding application build.
*/
routeCacheability?: "probe-manifest";
/**
* Response headers whose values can opt a response into or out of the
* adapter's shared cache, in addition to the framework-owned Cache-Control
* header.
*
* Prerender discovery uses these names to avoid collapsing a dynamic route
* pattern when next.config assigns different cache policy to its concrete
* pathnames.
*/
responsePolicyHeaderNames?: readonly string[];
};

export type CacheAdapterBuildOutput = {
Expand Down Expand Up @@ -79,6 +97,10 @@
return cache?.cdn?.capabilities?.responseVary === "verbatim";
}

export function hasUncachedRequestRouting(cache?: VinextCacheConfig | null): boolean {
return cache?.cdn?.capabilities?.requestRouting === "uncached-stage";
}

export function hasBuildIdentityResponseHeader(cache?: VinextCacheConfig | null): boolean {
return cache?.cdn?.capabilities?.buildIdentity === "response-header";
}
Expand All @@ -87,6 +109,20 @@
return cache?.cdn?.capabilities?.routeCacheability === "probe-manifest";
}

/** Lowercase response-policy names owned by core and the configured adapter. */
export function getConfiguredCdnResponsePolicyHeaderNames(
cache?: VinextCacheConfig | null,
): readonly string[] {
return [
...new Set([
"cache-control",
...(cache?.cdn?.capabilities?.responsePolicyHeaderNames ?? [])
.map((name) => name.trim().toLowerCase())
.filter(Boolean),
]),
];
}

/**
* The `cache` option of the vinext() plugin: declaratively register cache
* handlers instead of calling `setDataCacheHandler()` / `setCdnCacheAdapter()`
Expand All @@ -101,6 +137,8 @@

/** Public virtual module id imported by the server entries. */
export const VIRTUAL_CACHE_ADAPTERS = "virtual:vinext-cache-adapters";
/** Request-stage module that cannot retain the data-cache adapter graph. */
export const VIRTUAL_CDN_CACHE_ADAPTER = "virtual:vinext-cdn-cache-adapter";

// Custom metadata key attached to vinext's config plugin so deploy commands can
// inspect the normalized cache descriptors after loading the user's Vite config.
Expand Down Expand Up @@ -180,11 +218,11 @@

if (data?.adapter) {
lines.push(`import __vinextDataAdapterFactory from ${JSON.stringify(data.adapter)};`);
lines.push(`import { setDataCacheHandler } from "vinext/shims/cache-handler";`);
lines.push(`import { registerDataCacheHandler } from "vinext/shims/cache-handler";`);
}
if (cdn?.adapter) {
lines.push(`import __vinextCdnAdapterFactory from ${JSON.stringify(cdn.adapter)};`);
lines.push(`import { setCdnCacheAdapter } from "vinext/shims/cdn-cache";`);
lines.push(`import { registerCdnCacheAdapter } from "vinext/shims/cdn-cache-state";`);
}

lines.push(
Expand All @@ -210,10 +248,10 @@
if (data?.adapter) {
lines.push(
" try {",
` setDataCacheHandler(__vinextDataAdapterFactory({ env, options: ${inlineOptions(
` registerDataCacheHandler(() => __vinextDataAdapterFactory({ env, options: ${inlineOptions(
data.adapter,
data.options,
)} }));`,

Check warning

Code scanning / CodeQL

Improper code sanitization Medium

Code construction depends on an
improperly sanitized value
.
" } catch (error) {",
' console.warn("[vinext] failed to initialize the configured data cache adapter; ' +
'using the default handler.\\n" + __vinextFormatAdapterError(error));',
Expand All @@ -223,10 +261,10 @@
if (cdn?.adapter) {
lines.push(
" try {",
` setCdnCacheAdapter(__vinextCdnAdapterFactory({ env, options: ${inlineOptions(
` registerCdnCacheAdapter(() => __vinextCdnAdapterFactory({ env, options: ${inlineOptions(
cdn.adapter,
cdn.options,
)} }));`,

Check warning

Code scanning / CodeQL

Improper code sanitization Medium

Code construction depends on an
improperly sanitized value
.
" } catch (error) {",
' console.warn("[vinext] failed to initialize the configured CDN cache adapter; ' +
'using the default adapter.\\n" + __vinextFormatAdapterError(error));',
Expand All @@ -237,3 +275,8 @@

return lines.join("\n");
}

/** Generate request-stage registration without importing a configured data adapter. */
export function generateCdnCacheAdapterModule(cache?: VinextCacheConfig): string {
return `${generateCacheAdaptersModule(cache?.cdn ? { cdn: cache.cdn } : undefined)}export const hasConfiguredDataCache = ${Boolean(cache?.data?.adapter)};\n`;
}
5 changes: 5 additions & 0 deletions packages/vinext/src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,11 @@ declare module "virtual:vinext-cache-adapters" {
export function registerConfiguredCacheAdapters(env?: Record<string, unknown>): void;
}

declare module "virtual:vinext-cdn-cache-adapter" {
export const hasConfiguredDataCache: boolean;
export function registerConfiguredCacheAdapters(env?: Record<string, unknown>): void;
}

declare module "virtual:vinext-pages-client-assets" {
import type { PagesClientAssets } from "vinext/server/pages-client-assets";
const assets: PagesClientAssets;
Expand Down
86 changes: 86 additions & 0 deletions packages/vinext/src/shims/cache-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,55 @@ export class MemoryCacheHandler implements CacheHandler {
}

const HANDLER_KEY = Symbol.for("vinext.cacheHandler");
const CONFIGURED_HANDLER_KEY = Symbol.for("vinext.configuredCacheHandler");
const EXPLICIT_HANDLER_KEY = Symbol.for("vinext.explicitCacheHandler");
const LAZY_HANDLER_KEY = Symbol.for("vinext.lazyCacheHandler");
const globalHandlers = globalThis as unknown as Record<PropertyKey, CacheHandler>;

class LazyDataCacheHandler implements CacheHandler {
private resolved: Promise<CacheHandler> | undefined;
private loadedHandler: CacheHandler | undefined;

constructor(private readonly load: () => Promise<void>) {}

private resolve(): Promise<CacheHandler> {
return (this.resolved ??= this.load().then(() => {
const registered = globalHandlers[HANDLER_KEY];
if (registered && registered !== this) {
this.loadedHandler = registered;
return registered;
}

const fallback = new MemoryCacheHandler();
globalHandlers[HANDLER_KEY] = fallback;
globalHandlers[CONFIGURED_HANDLER_KEY] = fallback;
delete globalHandlers[LAZY_HANDLER_KEY];
this.loadedHandler = fallback;
return fallback;
}));
}

async get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null> {
return (await this.resolve()).get(key, ctx);
}

async set(
key: string,
data: IncrementalCacheValue | null,
ctx?: Record<string, unknown>,
): Promise<void> {
await (await this.resolve()).set(key, data, ctx);
}

async revalidateTag(tags: string | string[], durations?: { expire?: number }): Promise<void> {
await (await this.resolve()).revalidateTag(tags, durations);
}

resetRequestCache(): void {
this.loadedHandler?.resetRequestCache?.();
}
}

function getActiveHandler(): CacheHandler {
return globalHandlers[HANDLER_KEY] ?? (globalHandlers[HANDLER_KEY] = new MemoryCacheHandler());
}
Expand All @@ -387,6 +434,45 @@ export function configureMemoryCacheHandler(options?: MemoryCacheHandlerOptions)

export function setDataCacheHandler(handler: CacheHandler): void {
globalHandlers[HANDLER_KEY] = handler;
globalHandlers[EXPLICIT_HANDLER_KEY] = handler;
delete globalHandlers[LAZY_HANDLER_KEY];
}

/**
* Lazily keep the first declaratively configured handler shared across
* duplicated stage modules. Imperative setters remain able to replace it.
*/
export function registerDataCacheHandler(factory: () => CacheHandler): void {
if (
globalHandlers[EXPLICIT_HANDLER_KEY] !== undefined ||
globalHandlers[CONFIGURED_HANDLER_KEY] !== undefined
) {
return;
}
const handler = factory();
if (
globalHandlers[EXPLICIT_HANDLER_KEY] !== undefined ||
globalHandlers[CONFIGURED_HANDLER_KEY] !== undefined
) {
return;
}
globalHandlers[HANDLER_KEY] = handler;
globalHandlers[CONFIGURED_HANDLER_KEY] = handler;
delete globalHandlers[LAZY_HANDLER_KEY];
}

/** Register a lightweight proxy that imports the configured handler on first use. */
export function registerLazyDataCacheHandler(load: () => Promise<void>): void {
if (
globalHandlers[EXPLICIT_HANDLER_KEY] !== undefined ||
globalHandlers[CONFIGURED_HANDLER_KEY] !== undefined ||
globalHandlers[LAZY_HANDLER_KEY] !== undefined
) {
return;
}
const handler = new LazyDataCacheHandler(load);
globalHandlers[HANDLER_KEY] = handler;
globalHandlers[LAZY_HANDLER_KEY] = handler;
}

export function getDataCacheHandler(): CacheHandler {
Expand Down
3 changes: 1 addition & 2 deletions packages/vinext/src/shims/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@
* vinext({ cache: { data: kvDataAdapter({ binding: 'VINEXT_KV_CACHE' }) } })
*
* The imperative `setCacheHandler` / `setDataCacheHandler` setters are
* deprecated for consumers and retained only as the internal registration
* target used by the generated cache-adapter module.
* deprecated for consumers and retained for backwards compatibility.
*/

import {
Expand Down
28 changes: 28 additions & 0 deletions packages/vinext/src/shims/cdn-cache-state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { CdnCacheAdapter } from "./cdn-cache.js";

const CDN_CACHE_ADAPTER_KEY = Symbol.for("vinext.cdnCacheAdapter");
const globals = globalThis as unknown as Record<PropertyKey, unknown>;

/** Register an adapter without loading the origin cache implementation. */
export function setCdnCacheAdapter(adapter: CdnCacheAdapter): void {
globals[CDN_CACHE_ADAPTER_KEY] = adapter;
}

/**
* Lazily keep the first declaratively configured adapter shared across
* duplicated stage modules. Failed factories remain retryable so a later
* entrypoint with the required runtime bindings can register successfully.
*/
export function registerCdnCacheAdapter(factory: () => CdnCacheAdapter): void {
if (globals[CDN_CACHE_ADAPTER_KEY] !== undefined) return;
const adapter = factory();
// Preserve an imperative adapter installed re-entrantly by the factory.
if (globals[CDN_CACHE_ADAPTER_KEY] === undefined) {
globals[CDN_CACHE_ADAPTER_KEY] = adapter;
}
}

/** Read only an explicitly registered adapter; defaults belong to cdn-cache. */
export function getExplicitCdnCacheAdapter(): CdnCacheAdapter | null {
return (globals[CDN_CACHE_ADAPTER_KEY] as CdnCacheAdapter | undefined) ?? null;
}
27 changes: 11 additions & 16 deletions packages/vinext/src/shims/cdn-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,9 @@
* pre-split implementation.
*/

import {
getDataCacheHandler,
type CacheHandlerValue,
type IncrementalCacheValue,
} from "./cache-handler.js";
import type { CacheHandlerValue, IncrementalCacheValue } from "./cache-handler.js";
import { getExplicitCdnCacheAdapter } from "./cdn-cache-state.js";
export { setCdnCacheAdapter } from "./cdn-cache-state.js";

/** A map of response header name -> value the adapter wants applied or removed. */
export type CdnResponseHeaders = Record<string, string | null>;
Expand Down Expand Up @@ -106,6 +104,9 @@ export type CdnCacheAdapter = {
*/
readonly responseVary?: "verbatim";

/** Provider-specific response headers whose values control shared caching. */
readonly responsePolicyHeaderNames?: readonly string[];

/**
* Fresh App Page responses must reach clean EOF before this adapter may emit
* shared-cache headers. Used by edge adapters whose cache sits in front of
Expand Down Expand Up @@ -203,6 +204,7 @@ export class DefaultCdnCacheAdapter implements CdnCacheAdapter {
readonly ownsBackgroundRevalidation = true;

async get(key: string, ctx?: Record<string, unknown>): Promise<CacheHandlerValue | null> {
const { getDataCacheHandler } = await import("./cache-handler.js");
return getDataCacheHandler().get(key, ctx);
}

Expand All @@ -211,6 +213,7 @@ export class DefaultCdnCacheAdapter implements CdnCacheAdapter {
data: IncrementalCacheValue | null,
ctx?: Record<string, unknown>,
): Promise<void> {
const { getDataCacheHandler } = await import("./cache-handler.js");
await getDataCacheHandler().set(key, data, ctx);
}

Expand Down Expand Up @@ -241,9 +244,6 @@ export class DefaultCdnCacheAdapter implements CdnCacheAdapter {
// 2. Otherwise, the origin-managed DefaultCdnCacheAdapter.
// ---------------------------------------------------------------------------

const _CDN_KEY = Symbol.for("vinext.cdnCacheAdapter");
const _gCdn = globalThis as unknown as Record<PropertyKey, unknown>;

let _defaultAdapter: DefaultCdnCacheAdapter | null = null;

/**
Expand All @@ -265,20 +265,15 @@ let _defaultAdapter: DefaultCdnCacheAdapter | null = null;
* ```
*
* The plugin registers the adapter across every runtime/router entry, so you
* don't have to call this from a worker entry. This setter remains as the
* internal registration target and for backwards compatibility, but is not the
* recommended consumer API.
* don't have to call this from a worker entry. This setter remains for
* backwards compatibility, but is not the recommended consumer API.
*/
export function setCdnCacheAdapter(adapter: CdnCacheAdapter): void {
_gCdn[_CDN_KEY] = adapter;
}

/**
* Get the active CDN cache adapter. An explicitly configured adapter wins;
* otherwise the origin-managed {@link DefaultCdnCacheAdapter} is used.
*/
export function getCdnCacheAdapter(): CdnCacheAdapter {
const active = _gCdn[_CDN_KEY] as CdnCacheAdapter | undefined;
const active = getExplicitCdnCacheAdapter();
if (active) return active;

return (_defaultAdapter ??= new DefaultCdnCacheAdapter());
Expand Down
Loading
Loading