@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
package/dist/index.mjs CHANGED
@@ -1,6 +1,27 @@
1
1
  // src/RedisStringsHandler.ts
2
2
  import { commandOptions, createClient } from "redis";
3
3
 
4
+ // src/utils/debug.ts
5
+ function debug(color = "none", ...args) {
6
+ const colorCode = {
7
+ red: "\x1B[31m",
8
+ blue: "\x1B[34m",
9
+ green: "\x1B[32m",
10
+ yellow: "\x1B[33m",
11
+ cyan: "\x1B[36m",
12
+ white: "\x1B[37m",
13
+ none: ""
14
+ };
15
+ if (process.env.DEBUG_CACHE_HANDLER) {
16
+ console.log(colorCode[color], "DEBUG CACHE HANDLER: ", ...args);
17
+ }
18
+ }
19
+ function debugVerbose(color, ...args) {
20
+ if (process.env.DEBUG_CACHE_HANDLER_VERBOSE_VERBOSE) {
21
+ console.log("\x1B[35m", "DEBUG SYNCED MAP: ", ...args);
22
+ }
23
+ }
24
+
4
25
  // src/SyncedMap.ts
5
26
  var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
6
27
  var SyncedMap = class {
@@ -116,24 +137,56 @@ var SyncedMap = class {
116
137
  }
117
138
  }
118
139
  };
