@trieb.work/nextjs-turbo-redis-cache 1.17.0 → 1.18.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 (51) hide show
  1. package/.github/workflows/release.yml +8 -157
  2. package/ARCHITECTURE.md +86 -47
  3. package/README.md +87 -24
  4. package/dist/index.d.mts +40 -1
  5. package/dist/index.d.ts +40 -1
  6. package/dist/index.js +264 -96
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +261 -95
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -6
  11. package/release.config.cjs +6 -25
  12. package/src/CacheComponentsHandler.ts +141 -111
  13. package/src/RedisStringsHandler.ts +37 -25
  14. package/src/SyncedMap.ts +16 -2
  15. package/src/index.ts +7 -0
  16. package/src/utils/cacheTtl.ts +71 -0
  17. package/src/utils/compareAndUnlink.ts +29 -0
  18. package/src/utils/redisConnection.ts +17 -0
  19. package/src/utils/tagRevalidation.ts +162 -0
  20. package/test/README.md +10 -10
  21. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +2 -0
  22. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  23. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  24. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +22 -0
  25. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  26. package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  27. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +2 -0
  28. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  29. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  30. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +22 -0
  31. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  32. package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  33. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +2 -0
  34. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
  35. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +23 -0
  36. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +22 -0
  37. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
  38. package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
  39. package/test/playwright/cache-lab.spec.ts +75 -0
  40. package/test/vitest/integration/cache-components/cache-components.integration.test.ts +194 -36
  41. package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +16 -2
  42. package/test/vitest/integration/pages-router.integration.test.ts +16 -6
  43. package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +111 -0
  44. package/test/vitest/unit/cache-components-update-tags.test.ts +350 -0
  45. package/test/vitest/unit/cache-ttl.test.ts +67 -0
  46. package/test/vitest/unit/compare-and-unlink.test.ts +22 -0
  47. package/test/vitest/unit/index.test.ts +17 -0
  48. package/test/vitest/unit/pages-router-kinds.test.ts +11 -0
  49. package/test/vitest/unit/redis-connection.test.ts +39 -0
  50. package/test/vitest/unit/tag-revalidation.test.ts +178 -0
  51. package/CHANGELOG.md +0 -411
package/dist/index.mjs CHANGED
@@ -22,6 +22,12 @@ function debugVerbose(color, ...args) {
22
22
  }
23
23
  }
24
24
 
25
+ // src/utils/redisConnection.ts
26
+ var PHASE_PRODUCTION_BUILD = "phase-production-build";
27
+ function shouldDeferRedisConnection() {
28
+ return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
29
+ }
30
+
25
31
  // src/SyncedMap.ts
26
32
  var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
27
33
  var SyncedMap = class {
@@ -47,6 +53,10 @@ var SyncedMap = class {
47
53
  });
48
54
  }
49
55
  async setup() {
56
+ if (shouldDeferRedisConnection()) {
57
+ this.setupLockResolve();
58
+ return;
59
+ }
50
60
  let setupPromises = [];
51
61
  if (!this.customizedSync?.withoutRedisHashmap) {
52
62
  setupPromises.push(this.initialSync());
@@ -77,7 +87,9 @@ var SyncedMap = class {
77
87
  }
78
88
  cursor = remoteItems.cursor;
79
89
  } while (cursor !== 0);
80
- await this.cleanupKeysNotInRedis();
90
+ if (!this.customizedSync?.withoutOrphanCleanup) {
91
+ await this.cleanupKeysNotInRedis();
92
+ }
81
93
  } catch (error) {
82
94
  console.error("Error during initial sync:", error);
83
95
  throw error;
@@ -489,6 +501,37 @@ var jsonCacheValueSerializer = {
489
501
  }
490
502
  };
491
503
 
504
+ // src/utils/cacheTtl.ts
505
+ function resolveCacheEntryTtlSeconds(ctx, data, options) {
506
+ const expire = ctx.cacheControl?.expire;
507
+ if (Number.isFinite(expire) && expire > 0) {
508
+ return Math.max(1, Math.ceil(expire));
509
+ }
510
+ const revalidate = resolveRevalidateValue(ctx, data);
511
+ if (revalidate === false) {
512
+ return options.estimateExpireAge(options.defaultStaleAge);
513
+ }
514
+ if (revalidate && Number.isSafeInteger(revalidate) && revalidate > 0) {
515
+ return options.estimateExpireAge(revalidate);
516
+ }
517
+ return void 0;
518
+ }
519
+ function resolveRevalidateValue(ctx, data) {
520
+ if (data?.kind === "FETCH" && data.revalidate !== void 0) {
521
+ return data.revalidate;
522
+ }
523
+ if (ctx.revalidate !== void 0) {
524
+ return ctx.revalidate;
525
+ }
526
+ if (ctx.cacheControl?.revalidate !== void 0) {
527
+ return ctx.cacheControl.revalidate;
528
+ }
529
+ if (data?.revalidate !== void 0) {
530
+ return data.revalidate;
531
+ }
532
+ return void 0;
533
+ }
534
+
492
535
  // src/RedisStringsHandler.ts
493
536
  function redisErrorHandler(debugInfo, redisCommandResult) {
494
537
  const beforeTimestamp = performance.now();
@@ -566,6 +609,7 @@ var RedisStringsHandler = class {
566
609
  this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
567
610
  this.getTimeoutMs = getTimeoutMs;
568
611
  this.valueSerializer = valueSerializer;
612
+ this.redisConnectionDeferred = shouldDeferRedisConnection();
569
613
  try {
570
614
  this.client = createClient({
571
615
  url: redisUrl,
@@ -604,12 +648,14 @@ var RedisStringsHandler = class {
604
648
  }, 500);
605
649
  }
606
650
  });
607
- this.client.connect().then(() => {
608
- debug("green", "Redis client connected.");
609
- }).catch((error) => {
610
- console.error("Failed to connect Redis client:", error);
611
- throw error;
612
- });
651
+ if (!this.redisConnectionDeferred) {
652
+ this.client.connect().then(() => {
653
+ debug("green", "Redis client connected.");
654
+ }).catch((error) => {
655
+ console.error("Failed to connect Redis client:", error);
656
+ throw error;
657
+ });
658
+ }
613
659
  } catch (error) {
614
660
  console.error("Failed to initialize Redis client");
615
661
  throw error;
@@ -631,6 +677,7 @@ var RedisStringsHandler = class {
631
677
  database,
632
678
  querySize: revalidateTagQuerySize,
633
679
  filterKeys,
680
+ customizedSync: { withoutOrphanCleanup: true },
634
681
  resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
635
682
  });
636
683
  this.inMemoryDeduplicationCache = new SyncedMap({
@@ -700,11 +747,17 @@ var RedisStringsHandler = class {
700
747
  }
701
748
  this.clientReadyCalls = 0;
702
749
  if (!this.client.isReady) {
750
+ if (this.redisConnectionDeferred) {
751
+ return;
752
+ }
703
753
  throw new Error(
704
754
  "assertClientIsReady: Redis client is not ready yet or connection is lost."
705
755
  );
706
756
  }
707
757
  }
758
+ isClientUnavailable() {
759
+ return this.redisConnectionDeferred || !this.client.isReady;
760
+ }
708
761
  async get(key, ctx) {
709
762
  try {
710
763
  if (!SUPPORTED_GET_KINDS.includes(ctx.kind)) {
@@ -718,6 +771,9 @@ var RedisStringsHandler = class {
718
771
  }
719
772
  debug("green", "RedisStringsHandler.get() called with", key, ctx);
720
773
  await this.assertClientIsReady();
774
+ if (this.isClientUnavailable()) {
775
+ return null;
776
+ }
721
777
  const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
722
778
  const redisGetOperation = clientGet(
723
779
  commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
@@ -851,6 +907,9 @@ var RedisStringsHandler = class {
851
907
  );
852
908
  }
853
909
  await this.assertClientIsReady();
910
+ if (this.isClientUnavailable()) {
911
+ return;
912
+ }
854
913
  if (data?.kind === "APP_PAGE" || data?.kind === "APP_ROUTE") {
855
914
  const tags = data.headers["x-next-cache-tags"]?.split(",");
856
915
  ctx.tags = [...ctx.tags || [], ...tags || []];
@@ -873,18 +932,14 @@ var RedisStringsHandler = class {
873
932
  serializedCacheEntry
874
933
  );
875
934
  }
876
- const revalidate = (
877
- // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
878
- data?.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || // Legacy fallback: older Next.js versions attached `revalidate` directly
879
- // to the data payload. FETCH is already handled above, so this only
880
- // matters for those older, non-discriminated shapes.
881
- data?.revalidate
882
- );
883
- const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
935
+ const expireAt = resolveCacheEntryTtlSeconds(ctx, data, {
936
+ estimateExpireAge: this.estimateExpireAge,
937
+ defaultStaleAge: this.defaultStaleAge
938
+ });
884
939
  const setOperation = redisErrorHandler(
885
940
  "RedisStringsHandler.set(), operation: set " + this.keyPrefix + " " + key,
886
941
  this.client.set(this.keyPrefix + key, serializedCacheEntry, {
887
- EX: expireAt
942
+ ...expireAt ? { EX: expireAt } : {}
888
943
  })
889
944
  );
890
945
  debug(
@@ -947,6 +1002,9 @@ var RedisStringsHandler = class {
947
1002
  );
948
1003
  const tags = new Set([tagOrTags || []].flat());
949
1004
  await this.assertClientIsReady();
1005
+ if (this.isClientUnavailable()) {
1006
+ return;
1007
+ }
950
1008
  const keysToDelete = /* @__PURE__ */ new Set();
951
1009
  for (const tag of tags) {
952
1010
  if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
@@ -1085,6 +1143,88 @@ var CachedHandler = class {
1085
1143
 
1086
1144
  // src/CacheComponentsHandler.ts
1087
1145
  import { commandOptions as commandOptions2, createClient as createClient2 } from "redis";
1146
+
1147
+ // src/utils/compareAndUnlink.ts
1148
+ var COMPARE_AND_UNLINK = `
1149
+ if redis.call('GET', KEYS[1]) == ARGV[1] then
1150
+ return redis.call('UNLINK', KEYS[1])
1151
+ end
1152
+ return 0
1153
+ `;
1154
+ async function compareAndUnlink(client, key, expected) {
1155
+ const deleted = await client.eval(COMPARE_AND_UNLINK, {
1156
+ keys: [key],
1157
+ arguments: [expected]
1158
+ });
1159
+ return deleted === 1 || deleted === 1n;
1160
+ }
1161
+
1162
+ // src/utils/tagRevalidation.ts
1163
+ function normalizeTagManifest(stored, now = Date.now()) {
1164
+ if (typeof stored === "number" && Number.isFinite(stored)) {
1165
+ return { expired: Math.min(stored, now) };
1166
+ }
1167
+ if (stored && typeof stored === "object") {
1168
+ const rec = stored;
1169
+ const stale = typeof rec.stale === "number" && Number.isFinite(rec.stale) ? rec.stale : typeof rec.last === "number" && Number.isFinite(rec.last) ? rec.last : void 0;
1170
+ const expired = typeof rec.expired === "number" && Number.isFinite(rec.expired) ? rec.expired : typeof rec.pending === "number" && Number.isFinite(rec.pending) ? rec.pending : void 0;
1171
+ return {
1172
+ ...stale !== void 0 ? { stale } : {},
1173
+ ...expired !== void 0 ? { expired } : {}
1174
+ };
1175
+ }
1176
+ return {};
1177
+ }
1178
+ function applyTagUpdate(existing, durations, now) {
1179
+ if (durations) {
1180
+ const updates = { ...existing };
1181
+ updates.stale = now;
1182
+ if (durations.expire !== void 0) {
1183
+ updates.expired = now + durations.expire * 1e3;
1184
+ }
1185
+ return updates;
1186
+ }
1187
+ return {
1188
+ ...existing,
1189
+ expired: now
1190
+ };
1191
+ }
1192
+ function persistableTagManifest(entry, now) {
1193
+ if (typeof entry.expired === "number" && entry.expired <= now && (entry.stale === void 0 || entry.stale <= entry.expired)) {
1194
+ return entry.expired;
1195
+ }
1196
+ return entry;
1197
+ }
1198
+ function areTagsExpired(tags, entryTimestamp, now, getEntry) {
1199
+ for (const tag of tags) {
1200
+ const expiredAt = getEntry(tag)?.expired;
1201
+ if (typeof expiredAt === "number" && expiredAt <= now && expiredAt > entryTimestamp) {
1202
+ return true;
1203
+ }
1204
+ }
1205
+ return false;
1206
+ }
1207
+ function areTagsStale(tags, entryTimestamp, getEntry) {
1208
+ for (const tag of tags) {
1209
+ const staleAt = getEntry(tag)?.stale ?? 0;
1210
+ if (typeof staleAt === "number" && staleAt > entryTimestamp) {
1211
+ return true;
1212
+ }
1213
+ }
1214
+ return false;
1215
+ }
1216
+ function maxExpiredTimestamp(tags, getEntry) {
1217
+ let max = 0;
1218
+ for (const tag of tags) {
1219
+ const expired = getEntry(tag)?.expired || 0;
1220
+ if (expired > max) {
1221
+ max = expired;
1222
+ }
1223
+ }
1224
+ return max;
1225
+ }
1226
+
1227
+ // src/CacheComponentsHandler.ts
1088
1228
  var REVALIDATED_TAGS_KEY2 = "__cacheComponents_revalidated_tags__";
1089
1229
  var SHARED_TAGS_KEY = "__cacheComponents_sharedTags__";
1090
1230
  var killContainerOnErrorCount2 = 0;
@@ -1142,6 +1282,7 @@ var RedisCacheComponentsHandler = class {
1142
1282
  this.getTimeoutMs = getTimeoutMs;
1143
1283
  this.redisGetDeduplication = redisGetDeduplication;
1144
1284
  this.inMemoryCachingTime = inMemoryCachingTime;
1285
+ this.redisConnectionDeferred = shouldDeferRedisConnection();
1145
1286
  this.client = createClient2({
1146
1287
  url: redisUrl,
1147
1288
  pingInterval: 1e4,
@@ -1177,18 +1318,20 @@ var RedisCacheComponentsHandler = class {
1177
1318
  }, 500);
1178
1319
  }
1179
1320
  });
1180
- this.client.connect().then(() => {
1181
- debug("green", "RedisCacheComponentsHandler client connected.");
1182
- }).catch(() => {
1183
- this.client.connect().catch((error) => {
1184
- console.error(
1185
- "Failed to connect RedisCacheComponentsHandler client:",
1186
- error
1187
- );
1188
- this.client.disconnect();
1189
- throw error;
1321
+ if (!this.redisConnectionDeferred) {
1322
+ this.client.connect().then(() => {
1323
+ debug("green", "RedisCacheComponentsHandler client connected.");
1324
+ }).catch(() => {
1325
+ this.client.connect().catch((error) => {
1326
+ console.error(
1327
+ "Failed to connect RedisCacheComponentsHandler client:",
1328
+ error
1329
+ );
1330
+ this.client.disconnect();
1331
+ throw error;
1332
+ });
1190
1333
  });
1191
- });
1334
+ }
1192
1335
  const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY2 && key !== SHARED_TAGS_KEY;
1193
1336
  this.revalidatedTagsMap = new SyncedMap({
1194
1337
  client: this.client,
@@ -1197,6 +1340,7 @@ var RedisCacheComponentsHandler = class {
1197
1340
  database,
1198
1341
  querySize: revalidateTagQuerySize,
1199
1342
  filterKeys,
1343
+ customizedSync: { withoutOrphanCleanup: true },
1200
1344
  resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
1201
1345
  });
1202
1346
  this.sharedTagsMap = new SyncedMap({
@@ -1234,6 +1378,13 @@ var RedisCacheComponentsHandler = class {
1234
1378
  }
1235
1379
  }
1236
1380
  async assertClientIsReady() {
1381
+ if (this.redisConnectionDeferred) {
1382
+ await Promise.all([
1383
+ this.revalidatedTagsMap.waitUntilReady(),
1384
+ this.sharedTagsMap.waitUntilReady()
1385
+ ]);
1386
+ return;
1387
+ }
1237
1388
  if (!this.client.isReady && !this.client.isOpen) {
1238
1389
  await this.client.connect().catch((error) => {
1239
1390
  console.error(
@@ -1248,57 +1399,76 @@ var RedisCacheComponentsHandler = class {
1248
1399
  this.sharedTagsMap.waitUntilReady()
1249
1400
  ]);
1250
1401
  }
1251
- async computeMaxRevalidation(tags) {
1252
- let max = 0;
1253
- for (const tag of tags) {
1254
- const ts = this.revalidatedTagsMap.get(tag);
1255
- if (ts && ts > max) {
1256
- max = ts;
1257
- }
1258
- }
1259
- return max;
1402
+ isClientUnavailable() {
1403
+ return this.redisConnectionDeferred || !this.client.isReady;
1260
1404
  }
1261
- async get(cacheKey, softTags) {
1405
+ tagManifestFor(tag, now = Date.now()) {
1406
+ return normalizeTagManifest(this.revalidatedTagsMap.get(tag), now);
1407
+ }
1408
+ async get(cacheKey, _softTags) {
1409
+ void _softTags;
1262
1410
  const redisKey = `${this.keyPrefix}${cacheKey}`;
1263
1411
  try {
1264
1412
  await this.assertClientIsReady();
1265
- const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(cacheKey) : this.redisGet;
1266
- const serialized = await redisErrorHandler(
1267
- "RedisCacheComponentsHandler.get(), operation: get" + (this.redisGetDeduplication ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
1268
- clientGet(
1413
+ if (this.isClientUnavailable()) {
1414
+ return void 0;
1415
+ }
1416
+ const readSerialized = (useDedup) => redisErrorHandler(
1417
+ "RedisCacheComponentsHandler.get(), operation: get" + (useDedup ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
1418
+ (useDedup ? this.deduplicatedRedisGet(cacheKey) : this.redisGet)(
1269
1419
  commandOptions2({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
1270
1420
  redisKey
1271
1421
  )
1272
1422
  );
1273
- if (!serialized) {
1274
- return void 0;
1275
- }
1276
- const stored = JSON.parse(serialized);
1277
- const now = Date.now();
1278
- const expiryTime = stored.timestamp + stored.expire * 1e3;
1279
- if (Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime) {
1280
- await this.client.unlink(redisKey).catch(() => {
1281
- });
1282
- await this.sharedTagsMap.delete(cacheKey).catch(() => {
1283
- });
1284
- return void 0;
1285
- }
1286
- const maxRevalidation = await this.computeMaxRevalidation([
1287
- ...stored.tags || [],
1288
- ...softTags || []
1289
- ]);
1290
- if (maxRevalidation > 0 && maxRevalidation > stored.timestamp) {
1291
- await this.client.unlink(redisKey).catch(() => {
1292
- });
1293
- await this.sharedTagsMap.delete(cacheKey).catch(() => {
1294
- });
1295
- return void 0;
1423
+ let usedDedup = this.redisGetDeduplication;
1424
+ let serialized = await readSerialized(usedDedup);
1425
+ for (let attempt = 0; attempt < 2; attempt++) {
1426
+ if (!serialized) {
1427
+ return void 0;
1428
+ }
1429
+ const stored = JSON.parse(serialized);
1430
+ const now = Date.now();
1431
+ const tags = stored.tags || [];
1432
+ const expiryTime = stored.timestamp + stored.expire * 1e3;
1433
+ const ttlExpired = Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime;
1434
+ const tagsExpired = areTagsExpired(
1435
+ tags,
1436
+ stored.timestamp,
1437
+ now,
1438
+ (tag) => this.tagManifestFor(tag, now)
1439
+ );
1440
+ if (ttlExpired || tagsExpired) {
1441
+ if (usedDedup) {
1442
+ usedDedup = false;
1443
+ serialized = await readSerialized(false);
1444
+ continue;
1445
+ }
1446
+ const dropped = await compareAndUnlink(
1447
+ this.client,
1448
+ redisKey,
1449
+ serialized
1450
+ );
1451
+ if (dropped) {
1452
+ await this.sharedTagsMap.delete(cacheKey).catch(() => {
1453
+ });
1454
+ }
1455
+ return void 0;
1456
+ }
1457
+ const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
1458
+ const entry = {
1459
+ ...stored,
1460
+ value: bufferToReadableStream(valueBuffer)
1461
+ };
1462
+ if (areTagsStale(
1463
+ tags,
1464
+ stored.timestamp,
1465
+ (tag) => this.tagManifestFor(tag, now)
1466
+ )) {
1467
+ entry.revalidate = -1;
1468
+ }
1469
+ return entry;
1296
1470
  }
1297
- const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
1298
- return {
1299
- ...stored,
1300
- value: bufferToReadableStream(valueBuffer)
1301
- };
1471
+ return void 0;
1302
1472
  } catch (error) {
1303
1473
  console.error(
1304
1474
  "RedisCacheComponentsHandler.get() Error occurred while getting cache entry. Returning undefined so site can continue to serve content while cache is disabled. The original error was:",
@@ -1311,6 +1481,9 @@ var RedisCacheComponentsHandler = class {
1311
1481
  async set(cacheKey, pendingEntry) {
1312
1482
  try {
1313
1483
  await this.assertClientIsReady();
1484
+ if (this.isClientUnavailable()) {
1485
+ return;
1486
+ }
1314
1487
  const entry = await pendingEntry;
1315
1488
  const [storeStream] = entry.value.tee();
1316
1489
  const buffer = await streamToBuffer(storeStream);
@@ -1374,7 +1547,7 @@ var RedisCacheComponentsHandler = class {
1374
1547
  async getExpiration(tags) {
1375
1548
  try {
1376
1549
  await this.assertClientIsReady();
1377
- return this.computeMaxRevalidation(tags || []);
1550
+ return maxExpiredTimestamp(tags || [], (tag) => this.tagManifestFor(tag));
1378
1551
  } catch (error) {
1379
1552
  console.error(
1380
1553
  "RedisCacheComponentsHandler.getExpiration() Error occurred while getting expiration for tags. The original error was:",
@@ -1383,37 +1556,28 @@ var RedisCacheComponentsHandler = class {
1383
1556
  return 0;
1384
1557
  }
1385
1558
  }
1386
- async updateTags(tags, _durations) {
1559
+ async updateTags(tags, durations) {
1387
1560
  try {
1388
- void _durations;
1389
1561
  await this.assertClientIsReady();
1390
- const now = Date.now();
1391
- const tagsSet = new Set(tags || []);
1392
- for (const tag of tagsSet) {
1393
- await this.revalidatedTagsMap.set(tag, now);
1394
- }
1395
- const keysToDelete = /* @__PURE__ */ new Set();
1396
- for (const [key, storedTags] of this.sharedTagsMap.entries()) {
1397
- if (storedTags.some((tag) => tagsSet.has(tag))) {
1398
- keysToDelete.add(key);
1399
- }
1400
- }
1401
- if (keysToDelete.size === 0) {
1562
+ if (this.isClientUnavailable()) {
1402
1563
  return;
1403
1564
  }
1404
- const cacheKeys = Array.from(keysToDelete);
1405
- const fullRedisKeys = cacheKeys.map((key) => `${this.keyPrefix}${key}`);
1406
- await redisErrorHandler(
1407
- "RedisCacheComponentsHandler.updateTags(), operation: unlink",
1408
- this.client.unlink(fullRedisKeys)
1409
- );
1565
+ const now = Date.now();
1566
+ const updated = new Set(tags || []);
1567
+ for (const tag of updated) {
1568
+ const next = applyTagUpdate(this.tagManifestFor(tag), durations, now);
1569
+ await this.revalidatedTagsMap.set(
1570
+ tag,
1571
+ persistableTagManifest(next, now)
1572
+ );
1573
+ }
1410
1574
  if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
1411
- for (const key of keysToDelete) {
1412
- this.inMemoryDeduplicationCache.delete(key);
1575
+ for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
1576
+ if (sharedTags.some((tag) => updated.has(tag))) {
1577
+ await this.inMemoryDeduplicationCache.delete(key, true);
1578
+ }
1413
1579
  }
1414
1580
  }
1415
- const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
1416
- await deleteTagsOperation;
1417
1581
  } catch (error) {
1418
1582
  console.error(
1419
1583
  "RedisCacheComponentsHandler.updateTags() Error occurred while updating tags. The original error was:",
@@ -1457,6 +1621,8 @@ export {
1457
1621
  index_default as default,
1458
1622
  getRedisCacheComponentsHandler,
1459
1623
  jsonCacheValueSerializer,
1460
- redisCacheHandler
1624
+ redisCacheHandler,
1625
+ resolveCacheEntryTtlSeconds,
1626
+ shouldDeferRedisConnection
1461
1627
  };
1462
1628
  //# sourceMappingURL=index.mjs.map