Skip to content

Commit 814725c

Browse files
committed
add an optional cache layer
1 parent 02dbf97 commit 814725c

File tree

3 files changed

+114
-17
lines changed

3 files changed

+114
-17
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@ import shardedTagCache from "@opennextjs/cloudflare/do-sharded-tag-cache";
66

77
export default defineCloudflareConfig({
88
incrementalCache: kvIncrementalCache,
9-
tagCache: shardedTagCache({ numberOfShards: 4 }),
9+
tagCache: shardedTagCache({ numberOfShards: 12, regionalCache: true }),
1010
queue: doQueue,
1111
});

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

Lines changed: 113 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { debug } from "@opennextjs/aws/adapters/logger.js";
1+
import { debug, error } from "@opennextjs/aws/adapters/logger.js";
22
import { generateShardId } from "@opennextjs/aws/core/routing/queue.js";
33
import type { OpenNextConfig } from "@opennextjs/aws/types/open-next";
44
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
@@ -7,11 +7,30 @@ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
77
import { getCloudflareContext } from "./cloudflare-context";
88

99
interface ShardedD1TagCacheOptions {
10+
/**
11+
* The number of shards that will be used.
12+
* 1 shards means 1 durable object instance.
13+
* Be aware that the more shards you have, the more requests you will make to the Durable Object
14+
* @default 4
15+
*/
1016
numberOfShards: number;
17+
/**
18+
* Whether to enable a regional cache on a per-shard basis
19+
* Because of the way tags are implemented in Next.js, some shards will have more requests than others. For these cases, it is recommended to enable the regional cache.
20+
* @default false
21+
*/
22+
regionalCache?: boolean;
23+
/**
24+
* The TTL for the regional cache in seconds
25+
* Increasing this value will reduce the number of requests to the Durable Object, but it could make `revalidateTags`/`revalidatePath` call being longer to take effect
26+
* @default 5
27+
*/
28+
regionalCacheTtl?: number;
1129
}
1230
class ShardedD1TagCache implements NextModeTagCache {
1331
mode = "nextMode" as const;
1432
public readonly name = "sharded-d1-tag-cache";
33+
localCache?: Cache;
1534

1635
constructor(private opts: ShardedD1TagCacheOptions = { numberOfShards: 4 }) {}
1736

@@ -23,6 +42,11 @@ class ShardedD1TagCache implements NextModeTagCache {
2342
return durableObject.get(id);
2443
}
2544

45+
/**
46+
* Same tags are guaranteed to be in the same shard
47+
* @param tags
48+
* @returns A map of shardId to tags
49+
*/
2650
private generateShards(tags: string[]) {
2751
// For each tag, we generate a message group id
2852
const messageGroupIds = tags.map((tag) => ({
@@ -39,12 +63,11 @@ class ShardedD1TagCache implements NextModeTagCache {
3963
return shards;
4064
}
4165

42-
private getConfig() {
66+
private async getConfig() {
4367
const cfEnv = getCloudflareContext().env;
4468
const db = cfEnv.NEXT_CACHE_D1_SHARDED;
4569

4670
if (!db) debug("No Durable object found");
47-
4871
const isDisabled = !!(globalThis as unknown as { openNextConfig: OpenNextConfig }).openNextConfig
4972
.dangerous?.disableTagCache;
5073

@@ -58,32 +81,108 @@ class ShardedD1TagCache implements NextModeTagCache {
5881
};
5982
}
6083

84+
/**
85+
* This function checks if the tags have been revalidated
86+
* It is never supposed to throw and in case of error, it will return false
87+
* @param tags
88+
* @param lastModified default to `Date.now()`
89+
* @returns
90+
*/
6191
async hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean> {
62-
const { isDisabled } = this.getConfig();
92+
const { isDisabled } = await this.getConfig();
6393
if (isDisabled) return false;
64-
const shards = this.generateShards(tags);
65-
// We then create a new durable object for each shard
66-
const shardsResult = await Promise.all(
67-
Array.from(shards.entries()).map(async ([shardId, shardedTags]) => {
68-
const stub = this.getDurableObjectStub(shardId);
69-
return stub.hasBeenRevalidated(shardedTags, lastModified);
70-
})
71-
);
72-
return shardsResult.some((result) => result);
94+
try {
95+
const shards = this.generateShards(tags);
96+
// We then create a new durable object for each shard
97+
const shardsResult = await Promise.all(
98+
Array.from(shards.entries()).map(async ([shardId, shardedTags]) => {
99+
const cachedValue = await this.getFromRegionalCache(shardId, shardedTags);
100+
if (cachedValue) {
101+
return (await cachedValue.text()) === "true";
102+
}
103+
const stub = this.getDurableObjectStub(shardId);
104+
const _hasBeenRevalidated = await stub.hasBeenRevalidated(shardedTags, lastModified);
105+
//TODO: Do we want to cache the result if it has been revalidated ?
106+
// If we do so, we risk causing cache MISS even though it has been revalidated elsewhere
107+
// On the other hand revalidating a tag that is used in a lot of places will cause a lot of requests
108+
if (!_hasBeenRevalidated) {
109+
getCloudflareContext().ctx.waitUntil(
110+
this.putToRegionalCache(shardId, shardedTags, _hasBeenRevalidated)
111+
);
112+
}
113+
return _hasBeenRevalidated;
114+
})
115+
);
116+
return shardsResult.some((result) => result);
117+
} catch (e) {
118+
error("Error while checking revalidation", e);
119+
return false;
120+
}
73121
}
74122

123+
/**
124+
* This function writes the tags to the cache
125+
* Due to the way shards and regional cache are implemented, the regional cache may not be properly invalidated
126+
* @param tags
127+
* @returns
128+
*/
75129
async writeTags(tags: string[]): Promise<void> {
76-
const { isDisabled } = this.getConfig();
130+
const { isDisabled } = await this.getConfig();
77131
if (isDisabled) return;
78132
const shards = this.generateShards(tags);
79133
// We then create a new durable object for each shard
80134
await Promise.all(
81135
Array.from(shards.entries()).map(async ([shardId, shardedTags]) => {
82136
const stub = this.getDurableObjectStub(shardId);
83137
await stub.writeTags(shardedTags);
138+
// Depending on the shards and the tags, deleting from the regional cache will not work for every tag
139+
await this.deleteRegionalCache(shardId, shardedTags);
140+
})
141+
);
142+
}
143+
144+
// Cache API
145+
async getCacheInstance() {
146+
if (!this.localCache && this.opts.regionalCache) {
147+
this.localCache = await caches.open("sharded-d1-tag-cache");
148+
}
149+
return this.localCache;
150+
}
151+
152+
async getCacheKey(shardId: string, tags: string[]) {
153+
return new Request(
154+
new URL(`shard/${shardId}?tags=${encodeURIComponent(tags.join(";"))}`, "http://local.cache")
155+
);
156+
}
157+
158+
async getFromRegionalCache(shardId: string, tags: string[]) {
159+
if (!this.opts.regionalCache) return;
160+
const cache = await this.getCacheInstance();
161+
if (!cache) return;
162+
const key = await this.getCacheKey(shardId, tags);
163+
return cache.match(key);
164+
}
165+
166+
async putToRegionalCache(shardId: string, tags: string[], hasBeenRevalidated: boolean) {
167+
if (!this.opts.regionalCache) return;
168+
const cache = await this.getCacheInstance();
169+
if (!cache) return;
170+
const key = await this.getCacheKey(shardId, tags);
171+
await cache.put(
172+
key,
173+
new Response(`${hasBeenRevalidated}`, {
174+
headers: { "cache-control": `max-age=${this.opts.regionalCacheTtl ?? 5}` },
84175
})
85176
);
86177
}
178+
179+
async deleteRegionalCache(shardId: string, tags: string[]) {
180+
if (!this.opts.regionalCache) return;
181+
const cache = await this.getCacheInstance();
182+
if (!cache) return;
183+
const key = await this.getCacheKey(shardId, tags);
184+
await cache.delete(key);
185+
}
87186
}
88187

89188
export default (opts?: ShardedD1TagCacheOptions) => new ShardedD1TagCache(opts);

packages/cloudflare/src/api/durable-objects/sharded-tag-cache.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ export class DOShardedTagCache extends DurableObject<CloudflareEnv> {
2121
lastModified ?? Date.now()
2222
)
2323
.one();
24-
console.log("Checking revalidation for tags", tags, result);
2524
return result.cnt > 0;
2625
}
2726

@@ -33,6 +32,5 @@ export class DOShardedTagCache extends DurableObject<CloudflareEnv> {
3332
Date.now()
3433
);
3534
});
36-
console.log("Writing tags", tags);
3735
}
3836
}

0 commit comments

Comments
 (0)