@@ -5,6 +5,7 @@ import { IgnorableError } from "@opennextjs/aws/utils/error.js";
55
66import type { OpenNextConfig } from "../../../api/config.js" ;
77import { getCloudflareContext } from "../../cloudflare-context" ;
8+ import { DOShardedTagCache } from "../../durable-objects/sharded-tag-cache.js" ;
89import { debugCache , purgeCacheByTags } from "../internal" ;
910
1011export const DEFAULT_WRITE_RETRIES = 3 ;
@@ -120,7 +121,6 @@ export class DOId {
120121interface CacheTagKeyOptions {
121122 doId : DOId ;
122123 tags : string [ ] ;
123- type : "boolean" | "number" ;
124124}
125125class ShardedDOTagCache implements NextModeTagCache {
126126 readonly mode = "nextMode" as const ;
@@ -294,28 +294,31 @@ class ShardedDOTagCache implements NextModeTagCache {
294294 async getLastRevalidated ( tags : string [ ] ) : Promise < number > {
295295 const { isDisabled } = await this . getConfig ( ) ;
296296 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
297299 try {
298- const shardedTagGroups = this . groupTagsByDO ( { tags } ) ;
300+ const shardedTagGroups = this . groupTagsByDO ( { tags : deduplicatedTags } ) ;
299301 const shardedTagRevalidationOutcomes = await Promise . all (
300302 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 ) ) ;
310307 }
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+
311313 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 ;
319322 } )
320323 ) ;
321324 return Math . max ( ...shardedTagRevalidationOutcomes ) ;
@@ -339,20 +342,27 @@ class ShardedDOTagCache implements NextModeTagCache {
339342 const shardedTagGroups = this . groupTagsByDO ( { tags } ) ;
340343 const shardedTagRevalidationOutcomes = await Promise . all (
341344 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 ;
345354 }
346355 const stub = this . getDurableObjectStub ( doId ) ;
347356 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
352361 getCloudflareContext ( ) . ctx . waitUntil (
353- this . putToRegionalCache ( { doId, tags, type : "boolean" } , _hasBeenRevalidated )
362+ this . putToRegionalCache ( { doId, tags : remainingTags } , stub )
354363 ) ;
355364 }
365+
356366 return _hasBeenRevalidated ;
357367 } )
358368 ) ;
@@ -389,10 +399,7 @@ class ShardedDOTagCache implements NextModeTagCache {
389399 await stub . writeTags ( tags , lastModified ) ;
390400 // Depending on the shards and the tags, deleting from the regional cache will not work for every tag
391401 // 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 } ) ] ) ;
396403 } catch ( e ) {
397404 error ( "Error while writing tags" , e ) ;
398405 if ( retryNumber >= this . maxWriteRetries ) {
@@ -417,49 +424,86 @@ class ShardedDOTagCache implements NextModeTagCache {
417424 return this . localCache ;
418425 }
419426
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 ) } ` ;
423429 }
424430
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+ */
425436 async getFromRegionalCache ( opts : CacheTagKeyOptions ) {
426437 try {
427- if ( ! this . opts . regionalCache ) return ;
438+ if ( ! this . opts . regionalCache ) return [ ] ;
428439 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 ) ;
431455 } catch ( e ) {
432456 error ( "Error while fetching from regional cache" , e ) ;
457+ return [ ] ;
433458 }
434459 }
435-
436- async putToRegionalCache ( optsKey : CacheTagKeyOptions , value : number | boolean ) {
460+ async putToRegionalCache ( optsKey : CacheTagKeyOptions , stub : DurableObjectStub < DOShardedTagCache > ) {
437461 if ( ! this . opts . regionalCache ) return ;
438462 const cache = await this . getCacheInstance ( ) ;
439463 if ( ! cache ) return ;
440464 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+ ) ;
452486 } )
453487 ) ;
454488 }
455489
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+ */
456494 async deleteRegionalCache ( optsKey : CacheTagKeyOptions ) {
457495 // We never want to crash because of the cache
458496 try {
459497 if ( ! this . opts . regionalCache ) return ;
460498 const cache = await this . getCacheInstance ( ) ;
461499 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+ ) ;
463507 } catch ( e ) {
464508 debugCache ( "Error while deleting from regional cache" , e ) ;
465509 }
0 commit comments