Skip to content

Commit dfd1e33

Browse files
committed
DO cache purge
1 parent fd8c4e7 commit dfd1e33

File tree

8 files changed

+143
-13
lines changed

8 files changed

+143
-13
lines changed

examples/e2e/app-router/open-next.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { defineCloudflareConfig } from "@opennextjs/cloudflare";
22
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
33
import shardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
44
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
5-
import cachePurge from "@opennextjs/cloudflare/overrides/cache-purge/index";
5+
import { purgeCache } from "@opennextjs/cloudflare/overrides/cache-purge/index";
66

77
export default defineCloudflareConfig({
88
incrementalCache: r2IncrementalCache,
@@ -15,6 +15,6 @@ export default defineCloudflareConfig({
1515
},
1616
}),
1717
queue: doQueue,
18-
cachePurge: cachePurge,
18+
cachePurge: purgeCache({ type: "durableObject" }),
1919
enableCacheInterception: true,
2020
});

examples/e2e/app-router/wrangler.jsonc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717
{
1818
"name": "NEXT_TAG_CACHE_DO_SHARDED",
1919
"class_name": "DOShardedTagCache"
20+
},
21+
{
22+
"name": "NEXT_CACHE_DO_PURGE",
23+
"class_name": "BucketCachePurge"
2024
}
2125
]
2226
},
2327
"migrations": [
2428
{
2529
"tag": "v1",
26-
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache"]
30+
"new_sqlite_classes": ["DOQueueHandler", "DOShardedTagCache", "BucketCachePurge"]
2731
}
2832
],
2933
"r2_buckets": [

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Context, RunningCodeOptions } from "node:vm";
22

33
import type { GetPlatformProxyOptions } from "wrangler";
44

5+
import type { BucketCachePurge } from "./durable-objects/bucket-cache-purge.js";
56
import type { DOQueueHandler } from "./durable-objects/queue.js";
67
import type { DOShardedTagCache } from "./durable-objects/sharded-tag-cache.js";
78
import type { PREFIX_ENV_NAME as KV_CACHE_PREFIX_ENV_NAME } from "./overrides/incremental-cache/kv-incremental-cache.js";
@@ -56,8 +57,11 @@ declare global {
5657
NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE?: string;
5758

5859
// Below are the optional env variables for purging the cache
60+
// Durable Object namespace to use for the durable object queue
61+
NEXT_CACHE_DO_PURGE?: DurableObjectNamespace<BucketCachePurge>;
5962
CACHE_ZONE_ID?: string;
6063
CACHE_API_TOKEN?: string;
64+
CACHE_BUFFER_TIME_IN_SECONDS?: string;
6165
}
6266
}
6367

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { DurableObject } from "cloudflare:workers";
2+
3+
import { internalPurgeCacheByTags } from "../overrides/internal";
4+
5+
const DEFAULT_BUFFER_TIME = 5; // seconds
6+
7+
export class BucketCachePurge extends DurableObject<CloudflareEnv> {
8+
bufferTimeInSeconds: number;
9+
10+
constructor(state: DurableObjectState, env: CloudflareEnv) {
11+
super(state, env);
12+
this.bufferTimeInSeconds = env.CACHE_BUFFER_TIME_IN_SECONDS
13+
? parseInt(env.CACHE_BUFFER_TIME_IN_SECONDS)
14+
: DEFAULT_BUFFER_TIME; // Default buffer time
15+
16+
// Initialize the sql table if it doesn't exist
17+
state.storage.sql.exec(`
18+
CREATE TABLE IF NOT EXISTS cache_purge (
19+
tag TEXT NOT NULL
20+
);
21+
CREATE UNIQUE INDEX IF NOT EXISTS tag_index ON cache_purge (tag);
22+
`);
23+
}
24+
25+
async purgeCacheByTags(tags: string[]) {
26+
for (const tag of tags) {
27+
// Insert the tag into the sql table
28+
this.ctx.storage.sql.exec(
29+
`
30+
INSERT OR REPLACE INTO cache_purge (tag)
31+
VALUES (?)`,
32+
[tag]
33+
);
34+
}
35+
const nextAlarm = await this.ctx.storage.getAlarm();
36+
if (!nextAlarm) {
37+
// Set an alarm to trigger the cache purge
38+
this.ctx.storage.setAlarm(Date.now() + this.bufferTimeInSeconds * 1000);
39+
}
40+
}
41+
42+
override async alarm() {
43+
let tags = this.ctx.storage.sql
44+
.exec<{ tag: string }>(
45+
`
46+
SELECT * FROM cache_purge LIMIT 100
47+
`
48+
)
49+
.toArray();
50+
do {
51+
await internalPurgeCacheByTags(
52+
this.env,
53+
tags.map((row) => row.tag)
54+
);
55+
// Delete the tags from the sql table
56+
this.ctx.storage.sql.exec(
57+
`
58+
DELETE FROM cache_purge
59+
WHERE tag IN (${tags.map(() => "?").join(",")})
60+
`,
61+
tags.map((row) => row.tag)
62+
);
63+
if (tags.length < 100) {
64+
// If we have less than 100 tags, we can stop
65+
tags = [];
66+
} else {
67+
// Otherwise, we need to get the next 100 tags
68+
tags = this.ctx.storage.sql
69+
.exec<{ tag: string }>(
70+
`
71+
SELECT * FROM cache_purge LIMIT 100
72+
`
73+
)
74+
.toArray();
75+
}
76+
} while (tags.length > 0);
77+
}
78+
}
Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,33 @@
11
import type { CDNInvalidationHandler } from "@opennextjs/aws/types/overrides";
22

