@@ -56,12 +56,13 @@ export type CreateRedisStringsHandlerOptions = {
5656 estimateExpireAge ?: ( staleAge : number ) => number ;
5757} ;
5858
59+ // Identifier prefix used by Next.js to mark automatically generated cache tags
60+ // These tags are created internally by Next.js for route-based invalidation
5961const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_' ;
60- const REVALIDATED_TAGS_KEY = '__revalidated_tags__' ;
6162
62- function isImplicitTag ( tag : string ) : boolean {
63- return tag . startsWith ( NEXT_CACHE_IMPLICIT_TAG_ID ) ;
64- }
63+ // Redis key used to store a map of tags and their last revalidation timestamps
64+ // This helps track when specific tags were last invalidated
65+ const REVALIDATED_TAGS_KEY = '__revalidated_tags__' ;
6566
6667export function getTimeoutRedisCommandOptions (
6768 timeoutMs : number ,
@@ -238,7 +239,7 @@ export default class RedisStringsHandler {
238239 ) ;
239240 }
240241
241- debug ( 'RedisStringsHandler.get() called with' , key , ctx ) ;
242+ debug ( 'green' , ' RedisStringsHandler.get() called with', key , ctx ) ;
242243 await this . assertClientIsReady ( ) ;
243244
244245 const clientGet = this . redisGetDeduplication
@@ -250,6 +251,7 @@ export default class RedisStringsHandler {
250251 ) ;
251252
252253 debug (
254+ 'green' ,
253255 'RedisStringsHandler.get() finished with result (serializedCacheEntry)' ,
254256 serializedCacheEntry ?. substring ( 0 , 200 ) ,
255257 ) ;
@@ -264,6 +266,7 @@ export default class RedisStringsHandler {
264266 ) ;
265267
266268 debug (
269+ 'green' ,
267270 'RedisStringsHandler.get() finished with result (cacheEntry)' ,
268271 JSON . stringify ( cacheEntry ) . substring ( 0 , 200 ) ,
269272 ) ;
@@ -307,15 +310,24 @@ export default class RedisStringsHandler {
307310 return cacheEntry ;
308311 }
309312
310- // TODO: check how this revalidatedTagsMap is used or if it can be deleted
313+ // This code checks if any of the cache tags associated with this entry have been revalidated
314+ // since the entry was last modified. If any tag was revalidated more recently than the entry's
315+ // lastModified timestamp, then the cached content is considered stale and should be removed.
311316 for ( const tag of combinedTags ) {
317+ // Get the last revalidation time for this tag from our revalidatedTagsMap
312318 const revalidationTime = this . revalidatedTagsMap . get ( tag ) ;
319+
320+ // If we have a revalidation time for this tag and it's more recent than when
321+ // this cache entry was last modified, the entry is stale
313322 if ( revalidationTime && revalidationTime > cacheEntry . lastModified ) {
314323 const redisKey = this . keyPrefix + key ;
315- // Do not await here as this can happen in the background while we can already serve the cacheValue
324+
325+ // We don't await this cleanup since it can happen asynchronously in the background.
326+ // The cache entry is already considered invalid at this point.
316327 this . client
317328 . unlink ( getTimeoutRedisCommandOptions ( this . timeoutMs ) , redisKey )
318329 . catch ( ( err ) => {
330+ // If the first unlink fails, log the error and try one more time
319331 console . error (
320332 'Error occurred while unlinking stale data. Retrying now. Error was:' ,
321333 err ,
@@ -326,9 +338,21 @@ export default class RedisStringsHandler {
326338 ) ;
327339 } )
328340 . finally ( async ( ) => {
341+ // Clean up our tag tracking maps after the Redis key is removed
329342 await this . sharedTagsMap . delete ( key ) ;
330343 await this . revalidatedTagsMap . delete ( tag ) ;
331344 } ) ;
345+
346+ debug (
347+ 'green' ,
348+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.' ,
349+ tag ,
350+ redisKey ,
351+ revalidationTime ,
352+ cacheEntry ,
353+ ) ;
354+
355+ // Return null to indicate no valid cache entry was found
332356 return null ;
333357 }
334358 }
@@ -384,7 +408,7 @@ export default class RedisStringsHandler {
384408 data . kind !== 'FETCH'
385409 ) {
386410 console . warn (
387- 'RedisStringsHandler.get () called with' ,
411+ 'RedisStringsHandler.set () called with' ,
388412 key ,
389413 ctx ,
390414 data ,
@@ -402,15 +426,11 @@ export default class RedisStringsHandler {
402426
403427 // Constructing and serializing the value for storing it in redis
404428 const cacheEntry : CacheEntry = {
405- value : data ,
406429 lastModified : Date . now ( ) ,
407430 tags : ctx ?. tags || [ ] ,
431+ value : data ,
408432 } ;
409433 const serializedCacheEntry = JSON . stringify ( cacheEntry , bufferReplacer ) ;
410- debug (
411- 'RedisStringsHandler.set() will set the following serializedCacheEntry' ,
412- serializedCacheEntry ?. substring ( 0 , 200 ) ,
413- ) ;
414434
415435 // pre seed data into deduplicated get client. This will reduce redis load by not requesting
416436 // the same value from redis which was just set.
@@ -440,6 +460,15 @@ export default class RedisStringsHandler {
440460 } ,
441461 ) ;
442462
463+ debug (
464+ 'blue' ,
465+ 'RedisStringsHandler.set() will set the following serializedCacheEntry' ,
466+ this . keyPrefix ,
467+ key ,
468+ serializedCacheEntry ?. substring ( 0 , 200 ) ,
469+ expireAt ,
470+ ) ;
471+
443472 // Setting the tags for the cache entry in the sharedTagsMap (locally stored hashmap synced via redis)
444473 let setTagsOperation : Promise < void > | undefined ;
445474 if ( ctx . tags && ctx . tags . length > 0 ) {
@@ -457,44 +486,102 @@ export default class RedisStringsHandler {
457486 }
458487 }
459488
489+ debug (
490+ 'blue' ,
491+ 'RedisStringsHandler.set() will set the following sharedTagsMap' ,
492+ key ,
493+ ctx . tags as string [ ] ,
494+ ) ;
495+
460496 await Promise . all ( [ setOperation , setTagsOperation ] ) ;
461497 }
462498
463499 // eslint-disable-next-line @typescript-eslint/no-explicit-any
464500 public async revalidateTag ( tagOrTags : string | string [ ] , ...rest : any [ ] ) {
465- debug ( 'RedisStringsHandler.revalidateTag() called with' , tagOrTags , rest ) ;
501+ debug (
502+ 'red' ,
503+ 'RedisStringsHandler.revalidateTag() called with' ,
504+ tagOrTags ,
505+ rest ,
506+ ) ;
466507 const tags = new Set ( [ tagOrTags || [ ] ] . flat ( ) ) ;
467508 await this . assertClientIsReady ( ) ;
468509
469- // TODO: check how this revalidatedTagsMap is used or if it can be deleted
510+ // find all keys that are related to this tag
511+ const keysToDelete : Set < string > = new Set ( ) ;
512+
513+ // TODO right now this code is only tested for calls with revalidatePath. We need to test this code for calls with revalidateTag as well
514+ // a call to revalidatePath will result in revalidateTag(_N_T_...) -> therefore we could check of tagOrTags.startsWith(_N_T_) to only execute this code for revalidatePath calls
515+
470516 for ( const tag of tags ) {
471- if ( isImplicitTag ( tag ) ) {
517+ // If a page has a fetch request inside. This fetch request needs to be revalidated as well. This is done by the following code
518+ // sharedTags are containing all directly dependent tags. Need to find out the keys for these tags
519+ const sharedTags = this . sharedTagsMap . get (
520+ tag . replace ( NEXT_CACHE_IMPLICIT_TAG_ID , '' ) ,
521+ ) ;
522+ for ( const sharedTag of sharedTags || [ ] ) {
523+ // Implicit tags are not stored in cache therefore we can ignore them and only look at the non-implicit tags
524+ if ( ! sharedTag . startsWith ( NEXT_CACHE_IMPLICIT_TAG_ID ) ) {
525+ // For these non-implicit tags we then need to find the keys that are dependent on each of these tags
526+ for ( const [
527+ dependentKey ,
528+ sharedTagsForDependentKey ,
529+ ] of this . sharedTagsMap . entries ( ) ) {
530+ // We can do so by scanning the whole sharedTagsMap for keys that contain this tag in there sharedTags array
531+ if ( sharedTagsForDependentKey . includes ( sharedTag ) ) {
532+ keysToDelete . add ( dependentKey ) ;
533+ }
534+ }
535+ }
536+ }
537+
538+ debug (
539+ 'red' ,
540+ 'RedisStringsHandler.revalidateTag() directly dependent keys' ,
541+ tag ,
542+ sharedTags ?. filter (
543+ ( tag ) => ! tag . startsWith ( NEXT_CACHE_IMPLICIT_TAG_ID ) ,
544+ ) || [ ] ,
545+ ) ;
546+
547+ // TODO check if this can be deleted with the new logic --> I think yes but tests should be added first
548+ // For Next.js implicit tags (route-based), store the revalidation timestamp
549+ // This is used to track when routes were last invalidated
550+ if ( tag . startsWith ( NEXT_CACHE_IMPLICIT_TAG_ID ) ) {
472551 const now = Date . now ( ) ;
552+ debug (
553+ 'red' ,
554+ 'RedisStringsHandler.revalidateTag() set revalidation time for tag' ,
555+ tag ,
556+ 'to' ,
557+ now ,
558+ ) ;
473559 await this . revalidatedTagsMap . set ( tag , now ) ;
474560 }
475561 }
476562
477- // find all keys that are related to this tag
478- const keysToDelete : string [ ] = [ ] ;
563+ // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags
479564 for ( const [ key , sharedTags ] of this . sharedTagsMap . entries ( ) ) {
480565 if ( sharedTags . some ( ( tag ) => tags . has ( tag ) ) ) {
481- keysToDelete . push ( key ) ;
566+ keysToDelete . add ( key ) ;
482567 }
483568 }
484569
485570 debug (
571+ 'red' ,
486572 'RedisStringsHandler.revalidateTag() found' ,
487573 keysToDelete ,
488574 'keys to delete' ,
489575 ) ;
490576
491577 // exit early if no keys are related to this tag
492- if ( keysToDelete . length === 0 ) {
578+ if ( keysToDelete . size === 0 ) {
493579 return ;
494580 }
495581
496582 // prepare deletion of all keys in redis that are related to this tag
497- const fullRedisKeys = keysToDelete . map ( ( key ) => this . keyPrefix + key ) ;
583+ const redisKeys = Array . from ( keysToDelete ) ;
584+ const fullRedisKeys = redisKeys . map ( ( key ) => this . keyPrefix + key ) ;
498585 const options = getTimeoutRedisCommandOptions ( this . timeoutMs ) ;
499586 const deleteKeysOperation = this . client . unlink ( options , fullRedisKeys ) ;
500587
@@ -506,10 +593,13 @@ export default class RedisStringsHandler {
506593 }
507594
508595 // prepare deletion of entries from shared tags map if they get revalidated so that the map will not grow indefinitely
509- const deleteTagsOperation = this . sharedTagsMap . delete ( keysToDelete ) ;
596+ const deleteTagsOperation = this . sharedTagsMap . delete ( redisKeys ) ;
510597
511598 // execute keys and tag maps deletion
512599 await Promise . all ( [ deleteKeysOperation , deleteTagsOperation ] ) ;
513- debug ( 'RedisStringsHandler.revalidateTag() finished delete operations' ) ;
600+ debug (
601+ 'red' ,
602+ 'RedisStringsHandler.revalidateTag() finished delete operations' ,
603+ ) ;
514604 }
515605}
0 commit comments