@@ -5,6 +5,7 @@ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
5
5
6
6
import type { OpenNextConfig } from "../../../api/config.js" ;
7
7
import { getCloudflareContext } from "../../cloudflare-context" ;
8
+ import { DOShardedTagCache } from "../../durable-objects/sharded-tag-cache.js" ;
8
9
import { debugCache , purgeCacheByTags } from "../internal" ;
9
10
10
11
export const DEFAULT_WRITE_RETRIES = 3 ;
@@ -120,7 +121,6 @@ export class DOId {
120
121
interface CacheTagKeyOptions {
121
122
doId : DOId ;
122
123
tags : string [ ] ;
123
- type : "boolean" | "number" ;
124
124
}
125
125
class ShardedDOTagCache implements NextModeTagCache {
126
126
readonly mode = "nextMode" as const ;
@@ -294,28 +294,31 @@ class ShardedDOTagCache implements NextModeTagCache {
294
294
async getLastRevalidated ( tags : string [ ] ) : Promise < number > {
295
295
const { isDisabled } = await this . getConfig ( ) ;
296
296
if ( isDisabled ) return 0 ;
297
+ if ( tags . length === 0 ) return 0 ; // No tags to check
298
+ const deduplicatedTags = Array . from ( new Set ( tags ) ) ; // We deduplicate the tags to avoid unnecessary requests
297
299
try {
298
- const shardedTagGroups = this . groupTagsByDO ( { tags } ) ;
300
+ const shardedTagGroups = this . groupTagsByDO ( { tags : deduplicatedTags } ) ;
299
301
const shardedTagRevalidationOutcomes = await Promise . all (
300
302
shardedTagGroups . map ( async ( { doId, tags } ) => {
301
- const cachedValue = await this . getFromRegionalCache ( { doId, tags, type : "number" } ) ;
302
- if ( cachedValue ) {
303
- const cached = await cachedValue . text ( ) ;
304
- try {
305
- return parseInt ( cached , 10 ) ;
306
- } catch ( e ) {
307
- debug ( "Error while parsing cached value" , e ) ;
308
- // If we can't parse the cached value, we should just ignore it and go to the durable object
309
- }
303
+ const cachedValue = await this . getFromRegionalCache ( { doId, tags } ) ;
304
+ // If all the value were found in the regional cache, we can just return the max value
305
+ if ( cachedValue . length === tags . length ) {
306
+ return Math . max ( ...cachedValue . map ( ( item ) => item . time ) ) ;
310
307
}
308
+ // Otherwise we need to check the durable object on the ones that were not found in the cache
309
+ const filteredTags = deduplicatedTags . filter (
310
+ ( tag ) => ! cachedValue . some ( ( item ) => item . tag === tag )
311
+ ) ;
312
+
311
313
const stub = this . getDurableObjectStub ( doId ) ;
312
- const _lastRevalidated = await stub . getLastRevalidated ( tags ) ;
313
- if ( ! _lastRevalidated ) {
314
- getCloudflareContext ( ) . ctx . waitUntil (
315
- this . putToRegionalCache ( { doId, tags, type : "number" } , _lastRevalidated )
316
- ) ;
317
- }
318
- return _lastRevalidated ;
314
+ const lastRevalidated = await stub . getLastRevalidated ( filteredTags ) ;
315
+
316
+ const result = Math . max ( ...cachedValue . map ( ( item ) => item . time ) , lastRevalidated ) ;
317
+
318
+ // We then need to populate the regional cache with the missing tags
319
+ getCloudflareContext ( ) . ctx . waitUntil ( this . putToRegionalCache ( { doId, tags } , stub ) ) ;
320
+
321
+ return result ;
319
322
} )
320
323
) ;
321
324
return Math . max ( ...shardedTagRevalidationOutcomes ) ;
@@ -339,20 +342,27 @@ class ShardedDOTagCache implements NextModeTagCache {
339
342
const shardedTagGroups = this . groupTagsByDO ( { tags } ) ;
340
343
const shardedTagRevalidationOutcomes = await Promise . all (
341
344
shardedTagGroups . map ( async ( { doId, tags } ) => {
342
- const cachedValue = await this . getFromRegionalCache ( { doId, tags, type : "boolean" } ) ;
343
- if ( cachedValue ) {
344
- return ( await cachedValue . text ( ) ) === "true" ;
345
+ const cachedValue = await this . getFromRegionalCache ( { doId, tags } ) ;
346
+
347
+ // If one of the cached values is newer than the lastModified, we can return true
348
+ const cacheHasBeenRevalidated = cachedValue . some ( ( cachedValue ) => {
349
+ return ( cachedValue . time ?? 0 ) > ( lastModified ?? Date . now ( ) ) ;
350
+ } ) ;
351
+
352
+ if ( cacheHasBeenRevalidated ) {
353
+ return true ;
345
354
}
346
355
const stub = this . getDurableObjectStub ( doId ) ;
347
356
const _hasBeenRevalidated = await stub . hasBeenRevalidated ( tags , lastModified ) ;
348
- //TODO: Do we want to cache the result if it has been revalidated ?
349
- // If we do so, we risk causing cache MISS even though it has been revalidated elsewhere
350
- // On the other hand revalidating a tag that is used in a lot of places will cause a lot of requests
351
- if ( ! _hasBeenRevalidated ) {
357
+
358
+ const remainingTags = tags . filter ( ( tag ) => ! cachedValue . some ( ( item ) => item . tag === tag ) ) ;
359
+ if ( remainingTags . length > 0 ) {
360
+ // We need to put the missing tags in the regional cache
352
361
getCloudflareContext ( ) . ctx . waitUntil (
353
- this . putToRegionalCache ( { doId, tags, type : "boolean" } , _hasBeenRevalidated )
362
+ this . putToRegionalCache ( { doId, tags : remainingTags } , stub )
354
363
) ;
355
364
}
365
+
356
366
return _hasBeenRevalidated ;
357
367
} )
358
368
) ;
@@ -389,10 +399,7 @@ class ShardedDOTagCache implements NextModeTagCache {
389
399
await stub . writeTags ( tags , lastModified ) ;
390
400
// Depending on the shards and the tags, deleting from the regional cache will not work for every tag
391
401
// We also need to delete both cache
392
- await Promise . all ( [
393
- this . deleteRegionalCache ( { doId, tags, type : "boolean" } ) ,
394
- this . deleteRegionalCache ( { doId, tags, type : "number" } ) ,
395
- ] ) ;
402
+ await Promise . all ( [ this . deleteRegionalCache ( { doId, tags } ) ] ) ;
396
403
} catch ( e ) {
397
404
error ( "Error while writing tags" , e ) ;
398
405
if ( retryNumber >= this . maxWriteRetries ) {
@@ -417,49 +424,86 @@ class ShardedDOTagCache implements NextModeTagCache {
417
424
return this . localCache ;
418
425
}
419
426
420
- getCacheUrlKey ( opts : CacheTagKeyOptions ) : string {
421
- const { doId, tags, type } = opts ;
422
- return `http://local.cache/shard/${ doId . shardId } ?type=${ type } &tags=${ encodeURIComponent ( tags . join ( ";" ) ) } ` ;
427
+ getCacheUrlKey ( doId : DOId , tag : string ) {
428
+ return `http://local.cache/shard/${ doId . shardId } ?tag=${ encodeURIComponent ( tag ) } ` ;
423
429
}
424
430
431
+ /**
432
+ * Get the last revalidation time for the tags from the regional cache
433
+ * If the cache is not enabled, it will return an empty array
434
+ * @returns An array of objects with the tag and the last revalidation time
435
+ */
425
436
async getFromRegionalCache ( opts : CacheTagKeyOptions ) {
426
437
try {
427
- if ( ! this . opts . regionalCache ) return ;
438
+ if ( ! this . opts . regionalCache ) return [ ] ;
428
439
const cache = await this . getCacheInstance ( ) ;
429
- if ( ! cache ) return ;
430
- return cache . match ( this . getCacheUrlKey ( opts ) ) ;
440
+ if ( ! cache ) return [ ] ;
441
+ const result = await Promise . all (
442
+ opts . tags . map ( async ( tag ) => {
443
+ const cachedResponse = await cache . match ( this . getCacheUrlKey ( opts . doId , tag ) ) ;
444
+ if ( ! cachedResponse ) return null ;
445
+ const cachedText = await cachedResponse . text ( ) ;
446
+ try {
447
+ return { tag, time : parseInt ( cachedText , 10 ) } ;
448
+ } catch ( e ) {
449
+ debugCache ( "Error while parsing cached value" , e ) ;
450
+ return null ;
451
+ }
452
+ } )
453
+ ) ;
454
+ return result . filter ( ( item ) => item !== null ) ;
431
455
} catch ( e ) {
432
456
error ( "Error while fetching from regional cache" , e ) ;
457
+ return [ ] ;
433
458
}
434
459
}
435
-
436
- async putToRegionalCache ( optsKey : CacheTagKeyOptions , value : number | boolean ) {
460
+ async putToRegionalCache ( optsKey : CacheTagKeyOptions , stub : DurableObjectStub < DOShardedTagCache > ) {
437
461
if ( ! this . opts . regionalCache ) return ;
438
462
const cache = await this . getCacheInstance ( ) ;
439
463
if ( ! cache ) return ;
440
464
const tags = optsKey . tags ;
441
- await cache . put (
442
- this . getCacheUrlKey ( optsKey ) ,
443
- new Response ( `${ value } ` , {
444
- headers : {
445
- "cache-control" : `max-age=${ this . opts . regionalCacheTtlSec ?? 5 } ` ,
446
- ...( tags . length > 0
447
- ? {
448
- "cache-tag" : tags . join ( "," ) ,
449
- }
450
- : { } ) ,
451
- } ,
465
+ const tagsLastRevalidated = await stub . getRevalidationTimes ( tags ) ;
466
+ await Promise . all (
467
+ tags . map ( async ( tag ) => {
468
+ const lastRevalidated = tagsLastRevalidated [ tag ] ;
469
+ if ( lastRevalidated === undefined ) return ; // Should we store something in the cache if the tag is not found ?
470
+ const cacheKey = this . getCacheUrlKey ( optsKey . doId , tag ) ;
471
+ debugCache ( "Putting to regional cache" , { cacheKey, lastRevalidated } ) ;
472
+ await cache . put (
473
+ cacheKey ,
474
+ new Response ( lastRevalidated . toString ( ) , {
475
+ status : 200 ,
476
+ headers : {
477
+ "cache-control" : `max-age=${ this . opts . regionalCacheTtlSec ?? 5 } ` ,
478
+ ...( tags . length > 0
479
+ ? {
480
+ "cache-tag" : tags . join ( "," ) ,
481
+ }
482
+ : { } ) ,
483
+ } ,
484
+ } )
485
+ ) ;
452
486
} )
453
487
) ;
454
488
}
455
489
490
+ /**
491
+ * Deletes the regional cache for the given tags
492
+ * This is used to ensure that the cache is cleared when the tags are revalidated
493
+ */
456
494
async deleteRegionalCache ( optsKey : CacheTagKeyOptions ) {
457
495
// We never want to crash because of the cache
458
496
try {
459
497
if ( ! this . opts . regionalCache ) return ;
460
498
const cache = await this . getCacheInstance ( ) ;
461
499
if ( ! cache ) return ;
462
- await cache . delete ( this . getCacheUrlKey ( optsKey ) ) ;
500
+ await Promise . all (
501
+ optsKey . tags . map ( async ( tag ) => {
502
+ const cacheKey = this . getCacheUrlKey ( optsKey . doId , tag ) ;
503
+ debugCache ( "Deleting from regional cache" , { cacheKey } ) ;
504
+ await cache . delete ( cacheKey ) ;
505
+ } )
506
+ ) ;
463
507
} catch ( e ) {
464
508
debugCache ( "Error while deleting from regional cache" , e ) ;
465
509
}
0 commit comments