layercache 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,63 +46,260 @@ function Cacheable(options) {
46
46
  import { Global, Inject, Module } from "@nestjs/common";
47
47
 
48
48
  // ../../src/CacheStack.ts
49
- import { randomUUID } from "crypto";
50
49
  import { EventEmitter } from "events";
51
- import { promises as fs } from "fs";
50
+
51
+ // ../../node_modules/async-mutex/index.mjs
52
+ var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
53
+ var E_ALREADY_LOCKED = new Error("mutex already locked");
54
+ var E_CANCELED = new Error("request for lock canceled");
55
+ var __awaiter$2 = function(thisArg, _arguments, P, generator) {
56
+ function adopt(value) {
57
+ return value instanceof P ? value : new P(function(resolve) {
58
+ resolve(value);
59
+ });
60
+ }
61
+ return new (P || (P = Promise))(function(resolve, reject) {
62
+ function fulfilled(value) {
63
+ try {
64
+ step(generator.next(value));
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ }
69
+ function rejected(value) {
70
+ try {
71
+ step(generator["throw"](value));
72
+ } catch (e) {
73
+ reject(e);
74
+ }
75
+ }
76
+ function step(result) {
77
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
78
+ }
79
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
80
+ });
81
+ };
82
+ var Semaphore = class {
83
+ constructor(_value, _cancelError = E_CANCELED) {
84
+ this._value = _value;
85
+ this._cancelError = _cancelError;
86
+ this._weightedQueues = [];
87
+ this._weightedWaiters = [];
88
+ }
89
+ acquire(weight = 1) {
90
+ if (weight <= 0)
91
+ throw new Error(`invalid weight ${weight}: must be positive`);
92
+ return new Promise((resolve, reject) => {
93
+ if (!this._weightedQueues[weight - 1])
94
+ this._weightedQueues[weight - 1] = [];
95
+ this._weightedQueues[weight - 1].push({ resolve, reject });
96
+ this._dispatch();
97
+ });
98
+ }
99
+ runExclusive(callback, weight = 1) {
100
+ return __awaiter$2(this, void 0, void 0, function* () {
101
+ const [value, release] = yield this.acquire(weight);
102
+ try {
103
+ return yield callback(value);
104
+ } finally {
105
+ release();
106
+ }
107
+ });
108
+ }
109
+ waitForUnlock(weight = 1) {
110
+ if (weight <= 0)
111
+ throw new Error(`invalid weight ${weight}: must be positive`);
112
+ return new Promise((resolve) => {
113
+ if (!this._weightedWaiters[weight - 1])
114
+ this._weightedWaiters[weight - 1] = [];
115
+ this._weightedWaiters[weight - 1].push(resolve);
116
+ this._dispatch();
117
+ });
118
+ }
119
+ isLocked() {
120
+ return this._value <= 0;
121
+ }
122
+ getValue() {
123
+ return this._value;
124
+ }
125
+ setValue(value) {
126
+ this._value = value;
127
+ this._dispatch();
128
+ }
129
+ release(weight = 1) {
130
+ if (weight <= 0)
131
+ throw new Error(`invalid weight ${weight}: must be positive`);
132
+ this._value += weight;
133
+ this._dispatch();
134
+ }
135
+ cancel() {
136
+ this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
137
+ this._weightedQueues = [];
138
+ }
139
+ _dispatch() {
140
+ var _a;
141
+ for (let weight = this._value; weight > 0; weight--) {
142
+ const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
143
+ if (!queueEntry)
144
+ continue;
145
+ const previousValue = this._value;
146
+ const previousWeight = weight;
147
+ this._value -= weight;
148
+ weight = this._value + 1;
149
+ queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
150
+ }
151
+ this._drainUnlockWaiters();
152
+ }
153
+ _newReleaser(weight) {
154
+ let called = false;
155
+ return () => {
156
+ if (called)
157
+ return;
158
+ called = true;
159
+ this.release(weight);
160
+ };
161
+ }
162
+ _drainUnlockWaiters() {
163
+ for (let weight = this._value; weight > 0; weight--) {
164
+ if (!this._weightedWaiters[weight - 1])
165
+ continue;
166
+ this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
167
+ this._weightedWaiters[weight - 1] = [];
168
+ }
169
+ }
170
+ };
171
+ var __awaiter$1 = function(thisArg, _arguments, P, generator) {
172
+ function adopt(value) {
173
+ return value instanceof P ? value : new P(function(resolve) {
174
+ resolve(value);
175
+ });
176
+ }
177
+ return new (P || (P = Promise))(function(resolve, reject) {
178
+ function fulfilled(value) {
179
+ try {
180
+ step(generator.next(value));
181
+ } catch (e) {
182
+ reject(e);
183
+ }
184
+ }
185
+ function rejected(value) {
186
+ try {
187
+ step(generator["throw"](value));
188
+ } catch (e) {
189
+ reject(e);
190
+ }
191
+ }
192
+ function step(result) {
193
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
194
+ }
195
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
196
+ });
197
+ };
198
+ var Mutex = class {
199
+ constructor(cancelError) {
200
+ this._semaphore = new Semaphore(1, cancelError);
201
+ }
202
+ acquire() {
203
+ return __awaiter$1(this, void 0, void 0, function* () {
204
+ const [, releaser] = yield this._semaphore.acquire();
205
+ return releaser;
206
+ });
207
+ }
208
+ runExclusive(callback) {
209
+ return this._semaphore.runExclusive(() => callback());
210
+ }
211
+ isLocked() {
212
+ return this._semaphore.isLocked();
213
+ }
214
+ waitForUnlock() {
215
+ return this._semaphore.waitForUnlock();
216
+ }
217
+ release() {
218
+ if (this._semaphore.isLocked())
219
+ this._semaphore.release();
220
+ }
221
+ cancel() {
222
+ return this._semaphore.cancel();
223
+ }
224
+ };
52
225
 
53
226
  // ../../src/CacheNamespace.ts
54
- var CacheNamespace = class {
227
+ var CacheNamespace = class _CacheNamespace {
55
228
  constructor(cache, prefix) {
56
229
  this.cache = cache;
57
230
  this.prefix = prefix;
58
231
  }
59
232
  cache;
60
233
  prefix;
234
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
235
+ metrics = emptyMetrics();
61
236
  async get(key, fetcher, options) {
62
- return this.cache.get(this.qualify(key), fetcher, options);
237
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
63
238
  }
64
239
  async getOrSet(key, fetcher, options) {
65
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
240
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
241
+ }
242
+ /**
243
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
244
+ */
245
+ async getOrThrow(key, fetcher, options) {
246
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
66
247
  }
67
248
  async has(key) {
68
- return this.cache.has(this.qualify(key));
249
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
69
250
  }
70
251
  async ttl(key) {
71
- return this.cache.ttl(this.qualify(key));
252
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
72
253
  }
73
254
  async set(key, value, options) {
74
- await this.cache.set(this.qualify(key), value, options);
255
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
75
256
  }
76
257
  async delete(key) {
77
- await this.cache.delete(this.qualify(key));
258
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
78
259
  }
79
260
  async mdelete(keys) {
80
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
261
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
81
262
  }
82
263
  async clear() {
83
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
264
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
84
265
  }
85
266
  async mget(entries) {
86
- return this.cache.mget(
87
- entries.map((entry) => ({
88
- ...entry,
89
- key: this.qualify(entry.key)
90
- }))
267
+ return this.trackMetrics(
268
+ () => this.cache.mget(
269
+ entries.map((entry) => ({
270
+ ...entry,
271
+ key: this.qualify(entry.key)
272
+ }))
273
+ )
91
274
  );
92
275
  }
93
276
  async mset(entries) {
94
- await this.cache.mset(
95
- entries.map((entry) => ({
96
- ...entry,
97
- key: this.qualify(entry.key)
98
- }))
277
+ await this.trackMetrics(
278
+ () => this.cache.mset(
279
+ entries.map((entry) => ({
280
+ ...entry,
281
+ key: this.qualify(entry.key)
282
+ }))
283
+ )
99
284
  );
100
285
  }
101
286
  async invalidateByTag(tag) {
102
- await this.cache.invalidateByTag(tag);
287
+ await this.trackMetrics(() => this.cache.invalidateByTag(tag));
288
+ }
289
+ async invalidateByTags(tags, mode = "any") {
290
+ await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
103
291
  }
104
292
  async invalidateByPattern(pattern) {
105
- await this.cache.invalidateByPattern(this.qualify(pattern));
293
+ await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
294
+ }
295
+ async invalidateByPrefix(prefix) {
296
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
297
+ }
298
+ /**
299
+ * Returns detailed metadata about a single cache key within this namespace.
300
+ */
301
+ async inspect(key) {
302
+ return this.cache.inspect(this.qualify(key));
106
303
  }
107
304
  wrap(keyPrefix, fetcher, options) {
108
305
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
@@ -117,15 +314,159 @@ var CacheNamespace = class {
117
314
  );
118
315
  }
119
316
  getMetrics() {
120
- return this.cache.getMetrics();
317
+ return cloneMetrics(this.metrics);
121
318
  }
122
319
  getHitRate() {
123
- return this.cache.getHitRate();
320
+ const total = this.metrics.hits + this.metrics.misses;
321
+ const overall = total === 0 ? 0 : this.metrics.hits / total;
322
+ const byLayer = {};
323
+ const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
324
+ for (const layer of layers) {
325
+ const hits = this.metrics.hitsByLayer[layer] ?? 0;
326
+ const misses = this.metrics.missesByLayer[layer] ?? 0;
327
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
328
+ }
329
+ return { overall, byLayer };
330
+ }
331
+ /**
332
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
333
+ *
334
+ * ```ts
335
+ * const tenant = cache.namespace('tenant:abc')
336
+ * const posts = tenant.namespace('posts')
337
+ * // keys become: "tenant:abc:posts:mykey"
338
+ * ```
339
+ */
340
+ namespace(childPrefix) {
341
+ return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
124
342
  }
125
343
  qualify(key) {
126
344
  return `${this.prefix}:${key}`;
127
345
  }
346
+ async trackMetrics(operation) {
347
+ return this.getMetricsMutex().runExclusive(async () => {
348
+ const before = this.cache.getMetrics();
349
+ const result = await operation();
350
+ const after = this.cache.getMetrics();
351
+ this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
352
+ return result;
353
+ });
354
+ }
355
+ getMetricsMutex() {
356
+ const existing = _CacheNamespace.metricsMutexes.get(this.cache);
357
+ if (existing) {
358
+ return existing;
359
+ }
360
+ const mutex = new Mutex();
361
+ _CacheNamespace.metricsMutexes.set(this.cache, mutex);
362
+ return mutex;
363
+ }
128
364
  };
365
+ function emptyMetrics() {
366
+ return {
367
+ hits: 0,
368
+ misses: 0,
369
+ fetches: 0,
370
+ sets: 0,
371
+ deletes: 0,
372
+ backfills: 0,
373
+ invalidations: 0,
374
+ staleHits: 0,
375
+ refreshes: 0,
376
+ refreshErrors: 0,
377
+ writeFailures: 0,
378
+ singleFlightWaits: 0,
379
+ negativeCacheHits: 0,
380
+ circuitBreakerTrips: 0,
381
+ degradedOperations: 0,
382
+ hitsByLayer: {},
383
+ missesByLayer: {},
384
+ latencyByLayer: {},
385
+ resetAt: Date.now()
386
+ };
387
+ }
388
+ function cloneMetrics(metrics) {
389
+ return {
390
+ ...metrics,
391
+ hitsByLayer: { ...metrics.hitsByLayer },
392
+ missesByLayer: { ...metrics.missesByLayer },
393
+ latencyByLayer: Object.fromEntries(
394
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
395
+ )
396
+ };
397
+ }
398
+ function diffMetrics(before, after) {
399
+ const latencyByLayer = Object.fromEntries(
400
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
401
+ layer,
402
+ {
403
+ avgMs: value.avgMs,
404
+ maxMs: value.maxMs,
405
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
406
+ }
407
+ ])
408
+ );
409
+ return {
410
+ hits: after.hits - before.hits,
411
+ misses: after.misses - before.misses,
412
+ fetches: after.fetches - before.fetches,
413
+ sets: after.sets - before.sets,
414
+ deletes: after.deletes - before.deletes,
415
+ backfills: after.backfills - before.backfills,
416
+ invalidations: after.invalidations - before.invalidations,
417
+ staleHits: after.staleHits - before.staleHits,
418
+ refreshes: after.refreshes - before.refreshes,
419
+ refreshErrors: after.refreshErrors - before.refreshErrors,
420
+ writeFailures: after.writeFailures - before.writeFailures,
421
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
422
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
423
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
424
+ degradedOperations: after.degradedOperations - before.degradedOperations,
425
+ hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
426
+ missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
427
+ latencyByLayer,
428
+ resetAt: after.resetAt
429
+ };
430
+ }
431
+ function addMetrics(base, delta) {
432
+ return {
433
+ hits: base.hits + delta.hits,
434
+ misses: base.misses + delta.misses,
435
+ fetches: base.fetches + delta.fetches,
436
+ sets: base.sets + delta.sets,
437
+ deletes: base.deletes + delta.deletes,
438
+ backfills: base.backfills + delta.backfills,
439
+ invalidations: base.invalidations + delta.invalidations,
440
+ staleHits: base.staleHits + delta.staleHits,
441
+ refreshes: base.refreshes + delta.refreshes,
442
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
443
+ writeFailures: base.writeFailures + delta.writeFailures,
444
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
445
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
446
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
447
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
448
+ hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
449
+ missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
450
+ latencyByLayer: cloneMetrics(delta).latencyByLayer,
451
+ resetAt: base.resetAt
452
+ };
453
+ }
454
+ function diffMap(before, after) {
455
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
456
+ const result = {};
457
+ for (const key of keys) {
458
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
459
+ }
460
+ return result;
461
+ }
462
+ function addMap(base, delta) {
463
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
464
+ const result = {};
465
+ for (const key of keys) {
466
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
467
+ }
468
+ return result;
469
+ }
129
470
 
130
471
  // ../../src/internal/CircuitBreakerManager.ts
131
472
  var CircuitBreakerManager = class {
@@ -219,11 +560,105 @@ var CircuitBreakerManager = class {
219
560
  }
220
561
  };
221
562
 
563
+ // ../../src/internal/FetchRateLimiter.ts
564
+ var FetchRateLimiter = class {
565
+ active = 0;
566
+ queue = [];
567
+ startedAt = [];
568
+ drainTimer;
569
+ async schedule(options, task) {
570
+ if (!options) {
571
+ return task();
572
+ }
573
+ const normalized = this.normalize(options);
574
+ if (!normalized) {
575
+ return task();
576
+ }
577
+ return new Promise((resolve, reject) => {
578
+ this.queue.push({ options: normalized, task, resolve, reject });
579
+ this.drain();
580
+ });
581
+ }
582
+ normalize(options) {
583
+ const maxConcurrent = options.maxConcurrent;
584
+ const intervalMs = options.intervalMs;
585
+ const maxPerInterval = options.maxPerInterval;
586
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
587
+ return void 0;
588
+ }
589
+ return {
590
+ maxConcurrent,
591
+ intervalMs,
592
+ maxPerInterval
593
+ };
594
+ }
595
+ drain() {
596
+ if (this.drainTimer) {
597
+ clearTimeout(this.drainTimer);
598
+ this.drainTimer = void 0;
599
+ }
600
+ while (this.queue.length > 0) {
601
+ const next = this.queue[0];
602
+ if (!next) {
603
+ return;
604
+ }
605
+ const waitMs = this.waitTime(next.options);
606
+ if (waitMs > 0) {
607
+ this.drainTimer = setTimeout(() => {
608
+ this.drainTimer = void 0;
609
+ this.drain();
610
+ }, waitMs);
611
+ this.drainTimer.unref?.();
612
+ return;
613
+ }
614
+ this.queue.shift();
615
+ this.active += 1;
616
+ this.startedAt.push(Date.now());
617
+ void next.task().then(next.resolve, next.reject).finally(() => {
618
+ this.active -= 1;
619
+ this.drain();
620
+ });
621
+ }
622
+ }
623
+ waitTime(options) {
624
+ const now = Date.now();
625
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
626
+ return 1;
627
+ }
628
+ if (!options.intervalMs || !options.maxPerInterval) {
629
+ return 0;
630
+ }
631
+ this.prune(now, options.intervalMs);
632
+ if (this.startedAt.length < options.maxPerInterval) {
633
+ return 0;
634
+ }
635
+ const oldest = this.startedAt[0];
636
+ if (!oldest) {
637
+ return 0;
638
+ }
639
+ return Math.max(1, options.intervalMs - (now - oldest));
640
+ }
641
+ prune(now, intervalMs) {
642
+ while (this.startedAt.length > 0) {
643
+ const startedAt = this.startedAt[0];
644
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
645
+ break;
646
+ }
647
+ this.startedAt.shift();
648
+ }
649
+ }
650
+ };
651
+
222
652
  // ../../src/internal/MetricsCollector.ts
