@leejpsd/nextjs-cache-handler 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CHANGELOG.md +59 -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 +20 -3
@@ -1,15 +1,17 @@
1
1
  'use strict';
2
2
 
3
+ var module$1 = require('module');
4
+ var path = require('path');
5
+ var util = require('util');
6
+ var zlib = require('zlib');
3
7
  var crypto = require('crypto');
4
8
 
9
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
+
11
+ var path__default = /*#__PURE__*/_interopDefault(path);
12
+
5
13
  var __defProp = Object.defineProperty;
6
14
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
- var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
8
- get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
9
- }) : x)(function(x) {
10
- if (typeof require !== "undefined") return require.apply(this, arguments);
11
- throw Error('Dynamic require of "' + x + '" is not supported');
12
- });
13
15
  var __esm = (fn, res) => function __init() {
14
16
  return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
15
17
  };
@@ -18,6 +20,32 @@ var __export = (target, all) => {
18
20
  __defProp(target, name, { get: all[name], enumerable: true });
19
21
  };
20
22
 
23
+ // node_modules/tsup/assets/cjs_shims.js
24
+ var getImportMetaUrl, importMetaUrl;
25
+ var init_cjs_shims = __esm({
26
+ "node_modules/tsup/assets/cjs_shims.js"() {
27
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
28
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
+ }
30
+ });
31
+ function nodeRequire(id) {
32
+ try {
33
+ return packageRequire(id);
34
+ } catch {
35
+ return cwdRequire(id);
36
+ }
37
+ }
38
+ var packageRequire, cwdRequire;
39
+ var init_node_require = __esm({
40
+ "src/shared/node-require.ts"() {
41
+ init_cjs_shims();
42
+ packageRequire = module$1.createRequire(importMetaUrl);
43
+ cwdRequire = module$1.createRequire(
44
+ path__default.default.join(process.cwd(), "__resolve-anchor__.js")
45
+ );
46
+ }
47
+ });
48
+
21
49
  // src/shared/client/adapter-redis.ts
22
50
  var adapter_redis_exports = {};
