@trieb.work/nextjs-turbo-redis-cache 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.github/workflows/ci.yml +30 -6
  2. package/.github/workflows/release.yml +6 -3
  3. package/.next/trace +11 -0
  4. package/.vscode/settings.json +10 -0
  5. package/CHANGELOG.md +61 -0
  6. package/README.md +149 -34
  7. package/dist/index.d.mts +92 -20
  8. package/dist/index.d.ts +92 -20
  9. package/dist/index.js +319 -60
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +315 -60
  12. package/dist/index.mjs.map +1 -1
  13. package/package.json +14 -9
  14. package/scripts/vitest-run-staged.cjs +1 -1
  15. package/src/CachedHandler.ts +23 -9
  16. package/src/DeduplicatedRequestHandler.ts +50 -1
  17. package/src/RedisStringsHandler.ts +330 -89
  18. package/src/SyncedMap.ts +74 -4
  19. package/src/index.ts +4 -2
  20. package/src/utils/debug.ts +30 -0
  21. package/src/utils/json.ts +26 -0
  22. package/test/integration/next-app/README.md +36 -0
  23. package/test/integration/next-app/eslint.config.mjs +16 -0
  24. package/test/integration/next-app/next.config.js +6 -0
  25. package/test/integration/next-app/package-lock.json +5833 -0
  26. package/test/integration/next-app/package.json +29 -0
  27. package/test/integration/next-app/pnpm-lock.yaml +3679 -0
  28. package/test/integration/next-app/postcss.config.mjs +5 -0
  29. package/test/integration/next-app/public/file.svg +1 -0
  30. package/test/integration/next-app/public/globe.svg +1 -0
  31. package/test/integration/next-app/public/next.svg +1 -0
  32. package/test/integration/next-app/public/vercel.svg +1 -0
  33. package/test/integration/next-app/public/window.svg +1 -0
  34. package/test/integration/next-app/src/app/api/cached-static-fetch/route.ts +18 -0
  35. package/test/integration/next-app/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  36. package/test/integration/next-app/src/app/api/revalidatePath/route.ts +15 -0
  37. package/test/integration/next-app/src/app/api/revalidateTag/route.ts +15 -0
  38. package/test/integration/next-app/src/app/api/revalidated-fetch/route.ts +17 -0
  39. package/test/integration/next-app/src/app/api/uncached-fetch/route.ts +15 -0
  40. package/test/integration/next-app/src/app/globals.css +26 -0
  41. package/test/integration/next-app/src/app/layout.tsx +59 -0
  42. package/test/integration/next-app/src/app/page.tsx +755 -0
  43. package/test/integration/next-app/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  44. package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  45. package/test/integration/next-app/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  46. package/test/integration/next-app/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  47. package/test/integration/next-app/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  48. package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  49. package/test/integration/next-app/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  50. package/test/integration/next-app/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  51. package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  52. package/test/integration/next-app/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  53. package/test/integration/next-app/src/app/revalidation-interface.tsx +267 -0
  54. package/test/integration/next-app/tsconfig.json +27 -0
  55. package/test/integration/next-app-customized/README.md +36 -0
  56. package/test/integration/next-app-customized/customized-cache-handler.js +34 -0
  57. package/test/integration/next-app-customized/eslint.config.mjs +16 -0
  58. package/test/integration/next-app-customized/next.config.js +6 -0
  59. package/test/integration/nextjs-cache-handler.integration.test.ts +840 -0
  60. package/vite.config.ts +23 -8
@@ -1,37 +1,68 @@
1
1
  import { commandOptions, createClient } from 'redis';
2
2
  import { SyncedMap } from './SyncedMap';
3
3
  import { DeduplicatedRequestHandler } from './DeduplicatedRequestHandler';
4
- import {
5
- CacheHandler,
6
- CacheHandlerValue,
7
- IncrementalCache,
8
- } from 'next/dist/server/lib/incremental-cache';
4
+ import { debug } from './utils/debug';
5
+ import { bufferReviver, bufferReplacer } from './utils/json';
9
6
 
10
7
  export type CommandOptions = ReturnType<typeof commandOptions>;
11
- type GetParams = Parameters<IncrementalCache['get']>;
12
- type SetParams = Parameters<IncrementalCache['set']>;
13
- type RevalidateParams = Parameters<IncrementalCache['revalidateTag']>;
14
8
  export type Client = ReturnType<typeof createClient>;