119
- const keyEventHandler = async (_channel, message) => {
120
- const key = message;
140
+ const keyEventHandler = async (key, message) => {
141
+ debug(
142
+ "yellow",
143
+ "SyncedMap.keyEventHandler() called with message",
144
+ this.redisKey,
145
+ message,
146
+ key
147
+ );
121
148
  if (key.startsWith(this.keyPrefix)) {
122
149
  const keyInMap = key.substring(this.keyPrefix.length);
123
150
  if (this.filterKeys(keyInMap)) {
151
+ debugVerbose(
152
+ "SyncedMap.keyEventHandler() key matches filter and will be deleted",
153
+ this.redisKey,
154
+ message,
155
+ key
156
+ );
124
157
  await this.delete(keyInMap, true);
125
158
  }
159
+ } else {
160
+ debugVerbose(
161
+ "SyncedMap.keyEventHandler() key does not have prefix",
162
+ this.redisKey,
163
+ message,
164
+ key
165
+ );
126
166
  }
127
167
  };
128
168
  try {
129
- await this.subscriberClient.connect();
169
+ await this.subscriberClient.connect().catch(async () => {
170
+ await this.subscriberClient.connect();
171
+ });
172
+ const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
173
+ if (!keyspaceEventConfig.includes("E")) {
174
+ throw new Error(
175
+ "Keyspace event configuration has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
176
+ );
177
+ }
178
+ if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
179
+ throw new Error(
180
+ "Keyspace event configuration has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
181
+ );
182
+ }
130
183
  await Promise.all([
131
184
  // We use a custom channel for insert/delete For the following reason:
132
185
  // With custom channel we can delete multiple entries in one message. If we would listen to unlink / del we
133
186
  // could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages)
134
187
  // Also we can send the value in the message for insert
135
188
  this.subscriberClient.subscribe(this.syncChannel, syncHandler),
136
- // Subscribe to Redis keyspace notifications for evicted and expired keys
189
+ // Subscribe to Redis keyevent notifications for evicted and expired keys
137
190
  this.subscriberClient.subscribe(
138
191
  `__keyevent@${this.database}__:evicted`,
139
192
  keyEventHandler
@@ -165,9 +218,19 @@ var SyncedMap = class {
165
218
  await this.setupLock;
166
219
  }
167
220
  get(key) {
221
+ debugVerbose(
222
+ "SyncedMap.get() called with key",
223
+ key,
224
+ JSON.stringify(this.map.get(key))?.substring(0, 100)
225
+ );
168
226
  return this.map.get(key);
169
227
  }
170
228
  async set(key, value) {
229
+ debugVerbose(
230
+ "SyncedMap.set() called with key",
231
+ key,
232
+ JSON.stringify(value)?.substring(0, 100)
233
+ );
171
234
  this.map.set(key, value);
172
235
  const operations = [];
173
236
  if (this.customizedSync?.withoutSetSync) {
@@ -194,7 +257,15 @@ var SyncedMap = class {
194
257
  );
195
258
  await Promise.all(operations);
196
259
  }
260
+ // /api/revalidated-fetch
261
+ // true
197
262
  async delete(keys, withoutSyncMessage = false) {
263
+ debugVerbose(
264
+ "SyncedMap.delete() called with keys",
265
+ this.redisKey,
266
+ keys,
267
+ withoutSyncMessage
268
+ );
198
269
  const keysArray = Array.isArray(keys) ? keys : [keys];
199
270
  const operations = [];
200
271
  for (const key of keysArray) {
@@ -216,6 +287,12 @@ var SyncedMap = class {
216
287
  );
217
288
  }
218
289
  await Promise.all(operations);
290
+ debugVerbose(
291
+ "SyncedMap.delete() finished operations",
292
+ this.redisKey,
293
+ keys,
294
+ operations.length
295
+ );
219
296
  }
220
297
  has(key) {
221
298
  return this.map.has(key);
@@ -230,19 +307,59 @@ var DeduplicatedRequestHandler = class {
230
307
  constructor(fn, cachingTimeMs, inMemoryDeduplicationCache) {
231
308
  // Method to handle deduplicated requests
232
309
  this.deduplicatedFunction = (key) => {
310
+ debugVerbose(
311
+ "DeduplicatedRequestHandler.deduplicatedFunction() called with",
312
+ key
313
+ );
233
314
  const self = this;
234
315
  const dedupedFn = async (...args) => {
316
+ debugVerbose(
317
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn called with",
318
+ key
319
+ );
235
320
  if (self.inMemoryDeduplicationCache && self.inMemoryDeduplicationCache.has(key)) {
321
+ debugVerbose(
322
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
323
+ key,
324
+ "found key in inMemoryDeduplicationCache"
325
+ );
236
326
  const res = await self.inMemoryDeduplicationCache.get(key).then((v) => structuredClone(v));
327
+ debugVerbose(
328
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
329
+ key,
330
+ "found key in inMemoryDeduplicationCache and served result from there",
331
+ JSON.stringify(res).substring(0, 200)
332
+ );
237
333
  return res;
238
334
  }
239
335
  const promise = self.fn(...args);
240
336
  self.inMemoryDeduplicationCache.set(key, promise);
337
+ debugVerbose(
338
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
339
+ key,
340
+ "did not found key in inMemoryDeduplicationCache. Setting it now and waiting for promise to resolve"
341
+ );
241
342
  try {
343
+ const ts = performance.now();
242
344
  const result = await promise;
345
+ debugVerbose(
346
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
347
+ key,
348
+ "promise resolved (in ",
349
+ performance.now() - ts,
350
+ "ms). Returning result",
351
+ JSON.stringify(result).substring(0, 200)
352
+ );
243
353
  return structuredClone(result);
244
354
  } finally {
245
355
  setTimeout(() => {
356
+ debugVerbose(
357
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
358
+ key,
359
+ "deleting key from inMemoryDeduplicationCache after ",
360
+ self.cachingTimeMs,
361
+ "ms"
362
+ );
246
363
  self.inMemoryDeduplicationCache.delete(key);
247
364
  }, self.cachingTimeMs);
248
365
  }
@@ -257,18 +374,41 @@ var DeduplicatedRequestHandler = class {
257
374
  seedRequestReturn(key, value) {
258
375
  const resultPromise = new Promise((res) => res(value));
259
376
  this.inMemoryDeduplicationCache.set(key, resultPromise);
377
+ debugVerbose(
378
+ "DeduplicatedRequestHandler.seedRequestReturn() seeded result ",
379
+ key,
380
+ value.substring(0, 200)
381
+ );
260
382
  setTimeout(() => {
261
383
  this.inMemoryDeduplicationCache.delete(key);
262
384
  }, this.cachingTimeMs);
263
385
  }
264
386
  };
265
387
 
388
+ // src/utils/json.ts
389
+ function bufferReviver(_, value) {
390
+ if (value && typeof value === "object" && typeof value.$binary === "string") {
391
+ return Buffer.from(value.$binary, "base64");
392
+ }
393
+ return value;
394
+ }
395
+ function bufferReplacer(_, value) {
396
+ if (Buffer.isBuffer(value)) {
397
+ return {
398
+ $binary: value.toString("base64")
399
+ };
400
+ }
401
+ if (value && typeof value === "object" && value?.type === "Buffer" && Array.isArray(value.data)) {
402
+ return {
403
+ $binary: Buffer.from(value.data).toString("base64")
404
+ };
405
+ }
406
+ return value;
407
+ }
408
+
266
409
  // src/RedisStringsHandler.ts
267
410
  var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_";
268
411
  var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
269
- function isImplicitTag(tag) {
270
- return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID);
271
- }
272
412
  function getTimeoutRedisCommandOptions(timeoutMs) {
273
413
  return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
274
414
  }
@@ -283,7 +423,7 @@ var RedisStringsHandler = class {
283
423
  redisGetDeduplication = true,
284
424
  inMemoryCachingTime = 1e4,
285
425
  defaultStaleAge = 60 * 60 * 24 * 14,
286
- estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "preview" ? staleAge * 1.2 : staleAge * 2
426
+ estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2
287
427
  }) {
288
428
  this.keyPrefix = keyPrefix;
289
429
  this.timeoutMs = timeoutMs;
@@ -301,9 +441,12 @@ var RedisStringsHandler = class {
301
441
  });
302
442
  this.client.connect().then(() => {
303
443
  console.info("Redis client connected.");
304
- }).catch((error) => {
305
- console.error("Failed to connect Redis client:", error);
306
- this.client.disconnect();
444
+ }).catch(() => {
445
+ this.client.connect().catch((error) => {
446
+ console.error("Failed to connect Redis client:", error);
447
+ this.client.disconnect();
448
+ throw error;
449
+ });
307
450
  });
308
451
  } catch (error) {
309
452
  console.error("Failed to initialize Redis client");
@@ -352,8 +495,7 @@ var RedisStringsHandler = class {
352
495
  this.redisGet = redisGet;
353
496
  this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
354
497
  }
355
- resetRequestCache(...args) {
356
- console.warn("WARNING resetRequestCache() was called", args);
498
+ resetRequestCache() {
357
499
  }
358
500
  async assertClientIsReady() {
359
501
  await Promise.all([
@@ -365,75 +507,153 @@ var RedisStringsHandler = class {
365
507
  }
366
508
  }
367
509
  async get(key, ctx) {
510
+ if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
511
+ console.warn(
512
+ "RedisStringsHandler.get() called with",
513
+ key,
514
+ ctx,
515
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
516
+ ctx?.kind
517
+ );
518
+ }
519
+ debug("green", "RedisStringsHandler.get() called with", key, ctx);
368
520
  await this.assertClientIsReady();
369
521
  const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
370
- const result = await clientGet(
522
+ const serializedCacheEntry = await clientGet(
371
523
  getTimeoutRedisCommandOptions(this.timeoutMs),
372
524
  this.keyPrefix + key
373
525
  );
374
- if (!result) {
526
+ debug(
527
+ "green",
528
+ "RedisStringsHandler.get() finished with result (serializedCacheEntry)",
529
+ serializedCacheEntry?.substring(0, 200)
530
+ );
531
+ if (!serializedCacheEntry) {
375
532
  return null;
376
533
  }
377
- const cacheValue = JSON.parse(result);
378
- if (!cacheValue) {
534
+ const cacheEntry = JSON.parse(
535
+ serializedCacheEntry,
536
+ bufferReviver
537
+ );
538
+ debug(
539
+ "green",
540
+ "RedisStringsHandler.get() finished with result (cacheEntry)",
541
+ JSON.stringify(cacheEntry).substring(0, 200)
542
+ );
543
+ if (!cacheEntry) {
379
544
  return null;
380
545
  }
381
- if (cacheValue.value?.kind === "FETCH") {
382
- cacheValue.value.data.body = Buffer.from(
383
- cacheValue.value.data.body
384
- ).toString("base64");
546
+ if (!cacheEntry?.tags) {
547
+ console.warn(
548
+ "RedisStringsHandler.get() called with",
549
+ key,
550
+ ctx,
551
+ "cacheEntry is mall formed (missing tags)"
552
+ );
385
553
  }
386
- const combinedTags = /* @__PURE__ */ new Set([
387
- ...ctx?.softTags || [],
388
- ...ctx?.tags || []
389
- ]);
390
- if (combinedTags.size === 0) {
391
- return cacheValue;
554
+ if (!cacheEntry?.value) {
555
+ console.warn(
556
+ "RedisStringsHandler.get() called with",
557
+ key,
558
+ ctx,
559
+ "cacheEntry is mall formed (missing value)"
560
+ );
392
561
  }
393
- for (const tag of combinedTags) {
394
- const revalidationTime = this.revalidatedTagsMap.get(tag);
395
- if (revalidationTime && revalidationTime > cacheValue.lastModified) {
396
- const redisKey = this.keyPrefix + key;
397
- this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
398
- console.error(
399
- "Error occurred while unlinking stale data. Retrying now. Error was:",
400
- err
401
- );
402
- this.client.unlink(
403
- getTimeoutRedisCommandOptions(this.timeoutMs),
404
- redisKey
562
+ if (!cacheEntry?.lastModified) {
563
+ console.warn(
564
+ "RedisStringsHandler.get() called with",
565
+ key,
566
+ ctx,
567
+ "cacheEntry is mall formed (missing lastModified)"
568
+ );
569
+ }
570
+ if (ctx.kind === "FETCH") {
571
+ const combinedTags = /* @__PURE__ */ new Set([
572
+ ...ctx?.softTags || [],
573
+ ...ctx?.tags || []
574
+ ]);
575
+ if (combinedTags.size === 0) {
576
+ return cacheEntry;
577
+ }
578
+ for (const tag of combinedTags) {
579
+ const revalidationTime = this.revalidatedTagsMap.get(tag);
580
+ if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
581
+ const redisKey = this.keyPrefix + key;
582
+ this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
583
+ console.error(
584
+ "Error occurred while unlinking stale data. Retrying now. Error was:",
585
+ err
586
+ );
587
+ this.client.unlink(
588
+ getTimeoutRedisCommandOptions(this.timeoutMs),
589
+ redisKey
590
+ );
591
+ }).finally(async () => {
592
+ await this.sharedTagsMap.delete(key);
593
+ await this.revalidatedTagsMap.delete(tag);
594
+ });
595
+ debug(
596
+ "green",
597
+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
598
+ tag,
599
+ redisKey,
600
+ revalidationTime,
601
+ cacheEntry
405
602
  );
406
- }).finally(async () => {
407
- await this.sharedTagsMap.delete(key);
408
- await this.revalidatedTagsMap.delete(tag);
409
- });
410
- return null;
603
+ return null;
604
+ }
411
605
  }
412
606
  }
413
- return cacheValue;
607
+ return cacheEntry;
414
608
  }
415
609
  async set(key, data, ctx) {
416
- if (data.kind === "FETCH") {
417
- console.time("encoding" + key);
418
- data.data.body = Buffer.from(data.data.body, "base64").toString();
419
- console.timeEnd("encoding" + key);
610
+ if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
611
+ console.warn(
612
+ "RedisStringsHandler.set() called with",
613
+ key,
614
+ ctx,
615
+ data,
616
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
617
+ data?.kind
618
+ );
420
619
  }
421
620
  await this.assertClientIsReady();
422
- data.lastModified = Date.now();
423
- const value = JSON.stringify(data);
621
+ if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
622
+ const tags = data.headers["x-next-cache-tags"]?.split(",");
623
+ ctx.tags = [...ctx.tags || [], ...tags || []];
624
+ }
625
+ const cacheEntry = {
626
+ lastModified: Date.now(),
627
+ tags: ctx?.tags || [],
628
+ value: data
629
+ };
630
+ const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
424
631
  if (this.redisGetDeduplication) {
425
- this.redisDeduplicationHandler.seedRequestReturn(key, value);
632
+ this.redisDeduplicationHandler.seedRequestReturn(
633
+ key,
634
+ serializedCacheEntry
635
+ );
426
636
  }
427
637
  const expireAt = ctx.revalidate && Number.isSafeInteger(ctx.revalidate) && ctx.revalidate > 0 ? this.estimateExpireAge(ctx.revalidate) : this.estimateExpireAge(this.defaultStaleAge);
428
638
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
429
639
  const setOperation = this.client.set(
430
640
  options,
431
641
  this.keyPrefix + key,
432
- value,
642
+ serializedCacheEntry,
433
643
  {
434
644
  EX: expireAt
435
645
  }
436
646
  );
647
+ debug(
648
+ "blue",
649
+ "RedisStringsHandler.set() will set the following serializedCacheEntry",
650
+ this.keyPrefix,
651
+ key,
652
+ data,
653
+ ctx,
654
+ serializedCacheEntry?.substring(0, 200),
655
+ expireAt
656
+ );
437
657
  let setTagsOperation;
438
658
  if (ctx.tags && ctx.tags.length > 0) {
439
659
  const currentTags = this.sharedTagsMap.get(key);
@@ -445,27 +665,54 @@ var RedisStringsHandler = class {
445
665
  );
446
666
  }
447
667
  }
668
+ debug(
669
+ "blue",
670
+ "RedisStringsHandler.set() will set the following sharedTagsMap",
671
+ key,
672
+ ctx.tags
673
+ );
448
674
  await Promise.all([setOperation, setTagsOperation]);
449
675
  }
450
- async revalidateTag(tagOrTags) {
676
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
677
+ async revalidateTag(tagOrTags, ...rest) {
678
+ debug(
679
+ "red",
680
+ "RedisStringsHandler.revalidateTag() called with",
681
+ tagOrTags,
682
+ rest
683
+ );
451
684
  const tags = new Set([tagOrTags || []].flat());
452
685
  await this.assertClientIsReady();
686
+ const keysToDelete = /* @__PURE__ */ new Set();
453
687
  for (const tag of tags) {
454
- if (isImplicitTag(tag)) {
688
+ if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
455
689
  const now = Date.now();
690
+ debug(
691
+ "red",
692
+ "RedisStringsHandler.revalidateTag() set revalidation time for tag",
693
+ tag,
694
+ "to",
695
+ now
696
+ );
456
697
  await this.revalidatedTagsMap.set(tag, now);
457
698
  }
458
699
  }
459
- const keysToDelete = [];
460
700
  for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
461
701
  if (sharedTags.some((tag) => tags.has(tag))) {
462
- keysToDelete.push(key);
702
+ keysToDelete.add(key);
463
703
  }
464
704
  }
465
- if (keysToDelete.length === 0) {
705
+ debug(
706
+ "red",
707
+ "RedisStringsHandler.revalidateTag() found",
708
+ keysToDelete,
709
+ "keys to delete"
710
+ );
711
+ if (keysToDelete.size === 0) {
466
712
  return;
467
713
  }
468
- const fullRedisKeys = keysToDelete.map((key) => this.keyPrefix + key);
714
+ const redisKeys = Array.from(keysToDelete);
715
+ const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
469
716
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
470
717
  const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
471
718
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
@@ -473,8 +720,12 @@ var RedisStringsHandler = class {
473
720
  this.inMemoryDeduplicationCache.delete(key);
474
721
  }
475
722
  }
476
- const deleteTagsOperation = this.sharedTagsMap.delete(keysToDelete);
723
+ const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
477
724
  await Promise.all([deleteKeysOperation, deleteTagsOperation]);
725
+ debug(
726
+ "red",
727
+ "RedisStringsHandler.revalidateTag() finished delete operations"
728
+ );
478
729
  }
479
730
  };
480
731
 
@@ -488,12 +739,15 @@ var CachedHandler = class {
488
739
  }
489
740
  }
490
741
  get(...args) {
742
+ debugVerbose("CachedHandler.get called with", args);
491
743
  return cachedHandler.get(...args);
492
744
  }
493
745
  set(...args) {
746
+ debugVerbose("CachedHandler.set called with", args);
494
747
  return cachedHandler.set(...args);
495
748
  }
496
749
  revalidateTag(...args) {
750
+ debugVerbose("CachedHandler.revalidateTag called with", args);
497
751
  return cachedHandler.revalidateTag(...args);
498
752
  }
499
753
  resetRequestCache(...args) {
@@ -504,6 +758,7 @@ var CachedHandler = class {
504
758
  // src/index.ts
505
759
  var index_default = CachedHandler;
506
760
  export {
761
+ RedisStringsHandler,
507
762
  index_default as default
508
763
  };
509
764
  //# sourceMappingURL=index.mjs.map