23
51
  __export(adapter_redis_exports, {
@@ -28,7 +56,7 @@ function adaptRedisV5(client) {
28
56
  return client;
29
57
  }
30
58
  function createRedisV5Client(config) {
31
- const { createClient } = __require("redis");
59
+ const { createClient } = nodeRequire("redis");
32
60
  const useTls = config.tls ?? config.url.startsWith("rediss://");
33
61
  const opts = {
34
62
  url: config.url,
@@ -43,6 +71,8 @@ function createRedisV5Client(config) {
43
71
  }
44
72
  var init_adapter_redis = __esm({
45
73
  "src/shared/client/adapter-redis.ts"() {
74
+ init_cjs_shims();
75
+ init_node_require();
46
76
  }
47
77
  });
48
78
 
@@ -52,7 +82,8 @@ __export(adapter_ioredis_exports, {
52
82
  adaptCluster: () => adaptCluster,
53
83
  adaptIoredis: () => adaptIoredis,
54
84
  createIoredisClient: () => createIoredisClient,
55
- createIoredisCluster: () => createIoredisCluster
85
+ createIoredisCluster: () => createIoredisCluster,
86
+ createIoredisSentinel: () => createIoredisSentinel
56
87
  });
57
88
  function adapt(client, isClusterClient = false) {
58
89
  return {
@@ -135,6 +166,9 @@ function adapt(client, isClusterClient = false) {
135
166
  on: (event, listener) => {
136
167
  client.on(event, listener);
137
168
  return client;
169
+ },
170
+ dispose: () => {
171
+ client.disconnect();
138
172
  }
139
173
  };
140
174
  }
@@ -145,18 +179,36 @@ function adaptCluster(cluster) {
145
179
  return adapt(cluster, true);
146
180
  }
147
181
  function createIoredisClient(config) {
148
- const IORedis = __require("ioredis");
182
+ const IORedis = nodeRequire("ioredis");
149
183
  const Ctor = IORedis.default ?? IORedis;
150
184
  return new Ctor(config.url, {
151
185
  password: config.password,
152
186
  connectTimeout: config.connectTimeout ?? 1500,
153
187
  tls: config.tls ? {} : void 0,
154
188
  lazyConnect: true,
155
- maxRetriesPerRequest: 1
189
+ maxRetriesPerRequest: 1,
190
+ // Batch same-tick commands into one RTT (matches redis@5's default
191
+ // behavior) — makes Promise.all fan-outs a single pipeline.
192
+ enableAutoPipelining: true
193
+ });
194
+ }
195
+ function createIoredisSentinel(config) {
196
+ const IORedis = nodeRequire("ioredis");
197
+ const Ctor = IORedis.default ?? IORedis;
198
+ return new Ctor({
199
+ sentinels: config.sentinels,
200
+ name: config.name,
201
+ password: config.password,
202
+ sentinelPassword: config.sentinelPassword,
203
+ connectTimeout: config.connectTimeout ?? 1500,
204
+ tls: config.tls ? {} : void 0,
205
+ lazyConnect: true,
206
+ maxRetriesPerRequest: 1,
207
+ enableAutoPipelining: true
156
208
  });
157
209
  }
158
210
  function createIoredisCluster(config) {
159
- const IORedis = __require("ioredis");
211
+ const IORedis = nodeRequire("ioredis");
160
212
  const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
161
213
  return new ClusterCtor(config.nodes, {
162
214
  redisOptions: {
@@ -164,15 +216,26 @@ function createIoredisCluster(config) {
164
216
  tls: config.tls ? {} : void 0
165
217
  },
166
218
  enableReadyCheck: true,
167
- lazyConnect: true
219
+ lazyConnect: true,
220
+ // Same-slot commands issued in the same tick share one pipeline.
221
+ enableAutoPipelining: true
168
222
  });
169
223
  }
170
224
  var init_adapter_ioredis = __esm({
171
225
  "src/shared/client/adapter-ioredis.ts"() {
226
+ init_cjs_shims();
227
+ init_node_require();
172
228
  }
173
229
  });
174
230
 
231
+ // src/cache-components/index.ts
232
+ init_cjs_shims();
233
+
234
+ // src/cache-components/handler.ts
235
+ init_cjs_shims();
236
+
175
237
  // src/shared/abort.ts
238
+ init_cjs_shims();
176
239
  var CacheTimeoutError = class extends Error {
177
240
  constructor(opName, ms) {
178
241
  super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
@@ -204,7 +267,41 @@ async function withAbortSignal(opName, ms, fn) {
204
267
  }
205
268
  }
206
269
 
270
+ // src/shared/compress.ts
271
+ init_cjs_shims();
272
+ var gzipAsync = util.promisify(zlib.gzip);
273
+ var gunzipAsync = util.promisify(zlib.gunzip);
274
+ var brotliCompressAsync = util.promisify(zlib.brotliCompress);
275
+ var brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
276
+ var GZIP_MARKER = "__ncgz__:";
277
+ var BROTLI_MARKER = "__ncbr__:";
278
+ var MIN_COMPRESS_BYTES = 1024;
279
+ var BROTLI_OPTS = {
280
+ params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 }
281
+ };
282
+ async function compressValue(value, algo) {
283
+ if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
284
+ const input = Buffer.from(value);
285
+ if (algo === "gzip") {
286
+ return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
287
+ }
288
+ return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
289
+ }
290
+ async function decompressValue(value) {
291
+ if (value === null) return null;
292
+ if (value.startsWith(GZIP_MARKER)) {
293
+ const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
294
+ return (await gunzipAsync(buf)).toString("utf8");
295
+ }
296
+ if (value.startsWith(BROTLI_MARKER)) {
297
+ const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
298
+ return (await brotliDecompressAsync(buf)).toString("utf8");
299
+ }
300
+ return value;
301
+ }
302
+
207
303
  // src/shared/client/index.ts
304
+ init_cjs_shims();
208
305
  async function buildClient(source) {
209
306
  if (typeof source === "function") {
210
307
  return await source();
@@ -222,6 +319,10 @@ async function buildClient(source) {
222
319
  const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
223
320
  return adaptCluster2(createIoredisCluster2(source));
224
321
  }
322
+ case "sentinel": {
323
+ const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
324
+ return adaptIoredis2(createIoredisSentinel2(source));
325
+ }
225
326
  default: {
226
327
  const exhaustive = source;
227
328
  throw new Error(
@@ -231,28 +332,45 @@ async function buildClient(source) {
231
332
  }
232
333
  }
233
334
  var ConnectionManager = class {
234
- constructor(source, onError) {
335
+ constructor(source, onError, retry = {}) {
235
336
  this.source = source;
236
337
  this.onError = onError;
338
+ this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
339
+ this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
237
340
  }
238
341
  source;
239
342
  onError;
240
343
  client = null;
241
344
  connecting = null;
242
- failed = false;
345
+ consecutiveFailures = 0;
346
+ lastFailureAt = 0;
347
+ baseCooldownMs;
348
+ maxCooldownMs;
349
+ cooldownMs() {
350
+ if (this.consecutiveFailures === 0) return 0;
351
+ return Math.min(
352
+ this.maxCooldownMs,
353
+ this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
354
+ );
355
+ }
243
356
  async getOrConnect() {
244
357
  if (this.client?.isOpen) return this.client;
245
- if (this.failed) return null;
246
358
  if (this.connecting) return this.connecting;
359
+ if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
360
+ const stale = this.client;
361
+ this.client = null;
247
362
  this.connecting = (async () => {
363
+ disposeQuietly(stale);
248
364
  try {
249
365
  const c = await buildClient(this.source);
250
366
  c.on("error", (err) => this.onError(err));
251
367
  if (!c.isOpen) await c.connect();
252
368
  this.client = c;
369
+ this.consecutiveFailures = 0;
253
370
  return c;
254
371
  } catch (err) {
255
- this.failed = true;
372
+ this.consecutiveFailures += 1;
373
+ this.lastFailureAt = Date.now();
256
374
  this.onError(err);
257
375
  return null;
258
376
  } finally {
@@ -262,13 +380,24 @@ var ConnectionManager = class {
262
380
  return this.connecting;
263
381
  }
264
382
  reset() {
383
+ disposeQuietly(this.client);
265
384
  this.client = null;
266
385
  this.connecting = null;
267
- this.failed = false;
386
+ this.consecutiveFailures = 0;
387
+ this.lastFailureAt = 0;
268
388
  }
269
389
  };
390
+ function disposeQuietly(client) {
391
+ if (!client) return;
392
+ const c = client;
393
+ try {
394
+ (c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
395
+ } catch {
396
+ }
397
+ }
270
398
 
271
399
  // src/shared/logger.ts
400
+ init_cjs_shims();
272
401
  var defaultLogger = {
273
402
  debug: () => {
274
403
  },
@@ -284,6 +413,9 @@ var defaultLogger = {
284
413
  }
285
414
  };
286
415
 
416
+ // src/shared/lua/index.ts
417
+ init_cjs_shims();
418
+
287
419
  // src/shared/lua/set-with-tags.lua
288
420
  var set_with_tags_default = `-- set-with-tags.lua
289
421
  --
@@ -387,13 +519,19 @@ async function execLuaScript(client, name, keys, args) {
387
519
  }
388
520
 
389
521
  // src/shared/memory-fallback.ts
522
+ init_cjs_shims();
390
523
  var MAX_TIMER_MS = 2147483647;
391
524
  function clampTimerMs(ms) {
392
525
  return Math.min(Math.max(0, ms), MAX_TIMER_MS);
393
526
  }
527
+ var DEFAULT_MAX_ENTRIES = 1e3;
394
528
  var MemoryStore = class {
395
529
  kv = /* @__PURE__ */ new Map();
396
530
  timers = /* @__PURE__ */ new Map();
531
+ maxEntries;
532
+ constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
533
+ this.maxEntries = Math.max(1, maxEntries);
534
+ }
397
535
  set(key, value, ttlSeconds) {
398
536
  this.delete(key);
399
537
  const expiresAt = Date.now() + ttlSeconds * 1e3;
@@ -403,6 +541,10 @@ var MemoryStore = class {
403
541
  timer.unref();
404
542
  }
405
543
  this.timers.set(key, timer);
544
+ while (this.kv.size > this.maxEntries) {
545
+ const oldest = this.kv.keys().next().value;
546
+ this.delete(oldest);
547
+ }
406
548
  }
407
549
  get(key) {
408
550
  const entry = this.kv.get(key);
@@ -411,6 +553,8 @@ var MemoryStore = class {
411
553
  this.delete(key);
412
554
  return null;
413
555
  }
556
+ this.kv.delete(key);
557
+ this.kv.set(key, entry);
414
558
  return entry.value;
415
559
  }
416
560
  delete(key) {
@@ -445,13 +589,19 @@ var MemorySetStore = class {
445
589
  sets = /* @__PURE__ */ new Map();
446
590
  expiresAt = /* @__PURE__ */ new Map();
447
591
  timers = /* @__PURE__ */ new Map();
592
+ maxSets;
593
+ constructor(maxSets = DEFAULT_MAX_ENTRIES) {
594
+ this.maxSets = Math.max(1, maxSets);
595
+ }
448
596
  sAdd(key, member) {
449
597
  const members = Array.isArray(member) ? member : [member];
450
598
  let set = this.sets.get(key);
451
599
  if (!set) {
452
600
  set = /* @__PURE__ */ new Set();
453
- this.sets.set(key, set);
601
+ } else {
602
+ this.sets.delete(key);
454
603
  }
604
+ this.sets.set(key, set);
455
605
  let added = 0;
456
606
  for (const m of members) {
457
607
  if (!set.has(m)) {
@@ -459,6 +609,10 @@ var MemorySetStore = class {
459
609
  added += 1;
460
610
  }
461
611
  }
612
+ while (this.sets.size > this.maxSets) {
613
+ const oldest = this.sets.keys().next().value;
614
+ this.delete(oldest);
615
+ }
462
616
  return added;
463
617
  }
464
618
  sMembers(key) {
@@ -511,6 +665,7 @@ var MemorySetStore = class {
511
665
  };
512
666
 
513
667
  // src/shared/metrics.ts
668
+ init_cjs_shims();
514
669
  var counters = /* @__PURE__ */ new Map();
515
670
  function bump(event) {
516
671
  const existing = counters.get(event.type);
@@ -539,6 +694,7 @@ function createEmitter(onMetric) {
539
694
  }
540
695
 
541
696
  // src/shared/namespace.ts
697
+ init_cjs_shims();
542
698
  var FALLBACK = "unversioned";
543
699
  function resolveBuildNamespace(override) {
544
700
  if (override !== void 0) {
@@ -552,6 +708,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
552
708
  }
553
709
 
554
710
  // src/shared/build-phase.ts
711
+ init_cjs_shims();
555
712
  var PHASE_PRODUCTION_BUILD = "phase-production-build";
556
713
  function isBuildPhase() {
557
714
  return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
@@ -564,6 +721,7 @@ function shouldUseRedis(opts) {
564
721
  }
565
722
 
566
723
  // src/cache-components/serialize.ts
724
+ init_cjs_shims();
567
725
  async function readStreamFully(stream) {
568
726
  const reader = stream.getReader();
569
727
  const chunks = [];
@@ -606,6 +764,7 @@ function decodeEnvelope(raw) {
606
764
  }
607
765
 
608
766
  // src/cache-components/swr.ts
767
+ init_cjs_shims();
609
768
  var SECOND_MS = 1e3;
610
769
  function partitionEntry(entry, now = Date.now()) {
611
770
  const ageMs = now - entry.timestamp;
@@ -664,8 +823,8 @@ function init(opts) {
664
823
  logger,
665
824
  emit,
666
825
  conn,
667
- memEntries: new MemoryStore(),
668
- memTags: new MemorySetStore(),
826
+ memEntries: new MemoryStore(opts.memoryMaxEntries),
827
+ memTags: new MemorySetStore(opts.memoryMaxEntries),
669
828
  memTagExp: /* @__PURE__ */ new Map(),
670
829
  localTagTimestamps: /* @__PURE__ */ new Map(),
671
830
  resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
@@ -782,6 +941,12 @@ async function getImpl(state, cacheKey, softTags) {
782
941
  state.emit({ type: "cache.miss", ms: Date.now() - start });
783
942
  return void 0;
784
943
  }
944
+ try {
945
+ raw = await decompressValue(raw);
946
+ } catch {
947
+ state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
948
+ return void 0;
949
+ }
785
950
  envelope = decodeEnvelope(raw);
786
951
  if (!envelope) {
787
952
  state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
@@ -910,10 +1075,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
910
1075
  }
911
1076
  return;
912
1077
  }
1078
+ const wirePayload = await compressValue(
1079
+ payload,
1080
+ state.opts.rest.compression
1081
+ );
913
1082
  if (entry.tags.length === 0) {
914
- await client.set(eKey, payload, { EX: ttl });
1083
+ await client.set(eKey, wirePayload, { EX: ttl });
915
1084
  } else {
916
- await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
1085
+ await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
917
1086
  }
918
1087
  }
919
1088
  );
@@ -949,28 +1118,36 @@ async function refreshTagsImpl(state) {
949
1118
  async () => {
950
1119
  const client = await state.conn.getOrConnect();
951
1120
  if (!client) return;
952
- const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
953
- const seen = [];
1121
+ const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
1122
+ const pattern = `${escapeMatch(
1123
+ `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
1124
+ )}*`;
1125
+ const scanStartMs = Date.now();
1126
+ const fresh = /* @__PURE__ */ new Map();
954
1127
  for await (const chunk of client.scanIterator({
955
1128
  MATCH: pattern,
956
1129
  COUNT: 100
957
1130
  })) {
958
1131
  const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
959
- seen.push(...keys);
960
- if (seen.length >= 500) break;
1132
+ if (keys.length === 0) continue;
1133
+ const values = await client.mGet(keys);
1134
+ for (let i = 0; i < keys.length; i += 1) {
1135
+ const v = values[i];
1136
+ if (!v) continue;
1137
+ const ts = Number.parseInt(v, 10);
1138
+ if (!Number.isFinite(ts)) continue;
1139
+ const key = keys[i];
1140
+ if (key === void 0) continue;
1141
+ const tag = stripExpPrefix(state, key);
1142
+ if (tag) fresh.set(tag, ts);
1143
+ }
961
1144
  }
962
- if (seen.length === 0) return;
963
- const values = await client.mGet(seen);
964
- for (let i = 0; i < seen.length; i += 1) {
965
- const v = values[i];
966
- if (!v) continue;
967
- const ts = Number.parseInt(v, 10);
968
- if (!Number.isFinite(ts)) continue;
969
- const key = seen[i];
970
- if (key === void 0) continue;
971
- const tag = stripExpPrefix(state, key);
972
- if (tag) state.localTagTimestamps.set(tag, ts);
1145
+ for (const [tag, ts] of state.localTagTimestamps) {
1146
+ if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
1147
+ fresh.set(tag, ts);
1148
+ }
973
1149
  }
1150
+ state.localTagTimestamps = fresh;
974
1151
  }
975
1152
  );
976
1153
  } catch (err) {
@@ -979,6 +1156,19 @@ async function refreshTagsImpl(state) {
979
1156
  });
980
1157
  }
981
1158
  }
1159
+ function escapeMatch(s) {
1160
+ return s.replace(/[\\*?[\]]/g, "\\$&");
1161
+ }
1162
+ var MAX_LOCAL_TAG_ENTRIES = 1e4;
1163
+ function pruneOldestByTimestamp(map, maxEntries) {
1164
+ if (map.size <= maxEntries) return;
1165
+ const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
1166
+ const drop = map.size - maxEntries;
1167
+ for (let i = 0; i < drop; i += 1) {
1168
+ const entry = byAge[i];
1169
+ if (entry) map.delete(entry[0]);
1170
+ }
1171
+ }
982
1172
  function stripExpPrefix(state, fullKey) {
983
1173
  const ns = state.resolveNs();
984
1174
  const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
@@ -1030,6 +1220,7 @@ async function updateTagsImpl(state, tags, durations) {
1030
1220
  state.localTagTimestamps.set(t, now);
1031
1221
  state.memTagExp.set(t, now);
1032
1222
  }
1223
+ pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
1033
1224
  if (!gateRedis(state)) {
1034
1225
  if (isHardExpire) {
1035
1226
  for (const t of tags) {
@@ -1059,20 +1250,18 @@ async function updateTagsImpl(state, tags, durations) {
1059
1250
  }
1060
1251
  return;
1061
1252
  }
1062
- for (const t of tags) {
1063
- if (isHardExpire) {
1064
- await execLuaScript(
1253
+ await Promise.all(
1254
+ tags.map(
1255
+ (t) => isHardExpire ? execLuaScript(
1065
1256
  client,
1066
1257
  "revalidateHard",
1067
1258
  [tagKey(state, t), tagExpKey(state, t)],
1068
1259
  [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1069
- );
1070
- } else {
1071
- await client.set(tagExpKey(state, t), String(now), {
1260
+ ) : client.set(tagExpKey(state, t), String(now), {
1072
1261
  EX: TAG_EXP_MARKER_TTL_SEC
1073
- });
1074
- }
1075
- }
1262
+ })
1263
+ )
1264
+ );
1076
1265
  }
1077
1266
  );
1078
1267
  state.emit({