@trieb.work/nextjs-turbo-redis-cache 1.2.1 → 1.4.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 (94) hide show
  1. package/.github/workflows/ci.yml +31 -6
  2. package/.github/workflows/release.yml +7 -3
  3. package/.next/trace +11 -0
  4. package/.vscode/settings.json +10 -0
  5. package/CHANGELOG.md +71 -0
  6. package/README.md +154 -34
  7. package/dist/index.d.mts +96 -20
  8. package/dist/index.d.ts +96 -20
  9. package/dist/index.js +317 -61
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +313 -61
  12. package/dist/index.mjs.map +1 -1
  13. package/package.json +14 -7
  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 +331 -91
  18. package/src/SyncedMap.ts +74 -4
  19. package/src/ZodHandler.ts +45 -0
  20. package/src/index.ts +4 -2
  21. package/src/utils/debug.ts +30 -0
  22. package/src/utils/json.ts +26 -0
  23. package/test/integration/next-app-15-0-3/README.md +36 -0
  24. package/test/integration/next-app-15-0-3/eslint.config.mjs +16 -0
  25. package/test/integration/next-app-15-0-3/next.config.js +6 -0
  26. package/test/integration/next-app-15-0-3/package-lock.json +5833 -0
  27. package/test/integration/next-app-15-0-3/package.json +29 -0
  28. package/test/integration/next-app-15-0-3/pnpm-lock.yaml +3679 -0
  29. package/test/integration/next-app-15-0-3/postcss.config.mjs +5 -0
  30. package/test/integration/next-app-15-0-3/public/file.svg +1 -0
  31. package/test/integration/next-app-15-0-3/public/globe.svg +1 -0
  32. package/test/integration/next-app-15-0-3/public/next.svg +1 -0
  33. package/test/integration/next-app-15-0-3/public/vercel.svg +1 -0
  34. package/test/integration/next-app-15-0-3/public/window.svg +1 -0
  35. package/test/integration/next-app-15-0-3/src/app/api/cached-static-fetch/route.ts +18 -0
  36. package/test/integration/next-app-15-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  37. package/test/integration/next-app-15-0-3/src/app/api/revalidatePath/route.ts +15 -0
  38. package/test/integration/next-app-15-0-3/src/app/api/revalidateTag/route.ts +15 -0
  39. package/test/integration/next-app-15-0-3/src/app/api/revalidated-fetch/route.ts +17 -0
  40. package/test/integration/next-app-15-0-3/src/app/api/uncached-fetch/route.ts +15 -0
  41. package/test/integration/next-app-15-0-3/src/app/globals.css +26 -0
  42. package/test/integration/next-app-15-0-3/src/app/layout.tsx +59 -0
  43. package/test/integration/next-app-15-0-3/src/app/page.tsx +755 -0
  44. package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  45. package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  46. package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  47. package/test/integration/next-app-15-0-3/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  48. package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  49. package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  50. package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  51. package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  52. package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  53. package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  54. package/test/integration/next-app-15-0-3/src/app/revalidation-interface.tsx +267 -0
  55. package/test/integration/next-app-15-0-3/tsconfig.json +27 -0
  56. package/test/integration/next-app-15-3-2/README.md +36 -0
  57. package/test/integration/next-app-15-3-2/eslint.config.mjs +16 -0
  58. package/test/integration/next-app-15-3-2/next.config.js +6 -0
  59. package/test/integration/next-app-15-3-2/package-lock.json +5969 -0
  60. package/test/integration/next-app-15-3-2/package.json +33 -0
  61. package/test/integration/next-app-15-3-2/pnpm-lock.yaml +3688 -0
  62. package/test/integration/next-app-15-3-2/postcss.config.mjs +5 -0
  63. package/test/integration/next-app-15-3-2/public/file.svg +1 -0
  64. package/test/integration/next-app-15-3-2/public/globe.svg +1 -0
  65. package/test/integration/next-app-15-3-2/public/next.svg +1 -0
  66. package/test/integration/next-app-15-3-2/public/vercel.svg +1 -0
  67. package/test/integration/next-app-15-3-2/public/window.svg +1 -0
  68. package/test/integration/next-app-15-3-2/src/app/api/cached-static-fetch/route.ts +18 -0
  69. package/test/integration/next-app-15-3-2/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
  70. package/test/integration/next-app-15-3-2/src/app/api/revalidatePath/route.ts +15 -0
  71. package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +15 -0
  72. package/test/integration/next-app-15-3-2/src/app/api/revalidated-fetch/route.ts +17 -0
  73. package/test/integration/next-app-15-3-2/src/app/api/uncached-fetch/route.ts +15 -0
  74. package/test/integration/next-app-15-3-2/src/app/globals.css +26 -0
  75. package/test/integration/next-app-15-3-2/src/app/layout.tsx +59 -0
  76. package/test/integration/next-app-15-3-2/src/app/page.tsx +755 -0
  77. package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
  78. package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
  79. package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  80. package/test/integration/next-app-15-3-2/src/app/pages/no-fetch/default-page/page.tsx +55 -0
  81. package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
  82. package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
  83. package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  84. package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
  85. package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
  86. package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
  87. package/test/integration/next-app-15-3-2/src/app/revalidation-interface.tsx +267 -0
  88. package/test/integration/next-app-15-3-2/tsconfig.json +27 -0
  89. package/test/integration/next-app-customized/README.md +36 -0
  90. package/test/integration/next-app-customized/customized-cache-handler.js +34 -0
  91. package/test/integration/next-app-customized/eslint.config.mjs +16 -0
  92. package/test/integration/next-app-customized/next.config.js +6 -0
  93. package/test/integration/nextjs-cache-handler.integration.test.ts +859 -0
  94. package/vite.config.ts +23 -8
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ RedisStringsHandler: () => RedisStringsHandler,
23
24
  default: () => index_default