223
653
  var MetricsCollector = class {
224
654
  data = this.empty();
225
655
  get snapshot() {
226
- return { ...this.data };
656
+ return {
657
+ ...this.data,
658
+ hitsByLayer: { ...this.data.hitsByLayer },
659
+ missesByLayer: { ...this.data.missesByLayer },
660
+ latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
661
+ };
227
662
  }
228
663
  increment(field, amount = 1) {
229
664
  ;
@@ -232,6 +667,22 @@ var MetricsCollector = class {
232
667
  incrementLayer(map, layerName) {
233
668
  this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
234
669
  }
670
+ /**
671
+ * Records a read latency sample for the given layer.
672
+ * Maintains a rolling average and max using Welford's online algorithm.
673
+ */
674
+ recordLatency(layerName, durationMs) {
675
+ const existing = this.data.latencyByLayer[layerName];
676
+ if (!existing) {
677
+ this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
678
+ return;
679
+ }
680
+ existing.count += 1;
681
+ existing.avgMs += (durationMs - existing.avgMs) / existing.count;
682
+ if (durationMs > existing.maxMs) {
683
+ existing.maxMs = durationMs;
684
+ }
685
+ }
235
686
  reset() {
236
687
  this.data = this.empty();
237
688
  }
@@ -266,6 +717,7 @@ var MetricsCollector = class {
266
717
  degradedOperations: 0,
267
718
  hitsByLayer: {},
268
719
  missesByLayer: {},
720
+ latencyByLayer: {},
269
721
  resetAt: Date.now()
270
722
  };
271
723
  }
@@ -393,13 +845,14 @@ var TtlResolver = class {
393
845
  clearProfiles() {
394
846
  this.accessProfiles.clear();
395
847
  }
396
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
848
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
849
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
397
850
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
398
851
  layerName,
399
852
  options?.negativeTtl,
400
853
  globalNegativeTtl,
401
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
402
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
854
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
855
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
403
856
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
404
857
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
405
858
  return this.applyJitter(adaptiveTtl, jitter);
@@ -438,6 +891,29 @@ var TtlResolver = class {
438
891
  const delta = (Math.random() * 2 - 1) * jitter;
439
892
  return Math.max(1, Math.round(ttl + delta));
440
893
  }
894
+ resolvePolicyTtl(key, value, policy) {
895
+ if (!policy) {
896
+ return void 0;
897
+ }
898
+ if (typeof policy === "function") {
899
+ return policy({ key, value });
900
+ }
901
+ const now = /* @__PURE__ */ new Date();
902
+ if (policy === "until-midnight") {
903
+ const nextMidnight = new Date(now);
904
+ nextMidnight.setHours(24, 0, 0, 0);
905
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
906
+ }
907
+ if (policy === "next-hour") {
908
+ const nextHour = new Date(now);
909
+ nextHour.setMinutes(60, 0, 0);
910
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
911
+ }
912
+ const alignToSeconds = policy.alignTo;
913
+ const currentSeconds = Math.floor(Date.now() / 1e3);
914
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
915
+ return Math.max(1, nextBoundary - currentSeconds);
916
+ }
441
917
  readLayerNumber(layerName, value) {
442
918
  if (typeof value === "number") {
443
919
  return value;
@@ -465,269 +941,133 @@ var PatternMatcher = class _PatternMatcher {
465
941
  /**
466
942
  * Tests whether a glob-style pattern matches a value.
467
943
  * Supports `*` (any sequence of characters) and `?` (any single character).
468
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
944
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
945
+ * quadratic memory usage on long patterns/keys.
469
946
  */
470
947
  static matches(pattern, value) {
471
948
  return _PatternMatcher.matchLinear(pattern, value);
472
949
  }
473
950
  /**
474
- * Linear-time glob matching using dynamic programming.
475
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
951
+ * Linear-time glob matching with O(1) extra memory.
476
952
  */
477
953
  static matchLinear(pattern, value) {
478
- const m = pattern.length;
479
- const n = value.length;
480
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
481
- dp[0][0] = true;
482
- for (let i = 1; i <= m; i++) {
483
- if (pattern[i - 1] === "*") {
484
- dp[i][0] = dp[i - 1]?.[0];
485
- }
486
- }
487
- for (let i = 1; i <= m; i++) {
488
- for (let j = 1; j <= n; j++) {
489
- const pc = pattern[i - 1];
490
- if (pc === "*") {
491
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
492
- } else if (pc === "?" || pc === value[j - 1]) {
493
- dp[i][j] = dp[i - 1]?.[j - 1];
494
- }
495
- }
496
- }
497
- return dp[m]?.[n];
498
- }
499
- };
500
-
501
- // ../../src/invalidation/TagIndex.ts
502
- var TagIndex = class {
503
- tagToKeys = /* @__PURE__ */ new Map();
504
- keyToTags = /* @__PURE__ */ new Map();
505
- knownKeys = /* @__PURE__ */ new Set();
506
- async touch(key) {
507
- this.knownKeys.add(key);
508
- }
509
- async track(key, tags) {
510
- this.knownKeys.add(key);
511
- if (tags.length === 0) {
512
- return;
513
- }
514
- const existingTags = this.keyToTags.get(key);
515
- if (existingTags) {
516
- for (const tag of existingTags) {
517
- this.tagToKeys.get(tag)?.delete(key);
518
- }
519
- }
520
- const tagSet = new Set(tags);
521
- this.keyToTags.set(key, tagSet);
522
- for (const tag of tagSet) {
523
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
524
- keys.add(key);
525
- this.tagToKeys.set(tag, keys);
526
- }
527
- }
528
- async remove(key) {
529
- this.knownKeys.delete(key);
530
- const tags = this.keyToTags.get(key);
531
- if (!tags) {
532
- return;
533
- }
534
- for (const tag of tags) {
535
- const keys = this.tagToKeys.get(tag);
536
- if (!keys) {
954
+ let patternIndex = 0;
955
+ let valueIndex = 0;
956
+ let starIndex = -1;
957
+ let backtrackValueIndex = 0;
958
+ while (valueIndex < value.length) {
959
+ const patternChar = pattern[patternIndex];
960
+ const valueChar = value[valueIndex];
961
+ if (patternChar === "*" && patternIndex < pattern.length) {
962
+ starIndex = patternIndex;
963
+ patternIndex += 1;
964
+ backtrackValueIndex = valueIndex;
537
965
  continue;
538
966
  }
539
- keys.delete(key);
540
- if (keys.size === 0) {
541
- this.tagToKeys.delete(tag);
542
- }
543
- }
544
- this.keyToTags.delete(key);
545
- }
546
- async keysForTag(tag) {
547
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
548
- }
549
- async matchPattern(pattern) {
550
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
551
- }
552
- async clear() {
553
- this.tagToKeys.clear();
554
- this.keyToTags.clear();
555
- this.knownKeys.clear();
556
- }
557
- };
558
-
559
- // ../../node_modules/async-mutex/index.mjs
560
- var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
561
- var E_ALREADY_LOCKED = new Error("mutex already locked");
562
- var E_CANCELED = new Error("request for lock canceled");
563
- var __awaiter$2 = function(thisArg, _arguments, P, generator) {
564
- function adopt(value) {
565
- return value instanceof P ? value : new P(function(resolve) {
566
- resolve(value);
567
- });
568
- }
569
- return new (P || (P = Promise))(function(resolve, reject) {
570
- function fulfilled(value) {
571
- try {
572
- step(generator.next(value));
573
- } catch (e) {
574
- reject(e);
575
- }
576
- }
577
- function rejected(value) {
578
- try {
579
- step(generator["throw"](value));
580
- } catch (e) {
581
- reject(e);
582
- }
583
- }
584
- function step(result) {
585
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
586
- }
587
- step((generator = generator.apply(thisArg, _arguments || [])).next());
588
- });
589
- };
590
- var Semaphore = class {
591
- constructor(_value, _cancelError = E_CANCELED) {
592
- this._value = _value;
593
- this._cancelError = _cancelError;
594
- this._weightedQueues = [];
595
- this._weightedWaiters = [];
596
- }
597
- acquire(weight = 1) {
598
- if (weight <= 0)
599
- throw new Error(`invalid weight ${weight}: must be positive`);
600
- return new Promise((resolve, reject) => {
601
- if (!this._weightedQueues[weight - 1])
602
- this._weightedQueues[weight - 1] = [];
603
- this._weightedQueues[weight - 1].push({ resolve, reject });
604
- this._dispatch();
605
- });
606
- }
607
- runExclusive(callback, weight = 1) {
608
- return __awaiter$2(this, void 0, void 0, function* () {
609
- const [value, release] = yield this.acquire(weight);
610
- try {
611
- return yield callback(value);
612
- } finally {
613
- release();
614
- }
615
- });
616
- }
617
- waitForUnlock(weight = 1) {
618
- if (weight <= 0)
619
- throw new Error(`invalid weight ${weight}: must be positive`);
620
- return new Promise((resolve) => {
621
- if (!this._weightedWaiters[weight - 1])
622
- this._weightedWaiters[weight - 1] = [];
623
- this._weightedWaiters[weight - 1].push(resolve);
624
- this._dispatch();
625
- });
626
- }
627
- isLocked() {
628
- return this._value <= 0;
629
- }
630
- getValue() {
631
- return this._value;
632
- }
633
- setValue(value) {
634
- this._value = value;
635
- this._dispatch();
636
- }
637
- release(weight = 1) {
638
- if (weight <= 0)
639
- throw new Error(`invalid weight ${weight}: must be positive`);
640
- this._value += weight;
641
- this._dispatch();
642
- }
643
- cancel() {
644
- this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
645
- this._weightedQueues = [];
646
- }
647
- _dispatch() {
648
- var _a;
649
- for (let weight = this._value; weight > 0; weight--) {
650
- const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
651
- if (!queueEntry)
967
+ if (patternChar === "?" || patternChar === valueChar) {
968
+ patternIndex += 1;
969
+ valueIndex += 1;
652
970
  continue;
653
- const previousValue = this._value;
654
- const previousWeight = weight;
655
- this._value -= weight;
656
- weight = this._value + 1;
657
- queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
658
- }
659
- this._drainUnlockWaiters();
660
- }
661
- _newReleaser(weight) {
662
- let called = false;
663
- return () => {
664
- if (called)
665
- return;
666
- called = true;
667
- this.release(weight);
668
- };
669
- }
670
- _drainUnlockWaiters() {
671
- for (let weight = this._value; weight > 0; weight--) {
672
- if (!this._weightedWaiters[weight - 1])
971
+ }
972
+ if (starIndex !== -1) {
973
+ patternIndex = starIndex + 1;
974
+ backtrackValueIndex += 1;
975
+ valueIndex = backtrackValueIndex;
673
976
  continue;
674
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
675
- this._weightedWaiters[weight - 1] = [];
977
+ }
978
+ return false;
676
979
  }
980
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
981
+ patternIndex += 1;
982
+ }
983
+ return patternIndex === pattern.length;
677
984
  }
678
985
  };
679
- var __awaiter$1 = function(thisArg, _arguments, P, generator) {
680
- function adopt(value) {
681
- return value instanceof P ? value : new P(function(resolve) {
682
- resolve(value);
683
- });
986
+
987
+ // ../../src/invalidation/TagIndex.ts
988
+ var TagIndex = class {
989
+ tagToKeys = /* @__PURE__ */ new Map();
990
+ keyToTags = /* @__PURE__ */ new Map();
991
+ knownKeys = /* @__PURE__ */ new Set();
992
+ maxKnownKeys;
993
+ constructor(options = {}) {
994
+ this.maxKnownKeys = options.maxKnownKeys;
684
995
  }
685
- return new (P || (P = Promise))(function(resolve, reject) {
686
- function fulfilled(value) {
687
- try {
688
- step(generator.next(value));
689
- } catch (e) {
690
- reject(e);
691
- }
996
+ async touch(key) {
997
+ this.knownKeys.add(key);
998
+ this.pruneKnownKeysIfNeeded();
999
+ }
1000
+ async track(key, tags) {
1001
+ this.knownKeys.add(key);
1002
+ this.pruneKnownKeysIfNeeded();
1003
+ if (tags.length === 0) {
1004
+ return;
692
1005
  }
693
- function rejected(value) {
694
- try {
695
- step(generator["throw"](value));
696
- } catch (e) {
697
- reject(e);
1006
+ const existingTags = this.keyToTags.get(key);
1007
+ if (existingTags) {
1008
+ for (const tag of existingTags) {
1009
+ this.tagToKeys.get(tag)?.delete(key);
698
1010
  }
699
1011
  }
700
- function step(result) {
701
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1012
+ const tagSet = new Set(tags);
1013
+ this.keyToTags.set(key, tagSet);
1014
+ for (const tag of tagSet) {
1015
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1016
+ keys.add(key);
1017
+ this.tagToKeys.set(tag, keys);
702
1018
  }
703
- step((generator = generator.apply(thisArg, _arguments || [])).next());
704
- });
705
- };
706
- var Mutex = class {
707
- constructor(cancelError) {
708
- this._semaphore = new Semaphore(1, cancelError);
709
1019
  }
710
- acquire() {
711
- return __awaiter$1(this, void 0, void 0, function* () {
712
- const [, releaser] = yield this._semaphore.acquire();
713
- return releaser;
714
- });
1020
+ async remove(key) {
1021
+ this.removeKey(key);
715
1022
  }
716
- runExclusive(callback) {
717
- return this._semaphore.runExclusive(() => callback());
1023
+ async keysForTag(tag) {
1024
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
718
1025
  }
719
- isLocked() {
720
- return this._semaphore.isLocked();
1026
+ async keysForPrefix(prefix) {
1027
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
721
1028
  }
722
- waitForUnlock() {
723
- return this._semaphore.waitForUnlock();
1029
+ async tagsForKey(key) {
1030
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
724
1031
  }
725
- release() {
726
- if (this._semaphore.isLocked())
727
- this._semaphore.release();
1032
+ async matchPattern(pattern) {
1033
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
728
1034
  }
729
- cancel() {
730
- return this._semaphore.cancel();
1035
+ async clear() {
1036
+ this.tagToKeys.clear();
1037
+ this.keyToTags.clear();
1038
+ this.knownKeys.clear();
1039
+ }
1040
+ pruneKnownKeysIfNeeded() {
1041
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1042
+ return;
1043
+ }
1044
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1045
+ let removed = 0;
1046
+ for (const key of this.knownKeys) {
1047
+ if (removed >= toRemove) {
1048
+ break;
1049
+ }
1050
+ this.removeKey(key);
1051
+ removed += 1;
1052
+ }
1053
+ }
1054
+ removeKey(key) {
1055
+ this.knownKeys.delete(key);
1056
+ const tags = this.keyToTags.get(key);
1057
+ if (!tags) {
1058
+ return;
1059
+ }
1060
+ for (const tag of tags) {
1061
+ const keys = this.tagToKeys.get(tag);
1062
+ if (!keys) {
1063
+ continue;
1064
+ }
1065
+ keys.delete(key);
1066
+ if (keys.size === 0) {
1067
+ this.tagToKeys.delete(tag);
1068
+ }
1069
+ }
1070
+ this.keyToTags.delete(key);
731
1071
  }
732
1072
  };
733
1073
 
@@ -756,6 +1096,16 @@ var StampedeGuard = class {
756
1096
  }
757
1097
  };
758
1098
 
1099
+ // ../../src/types.ts
1100
+ var CacheMissError = class extends Error {
1101
+ key;
1102
+ constructor(key) {
1103
+ super(`Cache miss for key "${key}".`);
1104
+ this.name = "CacheMissError";
1105
+ this.key = key;
1106
+ }
1107
+ };
1108
+
759
1109
  // ../../src/CacheStack.ts
760
1110
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
761
1111
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
@@ -799,6 +1149,7 @@ var CacheStack = class extends EventEmitter {
799
1149
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
800
1150
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
801
1151
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1152
+ this.currentGeneration = options.generation;
802
1153
  if (options.publishSetInvalidation !== void 0) {
803
1154
  console.warn(
804
1155
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -807,21 +1158,27 @@ var CacheStack = class extends EventEmitter {
807
1158
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
808
1159
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
809
1160
  this.tagIndex = options.tagIndex ?? new TagIndex();
1161
+ this.initializeWriteBehind(options.writeBehind);
810
1162
  this.startup = this.initialize();
811
1163
  }
812
1164
  layers;
813
1165
  options;
814
1166
  stampedeGuard = new StampedeGuard();
815
1167
  metricsCollector = new MetricsCollector();
816
- instanceId = randomUUID();
1168
+ instanceId = createInstanceId();
817
1169
  startup;
818
1170
  unsubscribeInvalidation;
819
1171
  logger;
820
1172
  tagIndex;
1173
+ fetchRateLimiter = new FetchRateLimiter();
821
1174
  backgroundRefreshes = /* @__PURE__ */ new Map();
822
1175
  layerDegradedUntil = /* @__PURE__ */ new Map();
823
1176
  ttlResolver;
824
1177
  circuitBreakerManager;
1178
+ currentGeneration;
1179
+ writeBehindQueue = [];
1180
+ writeBehindTimer;
1181
+ writeBehindFlushPromise;
825
1182
  isDisconnecting = false;
826
1183
  disconnectPromise;
827
1184
  /**
@@ -831,9 +1188,9 @@ var CacheStack = class extends EventEmitter {
831
1188
  * and no `fetcher` is provided.
832
1189
  */
833
1190
  async get(key, fetcher, options) {
834
- const normalizedKey = this.validateCacheKey(key);
1191
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
835
1192
  this.validateWriteOptions(options);
836
- await this.startup;
1193
+ await this.awaitStartup("get");
837
1194
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
838
1195
  if (hit.found) {
839
1196
  this.ttlResolver.recordAccess(normalizedKey);
@@ -882,12 +1239,24 @@ var CacheStack = class extends EventEmitter {
882
1239
  async getOrSet(key, fetcher, options) {
883
1240
  return this.get(key, fetcher, options);
884
1241
  }
1242
+ /**
1243
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
1244
+ * Useful when the value is expected to exist or the fetcher is expected to
1245
+ * return non-null.
1246
+ */
1247
+ async getOrThrow(key, fetcher, options) {
1248
+ const value = await this.get(key, fetcher, options);
1249
+ if (value === null) {
1250
+ throw new CacheMissError(key);
1251
+ }
1252
+ return value;
1253
+ }
885
1254
  /**
886
1255
  * Returns true if the given key exists and is not expired in any layer.
887
1256
  */
888
1257
  async has(key) {
889
- const normalizedKey = this.validateCacheKey(key);
890
- await this.startup;
1258
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1259
+ await this.awaitStartup("has");
891
1260
  for (const layer of this.layers) {
892
1261
  if (this.shouldSkipLayer(layer)) {
893
1262
  continue;
@@ -917,8 +1286,8 @@ var CacheStack = class extends EventEmitter {
917
1286
  * that has it, or null if the key is not found / has no TTL.
918
1287
  */
919
1288
  async ttl(key) {
920
- const normalizedKey = this.validateCacheKey(key);
921
- await this.startup;
1289
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1290
+ await this.awaitStartup("ttl");
922
1291
  for (const layer of this.layers) {
923
1292
  if (this.shouldSkipLayer(layer)) {
924
1293
  continue;
@@ -939,17 +1308,17 @@ var CacheStack = class extends EventEmitter {
939
1308
  * Stores a value in all cache layers. Overwrites any existing value.
940
1309
  */
941
1310
  async set(key, value, options) {
942
- const normalizedKey = this.validateCacheKey(key);
1311
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
943
1312
  this.validateWriteOptions(options);
944
- await this.startup;
1313
+ await this.awaitStartup("set");
945
1314
  await this.storeEntry(normalizedKey, "value", value, options);
946
1315
  }
947
1316
  /**
948
1317
  * Deletes the key from all layers and publishes an invalidation message.
949
1318
  */
950
1319
  async delete(key) {
951
- const normalizedKey = this.validateCacheKey(key);
952
- await this.startup;
1320
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1321
+ await this.awaitStartup("delete");
953
1322
  await this.deleteKeys([normalizedKey]);
954
1323
  await this.publishInvalidation({
955
1324
  scope: "key",
@@ -959,7 +1328,7 @@ var CacheStack = class extends EventEmitter {
959
1328
  });
960
1329
  }
961
1330
  async clear() {
962
- await this.startup;
1331
+ await this.awaitStartup("clear");
963
1332
  await Promise.all(this.layers.map((layer) => layer.clear()));
964
1333
  await this.tagIndex.clear();
965
1334
  this.ttlResolver.clearProfiles();
@@ -975,23 +1344,25 @@ var CacheStack = class extends EventEmitter {
975
1344
  if (keys.length === 0) {
976
1345
  return;
977
1346
  }
978
- await this.startup;
1347
+ await this.awaitStartup("mdelete");
979
1348
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
980
- await this.deleteKeys(normalizedKeys);
1349
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1350
+ await this.deleteKeys(cacheKeys);
981
1351
  await this.publishInvalidation({
982
1352
  scope: "keys",
983
- keys: normalizedKeys,
1353
+ keys: cacheKeys,
984
1354
  sourceId: this.instanceId,
985
1355
  operation: "delete"
986
1356
  });
987
1357
  }
988
1358
  async mget(entries) {
1359
+ this.assertActive("mget");
989
1360
  if (entries.length === 0) {
990
1361
  return [];
991
1362
  }
992
1363
  const normalizedEntries = entries.map((entry) => ({
993
1364
  ...entry,
994
- key: this.validateCacheKey(entry.key)
1365
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
995
1366
  }));
996
1367
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
997
1368
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1017,7 +1388,7 @@ var CacheStack = class extends EventEmitter {
1017
1388
  })
1018
1389
  );
1019
1390
  }
1020
- await this.startup;
1391
+ await this.awaitStartup("mget");
1021
1392
  const pending = /* @__PURE__ */ new Set();
1022
1393
  const indexesByKey = /* @__PURE__ */ new Map();
1023
1394
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1065,14 +1436,17 @@ var CacheStack = class extends EventEmitter {
1065
1436
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1066
1437
  }
1067
1438
  async mset(entries) {
1439
+ this.assertActive("mset");
1068
1440
  const normalizedEntries = entries.map((entry) => ({
1069
1441
  ...entry,
1070
- key: this.validateCacheKey(entry.key)
1442
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1071
1443
  }));
1072
1444
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1073
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1445
+ await this.awaitStartup("mset");
1446
+ await this.writeBatch(normalizedEntries);
1074
1447
  }
1075
1448
  async warm(entries, options = {}) {
1449
+ this.assertActive("warm");
1076
1450
  const concurrency = Math.max(1, options.concurrency ?? 4);
1077
1451
  const total = entries.length;
1078
1452
  let completed = 0;
@@ -1121,14 +1495,31 @@ var CacheStack = class extends EventEmitter {
1121
1495
  return new CacheNamespace(this, prefix);
1122
1496
  }
1123
1497
  async invalidateByTag(tag) {
1124
- await this.startup;
1498
+ await this.awaitStartup("invalidateByTag");
1125
1499
  const keys = await this.tagIndex.keysForTag(tag);
1126
1500
  await this.deleteKeys(keys);
1127
1501
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1128
1502
  }
1503
+ async invalidateByTags(tags, mode = "any") {
1504
+ if (tags.length === 0) {
1505
+ return;
1506
+ }
1507
+ await this.awaitStartup("invalidateByTags");
1508
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1509
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1510
+ await this.deleteKeys(keys);
1511
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1512
+ }
1129
1513
  async invalidateByPattern(pattern) {
1130
- await this.startup;
1131
- const keys = await this.tagIndex.matchPattern(pattern);
1514
+ await this.awaitStartup("invalidateByPattern");
1515
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1516
+ await this.deleteKeys(keys);
1517
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1518
+ }
1519
+ async invalidateByPrefix(prefix) {
1520
+ await this.awaitStartup("invalidateByPrefix");
1521
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1522
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1132
1523
  await this.deleteKeys(keys);
1133
1524
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1134
1525
  }
@@ -1155,8 +1546,77 @@ var CacheStack = class extends EventEmitter {
1155
1546
  getHitRate() {
1156
1547
  return this.metricsCollector.hitRate();
1157
1548
  }
1158
- async exportState() {
1549
+ async healthCheck() {
1159
1550
  await this.startup;
1551
+ return Promise.all(
1552
+ this.layers.map(async (layer) => {
1553
+ const startedAt = performance.now();
1554
+ try {
1555
+ const healthy = layer.ping ? await layer.ping() : true;
1556
+ return {
1557
+ layer: layer.name,
1558
+ healthy,
1559
+ latencyMs: performance.now() - startedAt
1560
+ };
1561
+ } catch (error) {
1562
+ return {
1563
+ layer: layer.name,
1564
+ healthy: false,
1565
+ latencyMs: performance.now() - startedAt,
1566
+ error: this.formatError(error)
1567
+ };
1568
+ }
1569
+ })
1570
+ );
1571
+ }
1572
+ bumpGeneration(nextGeneration) {
1573
+ const current = this.currentGeneration ?? 0;
1574
+ this.currentGeneration = nextGeneration ?? current + 1;
1575
+ return this.currentGeneration;
1576
+ }
1577
+ /**
1578
+ * Returns detailed metadata about a single cache key: which layers contain it,
1579
+ * remaining fresh/stale/error TTLs, and associated tags.
1580
+ * Returns `null` if the key does not exist in any layer.
1581
+ */
1582
+ async inspect(key) {
1583
+ const userKey = this.validateCacheKey(key);
1584
+ const normalizedKey = this.qualifyKey(userKey);
1585
+ await this.awaitStartup("inspect");
1586
+ const foundInLayers = [];
1587
+ let freshTtlSeconds = null;
1588
+ let staleTtlSeconds = null;
1589
+ let errorTtlSeconds = null;
1590
+ let isStale = false;
1591
+ for (const layer of this.layers) {
1592
+ if (this.shouldSkipLayer(layer)) {
1593
+ continue;
1594
+ }
1595
+ const stored = await this.readLayerEntry(layer, normalizedKey);
1596
+ if (stored === null) {
1597
+ continue;
1598
+ }
1599
+ const resolved = resolveStoredValue(stored);
1600
+ if (resolved.state === "expired") {
1601
+ continue;
1602
+ }
1603
+ foundInLayers.push(layer.name);
1604
+ if (foundInLayers.length === 1 && resolved.envelope) {
1605
+ const now = Date.now();
1606
+ freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
1607
+ staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
1608
+ errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
1609
+ isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
1610
+ }
1611
+ }
1612
+ if (foundInLayers.length === 0) {
1613
+ return null;
1614
+ }
1615
+ const tags = await this.getTagsForKey(normalizedKey);
1616
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1617
+ }
1618
+ async exportState() {
1619
+ await this.awaitStartup("exportState");
1160
1620
  const exported = /* @__PURE__ */ new Map();
1161
1621
  for (const layer of this.layers) {
1162
1622
  if (!layer.keys) {
@@ -1164,15 +1624,16 @@ var CacheStack = class extends EventEmitter {
1164
1624
  }
1165
1625
  const keys = await layer.keys();
1166
1626
  for (const key of keys) {
1167
- if (exported.has(key)) {
1627
+ const exportedKey = this.stripQualifiedKey(key);
1628
+ if (exported.has(exportedKey)) {
1168
1629
  continue;
1169
1630
  }
1170
1631
  const stored = await this.readLayerEntry(layer, key);
1171
1632
  if (stored === null) {
1172
1633
  continue;
1173
1634
  }
1174
- exported.set(key, {
1175
- key,
1635
+ exported.set(exportedKey, {
1636
+ key: exportedKey,
1176
1637
  value: stored,
1177
1638
  ttl: remainingStoredTtlSeconds(stored)
1178
1639
  });
@@ -1181,19 +1642,24 @@ var CacheStack = class extends EventEmitter {
1181
1642
  return [...exported.values()];
1182
1643
  }
1183
1644
  async importState(entries) {
1184
- await this.startup;
1645
+ await this.awaitStartup("importState");
1185
1646
  await Promise.all(
1186
1647
  entries.map(async (entry) => {
1187
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1188
- await this.tagIndex.touch(entry.key);
1648
+ const qualifiedKey = this.qualifyKey(entry.key);
1649
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1650
+ await this.tagIndex.touch(qualifiedKey);
1189
1651
  })
1190
1652
  );
1191
1653
  }
1192
1654
  async persistToFile(filePath) {
1655
+ this.assertActive("persistToFile");
1193
1656
  const snapshot = await this.exportState();
1657
+ const { promises: fs } = await import("fs");
1194
1658
  await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1195
1659
  }
1196
1660
  async restoreFromFile(filePath) {
1661
+ this.assertActive("restoreFromFile");
1662
+ const { promises: fs } = await import("fs");
1197
1663
  const raw = await fs.readFile(filePath, "utf8");
1198
1664
  let parsed;
1199
1665
  try {
@@ -1217,7 +1683,13 @@ var CacheStack = class extends EventEmitter {
1217
1683
  this.disconnectPromise = (async () => {
1218
1684
  await this.startup;
1219
1685
  await this.unsubscribeInvalidation?.();
1686
+ await this.flushWriteBehindQueue();
1220
1687
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1688
+ if (this.writeBehindTimer) {
1689
+ clearInterval(this.writeBehindTimer);
1690
+ this.writeBehindTimer = void 0;
1691
+ }
1692
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1221
1693
  })();
1222
1694
  }
1223
1695
  await this.disconnectPromise;
@@ -1277,7 +1749,10 @@ var CacheStack = class extends EventEmitter {
1277
1749
  const fetchStart = Date.now();
1278
1750
  let fetched;
1279
1751
  try {
1280
- fetched = await fetcher();
1752
+ fetched = await this.fetchRateLimiter.schedule(
1753
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1754
+ fetcher
1755
+ );
1281
1756
  this.circuitBreakerManager.recordSuccess(key);
1282
1757
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1283
1758
  } catch (error) {
@@ -1291,6 +1766,9 @@ var CacheStack = class extends EventEmitter {
1291
1766
  await this.storeEntry(key, "empty", null, options);
1292
1767
  return null;
1293
1768
  }
1769
+ if (options?.shouldCache && !options.shouldCache(fetched)) {
1770
+ return fetched;
1771
+ }
1294
1772
  await this.storeEntry(key, "value", fetched, options);
1295
1773
  return fetched;
1296
1774
  }
@@ -1308,12 +1786,70 @@ var CacheStack = class extends EventEmitter {
1308
1786
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1309
1787
  }
1310
1788
  }
1789
+ async writeBatch(entries) {
1790
+ const now = Date.now();
1791
+ const entriesByLayer = /* @__PURE__ */ new Map();
1792
+ const immediateOperations = [];
1793
+ const deferredOperations = [];
1794
+ for (const entry of entries) {
1795
+ for (const layer of this.layers) {
1796
+ if (this.shouldSkipLayer(layer)) {
1797
+ continue;
1798
+ }
1799
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1800
+ const bucket = entriesByLayer.get(layer) ?? [];
1801
+ bucket.push(layerEntry);
1802
+ entriesByLayer.set(layer, bucket);
1803
+ }
1804
+ }
1805
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1806
+ const operation = async () => {
1807
+ try {
1808
+ if (layer.setMany) {
1809
+ await layer.setMany(layerEntries);
1810
+ return;
1811
+ }
1812
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1813
+ } catch (error) {
1814
+ await this.handleLayerFailure(layer, "write", error);
1815
+ }
1816
+ };
1817
+ if (this.shouldWriteBehind(layer)) {
1818
+ deferredOperations.push(operation);
1819
+ } else {
1820
+ immediateOperations.push(operation);
1821
+ }
1822
+ }
1823
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1824
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1825
+ for (const entry of entries) {
1826
+ if (entry.options?.tags) {
1827
+ await this.tagIndex.track(entry.key, entry.options.tags);
1828
+ } else {
1829
+ await this.tagIndex.touch(entry.key);
1830
+ }
1831
+ this.metricsCollector.increment("sets");
1832
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1833
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1834
+ }
1835
+ if (this.shouldBroadcastL1Invalidation()) {
1836
+ await this.publishInvalidation({
1837
+ scope: "keys",
1838
+ keys: entries.map((entry) => entry.key),
1839
+ sourceId: this.instanceId,
1840
+ operation: "write"
1841
+ });
1842
+ }
1843
+ }
1311
1844
  async readFromLayers(key, options, mode) {
1312
1845
  let sawRetainableValue = false;
1313
1846
  for (let index = 0; index < this.layers.length; index += 1) {
1314
1847
  const layer = this.layers[index];
1315
1848
  if (!layer) continue;
1849
+ const readStart = performance.now();
1316
1850
  const stored = await this.readLayerEntry(layer, key);
1851
+ const readDuration = performance.now() - readStart;
1852
+ this.metricsCollector.recordLatency(layer.name, readDuration);
1317
1853
  if (stored === null) {
1318
1854
  this.metricsCollector.incrementLayer("missesByLayer", layer.name);
1319
1855
  continue;
@@ -1388,33 +1924,28 @@ var CacheStack = class extends EventEmitter {
1388
1924
  }
1389
1925
  async writeAcrossLayers(key, kind, value, options) {
1390
1926
  const now = Date.now();
1391
- const operations = this.layers.map((layer) => async () => {
1392
- if (this.shouldSkipLayer(layer)) {
1393
- return;
1394
- }
1395
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1396
- const staleWhileRevalidate = this.resolveLayerSeconds(
1397
- layer.name,
1398
- options?.staleWhileRevalidate,
1399
- this.options.staleWhileRevalidate
1400
- );
1401
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1402
- const payload = createStoredValueEnvelope({
1403
- kind,
1404
- value,
1405
- freshTtlSeconds: freshTtl,
1406
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1407
- staleIfErrorSeconds: staleIfError,
1408
- now
1409
- });
1410
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1411
- try {
1412
- await layer.set(key, payload, ttl);
1413
- } catch (error) {
1414
- await this.handleLayerFailure(layer, "write", error);
1927
+ const immediateOperations = [];
1928
+ const deferredOperations = [];
1929
+ for (const layer of this.layers) {
1930
+ const operation = async () => {
1931
+ if (this.shouldSkipLayer(layer)) {
1932
+ return;
1933
+ }
1934
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1935
+ try {
1936
+ await layer.set(entry.key, entry.value, entry.ttl);
1937
+ } catch (error) {
1938
+ await this.handleLayerFailure(layer, "write", error);
1939
+ }
1940
+ };
1941
+ if (this.shouldWriteBehind(layer)) {
1942
+ deferredOperations.push(operation);
1943
+ } else {
1944
+ immediateOperations.push(operation);
1415
1945
  }
1416
- });
1417
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1946
+ }
1947
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1948
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1418
1949
  }
1419
1950
  async executeLayerOperations(operations, context) {
1420
1951
  if (this.options.writePolicy !== "best-effort") {
@@ -1438,8 +1969,17 @@ var CacheStack = class extends EventEmitter {
1438
1969
  );
1439
1970
  }
1440
1971
  }
1441
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1442
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1972
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1973
+ return this.ttlResolver.resolveFreshTtl(
1974
+ key,
1975
+ layerName,
1976
+ kind,
1977
+ options,
1978
+ fallbackTtl,
1979
+ this.options.negativeTtl,
1980
+ void 0,
1981
+ value
1982
+ );
1443
1983
  }
1444
1984
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1445
1985
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1515,6 +2055,12 @@ var CacheStack = class extends EventEmitter {
1515
2055
  }
1516
2056
  }
1517
2057
  }
2058
+ async getTagsForKey(key) {
2059
+ if (this.tagIndex.tagsForKey) {
2060
+ return this.tagIndex.tagsForKey(key);
2061
+ }
2062
+ return [];
2063
+ }
1518
2064
  formatError(error) {
1519
2065
  if (error instanceof Error) {
1520
2066
  return error.message;
@@ -1527,6 +2073,105 @@ var CacheStack = class extends EventEmitter {
1527
2073
  shouldBroadcastL1Invalidation() {
1528
2074
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1529
2075
  }
2076
+ initializeWriteBehind(options) {
2077
+ if (this.options.writeStrategy !== "write-behind") {
2078
+ return;
2079
+ }
2080
+ const flushIntervalMs = options?.flushIntervalMs;
2081
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2082
+ return;
2083
+ }
2084
+ this.writeBehindTimer = setInterval(() => {
2085
+ void this.flushWriteBehindQueue();
2086
+ }, flushIntervalMs);
2087
+ this.writeBehindTimer.unref?.();
2088
+ }
2089
+ shouldWriteBehind(layer) {
2090
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2091
+ }
2092
+ async enqueueWriteBehind(operation) {
2093
+ this.writeBehindQueue.push(operation);
2094
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2095
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2096
+ if (this.writeBehindQueue.length >= batchSize) {
2097
+ await this.flushWriteBehindQueue();
2098
+ return;
2099
+ }
2100
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2101
+ await this.flushWriteBehindQueue();
2102
+ }
2103
+ }
2104
+ async flushWriteBehindQueue() {
2105
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2106
+ await this.writeBehindFlushPromise;
2107
+ return;
2108
+ }
2109
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2110
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2111
+ this.writeBehindFlushPromise = (async () => {
2112
+ await Promise.allSettled(batch.map((operation) => operation()));
2113
+ })();
2114
+ await this.writeBehindFlushPromise;
2115
+ this.writeBehindFlushPromise = void 0;
2116
+ if (this.writeBehindQueue.length > 0) {
2117
+ await this.flushWriteBehindQueue();
2118
+ }
2119
+ }
2120
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2121
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2122
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2123
+ layer.name,
2124
+ options?.staleWhileRevalidate,
2125
+ this.options.staleWhileRevalidate
2126
+ );
2127
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2128
+ const payload = createStoredValueEnvelope({
2129
+ kind,
2130
+ value,
2131
+ freshTtlSeconds: freshTtl,
2132
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2133
+ staleIfErrorSeconds: staleIfError,
2134
+ now
2135
+ });
2136
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2137
+ return {
2138
+ key,
2139
+ value: payload,
2140
+ ttl
2141
+ };
2142
+ }
2143
+ intersectKeys(groups) {
2144
+ if (groups.length === 0) {
2145
+ return [];
2146
+ }
2147
+ const [firstGroup, ...rest] = groups;
2148
+ if (!firstGroup) {
2149
+ return [];
2150
+ }
2151
+ const restSets = rest.map((group) => new Set(group));
2152
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2153
+ }
2154
+ qualifyKey(key) {
2155
+ const prefix = this.generationPrefix();
2156
+ return prefix ? `${prefix}${key}` : key;
2157
+ }
2158
+ qualifyPattern(pattern) {
2159
+ const prefix = this.generationPrefix();
2160
+ return prefix ? `${prefix}${pattern}` : pattern;
2161
+ }
2162
+ stripQualifiedKey(key) {
2163
+ const prefix = this.generationPrefix();
2164
+ if (!prefix || !key.startsWith(prefix)) {
2165
+ return key;
2166
+ }
2167
+ return key.slice(prefix.length);
2168
+ }
2169
+ generationPrefix() {
2170
+ if (this.currentGeneration === void 0) {
2171
+ return "";
2172
+ }
2173
+ return `v${this.currentGeneration}:`;
2174
+ }
1530
2175
  async deleteKeysFromLayers(layers, keys) {
1531
2176
  await Promise.all(
1532
2177
  layers.map(async (layer) => {
@@ -1570,6 +2215,9 @@ var CacheStack = class extends EventEmitter {
1570
2215
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1571
2216
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1572
2217
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2218
+ if (this.options.generation !== void 0) {
2219
+ this.validateNonNegativeNumber("generation", this.options.generation);
2220
+ }
1573
2221
  }
1574
2222
  validateWriteOptions(options) {
1575
2223
  if (!options) {
@@ -1581,6 +2229,7 @@ var CacheStack = class extends EventEmitter {
1581
2229
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1582
2230
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1583
2231
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2232
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1584
2233
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1585
2234
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1586
2235
  }
@@ -1624,6 +2273,26 @@ var CacheStack = class extends EventEmitter {
1624
2273
  }
1625
2274
  return key;
1626
2275
  }
2276
+ validateTtlPolicy(name, policy) {
2277
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2278
+ return;
2279
+ }
2280
+ if ("alignTo" in policy) {
2281
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2282
+ return;
2283
+ }
2284
+ throw new Error(`${name} is invalid.`);
2285
+ }
2286
+ assertActive(operation) {
2287
+ if (this.isDisconnecting) {
2288
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2289
+ }
2290
+ }
2291
+ async awaitStartup(operation) {
2292
+ this.assertActive(operation);
2293
+ await this.startup;
2294
+ this.assertActive(operation);
2295
+ }
1627
2296
  serializeOptions(options) {
1628
2297
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1629
2298
  }
@@ -1729,6 +2398,9 @@ var CacheStack = class extends EventEmitter {
1729
2398
  return value;
1730
2399
  }
1731
2400
  };
2401
+ function createInstanceId() {
2402
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2403
+ }
1732
2404
 
1733
2405
  // src/module.ts
1734
2406
  var InjectCacheStack = () => Inject(CACHE_STACK);
@@ -1745,6 +2417,22 @@ var CacheStackModule = class {
1745
2417
  exports: [provider]
1746
2418
  };
1747
2419
  }
2420
+ static forRootAsync(options) {
2421
+ const provider = {
2422
+ provide: CACHE_STACK,
2423
+ inject: options.inject ?? [],
2424
+ useFactory: async (...args) => {
2425
+ const resolved = await options.useFactory(...args);
2426
+ return new CacheStack(resolved.layers, resolved.bridgeOptions);
2427
+ }
2428
+ };
2429
+ return {
2430
+ global: true,
2431
+ module: CacheStackModule,
2432
+ providers: [provider],
2433
+ exports: [provider]
2434
+ };
2435
+ }
1748
2436
  };
1749
2437
  CacheStackModule = __decorateClass([
1750
2438
  Global(),