layercache 1.0.1 → 1.0.2

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.
@@ -30,6 +30,7 @@ var index_exports = {};
30
30
  __export(index_exports, {
31
31
  CACHE_STACK: () => CACHE_STACK,
32
32
  CacheStackModule: () => CacheStackModule,
33
+ Cacheable: () => Cacheable,
33
34
  InjectCacheStack: () => InjectCacheStack
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
@@ -37,11 +38,43 @@ module.exports = __toCommonJS(index_exports);
37
38
  // src/constants.ts
38
39
  var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
39
40
 
41
+ // ../../src/decorators/createCachedMethodDecorator.ts
42
+ function createCachedMethodDecorator(options) {
43
+ const wrappedByInstance = /* @__PURE__ */ new WeakMap();
44
+ return ((_, propertyKey, descriptor) => {
45
+ const original = descriptor.value;
46
+ if (typeof original !== "function") {
47
+ throw new Error("createCachedMethodDecorator can only be applied to methods.");
48
+ }
49
+ descriptor.value = async function(...args) {
50
+ const instance = this;
51
+ let wrapped = wrappedByInstance.get(instance);
52
+ if (!wrapped) {
53
+ const cache = options.cache(instance);
54
+ wrapped = cache.wrap(
55
+ options.prefix ?? String(propertyKey),
56
+ (...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
57
+ options
58
+ );
59
+ wrappedByInstance.set(instance, wrapped);
60
+ }
61
+ return wrapped(...args);
62
+ };
63
+ });
64
+ }
65
+
66
+ // src/decorators.ts
67
+ function Cacheable(options) {
68
+ return createCachedMethodDecorator(options);
69
+ }
70
+
40
71
  // src/module.ts
41
72
  var import_common = require("@nestjs/common");
42
73
 
43
74
  // ../../src/CacheStack.ts
44
75
  var import_node_crypto = require("crypto");
76
+ var import_node_fs = require("fs");
77
+ var import_node_events = require("events");
45
78
 
46
79
  // ../../src/internal/StoredValue.ts
47
80
  function isStoredValueEnvelope(value) {
@@ -61,7 +94,10 @@ function createStoredValueEnvelope(options) {
61
94
  value: options.value,
62
95
  freshUntil,
63
96
  staleUntil,
64
- errorUntil
97
+ errorUntil,
98
+ freshTtlSeconds: freshTtlSeconds ?? null,
99
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
100
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
65
101
  };
66
102
  }
67
103
  function resolveStoredValue(stored, now = Date.now()) {
@@ -102,6 +138,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
102
138
  }
103
139
  return Math.max(1, Math.ceil(remainingMs / 1e3));
104
140
  }
141
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
142
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
143
+ return void 0;
144
+ }
145
+ const remainingMs = stored.freshUntil - now;
146
+ if (remainingMs <= 0) {
147
+ return 0;
148
+ }
149
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
150
+ }
151
+ function refreshStoredEnvelope(stored, now = Date.now()) {
152
+ if (!isStoredValueEnvelope(stored)) {
153
+ return stored;
154
+ }
155
+ return createStoredValueEnvelope({
156
+ kind: stored.kind,
157
+ value: stored.value,
158
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
159
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
160
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
161
+ now
162
+ });
163
+ }
105
164
  function maxExpiry(stored) {
106
165
  const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
107
166
  (value) => value !== null
@@ -118,6 +177,61 @@ function normalizePositiveSeconds(value) {
118
177
  return value;
119
178
  }
120
179
 
180
+ // ../../src/CacheNamespace.ts
181
+ var CacheNamespace = class {
182
+ constructor(cache, prefix) {
183
+ this.cache = cache;
184
+ this.prefix = prefix;
185
+ }
186
+ cache;
187
+ prefix;
188
+ async get(key, fetcher, options) {
189
+ return this.cache.get(this.qualify(key), fetcher, options);
190
+ }
191
+ async set(key, value, options) {
192
+ await this.cache.set(this.qualify(key), value, options);
193
+ }
194
+ async delete(key) {
195
+ await this.cache.delete(this.qualify(key));
196
+ }
197
+ async clear() {
198
+ await this.cache.invalidateByPattern(`${this.prefix}:*`);
199
+ }
200
+ async mget(entries) {
201
+ return this.cache.mget(entries.map((entry) => ({
202
+ ...entry,
203
+ key: this.qualify(entry.key)
204
+ })));
205
+ }
206
+ async mset(entries) {
207
+ await this.cache.mset(entries.map((entry) => ({
208
+ ...entry,
209
+ key: this.qualify(entry.key)
210
+ })));
211
+ }
212
+ async invalidateByTag(tag) {
213
+ await this.cache.invalidateByTag(tag);
214
+ }
215
+ async invalidateByPattern(pattern) {
216
+ await this.cache.invalidateByPattern(this.qualify(pattern));
217
+ }
218
+ wrap(keyPrefix, fetcher, options) {
219
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
220
+ }
221
+ warm(entries, options) {
222
+ return this.cache.warm(entries.map((entry) => ({
223
+ ...entry,
224
+ key: this.qualify(entry.key)
225
+ })), options);
226
+ }
227
+ getMetrics() {
228
+ return this.cache.getMetrics();
229
+ }
230
+ qualify(key) {
231
+ return `${this.prefix}:${key}`;
232
+ }
233
+ };
234
+
121
235
  // ../../src/invalidation/PatternMatcher.ts
122
236
  var PatternMatcher = class {
123
237
  static matches(pattern, value) {
@@ -364,22 +478,24 @@ var Mutex = class {
364
478
  var StampedeGuard = class {
365
479
  mutexes = /* @__PURE__ */ new Map();
366
480
  async execute(key, task) {
367
- const mutex = this.getMutex(key);
481
+ const entry = this.getMutexEntry(key);
368
482
  try {
369
- return await mutex.runExclusive(task);
483
+ return await entry.mutex.runExclusive(task);
370
484
  } finally {
371
- if (!mutex.isLocked()) {
485
+ entry.references -= 1;
486
+ if (entry.references === 0 && !entry.mutex.isLocked()) {
372
487
  this.mutexes.delete(key);
373
488
  }
374
489
  }
375
490
  }
376
- getMutex(key) {
377
- let mutex = this.mutexes.get(key);
378
- if (!mutex) {
379
- mutex = new Mutex();
380
- this.mutexes.set(key, mutex);
491
+ getMutexEntry(key) {
492
+ let entry = this.mutexes.get(key);
493
+ if (!entry) {
494
+ entry = { mutex: new Mutex(), references: 0 };
495
+ this.mutexes.set(key, entry);
381
496
  }
382
- return mutex;
497
+ entry.references += 1;
498
+ return entry;
383
499
  }
384
500
  };
385
501
 
@@ -388,6 +504,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
388
504
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
389
505
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
390
506
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
507
+ var MAX_CACHE_KEY_LENGTH = 1024;
391
508
  var EMPTY_METRICS = () => ({
392
509
  hits: 0,
393
510
  misses: 0,
@@ -400,7 +517,12 @@ var EMPTY_METRICS = () => ({
400
517
  refreshes: 0,
401
518
  refreshErrors: 0,
402
519
  writeFailures: 0,
403
- singleFlightWaits: 0
520
+ singleFlightWaits: 0,
521
+ negativeCacheHits: 0,
522
+ circuitBreakerTrips: 0,
523
+ degradedOperations: 0,
524
+ hitsByLayer: {},
525
+ missesByLayer: {}
404
526
  });
405
527
  var DebugLogger = class {
406
528
  enabled;
@@ -408,20 +530,34 @@ var DebugLogger = class {
408
530
  this.enabled = enabled;
409
531
  }
410
532
  debug(message, context) {
533
+ this.write("debug", message, context);
534
+ }
535
+ info(message, context) {
536
+ this.write("info", message, context);
537
+ }
538
+ warn(message, context) {
539
+ this.write("warn", message, context);
540
+ }
541
+ error(message, context) {
542
+ this.write("error", message, context);
543
+ }
544
+ write(level, message, context) {
411
545
  if (!this.enabled) {
412
546
  return;
413
547
  }
414
548
  const suffix = context ? ` ${JSON.stringify(context)}` : "";
415
- console.debug(`[layercache] ${message}${suffix}`);
549
+ console[level](`[layercache] ${message}${suffix}`);
416
550
  }
417
551
  };
418
- var CacheStack = class {
552
+ var CacheStack = class extends import_node_events.EventEmitter {
419
553
  constructor(layers, options = {}) {
554
+ super();
420
555
  this.layers = layers;
421
556
  this.options = options;
422
557
  if (layers.length === 0) {
423
558
  throw new Error("CacheStack requires at least one cache layer.");
424
559
  }
560
+ this.validateConfiguration();
425
561
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
426
562
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
427
563
  this.tagIndex = options.tagIndex ?? new TagIndex();
@@ -437,33 +573,47 @@ var CacheStack = class {
437
573
  logger;
438
574
  tagIndex;
439
575
  backgroundRefreshes = /* @__PURE__ */ new Map();
576
+ accessProfiles = /* @__PURE__ */ new Map();
577
+ layerDegradedUntil = /* @__PURE__ */ new Map();
578
+ circuitBreakers = /* @__PURE__ */ new Map();
579
+ isDisconnecting = false;
580
+ disconnectPromise;
440
581
  async get(key, fetcher, options) {
582
+ const normalizedKey = this.validateCacheKey(key);
583
+ this.validateWriteOptions(options);
441
584
  await this.startup;
442
- const hit = await this.readFromLayers(key, options, "allow-stale");
585
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
443
586
  if (hit.found) {
587
+ this.recordAccess(normalizedKey);
588
+ if (this.isNegativeStoredValue(hit.stored)) {
589
+ this.metrics.negativeCacheHits += 1;
590
+ }
444
591
  if (hit.state === "fresh") {
445
592
  this.metrics.hits += 1;
593
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
446
594
  return hit.value;
447
595
  }
448
596
  if (hit.state === "stale-while-revalidate") {
449
597
  this.metrics.hits += 1;
450
598
  this.metrics.staleHits += 1;
599
+ this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
451
600
  if (fetcher) {
452
- this.scheduleBackgroundRefresh(key, fetcher, options);
601
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
453
602
  }
454
603
  return hit.value;
455
604
  }
456
605
  if (!fetcher) {
457
606
  this.metrics.hits += 1;
458
607
  this.metrics.staleHits += 1;
608
+ this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
459
609
  return hit.value;
460
610
  }
461
611
  try {
462
- return await this.fetchWithGuards(key, fetcher, options);
612
+ return await this.fetchWithGuards(normalizedKey, fetcher, options);
463
613
  } catch (error) {
464
614
  this.metrics.staleHits += 1;
465
615
  this.metrics.refreshErrors += 1;
466
- this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
616
+ this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
467
617
  return hit.value;
468
618
  }
469
619
  }
@@ -471,71 +621,144 @@ var CacheStack = class {
471
621
  if (!fetcher) {
472
622
  return null;
473
623
  }
474
- return this.fetchWithGuards(key, fetcher, options);
624
+ return this.fetchWithGuards(normalizedKey, fetcher, options);
475
625
  }
476
626
  async set(key, value, options) {
627
+ const normalizedKey = this.validateCacheKey(key);
628
+ this.validateWriteOptions(options);
477
629
  await this.startup;
478
- await this.storeEntry(key, "value", value, options);
630
+ await this.storeEntry(normalizedKey, "value", value, options);
479
631
  }
480
632
  async delete(key) {
633
+ const normalizedKey = this.validateCacheKey(key);
481
634
  await this.startup;
482
- await this.deleteKeys([key]);
483
- await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "delete" });
635
+ await this.deleteKeys([normalizedKey]);
636
+ await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
484
637
  }
485
638
  async clear() {
486
639
  await this.startup;
487
640
  await Promise.all(this.layers.map((layer) => layer.clear()));
488
641
  await this.tagIndex.clear();
642
+ this.accessProfiles.clear();
489
643
  this.metrics.invalidations += 1;
490
- this.logger.debug("clear");
644
+ this.logger.debug?.("clear");
491
645
  await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
492
646
  }
493
647
  async mget(entries) {
494
648
  if (entries.length === 0) {
495
649
  return [];
496
650
  }
497
- const canFastPath = entries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
651
+ const normalizedEntries = entries.map((entry) => ({
652
+ ...entry,
653
+ key: this.validateCacheKey(entry.key)
654
+ }));
655
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
656
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
498
657
  if (!canFastPath) {
499
- return Promise.all(entries.map((entry) => this.get(entry.key, entry.fetch, entry.options)));
658
+ const pendingReads = /* @__PURE__ */ new Map();
659
+ return Promise.all(
660
+ normalizedEntries.map((entry) => {
661
+ const optionsSignature = this.serializeOptions(entry.options);
662
+ const existing = pendingReads.get(entry.key);
663
+ if (!existing) {
664
+ const promise = this.get(entry.key, entry.fetch, entry.options);
665
+ pendingReads.set(entry.key, {
666
+ promise,
667
+ fetch: entry.fetch,
668
+ optionsSignature
669
+ });
670
+ return promise;
671
+ }
672
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
673
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
674
+ }
675
+ return existing.promise;
676
+ })
677
+ );
500
678
  }
501
679
  await this.startup;
502
- const pending = new Set(entries.map((_, index) => index));
503
- const results = Array(entries.length).fill(null);
504
- for (const layer of this.layers) {
505
- const indexes = [...pending];
506
- if (indexes.length === 0) {
680
+ const pending = /* @__PURE__ */ new Set();
681
+ const indexesByKey = /* @__PURE__ */ new Map();
682
+ const resultsByKey = /* @__PURE__ */ new Map();
683
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
684
+ const key = normalizedEntries[index].key;
685
+ const indexes = indexesByKey.get(key) ?? [];
686
+ indexes.push(index);
687
+ indexesByKey.set(key, indexes);
688
+ pending.add(key);
689
+ }
690
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
691
+ const layer = this.layers[layerIndex];
692
+ const keys = [...pending];
693
+ if (keys.length === 0) {
507
694
  break;
508
695
  }
509
- const keys = indexes.map((index) => entries[index].key);
510
696
  const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
511
697
  for (let offset = 0; offset < values.length; offset += 1) {
512
- const index = indexes[offset];
698
+ const key = keys[offset];
513
699
  const stored = values[offset];
514
700
  if (stored === null) {
515
701
  continue;
516
702
  }
517
703
  const resolved = resolveStoredValue(stored);
518
704
  if (resolved.state === "expired") {
519
- await layer.delete(entries[index].key);
705
+ await layer.delete(key);
520
706
  continue;
521
707
  }
522
- await this.tagIndex.touch(entries[index].key);
523
- await this.backfill(entries[index].key, stored, this.layers.indexOf(layer) - 1, entries[index].options);
524
- results[index] = resolved.value;
525
- pending.delete(index);
526
- this.metrics.hits += 1;
708
+ await this.tagIndex.touch(key);
709
+ await this.backfill(key, stored, layerIndex - 1);
710
+ resultsByKey.set(key, resolved.value);
711
+ pending.delete(key);
712
+ this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
527
713
  }
528
714
  }
529
715
  if (pending.size > 0) {
530
- for (const index of pending) {
531
- await this.tagIndex.remove(entries[index].key);
532
- this.metrics.misses += 1;
716
+ for (const key of pending) {
717
+ await this.tagIndex.remove(key);
718
+ this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
533
719
  }
534
720
  }
535
- return results;
721
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
536
722
  }
537
723
  async mset(entries) {
538
- await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.options)));
724
+ const normalizedEntries = entries.map((entry) => ({
725
+ ...entry,
726
+ key: this.validateCacheKey(entry.key)
727
+ }));
728
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
729
+ await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
730
+ }
731
+ async warm(entries, options = {}) {
732
+ const concurrency = Math.max(1, options.concurrency ?? 4);
733
+ const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
734
+ const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
735
+ while (queue.length > 0) {
736
+ const entry = queue.shift();
737
+ if (!entry) {
738
+ return;
739
+ }
740
+ try {
741
+ await this.get(entry.key, entry.fetcher, entry.options);
742
+ this.emit("warm", { key: entry.key });
743
+ } catch (error) {
744
+ this.emitError("warm", { key: entry.key, error: this.formatError(error) });
745
+ if (!options.continueOnError) {
746
+ throw error;
747
+ }
748
+ }
749
+ }
750
+ });
751
+ await Promise.all(workers);
752
+ }
753
+ wrap(prefix, fetcher, options = {}) {
754
+ return (...args) => {
755
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
756
+ const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
757
+ return this.get(key, () => fetcher(...args), options);
758
+ };
759
+ }
760
+ namespace(prefix) {
761
+ return new CacheNamespace(this, prefix);
539
762
  }
540
763
  async invalidateByTag(tag) {
541
764
  await this.startup;
@@ -552,13 +775,74 @@ var CacheStack = class {
552
775
  getMetrics() {
553
776
  return { ...this.metrics };
554
777
  }
778
+ getStats() {
779
+ return {
780
+ metrics: this.getMetrics(),
781
+ layers: this.layers.map((layer) => ({
782
+ name: layer.name,
783
+ isLocal: Boolean(layer.isLocal),
784
+ degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
785
+ })),
786
+ backgroundRefreshes: this.backgroundRefreshes.size
787
+ };
788
+ }
555
789
  resetMetrics() {
556
790
  Object.assign(this.metrics, EMPTY_METRICS());
557
791
  }
558
- async disconnect() {
792
+ async exportState() {
793
+ await this.startup;
794
+ const exported = /* @__PURE__ */ new Map();
795
+ for (const layer of this.layers) {
796
+ if (!layer.keys) {
797
+ continue;
798
+ }
799
+ const keys = await layer.keys();
800
+ for (const key of keys) {
801
+ if (exported.has(key)) {
802
+ continue;
803
+ }
804
+ const stored = await this.readLayerEntry(layer, key);
805
+ if (stored === null) {
806
+ continue;
807
+ }
808
+ exported.set(key, {
809
+ key,
810
+ value: stored,
811
+ ttl: remainingStoredTtlSeconds(stored)
812
+ });
813
+ }
814
+ }
815
+ return [...exported.values()];
816
+ }
817
+ async importState(entries) {
559
818
  await this.startup;
560
- await this.unsubscribeInvalidation?.();
561
- await Promise.allSettled(this.backgroundRefreshes.values());
819
+ await Promise.all(entries.map(async (entry) => {
820
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
821
+ await this.tagIndex.touch(entry.key);
822
+ }));
823
+ }
824
+ async persistToFile(filePath) {
825
+ const snapshot = await this.exportState();
826
+ await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
827
+ }
828
+ async restoreFromFile(filePath) {
829
+ const raw = await import_node_fs.promises.readFile(filePath, "utf8");
830
+ const snapshot = JSON.parse(raw);
831
+ if (!this.isCacheSnapshotEntries(snapshot)) {
832
+ throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
833
+ }
834
+ await this.importState(snapshot);
835
+ }
836
+ async disconnect() {
837
+ if (!this.disconnectPromise) {
838
+ this.isDisconnecting = true;
839
+ this.disconnectPromise = (async () => {
840
+ await this.startup;
841
+ await this.unsubscribeInvalidation?.();
842
+ await Promise.allSettled([...this.backgroundRefreshes.values()]);
843
+ })();
844
+ }
845
+ await this.disconnectPromise;
562
846
  }
563
847
  async initialize() {
564
848
  if (!this.options.invalidationBus) {
@@ -598,6 +882,7 @@ var CacheStack = class {
598
882
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
599
883
  const deadline = Date.now() + timeoutMs;
600
884
  this.metrics.singleFlightWaits += 1;
885
+ this.emit("stampede-dedupe", { key });
601
886
  while (Date.now() < deadline) {
602
887
  const hit = await this.readFromLayers(key, options, "fresh-only");
603
888
  if (hit.found) {
@@ -609,8 +894,16 @@ var CacheStack = class {
609
894
  return this.fetchAndPopulate(key, fetcher, options);
610
895
  }
611
896
  async fetchAndPopulate(key, fetcher, options) {
897
+ this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
612
898
  this.metrics.fetches += 1;
613
- const fetched = await fetcher();
899
+ let fetched;
900
+ try {
901
+ fetched = await fetcher();
902
+ this.resetCircuitBreaker(key);
903
+ } catch (error) {
904
+ this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
905
+ throw error;
906
+ }
614
907
  if (fetched === null || fetched === void 0) {
615
908
  if (!this.shouldNegativeCache(options)) {
616
909
  return null;
@@ -629,8 +922,9 @@ var CacheStack = class {
629
922
  await this.tagIndex.touch(key);
630
923
  }
631
924
  this.metrics.sets += 1;
632
- this.logger.debug("set", { key, kind, tags: options?.tags });
633
- if (this.options.publishSetInvalidation !== false) {
925
+ this.logger.debug?.("set", { key, kind, tags: options?.tags });
926
+ this.emit("set", { key, kind, tags: options?.tags });
927
+ if (this.shouldBroadcastL1Invalidation()) {
634
928
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
635
929
  }
636
930
  }
@@ -640,6 +934,7 @@ var CacheStack = class {
640
934
  const layer = this.layers[index];
641
935
  const stored = await this.readLayerEntry(layer, key);
642
936
  if (stored === null) {
937
+ this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
643
938
  continue;
644
939
  }
645
940
  const resolved = resolveStoredValue(stored);
@@ -653,20 +948,34 @@ var CacheStack = class {
653
948
  }
654
949
  await this.tagIndex.touch(key);
655
950
  await this.backfill(key, stored, index - 1, options);
656
- this.logger.debug("hit", { key, layer: layer.name, state: resolved.state });
657
- return { found: true, value: resolved.value, stored, state: resolved.state };
951
+ this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
952
+ this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
953
+ this.emit("hit", { key, layer: layer.name, state: resolved.state });
954
+ return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
658
955
  }
659
956
  if (!sawRetainableValue) {
660
957
  await this.tagIndex.remove(key);
661
958
  }
662
- this.logger.debug("miss", { key, mode });
959
+ this.logger.debug?.("miss", { key, mode });
960
+ this.emit("miss", { key, mode });
663
961
  return { found: false, value: null, stored: null, state: "miss" };
664
962
  }
665
963
  async readLayerEntry(layer, key) {
964
+ if (this.shouldSkipLayer(layer)) {
965
+ return null;
966
+ }
666
967
  if (layer.getEntry) {
667
- return layer.getEntry(key);
968
+ try {
969
+ return await layer.getEntry(key);
970
+ } catch (error) {
971
+ return this.handleLayerFailure(layer, "read", error);
972
+ }
973
+ }
974
+ try {
975
+ return await layer.get(key);
976
+ } catch (error) {
977
+ return this.handleLayerFailure(layer, "read", error);
668
978
  }
669
- return layer.get(key);
670
979
  }
671
980
  async backfill(key, stored, upToIndex, options) {
672
981
  if (upToIndex < 0) {
@@ -674,16 +983,28 @@ var CacheStack = class {
674
983
  }
675
984
  for (let index = 0; index <= upToIndex; index += 1) {
676
985
  const layer = this.layers[index];
986
+ if (this.shouldSkipLayer(layer)) {
987
+ continue;
988
+ }
677
989
  const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
678
- await layer.set(key, stored, ttl);
990
+ try {
991
+ await layer.set(key, stored, ttl);
992
+ } catch (error) {
993
+ await this.handleLayerFailure(layer, "backfill", error);
994
+ continue;
995
+ }
679
996
  this.metrics.backfills += 1;
680
- this.logger.debug("backfill", { key, layer: layer.name });
997
+ this.logger.debug?.("backfill", { key, layer: layer.name });
998
+ this.emit("backfill", { key, layer: layer.name });
681
999
  }
682
1000
  }
683
1001
  async writeAcrossLayers(key, kind, value, options) {
684
1002
  const now = Date.now();
685
1003
  const operations = this.layers.map((layer) => async () => {
686
- const freshTtl = this.resolveFreshTtl(layer.name, kind, options, layer.defaultTtl);
1004
+ if (this.shouldSkipLayer(layer)) {
1005
+ return;
1006
+ }
1007
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
687
1008
  const staleWhileRevalidate = this.resolveLayerSeconds(
688
1009
  layer.name,
689
1010
  options?.staleWhileRevalidate,
@@ -703,7 +1024,11 @@ var CacheStack = class {
703
1024
  now
704
1025
  });
705
1026
  const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
706
- await layer.set(key, payload, ttl);
1027
+ try {
1028
+ await layer.set(key, payload, ttl);
1029
+ } catch (error) {
1030
+ await this.handleLayerFailure(layer, "write", error);
1031
+ }
707
1032
  });
708
1033
  await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
709
1034
  }
@@ -718,7 +1043,7 @@ var CacheStack = class {
718
1043
  return;
719
1044
  }
720
1045
  this.metrics.writeFailures += failures.length;
721
- this.logger.debug("write-failure", {
1046
+ this.logger.debug?.("write-failure", {
722
1047
  ...context,
723
1048
  failures: failures.map((failure) => this.formatError(failure.reason))
724
1049
  });
@@ -729,15 +1054,21 @@ var CacheStack = class {
729
1054
  );
730
1055
  }
731
1056
  }
732
- resolveFreshTtl(layerName, kind, options, fallbackTtl) {
1057
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
733
1058
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
734
1059
  layerName,
735
1060
  options?.negativeTtl,
736
1061
  this.options.negativeTtl,
737
1062
  this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
738
1063
  ) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
1064
+ const adaptiveTtl = this.applyAdaptiveTtl(
1065
+ key,
1066
+ layerName,
1067
+ baseTtl,
1068
+ options?.adaptiveTtl ?? this.options.adaptiveTtl
1069
+ );
739
1070
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
740
- return this.applyJitter(baseTtl, jitter);
1071
+ return this.applyJitter(adaptiveTtl, jitter);
741
1072
  }
742
1073
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
743
1074
  if (override !== void 0) {
@@ -765,7 +1096,7 @@ var CacheStack = class {
765
1096
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
766
1097
  }
767
1098
  scheduleBackgroundRefresh(key, fetcher, options) {
768
- if (this.backgroundRefreshes.has(key)) {
1099
+ if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
769
1100
  return;
770
1101
  }
771
1102
  const refresh = (async () => {
@@ -774,7 +1105,7 @@ var CacheStack = class {
774
1105
  await this.fetchWithGuards(key, fetcher, options);
775
1106
  } catch (error) {
776
1107
  this.metrics.refreshErrors += 1;
777
- this.logger.debug("refresh-error", { key, error: this.formatError(error) });
1108
+ this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
778
1109
  } finally {
779
1110
  this.backgroundRefreshes.delete(key);
780
1111
  }
@@ -792,21 +1123,15 @@ var CacheStack = class {
792
1123
  if (keys.length === 0) {
793
1124
  return;
794
1125
  }
795
- await Promise.all(
796
- this.layers.map(async (layer) => {
797
- if (layer.deleteMany) {
798
- await layer.deleteMany(keys);
799
- return;
800
- }
801
- await Promise.all(keys.map((key) => layer.delete(key)));
802
- })
803
- );
1126
+ await this.deleteKeysFromLayers(this.layers, keys);
804
1127
  for (const key of keys) {
805
1128
  await this.tagIndex.remove(key);
1129
+ this.accessProfiles.delete(key);
806
1130
  }
807
1131
  this.metrics.deletes += keys.length;
808
1132
  this.metrics.invalidations += 1;
809
- this.logger.debug("delete", { keys });
1133
+ this.logger.debug?.("delete", { keys });
1134
+ this.emit("delete", { keys });
810
1135
  }
811
1136
  async publishInvalidation(message) {
812
1137
  if (!this.options.invalidationBus) {
@@ -825,21 +1150,15 @@ var CacheStack = class {
825
1150
  if (message.scope === "clear") {
826
1151
  await Promise.all(localLayers.map((layer) => layer.clear()));
827
1152
  await this.tagIndex.clear();
1153
+ this.accessProfiles.clear();
828
1154
  return;
829
1155
  }
830
1156
  const keys = message.keys ?? [];
831
- await Promise.all(
832
- localLayers.map(async (layer) => {
833
- if (layer.deleteMany) {
834
- await layer.deleteMany(keys);
835
- return;
836
- }
837
- await Promise.all(keys.map((key) => layer.delete(key)));
838
- })
839
- );
1157
+ await this.deleteKeysFromLayers(localLayers, keys);
840
1158
  if (message.operation !== "write") {
841
1159
  for (const key of keys) {
842
1160
  await this.tagIndex.remove(key);
1161
+ this.accessProfiles.delete(key);
843
1162
  }
844
1163
  }
845
1164
  }
@@ -852,6 +1171,257 @@ var CacheStack = class {
852
1171
  sleep(ms) {
853
1172
  return new Promise((resolve) => setTimeout(resolve, ms));
854
1173
  }
1174
+ shouldBroadcastL1Invalidation() {
1175
+ return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1176
+ }
1177
+ async deleteKeysFromLayers(layers, keys) {
1178
+ await Promise.all(
1179
+ layers.map(async (layer) => {
1180
+ if (this.shouldSkipLayer(layer)) {
1181
+ return;
1182
+ }
1183
+ if (layer.deleteMany) {
1184
+ try {
1185
+ await layer.deleteMany(keys);
1186
+ } catch (error) {
1187
+ await this.handleLayerFailure(layer, "delete", error);
1188
+ }
1189
+ return;
1190
+ }
1191
+ await Promise.all(keys.map(async (key) => {
1192
+ try {
1193
+ await layer.delete(key);
1194
+ } catch (error) {
1195
+ await this.handleLayerFailure(layer, "delete", error);
1196
+ }
1197
+ }));
1198
+ })
1199
+ );
1200
+ }
1201
+ validateConfiguration() {
1202
+ if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
1203
+ throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
1204
+ }
1205
+ if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
1206
+ throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
1207
+ }
1208
+ this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
1209
+ this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
1210
+ this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
1211
+ this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
1212
+ this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
1213
+ this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1214
+ this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1215
+ this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1216
+ this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1217
+ this.validateCircuitBreakerOptions(this.options.circuitBreaker);
1218
+ }
1219
+ validateWriteOptions(options) {
1220
+ if (!options) {
1221
+ return;
1222
+ }
1223
+ this.validateLayerNumberOption("options.ttl", options.ttl);
1224
+ this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
1225
+ this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
1226
+ this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1227
+ this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1228
+ this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
1229
+ this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1230
+ this.validateCircuitBreakerOptions(options.circuitBreaker);
1231
+ }
1232
+ validateLayerNumberOption(name, value) {
1233
+ if (value === void 0) {
1234
+ return;
1235
+ }
1236
+ if (typeof value === "number") {
1237
+ this.validateNonNegativeNumber(name, value);
1238
+ return;
1239
+ }
1240
+ for (const [layerName, layerValue] of Object.entries(value)) {
1241
+ if (layerValue === void 0) {
1242
+ continue;
1243
+ }
1244
+ this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
1245
+ }
1246
+ }
1247
+ validatePositiveNumber(name, value) {
1248
+ if (value === void 0) {
1249
+ return;
1250
+ }
1251
+ if (!Number.isFinite(value) || value <= 0) {
1252
+ throw new Error(`${name} must be a positive finite number.`);
1253
+ }
1254
+ }
1255
+ validateNonNegativeNumber(name, value) {
1256
+ if (!Number.isFinite(value) || value < 0) {
1257
+ throw new Error(`${name} must be a non-negative finite number.`);
1258
+ }
1259
+ }
1260
+ validateCacheKey(key) {
1261
+ if (key.length === 0) {
1262
+ throw new Error("Cache key must not be empty.");
1263
+ }
1264
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
1265
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1266
+ }
1267
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
1268
+ throw new Error("Cache key contains unsupported control characters.");
1269
+ }
1270
+ return key;
1271
+ }
1272
+ serializeOptions(options) {
1273
+ return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1274
+ }
1275
+ validateAdaptiveTtlOptions(options) {
1276
+ if (!options || options === true) {
1277
+ return;
1278
+ }
1279
+ this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
1280
+ this.validateLayerNumberOption("adaptiveTtl.step", options.step);
1281
+ this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
1282
+ }
1283
+ validateCircuitBreakerOptions(options) {
1284
+ if (!options) {
1285
+ return;
1286
+ }
1287
+ this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1288
+ this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1289
+ }
1290
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1291
+ const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
1292
+ const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
1293
+ if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
1294
+ const refreshed = refreshStoredEnvelope(hit.stored);
1295
+ const ttl = remainingStoredTtlSeconds(refreshed);
1296
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1297
+ const layer = this.layers[index];
1298
+ if (this.shouldSkipLayer(layer)) {
1299
+ continue;
1300
+ }
1301
+ try {
1302
+ await layer.set(key, refreshed, ttl);
1303
+ } catch (error) {
1304
+ await this.handleLayerFailure(layer, "sliding-ttl", error);
1305
+ }
1306
+ }
1307
+ }
1308
+ if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
1309
+ this.scheduleBackgroundRefresh(key, fetcher, options);
1310
+ }
1311
+ }
1312
+ applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
1313
+ if (!ttl || !adaptiveTtl) {
1314
+ return ttl;
1315
+ }
1316
+ const profile = this.accessProfiles.get(key);
1317
+ if (!profile) {
1318
+ return ttl;
1319
+ }
1320
+ const config = adaptiveTtl === true ? {} : adaptiveTtl;
1321
+ const hotAfter = config.hotAfter ?? 3;
1322
+ if (profile.hits < hotAfter) {
1323
+ return ttl;
1324
+ }
1325
+ const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
1326
+ const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
1327
+ const multiplier = Math.floor(profile.hits / hotAfter);
1328
+ return Math.min(maxTtl, ttl + step * multiplier);
1329
+ }
1330
+ recordAccess(key) {
1331
+ const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
1332
+ profile.hits += 1;
1333
+ profile.lastAccessAt = Date.now();
1334
+ this.accessProfiles.set(key, profile);
1335
+ }
1336
+ incrementMetricMap(target, key) {
1337
+ target[key] = (target[key] ?? 0) + 1;
1338
+ }
1339
+ shouldSkipLayer(layer) {
1340
+ const degradedUntil = this.layerDegradedUntil.get(layer.name);
1341
+ return degradedUntil !== void 0 && degradedUntil > Date.now();
1342
+ }
1343
+ async handleLayerFailure(layer, operation, error) {
1344
+ if (!this.isGracefulDegradationEnabled()) {
1345
+ throw error;
1346
+ }
1347
+ const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1348
+ this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
1349
+ this.metrics.degradedOperations += 1;
1350
+ this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
1351
+ this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
1352
+ return null;
1353
+ }
1354
+ isGracefulDegradationEnabled() {
1355
+ return Boolean(this.options.gracefulDegradation);
1356
+ }
1357
+ assertCircuitClosed(key, options) {
1358
+ const state = this.circuitBreakers.get(key);
1359
+ if (!state?.openUntil) {
1360
+ return;
1361
+ }
1362
+ if (state.openUntil <= Date.now()) {
1363
+ state.openUntil = null;
1364
+ state.failures = 0;
1365
+ this.circuitBreakers.set(key, state);
1366
+ return;
1367
+ }
1368
+ this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
1369
+ throw new Error(`Circuit breaker is open for key "${key}".`);
1370
+ }
1371
+ recordCircuitFailure(key, options, error) {
1372
+ if (!options) {
1373
+ return;
1374
+ }
1375
+ const failureThreshold = options.failureThreshold ?? 3;
1376
+ const cooldownMs = options.cooldownMs ?? 3e4;
1377
+ const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
1378
+ state.failures += 1;
1379
+ if (state.failures >= failureThreshold) {
1380
+ state.openUntil = Date.now() + cooldownMs;
1381
+ this.metrics.circuitBreakerTrips += 1;
1382
+ }
1383
+ this.circuitBreakers.set(key, state);
1384
+ this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
1385
+ }
1386
+ resetCircuitBreaker(key) {
1387
+ this.circuitBreakers.delete(key);
1388
+ }
1389
+ isNegativeStoredValue(stored) {
1390
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1391
+ }
1392
+ emitError(operation, context) {
1393
+ this.logger.error?.(operation, context);
1394
+ if (this.listenerCount("error") > 0) {
1395
+ this.emit("error", { operation, ...context });
1396
+ }
1397
+ }
1398
+ serializeKeyPart(value) {
1399
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1400
+ return String(value);
1401
+ }
1402
+ return JSON.stringify(this.normalizeForSerialization(value));
1403
+ }
1404
+ isCacheSnapshotEntries(value) {
1405
+ return Array.isArray(value) && value.every((entry) => {
1406
+ if (!entry || typeof entry !== "object") {
1407
+ return false;
1408
+ }
1409
+ const candidate = entry;
1410
+ return typeof candidate.key === "string";
1411
+ });
1412
+ }
1413
+ normalizeForSerialization(value) {
1414
+ if (Array.isArray(value)) {
1415
+ return value.map((entry) => this.normalizeForSerialization(entry));
1416
+ }
1417
+ if (value && typeof value === "object") {
1418
+ return Object.keys(value).sort().reduce((normalized, key) => {
1419
+ normalized[key] = this.normalizeForSerialization(value[key]);
1420
+ return normalized;
1421
+ }, {});
1422
+ }
1423
+ return value;
1424
+ }
855
1425
  };
856
1426
 
857
1427
  // src/module.ts
@@ -878,5 +1448,6 @@ CacheStackModule = __decorateClass([
878
1448
  0 && (module.exports = {
879
1449
  CACHE_STACK,
880
1450
  CacheStackModule,
1451
+ Cacheable,
881
1452
  InjectCacheStack
882
1453
  });