15
9
 
10
+ export type CacheEntry = {
11
+ value: unknown;
12
+ lastModified: number;
13
+ tags: string[];
14
+ };
15
+
16
16
  export type CreateRedisStringsHandlerOptions = {
17
+ /** Redis database number to use. Uses DB 0 for production, DB 1 otherwise
18
+ * @default process.env.VERCEL_ENV === 'production' ? 0 : 1
19
+ */
17
20
  database?: number;
21
+ /** Prefix added to all Redis keys
22
+ * @default process.env.VERCEL_URL || 'UNDEFINED_URL_'
23
+ */
18
24
  keyPrefix?: string;
25
+ /** Timeout in milliseconds for Redis operations
26
+ * @default 5000
27
+ */
19
28
  timeoutMs?: number;
29
+ /** Number of entries to query in one batch during full sync of shared tags hash map
30
+ * @default 250
31
+ */
20
32
  revalidateTagQuerySize?: number;
33
+ /** Key used to store shared tags hash map in Redis
34
+ * @default '__sharedTags__'
35
+ */
21
36
  sharedTagsKey?: string;
37
+ /** Average interval in milliseconds between tag map full re-syncs
38
+ * @default 3600000 (1 hour)
39
+ */
22
40
  avgResyncIntervalMs?: number;
41
+ /** Enable deduplication of Redis get requests via internal in-memory cache
42
+ * @default true
43
+ */
23
44
  redisGetDeduplication?: boolean;
45
+ /** Time in milliseconds to cache Redis get results in memory. Set this to 0 to disable in-memory caching completely
46
+ * @default 10000
47
+ */
24
48
  inMemoryCachingTime?: number;
49
+ /** Default stale age in seconds for cached items
50
+ * @default 1209600 (14 days)
51
+ */
25
52
  defaultStaleAge?: number;
53
+ /** Function to calculate expire age (redis TTL value) from stale age
54
+ * @default Production: staleAge * 2, Other: staleAge * 1.2
55
+ */
26
56
  estimateExpireAge?: (staleAge: number) => number;
27
57
  };
28
58
 
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
29
61
  const NEXT_CACHE_IMPLICIT_TAG_ID = '_N_T_';
30
- const REVALIDATED_TAGS_KEY = '__revalidated_tags__';
31
62
 
32
- function isImplicitTag(tag: string): boolean {
33
- return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID);
34
- }
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__';
35
66
 