24
25
  });
25
26
  module.exports = __toCommonJS(index_exports);
@@ -27,6 +28,27 @@ module.exports = __toCommonJS(index_exports);
27
28
  // src/RedisStringsHandler.ts
28
29
  var import_redis = require("redis");
29
30
 
31
+ // src/utils/debug.ts
32
+ function debug(color = "none", ...args) {
33
+ const colorCode = {
34
+ red: "\x1B[31m",
35
+ blue: "\x1B[34m",
36
+ green: "\x1B[32m",
37
+ yellow: "\x1B[33m",
38
+ cyan: "\x1B[36m",
39
+ white: "\x1B[37m",
40
+ none: ""
41
+ };
42
+ if (process.env.DEBUG_CACHE_HANDLER) {
43
+ console.log(colorCode[color], "DEBUG CACHE HANDLER: ", ...args);
44
+ }
45
+ }
46
+ function debugVerbose(color, ...args) {
47
+ if (process.env.DEBUG_CACHE_HANDLER_VERBOSE_VERBOSE) {
48
+ console.log("\x1B[35m", "DEBUG SYNCED MAP: ", ...args);
49
+ }
50
+ }
51
+
30
52
  // src/SyncedMap.ts
31
53
  var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
32
54
  var SyncedMap = class {
@@ -142,24 +164,56 @@ var SyncedMap = class {
142
164
  }
143
165
  }
144
166
  };
