@leejpsd/nextjs-cache-handler 0.1.1 → 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 +117 -2
  2. package/README.md +131 -30
  3. package/dist/cache-components/index.cjs +297 -52
  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 +289 -52
  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 +375 -76
  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 +367 -76
  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-CgriT7My.d.cts → types-Bl-rkLKX.d.cts} +59 -1
  42. package/dist/{types-CgriT7My.d.ts → types-Bl-rkLKX.d.ts} +59 -1
  43. package/package.json +23 -4
@@ -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;
@@ -630,7 +781,9 @@ var DEFAULT_KEY_PREFIX = "next-cache:";
630
781
  var ENTRY_SUFFIX = "entry:";
631
782
  var TAG_SUFFIX = "tag:";
632
783
  var TAG_EXP_SUFFIX = "tag-expiration:";
784
+ var LOCK_SUFFIX = "lock:";
633
785
  var DEFAULT_ABORT_MS = 1500;
786
+ var DEFAULT_LOCK_TTL_SEC = 10;
634
787
  var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
635
788
  var FALLBACK_TTL_SEC = 3600;
636
789
  var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
@@ -653,16 +806,19 @@ function init(opts) {
653
806
  staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
654
807
  hashTag: opts.hashTag ?? false,
655
808
  keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
809
+ singleFlight: opts.singleFlight ?? false,
810
+ singleFlightLockTtlSec: opts.singleFlightLockTtlSec ?? DEFAULT_LOCK_TTL_SEC,
656
811
  rest: opts
657
812
  },
658
813
  logger,
659
814
  emit,
660
815
  conn,
661
- memEntries: new MemoryStore(),
662
- memTags: new MemorySetStore(),
816
+ memEntries: new MemoryStore(opts.memoryMaxEntries),
817
+ memTags: new MemorySetStore(opts.memoryMaxEntries),
663
818
  memTagExp: /* @__PURE__ */ new Map(),
664
819
  localTagTimestamps: /* @__PURE__ */ new Map(),
665
- resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
820
+ resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
821
+ instanceId: process.env.HOSTNAME || process.env.ECS_TASK_ID || `pid-${process.pid}`
666
822
  };
667
823
  }
668
824
  function entryKey(state, cacheKey) {
@@ -689,6 +845,40 @@ function tagExpKey(state, tag) {
689
845
  state.opts.hashTag
690
846
  );
691
847
  }
848
+ function lockKey(state, cacheKey) {
849
+ return buildKey(
850
+ `${state.opts.keyPrefix}${LOCK_SUFFIX}`,
851
+ state.resolveNs(),
852
+ cacheKey,
853
+ state.opts.hashTag
854
+ );
855
+ }
856
+ async function tryAcquireRefreshLock(state, cacheKey) {
857
+ if (!state.opts.singleFlight) return false;
858
+ if (!gateRedis(state)) return false;
859
+ try {
860
+ return await withAbortSignal(
861
+ "cacheHandlers.singleFlight.lock",
862
+ state.opts.abortTimeoutMs,
863
+ async () => {
864
+ const client = await state.conn.getOrConnect();
865
+ if (!client) return false;
866
+ const result = await execLuaScript(
867
+ client,
868
+ "refreshTagLock",
869
+ [lockKey(state, cacheKey)],
870
+ [
871
+ state.instanceId,
872
+ String(state.opts.singleFlightLockTtlSec)
873
+ ]
874
+ );
875
+ return result === 1;
876
+ }
877
+ );
878
+ } catch {
879
+ return false;
880
+ }
881
+ }
692
882
  function resolveTtlSeconds(entry) {
693
883
  if (Number.isFinite(entry.expire) && entry.expire > 0) {
694
884
  return Math.max(60, Math.ceil(entry.expire));
@@ -741,6 +931,12 @@ async function getImpl(state, cacheKey, softTags) {
741
931
  state.emit({ type: "cache.miss", ms: Date.now() - start });
742
932
  return void 0;
743
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
+ }
744
940
  envelope = decodeEnvelope(raw);
745
941
  if (!envelope) {
746
942
  state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
@@ -783,11 +979,28 @@ async function getImpl(state, cacheKey, softTags) {
783
979
  return void 0;
784
980
  }
785
981
  }
786
- state.emit({
787
- type: partition.freshness === "stale" ? "cache.stale" : "cache.hit",
788
- ms: Date.now() - start,
789
- meta: { freshness: partition.freshness }
790
- });
982
+ if (partition.freshness === "stale") {
983
+ if (state.opts.singleFlight) {
984
+ const isLeader = await tryAcquireRefreshLock(state, cacheKey);
985
+ state.emit({
986
+ type: isLeader ? "cache.stale.refresh.leader" : "cache.stale.refresh.follower",
987
+ ms: Date.now() - start,
988
+ meta: { freshness: partition.freshness }
989
+ });
990
+ } else {
991
+ state.emit({
992
+ type: "cache.stale",
993
+ ms: Date.now() - start,
994
+ meta: { freshness: partition.freshness }
995
+ });
996
+ }
997
+ } else {
998
+ state.emit({
999
+ type: "cache.hit",
1000
+ ms: Date.now() - start,
1001
+ meta: { freshness: partition.freshness }
1002
+ });
1003
+ }
791
1004
  return {
792
1005
  value: bufferToStream(Buffer.from(envelope.value, "base64")),
793
1006
  tags: envelope.tags,
@@ -852,10 +1065,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
852
1065
  }
853
1066
  return;
854
1067
  }
1068
+ const wirePayload = await compressValue(
1069
+ payload,
1070
+ state.opts.rest.compression
1071
+ );
855
1072
  if (entry.tags.length === 0) {
856
- await client.set(eKey, payload, { EX: ttl });
1073
+ await client.set(eKey, wirePayload, { EX: ttl });
857
1074
  } else {
858
- await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
1075
+ await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
859
1076
  }
860
1077
  }