3-
import { debugCache, purgeCacheByTags } from "../internal.js";
3+
import { getCloudflareContext } from "../../cloudflare-context";
4+
import { debugCache, internalPurgeCacheByTags } from "../internal.js";
45

5-
export default {
6-
name: "cloudflare",
7-
async invalidatePaths(paths) {
8-
const tags = paths.map((path) => `_N_T_${path.rawPath}`);
9-
debugCache("cdnInvalidation", "Invalidating paths:", tags);
10-
await purgeCacheByTags(tags);
11-
debugCache("cdnInvalidation", "Invalidated paths:", tags);
12-
},
13-
} satisfies CDNInvalidationHandler;
6+
interface PurgeOptions {
7+
type: "durableObject" | "direct";
8+
}
9+
10+
export const purgeCache = ({ type = "direct" }: PurgeOptions) => {
11+
return {
12+
name: "cloudflare",
13+
async invalidatePaths(paths) {
14+
const { env } = getCloudflareContext();
15+
const tags = paths.map((path) => `_N_T_${path.rawPath}`);
16+
debugCache("cdnInvalidation", "Invalidating paths:", tags);
17+
if (type === "direct") {
18+
await internalPurgeCacheByTags(env, tags);
19+
} else {
20+
console.log("purgeCacheByTags DO", tags);
21+
const durableObject = env.NEXT_CACHE_DO_PURGE;
22+
if (!durableObject) {
23+
debugCache("cdnInvalidation", "No durable object found. Skipping cache purge.");
24+
return;
25+
}
26+
const id = durableObject.idFromName("cache-purge");
27+
const obj = durableObject.get(id);
28+
await obj.purgeCacheByTags(tags);
29+
}
30+
debugCache("cdnInvalidation", "Invalidated paths:", tags);
31+
},
32+
} satisfies CDNInvalidationHandler;
33+
};

packages/cloudflare/src/api/overrides/internal.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ export function computeCacheKey(key: string, options: KeyOptions) {
3333

3434
export async function purgeCacheByTags(tags: string[]) {
3535
const { env } = getCloudflareContext();
36+
// We have a durable object for purging cache
37+
// We should use it
38+
if (env.NEXT_CACHE_DO_PURGE) {
39+
const durableObject = env.NEXT_CACHE_DO_PURGE;
40+
if (!durableObject) {
41+
debugCache("purgeCacheByTags", "No durable object found. Skipping cache purge.");
42+
return;
43+
}
44+
const id = durableObject.idFromName("cache-purge");
45+
const obj = durableObject.get(id);
46+
await obj.purgeCacheByTags(tags);
47+
} else {
48+
// We don't have a durable object for purging cache
49+
// We should use the API directly
50+
await internalPurgeCacheByTags(env, tags);
51+
}
52+
}
53+
54+
export async function internalPurgeCacheByTags(env: CloudflareEnv, tags: string[]) {
55+
//TODO: Remove this before commit
56+
console.log("purgeCacheByTags", tags);
3657

3758
if (!env.CACHE_ZONE_ID && !env.CACHE_API_TOKEN) {
3859
// THIS IS A NO-OP

packages/cloudflare/src/cli/build/open-next/compileDurableObjects.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export function compileDurableObjects(buildOpts: BuildOptions) {
99
const entryPoints = [
1010
_require.resolve("@opennextjs/cloudflare/durable-objects/queue"),
1111
_require.resolve("@opennextjs/cloudflare/durable-objects/sharded-tag-cache"),
12+
_require.resolve("@opennextjs/cloudflare/durable-objects/bucket-cache-purge"),
1213
];
1314

1415
const { outputDir } = buildOpts;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { runWithCloudflareRequestContext } from "./cloudflare/init.js";
55
export { DOQueueHandler } from "./.build/durable-objects/queue.js";
66
//@ts-expect-error: Will be resolved by wrangler build
77
export { DOShardedTagCache } from "./.build/durable-objects/sharded-tag-cache.js";
8+
//@ts-expect-error: Will be resolved by wrangler build
9+
export { BucketCachePurge } from "./.build/durable-objects/bucket-cache-purge.js";
810

911
export default {
1012
async fetch(request, env, ctx) {

0 commit comments

Comments
 (0)