@@ -60,6 +60,10 @@ export type CreateRedisStringsHandlerOptions = {
6060// These tags are created internally by Next.js for route-based invalidation
6161const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_' ;
6262
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__' ;
66+
6367export function getTimeoutRedisCommandOptions (
6468 timeoutMs : number ,
6569) : CommandOptions {
@@ -69,6 +73,7 @@ export function getTimeoutRedisCommandOptions(
6973export default class RedisStringsHandler {
7074 private client : Client ;
7175 private sharedTagsMap : SyncedMap < string [ ] > ;
76+ private revalidatedTagsMap : SyncedMap < number > ;
7277 private inMemoryDeduplicationCache : SyncedMap <
7378 Promise < ReturnType < Client [ 'get' ] > >
7479 > ;
@@ -134,7 +139,8 @@ export default class RedisStringsHandler {
134139 throw error ;
135140 }
136141
137- const filterKeys = ( key : string ) : boolean => key !== sharedTagsKey ;
142+ const filterKeys = ( key : string ) : boolean =>
143+ key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey ;
138144
139145 this . sharedTagsMap = new SyncedMap < string [ ] > ( {
140146 client : this . client ,
@@ -150,6 +156,20 @@ export default class RedisStringsHandler {
150156 Math . random ( ) * ( avgResyncIntervalMs / 10 ) ,
151157 } ) ;
152158
159+ this . revalidatedTagsMap = new SyncedMap < number > ( {
160+ client : this . client ,
161+ keyPrefix,
162+ redisKey : REVALIDATED_TAGS_KEY ,
163+ database,
164+ timeoutMs,
165+ querySize : revalidateTagQuerySize ,
166+ filterKeys,
167+ resyncIntervalMs :
168+ avgResyncIntervalMs +
169+ avgResyncIntervalMs / 10 +
170+ Math . random ( ) * ( avgResyncIntervalMs / 10 ) ,
171+ } ) ;
172+
153173 this . inMemoryDeduplicationCache = new SyncedMap ( {
154174 client : this . client ,
155175 keyPrefix,
@@ -178,7 +198,10 @@ export default class RedisStringsHandler {
178198 resetRequestCache ( ) : void { }
179199
180200 private async assertClientIsReady ( ) : Promise < void > {
181- await Promise . all ( [ this . sharedTagsMap . waitUntilReady ( ) ] ) ;
201+ await Promise . all ( [
202+ this . sharedTagsMap . waitUntilReady ( ) ,
203+ this . revalidatedTagsMap . waitUntilReady ( ) ,
204+ ] ) ;
182205 if ( ! this . client . isReady ) {
183206 throw new Error ( 'Redis client is not ready yet or connection is lost.' ) ;
184207 }
@@ -287,7 +310,52 @@ export default class RedisStringsHandler {
287310 return cacheEntry ;
288311 }
289312
290- return cacheEntry ;
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.
316+ for ( const tag of combinedTags ) {
317+ // Get the last revalidation time for this tag from our revalidatedTagsMap
318+ 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
322+ if ( revalidationTime && revalidationTime > cacheEntry . lastModified ) {
323+ const redisKey = this . keyPrefix + key ;
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.
327+ this . client
328+ . unlink ( getTimeoutRedisCommandOptions ( this . timeoutMs ) , redisKey )
329+ . catch ( ( err ) => {
330+ // If the first unlink fails, log the error and try one more time
331+ console . error (
332+ 'Error occurred while unlinking stale data. Retrying now. Error was:' ,
333+ err ,
334+ ) ;
335+ this . client . unlink (
336+ getTimeoutRedisCommandOptions ( this . timeoutMs ) ,
337+ redisKey ,
338+ ) ;
339+ } )
340+ . finally ( async ( ) => {
341+ // Clean up our tag tracking maps after the Redis key is removed
342+ await this . sharedTagsMap . delete ( key ) ;
343+ await this . revalidatedTagsMap . delete ( tag ) ;
344+ } ) ;
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
356+ return null ;
357+ }
358+ }
291359 }
292360
293361 return cacheEntry ;
@@ -475,6 +543,21 @@ export default class RedisStringsHandler {
475543 ( tag ) => ! tag . startsWith ( NEXT_CACHE_IMPLICIT_TAG_ID ) ,
476544 ) || [ ] ,
477545 ) ;
546+
547+ // TODO check if this can be deleted with the new logic --> Seems like it can not be deleted --> above implementation only works for pages and not for api routes
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 ) ) {
551+ const now = Date . now ( ) ;
552+ debug (
553+ 'red' ,
554+ 'RedisStringsHandler.revalidateTag() set revalidation time for tag' ,
555+ tag ,
556+ 'to' ,
557+ now ,
558+ ) ;
559+ await this . revalidatedTagsMap . set ( tag , now ) ;
560+ }
478561 }
479562
480563 // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags
0 commit comments