861
1078
  );
@@ -891,28 +1108,36 @@ async function refreshTagsImpl(state) {
891
1108
  async () => {
892
1109
  const client = await state.conn.getOrConnect();
893
1110
  if (!client) return;
894
- const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
895
- 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();
896
1117
  for await (const chunk of client.scanIterator({
897
1118
  MATCH: pattern,
898
1119
  COUNT: 100
899
1120
  })) {
900
1121
  const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
901
- seen.push(...keys);
902
- 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
+ }
903
1134
  }
904
- if (seen.length === 0) return;
905
- const values = await client.mGet(seen);
906
- for (let i = 0; i < seen.length; i += 1) {
907
- const v = values[i];
908
- if (!v) continue;
909
- const ts = Number.parseInt(v, 10);
910
- if (!Number.isFinite(ts)) continue;
911
- const key = seen[i];
912
- if (key === void 0) continue;
913
- const tag = stripExpPrefix(state, key);
914
- 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
+ }
915
1139
  }
1140
+ state.localTagTimestamps = fresh;
916
1141
  }
917
1142
  );
918
1143
  } catch (err) {
@@ -921,6 +1146,19 @@ async function refreshTagsImpl(state) {
921
1146
  });
922
1147
  }
923
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
+ }
924
1162
  function stripExpPrefix(state, fullKey) {
925
1163
  const ns = state.resolveNs();
926
1164
  const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
@@ -972,6 +1210,7 @@ async function updateTagsImpl(state, tags, durations) {
972
1210
  state.localTagTimestamps.set(t, now);
973
1211
  state.memTagExp.set(t, now);
974
1212
  }
1213
+ pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
975
1214
  if (!gateRedis(state)) {
976
1215
  if (isHardExpire) {
977
1216
  for (const t of tags) {
@@ -1001,20 +1240,18 @@ async function updateTagsImpl(state, tags, durations) {
1001
1240
  }
1002
1241
  return;
1003
1242
  }
1004
- for (const t of tags) {
1005
- if (isHardExpire) {
1006
- await execLuaScript(
1243
+ await Promise.all(
1244
+ tags.map(
1245
+ (t) => isHardExpire ? execLuaScript(
1007
1246
  client,
1008
1247
  "revalidateHard",
1009
1248
  [tagKey(state, t), tagExpKey(state, t)],
1010
1249
  [String(now), String(TAG_EXP_MARKER_TTL_SEC)]
1011
- );
1012
- } else {
1013
- await client.set(tagExpKey(state, t), String(now), {
1250
+ ) : client.set(tagExpKey(state, t), String(now), {
1014
1251
  EX: TAG_EXP_MARKER_TTL_SEC
1015
- });
1016
- }
1017
- }
1252
+ })
1253
+ )
1254
+ );
1018
1255
  }
1019
1256
  );
1020
1257
  state.emit({