Skip to content

Commit cb6d36d

Browse files
committed
fix: tests + re-add old implementation
1 parent dbd9141 commit cb6d36d

File tree

3 files changed

+88
-6
lines changed

3 files changed

+88
-6
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ jobs:
6464
run: cd test/integration/next-app && pnpm build
6565

6666
- name: Run tests
67-
run: DEBUG_INTEGRATION=true DEBUG_CACHE_HANDLER=true pnpm test:integration -t DEBUG
67+
run: pnpm test
6868
env:
6969
SKIP_BUILD: true
7070

src/RedisStringsHandler.ts

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ export type CreateRedisStringsHandlerOptions = {
6060
// These tags are created internally by Next.js for route-based invalidation
6161
const 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+
6367
export function getTimeoutRedisCommandOptions(
6468
timeoutMs: number,
6569
): CommandOptions {
@@ -69,6 +73,7 @@ export function getTimeoutRedisCommandOptions(
6973
export 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

test/integration/nextjs-cache-handler.integration.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
328328
});
329329
});
330330

331-
describe('DEBUG should cache a nested fetch request inside a uncached API route', () => {
331+
describe('should cache a nested fetch request inside a uncached API route', () => {
332332
describe('should cache the nested fetch request (but not the API route itself)', () => {
333333
let counter: number;
334334
let subCounter: number;
@@ -435,7 +435,6 @@ describe('Next.js Turbo Redis Cache Integration', () => {
435435
});
436436

437437
it('A new request after the revalidation should increment the counter (because the route was re-evaluated)', async () => {
438-
await delay(2000);
439438
const res = await fetch(
440439
NEXT_START_URL +
441440
'/api/nested-fetch-in-api-route/revalidated-fetch',

0 commit comments

Comments
 (0)