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.
@@ -12,11 +12,43 @@ var __decorateClass = (decorators, target, key, kind) => {
12
12
  // src/constants.ts
13
13
  var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
14
14
 
15
+ // ../../src/decorators/createCachedMethodDecorator.ts
16
+ function createCachedMethodDecorator(options) {
17
+ const wrappedByInstance = /* @__PURE__ */ new WeakMap();
18
+ return ((_, propertyKey, descriptor) => {
19
+ const original = descriptor.value;
20
+ if (typeof original !== "function") {
21
+ throw new Error("createCachedMethodDecorator can only be applied to methods.");
22
+ }
23
+ descriptor.value = async function(...args) {
24
+ const instance = this;
25
+ let wrapped = wrappedByInstance.get(instance);
26
+ if (!wrapped) {
27
+ const cache = options.cache(instance);
28
+ wrapped = cache.wrap(
29
+ options.prefix ?? String(propertyKey),
30
+ (...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
31
+ options
32
+ );
33
+ wrappedByInstance.set(instance, wrapped);
34
+ }
35
+ return wrapped(...args);
36
+ };
37
+ });
38
+ }
39
+
40
+ // src/decorators.ts
41
+ function Cacheable(options) {
42
+ return createCachedMethodDecorator(options);
43
+ }
44
+
15
45
  // src/module.ts
16
46
  import { Global, Inject, Module } from "@nestjs/common";
17
47
 
18
48
  // ../../src/CacheStack.ts
19
49
  import { randomUUID } from "crypto";
50
+ import { promises as fs } from "fs";
51
+ import { EventEmitter } from "events";
20
52
 
21
53
  // ../../src/internal/StoredValue.ts
22
54
  function isStoredValueEnvelope(value) {
@@ -36,7 +68,10 @@ function createStoredValueEnvelope(options) {
36
68
  value: options.value,
37
69
  freshUntil,
38
70
  staleUntil,
39
- errorUntil
71
+ errorUntil,
72
+ freshTtlSeconds: freshTtlSeconds ?? null,
73
+ staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
74
+ staleIfErrorSeconds: staleIfErrorSeconds ?? null
40
75
  };
41
76
  }
42
77
  function resolveStoredValue(stored, now = Date.now()) {
@@ -77,6 +112,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
77
112
  }
78
113
  return Math.max(1, Math.ceil(remainingMs / 1e3));
79
114
  }
115
+ function remainingFreshTtlSeconds(stored, now = Date.now()) {
116
+ if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
117
+ return void 0;
118
+ }
119
+ const remainingMs = stored.freshUntil - now;
120
+ if (remainingMs <= 0) {
121
+ return 0;
122
+ }
123
+ return Math.max(1, Math.ceil(remainingMs / 1e3));
124
+ }
125
+ function refreshStoredEnvelope(stored, now = Date.now()) {
126
+ if (!isStoredValueEnvelope(stored)) {
127
+ return stored;
128
+ }
129
+ return createStoredValueEnvelope({
130
+ kind: stored.kind,
131
+ value: stored.value,
132
+ freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
133
+ staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
134
+ staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
135
+ now
136
+ });
137
+ }
80
138
  function maxExpiry(stored) {
81
139
  const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
82
140
  (value) => value !== null
@@ -93,6 +151,61 @@ function normalizePositiveSeconds(value) {
93
151
  return value;
94
152
  }
95
153
 
154
+ // ../../src/CacheNamespace.ts
155
+ var CacheNamespace = class {
156
+ constructor(cache, prefix) {
157
+ this.cache = cache;
158
+ this.prefix = prefix;
159
+ }
160
+ cache;
161
+ prefix;
162
+ async get(key, fetcher, options) {
163
+ return this.cache.get(this.qualify(key), fetcher, options);
164
+ }
165
+ async set(key, value, options) {
166
+ await this.cache.set(this.qualify(key), value, options);
167
+ }
168
+ async delete(key) {
169
+ await this.cache.delete(this.qualify(key));
170
+ }
171
+ async clear() {
172
+ await this.cache.invalidateByPattern(`${this.prefix}:*`);
173
+ }
174
+ async mget(entries) {
175
+ return this.cache.mget(entries.map((entry) => ({
176
+ ...entry,
177
+ key: this.qualify(entry.key)
178
+ })));
179
+ }
180
+ async mset(entries) {
181
+ await this.cache.mset(entries.map((entry) => ({
182
+ ...entry,
183
+ key: this.qualify(entry.key)
184
+ })));
185
+ }
186
+ async invalidateByTag(tag) {
187
+ await this.cache.invalidateByTag(tag);
188
+ }
189
+ async invalidateByPattern(pattern) {
190
+ await this.cache.invalidateByPattern(this.qualify(pattern));
191
+ }
192
+ wrap(keyPrefix, fetcher, options) {
193
+ return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
194
+ }
195
+ warm(entries, options) {
196
+ return this.cache.warm(entries.map((entry) => ({
197
+ ...entry,
198
+ key: this.qualify(entry.key)
199
+ })), options);
200
+ }
201
+ getMetrics() {
202
+ return this.cache.getMetrics();
203
+ }
204
+ qualify(key) {
205
+ return `${this.prefix}:${key}`;
206
+ }
207
+ };
208
+
96
209
  // ../../src/invalidation/PatternMatcher.ts
97
210
  var PatternMatcher = class {
98
211
  static matches(pattern, value) {
@@ -339,22 +452,24 @@ var Mutex = class {
339
452
  var StampedeGuard = class {
340
453
  mutexes = /* @__PURE__ */ new Map();
341
454
  async execute(key, task) {
342
- const mutex = this.getMutex(key);
455
+ const entry = this.getMutexEntry(key);
343
456
  try {
344
- return await mutex.runExclusive(task);
457
+ return await entry.mutex.runExclusive(task);
345
458
  } finally {
346
- if (!mutex.isLocked()) {
459
+ entry.references -= 1;
460
+ if (entry.references === 0 && !entry.mutex.isLocked()) {
347
461
  this.mutexes.delete(key);
348
462
  }
349
463
  }
350
464
  }
351
- getMutex(key) {
352
- let mutex = this.mutexes.get(key);
353
- if (!mutex) {
354
- mutex = new Mutex();
355
- this.mutexes.set(key, mutex);
465
+ getMutexEntry(key) {
466
+ let entry = this.mutexes.get(key);
467
+ if (!entry) {
468
+ entry = { mutex: new Mutex(), references: 0 };
469
+ this.mutexes.set(key, entry);
356
470
  }
357
- return mutex;
471
+ entry.references += 1;
472
+ return entry;
358
473
  }
359
474
  };
360
475
 
@@ -363,6 +478,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
363
478
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
364
479
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
365
480
  var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
481
+ var MAX_CACHE_KEY_LENGTH = 1024;
366
482
  var EMPTY_METRICS = () => ({
367
483
  hits: 0,
368
484
  misses: 0,
@@ -375,7 +491,12 @@ var EMPTY_METRICS = () => ({
375
491
  refreshes: 0,
376
492
  refreshErrors: 0,
377
493
  writeFailures: 0,
378
- singleFlightWaits: 0
494
+ singleFlightWaits: 0,
495
+ negativeCacheHits: 0,
496
+ circuitBreakerTrips: 0,
497
+ degradedOperations: 0,
498
+ hitsByLayer: {},
499
+ missesByLayer: {}
379
500
  });
380
501
  var DebugLogger = class {
381
502
  enabled;
@@ -383,20 +504,34 @@ var DebugLogger = class {
383
504
  this.enabled = enabled;
384
505
  }
385
506
  debug(message, context) {
507
+ this.write("debug", message, context);
508
+ }
509
+ info(message, context) {
510
+ this.write("info", message, context);
511
+ }
512
+ warn(message, context) {
513
+ this.write("warn", message, context);
514
+ }
515
+ error(message, context) {
516
+ this.write("error", message, context);
517
+ }
518
+ write(level, message, context) {
386
519
  if (!this.enabled) {
387
520
  return;
388
521
  }
389
522
  const suffix = context ? ` ${JSON.stringify(context)}` : "";
390
- console.debug(`[layercache] ${message}${suffix}`);
523
+ console[level](`[layercache] ${message}${suffix}`);
391
524
  }
392
525
  };
393
- var CacheStack = class {
526
+ var CacheStack = class extends EventEmitter {
394
527
  constructor(layers, options = {}) {
528
+ super();
395
529
  this.layers = layers;
396
530
  this.options = options;
397
531
  if (layers.length === 0) {
398
532
  throw new Error("CacheStack requires at least one cache layer.");
399
533
  }
534
+ this.validateConfiguration();
400
535
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
401
536
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
402
537
  this.tagIndex = options.tagIndex ?? new TagIndex();
@@ -412,33 +547,47 @@ var CacheStack = class {
412
547
  logger;
413
548
  tagIndex;
414
549
  backgroundRefreshes = /* @__PURE__ */ new Map();
550
+ accessProfiles = /* @__PURE__ */ new Map();
551
+ layerDegradedUntil = /* @__PURE__ */ new Map();
552
+ circuitBreakers = /* @__PURE__ */ new Map();
553
+ isDisconnecting = false;
554
+ disconnectPromise;
415
555
  async get(key, fetcher, options) {
556
+ const normalizedKey = this.validateCacheKey(key);
557
+ this.validateWriteOptions(options);
416
558
  await this.startup;
417
- const hit = await this.readFromLayers(key, options, "allow-stale");
559
+ const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
418
560
  if (hit.found) {
561
+ this.recordAccess(normalizedKey);
562
+ if (this.isNegativeStoredValue(hit.stored)) {
563
+ this.metrics.negativeCacheHits += 1;
564
+ }
419
565
  if (hit.state === "fresh") {
420
566
  this.metrics.hits += 1;
567
+ await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
421
568
  return hit.value;
422
569
  }
423
570
  if (hit.state === "stale-while-revalidate") {
424
571
  this.metrics.hits += 1;
425
572
  this.metrics.staleHits += 1;
573
+ this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
426
574
  if (fetcher) {
427
- this.scheduleBackgroundRefresh(key, fetcher, options);
575
+ this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
428
576
  }
429
577
  return hit.value;
430
578
  }
431
579
  if (!fetcher) {
432
580
  this.metrics.hits += 1;
433
581
  this.metrics.staleHits += 1;
582
+ this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
434
583
  return hit.value;
435
584
  }
436
585
  try {
437
- return await this.fetchWithGuards(key, fetcher, options);
586
+ return await this.fetchWithGuards(normalizedKey, fetcher, options);
438
587
  } catch (error) {
439
588
  this.metrics.staleHits += 1;
440
589
  this.metrics.refreshErrors += 1;
441
- this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
590
+ this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
442
591
  return hit.value;
443
592
  }
444
593
  }
@@ -446,71 +595,144 @@ var CacheStack = class {
446
595
  if (!fetcher) {
447
596
  return null;
448
597
  }
449
- return this.fetchWithGuards(key, fetcher, options);
598
+ return this.fetchWithGuards(normalizedKey, fetcher, options);
450
599
  }
451
600
  async set(key, value, options) {
601
+ const normalizedKey = this.validateCacheKey(key);
602
+ this.validateWriteOptions(options);
452
603
  await this.startup;
453
- await this.storeEntry(key, "value", value, options);
604
+ await this.storeEntry(normalizedKey, "value", value, options);
454
605
  }
455
606
  async delete(key) {
607
+ const normalizedKey = this.validateCacheKey(key);
456
608
  await this.startup;
457
- await this.deleteKeys([key]);
458
- await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "delete" });
609
+ await this.deleteKeys([normalizedKey]);
610
+ await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
459
611
  }
460
612
  async clear() {
461
613
  await this.startup;
462
614
  await Promise.all(this.layers.map((layer) => layer.clear()));
463
615
  await this.tagIndex.clear();
616
+ this.accessProfiles.clear();
464
617
  this.metrics.invalidations += 1;
465
- this.logger.debug("clear");
618
+ this.logger.debug?.("clear");
466
619
  await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
467
620
  }
468
621
  async mget(entries) {
469
622
  if (entries.length === 0) {
470
623
  return [];
471
624
  }
472
- const canFastPath = entries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
625
+ const normalizedEntries = entries.map((entry) => ({
626
+ ...entry,
627
+ key: this.validateCacheKey(entry.key)
628
+ }));
629
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
630
+ const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
473
631
  if (!canFastPath) {
474
- return Promise.all(entries.map((entry) => this.get(entry.key, entry.fetch, entry.options)));
632
+ const pendingReads = /* @__PURE__ */ new Map();
633
+ return Promise.all(
634
+ normalizedEntries.map((entry) => {
635
+ const optionsSignature = this.serializeOptions(entry.options);
636
+ const existing = pendingReads.get(entry.key);
637
+ if (!existing) {
638
+ const promise = this.get(entry.key, entry.fetch, entry.options);
639
+ pendingReads.set(entry.key, {
640
+ promise,
641
+ fetch: entry.fetch,
642
+ optionsSignature
643
+ });
644
+ return promise;
645
+ }
646
+ if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
647
+ throw new Error(`mget received conflicting entries for key "${entry.key}".`);
648
+ }
649
+ return existing.promise;
650
+ })
651
+ );
475
652
  }
476
653
  await this.startup;
477
- const pending = new Set(entries.map((_, index) => index));
478
- const results = Array(entries.length).fill(null);
479
- for (const layer of this.layers) {
480
- const indexes = [...pending];
481
- if (indexes.length === 0) {
654
+ const pending = /* @__PURE__ */ new Set();
655
+ const indexesByKey = /* @__PURE__ */ new Map();
656
+ const resultsByKey = /* @__PURE__ */ new Map();
657
+ for (let index = 0; index < normalizedEntries.length; index += 1) {
658
+ const key = normalizedEntries[index].key;
659
+ const indexes = indexesByKey.get(key) ?? [];
660
+ indexes.push(index);
661
+ indexesByKey.set(key, indexes);
662
+ pending.add(key);
663
+ }
664
+ for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
665
+ const layer = this.layers[layerIndex];
666
+ const keys = [...pending];
667
+ if (keys.length === 0) {
482
668
  break;
483
669
  }
484
- const keys = indexes.map((index) => entries[index].key);
485
670
  const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
486
671
  for (let offset = 0; offset < values.length; offset += 1) {
487
- const index = indexes[offset];
672
+ const key = keys[offset];
488
673
  const stored = values[offset];
489
674
  if (stored === null) {
490
675
  continue;
491
676
  }
492
677
  const resolved = resolveStoredValue(stored);
493
678
  if (resolved.state === "expired") {
494
- await layer.delete(entries[index].key);
679
+ await layer.delete(key);
495
680
  continue;
496
681
  }
497
- await this.tagIndex.touch(entries[index].key);
498
- await this.backfill(entries[index].key, stored, this.layers.indexOf(layer) - 1, entries[index].options);
499
- results[index] = resolved.value;
500
- pending.delete(index);
501
- this.metrics.hits += 1;
682
+ await this.tagIndex.touch(key);
683
+ await this.backfill(key, stored, layerIndex - 1);
684
+ resultsByKey.set(key, resolved.value);
685
+ pending.delete(key);
686
+ this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
502
687
  }
503
688
  }
504
689
  if (pending.size > 0) {
505
- for (const index of pending) {
506
- await this.tagIndex.remove(entries[index].key);
507
- this.metrics.misses += 1;
690
+ for (const key of pending) {
691
+ await this.tagIndex.remove(key);
692
+ this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
508
693
  }
509
694
  }
510
- return results;
695
+ return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
511
696
  }
512
697
  async mset(entries) {
513
- await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.options)));
698
+ const normalizedEntries = entries.map((entry) => ({
699
+ ...entry,
700
+ key: this.validateCacheKey(entry.key)
701
+ }));
702
+ normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
703
+ await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
704
+ }
705
+ async warm(entries, options = {}) {
706
+ const concurrency = Math.max(1, options.concurrency ?? 4);
707
+ const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
708
+ const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
709
+ while (queue.length > 0) {
710
+ const entry = queue.shift();
711
+ if (!entry) {
712
+ return;
713
+ }
714
+ try {
715
+ await this.get(entry.key, entry.fetcher, entry.options);
716
+ this.emit("warm", { key: entry.key });
717
+ } catch (error) {
718
+ this.emitError("warm", { key: entry.key, error: this.formatError(error) });
719
+ if (!options.continueOnError) {
720
+ throw error;
721
+ }
722
+ }
723
+ }
724
+ });
725
+ await Promise.all(workers);
726
+ }
727
+ wrap(prefix, fetcher, options = {}) {
728
+ return (...args) => {
729
+ const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
730
+ const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
731
+ return this.get(key, () => fetcher(...args), options);
732
+ };
733
+ }
734
+ namespace(prefix) {
735
+ return new CacheNamespace(this, prefix);
514
736
  }
515
737
  async invalidateByTag(tag) {
516
738
  await this.startup;
@@ -527,13 +749,74 @@ var CacheStack = class {
527
749
  getMetrics() {
528
750
  return { ...this.metrics };
529
751
  }
752
+ getStats() {
753
+ return {
754
+ metrics: this.getMetrics(),
755
+ layers: this.layers.map((layer) => ({
756
+ name: layer.name,
757
+ isLocal: Boolean(layer.isLocal),
758
+ degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
759
+ })),
760
+ backgroundRefreshes: this.backgroundRefreshes.size
761
+ };
762
+ }
530
763
  resetMetrics() {
531
764
  Object.assign(this.metrics, EMPTY_METRICS());
532
765
  }
533
- async disconnect() {
766
+ async exportState() {
767
+ await this.startup;
768
+ const exported = /* @__PURE__ */ new Map();
769
+ for (const layer of this.layers) {
770
+ if (!layer.keys) {
771
+ continue;
772
+ }
773
+ const keys = await layer.keys();
774
+ for (const key of keys) {
775
+ if (exported.has(key)) {
776
+ continue;
777
+ }
778
+ const stored = await this.readLayerEntry(layer, key);
779
+ if (stored === null) {
780
+ continue;
781
+ }
782
+ exported.set(key, {
783
+ key,
784
+ value: stored,
785
+ ttl: remainingStoredTtlSeconds(stored)
786
+ });
787
+ }
788
+ }
789
+ return [...exported.values()];
790
+ }
791
+ async importState(entries) {
534
792
  await this.startup;
535
- await this.unsubscribeInvalidation?.();
536
- await Promise.allSettled(this.backgroundRefreshes.values());
793
+ await Promise.all(entries.map(async (entry) => {
794
+ await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
795
+ await this.tagIndex.touch(entry.key);
796
+ }));
797
+ }
798
+ async persistToFile(filePath) {
799
+ const snapshot = await this.exportState();
800
+ await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
801
+ }
802
+ async restoreFromFile(filePath) {
803
+ const raw = await fs.readFile(filePath, "utf8");
804
+ const snapshot = JSON.parse(raw);
805
+ if (!this.isCacheSnapshotEntries(snapshot)) {
806
+ throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
807
+ }
808
+ await this.importState(snapshot);
809
+ }
810
+ async disconnect() {
811
+ if (!this.disconnectPromise) {
812
+ this.isDisconnecting = true;
813
+ this.disconnectPromise = (async () => {
814
+ await this.startup;
815
+ await this.unsubscribeInvalidation?.();
816
+ await Promise.allSettled([...this.backgroundRefreshes.values()]);
817
+ })();
818
+ }
819
+ await this.disconnectPromise;
537
820
  }
538
821
  async initialize() {
539
822
  if (!this.options.invalidationBus) {
@@ -573,6 +856,7 @@ var CacheStack = class {
573
856
  const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
574
857
  const deadline = Date.now() + timeoutMs;
575
858
  this.metrics.singleFlightWaits += 1;
859
+ this.emit("stampede-dedupe", { key });
576
860
  while (Date.now() < deadline) {
577
861
  const hit = await this.readFromLayers(key, options, "fresh-only");
578
862
  if (hit.found) {
@@ -584,8 +868,16 @@ var CacheStack = class {
584
868
  return this.fetchAndPopulate(key, fetcher, options);
585
869
  }
586
870
  async fetchAndPopulate(key, fetcher, options) {
871
+ this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
587
872
  this.metrics.fetches += 1;
588
- const fetched = await fetcher();
873
+ let fetched;
874
+ try {
875
+ fetched = await fetcher();
876
+ this.resetCircuitBreaker(key);
877
+ } catch (error) {
878
+ this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
879
+ throw error;
880
+ }
589
881
  if (fetched === null || fetched === void 0) {
590
882
  if (!this.shouldNegativeCache(options)) {
591
883
  return null;
@@ -604,8 +896,9 @@ var CacheStack = class {
604
896
  await this.tagIndex.touch(key);
605
897
  }
606
898
  this.metrics.sets += 1;
607
- this.logger.debug("set", { key, kind, tags: options?.tags });
608
- if (this.options.publishSetInvalidation !== false) {
899
+ this.logger.debug?.("set", { key, kind, tags: options?.tags });
900
+ this.emit("set", { key, kind, tags: options?.tags });
901
+ if (this.shouldBroadcastL1Invalidation()) {
609
902
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
610
903
  }
611
904
  }
@@ -615,6 +908,7 @@ var CacheStack = class {
615
908
  const layer = this.layers[index];
616
909
  const stored = await this.readLayerEntry(layer, key);
617
910
  if (stored === null) {
911
+ this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
618
912
  continue;
619
913
  }
620
914
  const resolved = resolveStoredValue(stored);
@@ -628,20 +922,34 @@ var CacheStack = class {
628
922
  }
629
923
  await this.tagIndex.touch(key);
630
924
  await this.backfill(key, stored, index - 1, options);
631
- this.logger.debug("hit", { key, layer: layer.name, state: resolved.state });
632
- return { found: true, value: resolved.value, stored, state: resolved.state };
925
+ this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
926
+ this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
927
+ this.emit("hit", { key, layer: layer.name, state: resolved.state });
928
+ return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
633
929
  }
634
930
  if (!sawRetainableValue) {
635
931
  await this.tagIndex.remove(key);
636
932
  }
637
- this.logger.debug("miss", { key, mode });
933
+ this.logger.debug?.("miss", { key, mode });
934
+ this.emit("miss", { key, mode });
638
935
  return { found: false, value: null, stored: null, state: "miss" };
639
936
  }
640
937
  async readLayerEntry(layer, key) {
938
+ if (this.shouldSkipLayer(layer)) {
939
+ return null;
940
+ }
641
941
  if (layer.getEntry) {
642
- return layer.getEntry(key);
942
+ try {
943
+ return await layer.getEntry(key);
944
+ } catch (error) {
945
+ return this.handleLayerFailure(layer, "read", error);
946
+ }
947
+ }
948
+ try {
949
+ return await layer.get(key);
950
+ } catch (error) {
951
+ return this.handleLayerFailure(layer, "read", error);
643
952
  }
644
- return layer.get(key);
645
953
  }
646
954
  async backfill(key, stored, upToIndex, options) {
647
955
  if (upToIndex < 0) {
@@ -649,16 +957,28 @@ var CacheStack = class {
649
957
  }
650
958
  for (let index = 0; index <= upToIndex; index += 1) {
651
959
  const layer = this.layers[index];
960
+ if (this.shouldSkipLayer(layer)) {
961
+ continue;
962
+ }
652
963
  const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
653
- await layer.set(key, stored, ttl);
964
+ try {
965
+ await layer.set(key, stored, ttl);
966
+ } catch (error) {
967
+ await this.handleLayerFailure(layer, "backfill", error);
968
+ continue;
969
+ }
654
970
  this.metrics.backfills += 1;
655
- this.logger.debug("backfill", { key, layer: layer.name });
971
+ this.logger.debug?.("backfill", { key, layer: layer.name });
972
+ this.emit("backfill", { key, layer: layer.name });
656
973
  }
657
974
  }
658
975
  async writeAcrossLayers(key, kind, value, options) {
659
976
  const now = Date.now();
660
977
  const operations = this.layers.map((layer) => async () => {
661
- const freshTtl = this.resolveFreshTtl(layer.name, kind, options, layer.defaultTtl);
978
+ if (this.shouldSkipLayer(layer)) {
979
+ return;
980
+ }
981
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
662
982
  const staleWhileRevalidate = this.resolveLayerSeconds(
663
983
  layer.name,
664
984
  options?.staleWhileRevalidate,
@@ -678,7 +998,11 @@ var CacheStack = class {
678
998
  now
679
999
  });
680
1000
  const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
681
- await layer.set(key, payload, ttl);
1001
+ try {
1002
+ await layer.set(key, payload, ttl);
1003
+ } catch (error) {
1004
+ await this.handleLayerFailure(layer, "write", error);
1005
+ }
682
1006
  });
683
1007
  await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
684
1008
  }
@@ -693,7 +1017,7 @@ var CacheStack = class {
693
1017
  return;
694
1018
  }
695
1019
  this.metrics.writeFailures += failures.length;
696
- this.logger.debug("write-failure", {
1020
+ this.logger.debug?.("write-failure", {
697
1021
  ...context,
698
1022
  failures: failures.map((failure) => this.formatError(failure.reason))
699
1023
  });
@@ -704,15 +1028,21 @@ var CacheStack = class {
704
1028
  );
705
1029
  }
706
1030
  }
707
- resolveFreshTtl(layerName, kind, options, fallbackTtl) {
1031
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
708
1032
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
709
1033
  layerName,
710
1034
  options?.negativeTtl,
711
1035
  this.options.negativeTtl,
712
1036
  this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
713
1037
  ) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
1038
+ const adaptiveTtl = this.applyAdaptiveTtl(
1039
+ key,
1040
+ layerName,
1041
+ baseTtl,
1042
+ options?.adaptiveTtl ?? this.options.adaptiveTtl
1043
+ );
714
1044
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
715
- return this.applyJitter(baseTtl, jitter);
1045
+ return this.applyJitter(adaptiveTtl, jitter);
716
1046
  }
717
1047
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
718
1048
  if (override !== void 0) {
@@ -740,7 +1070,7 @@ var CacheStack = class {
740
1070
  return options?.negativeCache ?? this.options.negativeCaching ?? false;
741
1071
  }
742
1072
  scheduleBackgroundRefresh(key, fetcher, options) {
743
- if (this.backgroundRefreshes.has(key)) {
1073
+ if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
744
1074
  return;
745
1075
  }
746
1076
  const refresh = (async () => {
@@ -749,7 +1079,7 @@ var CacheStack = class {
749
1079
  await this.fetchWithGuards(key, fetcher, options);
750
1080
  } catch (error) {
751
1081
  this.metrics.refreshErrors += 1;
752
- this.logger.debug("refresh-error", { key, error: this.formatError(error) });
1082
+ this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
753
1083
  } finally {
754
1084
  this.backgroundRefreshes.delete(key);
755
1085
  }
@@ -767,21 +1097,15 @@ var CacheStack = class {
767
1097
  if (keys.length === 0) {
768
1098
  return;
769
1099
  }
770
- await Promise.all(
771
- this.layers.map(async (layer) => {
772
- if (layer.deleteMany) {
773
- await layer.deleteMany(keys);
774
- return;
775
- }
776
- await Promise.all(keys.map((key) => layer.delete(key)));
777
- })
778
- );
1100
+ await this.deleteKeysFromLayers(this.layers, keys);
779
1101
  for (const key of keys) {
780
1102
  await this.tagIndex.remove(key);
1103
+ this.accessProfiles.delete(key);
781
1104
  }
782
1105
  this.metrics.deletes += keys.length;
783
1106
  this.metrics.invalidations += 1;
784
- this.logger.debug("delete", { keys });
1107
+ this.logger.debug?.("delete", { keys });
1108
+ this.emit("delete", { keys });
785
1109
  }
786
1110
  async publishInvalidation(message) {
787
1111
  if (!this.options.invalidationBus) {
@@ -800,21 +1124,15 @@ var CacheStack = class {
800
1124
  if (message.scope === "clear") {
801
1125
  await Promise.all(localLayers.map((layer) => layer.clear()));
802
1126
  await this.tagIndex.clear();
1127
+ this.accessProfiles.clear();
803
1128
  return;
804
1129
  }
805
1130
  const keys = message.keys ?? [];
806
- await Promise.all(
807
- localLayers.map(async (layer) => {
808
- if (layer.deleteMany) {
809
- await layer.deleteMany(keys);
810
- return;
811
- }
812
- await Promise.all(keys.map((key) => layer.delete(key)));
813
- })
814
- );
1131
+ await this.deleteKeysFromLayers(localLayers, keys);
815
1132
  if (message.operation !== "write") {
816
1133
  for (const key of keys) {
817
1134
  await this.tagIndex.remove(key);
1135
+ this.accessProfiles.delete(key);
818
1136
  }
819
1137
  }
820
1138
  }
@@ -827,6 +1145,257 @@ var CacheStack = class {
827
1145
  sleep(ms) {
828
1146
  return new Promise((resolve) => setTimeout(resolve, ms));
829
1147
  }
1148
+ shouldBroadcastL1Invalidation() {
1149
+ return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1150
+ }
1151
+ async deleteKeysFromLayers(layers, keys) {
1152
+ await Promise.all(
1153
+ layers.map(async (layer) => {
1154
+ if (this.shouldSkipLayer(layer)) {
1155
+ return;
1156
+ }
1157
+ if (layer.deleteMany) {
1158
+ try {
1159
+ await layer.deleteMany(keys);
1160
+ } catch (error) {
1161
+ await this.handleLayerFailure(layer, "delete", error);
1162
+ }
1163
+ return;
1164
+ }
1165
+ await Promise.all(keys.map(async (key) => {
1166
+ try {
1167
+ await layer.delete(key);
1168
+ } catch (error) {
1169
+ await this.handleLayerFailure(layer, "delete", error);
1170
+ }
1171
+ }));
1172
+ })
1173
+ );
1174
+ }
1175
+ validateConfiguration() {
1176
+ if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
1177
+ throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
1178
+ }
1179
+ if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
1180
+ throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
1181
+ }
1182
+ this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
1183
+ this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
1184
+ this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
1185
+ this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
1186
+ this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
1187
+ this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1188
+ this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1189
+ this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1190
+ this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1191
+ this.validateCircuitBreakerOptions(this.options.circuitBreaker);
1192
+ }
1193
+ validateWriteOptions(options) {
1194
+ if (!options) {
1195
+ return;
1196
+ }
1197
+ this.validateLayerNumberOption("options.ttl", options.ttl);
1198
+ this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
1199
+ this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
1200
+ this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1201
+ this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1202
+ this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
1203
+ this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1204
+ this.validateCircuitBreakerOptions(options.circuitBreaker);
1205
+ }
1206
+ validateLayerNumberOption(name, value) {
1207
+ if (value === void 0) {
1208
+ return;
1209
+ }
1210
+ if (typeof value === "number") {
1211
+ this.validateNonNegativeNumber(name, value);
1212
+ return;
1213
+ }
1214
+ for (const [layerName, layerValue] of Object.entries(value)) {
1215
+ if (layerValue === void 0) {
1216
+ continue;
1217
+ }
1218
+ this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
1219
+ }
1220
+ }
1221
+ validatePositiveNumber(name, value) {
1222
+ if (value === void 0) {
1223
+ return;
1224
+ }
1225
+ if (!Number.isFinite(value) || value <= 0) {
1226
+ throw new Error(`${name} must be a positive finite number.`);
1227
+ }
1228
+ }
1229
+ validateNonNegativeNumber(name, value) {
1230
+ if (!Number.isFinite(value) || value < 0) {
1231
+ throw new Error(`${name} must be a non-negative finite number.`);
1232
+ }
1233
+ }
1234
+ validateCacheKey(key) {
1235
+ if (key.length === 0) {
1236
+ throw new Error("Cache key must not be empty.");
1237
+ }
1238
+ if (key.length > MAX_CACHE_KEY_LENGTH) {
1239
+ throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1240
+ }
1241
+ if (/[\u0000-\u001F\u007F]/.test(key)) {
1242
+ throw new Error("Cache key contains unsupported control characters.");
1243
+ }
1244
+ return key;
1245
+ }
1246
+ serializeOptions(options) {
1247
+ return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1248
+ }
1249
+ validateAdaptiveTtlOptions(options) {
1250
+ if (!options || options === true) {
1251
+ return;
1252
+ }
1253
+ this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
1254
+ this.validateLayerNumberOption("adaptiveTtl.step", options.step);
1255
+ this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
1256
+ }
1257
+ validateCircuitBreakerOptions(options) {
1258
+ if (!options) {
1259
+ return;
1260
+ }
1261
+ this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1262
+ this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1263
+ }
1264
+ async applyFreshReadPolicies(key, hit, options, fetcher) {
1265
+ const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
1266
+ const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
1267
+ if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
1268
+ const refreshed = refreshStoredEnvelope(hit.stored);
1269
+ const ttl = remainingStoredTtlSeconds(refreshed);
1270
+ for (let index = 0; index <= hit.layerIndex; index += 1) {
1271
+ const layer = this.layers[index];
1272
+ if (this.shouldSkipLayer(layer)) {
1273
+ continue;
1274
+ }
1275
+ try {
1276
+ await layer.set(key, refreshed, ttl);
1277
+ } catch (error) {
1278
+ await this.handleLayerFailure(layer, "sliding-ttl", error);
1279
+ }
1280
+ }
1281
+ }
1282
+ if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
1283
+ this.scheduleBackgroundRefresh(key, fetcher, options);
1284
+ }
1285
+ }
1286
+ applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
1287
+ if (!ttl || !adaptiveTtl) {
1288
+ return ttl;
1289
+ }
1290
+ const profile = this.accessProfiles.get(key);
1291
+ if (!profile) {
1292
+ return ttl;
1293
+ }
1294
+ const config = adaptiveTtl === true ? {} : adaptiveTtl;
1295
+ const hotAfter = config.hotAfter ?? 3;
1296
+ if (profile.hits < hotAfter) {
1297
+ return ttl;
1298
+ }
1299
+ const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
1300
+ const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
1301
+ const multiplier = Math.floor(profile.hits / hotAfter);
1302
+ return Math.min(maxTtl, ttl + step * multiplier);
1303
+ }
1304
+ recordAccess(key) {
1305
+ const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
1306
+ profile.hits += 1;
1307
+ profile.lastAccessAt = Date.now();
1308
+ this.accessProfiles.set(key, profile);
1309
+ }
1310
+ incrementMetricMap(target, key) {
1311
+ target[key] = (target[key] ?? 0) + 1;
1312
+ }
1313
+ shouldSkipLayer(layer) {
1314
+ const degradedUntil = this.layerDegradedUntil.get(layer.name);
1315
+ return degradedUntil !== void 0 && degradedUntil > Date.now();
1316
+ }
1317
+ async handleLayerFailure(layer, operation, error) {
1318
+ if (!this.isGracefulDegradationEnabled()) {
1319
+ throw error;
1320
+ }
1321
+ const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1322
+ this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
1323
+ this.metrics.degradedOperations += 1;
1324
+ this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
1325
+ this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
1326
+ return null;
1327
+ }
1328
+ isGracefulDegradationEnabled() {
1329
+ return Boolean(this.options.gracefulDegradation);
1330
+ }
1331
+ assertCircuitClosed(key, options) {
1332
+ const state = this.circuitBreakers.get(key);
1333
+ if (!state?.openUntil) {
1334
+ return;
1335
+ }
1336
+ if (state.openUntil <= Date.now()) {
1337
+ state.openUntil = null;
1338
+ state.failures = 0;
1339
+ this.circuitBreakers.set(key, state);
1340
+ return;
1341
+ }
1342
+ this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
1343
+ throw new Error(`Circuit breaker is open for key "${key}".`);
1344
+ }
1345
+ recordCircuitFailure(key, options, error) {
1346
+ if (!options) {
1347
+ return;
1348
+ }
1349
+ const failureThreshold = options.failureThreshold ?? 3;
1350
+ const cooldownMs = options.cooldownMs ?? 3e4;
1351
+ const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
1352
+ state.failures += 1;
1353
+ if (state.failures >= failureThreshold) {
1354
+ state.openUntil = Date.now() + cooldownMs;
1355
+ this.metrics.circuitBreakerTrips += 1;
1356
+ }
1357
+ this.circuitBreakers.set(key, state);
1358
+ this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
1359
+ }
1360
+ resetCircuitBreaker(key) {
1361
+ this.circuitBreakers.delete(key);
1362
+ }
1363
+ isNegativeStoredValue(stored) {
1364
+ return isStoredValueEnvelope(stored) && stored.kind === "empty";
1365
+ }
1366
+ emitError(operation, context) {
1367
+ this.logger.error?.(operation, context);
1368
+ if (this.listenerCount("error") > 0) {
1369
+ this.emit("error", { operation, ...context });
1370
+ }
1371
+ }
1372
+ serializeKeyPart(value) {
1373
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1374
+ return String(value);
1375
+ }
1376
+ return JSON.stringify(this.normalizeForSerialization(value));
1377
+ }
1378
+ isCacheSnapshotEntries(value) {
1379
+ return Array.isArray(value) && value.every((entry) => {
1380
+ if (!entry || typeof entry !== "object") {
1381
+ return false;
1382
+ }
1383
+ const candidate = entry;
1384
+ return typeof candidate.key === "string";
1385
+ });
1386
+ }
1387
+ normalizeForSerialization(value) {
1388
+ if (Array.isArray(value)) {
1389
+ return value.map((entry) => this.normalizeForSerialization(entry));
1390
+ }
1391
+ if (value && typeof value === "object") {
1392
+ return Object.keys(value).sort().reduce((normalized, key) => {
1393
+ normalized[key] = this.normalizeForSerialization(value[key]);
1394
+ return normalized;
1395
+ }, {});
1396
+ }
1397
+ return value;
1398
+ }
830
1399
  };
831
1400
 
832
1401
  // src/module.ts
@@ -852,5 +1421,6 @@ CacheStackModule = __decorateClass([
852
1421
  export {
853
1422
  CACHE_STACK,
854
1423
  CacheStackModule,
1424
+ Cacheable,
855
1425
  InjectCacheStack
856
1426
  };