Skip to content

Commit 7810d8d

Browse files
committed
basic purge cache
1 parent 7066330 commit 7810d8d

File tree

7 files changed

+84
-5
lines changed

7 files changed

+84
-5
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { defineCloudflareConfig } from "@opennextjs/cloudflare";
22
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
3+
import {withRegionalCache} from "@opennextjs/cloudflare/overrides/incremental-cache/regional-cache";
34
import shardedTagCache from "@opennextjs/cloudflare/overrides/tag-cache/do-sharded-tag-cache";
45
import doQueue from "@opennextjs/cloudflare/overrides/queue/do-queue";
6+
import cachePurge from "@opennextjs/cloudflare/overrides/cache-purge/index";
57

68
export default defineCloudflareConfig({
7-
incrementalCache: r2IncrementalCache,
9+
incrementalCache: withRegionalCache(r2IncrementalCache, {mode: "long-lived"}),
810
// With such a configuration, we could have up to 12 * (8 + 2) = 120 Durable Objects instances
911
tagCache: shardedTagCache({
1012
baseShardSize: 12,
@@ -14,5 +16,6 @@ export default defineCloudflareConfig({
1416
},
1517
}),
1618
queue: doQueue,
19+
cachePurge: cachePurge,
1720
enableCacheInterception: true,
1821
});

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ declare global {
5454
// Disable SQLite for the durable object queue handler
5555
// This can be safely used if you don't use an eventually consistent incremental cache (i.e. R2 without the regional cache for example)
5656
NEXT_CACHE_DO_QUEUE_DISABLE_SQLITE?: string;
57+
58+
// Below are the optional env variables for purging the cache
59+
CACHE_ZONE_ID?: string;
60+
CACHE_API_TOKEN?: string;
5761
}
5862
}
5963

packages/cloudflare/src/api/config.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {
44
LazyLoadedOverride,
55
OpenNextConfig as AwsOpenNextConfig,
66
} from "@opennextjs/aws/types/open-next";
7-
import type { IncrementalCache, Queue, TagCache } from "@opennextjs/aws/types/overrides";
7+
import type { CDNInvalidationHandler, IncrementalCache, Queue, TagCache } from "@opennextjs/aws/types/overrides";
88

99
export type Override<T extends BaseOverride> = "dummy" | T | LazyLoadedOverride<T>;
1010

