Skip to content
Merged
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
19 changes: 3 additions & 16 deletions packages/vinext/src/cloudflare/kv-cache-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/

import type { CacheHandler, CacheHandlerValue, IncrementalCacheValue } from "../shims/cache.js";
import { getRequestExecutionContext } from "../shims/request-context.js";
import { getRequestExecutionContext, type ExecutionContextLike } from "../shims/request-context.js";

// Cloudflare KV namespace interface (matches Workers types)
interface KVNamespace {
Expand All @@ -48,19 +48,6 @@ interface KVNamespace {
}>;
}

/**
* Minimal ExecutionContext interface for Cloudflare Workers.
* Background KV operations (cleanup deletes, cache writes) are registered
* with ctx.waitUntil() so they are not killed when the Response is returned.
*
* The preferred way to supply ctx is via runWithExecutionContext() in the
* worker entry (see vinext/shims/request-context). The constructor option
* is kept as a fallback for callers that set it explicitly.
*/
interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void;
}

/** Shape stored in KV for each cache entry. */
interface KVCacheEntry {
value: IncrementalCacheValue | null;
Expand Down Expand Up @@ -95,12 +82,12 @@ function validateTag(tag: string): string | null {
export class KVCacheHandler implements CacheHandler {
private kv: KVNamespace;
private prefix: string;
private ctx: ExecutionContext | undefined;
private ctx: ExecutionContextLike | undefined;
private ttlSeconds: number;

constructor(
kvNamespace: KVNamespace,
options?: { appPrefix?: string; ctx?: ExecutionContext; ttlSeconds?: number },
options?: { appPrefix?: string; ctx?: ExecutionContextLike; ttlSeconds?: number },
) {
this.kv = kvNamespace;
this.prefix = options?.appPrefix ? `${options.appPrefix}:` : "";
Expand Down
12 changes: 6 additions & 6 deletions packages/vinext/src/entries/app-rsc-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@ export default async function handler(request, ctx) {
// _handleRequest which fills in .headers and .status;
// avoids module-level variables that race on Workers.
const _mwCtx = { headers: null, status: null };
const response = await _handleRequest(request, __reqCtx, _mwCtx, ctx);
const response = await _handleRequest(request, __reqCtx, _mwCtx);
// Apply custom headers from next.config.js to non-redirect responses.
// Skip redirects (3xx) because Response.redirect() creates immutable headers,
// and Next.js doesn't apply custom headers to redirects anyway.
Expand Down Expand Up @@ -1440,7 +1440,7 @@ export default async function handler(request, ctx) {
return ctx ? _runWithExecutionContext(ctx, _run) : _run();
}

async function _handleRequest(request, __reqCtx, _mwCtx, ctx) {
async function _handleRequest(request, __reqCtx, _mwCtx) {
const __reqStart = process.env.NODE_ENV !== "production" ? performance.now() : 0;
let __compileEnd;
let __renderEnd;
Expand Down Expand Up @@ -2643,7 +2643,7 @@ async function _handleRequest(request, __reqCtx, _mwCtx, ctx) {
console.error("[vinext] ISR RSC cache write error:", __rscWriteErr);
}
})();
if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(__rscWritePromise);
_getRequestExecutionContext()?.waitUntil(__rscWritePromise);
}
return new Response(__rscForResponse, { status: _mwCtx.status || 200, headers: responseHeaders });
}
Expand Down Expand Up @@ -2863,9 +2863,9 @@ async function _handleRequest(request, __reqCtx, _mwCtx, ctx) {
console.error("[vinext] ISR cache write error:", __cacheErr);
}
})();
// Register with ExecutionContext so the Workers runtime keeps the isolate
// alive until the cache write finishes, even after the response is sent.
if (ctx && typeof ctx.waitUntil === "function") ctx.waitUntil(__cachePromise);
// Register with ExecutionContext (from ALS) so the Workers runtime keeps
// the isolate alive until the cache write finishes, even after the response is sent.
_getRequestExecutionContext()?.waitUntil(__cachePromise);
return new Response(__streamForClient, { status: __isrResponseProd.status, headers: __isrResponseProd.headers });
}
return __isrResponseProd;
Expand Down
30 changes: 8 additions & 22 deletions packages/vinext/src/server/isr-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ import {
import { fnv1a64 } from "../utils/hash.js";
import { getRequestExecutionContext } from "../shims/request-context.js";

/**
* Minimal ExecutionContext interface for Cloudflare Workers.
* Matches the Workers runtime type; also works with the stub used in tests.
*/
export interface ExecutionContext {
waitUntil(promise: Promise<unknown>): void;
}

export interface ISRCacheEntry {
value: CacheHandlerValue;
isStale: boolean;
Expand Down Expand Up @@ -82,16 +74,11 @@ const pendingRegenerations = new Map<string, Promise<void>>();
* If a regeneration for this key is already in progress, this is a no-op.
* The renderFn should produce the new cache value and call isrSet internally.
*
* On Cloudflare Workers, pass the `ExecutionContext` as `ctx` so the
* regeneration promise is registered with `ctx.waitUntil()`. Without this,
* the Workers runtime terminates the isolate as soon as the Response is
* returned, silently killing any pending background work.
* On Cloudflare Workers the regeneration promise is registered with
* `ctx.waitUntil()` via the ALS-backed ExecutionContext, keeping the isolate
* alive until the regeneration completes even after the Response is returned.
*/
export function triggerBackgroundRegeneration(
key: string,
renderFn: () => Promise<void>,
ctx?: ExecutionContext,
): void {
export function triggerBackgroundRegeneration(key: string, renderFn: () => Promise<void>): void {
if (pendingRegenerations.has(key)) return;

const promise = renderFn()
Expand All @@ -104,11 +91,10 @@ export function triggerBackgroundRegeneration(

pendingRegenerations.set(key, promise);

// Register with the Workers ExecutionContext so the runtime keeps the
// isolate alive until the regeneration completes, even after the Response
// has already been sent to the client.
const execCtx = ctx ?? getRequestExecutionContext();
execCtx?.waitUntil(promise);
// Register with the Workers ExecutionContext (retrieved from ALS) so the
// runtime keeps the isolate alive until the regeneration completes, even
// after the Response has already been sent to the client.
getRequestExecutionContext()?.waitUntil(promise);
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading