@leejpsd/nextjs-cache-handler 0.2.0 → 0.3.1

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 (43) hide show
  1. package/CHANGELOG.md +67 -2
  2. package/README.md +47 -25
  3. package/dist/cache-components/index.cjs +235 -46
  4. package/dist/cache-components/index.cjs.map +1 -1
  5. package/dist/cache-components/index.d.cts +1 -1
  6. package/dist/cache-components/index.d.ts +1 -1
  7. package/dist/cache-components/index.js +227 -46
  8. package/dist/cache-components/index.js.map +1 -1
  9. package/dist/incremental/index.cjs +231 -41
  10. package/dist/incremental/index.cjs.map +1 -1
  11. package/dist/incremental/index.d.cts +42 -3
  12. package/dist/incremental/index.d.ts +42 -3
  13. package/dist/incremental/index.js +223 -41
  14. package/dist/incremental/index.js.map +1 -1
  15. package/dist/index.cjs +313 -70
  16. package/dist/index.cjs.map +1 -1
  17. package/dist/index.d.cts +1 -1
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +305 -70
  20. package/dist/index.js.map +1 -1
  21. package/dist/ops/index.d.cts +1 -1
  22. package/dist/ops/index.d.ts +1 -1
  23. package/dist/otel/index.cjs +72 -0
  24. package/dist/otel/index.cjs.map +1 -0
  25. package/dist/otel/index.d.cts +72 -0
  26. package/dist/otel/index.d.ts +72 -0
  27. package/dist/otel/index.js +64 -0
  28. package/dist/otel/index.js.map +1 -0
  29. package/dist/shared/client/adapter-ioredis.cjs +49 -10
  30. package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
  31. package/dist/shared/client/adapter-ioredis.d.cts +5 -2
  32. package/dist/shared/client/adapter-ioredis.d.ts +5 -2
  33. package/dist/shared/client/adapter-ioredis.js +43 -11
  34. package/dist/shared/client/adapter-ioredis.js.map +1 -1
  35. package/dist/shared/client/adapter-redis.cjs +22 -7
  36. package/dist/shared/client/adapter-redis.cjs.map +1 -1
  37. package/dist/shared/client/adapter-redis.d.cts +1 -1
  38. package/dist/shared/client/adapter-redis.d.ts +1 -1
  39. package/dist/shared/client/adapter-redis.js +16 -7
  40. package/dist/shared/client/adapter-redis.js.map +1 -1
  41. package/dist/{types-Dq1q6qXd.d.cts → types-Bl-rkLKX.d.cts} +37 -1
  42. package/dist/{types-Dq1q6qXd.d.ts → types-Bl-rkLKX.d.ts} +37 -1
  43. package/package.json +27 -5
@@ -1,13 +1,12 @@
1
+ import path2 from 'path';
2
+ import 'url';
3
+ import { createRequire } from 'module';
4
+ import { promisify } from 'util';
5
+ import { gzip, gunzip, brotliCompress, brotliDecompress, constants } from 'zlib';
1
6
  import { createHash } from 'crypto';
2
7
 
3
8
  var __defProp = Object.defineProperty;
4
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
7
- }) : x)(function(x) {
8
- if (typeof require !== "undefined") return require.apply(this, arguments);
9
- throw Error('Dynamic require of "' + x + '" is not supported');
10
- });
11
10
  var __esm = (fn, res) => function __init() {
12
11
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
12
  };
@@ -15,6 +14,27 @@ var __export = (target, all) => {
15
14
  for (var name in all)
16
15
  __defProp(target, name, { get: all[name], enumerable: true });
17
16
  };
17
+ var init_esm_shims = __esm({
18
+ "node_modules/tsup/assets/esm_shims.js"() {
19
+ }
20
+ });
21
+ function nodeRequire(id) {
22
+ try {
23
+ return packageRequire(id);
24
+ } catch {
25
+ return cwdRequire(id);
26
+ }
27
+ }
28
+ var packageRequire, cwdRequire;
29
+ var init_node_require = __esm({
30
+ "src/shared/node-require.ts"() {
31
+ init_esm_shims();
32
+ packageRequire = createRequire(import.meta.url);
33
+ cwdRequire = createRequire(
34
+ path2.join(process.cwd(), "__resolve-anchor__.js")
35
+ );
36
+ }
37
+ });
18
38
 
19
39
  // src/shared/client/adapter-redis.ts
20
40
  var adapter_redis_exports = {};
@@ -26,7 +46,7 @@ function adaptRedisV5(client) {
26
46
  return client;
27
47
  }
28
48
  function createRedisV5Client(config) {
29
- const { createClient } = __require("redis");
49
+ const { createClient } = nodeRequire("redis");
30
50
  const useTls = config.tls ?? config.url.startsWith("rediss://");
31
51
  const opts = {
32
52
  url: config.url,
@@ -41,6 +61,8 @@ function createRedisV5Client(config) {
41
61
  }
42
62
  var init_adapter_redis = __esm({
43
63
  "src/shared/client/adapter-redis.ts"() {
64
+ init_esm_shims();
65
+ init_node_require();
44
66
  }
45
67
  });
46
68
 
@@ -50,7 +72,8 @@ __export(adapter_ioredis_exports, {
50
72
  adaptCluster: () => adaptCluster,
51
73
  adaptIoredis: () => adaptIoredis,
52
74
  createIoredisClient: () => createIoredisClient,
53
- createIoredisCluster: () => createIoredisCluster
75
+ createIoredisCluster: () => createIoredisCluster,
76
+ createIoredisSentinel: () => createIoredisSentinel
54
77
  });
55
78
  function adapt(client, isClusterClient = false) {
56
79
  return {
@@ -133,6 +156,9 @@ function adapt(client, isClusterClient = false) {
133
156
  on: (event, listener) => {
134
157
  client.on(event, listener);
135
158
  return client;
159
+ },
160
+ dispose: () => {
161
+ client.disconnect();
136
162
  }
137
163
  };
138
164
  }
@@ -143,18 +169,36 @@ function adaptCluster(cluster) {
143
169
  return adapt(cluster, true);
144
170
  }
145
171
  function createIoredisClient(config) {
146
- const IORedis = __require("ioredis");
172
+ const IORedis = nodeRequire("ioredis");
147
173
  const Ctor = IORedis.default ?? IORedis;
148
174
  return new Ctor(config.url, {
149
175
  password: config.password,
150
176
  connectTimeout: config.connectTimeout ?? 1500,
151
177
  tls: config.tls ? {} : void 0,
152
178
  lazyConnect: true,
153
- maxRetriesPerRequest: 1
179
+ maxRetriesPerRequest: 1,
180
+ // Batch same-tick commands into one RTT (matches redis@5's default
181
+ // behavior) — makes Promise.all fan-outs a single pipeline.
182
+ enableAutoPipelining: true
183
+ });
184
+ }
185
+ function createIoredisSentinel(config) {
186
+ const IORedis = nodeRequire("ioredis");
187
+ const Ctor = IORedis.default ?? IORedis;
188
+ return new Ctor({
189
+ sentinels: config.sentinels,
190
+ name: config.name,
191
+ password: config.password,
192
+ sentinelPassword: config.sentinelPassword,
193
+ connectTimeout: config.connectTimeout ?? 1500,
194
+ tls: config.tls ? {} : void 0,
195
+ lazyConnect: true,
196
+ maxRetriesPerRequest: 1,
197
+ enableAutoPipelining: true
154
198
  });
155
199
  }
156
200
  function createIoredisCluster(config) {
157
- const IORedis = __require("ioredis");
201
+ const IORedis = nodeRequire("ioredis");
158
202
  const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
159
203
  return new ClusterCtor(config.nodes, {
160
204
  redisOptions: {
@@ -162,15 +206,26 @@ function createIoredisCluster(config) {
162
206
  tls: config.tls ? {} : void 0
163
207
  },
164
208
  enableReadyCheck: true,
165
- lazyConnect: true
209
+ lazyConnect: true,
210
+ // Same-slot commands issued in the same tick share one pipeline.
211
+ enableAutoPipelining: true
166
212
  });
167
213
  }
168
214
  var init_adapter_ioredis = __esm({
169
215
  "src/shared/client/adapter-ioredis.ts"() {
216
+ init_esm_shims();
217
+ init_node_require();
170
218
  }
171
219
  });
172
220
 
221
+ // src/cache-components/index.ts
222
+ init_esm_shims();
223
+
224
+ // src/cache-components/handler.ts
225
+ init_esm_shims();
226
+
173
227
  // src/shared/abort.ts
228
+ init_esm_shims();
174
229
  var CacheTimeoutError = class extends Error {
175
230
  constructor(opName, ms) {
176
231
  super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
@@ -202,7 +257,41 @@ async function withAbortSignal(opName, ms, fn) {
202
257
  }
203
258
  }
204
259
 
260
+ // src/shared/compress.ts
261
+ init_esm_shims();
262
+ var gzipAsync = promisify(gzip);
263
+ var gunzipAsync = promisify(gunzip);
264
+ var brotliCompressAsync = promisify(brotliCompress);
265
+ var brotliDecompressAsync = promisify(brotliDecompress);
266
+ var GZIP_MARKER = "__ncgz__:";
267
+ var BROTLI_MARKER = "__ncbr__:";
268
+ var MIN_COMPRESS_BYTES = 1024;
269
+ var BROTLI_OPTS = {
270
+ params: { [constants.BROTLI_PARAM_QUALITY]: 5 }
271
+ };
272
+ async function compressValue(value, algo) {
273
+ if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
274
+ const input = Buffer.from(value);
275
+ if (algo === "gzip") {
276
+ return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
277
+ }
278
+ return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
279
+ }
280
+ async function decompressValue(value) {
281
+ if (value === null) return null;
282
+ if (value.startsWith(GZIP_MARKER)) {
283
+ const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
284
+ return (await gunzipAsync(buf)).toString("utf8");
285
+ }
286
+ if (value.startsWith(BROTLI_MARKER)) {
287
+ const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
288
+ return (await brotliDecompressAsync(buf)).toString("utf8");
289
+ }
290
+ return value;
291
+ }
292
+
205
293
  // src/shared/client/index.ts
294
+ init_esm_shims();
206
295
  async function buildClient(source) {
207
296
  if (typeof source === "function") {
208
297
  return await source();
@@ -220,6 +309,10 @@ async function buildClient(source) {
220
309
  const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
221
310
  return adaptCluster2(createIoredisCluster2(source));
222
311
  }
312
+ case "sentinel": {
313
+ const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
314
+ return adaptIoredis2(createIoredisSentinel2(source));
315
+ }
223
316
  default: {
224
317
  const exhaustive = source;
225
318
  throw new Error(
@@ -229,28 +322,45 @@ async function buildClient(source) {
229
322
  }
230
323
  }
231
324
  var ConnectionManager = class {
232
- constructor(source, onError) {
325
+ constructor(source, onError, retry = {}) {
233
326
  this.source = source;
234
327
  this.onError = onError;
328
+ this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
329
+ this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
235
330
  }
236
331
  source;
237
332
  onError;
238
333
  client = null;
239
334
  connecting = null;
240
- failed = false;
335
+ consecutiveFailures = 0;
336
+ lastFailureAt = 0;
337
+ baseCooldownMs;
338
+ maxCooldownMs;
339
+ cooldownMs() {
340
+ if (this.consecutiveFailures === 0) return 0;
341
+ return Math.min(
342
+ this.maxCooldownMs,
343
+ this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
344
+ );
345
+ }
241
346
  async getOrConnect() {
242
347
  if (this.client?.isOpen) return this.client;
243
- if (this.failed) return null;
244
348
  if (this.connecting) return this.connecting;
349
+ if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
350
+ const stale = this.client;
351
+ this.client = null;
245
352
  this.connecting = (async () => {
353
+ disposeQuietly(stale);
246
354
  try {
247
355
  const c = await buildClient(this.source);
248
356
  c.on("error", (err) => this.onError(err));
249
357
  if (!c.isOpen) await c.connect();
250
358
  this.client = c;
359
+ this.consecutiveFailures = 0;
251
360
  return c;
252
361
  } catch (err) {
253
- this.failed = true;
362
+ this.consecutiveFailures += 1;
363
+ this.lastFailureAt = Date.now();
254
364
  this.onError(err);
255
365
  return null;
256
366
  } finally {
@@ -260,13 +370,24 @@ var ConnectionManager = class {
260
370
  return this.connecting;
261
371
  }
262
372
  reset() {
373
+ disposeQuietly(this.client);
263
374
  this.client = null;
264
375
  this.connecting = null;
265
- this.failed = false;
376
+ this.consecutiveFailures = 0;
377
+ this.lastFailureAt = 0;
266
378
  }
267
379
  };
380
+ function disposeQuietly(client) {
381
+ if (!client) return;
382
+ const c = client;
383
+ try {
384
+ (c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
385
+ } catch {
386
+ }
387
+ }
268
388
 
269
389
  // src/shared/logger.ts
390
+ init_esm_shims();
270
391
  var defaultLogger = {
271
392
  debug: () => {
272
393
  },
@@ -282,6 +403,9 @@ var defaultLogger = {
282
403
  }
283
404
  };
284
405
 
406
+ // src/shared/lua/index.ts
407
+ init_esm_shims();
408
+
285
409
  // src/shared/lua/set-with-tags.lua
286
410
  var set_with_tags_default = `-- set-with-tags.lua
287
411
  --
@@ -385,13 +509,19 @@ async function execLuaScript(client, name, keys, args) {
385
509
  }
386
510
 
387
511
  // src/shared/memory-fallback.ts
512
+ init_esm_shims();
388
513
  var MAX_TIMER_MS = 2147483647;
389
514
  function clampTimerMs(ms) {
390
515
  return Math.min(Math.max(0, ms), MAX_TIMER_MS);
391
516
  }
517
+ var DEFAULT_MAX_ENTRIES = 1e3;
392
518
  var MemoryStore = class {
393
519
  kv = /* @__PURE__ */ new Map();
394
520
  timers = /* @__PURE__ */ new Map();
521
+ maxEntries;
522
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
523
+ this.maxEntries = Math.max(1, maxEntries);
524
+ }
395
525
  set(key, value, ttlSeconds) {
396
526
  this.delete(key);
397
527
  const expiresAt = Date.now() + ttlSeconds * 1e3;
@@ -401,6 +531,10 @@ var MemoryStore = class {
401
531
  timer.unref();
402
532
  }
403
533
  this.timers.set(key, timer);
534
+ while (this.kv.size > this.maxEntries) {
535
+ const oldest = this.kv.keys().next().value;
536
+ this.delete(oldest);
537
+ }
404
538
  }
405
539
  get(key) {
406
540
  const entry = this.kv.get(key);
@@ -409,6 +543,8 @@ var MemoryStore = class {
409
543
  this.delete(key);
410
544
  return null;
411
545
  }
546
+ this.kv.delete(key);
547
+ this.kv.set(key, entry);
412
548
  return entry.value;
413
549
  }
414
550
  delete(key) {
@@ -443,13 +579,19 @@ var MemorySetStore = class {
443
579
  sets = /* @__PURE__ */ new Map();
444
580
  expiresAt = /* @__PURE__ */ new Map();
445
581
  timers = /* @__PURE__ */ new Map();
582
+ maxSets;
583
+ constructor(maxSets = DEFAULT_MAX_ENTRIES) {
584
+ this.maxSets = Math.max(1, maxSets);
585
+ }
446
586
  sAdd(key, member) {
447
587
  const members = Array.isArray(member) ? member : [member];
448
588
  let set = this.sets.get(key);
449
589
  if (!set) {
450
590
  set = /* @__PURE__ */ new Set();
451
- this.sets.set(key, set);
591
+ } else {
592
+ this.sets.delete(key);
452
593
  }
594
+ this.sets.set(key, set);
453
595
  let added = 0;
454
596
  for (const m of members) {
455
597
  if (!set.has(m)) {
@@ -457,6 +599,10 @@ var MemorySetStore = class {
457
599
  added += 1;
458
600
  }
459
601
  }
602
+ while (this.sets.size > this.maxSets) {
603
+ const oldest = this.sets.keys().next().value;
604
+ this.delete(oldest);
605
+ }
460
606
  return added;
461
607
  }
462
608
  sMembers(key) {
@@ -509,6 +655,7 @@ var MemorySetStore = class {
509
655
  };
510
656
 
511
657
  // src/shared/metrics.ts
658
+ init_esm_shims();
512
659
  var counters = /* @__PURE__ */ new Map();
513
660
  function bump(event) {
514
661
  const existing = counters.get(event.type);
@@ -537,6 +684,7 @@ function createEmitter(onMetric) {
537
684
  }
538
685
 
539
686
  // src/shared/namespace.ts
687
+ init_esm_shims();
540
688
  var FALLBACK = "unversioned";
541
689
  function resolveBuildNamespace(override) {
542
690
  if (override !== void 0) {
@@ -550,6 +698,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
550
698
  }
551
699
 
552
700
  // src/shared/build-phase.ts
701
+ init_esm_shims();
553
702
  var PHASE_PRODUCTION_BUILD = "phase-production-build";
554
703
  function isBuildPhase() {
555
704
  return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
@@ -562,6 +711,7 @@ function shouldUseRedis(opts) {
562
711
  }
563
712
 
564
713
  // src/cache-components/serialize.ts
714
+ init_esm_shims();
565
715
  async function readStreamFully(stream) {
566
716
  const reader = stream.getReader();
567
717
  const chunks = [];
@@ -604,6 +754,7 @@ function decodeEnvelope(raw) {
604
754
  }
605
755
 
606
756
  // src/cache-components/swr.ts
757
+ init_esm_shims();
607
758
  var SECOND_MS = 1e3;
608
759
  function partitionEntry(entry, now = Date.now()) {
609
760
  const ageMs = now - entry.timestamp;
@@ -662,8 +813,8 @@ function init(opts) {
662
813
  logger,
663
814
  emit,
664
815
  conn,
665
- memEntries: new MemoryStore(),
666
- memTags: new MemorySetStore(),
816
+ memEntries: new MemoryStore(opts.memoryMaxEntries),
817
+ memTags: new MemorySetStore(opts.memoryMaxEntries),
667
818
  memTagExp: /* @__PURE__ */ new Map(),
668
819
  localTagTimestamps: /* @__PURE__ */ new Map(),
669
820
  resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
@@ -780,6 +931,12 @@ async function getImpl(state, cacheKey, softTags) {
780
931
  state.emit({ type: "cache.miss", ms: Date.now() - start });
781
932
  return void 0;
782
933
  }
934
+ try {
935
+ raw = await decompressValue(raw);
936
+ } catch {
937
+ state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
938
+ return void 0;
939
+ }
783
940
  envelope = decodeEnvelope(raw);
784
941
  if (!envelope) {
785
942
  state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
@@ -908,10 +1065,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
908
1065
  }
909
1066
  return;
910
1067
  }
1068
+ const wirePayload = await compressValue(
1069
+ payload,
1070
+ state.opts.rest.compression
1071
+ );
911
1072
  if (entry.tags.length === 0) {
912
- await client.set(eKey, payload, { EX: ttl });
1073
+ await client.set(eKey, wirePayload, { EX: ttl });
913
1074
  } else {
914
- await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
1075
+ await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
915
1076
  }
916
1077
  }
917
1078
  );
@@ -947,28 +1108,36 @@ async function refreshTagsImpl(state) {
947
1108
  async () => {
948
1109
  const client = await state.conn.getOrConnect();
949
1110
  if (!client) return;
950
- const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
951
- const seen = [];
1111
+ const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
1112
+ const pattern = `${escapeMatch(
1113
+ `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
1114
+ )}*`;
1115
+ const scanStartMs = Date.now();
1116
+ const fresh = /* @__PURE__ */ new Map();
952
1117
  for await (const chunk of client.scanIterator({
953
1118
  MATCH: pattern,
954
1119
  COUNT: 100
955
1120
  })) {
956
1121
  const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
957
- seen.push(...keys);
958
- if (seen.length >= 500) break;
1122
+ if (keys.length === 0) continue;
1123
+ const values = await client.mGet(keys);
1124
+ for (let i = 0; i < keys.length; i += 1) {
1125
+ const v = values[i];
1126
+ if (!v) continue;
1127
+ const ts = Number.parseInt(v, 10);
1128
+ if (!Number.isFinite(ts)) continue;
1129
+ const key = keys[i];
1130
+ if (key === void 0) continue;
1131
+ const tag = stripExpPrefix(state, key);
1132
+ if (tag) fresh.set(tag, ts);
1133
+ }
959
1134
  }
960
- if (seen.length === 0) return;
961
- const values = await client.mGet(seen);
962
- for (let i = 0; i < seen.length; i += 1) {
963
- const v = values[i];
964
- if (!v) continue;
965
- const ts = Number.parseInt(v, 10);
966
- if (!Number.isFinite(ts)) continue;
967
- const key = seen[i];
968
- if (key === void 0) continue;
969
- const tag = stripExpPrefix(state, key);
970
- if (tag) state.localTagTimestamps.set(tag, ts);
1135
+ for (const [tag, ts] of state.localTagTimestamps) {
1136
+ if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1137
+ fresh.set(tag, ts);
1138
+ }
971
1139
  }
1140
+ state.localTagTimestamps = fresh;
972
1141
  }
973
1142
  );
974
1143
  } catch (err) {
@@ -977,6 +1146,19 @@ async function refreshTagsImpl(state) {
977
1146
  });
978
1147
  }
979
1148
  }
1149
+ function escapeMatch(s) {
1150
+ return s.replace(/[\\*?[\]]/g, "\\$&");
1151
+ }
1152
+ var MAX_LOCAL_TAG_ENTRIES = 1e4;
1153
+ function pruneOldestByTimestamp(map, maxEntries) {
1154
+ if (map.size <= maxEntries) return;
1155
+ const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1156
+ const drop = map.size - maxEntries;
1157
+ for (let i = 0; i < drop; i += 1) {
1158
+ const entry = byAge[i];
1159
+ if (entry) map.delete(entry[0]);
1160
+ }
1161
+ }
980
1162
  function stripExpPrefix(state, fullKey) {
981
1163
  const ns = state.resolveNs();
982
1164
  const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
@@ -1028,6 +1210,7 @@ async function updateTagsImpl(state, tags, durations) {
1028
1210
  state.localTagTimestamps.set(t, now);
1029
1211
  state.memTagExp.set(t, now);
1030
1212
  }
1213
+ pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1031
1214
  if (!gateRedis(state)) {
1032
1215
  if (isHardExpire) {
1033
1216
  for (const t of tags) {
@@ -1057,20 +1240,18 @@ async function updateTagsImpl(state, tags, durations) {
1057
1240
  }
1058
1241
  return;
1059
1242
  }
1060
- for (const t of tags) {
1061
- if (isHardExpire) {
1062
- await execLuaScript(
1243
+ await Promise.all(
1244
+ tags.map(
1245
+ (t) => isHardExpire ? execLuaScript(
1063
1246
  client,
1064
1247
  "revalidateHard",
1065
1248
  [tagKey(state, t), tagExpKey(state, t)],
1066
1249
  [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1067
- );
1068
- } else {
1069
- await client.set(tagExpKey(state, t), String(now), {
1250
+ ) : client.set(tagExpKey(state, t), String(now), {
1070
1251
  EX: TAG_EXP_MARKER_TTL_SEC
1071
- });
1072
- }
1073
- }
1252
+ })
1253
+ )
1254
+ );
1074
1255
  }
1075
1256
  );
1076
1257
  state.emit({