Skip to content

Commit 5c63dfd

Browse files
committed
initial implementation
1 parent 20f0095 commit 5c63dfd

File tree

7 files changed

+140
-4
lines changed

7 files changed

+140
-4
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
22
import d1TagCache from "@opennextjs/cloudflare/d1-tag-cache";
33
import kvIncrementalCache from "@opennextjs/cloudflare/kv-cache";
4-
import memoryQueue from "@opennextjs/cloudflare/memory-queue";
4+
// import memoryQueue from "@opennextjs/cloudflare/memory-queue";
5+
import doQueue from "@opennextjs/cloudflare/durable-queue";
56

67
export default defineCloudflareConfig({
78
incrementalCache: kvIncrementalCache,
89
tagCache: d1TagCache,
9-
queue: memoryQueue,
10+
queue: doQueue,
1011
});

examples/e2e/app-router/wrangler.jsonc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,22 @@
88
"directory": ".open-next/assets",
99
"binding": "ASSETS"
1010
},
11+
"durable_objects": {
12+
"bindings": [
13+
{
14+
"name": "NEXT_CACHE_REVALIDATION_DURABLE_OBJECT",
15+
"class_name": "DurableObjectQueueHandler"
16+
}
17+
]
18+
},
19+
"migrations": [
20+
{
21+
"tag": "v1",
22+
"new_classes": [
23+
"DurableObjectQueueHandler"
24+
]
25+
}
26+
],
1127
"kv_namespaces": [
1228
{
1329
"binding": "NEXT_CACHE_WORKERS_KV",

packages/cloudflare/src/api/cloudflare-context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
import type { Context, RunningCodeOptions } from "node:vm";
22

3+
import type { DurableObjectQueueHandler } from "./durable-objects/queue";
4+
35
declare global {
46
interface CloudflareEnv {
57
NEXT_CACHE_WORKERS_KV?: KVNamespace;
68
NEXT_CACHE_D1?: D1Database;
79
NEXT_CACHE_D1_TAGS_TABLE?: string;
810
NEXT_CACHE_D1_REVALIDATIONS_TABLE?: string;
911
NEXT_CACHE_REVALIDATION_WORKER?: Service;
12+
NEXT_CACHE_REVALIDATION_DURABLE_OBJECT?: DurableObjectNamespace<DurableObjectQueueHandler>;
1013
ASSETS?: Fetcher;
1114
}
1215
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { error } from "@opennextjs/aws/adapters/logger.js";
2+
import type { QueueMessage } from "@opennextjs/aws/types/overrides";
3+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
4+
import { DurableObject } from "cloudflare:workers";
5+
6+
const MAX_REVALIDATION_BY_DURABLE_OBJECT = 5;
7+
const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
8+
9+
interface ExtendedQueueMessage extends QueueMessage {
10+
previewModeId: string;
11+
}
12+
13+
export class DurableObjectQueueHandler extends DurableObject<CloudflareEnv> {
14+
// Ongoing revalidations are deduped by the deduplication id
15+
// Since this is running in waitUntil, we expect the durable object state to persist this during the duration of the revalidation
16+
// TODO: handle incremental cache with only eventual consistency (i.e. KV or R2/D1 with the optional cache layer on top)
17+
ongoingRevalidations = new Map<string, Promise<void>>();
18+
19+
service: NonNullable<CloudflareEnv["NEXT_CACHE_REVALIDATION_WORKER"]>;
20+
21+
// TODO: allow this to be configurable
22+
maxRevalidations = MAX_REVALIDATION_BY_DURABLE_OBJECT;
23+
24+
constructor(ctx: DurableObjectState, env: CloudflareEnv) {
25+
super(ctx, env);
26+
const service = env.NEXT_CACHE_REVALIDATION_WORKER;
27+
// If there is no service binding, we throw an error because we can't revalidate without it
28+
if (!service) throw new IgnorableError("No service binding for cache revalidation worker");
29+
this.service = service;
30+
31+
}
32+
33+
async revalidate(msg: ExtendedQueueMessage) {
34+
// If there is already an ongoing revalidation, we don't need to revalidate again
35+
if (this.ongoingRevalidations.has(msg.MessageDeduplicationId)) return;
36+
37+
if(this.ongoingRevalidations.size >= MAX_REVALIDATION_BY_DURABLE_OBJECT) {
38+
const ongoingRevalidations = this.ongoingRevalidations.values()
39+
await this.ctx.blockConcurrencyWhile(() => Promise.race(ongoingRevalidations));
40+
}
41+
42+
const revalidationPromise = this.executeRevalidation(msg);
43+
44+
// We store the promise to dedupe the revalidation
45+
this.ongoingRevalidations.set(
46+
msg.MessageDeduplicationId,
47+
revalidationPromise
48+
);
49+
50+
this.ctx.waitUntil(revalidationPromise);
51+
}
52+
53+
private async executeRevalidation({MessageBody: {host, url}, MessageDeduplicationId, previewModeId}: ExtendedQueueMessage) {
54+
try {
55+
const protocol = host.includes("localhost") ? "http" : "https";
56+
57+
//TODO: handle the different types of errors that can occur during the fetch (i.e. timeout, network error, etc)
58+
await this.service.fetch(`${protocol}://${host}${url}`, {
59+
method: "HEAD",
60+
headers: {
61+
"x-prerender-revalidate": previewModeId,
62+
"x-isr": "1",
63+
},
64+
signal: AbortSignal.timeout(DEFAULT_REVALIDATION_TIMEOUT_MS)
65+
})
66+
} catch (e) {
67+
error(e);
68+
} finally {
69+
this.ongoingRevalidations.delete(MessageDeduplicationId);
70+
}
71+
}
72+
73+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Queue, QueueMessage } from "@opennextjs/aws/types/overrides";
2+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
3+
4+
import { getCloudflareContext } from "./cloudflare-context";
5+
6+
7+
8+
export default {
9+
name: "durable-queue",
10+
send: async (msg: QueueMessage) => {
11+
const durableObject = getCloudflareContext().env.NEXT_CACHE_REVALIDATION_DURABLE_OBJECT;
12+
if (!durableObject) throw new IgnorableError("No durable object binding for cache revalidation");
13+
14+
const id = durableObject.idFromName(msg.MessageGroupId);
15+
const stub = durableObject.get(id);
16+
const previewModeId = process.env.__NEXT_PREVIEW_MODE_ID!;
17+
await stub.revalidate({
18+
...msg,
19+
previewModeId,
20+
});
21+
}
22+
} satisfies Queue;

packages/cloudflare/src/cli/build/bundle-server.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,30 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
4949
patches.copyPackageCliFiles(packageDistDir, buildOpts);
5050

5151
const { appPath, outputDir, monorepoRoot } = buildOpts;
52-
const serverFiles = path.join(
52+
const baseManifestPath = path.join(
5353
outputDir,
5454
"server-functions/default",
5555
getPackagePath(buildOpts),
56-
".next/required-server-files.json"
56+
".next"
57+
)
58+
const serverFiles = path.join(
59+
baseManifestPath,
60+
"required-server-files.json"
5761
);
5862
const nextConfig = JSON.parse(fs.readFileSync(serverFiles, "utf-8")).config;
5963

64+
// TODO: This is a temporary solution to get the previewModeId from the prerender-manifest.json
65+
// We should find a better way to get this value, probably directly provided from aws
66+
// probably in an env variable exactly as for BUILD_ID
67+
const prerenderManifest = path.join(
68+
baseManifestPath,
69+
"prerender-manifest.json"
70+
);
71+
const prerenderManifestContent = fs.readFileSync(prerenderManifest, "utf-8");
72+
const prerenderManifestJson = JSON.parse(prerenderManifestContent);
73+
const previewModeId = prerenderManifestJson.preview.previewModeId;
74+
75+
6076
console.log(`\x1b[35m⚙️ Bundling the OpenNext server...\n\x1b[0m`);
6177

6278
await patchWebpackRuntime(buildOpts);
@@ -144,6 +160,8 @@ export async function bundleServer(buildOpts: BuildOptions): Promise<void> {
144160
"process.env.TURBOPACK": "false",
145161
// This define should be safe to use for Next 14.2+, earlier versions (13.5 and less) will cause trouble
146162
"process.env.__NEXT_EXPERIMENTAL_REACT": `${needsExperimentalReact(nextConfig)}`,
163+
// Used for the durable object queue handler
164+
"process.env.__NEXT_PREVIEW_MODE_ID": `"${previewModeId}"`,
147165
},
148166
platform: "node",
149167
banner: {

packages/cloudflare/src/cli/templates/worker.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), {
1717
},
1818
});
1919

20+
//@ts-expect-error: Will be resolved by wrangler build
21+
export { DurableObjectQueueHandler } from "@opennextjs/cloudflare/durable-objects/queue";
22+
2023
// Populate process.env on the first request
2124
let processEnvPopulated = false;
2225

0 commit comments

Comments
 (0)