@@ -7,6 +7,8 @@ import type {
77} from '@opennextjs/aws/types/overrides.js' ;
88import { getCloudflareContext } from '@opennextjs/cloudflare' ;
99
10+ import { withRegionalCache } from '@opennextjs/cloudflare/overrides/incremental-cache/regional-cache' ;
11+
1012import type { DurableObjectNamespace , Rpc } from '@cloudflare/workers-types' ;
1113
1214export const BINDING_NAME = 'NEXT_INC_CACHE_R2_BUCKET' ;
@@ -18,51 +20,37 @@ export type KeyOptions = {
1820
1921/**
2022 *
21- * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it allow us to trace
22- * the cache operations. It also integrates both R2 and Cache API in a single class.
23- * Having our own, will allow us to customize it in the future if needed.
23+ * It is very similar to the `R2IncrementalCache` in the `@opennextjs/cloudflare` package, but it has an additional
24+ * R2WriteBuffer Durable Object to handle writes to R2. Given how we set up cache, we often end up writing to the same key too fast.
2425 */
2526class GitbookIncrementalCache implements IncrementalCache {
2627 name = 'GitbookIncrementalCache' ;
2728
28- protected localCache : Cache | undefined ;
29-
3029 async get < CacheType extends CacheEntryType = 'cache' > (
3130 key : string ,
3231 cacheType ?: CacheType
3332 ) : Promise < WithLastModified < CacheValue < CacheType > > | null > {
3433 const cacheKey = this . getR2Key ( key , cacheType ) ;
3534
3635 const r2 = getCloudflareContext ( ) . env [ BINDING_NAME ] ;
37- const localCache = await this . getCacheInstance ( ) ;
3836 if ( ! r2 ) throw new Error ( 'No R2 bucket' ) ;
3937 if ( process . env . SHOULD_BYPASS_CACHE === 'true' ) {
4038 // We are in a local middleware environment, we should bypass the cache
4139 // and go directly to the server.
4240 return null ;
4341 }
4442 try {
45- // Check local cache first if available
46- const localCacheEntry = await localCache . match ( this . getCacheUrlKey ( cacheKey ) ) ;
47- if ( localCacheEntry ) {
48- const result = ( await localCacheEntry . json ( ) ) as WithLastModified <
49- CacheValue < CacheType >
50- > ;
51- return this . returnNullOn404 ( {
52- ...result ,
53- // Because we use tag cache and also invalidate them every time,
54- // if we get a cache hit, we don't need to check the tag cache as we already know it's not been revalidated
55- // this should improve performance even further, and reduce costs
56- shouldBypassTagCache : true ,
57- } ) ;
58- }
59-
6043 const r2Object = await r2 . get ( cacheKey ) ;
6144 if ( ! r2Object ) return null ;
6245
46+ const json = ( await r2Object . json ( ) ) as CacheValue < CacheType > ;
47+ const lastModified = r2Object . uploaded . getTime ( ) ;
48+
49+ if ( ! json ) return null ;
50+
6351 return this . returnNullOn404 ( {
64- value : await r2Object . json ( ) ,
65- lastModified : r2Object . uploaded . getTime ( ) ,
52+ value : json ,
53+ lastModified,
6654 } ) ;
6755 } catch ( e ) {
6856 console . error ( 'Failed to get from cache' , e ) ;
@@ -89,39 +77,8 @@ class GitbookIncrementalCache implements IncrementalCache {
8977 ) : Promise < void > {
9078 const cacheKey = this . getR2Key ( key , cacheType ) ;
9179
92- const localCache = await this . getCacheInstance ( ) ;
93-
9480 try {
9581 await this . writeToR2 ( cacheKey , JSON . stringify ( value ) ) ;
96-
97- //TODO: Check if there is any places where we don't have tags
98- // Ideally we should always have tags, but in case we don't, we need to decide how to handle it
99- // For now we default to a build ID tag, which allow us to invalidate the cache in case something is wrong in this deployment
100- const tags = this . getTagsFromCacheEntry ( value ) ?? [
101- `build_id/${ process . env . NEXT_BUILD_ID } ` ,
102- ] ;
103-
104- // We consider R2 as the source of truth, so we update the local cache
105- // only after a successful R2 write
106- await localCache . put (
107- this . getCacheUrlKey ( cacheKey ) ,
108- new Response (
109- JSON . stringify ( {
110- value,
111- // Note: `Date.now()` returns the time of the last IO rather than the actual time.
112- // See https://developers.cloudflare.com/workers/reference/security-model/
113- lastModified : Date . now ( ) ,
114- } ) ,
115- {
116- headers : {
117- // Cache-Control default to 30 minutes, will be overridden by `revalidate`
118- // In theory we should always get the `revalidate` value
119- 'cache-control' : `max-age=${ value . revalidate ?? 60 * 30 } ` ,
120- 'cache-tag' : tags . join ( ',' ) ,
121- } ,
122- }
123- )
124- ) ;
12582 } catch ( e ) {
12683 console . error ( 'Failed to set to cache' , e ) ;
12784 }
@@ -131,14 +88,10 @@ class GitbookIncrementalCache implements IncrementalCache {
13188 const cacheKey = this . getR2Key ( key ) ;
13289
13390 const r2 = getCloudflareContext ( ) . env [ BINDING_NAME ] ;
134- const localCache = await this . getCacheInstance ( ) ;
13591 if ( ! r2 ) throw new Error ( 'No R2 bucket' ) ;
13692
13793 try {
13894 await r2 . delete ( cacheKey ) ;
139-
140- // Here again R2 is the source of truth, so we delete from local cache first
141- await localCache . delete ( this . getCacheUrlKey ( cacheKey ) ) ;
14295 } catch ( e ) {
14396 console . error ( 'Failed to delete from cache' , e ) ;
14497 }
@@ -168,12 +121,6 @@ class GitbookIncrementalCache implements IncrementalCache {
168121 }
169122 }
170123
171- async getCacheInstance ( ) : Promise < Cache > {
172- if ( this . localCache ) return this . localCache ;
173- this . localCache = await caches . open ( 'incremental-cache' ) ;
174- return this . localCache ;
175- }
176-
177124 // Utility function to generate keys for R2/Cache API
178125 getR2Key ( initialKey : string , cacheType : CacheEntryType = 'cache' ) : string {
179126 let key = initialKey ;
@@ -189,28 +136,13 @@ class GitbookIncrementalCache implements IncrementalCache {
189136 '/'
190137 ) ;
191138 }
192-
193- getCacheUrlKey ( cacheKey : string ) : string {
194- return `http://cache.local/${ cacheKey } ` ;
195- }
196-
197- getTagsFromCacheEntry < CacheType extends CacheEntryType > (
198- entry : CacheValue < CacheType >
199- ) : string [ ] | undefined {
200- if ( 'tags' in entry && entry . tags ) {
201- return entry . tags ;
202- }
203-
204- if ( 'meta' in entry && entry . meta && 'headers' in entry . meta && entry . meta . headers ) {
205- const rawTags = entry . meta . headers [ 'x-next-cache-tags' ] ;
206- if ( typeof rawTags === 'string' ) {
207- return rawTags . split ( ',' ) ;
208- }
209- }
210- if ( 'value' in entry ) {
211- return entry . tags ;
212- }
213- }
214139}
215140
216- export default new GitbookIncrementalCache ( ) ;
141+ export default withRegionalCache ( new GitbookIncrementalCache ( ) , {
142+ mode : 'long-lived' ,
143+ // We can do it because we use our own logic to invalidate the cache
144+ bypassTagCacheOnCacheHit : true ,
145+ defaultLongLivedTtlSec : 60 * 60 * 24 /* 24 hours */ ,
146+ // We don't want to update the cache entry on every cache hit
147+ shouldLazilyUpdateOnCacheHit : false ,
148+ } ) ;
0 commit comments