145
- const keyEventHandler = async (_channel, message) => {
146
- const key = message;
167
+ const keyEventHandler = async (key, message) => {
168
+ debug(
169
+ "yellow",
170
+ "SyncedMap.keyEventHandler() called with message",
171
+ this.redisKey,
172
+ message,
173
+ key
174
+ );
147
175
  if (key.startsWith(this.keyPrefix)) {
148
176
  const keyInMap = key.substring(this.keyPrefix.length);
149
177
  if (this.filterKeys(keyInMap)) {
178
+ debugVerbose(
179
+ "SyncedMap.keyEventHandler() key matches filter and will be deleted",
180
+ this.redisKey,
181
+ message,
182
+ key
183
+ );
150
184
  await this.delete(keyInMap, true);
151
185
  }
186
+ } else {
187
+ debugVerbose(
188
+ "SyncedMap.keyEventHandler() key does not have prefix",
189
+ this.redisKey,
190
+ message,
191
+ key
192
+ );
152
193
  }
153
194
  };
154
195
  try {
155
- await this.subscriberClient.connect();
196
+ await this.subscriberClient.connect().catch(async () => {
197
+ await this.subscriberClient.connect();
198
+ });
199
+ const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
200
+ if (!keyspaceEventConfig.includes("E")) {
201
+ throw new Error(
202
+ "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`"
203
+ );
204
+ }
205
+ if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
206
+ throw new Error(
207
+ "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`"
208
+ );
209
+ }
156
210
  await Promise.all([
157
211
  // We use a custom channel for insert/delete For the following reason:
158
212
  // With custom channel we can delete multiple entries in one message. If we would listen to unlink / del we
159
213
  // could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages)
160
214
  // Also we can send the value in the message for insert
161
215
  this.subscriberClient.subscribe(this.syncChannel, syncHandler),
162
- // Subscribe to Redis keyspace notifications for evicted and expired keys
216
+ // Subscribe to Redis keyevent notifications for evicted and expired keys
163
217
  this.subscriberClient.subscribe(
164
218
  `__keyevent@${this.database}__:evicted`,
165
219
  keyEventHandler
@@ -191,9 +245,19 @@ var SyncedMap = class {
191
245
  await this.setupLock;
192
246
  }
193
247
  get(key) {
248
+ debugVerbose(
249
+ "SyncedMap.get() called with key",
250
+ key,
251
+ JSON.stringify(this.map.get(key))?.substring(0, 100)
252
+ );
194
253
  return this.map.get(key);
195
254
  }
196
255
  async set(key, value) {
256
+ debugVerbose(
257
+ "SyncedMap.set() called with key",
258
+ key,
259
+ JSON.stringify(value)?.substring(0, 100)
260
+ );
197
261
  this.map.set(key, value);
198
262
  const operations = [];
199
263
  if (this.customizedSync?.withoutSetSync) {
@@ -220,7 +284,15 @@ var SyncedMap = class {
220
284
  );
221
285
  await Promise.all(operations);
222
286
  }
287
+ // /api/revalidated-fetch
288
+ // true
223
289
  async delete(keys, withoutSyncMessage = false) {
290
+ debugVerbose(
291
+ "SyncedMap.delete() called with keys",
292
+ this.redisKey,
293
+ keys,
294
+ withoutSyncMessage
295
+ );
224
296
  const keysArray = Array.isArray(keys) ? keys : [keys];
225
297
  const operations = [];
226
298
  for (const key of keysArray) {
@@ -242,6 +314,12 @@ var SyncedMap = class {
242
314
  );
243
315
  }
244
316
  await Promise.all(operations);
317
+ debugVerbose(
318
+ "SyncedMap.delete() finished operations",
319
+ this.redisKey,
320
+ keys,
321
+ operations.length
322
+ );
245
323
  }
246
324
  has(key) {
247
325
  return this.map.has(key);
@@ -256,19 +334,59 @@ var DeduplicatedRequestHandler = class {
256
334
  constructor(fn, cachingTimeMs, inMemoryDeduplicationCache) {
257
335
  // Method to handle deduplicated requests
258
336
  this.deduplicatedFunction = (key) => {
337
+ debugVerbose(
338
+ "DeduplicatedRequestHandler.deduplicatedFunction() called with",
339
+ key
340
+ );
259
341
  const self = this;
260
342
  const dedupedFn = async (...args) => {
343
+ debugVerbose(
344
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn called with",
345
+ key
346
+ );
261
347
  if (self.inMemoryDeduplicationCache && self.inMemoryDeduplicationCache.has(key)) {
348
+ debugVerbose(
349
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
350
+ key,
351
+ "found key in inMemoryDeduplicationCache"
352
+ );
262
353
  const res = await self.inMemoryDeduplicationCache.get(key).then((v) => structuredClone(v));
354
+ debugVerbose(
355
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
356
+ key,
357
+ "found key in inMemoryDeduplicationCache and served result from there",
358
+ JSON.stringify(res).substring(0, 200)
359
+ );
263
360
  return res;
264
361
  }
265
362
  const promise = self.fn(...args);
266
363
  self.inMemoryDeduplicationCache.set(key, promise);
364
+ debugVerbose(
365
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
366
+ key,
367
+ "did not found key in inMemoryDeduplicationCache. Setting it now and waiting for promise to resolve"
368
+ );
267
369
  try {
370
+ const ts = performance.now();
268
371
  const result = await promise;
372
+ debugVerbose(
373
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
374
+ key,
375
+ "promise resolved (in ",
376
+ performance.now() - ts,
377
+ "ms). Returning result",
378
+ JSON.stringify(result).substring(0, 200)
379
+ );
269
380
  return structuredClone(result);
270
381
  } finally {
271
382
  setTimeout(() => {
383
+ debugVerbose(
384
+ "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ",
385
+ key,
386
+ "deleting key from inMemoryDeduplicationCache after ",
387
+ self.cachingTimeMs,
388
+ "ms"
389
+ );
272
390
  self.inMemoryDeduplicationCache.delete(key);
273
391
  }, self.cachingTimeMs);
274
392
  }
@@ -283,18 +401,41 @@ var DeduplicatedRequestHandler = class {
283
401
  seedRequestReturn(key, value) {
284
402
  const resultPromise = new Promise((res) => res(value));
285
403
  this.inMemoryDeduplicationCache.set(key, resultPromise);
404
+ debugVerbose(
405
+ "DeduplicatedRequestHandler.seedRequestReturn() seeded result ",
406
+ key,
407
+ value.substring(0, 200)
408
+ );
286
409
  setTimeout(() => {
287
410
  this.inMemoryDeduplicationCache.delete(key);
288
411
  }, this.cachingTimeMs);
289
412
  }
290
413
  };
291
414
 
415
+ // src/utils/json.ts
416
+ function bufferReviver(_, value) {
417
+ if (value && typeof value === "object" && typeof value.$binary === "string") {
418
+ return Buffer.from(value.$binary, "base64");
419
+ }
420
+ return value;
421
+ }
422
+ function bufferReplacer(_, value) {
423
+ if (Buffer.isBuffer(value)) {
424
+ return {
425
+ $binary: value.toString("base64")
426
+ };
427
+ }
428
+ if (value && typeof value === "object" && value?.type === "Buffer" && Array.isArray(value.data)) {
429
+ return {
430
+ $binary: Buffer.from(value.data).toString("base64")
431
+ };
432
+ }
433
+ return value;
434
+ }
435
+
292
436
  // src/RedisStringsHandler.ts
293
437
  var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_";
294
438
  var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
295
- function isImplicitTag(tag) {
296
- return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID);
297
- }
298
439
  function getTimeoutRedisCommandOptions(timeoutMs) {
299
440
  return (0, import_redis.commandOptions)({ signal: AbortSignal.timeout(timeoutMs) });
300
441
  }
@@ -309,7 +450,7 @@ var RedisStringsHandler = class {
309
450
  redisGetDeduplication = true,
310
451
  inMemoryCachingTime = 1e4,
311
452
  defaultStaleAge = 60 * 60 * 24 * 14,
312
- estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "preview" ? staleAge * 1.2 : staleAge * 2
453
+ estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2
313
454
  }) {
314
455
  this.keyPrefix = keyPrefix;
315
456
  this.timeoutMs = timeoutMs;
@@ -327,9 +468,12 @@ var RedisStringsHandler = class {
327
468
  });
328
469
  this.client.connect().then(() => {
329
470
  console.info("Redis client connected.");
330
- }).catch((error) => {
331
- console.error("Failed to connect Redis client:", error);
332
- this.client.disconnect();
471
+ }).catch(() => {
472
+ this.client.connect().catch((error) => {
473
+ console.error("Failed to connect Redis client:", error);
474
+ this.client.disconnect();
475
+ throw error;
476
+ });
333
477
  });
334
478
  } catch (error) {
335
479
  console.error("Failed to initialize Redis client");
@@ -378,8 +522,7 @@ var RedisStringsHandler = class {
378
522
  this.redisGet = redisGet;
379
523
  this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
380
524
  }
381
- resetRequestCache(...args) {
382
- console.warn("WARNING resetRequestCache() was called", args);
525
+ resetRequestCache() {
383
526
  }
384
527
  async assertClientIsReady() {
385
528
  await Promise.all([
@@ -391,75 +534,150 @@ var RedisStringsHandler = class {
391
534
  }
392
535
  }
393
536
  async get(key, ctx) {
537
+ if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
538
+ console.warn(
539
+ "RedisStringsHandler.get() called with",
540
+ key,
541
+ ctx,
542
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
543
+ ctx?.kind
544
+ );
545
+ }
546
+ debug("green", "RedisStringsHandler.get() called with", key, ctx);
394
547
  await this.assertClientIsReady();
395
548
  const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
396
- const result = await clientGet(
549
+ const serializedCacheEntry = await clientGet(
397
550
  getTimeoutRedisCommandOptions(this.timeoutMs),
398
551
  this.keyPrefix + key
399
552
  );
400
- if (!result) {
553
+ debug(
554
+ "green",
555
+ "RedisStringsHandler.get() finished with result (serializedCacheEntry)",
556
+ serializedCacheEntry?.substring(0, 200)
557
+ );
558
+ if (!serializedCacheEntry) {
401
559
  return null;
402
560
  }
403
- const cacheValue = JSON.parse(result);
404
- if (!cacheValue) {
561
+ const cacheEntry = JSON.parse(
562
+ serializedCacheEntry,
563
+ bufferReviver
564
+ );
565
+ debug(
566
+ "green",
567
+ "RedisStringsHandler.get() finished with result (cacheEntry)",
568
+ JSON.stringify(cacheEntry).substring(0, 200)
569
+ );
570
+ if (!cacheEntry) {
405
571
  return null;
406
572
  }
407
- if (cacheValue.value?.kind === "FETCH") {
408
- cacheValue.value.data.body = Buffer.from(
409
- cacheValue.value.data.body
410
- ).toString("base64");
573
+ if (!cacheEntry?.tags) {
574
+ console.warn(
575
+ "RedisStringsHandler.get() called with",
576
+ key,
577
+ ctx,
578
+ "cacheEntry is mall formed (missing tags)"
579
+ );
411
580
  }
412
- const combinedTags = /* @__PURE__ */ new Set([
413
- ...ctx?.softTags || [],
414
- ...ctx?.tags || []
415
- ]);
416
- if (combinedTags.size === 0) {
417
- return cacheValue;
581
+ if (!cacheEntry?.value) {
582
+ console.warn(
583
+ "RedisStringsHandler.get() called with",
584
+ key,
585
+ ctx,
586
+ "cacheEntry is mall formed (missing value)"
587
+ );
418
588
  }
419
- for (const tag of combinedTags) {
420
- const revalidationTime = this.revalidatedTagsMap.get(tag);
421
- if (revalidationTime && revalidationTime > cacheValue.lastModified) {
422
- const redisKey = this.keyPrefix + key;
423
- this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
424
- console.error(
425
- "Error occurred while unlinking stale data. Retrying now. Error was:",
426
- err
427
- );
428
- this.client.unlink(
429
- getTimeoutRedisCommandOptions(this.timeoutMs),
430
- redisKey
589
+ if (!cacheEntry?.lastModified) {
590
+ console.warn(
591
+ "RedisStringsHandler.get() called with",
592
+ key,
593
+ ctx,
594
+ "cacheEntry is mall formed (missing lastModified)"
595
+ );
596
+ }
597
+ if (ctx.kind === "FETCH") {
598
+ const combinedTags = /* @__PURE__ */ new Set([
599
+ ...ctx?.softTags || [],
600
+ ...ctx?.tags || []
601
+ ]);
602
+ if (combinedTags.size === 0) {
603
+ return cacheEntry;
604
+ }
605
+ for (const tag of combinedTags) {
606
+ const revalidationTime = this.revalidatedTagsMap.get(tag);
607
+ if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
608
+ const redisKey = this.keyPrefix + key;
609
+ this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
610
+ console.error(
611
+ "Error occurred while unlinking stale data. Error was:",
612
+ err
613
+ );
614
+ }).finally(async () => {
615
+ await this.sharedTagsMap.delete(key);
616
+ await this.revalidatedTagsMap.delete(tag);
617
+ });
618
+ debug(
619
+ "green",
620
+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
621
+ tag,
622
+ redisKey,
623
+ revalidationTime,
624
+ cacheEntry
431
625
  );
432
- }).finally(async () => {
433
- await this.sharedTagsMap.delete(key);
434
- await this.revalidatedTagsMap.delete(tag);
435
- });
436
- return null;
626
+ return null;
627
+ }
437
628
  }
438
629
  }
439
- return cacheValue;
630
+ return cacheEntry;
440
631
  }
441
632
  async set(key, data, ctx) {
442
- if (data.kind === "FETCH") {
443
- console.time("encoding" + key);
444
- data.data.body = Buffer.from(data.data.body, "base64").toString();
445
- console.timeEnd("encoding" + key);
633
+ if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
634
+ console.warn(
635
+ "RedisStringsHandler.set() called with",
636
+ key,
637
+ ctx,
638
+ data,
639
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
640
+ data?.kind
641
+ );
446
642
  }
447
643
  await this.assertClientIsReady();
448
- data.lastModified = Date.now();
449
- const value = JSON.stringify(data);
644
+ if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
645
+ const tags = data.headers["x-next-cache-tags"]?.split(",");
646
+ ctx.tags = [...ctx.tags || [], ...tags || []];
647
+ }
648
+ const cacheEntry = {
649
+ lastModified: Date.now(),
650
+ tags: ctx?.tags || [],
651
+ value: data
652
+ };
653
+ const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
450
654
  if (this.redisGetDeduplication) {
451
- this.redisDeduplicationHandler.seedRequestReturn(key, value);
655
+ this.redisDeduplicationHandler.seedRequestReturn(
656
+ key,
657
+ serializedCacheEntry
658
+ );
452
659
  }
453
- const expireAt = ctx.revalidate && Number.isSafeInteger(ctx.revalidate) && ctx.revalidate > 0 ? this.estimateExpireAge(ctx.revalidate) : this.estimateExpireAge(this.defaultStaleAge);
660
+ const revalidate = ctx.revalidate || ctx.cacheControl?.revalidate;
661
+ const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
454
662
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
455
663
  const setOperation = this.client.set(
456
664
  options,
457
665
  this.keyPrefix + key,
458
- value,
666
+ serializedCacheEntry,
459
667
  {
460
668
  EX: expireAt
461
669
  }
462
670
  );
671
+ debug(
672
+ "blue",
673
+ "RedisStringsHandler.set() will set the following serializedCacheEntry",
674
+ this.keyPrefix,
675
+ key,
676
+ data,
677
+ ctx,
678
+ serializedCacheEntry?.substring(0, 200),
679
+ expireAt
680
+ );
463
681
  let setTagsOperation;
464
682
  if (ctx.tags && ctx.tags.length > 0) {
465
683
  const currentTags = this.sharedTagsMap.get(key);
@@ -471,27 +689,54 @@ var RedisStringsHandler = class {
471
689
  );
472
690
  }
473
691
  }
692
+ debug(
693
+ "blue",
694
+ "RedisStringsHandler.set() will set the following sharedTagsMap",
695
+ key,
696
+ ctx.tags
697
+ );
474
698
  await Promise.all([setOperation, setTagsOperation]);
475
699
  }
476
- async revalidateTag(tagOrTags) {
700
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
701
+ async revalidateTag(tagOrTags, ...rest) {
702
+ debug(
703
+ "red",
704
+ "RedisStringsHandler.revalidateTag() called with",
705
+ tagOrTags,
706
+ rest
707
+ );
477
708
  const tags = new Set([tagOrTags || []].flat());
478
709
  await this.assertClientIsReady();
710
+ const keysToDelete = /* @__PURE__ */ new Set();
479
711
  for (const tag of tags) {
480
- if (isImplicitTag(tag)) {
712
+ if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
481
713
  const now = Date.now();
714
+ debug(
715
+ "red",
716
+ "RedisStringsHandler.revalidateTag() set revalidation time for tag",
717
+ tag,
718
+ "to",
719
+ now
720
+ );
482
721
  await this.revalidatedTagsMap.set(tag, now);
483
722
  }
484
723
  }
485
- const keysToDelete = [];
486
724
  for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
487
725
  if (sharedTags.some((tag) => tags.has(tag))) {
488
- keysToDelete.push(key);
726
+ keysToDelete.add(key);
489
727
  }
490
728
  }
491
- if (keysToDelete.length === 0) {
729
+ debug(
730
+ "red",
731
+ "RedisStringsHandler.revalidateTag() found",
732
+ keysToDelete,
733
+ "keys to delete"
734
+ );
735
+ if (keysToDelete.size === 0) {
492
736
  return;
493
737
  }
494
- const fullRedisKeys = keysToDelete.map((key) => this.keyPrefix + key);
738
+ const redisKeys = Array.from(keysToDelete);
739
+ const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
495
740
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
496
741
  const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
497
742
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
@@ -499,8 +744,12 @@ var RedisStringsHandler = class {
499
744
  this.inMemoryDeduplicationCache.delete(key);
500
745
  }
501
746
  }
502
- const deleteTagsOperation = this.sharedTagsMap.delete(keysToDelete);
747
+ const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
503
748
  await Promise.all([deleteKeysOperation, deleteTagsOperation]);
749
+ debug(
750
+ "red",
751
+ "RedisStringsHandler.revalidateTag() finished delete operations"
752
+ );
504
753
  }
505
754
  };
506
755
 
@@ -514,12 +763,15 @@ var CachedHandler = class {
514
763
  }
515
764
  }
516
765
  get(...args) {
766
+ debugVerbose("CachedHandler.get called with", args);
517
767
  return cachedHandler.get(...args);
518
768
  }
519
769
  set(...args) {
770
+ debugVerbose("CachedHandler.set called with", args);
520
771
  return cachedHandler.set(...args);
521
772
  }
522
773
  revalidateTag(...args) {
774
+ debugVerbose("CachedHandler.revalidateTag called with", args);
523
775
  return cachedHandler.revalidateTag(...args);
524
776
  }
525
777
  resetRequestCache(...args) {
@@ -529,4 +781,8 @@ var CachedHandler = class {
529
781
 
530
782
  // src/index.ts
531
783
  var index_default = CachedHandler;
784
+ // Annotate the CommonJS export names for ESM import in node:
785
+ 0 && (module.exports = {
786
+ RedisStringsHandler
787
+ });
532
788
  //# sourceMappingURL=index.js.map