1- import { debug } from "@opennextjs/aws/adapters/logger.js" ;
1+ import { debug , error } from "@opennextjs/aws/adapters/logger.js" ;
22import { generateShardId } from "@opennextjs/aws/core/routing/queue.js" ;
33import type { OpenNextConfig } from "@opennextjs/aws/types/open-next" ;
44import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js" ;
@@ -7,11 +7,30 @@ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
77import { getCloudflareContext } from "./cloudflare-context" ;
88
99interface 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}
1230class 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
89188export default ( opts ?: ShardedD1TagCacheOptions ) => new ShardedD1TagCache ( opts ) ;
0 commit comments