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.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var __decorateClass = (decorators, target, key, kind) => {
20
30
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
@@ -72,63 +82,260 @@ function Cacheable(options) {
72
82
  var import_common = require("@nestjs/common");
73
83
 
74
84
  // ../../src/CacheStack.ts
75
- var import_node_crypto = require("crypto");
76
85
  var import_node_events = require("events");
77
- var import_node_fs = require("fs");
86
+
87
+ // ../../node_modules/async-mutex/index.mjs
88
+ var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
89
+ var E_ALREADY_LOCKED = new Error("mutex already locked");
90
+ var E_CANCELED = new Error("request for lock canceled");
91
+ var __awaiter$2 = function(thisArg, _arguments, P, generator) {
92
+ function adopt(value) {
93
+ return value instanceof P ? value : new P(function(resolve) {
94
+ resolve(value);
95
+ });
96
+ }
97
+ return new (P || (P = Promise))(function(resolve, reject) {
98
+ function fulfilled(value) {
99
+ try {
100
+ step(generator.next(value));
101
+ } catch (e) {
102
+ reject(e);
103
+ }
104
+ }
105
+ function rejected(value) {
106
+ try {
107
+ step(generator["throw"](value));
108
+ } catch (e) {
109
+ reject(e);
110
+ }
111
+ }
112
+ function step(result) {
113
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
114
+ }
115
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
116
+ });
117
+ };
118
+ var Semaphore = class {
119
+ constructor(_value, _cancelError = E_CANCELED) {
120
+ this._value = _value;
121
+ this._cancelError = _cancelError;
122
+ this._weightedQueues = [];
123
+ this._weightedWaiters = [];
124
+ }
125
+ acquire(weight = 1) {
126
+ if (weight <= 0)
127
+ throw new Error(`invalid weight ${weight}: must be positive`);
128
+ return new Promise((resolve, reject) => {
129
+ if (!this._weightedQueues[weight - 1])
130
+ this._weightedQueues[weight - 1] = [];
131
+ this._weightedQueues[weight - 1].push({ resolve, reject });
132
+ this._dispatch();
133
+ });
134
+ }
135
+ runExclusive(callback, weight = 1) {
136
+ return __awaiter$2(this, void 0, void 0, function* () {
137
+ const [value, release] = yield this.acquire(weight);
138
+ try {
139
+ return yield callback(value);
140
+ } finally {
141
+ release();
142
+ }
143
+ });
144
+ }
145
+ waitForUnlock(weight = 1) {
146
+ if (weight <= 0)
147
+ throw new Error(`invalid weight ${weight}: must be positive`);
148
+ return new Promise((resolve) => {
149
+ if (!this._weightedWaiters[weight - 1])
150
+ this._weightedWaiters[weight - 1] = [];
151
+ this._weightedWaiters[weight - 1].push(resolve);
152
+ this._dispatch();
153
+ });
154
+ }
155
+ isLocked() {
156
+ return this._value <= 0;
157
+ }
158
+ getValue() {
159
+ return this._value;
160
+ }
161
+ setValue(value) {
162
+ this._value = value;
163
+ this._dispatch();
164
+ }
165
+ release(weight = 1) {
166
+ if (weight <= 0)
167
+ throw new Error(`invalid weight ${weight}: must be positive`);
168
+ this._value += weight;
169
+ this._dispatch();
170
+ }
171
+ cancel() {
172
+ this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
173
+ this._weightedQueues = [];
174
+ }
175
+ _dispatch() {
176
+ var _a;
177
+ for (let weight = this._value; weight > 0; weight--) {
178
+ const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
179
+ if (!queueEntry)
180
+ continue;
181
+ const previousValue = this._value;
182
+ const previousWeight = weight;
183
+ this._value -= weight;
184
+ weight = this._value + 1;
185
+ queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
186
+ }
187
+ this._drainUnlockWaiters();
188
+ }
189
+ _newReleaser(weight) {
190
+ let called = false;
191
+ return () => {
192
+ if (called)
193
+ return;
194
+ called = true;
195
+ this.release(weight);
196
+ };
197
+ }
198
+ _drainUnlockWaiters() {
199
+ for (let weight = this._value; weight > 0; weight--) {
200
+ if (!this._weightedWaiters[weight - 1])
201
+ continue;
202
+ this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
203
+ this._weightedWaiters[weight - 1] = [];
204
+ }
205
+ }
206
+ };
207
+ var __awaiter$1 = function(thisArg, _arguments, P, generator) {
208
+ function adopt(value) {
209
+ return value instanceof P ? value : new P(function(resolve) {
210
+ resolve(value);
211
+ });
212
+ }
213
+ return new (P || (P = Promise))(function(resolve, reject) {
214
+ function fulfilled(value) {
215
+ try {
216
+ step(generator.next(value));
217
+ } catch (e) {
218
+ reject(e);
219
+ }
220
+ }
221
+ function rejected(value) {
222
+ try {
223
+ step(generator["throw"](value));
224
+ } catch (e) {
225
+ reject(e);
226
+ }
227
+ }
228
+ function step(result) {
229
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
230
+ }
231
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
232
+ });
233
+ };
234
+ var Mutex = class {
235
+ constructor(cancelError) {
236
+ this._semaphore = new Semaphore(1, cancelError);
237
+ }
238
+ acquire() {
239
+ return __awaiter$1(this, void 0, void 0, function* () {
240
+ const [, releaser] = yield this._semaphore.acquire();
241
+ return releaser;
242
+ });
243
+ }
244
+ runExclusive(callback) {
245
+ return this._semaphore.runExclusive(() => callback());
246
+ }
247
+ isLocked() {
248
+ return this._semaphore.isLocked();
249
+ }
250
+ waitForUnlock() {
251
+ return this._semaphore.waitForUnlock();
252
+ }
253
+ release() {
254
+ if (this._semaphore.isLocked())
255
+ this._semaphore.release();
256
+ }
257
+ cancel() {
258
+ return this._semaphore.cancel();
259
+ }
260
+ };
78
261
 
79
262
  // ../../src/CacheNamespace.ts
80
- var CacheNamespace = class {
263
+ var CacheNamespace = class _CacheNamespace {
81
264
  constructor(cache, prefix) {
82
265
  this.cache = cache;
83
266
  this.prefix = prefix;
84
267
  }
85
268
  cache;
86
269
  prefix;
270
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
271
+ metrics = emptyMetrics();
87
272
  async get(key, fetcher, options) {
88
- return this.cache.get(this.qualify(key), fetcher, options);
273
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
89
274
  }
90
275
  async getOrSet(key, fetcher, options) {
91
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
276
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
277
+ }
278
+ /**
279
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
280
+ */
281
+ async getOrThrow(key, fetcher, options) {
282
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
92
283
  }
93
284
  async has(key) {
94
- return this.cache.has(this.qualify(key));
285
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
95
286
  }
96
287
  async ttl(key) {
97
- return this.cache.ttl(this.qualify(key));
288
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
98
289
  }
99
290
  async set(key, value, options) {
100
- await this.cache.set(this.qualify(key), value, options);
291
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
101
292
  }
102
293
  async delete(key) {
103
- await this.cache.delete(this.qualify(key));
294
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
104
295
  }
105
296
  async mdelete(keys) {
106
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
297
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
107
298
  }
108
299
  async clear() {
109
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
300
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
110
301
  }
111
302
  async mget(entries) {
112
- return this.cache.mget(
113
- entries.map((entry) => ({
114
- ...entry,
115
- key: this.qualify(entry.key)
116
- }))
303
+ return this.trackMetrics(
304
+ () => this.cache.mget(
305
+ entries.map((entry) => ({
306
+ ...entry,
307
+ key: this.qualify(entry.key)
308
+ }))
309
+ )
117
310
  );
118
311
  }
119
312
  async mset(entries) {
120
- await this.cache.mset(
121
- entries.map((entry) => ({
122
- ...entry,
123
- key: this.qualify(entry.key)
124
- }))
313
+ await this.trackMetrics(
314
+ () => this.cache.mset(
315
+ entries.map((entry) => ({
316
+ ...entry,
317
+ key: this.qualify(entry.key)
318
+ }))
319
+ )
125
320
  );
126
321
  }
127
322
  async invalidateByTag(tag) {
128
- await this.cache.invalidateByTag(tag);
323
+ await this.trackMetrics(() => this.cache.invalidateByTag(tag));
324
+ }
325
+ async invalidateByTags(tags, mode = "any") {
326
+ await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
129
327
  }
130
328
  async invalidateByPattern(pattern) {
131
- await this.cache.invalidateByPattern(this.qualify(pattern));
329
+ await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
330
+ }
331
+ async invalidateByPrefix(prefix) {
332
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
333
+ }
334
+ /**
335
+ * Returns detailed metadata about a single cache key within this namespace.
336
+ */
337
+ async inspect(key) {
338
+ return this.cache.inspect(this.qualify(key));
132
339
  }
133
340
  wrap(keyPrefix, fetcher, options) {
134
341
  return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
@@ -143,15 +350,159 @@ var CacheNamespace = class {
143
350
  );
144
351
  }
145
352
  getMetrics() {
146
- return this.cache.getMetrics();
353
+ return cloneMetrics(this.metrics);
147
354
  }
148
355
  getHitRate() {
149
- return this.cache.getHitRate();
356
+ const total = this.metrics.hits + this.metrics.misses;
357
+ const overall = total === 0 ? 0 : this.metrics.hits / total;
358
+ const byLayer = {};
359
+ const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
360
+ for (const layer of layers) {
361
+ const hits = this.metrics.hitsByLayer[layer] ?? 0;
362
+ const misses = this.metrics.missesByLayer[layer] ?? 0;
363
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
364
+ }
365
+ return { overall, byLayer };
366
+ }
367
+ /**
368
+ * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
369
+ *
370
+ * ```ts
371
+ * const tenant = cache.namespace('tenant:abc')
372
+ * const posts = tenant.namespace('posts')
373
+ * // keys become: "tenant:abc:posts:mykey"
374
+ * ```
375
+ */
376
+ namespace(childPrefix) {
377
+ return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
150
378
  }
151
379
  qualify(key) {
152
380
  return `${this.prefix}:${key}`;
153
381
  }
382
+ async trackMetrics(operation) {
383
+ return this.getMetricsMutex().runExclusive(async () => {
384
+ const before = this.cache.getMetrics();
385
+ const result = await operation();
386
+ const after = this.cache.getMetrics();
387
+ this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
388
+ return result;
389
+ });
390
+ }
391
+ getMetricsMutex() {
392
+ const existing = _CacheNamespace.metricsMutexes.get(this.cache);
393
+ if (existing) {
394
+ return existing;
395
+ }
396
+ const mutex = new Mutex();
397
+ _CacheNamespace.metricsMutexes.set(this.cache, mutex);
398
+ return mutex;
399
+ }
154
400
  };
401
+ function emptyMetrics() {
402
+ return {
403
+ hits: 0,
404
+ misses: 0,
405
+ fetches: 0,
406
+ sets: 0,
407
+ deletes: 0,
408
+ backfills: 0,
409
+ invalidations: 0,
410
+ staleHits: 0,
411
+ refreshes: 0,
412
+ refreshErrors: 0,
413
+ writeFailures: 0,
414
+ singleFlightWaits: 0,
415
+ negativeCacheHits: 0,
416
+ circuitBreakerTrips: 0,
417
+ degradedOperations: 0,
418
+ hitsByLayer: {},
419
+ missesByLayer: {},
420
+ latencyByLayer: {},
421
+ resetAt: Date.now()
422
+ };
423
+ }
424
+ function cloneMetrics(metrics) {
425
+ return {
426
+ ...metrics,
427
+ hitsByLayer: { ...metrics.hitsByLayer },
428
+ missesByLayer: { ...metrics.missesByLayer },
429
+ latencyByLayer: Object.fromEntries(
430
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
431
+ )
432
+ };
433
+ }
434
+ function diffMetrics(before, after) {
435
+ const latencyByLayer = Object.fromEntries(
436
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
437
+ layer,
438
+ {
439
+ avgMs: value.avgMs,
440
+ maxMs: value.maxMs,
441
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
442
+ }
443
+ ])
444
+ );
445
+ return {
446
+ hits: after.hits - before.hits,
447
+ misses: after.misses - before.misses,
448
+ fetches: after.fetches - before.fetches,
449
+ sets: after.sets - before.sets,
450
+ deletes: after.deletes - before.deletes,
451
+ backfills: after.backfills - before.backfills,
452
+ invalidations: after.invalidations - before.invalidations,
453
+ staleHits: after.staleHits - before.staleHits,
454
+ refreshes: after.refreshes - before.refreshes,
455
+ refreshErrors: after.refreshErrors - before.refreshErrors,
456
+ writeFailures: after.writeFailures - before.writeFailures,
457
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
458
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
459
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
460
+ degradedOperations: after.degradedOperations - before.degradedOperations,
461
+ hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
462
+ missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
463
+ latencyByLayer,
464
+ resetAt: after.resetAt
465
+ };
466
+ }
467
+ function addMetrics(base, delta) {
468
+ return {
469
+ hits: base.hits + delta.hits,
470
+ misses: base.misses + delta.misses,
471
+ fetches: base.fetches + delta.fetches,
472
+ sets: base.sets + delta.sets,
473
+ deletes: base.deletes + delta.deletes,
474
+ backfills: base.backfills + delta.backfills,
475
+ invalidations: base.invalidations + delta.invalidations,
476
+ staleHits: base.staleHits + delta.staleHits,
477
+ refreshes: base.refreshes + delta.refreshes,
478
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
479
+ writeFailures: base.writeFailures + delta.writeFailures,
480
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
481
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
482
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
483
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
484
+ hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
485
+ missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
486
+ latencyByLayer: cloneMetrics(delta).latencyByLayer,
487
+ resetAt: base.resetAt
488
+ };
489
+ }
490
+ function diffMap(before, after) {
491
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
492
+ const result = {};
493
+ for (const key of keys) {
494
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
495
+ }
496
+ return result;
497
+ }
498
+ function addMap(base, delta) {
499
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
500
+ const result = {};
501
+ for (const key of keys) {
502
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
503
+ }
504
+ return result;
505
+ }
155
506
 
156
507
  // ../../src/internal/CircuitBreakerManager.ts
157
508
  var CircuitBreakerManager = class {
@@ -245,11 +596,105 @@ var CircuitBreakerManager = class {
245
596
  }
246
597
  };
247
598
 
599
+ // ../../src/internal/FetchRateLimiter.ts
600
+ var FetchRateLimiter = class {
601
+ active = 0;
602
+ queue = [];
603
+ startedAt = [];
604
+ drainTimer;
605
+ async schedule(options, task) {
606
+ if (!options) {
607
+ return task();
608
+ }
609
+ const normalized = this.normalize(options);
610
+ if (!normalized) {
611
+ return task();
612
+ }
613
+ return new Promise((resolve, reject) => {
614
+ this.queue.push({ options: normalized, task, resolve, reject });
615
+ this.drain();
616
+ });
617
+ }
618
+ normalize(options) {
619
+ const maxConcurrent = options.maxConcurrent;
620
+ const intervalMs = options.intervalMs;
621
+ const maxPerInterval = options.maxPerInterval;
622
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
623
+ return void 0;
624
+ }
625
+ return {
626
+ maxConcurrent,
627
+ intervalMs,
628
+ maxPerInterval
629
+ };
630
+ }
631
+ drain() {
632
+ if (this.drainTimer) {
633
+ clearTimeout(this.drainTimer);
634
+ this.drainTimer = void 0;
635
+ }
636
+ while (this.queue.length > 0) {
637
+ const next = this.queue[0];
638
+ if (!next) {
639
+ return;
640
+ }
641
+ const waitMs = this.waitTime(next.options);
642
+ if (waitMs > 0) {
643
+ this.drainTimer = setTimeout(() => {
644
+ this.drainTimer = void 0;
645
+ this.drain();
646
+ }, waitMs);
647
+ this.drainTimer.unref?.();
648
+ return;
649
+ }
650
+ this.queue.shift();
651
+ this.active += 1;
652
+ this.startedAt.push(Date.now());
653
+ void next.task().then(next.resolve, next.reject).finally(() => {
654
+ this.active -= 1;
655
+ this.drain();
656
+ });
657
+ }
658
+ }
659
+ waitTime(options) {
660
+ const now = Date.now();
661
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
662
+ return 1;
663
+ }
664
+ if (!options.intervalMs || !options.maxPerInterval) {
665
+ return 0;
666
+ }
667
+ this.prune(now, options.intervalMs);
668
+ if (this.startedAt.length < options.maxPerInterval) {
669
+ return 0;
670
+ }
671
+ const oldest = this.startedAt[0];
672
+ if (!oldest) {
673
+ return 0;
674
+ }
675
+ return Math.max(1, options.intervalMs - (now - oldest));
676
+ }
677
+ prune(now, intervalMs) {
678
+ while (this.startedAt.length > 0) {
679
+ const startedAt = this.startedAt[0];
680
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
681
+ break;
682
+ }
683
+ this.startedAt.shift();
684
+ }
685
+ }
686
+ };
687
+
248
688
  // ../../src/internal/MetricsCollector.ts
