@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.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,150 @@ 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. Error was:",
585
+ err
586
+ );
587
+ }).finally(async () => {
588
+ await this.sharedTagsMap.delete(key);
589
+ await this.revalidatedTagsMap.delete(tag);
590
+ });
591
+ debug(
592
+ "green",
593
+ 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
594
+ tag,
595
+ redisKey,
596
+ revalidationTime,
597
+ cacheEntry
405
598
  );
406
- }).finally(async () => {
407
- await this.sharedTagsMap.delete(key);
408
- await this.revalidatedTagsMap.delete(tag);
409
- });
410
- return null;
599
+ return null;
600
+ }
411
601
  }
412
602
  }
413
- return cacheValue;
603
+ return cacheEntry;
414
604
  }
415
605
  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);
606
+ if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
607
+ console.warn(
608
+ "RedisStringsHandler.set() called with",
609
+ key,
610
+ ctx,
611
+ data,
612
+ " this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
613
+ data?.kind
614
+ );
420
615
  }
421
616
  await this.assertClientIsReady();
422
- data.lastModified = Date.now();
423
- const value = JSON.stringify(data);
617
+ if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
618
+ const tags = data.headers["x-next-cache-tags"]?.split(",");
619
+ ctx.tags = [...ctx.tags || [], ...tags || []];
620
+ }
621
+ const cacheEntry = {
622
+ lastModified: Date.now(),
623
+ tags: ctx?.tags || [],
624
+ value: data
625
+ };
626
+ const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
424
627
  if (this.redisGetDeduplication) {
425
- this.redisDeduplicationHandler.seedRequestReturn(key, value);
628
+ this.redisDeduplicationHandler.seedRequestReturn(
629
+ key,
630
+ serializedCacheEntry
631
+ );
426
632
  }
427
- const expireAt = ctx.revalidate && Number.isSafeInteger(ctx.revalidate) && ctx.revalidate > 0 ? this.estimateExpireAge(ctx.revalidate) : this.estimateExpireAge(this.defaultStaleAge);
633
+ const revalidate = ctx.revalidate || ctx.cacheControl?.revalidate;
634
+ const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
428
635
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
429
636
  const setOperation = this.client.set(
430
637
  options,
431
638
  this.keyPrefix + key,
432
- value,
639
+ serializedCacheEntry,
433
640
  {
434
641
  EX: expireAt
435
642
  }
436
643
  );
644
+ debug(
645
+ "blue",
646
+ "RedisStringsHandler.set() will set the following serializedCacheEntry",
647
+ this.keyPrefix,
648
+ key,
649
+ data,
650
+ ctx,
651
+ serializedCacheEntry?.substring(0, 200),
652
+ expireAt
653
+ );
437
654
  let setTagsOperation;
438
655
  if (ctx.tags && ctx.tags.length > 0) {
439
656
  const currentTags = this.sharedTagsMap.get(key);
@@ -445,27 +662,54 @@ var RedisStringsHandler = class {
445
662
  );
446
663
  }
447
664
  }
665
+ debug(
666
+ "blue",
667
+ "RedisStringsHandler.set() will set the following sharedTagsMap",
668
+ key,
669
+ ctx.tags
670
+ );
448
671
  await Promise.all([setOperation, setTagsOperation]);
449
672
  }
450
- async revalidateTag(tagOrTags) {
673
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
674
+ async revalidateTag(tagOrTags, ...rest) {
675
+ debug(
676
+ "red",
677
+ "RedisStringsHandler.revalidateTag() called with",
678
+ tagOrTags,
679
+ rest
680
+ );
451
681
  const tags = new Set([tagOrTags || []].flat());
452
682
  await this.assertClientIsReady();
683
+ const keysToDelete = /* @__PURE__ */ new Set();
453
684
  for (const tag of tags) {
454
- if (isImplicitTag(tag)) {
685
+ if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
455
686
  const now = Date.now();
687
+ debug(
688
+ "red",
689
+ "RedisStringsHandler.revalidateTag() set revalidation time for tag",
690
+ tag,
691
+ "to",
692
+ now
693
+ );
456
694
  await this.revalidatedTagsMap.set(tag, now);
457
695
  }
458
696
  }
459
- const keysToDelete = [];
460
697
  for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
461
698
  if (sharedTags.some((tag) => tags.has(tag))) {
462
- keysToDelete.push(key);
699
+ keysToDelete.add(key);
463
700
  }
464
701
  }
465
- if (keysToDelete.length === 0) {
702
+ debug(
703
+ "red",
704
+ "RedisStringsHandler.revalidateTag() found",
705
+ keysToDelete,
706
+ "keys to delete"
707
+ );
708
+ if (keysToDelete.size === 0) {
466
709
  return;
467
710
  }
468
- const fullRedisKeys = keysToDelete.map((key) => this.keyPrefix + key);
711
+ const redisKeys = Array.from(keysToDelete);
712
+ const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
469
713
  const options = getTimeoutRedisCommandOptions(this.timeoutMs);
470
714
  const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
471
715
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
@@ -473,8 +717,12 @@ var RedisStringsHandler = class {
473
717
  this.inMemoryDeduplicationCache.delete(key);
474
718
  }
475
719
  }
476
- const deleteTagsOperation = this.sharedTagsMap.delete(keysToDelete);
720
+ const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
477
721
  await Promise.all([deleteKeysOperation, deleteTagsOperation]);
722
+ debug(
723
+ "red",
724
+ "RedisStringsHandler.revalidateTag() finished delete operations"
725
+ );
478
726
  }
479
727
  };
480
728
 
@@ -488,12 +736,15 @@ var CachedHandler = class {
488
736
  }
489
737
  }
490
738
  get(...args) {
739
+ debugVerbose("CachedHandler.get called with", args);
491
740
  return cachedHandler.get(...args);
492
741
  }
493
742
  set(...args) {
743
+ debugVerbose("CachedHandler.set called with", args);
494
744
  return cachedHandler.set(...args);
495
745
  }
496
746
  revalidateTag(...args) {
747
+ debugVerbose("CachedHandler.revalidateTag called with", args);
497
748
  return cachedHandler.revalidateTag(...args);
498
749
  }
499
750
  resetRequestCache(...args) {
@@ -504,6 +755,7 @@ var CachedHandler = class {
504
755
  // src/index.ts
505
756
  var index_default = CachedHandler;
506
757
  export {
758
+ RedisStringsHandler,
507
759
  index_default as default
508
760
  };
509
761
  //# sourceMappingURL=index.mjs.map