@@ -29,6 +29,11 @@ export type CloudflareOverrides = {
2929
*/
3030
queue?: "direct" | Override<Queue>;
3131

32+
/**
33+
* Sets the automatic cache purge implementation
34+
*/
35+
cachePurge?: Override<CDNInvalidationHandler>
36+
3237
/**
3338
* Enable cache interception
3439
* Should be `false` when PPR is used
@@ -44,7 +49,7 @@ export type CloudflareOverrides = {
4449
* @returns the OpenNext configuration object
4550
*/
4651
export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNextConfig {
47-
const { incrementalCache, tagCache, queue, enableCacheInterception = false } = config;
52+
const { incrementalCache, tagCache, queue, cachePurge, enableCacheInterception = false } = config;
4853

4954
return {
5055
default: {
@@ -55,6 +60,7 @@ export function defineCloudflareConfig(config: CloudflareOverrides = {}): OpenNe
5560
incrementalCache: resolveIncrementalCache(incrementalCache),
5661
tagCache: resolveTagCache(tagCache),
5762
queue: resolveQueue(queue),
63+
cdnInvalidation: resolveCdnInvalidation(cachePurge),
5864
},
5965
routePreloadingBehavior: "withWaitUntil",
6066
},
@@ -93,6 +99,14 @@ function resolveQueue(value: CloudflareOverrides["queue"] = "dummy") {
9399
return typeof value === "function" ? value : () => value;
94100
}
95101

102+
function resolveCdnInvalidation(value: CloudflareOverrides["cachePurge"] = "dummy") {
103+
if (typeof value === "string") {
104+
return value;
105+
}
106+
107+
return typeof value === "function" ? value : () => value;
108+
}
109+
96110
interface OpenNextConfig extends AwsOpenNextConfig {
97111
cloudflare?: {
98112
/**
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import type { CDNInvalidationHandler } from "@opennextjs/aws/types/overrides";
2+
3+
import { debugCache, purgeCacheByTags } from "../internal.js";
4+
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;

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { createHash } from "node:crypto";
22

33
import type { CacheEntryType, CacheValue } from "@opennextjs/aws/types/overrides.js";
44

5+
import { getCloudflareContext } from "../cloudflare-context.js";
6+
57
export type IncrementalCacheEntry<CacheType extends CacheEntryType> = {
68
value: CacheValue<CacheType>;
79
lastModified: number;
@@ -28,3 +30,44 @@ export function computeCacheKey(key: string, options: KeyOptions) {
2830
const hash = createHash("sha256").update(key).digest("hex");
2931
return `${prefix}/${buildId}/${hash}.${cacheType}`.replace(/\/+/g, "/");
3032
}
33+
34+
35+
export async function purgeCacheByTags(tags: string[]) {
36+
const {env} = getCloudflareContext()
37+
38+
if(!env.CACHE_ZONE_ID && !env.CACHE_API_TOKEN) {
39+
// THIS IS A NO-OP
40+
debugCache("purgeCacheByTags", "No cache zone ID or API token provided. Skipping cache purge.");
41+
return;
42+
}
43+
44+
try {
45+
const response = await fetch(
46+
`https://api.cloudflare.com/client/v4/zones/${env.CACHE_ZONE_ID}/purge_cache`,
47+
{
48+
headers: {
49+
"Authorization": `Bearer ${env.CACHE_API_TOKEN}`,
50+
"Content-Type": "application/json",
51+
},
52+
method: "POST",
53+
body: JSON.stringify({
54+
tags,
55+
}),
56+
})
57+
if (!response.ok) {
58+
const text = await response.text();
59+
throw new Error(`Failed to purge cache: ${response.status} ${text}`);
60+
}
61+
const bodyResponse = await response.json() as {
62+
success: boolean;
63+
errors: Array<{ code: number; message: string }>;
64+
messages: Array<{ code: number; message: string }>;
65+
}
66+
if (!bodyResponse.success) {
67+
throw new Error(`Failed to purge cache: ${JSON.stringify(bodyResponse.errors)}`);
68+
}
69+
debugCache("purgeCacheByTags", "Cache purged successfully for tags:", tags);
70+
}catch (error) {
71+
console.error("Error purging cache by tags:", error);
72+
}
73+
}

packages/cloudflare/src/api/overrides/tag-cache/d1-next-tag-cache.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
33

44
import type { OpenNextConfig } from "../../../api/config.js";
55
import { getCloudflareContext } from "../../cloudflare-context.js";
6-
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
6+
import { debugCache, FALLBACK_BUILD_ID, purgeCacheByTags } from "../internal.js";
77

88
export const NAME = "d1-next-mode-tag-cache";
99

@@ -66,6 +66,7 @@ export class D1NextModeTagCache implements NextModeTagCache {
6666
.bind(this.getCacheKey(tag), Date.now())
6767
)
6868
);
69+
await purgeCacheByTags(tags);
6970
}
7071

7172
private getConfig() {

packages/cloudflare/src/api/overrides/tag-cache/do-sharded-tag-cache.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
55

66
import type { OpenNextConfig } from "../../../api/config.js";
77
import { getCloudflareContext } from "../../cloudflare-context";
8-
import { debugCache } from "../internal";
8+
import { debugCache, purgeCacheByTags } from "../internal";
99

1010
export const DEFAULT_WRITE_RETRIES = 3;
1111
export const DEFAULT_NUM_SHARDS = 4;
@@ -304,6 +304,7 @@ class ShardedDOTagCache implements NextModeTagCache {
304304
await this.performWriteTagsWithRetry(doId, tags, currentTime);
305305
})
306306
);
307+
await purgeCacheByTags(tags);
307308
}
308309

309310
async performWriteTagsWithRetry(doId: DOId, tags: string[], lastModified: number, retryNumber = 0) {

0 commit comments

Comments
 (0)