249
689
  var MetricsCollector = class {
250
690
  data = this.empty();
251
691
  get snapshot() {
252
- return { ...this.data };
692
+ return {
693
+ ...this.data,
694
+ hitsByLayer: { ...this.data.hitsByLayer },
695
+ missesByLayer: { ...this.data.missesByLayer },
696
+ latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
697
+ };
253
698
  }
254
699
  increment(field, amount = 1) {
255
700
  ;
@@ -258,6 +703,22 @@ var MetricsCollector = class {
258
703
  incrementLayer(map, layerName) {
259
704
  this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
260
705
  }
706
+ /**
707
+ * Records a read latency sample for the given layer.
708
+ * Maintains a rolling average and max using Welford's online algorithm.
709
+ */
710
+ recordLatency(layerName, durationMs) {
711
+ const existing = this.data.latencyByLayer[layerName];
712
+ if (!existing) {
713
+ this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
714
+ return;
715
+ }
716
+ existing.count += 1;
717
+ existing.avgMs += (durationMs - existing.avgMs) / existing.count;
718
+ if (durationMs > existing.maxMs) {
719
+ existing.maxMs = durationMs;
720
+ }
721
+ }
261
722
  reset() {
262
723
  this.data = this.empty();
263
724
  }
@@ -292,6 +753,7 @@ var MetricsCollector = class {
292
753
  degradedOperations: 0,
293
754
  hitsByLayer: {},
294
755
  missesByLayer: {},
756
+ latencyByLayer: {},
295
757
  resetAt: Date.now()
296
758
  };
297
759
  }
@@ -419,13 +881,14 @@ var TtlResolver = class {
419
881
  clearProfiles() {
420
882
  this.accessProfiles.clear();
421
883
  }
422
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
884
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
885
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
423
886
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
424
887
  layerName,
425
888
  options?.negativeTtl,
426
889
  globalNegativeTtl,
427
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
428
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
890
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
891
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
429
892
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
430
893
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
431
894
  return this.applyJitter(adaptiveTtl, jitter);
@@ -464,6 +927,29 @@ var TtlResolver = class {
464
927
  const delta = (Math.random() * 2 - 1) * jitter;
465
928
  return Math.max(1, Math.round(ttl + delta));
466
929
  }
930
+ resolvePolicyTtl(key, value, policy) {
931
+ if (!policy) {
932
+ return void 0;
933
+ }
934
+ if (typeof policy === "function") {
935
+ return policy({ key, value });
936
+ }
937
+ const now = /* @__PURE__ */ new Date();
938
+ if (policy === "until-midnight") {
939
+ const nextMidnight = new Date(now);
940
+ nextMidnight.setHours(24, 0, 0, 0);
941
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
942
+ }
943
+ if (policy === "next-hour") {
944
+ const nextHour = new Date(now);
945
+ nextHour.setMinutes(60, 0, 0);
946
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
947
+ }
948
+ const alignToSeconds = policy.alignTo;
949
+ const currentSeconds = Math.floor(Date.now() / 1e3);
950
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
951
+ return Math.max(1, nextBoundary - currentSeconds);
952
+ }
467
953
  readLayerNumber(layerName, value) {
468
954
  if (typeof value === "number") {
469
955
  return value;
@@ -491,269 +977,133 @@ var PatternMatcher = class _PatternMatcher {
491
977
  /**
492
978
  * Tests whether a glob-style pattern matches a value.
493
979
  * Supports `*` (any sequence of characters) and `?` (any single character).
494
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
980
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
981
+ * quadratic memory usage on long patterns/keys.
495
982
  */
496
983
  static matches(pattern, value) {
497
984
  return _PatternMatcher.matchLinear(pattern, value);
498
985
  }
499
986
  /**
500
- * Linear-time glob matching using dynamic programming.
501
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
987
+ * Linear-time glob matching with O(1) extra memory.
502
988
  */
503
989
  static matchLinear(pattern, value) {
504
- const m = pattern.length;
505
- const n = value.length;
506
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
507
- dp[0][0] = true;
508
- for (let i = 1; i <= m; i++) {
509
- if (pattern[i - 1] === "*") {
510
- dp[i][0] = dp[i - 1]?.[0];
511
- }
512
- }
513
- for (let i = 1; i <= m; i++) {
514
- for (let j = 1; j <= n; j++) {
515
- const pc = pattern[i - 1];
516
- if (pc === "*") {
517
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
518
- } else if (pc === "?" || pc === value[j - 1]) {
519
- dp[i][j] = dp[i - 1]?.[j - 1];
520
- }
521
- }
522
- }
523
- return dp[m]?.[n];
524
- }
525
- };
526
-
527
- // ../../src/invalidation/TagIndex.ts
528
- var TagIndex = class {
529
- tagToKeys = /* @__PURE__ */ new Map();
530
- keyToTags = /* @__PURE__ */ new Map();
531
- knownKeys = /* @__PURE__ */ new Set();
532
- async touch(key) {
533
- this.knownKeys.add(key);
534
- }
535
- async track(key, tags) {
536
- this.knownKeys.add(key);
537
- if (tags.length === 0) {
538
- return;
539
- }
540
- const existingTags = this.keyToTags.get(key);
541
- if (existingTags) {
542
- for (const tag of existingTags) {
543
- this.tagToKeys.get(tag)?.delete(key);
544
- }
545
- }
546
- const tagSet = new Set(tags);
547
- this.keyToTags.set(key, tagSet);
548
- for (const tag of tagSet) {
549
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
550
- keys.add(key);
551
- this.tagToKeys.set(tag, keys);
552
- }
553
- }
554
- async remove(key) {
555
- this.knownKeys.delete(key);
556
- const tags = this.keyToTags.get(key);
557
- if (!tags) {
558
- return;
559
- }
560
- for (const tag of tags) {
561
- const keys = this.tagToKeys.get(tag);
562
- if (!keys) {
990
+ let patternIndex = 0;
991
+ let valueIndex = 0;
992
+ let starIndex = -1;
993
+ let backtrackValueIndex = 0;
994
+ while (valueIndex < value.length) {
995
+ const patternChar = pattern[patternIndex];
996
+ const valueChar = value[valueIndex];
997
+ if (patternChar === "*" && patternIndex < pattern.length) {
998
+ starIndex = patternIndex;
999
+ patternIndex += 1;
1000
+ backtrackValueIndex = valueIndex;
563
1001
  continue;
564
1002
  }
565
- keys.delete(key);
566
- if (keys.size === 0) {
567
- this.tagToKeys.delete(tag);
568
- }
569
- }
570
- this.keyToTags.delete(key);
571
- }
572
- async keysForTag(tag) {
573
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
574
- }
575
- async matchPattern(pattern) {
576
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
577
- }
578
- async clear() {
579
- this.tagToKeys.clear();
580
- this.keyToTags.clear();
581
- this.knownKeys.clear();
582
- }
583
- };
584
-
585
- // ../../node_modules/async-mutex/index.mjs
586
- var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
587
- var E_ALREADY_LOCKED = new Error("mutex already locked");
588
- var E_CANCELED = new Error("request for lock canceled");
589
- var __awaiter$2 = function(thisArg, _arguments, P, generator) {
590
- function adopt(value) {
591
- return value instanceof P ? value : new P(function(resolve) {
592
- resolve(value);
593
- });
594
- }
595
- return new (P || (P = Promise))(function(resolve, reject) {
596
- function fulfilled(value) {
597
- try {
598
- step(generator.next(value));
599
- } catch (e) {
600
- reject(e);
601
- }
602
- }
603
- function rejected(value) {
604
- try {
605
- step(generator["throw"](value));
606
- } catch (e) {
607
- reject(e);
608
- }
609
- }
610
- function step(result) {
611
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
612
- }
613
- step((generator = generator.apply(thisArg, _arguments || [])).next());
614
- });
615
- };
616
- var Semaphore = class {
617
- constructor(_value, _cancelError = E_CANCELED) {
618
- this._value = _value;
619
- this._cancelError = _cancelError;
620
- this._weightedQueues = [];
621
- this._weightedWaiters = [];
622
- }
623
- acquire(weight = 1) {
624
- if (weight <= 0)
625
- throw new Error(`invalid weight ${weight}: must be positive`);
626
- return new Promise((resolve, reject) => {
627
- if (!this._weightedQueues[weight - 1])
628
- this._weightedQueues[weight - 1] = [];
629
- this._weightedQueues[weight - 1].push({ resolve, reject });
630
- this._dispatch();
631
- });
632
- }
633
- runExclusive(callback, weight = 1) {
634
- return __awaiter$2(this, void 0, void 0, function* () {
635
- const [value, release] = yield this.acquire(weight);
636
- try {
637
- return yield callback(value);
638
- } finally {
639
- release();
640
- }
641
- });
642
- }
643
- waitForUnlock(weight = 1) {
644
- if (weight <= 0)
645
- throw new Error(`invalid weight ${weight}: must be positive`);
646
- return new Promise((resolve) => {
647
- if (!this._weightedWaiters[weight - 1])
648
- this._weightedWaiters[weight - 1] = [];
649
- this._weightedWaiters[weight - 1].push(resolve);
650
- this._dispatch();
651
- });
652
- }
653
- isLocked() {
654
- return this._value <= 0;
655
- }
656
- getValue() {
657
- return this._value;
658
- }
659
- setValue(value) {
660
- this._value = value;
661
- this._dispatch();
662
- }
663
- release(weight = 1) {
664
- if (weight <= 0)
665
- throw new Error(`invalid weight ${weight}: must be positive`);
666
- this._value += weight;
667
- this._dispatch();
668
- }
669
- cancel() {
670
- this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
671
- this._weightedQueues = [];
672
- }
673
- _dispatch() {
674
- var _a;
675
- for (let weight = this._value; weight > 0; weight--) {
676
- const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
677
- if (!queueEntry)
1003
+ if (patternChar === "?" || patternChar === valueChar) {
1004
+ patternIndex += 1;
1005
+ valueIndex += 1;
678
1006
  continue;
679
- const previousValue = this._value;
680
- const previousWeight = weight;
681
- this._value -= weight;
682
- weight = this._value + 1;
683
- queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
684
- }
685
- this._drainUnlockWaiters();
686
- }
687
- _newReleaser(weight) {
688
- let called = false;
689
- return () => {
690
- if (called)
691
- return;
692
- called = true;
693
- this.release(weight);
694
- };
695
- }
696
- _drainUnlockWaiters() {
697
- for (let weight = this._value; weight > 0; weight--) {
698
- if (!this._weightedWaiters[weight - 1])
1007
+ }
1008
+ if (starIndex !== -1) {
1009
+ patternIndex = starIndex + 1;
1010
+ backtrackValueIndex += 1;
1011
+ valueIndex = backtrackValueIndex;
699
1012
  continue;
700
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
701
- this._weightedWaiters[weight - 1] = [];
1013
+ }
1014
+ return false;
702
1015
  }
1016
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
1017
+ patternIndex += 1;
1018
+ }
1019
+ return patternIndex === pattern.length;
703
1020
  }
704
1021
  };
705
- var __awaiter$1 = function(thisArg, _arguments, P, generator) {
706
- function adopt(value) {
707
- return value instanceof P ? value : new P(function(resolve) {
708
- resolve(value);
709
- });
1022
+
1023
+ // ../../src/invalidation/TagIndex.ts
1024
+ var TagIndex = class {
1025
+ tagToKeys = /* @__PURE__ */ new Map();
1026
+ keyToTags = /* @__PURE__ */ new Map();
1027
+ knownKeys = /* @__PURE__ */ new Set();
1028
+ maxKnownKeys;
1029
+ constructor(options = {}) {
1030
+ this.maxKnownKeys = options.maxKnownKeys;
710
1031
  }
711
- return new (P || (P = Promise))(function(resolve, reject) {
712
- function fulfilled(value) {
713
- try {
714
- step(generator.next(value));
715
- } catch (e) {
716
- reject(e);
717
- }
1032
+ async touch(key) {
1033
+ this.knownKeys.add(key);
1034
+ this.pruneKnownKeysIfNeeded();
1035
+ }
1036
+ async track(key, tags) {
1037
+ this.knownKeys.add(key);
1038
+ this.pruneKnownKeysIfNeeded();
1039
+ if (tags.length === 0) {
1040
+ return;
718
1041
  }
719
- function rejected(value) {
720
- try {
721
- step(generator["throw"](value));
722
- } catch (e) {
723
- reject(e);
1042
+ const existingTags = this.keyToTags.get(key);
1043
+ if (existingTags) {
1044
+ for (const tag of existingTags) {
1045
+ this.tagToKeys.get(tag)?.delete(key);
724
1046
  }
725
1047
  }
726
- function step(result) {
727
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1048
+ const tagSet = new Set(tags);
1049
+ this.keyToTags.set(key, tagSet);
1050
+ for (const tag of tagSet) {
1051
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1052
+ keys.add(key);
1053
+ this.tagToKeys.set(tag, keys);
728
1054
  }
729
- step((generator = generator.apply(thisArg, _arguments || [])).next());
730
- });
731
- };
732
- var Mutex = class {
733
- constructor(cancelError) {
734
- this._semaphore = new Semaphore(1, cancelError);
735
1055
  }
736
- acquire() {
737
- return __awaiter$1(this, void 0, void 0, function* () {
738
- const [, releaser] = yield this._semaphore.acquire();
739
- return releaser;
740
- });
1056
+ async remove(key) {
1057
+ this.removeKey(key);
741
1058
  }
742
- runExclusive(callback) {
743
- return this._semaphore.runExclusive(() => callback());
1059
+ async keysForTag(tag) {
1060
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
744
1061
  }
745
- isLocked() {
746
- return this._semaphore.isLocked();
1062
+ async keysForPrefix(prefix) {
1063
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
747
1064
  }
748
- waitForUnlock() {
749
- return this._semaphore.waitForUnlock();
1065
+ async tagsForKey(key) {
1066
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
750
1067
  }
751
- release() {
752
- if (this._semaphore.isLocked())
753
- this._semaphore.release();
1068
+ async matchPattern(pattern) {
1069
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
754
1070
  }
755
- cancel() {
756
- return this._semaphore.cancel();
1071
+ async clear() {
1072
+ this.tagToKeys.clear();
1073
+ this.keyToTags.clear();
1074
+ this.knownKeys.clear();
1075
+ }
1076
+ pruneKnownKeysIfNeeded() {
1077
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1078
+ return;
1079
+ }
1080
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1081
+ let removed = 0;
1082
+ for (const key of this.knownKeys) {
1083
+ if (removed >= toRemove) {
1084
+ break;
1085
+ }
1086
+ this.removeKey(key);
1087
+ removed += 1;
1088
+ }
1089
+ }
1090
+ removeKey(key) {
1091
+ this.knownKeys.delete(key);
1092
+ const tags = this.keyToTags.get(key);
1093
+ if (!tags) {
1094
+ return;
1095
+ }
1096
+ for (const tag of tags) {
1097
+ const keys = this.tagToKeys.get(tag);
1098
+ if (!keys) {
1099
+ continue;
1100
+ }
1101
+ keys.delete(key);
1102
+ if (keys.size === 0) {
1103
+ this.tagToKeys.delete(tag);
1104
+ }
1105
+ }
1106
+ this.keyToTags.delete(key);
757
1107
  }
758
1108
  };
759
1109
 
@@ -782,6 +1132,16 @@ var StampedeGuard = class {
782
1132
  }
783
1133
  };
784
1134
 
1135
+ // ../../src/types.ts
1136
+ var CacheMissError = class extends Error {
1137
+ key;
1138
+ constructor(key) {
1139
+ super(`Cache miss for key "${key}".`);
1140
+ this.name = "CacheMissError";
1141
+ this.key = key;
1142
+ }
1143
+ };
1144
+
785
1145
  // ../../src/CacheStack.ts
786
1146
  var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
787
1147
  var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
@@ -825,6 +1185,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
825
1185
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
826
1186
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
827
1187
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1188
+ this.currentGeneration = options.generation;
828
1189
  if (options.publishSetInvalidation !== void 0) {
829
1190
  console.warn(
830
1191
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -833,21 +1194,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
833
1194
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
834
1195
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
835
1196
  this.tagIndex = options.tagIndex ?? new TagIndex();
1197
+ this.initializeWriteBehind(options.writeBehind);
836
1198
  this.startup = this.initialize();
837
1199
  }
838
1200
  layers;
839
1201
  options;
840
1202
  stampedeGuard = new StampedeGuard();
841
1203
  metricsCollector = new MetricsCollector();
842
- instanceId = (0, import_node_crypto.randomUUID)();
1204
+ instanceId = createInstanceId();
843
1205
  startup;
844
1206
  unsubscribeInvalidation;
845
1207
  logger;
846
1208
  tagIndex;
1209
+ fetchRateLimiter = new FetchRateLimiter();
847
1210
  backgroundRefreshes = /* @__PURE__ */ new Map();
848
1211
  layerDegradedUntil = /* @__PURE__ */ new Map();
849
1212
  ttlResolver;
850
1213
  circuitBreakerManager;
1214
+ currentGeneration;
1215
+ writeBehindQueue = [];
1216
+ writeBehindTimer;
1217
+ writeBehindFlushPromise;
851
1218
  isDisconnecting = false;
852
1219
  disconnectPromise;
853
1220
  /**
@@ -857,9 +1224,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
857
1224
  * and no `fetcher` is provided.
858
1225
  */
859
1226
  async get(key, fetcher, options) {
860
- const normalizedKey = this.validateCacheKey(key);
1227
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
861
1228
  this.validateWriteOptions(options);
862
- await this.startup;
1229
+ await this.awaitStartup("get");
863
1230
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
864
1231
  if (hit.found) {
865
1232
  this.ttlResolver.recordAccess(normalizedKey);
@@ -908,12 +1275,24 @@ var CacheStack = class extends import_node_events.EventEmitter {
908
1275
  async getOrSet(key, fetcher, options) {
909
1276
  return this.get(key, fetcher, options);
910
1277
  }
1278
+ /**
1279
+ * Like `get()`, but throws `CacheMissError` instead of returning `null`.
1280
+ * Useful when the value is expected to exist or the fetcher is expected to
1281
+ * return non-null.
1282
+ */
1283
+ async getOrThrow(key, fetcher, options) {
1284
+ const value = await this.get(key, fetcher, options);
1285
+ if (value === null) {
1286
+ throw new CacheMissError(key);
1287
+ }
1288
+ return value;
1289
+ }
911
1290
  /**
912
1291
  * Returns true if the given key exists and is not expired in any layer.
913
1292
  */
914
1293
  async has(key) {
915
- const normalizedKey = this.validateCacheKey(key);
916
- await this.startup;
1294
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1295
+ await this.awaitStartup("has");
917
1296
  for (const layer of this.layers) {
918
1297
  if (this.shouldSkipLayer(layer)) {
919
1298
  continue;
@@ -943,8 +1322,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
943
1322
  * that has it, or null if the key is not found / has no TTL.
944
1323
  */
945
1324
  async ttl(key) {
946
- const normalizedKey = this.validateCacheKey(key);
947
- await this.startup;
1325
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1326
+ await this.awaitStartup("ttl");
948
1327
  for (const layer of this.layers) {
949
1328
  if (this.shouldSkipLayer(layer)) {
950
1329
  continue;
@@ -965,17 +1344,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
965
1344
  * Stores a value in all cache layers. Overwrites any existing value.
966
1345
  */
967
1346
  async set(key, value, options) {
968
- const normalizedKey = this.validateCacheKey(key);
1347
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
969
1348
  this.validateWriteOptions(options);
970
- await this.startup;
1349
+ await this.awaitStartup("set");
971
1350
  await this.storeEntry(normalizedKey, "value", value, options);
972
1351
  }
973
1352
  /**
974
1353
  * Deletes the key from all layers and publishes an invalidation message.
975
1354
  */
976
1355
  async delete(key) {
977
- const normalizedKey = this.validateCacheKey(key);
978
- await this.startup;
1356
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1357
+ await this.awaitStartup("delete");
979
1358
  await this.deleteKeys([normalizedKey]);
980
1359
  await this.publishInvalidation({
981
1360
  scope: "key",
@@ -985,7 +1364,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
985
1364
  });
986
1365
  }
987
1366
  async clear() {
988
- await this.startup;
1367
+ await this.awaitStartup("clear");
989
1368
  await Promise.all(this.layers.map((layer) => layer.clear()));
990
1369
  await this.tagIndex.clear();
991
1370
  this.ttlResolver.clearProfiles();
@@ -1001,23 +1380,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1001
1380
  if (keys.length === 0) {
1002
1381
  return;
1003
1382
  }
1004
- await this.startup;
1383
+ await this.awaitStartup("mdelete");
1005
1384
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1006
- await this.deleteKeys(normalizedKeys);
1385
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1386
+ await this.deleteKeys(cacheKeys);
1007
1387
  await this.publishInvalidation({
1008
1388
  scope: "keys",
1009
- keys: normalizedKeys,
1389
+ keys: cacheKeys,
1010
1390
  sourceId: this.instanceId,
1011
1391
  operation: "delete"
1012
1392
  });
1013
1393
  }
1014
1394
  async mget(entries) {
1395
+ this.assertActive("mget");
1015
1396
  if (entries.length === 0) {
1016
1397
  return [];
1017
1398
  }
1018
1399
  const normalizedEntries = entries.map((entry) => ({
1019
1400
  ...entry,
1020
- key: this.validateCacheKey(entry.key)
1401
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1021
1402
  }));
1022
1403
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1023
1404
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1043,7 +1424,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1043
1424
  })
1044
1425
  );
1045
1426
  }
1046
- await this.startup;
1427
+ await this.awaitStartup("mget");
1047
1428
  const pending = /* @__PURE__ */ new Set();
1048
1429
  const indexesByKey = /* @__PURE__ */ new Map();
1049
1430
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1091,14 +1472,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1091
1472
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1092
1473
  }
1093
1474
  async mset(entries) {
1475
+ this.assertActive("mset");
1094
1476
  const normalizedEntries = entries.map((entry) => ({
1095
1477
  ...entry,
1096
- key: this.validateCacheKey(entry.key)
1478
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1097
1479
  }));
1098
1480
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1099
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1481
+ await this.awaitStartup("mset");
1482
+ await this.writeBatch(normalizedEntries);
1100
1483
  }
1101
1484
  async warm(entries, options = {}) {
1485
+ this.assertActive("warm");
1102
1486
  const concurrency = Math.max(1, options.concurrency ?? 4);
1103
1487
  const total = entries.length;
1104
1488
  let completed = 0;
@@ -1147,14 +1531,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
1147
1531
  return new CacheNamespace(this, prefix);
1148
1532
  }
1149
1533
  async invalidateByTag(tag) {
1150
- await this.startup;
1534
+ await this.awaitStartup("invalidateByTag");
1151
1535
  const keys = await this.tagIndex.keysForTag(tag);
1152
1536
  await this.deleteKeys(keys);
1153
1537
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1154
1538
  }
1539
+ async invalidateByTags(tags, mode = "any") {
1540
+ if (tags.length === 0) {
1541
+ return;
1542
+ }
1543
+ await this.awaitStartup("invalidateByTags");
1544
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1545
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1546
+ await this.deleteKeys(keys);
1547
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1548
+ }
1155
1549
  async invalidateByPattern(pattern) {
1156
- await this.startup;
1157
- const keys = await this.tagIndex.matchPattern(pattern);
1550
+ await this.awaitStartup("invalidateByPattern");
1551
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1552
+ await this.deleteKeys(keys);
1553
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1554
+ }
1555
+ async invalidateByPrefix(prefix) {
1556
+ await this.awaitStartup("invalidateByPrefix");
1557
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1558
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1158
1559
  await this.deleteKeys(keys);
1159
1560
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1160
1561
  }
@@ -1181,8 +1582,77 @@ var CacheStack = class extends import_node_events.EventEmitter {
1181
1582
  getHitRate() {
1182
1583
  return this.metricsCollector.hitRate();
1183
1584
  }
1184
- async exportState() {
1585
+ async healthCheck() {
1185
1586
  await this.startup;
1587
+ return Promise.all(
1588
+ this.layers.map(async (layer) => {
1589
+ const startedAt = performance.now();
1590
+ try {
1591
+ const healthy = layer.ping ? await layer.ping() : true;
1592
+ return {
1593
+ layer: layer.name,
1594
+ healthy,
1595
+ latencyMs: performance.now() - startedAt
1596
+ };
1597
+ } catch (error) {
1598
+ return {
1599
+ layer: layer.name,
1600
+ healthy: false,
1601
+ latencyMs: performance.now() - startedAt,
1602
+ error: this.formatError(error)
1603
+ };
1604
+ }
1605
+ })
1606
+ );
1607
+ }
1608
+ bumpGeneration(nextGeneration) {
1609
+ const current = this.currentGeneration ?? 0;
1610
+ this.currentGeneration = nextGeneration ?? current + 1;
1611
+ return this.currentGeneration;
1612
+ }
1613
+ /**
1614
+ * Returns detailed metadata about a single cache key: which layers contain it,
1615
+ * remaining fresh/stale/error TTLs, and associated tags.
1616
+ * Returns `null` if the key does not exist in any layer.
1617
+ */
1618
+ async inspect(key) {
1619
+ const userKey = this.validateCacheKey(key);
1620
+ const normalizedKey = this.qualifyKey(userKey);
1621
+ await this.awaitStartup("inspect");
1622
+ const foundInLayers = [];
1623
+ let freshTtlSeconds = null;
1624
+ let staleTtlSeconds = null;
1625
+ let errorTtlSeconds = null;
1626
+ let isStale = false;
1627
+ for (const layer of this.layers) {
1628
+ if (this.shouldSkipLayer(layer)) {
1629
+ continue;
1630
+ }
1631
+ const stored = await this.readLayerEntry(layer, normalizedKey);
1632
+ if (stored === null) {
1633
+ continue;
1634
+ }
1635
+ const resolved = resolveStoredValue(stored);
1636
+ if (resolved.state === "expired") {
1637
+ continue;
1638
+ }
1639
+ foundInLayers.push(layer.name);
1640
+ if (foundInLayers.length === 1 && resolved.envelope) {
1641
+ const now = Date.now();
1642
+ freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
1643
+ staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
1644
+ errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
1645
+ isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
1646
+ }
1647
+ }
1648
+ if (foundInLayers.length === 0) {
1649
+ return null;
1650
+ }
1651
+ const tags = await this.getTagsForKey(normalizedKey);
1652
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1653
+ }
1654
+ async exportState() {
1655
+ await this.awaitStartup("exportState");
1186
1656
  const exported = /* @__PURE__ */ new Map();
1187
1657
  for (const layer of this.layers) {
1188
1658
  if (!layer.keys) {
@@ -1190,15 +1660,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1190
1660
  }
1191
1661
  const keys = await layer.keys();
1192
1662
  for (const key of keys) {
1193
- if (exported.has(key)) {
1663
+ const exportedKey = this.stripQualifiedKey(key);
1664
+ if (exported.has(exportedKey)) {
1194
1665
  continue;
1195
1666
  }
1196
1667
  const stored = await this.readLayerEntry(layer, key);
1197
1668
  if (stored === null) {
1198
1669
  continue;
1199
1670
  }
1200
- exported.set(key, {
1201
- key,
1671
+ exported.set(exportedKey, {
1672
+ key: exportedKey,
1202
1673
  value: stored,
1203
1674
  ttl: remainingStoredTtlSeconds(stored)
1204
1675
  });
@@ -1207,20 +1678,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1207
1678
  return [...exported.values()];
1208
1679
  }
1209
1680
  async importState(entries) {
1210
- await this.startup;
1681
+ await this.awaitStartup("importState");
1211
1682
  await Promise.all(
1212
1683
  entries.map(async (entry) => {
1213
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1214
- await this.tagIndex.touch(entry.key);
1684
+ const qualifiedKey = this.qualifyKey(entry.key);
1685
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1686
+ await this.tagIndex.touch(qualifiedKey);
1215
1687
  })
1216
1688
  );
1217
1689
  }
1218
1690
  async persistToFile(filePath) {
1691
+ this.assertActive("persistToFile");
1219
1692
  const snapshot = await this.exportState();
1220
- await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1693
+ const { promises: fs } = await import("fs");
1694
+ await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1221
1695
  }
1222
1696
  async restoreFromFile(filePath) {
1223
- const raw = await import_node_fs.promises.readFile(filePath, "utf8");
1697
+ this.assertActive("restoreFromFile");
1698
+ const { promises: fs } = await import("fs");
1699
+ const raw = await fs.readFile(filePath, "utf8");
1224
1700
  let parsed;
1225
1701
  try {
1226
1702
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1243,7 +1719,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1243
1719
  this.disconnectPromise = (async () => {
1244
1720
  await this.startup;
1245
1721
  await this.unsubscribeInvalidation?.();
1722
+ await this.flushWriteBehindQueue();
1246
1723
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1724
+ if (this.writeBehindTimer) {
1725
+ clearInterval(this.writeBehindTimer);
1726
+ this.writeBehindTimer = void 0;
1727
+ }
1728
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1247
1729
  })();
1248
1730
  }
1249
1731
  await this.disconnectPromise;
@@ -1303,7 +1785,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1303
1785
  const fetchStart = Date.now();
1304
1786
  let fetched;
1305
1787
  try {
1306
- fetched = await fetcher();
1788
+ fetched = await this.fetchRateLimiter.schedule(
1789
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1790
+ fetcher
1791
+ );
1307
1792
  this.circuitBreakerManager.recordSuccess(key);
1308
1793
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1309
1794
  } catch (error) {
@@ -1317,6 +1802,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1317
1802
  await this.storeEntry(key, "empty", null, options);
1318
1803
  return null;
1319
1804
  }
1805
+ if (options?.shouldCache && !options.shouldCache(fetched)) {
1806
+ return fetched;
1807
+ }
1320
1808
  await this.storeEntry(key, "value", fetched, options);
1321
1809
  return fetched;
1322
1810
  }
@@ -1334,12 +1822,70 @@ var CacheStack = class extends import_node_events.EventEmitter {
1334
1822
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1335
1823
  }
1336
1824
  }
1825
+ async writeBatch(entries) {
1826
+ const now = Date.now();
1827
+ const entriesByLayer = /* @__PURE__ */ new Map();
1828
+ const immediateOperations = [];
1829
+ const deferredOperations = [];
1830
+ for (const entry of entries) {
1831
+ for (const layer of this.layers) {
1832
+ if (this.shouldSkipLayer(layer)) {
1833
+ continue;
1834
+ }
1835
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1836
+ const bucket = entriesByLayer.get(layer) ?? [];
1837
+ bucket.push(layerEntry);
1838
+ entriesByLayer.set(layer, bucket);
1839
+ }
1840
+ }
1841
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1842
+ const operation = async () => {
1843
+ try {
1844
+ if (layer.setMany) {
1845
+ await layer.setMany(layerEntries);
1846
+ return;
1847
+ }
1848
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1849
+ } catch (error) {
1850
+ await this.handleLayerFailure(layer, "write", error);
1851
+ }
1852
+ };
1853
+ if (this.shouldWriteBehind(layer)) {
1854
+ deferredOperations.push(operation);
1855
+ } else {
1856
+ immediateOperations.push(operation);
1857
+ }
1858
+ }
1859
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1860
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1861
+ for (const entry of entries) {
1862
+ if (entry.options?.tags) {
1863
+ await this.tagIndex.track(entry.key, entry.options.tags);
1864
+ } else {
1865
+ await this.tagIndex.touch(entry.key);
1866
+ }
1867
+ this.metricsCollector.increment("sets");
1868
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1869
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1870
+ }
1871
+ if (this.shouldBroadcastL1Invalidation()) {
1872
+ await this.publishInvalidation({
1873
+ scope: "keys",
1874
+ keys: entries.map((entry) => entry.key),
1875
+ sourceId: this.instanceId,
1876
+ operation: "write"
1877
+ });
1878
+ }
1879
+ }
1337
1880
  async readFromLayers(key, options, mode) {
1338
1881
  let sawRetainableValue = false;
1339
1882
  for (let index = 0; index < this.layers.length; index += 1) {
1340
1883
  const layer = this.layers[index];
1341
1884
  if (!layer) continue;
1885
+ const readStart = performance.now();
1342
1886
  const stored = await this.readLayerEntry(layer, key);
1887
+ const readDuration = performance.now() - readStart;
1888
+ this.metricsCollector.recordLatency(layer.name, readDuration);
1343
1889
  if (stored === null) {
1344
1890
  this.metricsCollector.incrementLayer("missesByLayer", layer.name);
1345
1891
  continue;
@@ -1414,33 +1960,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1414
1960
  }
1415
1961
  async writeAcrossLayers(key, kind, value, options) {
1416
1962
  const now = Date.now();
1417
- const operations = this.layers.map((layer) => async () => {
1418
- if (this.shouldSkipLayer(layer)) {
1419
- return;
1420
- }
1421
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1422
- const staleWhileRevalidate = this.resolveLayerSeconds(
1423
- layer.name,
1424
- options?.staleWhileRevalidate,
1425
- this.options.staleWhileRevalidate
1426
- );
1427
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1428
- const payload = createStoredValueEnvelope({
1429
- kind,
1430
- value,
1431
- freshTtlSeconds: freshTtl,
1432
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1433
- staleIfErrorSeconds: staleIfError,
1434
- now
1435
- });
1436
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1437
- try {
1438
- await layer.set(key, payload, ttl);
1439
- } catch (error) {
1440
- await this.handleLayerFailure(layer, "write", error);
1963
+ const immediateOperations = [];
1964
+ const deferredOperations = [];
1965
+ for (const layer of this.layers) {
1966
+ const operation = async () => {
1967
+ if (this.shouldSkipLayer(layer)) {
1968
+ return;
1969
+ }
1970
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1971
+ try {
1972
+ await layer.set(entry.key, entry.value, entry.ttl);
1973
+ } catch (error) {
1974
+ await this.handleLayerFailure(layer, "write", error);
1975
+ }
1976
+ };
1977
+ if (this.shouldWriteBehind(layer)) {
1978
+ deferredOperations.push(operation);
1979
+ } else {
1980
+ immediateOperations.push(operation);
1441
1981
  }
1442
- });
1443
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1982
+ }
1983
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1984
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1444
1985
  }
1445
1986
  async executeLayerOperations(operations, context) {
1446
1987
  if (this.options.writePolicy !== "best-effort") {
@@ -1464,8 +2005,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1464
2005
  );
1465
2006
  }
1466
2007
  }
1467
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1468
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
2008
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2009
+ return this.ttlResolver.resolveFreshTtl(
2010
+ key,
2011
+ layerName,
2012
+ kind,
2013
+ options,
2014
+ fallbackTtl,
2015
+ this.options.negativeTtl,
2016
+ void 0,
2017
+ value
2018
+ );
1469
2019
  }
1470
2020
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1471
2021
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1541,6 +2091,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
1541
2091
  }
1542
2092
  }
1543
2093
  }
2094
+ async getTagsForKey(key) {
2095
+ if (this.tagIndex.tagsForKey) {
2096
+ return this.tagIndex.tagsForKey(key);
2097
+ }
2098
+ return [];
2099
+ }
1544
2100
  formatError(error) {
1545
2101
  if (error instanceof Error) {
1546
2102
  return error.message;
@@ -1553,6 +2109,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
1553
2109
  shouldBroadcastL1Invalidation() {
1554
2110
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1555
2111
  }
2112
+ initializeWriteBehind(options) {
2113
+ if (this.options.writeStrategy !== "write-behind") {
2114
+ return;
2115
+ }
2116
+ const flushIntervalMs = options?.flushIntervalMs;
2117
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2118
+ return;
2119
+ }
2120
+ this.writeBehindTimer = setInterval(() => {
2121
+ void this.flushWriteBehindQueue();
2122
+ }, flushIntervalMs);
2123
+ this.writeBehindTimer.unref?.();
2124
+ }
2125
+ shouldWriteBehind(layer) {
2126
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2127
+ }
2128
+ async enqueueWriteBehind(operation) {
2129
+ this.writeBehindQueue.push(operation);
2130
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2131
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2132
+ if (this.writeBehindQueue.length >= batchSize) {
2133
+ await this.flushWriteBehindQueue();
2134
+ return;
2135
+ }
2136
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2137
+ await this.flushWriteBehindQueue();
2138
+ }
2139
+ }
2140
+ async flushWriteBehindQueue() {
2141
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2142
+ await this.writeBehindFlushPromise;
2143
+ return;
2144
+ }
2145
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2146
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2147
+ this.writeBehindFlushPromise = (async () => {
2148
+ await Promise.allSettled(batch.map((operation) => operation()));
2149
+ })();
2150
+ await this.writeBehindFlushPromise;
2151
+ this.writeBehindFlushPromise = void 0;
2152
+ if (this.writeBehindQueue.length > 0) {
2153
+ await this.flushWriteBehindQueue();
2154
+ }
2155
+ }
2156
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2157
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2158
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2159
+ layer.name,
2160
+ options?.staleWhileRevalidate,
2161
+ this.options.staleWhileRevalidate
2162
+ );
2163
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2164
+ const payload = createStoredValueEnvelope({
2165
+ kind,
2166
+ value,
2167
+ freshTtlSeconds: freshTtl,
2168
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2169
+ staleIfErrorSeconds: staleIfError,
2170
+ now
2171
+ });
2172
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2173
+ return {
2174
+ key,
2175
+ value: payload,
2176
+ ttl
2177
+ };
2178
+ }
2179
+ intersectKeys(groups) {
2180
+ if (groups.length === 0) {
2181
+ return [];
2182
+ }
2183
+ const [firstGroup, ...rest] = groups;
2184
+ if (!firstGroup) {
2185
+ return [];
2186
+ }
2187
+ const restSets = rest.map((group) => new Set(group));
2188
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2189
+ }
2190
+ qualifyKey(key) {
2191
+ const prefix = this.generationPrefix();
2192
+ return prefix ? `${prefix}${key}` : key;
2193
+ }
2194
+ qualifyPattern(pattern) {
2195
+ const prefix = this.generationPrefix();
2196
+ return prefix ? `${prefix}${pattern}` : pattern;
2197
+ }
2198
+ stripQualifiedKey(key) {
2199
+ const prefix = this.generationPrefix();
2200
+ if (!prefix || !key.startsWith(prefix)) {
2201
+ return key;
2202
+ }
2203
+ return key.slice(prefix.length);
2204
+ }
2205
+ generationPrefix() {
2206
+ if (this.currentGeneration === void 0) {
2207
+ return "";
2208
+ }
2209
+ return `v${this.currentGeneration}:`;
2210
+ }
1556
2211
  async deleteKeysFromLayers(layers, keys) {
1557
2212
  await Promise.all(
1558
2213
  layers.map(async (layer) => {
@@ -1596,6 +2251,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1596
2251
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1597
2252
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1598
2253
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2254
+ if (this.options.generation !== void 0) {
2255
+ this.validateNonNegativeNumber("generation", this.options.generation);
2256
+ }
1599
2257
  }
1600
2258
  validateWriteOptions(options) {
1601
2259
  if (!options) {
@@ -1607,6 +2265,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1607
2265
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1608
2266
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1609
2267
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2268
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1610
2269
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1611
2270
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1612
2271
  }
@@ -1650,6 +2309,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1650
2309
  }
1651
2310
  return key;
1652
2311
  }
2312
+ validateTtlPolicy(name, policy) {
2313
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2314
+ return;
2315
+ }
2316
+ if ("alignTo" in policy) {
2317
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2318
+ return;
2319
+ }
2320
+ throw new Error(`${name} is invalid.`);
2321
+ }
2322
+ assertActive(operation) {
2323
+ if (this.isDisconnecting) {
2324
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2325
+ }
2326
+ }
2327
+ async awaitStartup(operation) {
2328
+ this.assertActive(operation);
2329
+ await this.startup;
2330
+ this.assertActive(operation);
2331
+ }
1653
2332
  serializeOptions(options) {
1654
2333
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1655
2334
  }
@@ -1755,6 +2434,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1755
2434
  return value;
1756
2435
  }
1757
2436
  };
2437
+ function createInstanceId() {
2438
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2439
+ }
1758
2440
 
1759
2441
  // src/module.ts
1760
2442
  var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
@@ -1771,6 +2453,22 @@ var CacheStackModule = class {
1771
2453
  exports: [provider]
1772
2454
  };
1773
2455
  }
2456
+ static forRootAsync(options) {
2457
+ const provider = {
2458
+ provide: CACHE_STACK,
2459
+ inject: options.inject ?? [],
2460
+ useFactory: async (...args) => {
2461
+ const resolved = await options.useFactory(...args);
2462
+ return new CacheStack(resolved.layers, resolved.bridgeOptions);
2463
+ }
2464
+ };
2465
+ return {
2466
+ global: true,
2467
+ module: CacheStackModule,
2468
+ providers: [provider],
2469
+ exports: [provider]
2470
+ };
2471
+ }
1774
2472
  };
1775
2473
  CacheStackModule = __decorateClass([
1776
2474
  (0, import_common.Global)(),