36
67
  export function getTimeoutRedisCommandOptions(
37
68
  timeoutMs: number,
@@ -39,7 +70,7 @@ export function getTimeoutRedisCommandOptions(
39
70
  return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
40
71
  }
41
72
 
42
- export default class RedisStringsHandler implements CacheHandler {
73
+ export default class RedisStringsHandler {
43
74
  private client: Client;
44
75
  private sharedTagsMap: SyncedMap<string[]>;
45
76
  private revalidatedTagsMap: SyncedMap<number>;
@@ -63,14 +94,14 @@ export default class RedisStringsHandler implements CacheHandler {
63
94
  database = process.env.VERCEL_ENV === 'production' ? 0 : 1,
64
95
  keyPrefix = process.env.VERCEL_URL || 'UNDEFINED_URL_',
65
96
  sharedTagsKey = '__sharedTags__',
66
- timeoutMs = 5000,
97
+ timeoutMs = 5_000,
67
98
  revalidateTagQuerySize = 250,
68
- avgResyncIntervalMs = 60 * 60 * 1000,
99
+ avgResyncIntervalMs = 60 * 60 * 1_000,
69
100
  redisGetDeduplication = true,
70
101
  inMemoryCachingTime = 10_000,
71
102
  defaultStaleAge = 60 * 60 * 24 * 14,
72
103
  estimateExpireAge = (staleAge) =>
73
- process.env.VERCEL_ENV === 'preview' ? staleAge * 1.2 : staleAge * 2,
104
+ process.env.VERCEL_ENV === 'production' ? staleAge * 2 : staleAge * 1.2,
74
105
  }: CreateRedisStringsHandlerOptions) {
75
106
  this.keyPrefix = keyPrefix;
76
107
  this.timeoutMs = timeoutMs;
@@ -96,9 +127,12 @@ export default class RedisStringsHandler implements CacheHandler {
96
127
  .then(() => {
97
128
  console.info('Redis client connected.');
98
129
  })
99
- .catch((error) => {
100
- console.error('Failed to connect Redis client:', error);
101
- this.client.disconnect();
130
+ .catch(() => {
131
+ this.client.connect().catch((error) => {
132
+ console.error('Failed to connect Redis client:', error);
133
+ this.client.disconnect();
134
+ throw error;
135
+ });
102
136
  });
103
137
  } catch (error: unknown) {
104
138
  console.error('Failed to initialize Redis client');
@@ -160,9 +194,8 @@ export default class RedisStringsHandler implements CacheHandler {
160
194
  this.deduplicatedRedisGet =
161
195
  this.redisDeduplicationHandler.deduplicatedFunction;
162
196
  }
163
- resetRequestCache(...args: never[]): void {
164
- console.warn('WARNING resetRequestCache() was called', args);
165
- }
197
+
198
+ resetRequestCache(): void { }
166
199
 
167
200
  private async assertClientIsReady(): Promise<void> {
168
201
  await Promise.all([
@@ -174,110 +207,273 @@ export default class RedisStringsHandler implements CacheHandler {
174
207
  }
175
208
  }
176
209
 
177
- public async get(key: GetParams[0], ctx: GetParams[1]) {
210
+ public async get(
211
+ key: string,
212
+ ctx:
213
+ | {
214
+ kind: 'APP_ROUTE' | 'APP_PAGE';
215
+ isRoutePPREnabled: boolean;
216
+ isFallback: boolean;
217
+ }
218
+ | {
219
+ kind: 'FETCH';
220
+ revalidate: number;
221
+ fetchUrl: string;
222
+ fetchIdx: number;
223
+ tags: string[];
224
+ softTags: string[];
225
+ isFallback: boolean;
226
+ },
227
+ ): Promise<CacheEntry | null> {
228
+ if (
229
+ ctx.kind !== 'APP_ROUTE' &&
230
+ ctx.kind !== 'APP_PAGE' &&
231
+ ctx.kind !== 'FETCH'
232
+ ) {
233
+ console.warn(
234
+ 'RedisStringsHandler.get() called with',
235
+ key,
236
+ ctx,
237
+ ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ',
238
+ (ctx as { kind: string })?.kind,
239
+ );
240
+ }
241
+
242
+ debug('green', 'RedisStringsHandler.get() called with', key, ctx);
178
243
  await this.assertClientIsReady();
179
244
 
180
245
  const clientGet = this.redisGetDeduplication
181
246
  ? this.deduplicatedRedisGet(key)
182
247
  : this.redisGet;
183
- const result = await clientGet(
248
+ const serializedCacheEntry = await clientGet(
184
249
  getTimeoutRedisCommandOptions(this.timeoutMs),
185
250
  this.keyPrefix + key,
186
251
  );
187
252
 
188
- if (!result) {
253
+ debug(
254
+ 'green',
255
+ 'RedisStringsHandler.get() finished with result (serializedCacheEntry)',
256
+ serializedCacheEntry?.substring(0, 200),
257
+ );
258
+
259
+ if (!serializedCacheEntry) {
189
260
  return null;
190
261
  }
191
262
 
192
- const cacheValue = JSON.parse(result) as
193
- | (CacheHandlerValue & { lastModified: number })
194
- | null;
263
+ const cacheEntry: CacheEntry | null = JSON.parse(
264
+ serializedCacheEntry,
265
+ bufferReviver,
266
+ );
267
+
268
+ debug(
269
+ 'green',
270
+ 'RedisStringsHandler.get() finished with result (cacheEntry)',
271
+ JSON.stringify(cacheEntry).substring(0, 200),
272
+ );
195
273
 
196
- if (!cacheValue) {
274
+ if (!cacheEntry) {
197
275
  return null;
198
276
  }
199
277
 
200
- if (cacheValue.value?.kind === 'FETCH') {
201
- cacheValue.value.data.body = Buffer.from(
202
- cacheValue.value.data.body,
203
- ).toString('base64');
278
+ if (!cacheEntry?.tags) {
279
+ console.warn(
280
+ 'RedisStringsHandler.get() called with',
281
+ key,
282
+ ctx,
283
+ 'cacheEntry is mall formed (missing tags)',
284
+ );
285
+ }
286
+ if (!cacheEntry?.value) {
287
+ console.warn(
288
+ 'RedisStringsHandler.get() called with',
289
+ key,
290
+ ctx,
291
+ 'cacheEntry is mall formed (missing value)',
292
+ );
293
+ }
294
+ if (!cacheEntry?.lastModified) {
295
+ console.warn(
296
+ 'RedisStringsHandler.get() called with',
297
+ key,
298
+ ctx,
299
+ 'cacheEntry is mall formed (missing lastModified)',
300
+ );
204
301
  }
205
302
 
206
- const combinedTags = new Set([
207
- ...(ctx?.softTags || []),
208
- ...(ctx?.tags || []),
209
- ]);
303
+ if (ctx.kind === 'FETCH') {
304
+ const combinedTags = new Set([
305
+ ...(ctx?.softTags || []),
306
+ ...(ctx?.tags || []),
307
+ ]);
210
308
 
211
- if (combinedTags.size === 0) {
212
- return cacheValue;
213
- }
309
+ if (combinedTags.size === 0) {
310
+ return cacheEntry;
311
+ }
214
312
 
215
- for (const tag of combinedTags) {
216
- // TODO: check how this revalidatedTagsMap is used or if it can be deleted
217
- const revalidationTime = this.revalidatedTagsMap.get(tag);
218
- if (revalidationTime && revalidationTime > cacheValue.lastModified) {
219
- const redisKey = this.keyPrefix + key;
220
- // Do not await here as this can happen in the background while we can already serve the cacheValue
221
- this.client
222
- .unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey)
223
- .catch((err) => {
224
- console.error(
225
- 'Error occurred while unlinking stale data. Retrying now. Error was:',
226
- err,
227
- );
228
- this.client.unlink(
229
- getTimeoutRedisCommandOptions(this.timeoutMs),
230
- redisKey,
231
- );
232
- })
233
- .finally(async () => {
234
- await this.sharedTagsMap.delete(key);
235
- await this.revalidatedTagsMap.delete(tag);
236
- });
237
- return null;
313
+ // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route). See revalidateTag() for more information
314
+ //
315
+ // This code checks if any of the cache tags associated with this entry (normally the internal tag of the parent page/api route containing the fetch request)
316
+ // have been revalidated since the entry was last modified. If any tag was revalidated more recently than the entry's
317
+ // lastModified timestamp, then the cached content is considered stale (therefore return null) and should be removed.
318
+ for (const tag of combinedTags) {
319
+ // Get the last revalidation time for this tag from our revalidatedTagsMap
320
+ const revalidationTime = this.revalidatedTagsMap.get(tag);
321
+
322
+ // If we have a revalidation time for this tag and it's more recent than when
323
+ // this cache entry was last modified, the entry is stale
324
+ if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
325
+ const redisKey = this.keyPrefix + key;
326
+
327
+ // We don't await this cleanup since it can happen asynchronously in the background.
328
+ // The cache entry is already considered invalid at this point.
329
+ this.client
330
+ .unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey)
331
+ .catch((err) => {
332
+ // If the first unlink fails, log the error and try one more time
333
+ console.error(
334
+ 'Error occurred while unlinking stale data. Retrying now. Error was:',
335
+ err,
336
+ );
337
+ this.client.unlink(
338
+ getTimeoutRedisCommandOptions(this.timeoutMs),
339
+ redisKey,
340
+ );
341
+ })
342
+ .finally(async () => {
343
+ // Clean up our tag tracking maps after the Redis key is removed
344
+ await this.sharedTagsMap.delete(key);
345
+ await this.revalidatedTagsMap.delete(tag);
346
+ });
347
+
348
+ debug(
349
+ 'green',
350
+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
351
+ tag,
352
+ redisKey,
353
+ revalidationTime,
354
+ cacheEntry,
355
+ );
356
+
357
+ // Return null to indicate no valid cache entry was found
358
+ return null;
359
+ }
238
360
  }
239
361
  }
240
362
 
241
- return cacheValue;
363
+ return cacheEntry;
242
364
  }
243
365
  public async set(
244
- key: SetParams[0],
245
- data: SetParams[1] & { lastModified: number },
246
- ctx: SetParams[2],
366
+ key: string,
367
+ data:
368
+ | {
369
+ kind: 'APP_PAGE';
370
+ status: number;
371
+ headers: {
372
+ 'x-nextjs-stale-time': string; // timestamp in ms
373
+ 'x-next-cache-tags': string; // comma separated paths (tags)
374
+ };
375
+ html: string;
376
+ rscData: Buffer;
377
+ segmentData: unknown;
378
+ postboned: unknown;
379
+ }
380
+ | {
381
+ kind: 'APP_ROUTE';
382
+ status: number;
383
+ headers: {
384
+ 'cache-control'?: string;
385
+ 'x-nextjs-stale-time': string; // timestamp in ms
386
+ 'x-next-cache-tags': string; // comma separated paths (tags)
387
+ };
388
+ body: Buffer;
389
+ }
390
+ | {
391
+ kind: 'FETCH';
392
+ data: {
393
+ headers: Record<string, string>;
394
+ body: string; // base64 encoded
395
+ status: number;
396
+ url: string;
397
+ };
398
+ revalidate: number | false;
399
+ },
400
+ ctx: {
401
+ revalidate: number | false;
402
+ isRoutePPREnabled: boolean;
403
+ isFallback: boolean;
404
+ tags?: string[];
405
+ },
247
406
  ) {
248
- if (data.kind === 'FETCH') {
249
- console.time('encoding' + key);
250
- data.data.body = Buffer.from(data.data.body, 'base64').toString();
251
- console.timeEnd('encoding' + key);
407
+ if (
408
+ data.kind !== 'APP_ROUTE' &&
409
+ data.kind !== 'APP_PAGE' &&
410
+ data.kind !== 'FETCH'
411
+ ) {
412
+ console.warn(
413
+ 'RedisStringsHandler.set() called with',
414
+ key,
415
+ ctx,
416
+ data,
417
+ ' this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ',
418
+ (data as { kind: string })?.kind,
419
+ );
252
420
  }
421
+
253
422
  await this.assertClientIsReady();
254
423
 
255
- data.lastModified = Date.now();
424
+ if (data.kind === 'APP_PAGE' || data.kind === 'APP_ROUTE') {
425
+ const tags = data.headers['x-next-cache-tags']?.split(',');
426
+ ctx.tags = [...(ctx.tags || []), ...(tags || [])];
427
+ }
256
428
 
257
- const value = JSON.stringify(data);
429
+ // Constructing and serializing the value for storing it in redis
430
+ const cacheEntry: CacheEntry = {
431
+ lastModified: Date.now(),
432
+ tags: ctx?.tags || [],
433
+ value: data,
434
+ };
435
+ const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
258
436
 
259
437
  // pre seed data into deduplicated get client. This will reduce redis load by not requesting
260
438
  // the same value from redis which was just set.
261
439
  if (this.redisGetDeduplication) {
262
- this.redisDeduplicationHandler.seedRequestReturn(key, value);
440
+ this.redisDeduplicationHandler.seedRequestReturn(
441
+ key,
442
+ serializedCacheEntry,
443
+ );
263
444
  }
264
445
 
446
+ // Constructing the expire time for the cache entry
265
447
  const expireAt =
266
448
  ctx.revalidate &&
267
- Number.isSafeInteger(ctx.revalidate) &&
268
- ctx.revalidate > 0
449
+ Number.isSafeInteger(ctx.revalidate) &&
450
+ ctx.revalidate > 0
269
451
  ? this.estimateExpireAge(ctx.revalidate)
270
452
  : this.estimateExpireAge(this.defaultStaleAge);
453
+
454
+ // Setting the cache entry in redis
271
455
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
272
456
  const setOperation: Promise<string | null> = this.client.set(
273
457
  options,
274
458
  this.keyPrefix + key,
275
- value,
459
+ serializedCacheEntry,
276
460
  {
277
461
  EX: expireAt,
278
462
  },
279
463
  );
280
464
 
465
+ debug(
466
+ 'blue',
467
+ 'RedisStringsHandler.set() will set the following serializedCacheEntry',
468
+ this.keyPrefix,
469
+ key,
470
+ data,
471
+ ctx,
472
+ serializedCacheEntry?.substring(0, 200),
473
+ expireAt,
474
+ );
475
+
476
+ // Setting the tags for the cache entry in the sharedTagsMap (locally stored hashmap synced via redis)
281
477
  let setTagsOperation: Promise<void> | undefined;
282
478
  if (ctx.tags && ctx.tags.length > 0) {
283
479
  const currentTags = this.sharedTagsMap.get(key);
@@ -294,47 +490,92 @@ export default class RedisStringsHandler implements CacheHandler {
294
490
  }
295
491
  }
296
492
 
493
+ debug(
494
+ 'blue',
495
+ 'RedisStringsHandler.set() will set the following sharedTagsMap',
496
+ key,
497
+ ctx.tags as string[],
498
+ );
499
+
297
500
  await Promise.all([setOperation, setTagsOperation]);
298
501
  }
299
- public async revalidateTag(tagOrTags: RevalidateParams[0]) {
502
+
503
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
504
+ public async revalidateTag(tagOrTags: string | string[], ...rest: any[]) {
505
+ debug(
506
+ 'red',
507
+ 'RedisStringsHandler.revalidateTag() called with',
508
+ tagOrTags,
509
+ rest,
510
+ );
300
511
  const tags = new Set([tagOrTags || []].flat());
301
512
  await this.assertClientIsReady();
302
513
 
303
- // TODO: check how this revalidatedTagsMap is used or if it can be deleted
514
+ // find all keys that are related to this tag
515
+ const keysToDelete: Set<string> = new Set();
516
+
304
517
  for (const tag of tags) {
305
- if (isImplicitTag(tag)) {
518
+ // INFO: implicit tags (revalidate of nested fetch in api route/page on revalidatePath call of the page/api route)
519
+ //
520
+ // Invalidation logic for fetch requests that are related to a invalidated page.
521
+ // revalidateTag is called for the page tag (_N_T_...) and the fetch request needs to be invalidated as well
522
+ // unfortunately this is not possible since the revalidateTag is not called with any data that would allow us to find the cache entry of the fetch request
523
+ // in case of a fetch request get method call, the get method of the cache handler is called with some information about the pages/routes the fetch request is inside
524
+ // therefore we only mark the page/route as stale here (with help of the revalidatedTagsMap)
525
+ // and delete the cache entry of the fetch request on the next request to the get function
526
+ if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
306
527
  const now = Date.now();
528
+ debug(
529
+ 'red',
530
+ 'RedisStringsHandler.revalidateTag() set revalidation time for tag',
531
+ tag,
532
+ 'to',
533
+ now,
534
+ );
307
535
  await this.revalidatedTagsMap.set(tag, now);
308
536
  }
309
537
  }
310
538
 
311
- const keysToDelete: string[] = [];
312
-
539
+ // Scan the whole sharedTagsMap for keys that are dependent on any of the revalidated tags
313
540
  for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
314
541
  if (sharedTags.some((tag) => tags.has(tag))) {
315
- keysToDelete.push(key);
542
+ keysToDelete.add(key);
316
543
  }
317
544
  }
318
545
 
319
- if (keysToDelete.length === 0) {
546
+ debug(
547
+ 'red',
548
+ 'RedisStringsHandler.revalidateTag() found',
549
+ keysToDelete,
550
+ 'keys to delete',
551
+ );
552
+
553
+ // exit early if no keys are related to this tag
554
+ if (keysToDelete.size === 0) {
320
555
  return;
321
556
  }
322
557
 
323
- const fullRedisKeys = keysToDelete.map((key) => this.keyPrefix + key);
324
-
558
+ // prepare deletion of all keys in redis that are related to this tag
559
+ const redisKeys = Array.from(keysToDelete);
560
+ const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
325
561
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
326
-
327
562
  const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
328
563
 
329
- // delete entries from in-memory deduplication cache
564
+ // also delete entries from in-memory deduplication cache if they get revalidated
330
565
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
331
566
  for (const key of keysToDelete) {
332
567
  this.inMemoryDeduplicationCache.delete(key);
333
568
  }
334
569
  }
335
570
 
336
- const deleteTagsOperation = this.sharedTagsMap.delete(keysToDelete);
571
+ // prepare deletion of entries from shared tags map if they get revalidated so that the map will not grow indefinitely
572
+ const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
337
573
 
574
+ // execute keys and tag maps deletion
338
575
  await Promise.all([deleteKeysOperation, deleteTagsOperation]);
576
+ debug(
577
+ 'red',
578
+ 'RedisStringsHandler.revalidateTag() finished delete operations',
579
+ );
339
580
  }
340
581
  }