layercache 1.2.0 → 1.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,9 +82,182 @@ 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
263
  var CacheNamespace = class _CacheNamespace {
@@ -84,57 +267,69 @@ var CacheNamespace = class _CacheNamespace {
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));
92
277
  }
93
278
  /**
94
279
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
95
280
  */
96
281
  async getOrThrow(key, fetcher, options) {
97
- return this.cache.getOrThrow(this.qualify(key), fetcher, options);
282
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
98
283
  }
99
284
  async has(key) {
100
- return this.cache.has(this.qualify(key));
285
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
101
286
  }
102
287
  async ttl(key) {
103
- return this.cache.ttl(this.qualify(key));
288
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
104
289
  }
105
290
  async set(key, value, options) {
106
- await this.cache.set(this.qualify(key), value, options);
291
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
107
292
  }
108
293
  async delete(key) {
109
- await this.cache.delete(this.qualify(key));
294
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
110
295
  }
111
296
  async mdelete(keys) {
112
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
297
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
113
298
  }
114
299
  async clear() {
115
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
300
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
116
301
  }
117
302
  async mget(entries) {
118
- return this.cache.mget(
119
- entries.map((entry) => ({
120
- ...entry,
121
- key: this.qualify(entry.key)
122
- }))
303
+ return this.trackMetrics(
304
+ () => this.cache.mget(
305
+ entries.map((entry) => ({
306
+ ...entry,
307
+ key: this.qualify(entry.key)
308
+ }))
309
+ )
123
310
  );
124
311
  }
125
312
  async mset(entries) {
126
- await this.cache.mset(
127
- entries.map((entry) => ({
128
- ...entry,
129
- key: this.qualify(entry.key)
130
- }))
313
+ await this.trackMetrics(
314
+ () => this.cache.mset(
315
+ entries.map((entry) => ({
316
+ ...entry,
317
+ key: this.qualify(entry.key)
318
+ }))
319
+ )
131
320
  );
132
321
  }
133
322
  async invalidateByTag(tag) {
134
- 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));
135
327
  }
136
328
  async invalidateByPattern(pattern) {
137
- 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)));
138
333
  }
139
334
  /**
140
335
  * Returns detailed metadata about a single cache key within this namespace.
@@ -155,10 +350,19 @@ var CacheNamespace = class _CacheNamespace {
155
350
  );
156
351
  }
157
352
  getMetrics() {
158
- return this.cache.getMetrics();
353
+ return cloneMetrics(this.metrics);
159
354
  }
160
355
  getHitRate() {
161
- 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 };
162
366
  }
163
367
  /**
164
368
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -175,7 +379,130 @@ var CacheNamespace = class _CacheNamespace {
175
379
  qualify(key) {
176
380
  return `${this.prefix}:${key}`;
177
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
+ }
178
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
+ }
179
506
 
180
507
  // ../../src/internal/CircuitBreakerManager.ts
181
508
  var CircuitBreakerManager = class {
@@ -269,6 +596,148 @@ var CircuitBreakerManager = class {
269
596
  }
270
597
  };
271
598
 
599
+ // ../../src/internal/FetchRateLimiter.ts
600
+ var FetchRateLimiter = class {
601
+ queue = [];
602
+ buckets = /* @__PURE__ */ new Map();
603
+ fetcherBuckets = /* @__PURE__ */ new WeakMap();
604
+ nextFetcherBucketId = 0;
605
+ drainTimer;
606
+ async schedule(options, context, task) {
607
+ if (!options) {
608
+ return task();
609
+ }
610
+ const normalized = this.normalize(options);
611
+ if (!normalized) {
612
+ return task();
613
+ }
614
+ return new Promise((resolve, reject) => {
615
+ this.queue.push({
616
+ bucketKey: this.resolveBucketKey(normalized, context),
617
+ options: normalized,
618
+ task,
619
+ resolve,
620
+ reject
621
+ });
622
+ this.drain();
623
+ });
624
+ }
625
+ normalize(options) {
626
+ const maxConcurrent = options.maxConcurrent;
627
+ const intervalMs = options.intervalMs;
628
+ const maxPerInterval = options.maxPerInterval;
629
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
630
+ return void 0;
631
+ }
632
+ return {
633
+ maxConcurrent,
634
+ intervalMs,
635
+ maxPerInterval,
636
+ scope: options.scope ?? "global",
637
+ bucketKey: options.bucketKey
638
+ };
639
+ }
640
+ resolveBucketKey(options, context) {
641
+ if (options.bucketKey) {
642
+ return `custom:${options.bucketKey}`;
643
+ }
644
+ if (options.scope === "key") {
645
+ return `key:${context.key}`;
646
+ }
647
+ if (options.scope === "fetcher") {
648
+ const existing = this.fetcherBuckets.get(context.fetcher);
649
+ if (existing) {
650
+ return existing;
651
+ }
652
+ const bucket = `fetcher:${this.nextFetcherBucketId}`;
653
+ this.nextFetcherBucketId += 1;
654
+ this.fetcherBuckets.set(context.fetcher, bucket);
655
+ return bucket;
656
+ }
657
+ return "global";
658
+ }
659
+ drain() {
660
+ if (this.drainTimer) {
661
+ clearTimeout(this.drainTimer);
662
+ this.drainTimer = void 0;
663
+ }
664
+ while (this.queue.length > 0) {
665
+ let nextIndex = -1;
666
+ let nextWaitMs = Number.POSITIVE_INFINITY;
667
+ for (let index = 0; index < this.queue.length; index += 1) {
668
+ const next2 = this.queue[index];
669
+ if (!next2) {
670
+ continue;
671
+ }
672
+ const waitMs = this.waitTime(next2.bucketKey, next2.options);
673
+ if (waitMs <= 0) {
674
+ nextIndex = index;
675
+ break;
676
+ }
677
+ nextWaitMs = Math.min(nextWaitMs, waitMs);
678
+ }
679
+ if (nextIndex < 0) {
680
+ if (Number.isFinite(nextWaitMs)) {
681
+ this.drainTimer = setTimeout(() => {
682
+ this.drainTimer = void 0;
683
+ this.drain();
684
+ }, nextWaitMs);
685
+ this.drainTimer.unref?.();
686
+ }
687
+ return;
688
+ }
689
+ const next = this.queue.splice(nextIndex, 1)[0];
690
+ if (!next) {
691
+ return;
692
+ }
693
+ const bucket = this.bucketState(next.bucketKey);
694
+ bucket.active += 1;
695
+ bucket.startedAt.push(Date.now());
696
+ void next.task().then(next.resolve, next.reject).finally(() => {
697
+ bucket.active -= 1;
698
+ this.drain();
699
+ });
700
+ }
701
+ }
702
+ waitTime(bucketKey, options) {
703
+ const bucket = this.bucketState(bucketKey);
704
+ const now = Date.now();
705
+ if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
706
+ return 1;
707
+ }
708
+ if (!options.intervalMs || !options.maxPerInterval) {
709
+ return 0;
710
+ }
711
+ this.prune(bucket, now, options.intervalMs);
712
+ if (bucket.startedAt.length < options.maxPerInterval) {
713
+ return 0;
714
+ }
715
+ const oldest = bucket.startedAt[0];
716
+ if (!oldest) {
717
+ return 0;
718
+ }
719
+ return Math.max(1, options.intervalMs - (now - oldest));
720
+ }
721
+ prune(bucket, now, intervalMs) {
722
+ while (bucket.startedAt.length > 0) {
723
+ const startedAt = bucket.startedAt[0];
724
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
725
+ break;
726
+ }
727
+ bucket.startedAt.shift();
728
+ }
729
+ }
730
+ bucketState(bucketKey) {
731
+ const existing = this.buckets.get(bucketKey);
732
+ if (existing) {
733
+ return existing;
734
+ }
735
+ const bucket = { active: 0, startedAt: [] };
736
+ this.buckets.set(bucketKey, bucket);
737
+ return bucket;
738
+ }
739
+ };
740
+
272
741
  // ../../src/internal/MetricsCollector.ts
273
742
  var MetricsCollector = class {
274
743
  data = this.empty();
@@ -465,13 +934,14 @@ var TtlResolver = class {
465
934
  clearProfiles() {
466
935
  this.accessProfiles.clear();
467
936
  }
468
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
937
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
938
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
469
939
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
470
940
  layerName,
471
941
  options?.negativeTtl,
472
942
  globalNegativeTtl,
473
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
474
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
943
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
944
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
475
945
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
476
946
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
477
947
  return this.applyJitter(adaptiveTtl, jitter);
@@ -510,6 +980,29 @@ var TtlResolver = class {
510
980
  const delta = (Math.random() * 2 - 1) * jitter;
511
981
  return Math.max(1, Math.round(ttl + delta));
512
982
  }
983
+ resolvePolicyTtl(key, value, policy) {
984
+ if (!policy) {
985
+ return void 0;
986
+ }
987
+ if (typeof policy === "function") {
988
+ return policy({ key, value });
989
+ }
990
+ const now = /* @__PURE__ */ new Date();
991
+ if (policy === "until-midnight") {
992
+ const nextMidnight = new Date(now);
993
+ nextMidnight.setHours(24, 0, 0, 0);
994
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
995
+ }
996
+ if (policy === "next-hour") {
997
+ const nextHour = new Date(now);
998
+ nextHour.setMinutes(60, 0, 0);
999
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
1000
+ }
1001
+ const alignToSeconds = policy.alignTo;
1002
+ const currentSeconds = Math.floor(Date.now() / 1e3);
1003
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
1004
+ return Math.max(1, nextBoundary - currentSeconds);
1005
+ }
513
1006
  readLayerNumber(layerName, value) {
514
1007
  if (typeof value === "number") {
515
1008
  return value;
@@ -530,300 +1023,140 @@ var TtlResolver = class {
530
1023
  removed += 1;
531
1024
  }
532
1025
  }
533
- };
534
-
535
- // ../../src/invalidation/PatternMatcher.ts
536
- var PatternMatcher = class _PatternMatcher {
537
- /**
538
- * Tests whether a glob-style pattern matches a value.
539
- * Supports `*` (any sequence of characters) and `?` (any single character).
540
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
541
- */
542
- static matches(pattern, value) {
543
- return _PatternMatcher.matchLinear(pattern, value);
544
- }
545
- /**
546
- * Linear-time glob matching using dynamic programming.
547
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
548
- */
549
- static matchLinear(pattern, value) {
550
- const m = pattern.length;
551
- const n = value.length;
552
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
553
- dp[0][0] = true;
554
- for (let i = 1; i <= m; i++) {
555
- if (pattern[i - 1] === "*") {
556
- dp[i][0] = dp[i - 1]?.[0];
557
- }
558
- }
559
- for (let i = 1; i <= m; i++) {
560
- for (let j = 1; j <= n; j++) {
561
- const pc = pattern[i - 1];
562
- if (pc === "*") {
563
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
564
- } else if (pc === "?" || pc === value[j - 1]) {
565
- dp[i][j] = dp[i - 1]?.[j - 1];
566
- }
567
- }
568
- }
569
- return dp[m]?.[n];
570
- }
571
- };
572
-
573
- // ../../src/invalidation/TagIndex.ts
574
- var TagIndex = class {
575
- tagToKeys = /* @__PURE__ */ new Map();
576
- keyToTags = /* @__PURE__ */ new Map();
577
- knownKeys = /* @__PURE__ */ new Set();
578
- maxKnownKeys;
579
- constructor(options = {}) {
580
- this.maxKnownKeys = options.maxKnownKeys;
581
- }
582
- async touch(key) {
583
- this.knownKeys.add(key);
584
- this.pruneKnownKeysIfNeeded();
585
- }
586
- async track(key, tags) {
587
- this.knownKeys.add(key);
588
- this.pruneKnownKeysIfNeeded();
589
- if (tags.length === 0) {
590
- return;
591
- }
592
- const existingTags = this.keyToTags.get(key);
593
- if (existingTags) {
594
- for (const tag of existingTags) {
595
- this.tagToKeys.get(tag)?.delete(key);
596
- }
597
- }
598
- const tagSet = new Set(tags);
599
- this.keyToTags.set(key, tagSet);
600
- for (const tag of tagSet) {
601
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
602
- keys.add(key);
603
- this.tagToKeys.set(tag, keys);
604
- }
605
- }
606
- async remove(key) {
607
- this.knownKeys.delete(key);
608
- const tags = this.keyToTags.get(key);
609
- if (!tags) {
610
- return;
611
- }
612
- for (const tag of tags) {
613
- const keys = this.tagToKeys.get(tag);
614
- if (!keys) {
615
- continue;
616
- }
617
- keys.delete(key);
618
- if (keys.size === 0) {
619
- this.tagToKeys.delete(tag);
620
- }
621
- }
622
- this.keyToTags.delete(key);
623
- }
624
- async keysForTag(tag) {
625
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
626
- }
627
- async tagsForKey(key) {
628
- return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
629
- }
630
- async matchPattern(pattern) {
631
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
632
- }
633
- async clear() {
634
- this.tagToKeys.clear();
635
- this.keyToTags.clear();
636
- this.knownKeys.clear();
637
- }
638
- pruneKnownKeysIfNeeded() {
639
- if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
640
- return;
641
- }
642
- const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
643
- let removed = 0;
644
- for (const key of this.knownKeys) {
645
- if (removed >= toRemove) {
646
- break;
647
- }
648
- this.knownKeys.delete(key);
649
- this.keyToTags.delete(key);
650
- removed += 1;
651
- }
652
- }
653
- };
654
-
655
- // ../../node_modules/async-mutex/index.mjs
656
- var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
657
- var E_ALREADY_LOCKED = new Error("mutex already locked");
658
- var E_CANCELED = new Error("request for lock canceled");
659
- var __awaiter$2 = function(thisArg, _arguments, P, generator) {
660
- function adopt(value) {
661
- return value instanceof P ? value : new P(function(resolve) {
662
- resolve(value);
663
- });
664
- }
665
- return new (P || (P = Promise))(function(resolve, reject) {
666
- function fulfilled(value) {
667
- try {
668
- step(generator.next(value));
669
- } catch (e) {
670
- reject(e);
671
- }
672
- }
673
- function rejected(value) {
674
- try {
675
- step(generator["throw"](value));
676
- } catch (e) {
677
- reject(e);
678
- }
679
- }
680
- function step(result) {
681
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
682
- }
683
- step((generator = generator.apply(thisArg, _arguments || [])).next());
684
- });
685
- };
686
- var Semaphore = class {
687
- constructor(_value, _cancelError = E_CANCELED) {
688
- this._value = _value;
689
- this._cancelError = _cancelError;
690
- this._weightedQueues = [];
691
- this._weightedWaiters = [];
692
- }
693
- acquire(weight = 1) {
694
- if (weight <= 0)
695
- throw new Error(`invalid weight ${weight}: must be positive`);
696
- return new Promise((resolve, reject) => {
697
- if (!this._weightedQueues[weight - 1])
698
- this._weightedQueues[weight - 1] = [];
699
- this._weightedQueues[weight - 1].push({ resolve, reject });
700
- this._dispatch();
701
- });
702
- }
703
- runExclusive(callback, weight = 1) {
704
- return __awaiter$2(this, void 0, void 0, function* () {
705
- const [value, release] = yield this.acquire(weight);
706
- try {
707
- return yield callback(value);
708
- } finally {
709
- release();
710
- }
711
- });
712
- }
713
- waitForUnlock(weight = 1) {
714
- if (weight <= 0)
715
- throw new Error(`invalid weight ${weight}: must be positive`);
716
- return new Promise((resolve) => {
717
- if (!this._weightedWaiters[weight - 1])
718
- this._weightedWaiters[weight - 1] = [];
719
- this._weightedWaiters[weight - 1].push(resolve);
720
- this._dispatch();
721
- });
722
- }
723
- isLocked() {
724
- return this._value <= 0;
725
- }
726
- getValue() {
727
- return this._value;
728
- }
729
- setValue(value) {
730
- this._value = value;
731
- this._dispatch();
732
- }
733
- release(weight = 1) {
734
- if (weight <= 0)
735
- throw new Error(`invalid weight ${weight}: must be positive`);
736
- this._value += weight;
737
- this._dispatch();
738
- }
739
- cancel() {
740
- this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
741
- this._weightedQueues = [];
742
- }
743
- _dispatch() {
744
- var _a;
745
- for (let weight = this._value; weight > 0; weight--) {
746
- const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
747
- if (!queueEntry)
748
- continue;
749
- const previousValue = this._value;
750
- const previousWeight = weight;
751
- this._value -= weight;
752
- weight = this._value + 1;
753
- queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
754
- }
755
- this._drainUnlockWaiters();
756
- }
757
- _newReleaser(weight) {
758
- let called = false;
759
- return () => {
760
- if (called)
761
- return;
762
- called = true;
763
- this.release(weight);
764
- };
1026
+ };
1027
+
1028
+ // ../../src/invalidation/PatternMatcher.ts
1029
+ var PatternMatcher = class _PatternMatcher {
1030
+ /**
1031
+ * Tests whether a glob-style pattern matches a value.
1032
+ * Supports `*` (any sequence of characters) and `?` (any single character).
1033
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
1034
+ * quadratic memory usage on long patterns/keys.
1035
+ */
1036
+ static matches(pattern, value) {
1037
+ return _PatternMatcher.matchLinear(pattern, value);
765
1038
  }
766
- _drainUnlockWaiters() {
767
- for (let weight = this._value; weight > 0; weight--) {
768
- if (!this._weightedWaiters[weight - 1])
1039
+ /**
1040
+ * Linear-time glob matching with O(1) extra memory.
1041
+ */
1042
+ static matchLinear(pattern, value) {
1043
+ let patternIndex = 0;
1044
+ let valueIndex = 0;
1045
+ let starIndex = -1;
1046
+ let backtrackValueIndex = 0;
1047
+ while (valueIndex < value.length) {
1048
+ const patternChar = pattern[patternIndex];
1049
+ const valueChar = value[valueIndex];
1050
+ if (patternChar === "*" && patternIndex < pattern.length) {
1051
+ starIndex = patternIndex;
1052
+ patternIndex += 1;
1053
+ backtrackValueIndex = valueIndex;
769
1054
  continue;
770
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
771
- this._weightedWaiters[weight - 1] = [];
1055
+ }
1056
+ if (patternChar === "?" || patternChar === valueChar) {
1057
+ patternIndex += 1;
1058
+ valueIndex += 1;
1059
+ continue;
1060
+ }
1061
+ if (starIndex !== -1) {
1062
+ patternIndex = starIndex + 1;
1063
+ backtrackValueIndex += 1;
1064
+ valueIndex = backtrackValueIndex;
1065
+ continue;
1066
+ }
1067
+ return false;
772
1068
  }
1069
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
1070
+ patternIndex += 1;
1071
+ }
1072
+ return patternIndex === pattern.length;
773
1073
  }
774
1074
  };
775
- var __awaiter$1 = function(thisArg, _arguments, P, generator) {
776
- function adopt(value) {
777
- return value instanceof P ? value : new P(function(resolve) {
778
- resolve(value);
779
- });
1075
+
1076
+ // ../../src/invalidation/TagIndex.ts
1077
+ var TagIndex = class {
1078
+ tagToKeys = /* @__PURE__ */ new Map();
1079
+ keyToTags = /* @__PURE__ */ new Map();
1080
+ knownKeys = /* @__PURE__ */ new Set();
1081
+ maxKnownKeys;
1082
+ constructor(options = {}) {
1083
+ this.maxKnownKeys = options.maxKnownKeys;
780
1084
  }
781
- return new (P || (P = Promise))(function(resolve, reject) {
782
- function fulfilled(value) {
783
- try {
784
- step(generator.next(value));
785
- } catch (e) {
786
- reject(e);
787
- }
1085
+ async touch(key) {
1086
+ this.knownKeys.add(key);
1087
+ this.pruneKnownKeysIfNeeded();
1088
+ }
1089
+ async track(key, tags) {
1090
+ this.knownKeys.add(key);
1091
+ this.pruneKnownKeysIfNeeded();
1092
+ if (tags.length === 0) {
1093
+ return;
788
1094
  }
789
- function rejected(value) {
790
- try {
791
- step(generator["throw"](value));
792
- } catch (e) {
793
- reject(e);
1095
+ const existingTags = this.keyToTags.get(key);
1096
+ if (existingTags) {
1097
+ for (const tag of existingTags) {
1098
+ this.tagToKeys.get(tag)?.delete(key);
794
1099
  }
795
1100
  }
796
- function step(result) {
797
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1101
+ const tagSet = new Set(tags);
1102
+ this.keyToTags.set(key, tagSet);
1103
+ for (const tag of tagSet) {
1104
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1105
+ keys.add(key);
1106
+ this.tagToKeys.set(tag, keys);
798
1107
  }
799
- step((generator = generator.apply(thisArg, _arguments || [])).next());
800
- });
801
- };
802
- var Mutex = class {
803
- constructor(cancelError) {
804
- this._semaphore = new Semaphore(1, cancelError);
805
1108
  }
806
- acquire() {
807
- return __awaiter$1(this, void 0, void 0, function* () {
808
- const [, releaser] = yield this._semaphore.acquire();
809
- return releaser;
810
- });
1109
+ async remove(key) {
1110
+ this.removeKey(key);
811
1111
  }
812
- runExclusive(callback) {
813
- return this._semaphore.runExclusive(() => callback());
1112
+ async keysForTag(tag) {
1113
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
814
1114
  }
815
- isLocked() {
816
- return this._semaphore.isLocked();
1115
+ async keysForPrefix(prefix) {
1116
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
817
1117
  }
818
- waitForUnlock() {
819
- return this._semaphore.waitForUnlock();
1118
+ async tagsForKey(key) {
1119
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
820
1120
  }
821
- release() {
822
- if (this._semaphore.isLocked())
823
- this._semaphore.release();
1121
+ async matchPattern(pattern) {
1122
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
824
1123
  }
825
- cancel() {
826
- return this._semaphore.cancel();
1124
+ async clear() {
1125
+ this.tagToKeys.clear();
1126
+ this.keyToTags.clear();
1127
+ this.knownKeys.clear();
1128
+ }
1129
+ pruneKnownKeysIfNeeded() {
1130
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1131
+ return;
1132
+ }
1133
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1134
+ let removed = 0;
1135
+ for (const key of this.knownKeys) {
1136
+ if (removed >= toRemove) {
1137
+ break;
1138
+ }
1139
+ this.removeKey(key);
1140
+ removed += 1;
1141
+ }
1142
+ }
1143
+ removeKey(key) {
1144
+ this.knownKeys.delete(key);
1145
+ const tags = this.keyToTags.get(key);
1146
+ if (!tags) {
1147
+ return;
1148
+ }
1149
+ for (const tag of tags) {
1150
+ const keys = this.tagToKeys.get(tag);
1151
+ if (!keys) {
1152
+ continue;
1153
+ }
1154
+ keys.delete(key);
1155
+ if (keys.size === 0) {
1156
+ this.tagToKeys.delete(tag);
1157
+ }
1158
+ }
1159
+ this.keyToTags.delete(key);
827
1160
  }
828
1161
  };
829
1162
 
@@ -905,6 +1238,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
905
1238
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
906
1239
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
907
1240
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1241
+ this.currentGeneration = options.generation;
908
1242
  if (options.publishSetInvalidation !== void 0) {
909
1243
  console.warn(
910
1244
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -913,21 +1247,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
913
1247
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
914
1248
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
915
1249
  this.tagIndex = options.tagIndex ?? new TagIndex();
1250
+ this.initializeWriteBehind(options.writeBehind);
916
1251
  this.startup = this.initialize();
917
1252
  }
918
1253
  layers;
919
1254
  options;
920
1255
  stampedeGuard = new StampedeGuard();
921
1256
  metricsCollector = new MetricsCollector();
922
- instanceId = (0, import_node_crypto.randomUUID)();
1257
+ instanceId = createInstanceId();
923
1258
  startup;
924
1259
  unsubscribeInvalidation;
925
1260
  logger;
926
1261
  tagIndex;
1262
+ fetchRateLimiter = new FetchRateLimiter();
927
1263
  backgroundRefreshes = /* @__PURE__ */ new Map();
928
1264
  layerDegradedUntil = /* @__PURE__ */ new Map();
929
1265
  ttlResolver;
930
1266
  circuitBreakerManager;
1267
+ currentGeneration;
1268
+ writeBehindQueue = [];
1269
+ writeBehindTimer;
1270
+ writeBehindFlushPromise;
931
1271
  isDisconnecting = false;
932
1272
  disconnectPromise;
933
1273
  /**
@@ -937,9 +1277,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
937
1277
  * and no `fetcher` is provided.
938
1278
  */
939
1279
  async get(key, fetcher, options) {
940
- const normalizedKey = this.validateCacheKey(key);
1280
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
941
1281
  this.validateWriteOptions(options);
942
- await this.startup;
1282
+ await this.awaitStartup("get");
943
1283
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
944
1284
  if (hit.found) {
945
1285
  this.ttlResolver.recordAccess(normalizedKey);
@@ -1004,8 +1344,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1004
1344
  * Returns true if the given key exists and is not expired in any layer.
1005
1345
  */
1006
1346
  async has(key) {
1007
- const normalizedKey = this.validateCacheKey(key);
1008
- await this.startup;
1347
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1348
+ await this.awaitStartup("has");
1009
1349
  for (const layer of this.layers) {
1010
1350
  if (this.shouldSkipLayer(layer)) {
1011
1351
  continue;
@@ -1035,8 +1375,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1035
1375
  * that has it, or null if the key is not found / has no TTL.
1036
1376
  */
1037
1377
  async ttl(key) {
1038
- const normalizedKey = this.validateCacheKey(key);
1039
- await this.startup;
1378
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1379
+ await this.awaitStartup("ttl");
1040
1380
  for (const layer of this.layers) {
1041
1381
  if (this.shouldSkipLayer(layer)) {
1042
1382
  continue;
@@ -1057,17 +1397,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1057
1397
  * Stores a value in all cache layers. Overwrites any existing value.
1058
1398
  */
1059
1399
  async set(key, value, options) {
1060
- const normalizedKey = this.validateCacheKey(key);
1400
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1061
1401
  this.validateWriteOptions(options);
1062
- await this.startup;
1402
+ await this.awaitStartup("set");
1063
1403
  await this.storeEntry(normalizedKey, "value", value, options);
1064
1404
  }
1065
1405
  /**
1066
1406
  * Deletes the key from all layers and publishes an invalidation message.
1067
1407
  */
1068
1408
  async delete(key) {
1069
- const normalizedKey = this.validateCacheKey(key);
1070
- await this.startup;
1409
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1410
+ await this.awaitStartup("delete");
1071
1411
  await this.deleteKeys([normalizedKey]);
1072
1412
  await this.publishInvalidation({
1073
1413
  scope: "key",
@@ -1077,7 +1417,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1077
1417
  });
1078
1418
  }
1079
1419
  async clear() {
1080
- await this.startup;
1420
+ await this.awaitStartup("clear");
1081
1421
  await Promise.all(this.layers.map((layer) => layer.clear()));
1082
1422
  await this.tagIndex.clear();
1083
1423
  this.ttlResolver.clearProfiles();
@@ -1093,23 +1433,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1093
1433
  if (keys.length === 0) {
1094
1434
  return;
1095
1435
  }
1096
- await this.startup;
1436
+ await this.awaitStartup("mdelete");
1097
1437
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1098
- await this.deleteKeys(normalizedKeys);
1438
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1439
+ await this.deleteKeys(cacheKeys);
1099
1440
  await this.publishInvalidation({
1100
1441
  scope: "keys",
1101
- keys: normalizedKeys,
1442
+ keys: cacheKeys,
1102
1443
  sourceId: this.instanceId,
1103
1444
  operation: "delete"
1104
1445
  });
1105
1446
  }
1106
1447
  async mget(entries) {
1448
+ this.assertActive("mget");
1107
1449
  if (entries.length === 0) {
1108
1450
  return [];
1109
1451
  }
1110
1452
  const normalizedEntries = entries.map((entry) => ({
1111
1453
  ...entry,
1112
- key: this.validateCacheKey(entry.key)
1454
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1113
1455
  }));
1114
1456
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1115
1457
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1135,7 +1477,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1135
1477
  })
1136
1478
  );
1137
1479
  }
1138
- await this.startup;
1480
+ await this.awaitStartup("mget");
1139
1481
  const pending = /* @__PURE__ */ new Set();
1140
1482
  const indexesByKey = /* @__PURE__ */ new Map();
1141
1483
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1183,14 +1525,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1183
1525
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1184
1526
  }
1185
1527
  async mset(entries) {
1528
+ this.assertActive("mset");
1186
1529
  const normalizedEntries = entries.map((entry) => ({
1187
1530
  ...entry,
1188
- key: this.validateCacheKey(entry.key)
1531
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1189
1532
  }));
1190
1533
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1191
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1534
+ await this.awaitStartup("mset");
1535
+ await this.writeBatch(normalizedEntries);
1192
1536
  }
1193
1537
  async warm(entries, options = {}) {
1538
+ this.assertActive("warm");
1194
1539
  const concurrency = Math.max(1, options.concurrency ?? 4);
1195
1540
  const total = entries.length;
1196
1541
  let completed = 0;
@@ -1239,14 +1584,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
1239
1584
  return new CacheNamespace(this, prefix);
1240
1585
  }
1241
1586
  async invalidateByTag(tag) {
1242
- await this.startup;
1587
+ await this.awaitStartup("invalidateByTag");
1243
1588
  const keys = await this.tagIndex.keysForTag(tag);
1244
1589
  await this.deleteKeys(keys);
1245
1590
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1246
1591
  }
1592
+ async invalidateByTags(tags, mode = "any") {
1593
+ if (tags.length === 0) {
1594
+ return;
1595
+ }
1596
+ await this.awaitStartup("invalidateByTags");
1597
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1598
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1599
+ await this.deleteKeys(keys);
1600
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1601
+ }
1247
1602
  async invalidateByPattern(pattern) {
1248
- await this.startup;
1249
- const keys = await this.tagIndex.matchPattern(pattern);
1603
+ await this.awaitStartup("invalidateByPattern");
1604
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1605
+ await this.deleteKeys(keys);
1606
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1607
+ }
1608
+ async invalidateByPrefix(prefix) {
1609
+ await this.awaitStartup("invalidateByPrefix");
1610
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1611
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1250
1612
  await this.deleteKeys(keys);
1251
1613
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1252
1614
  }
@@ -1273,14 +1635,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
1273
1635
  getHitRate() {
1274
1636
  return this.metricsCollector.hitRate();
1275
1637
  }
1638
+ async healthCheck() {
1639
+ await this.startup;
1640
+ return Promise.all(
1641
+ this.layers.map(async (layer) => {
1642
+ const startedAt = performance.now();
1643
+ try {
1644
+ const healthy = layer.ping ? await layer.ping() : true;
1645
+ return {
1646
+ layer: layer.name,
1647
+ healthy,
1648
+ latencyMs: performance.now() - startedAt
1649
+ };
1650
+ } catch (error) {
1651
+ return {
1652
+ layer: layer.name,
1653
+ healthy: false,
1654
+ latencyMs: performance.now() - startedAt,
1655
+ error: this.formatError(error)
1656
+ };
1657
+ }
1658
+ })
1659
+ );
1660
+ }
1661
+ bumpGeneration(nextGeneration) {
1662
+ const current = this.currentGeneration ?? 0;
1663
+ this.currentGeneration = nextGeneration ?? current + 1;
1664
+ return this.currentGeneration;
1665
+ }
1276
1666
  /**
1277
1667
  * Returns detailed metadata about a single cache key: which layers contain it,
1278
1668
  * remaining fresh/stale/error TTLs, and associated tags.
1279
1669
  * Returns `null` if the key does not exist in any layer.
1280
1670
  */
1281
1671
  async inspect(key) {
1282
- const normalizedKey = this.validateCacheKey(key);
1283
- await this.startup;
1672
+ const userKey = this.validateCacheKey(key);
1673
+ const normalizedKey = this.qualifyKey(userKey);
1674
+ await this.awaitStartup("inspect");
1284
1675
  const foundInLayers = [];
1285
1676
  let freshTtlSeconds = null;
1286
1677
  let staleTtlSeconds = null;
@@ -1311,10 +1702,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1311
1702
  return null;
1312
1703
  }
1313
1704
  const tags = await this.getTagsForKey(normalizedKey);
1314
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1705
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1315
1706
  }
1316
1707
  async exportState() {
1317
- await this.startup;
1708
+ await this.awaitStartup("exportState");
1318
1709
  const exported = /* @__PURE__ */ new Map();
1319
1710
  for (const layer of this.layers) {
1320
1711
  if (!layer.keys) {
@@ -1322,15 +1713,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1322
1713
  }
1323
1714
  const keys = await layer.keys();
1324
1715
  for (const key of keys) {
1325
- if (exported.has(key)) {
1716
+ const exportedKey = this.stripQualifiedKey(key);
1717
+ if (exported.has(exportedKey)) {
1326
1718
  continue;
1327
1719
  }
1328
1720
  const stored = await this.readLayerEntry(layer, key);
1329
1721
  if (stored === null) {
1330
1722
  continue;
1331
1723
  }
1332
- exported.set(key, {
1333
- key,
1724
+ exported.set(exportedKey, {
1725
+ key: exportedKey,
1334
1726
  value: stored,
1335
1727
  ttl: remainingStoredTtlSeconds(stored)
1336
1728
  });
@@ -1339,20 +1731,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1339
1731
  return [...exported.values()];
1340
1732
  }
1341
1733
  async importState(entries) {
1342
- await this.startup;
1734
+ await this.awaitStartup("importState");
1343
1735
  await Promise.all(
1344
1736
  entries.map(async (entry) => {
1345
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1346
- await this.tagIndex.touch(entry.key);
1737
+ const qualifiedKey = this.qualifyKey(entry.key);
1738
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1739
+ await this.tagIndex.touch(qualifiedKey);
1347
1740
  })
1348
1741
  );
1349
1742
  }
1350
1743
  async persistToFile(filePath) {
1744
+ this.assertActive("persistToFile");
1351
1745
  const snapshot = await this.exportState();
1352
- await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1746
+ const { promises: fs } = await import("fs");
1747
+ await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1353
1748
  }
1354
1749
  async restoreFromFile(filePath) {
1355
- const raw = await import_node_fs.promises.readFile(filePath, "utf8");
1750
+ this.assertActive("restoreFromFile");
1751
+ const { promises: fs } = await import("fs");
1752
+ const raw = await fs.readFile(filePath, "utf8");
1356
1753
  let parsed;
1357
1754
  try {
1358
1755
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1375,7 +1772,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1375
1772
  this.disconnectPromise = (async () => {
1376
1773
  await this.startup;
1377
1774
  await this.unsubscribeInvalidation?.();
1775
+ await this.flushWriteBehindQueue();
1378
1776
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1777
+ if (this.writeBehindTimer) {
1778
+ clearInterval(this.writeBehindTimer);
1779
+ this.writeBehindTimer = void 0;
1780
+ }
1781
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1379
1782
  })();
1380
1783
  }
1381
1784
  await this.disconnectPromise;
@@ -1435,7 +1838,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
1435
1838
  const fetchStart = Date.now();
1436
1839
  let fetched;
1437
1840
  try {
1438
- fetched = await fetcher();
1841
+ fetched = await this.fetchRateLimiter.schedule(
1842
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1843
+ { key, fetcher },
1844
+ fetcher
1845
+ );
1439
1846
  this.circuitBreakerManager.recordSuccess(key);
1440
1847
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1441
1848
  } catch (error) {
@@ -1469,6 +1876,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
1469
1876
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1470
1877
  }
1471
1878
  }
1879
+ async writeBatch(entries) {
1880
+ const now = Date.now();
1881
+ const entriesByLayer = /* @__PURE__ */ new Map();
1882
+ const immediateOperations = [];
1883
+ const deferredOperations = [];
1884
+ for (const entry of entries) {
1885
+ for (const layer of this.layers) {
1886
+ if (this.shouldSkipLayer(layer)) {
1887
+ continue;
1888
+ }
1889
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1890
+ const bucket = entriesByLayer.get(layer) ?? [];
1891
+ bucket.push(layerEntry);
1892
+ entriesByLayer.set(layer, bucket);
1893
+ }
1894
+ }
1895
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1896
+ const operation = async () => {
1897
+ try {
1898
+ if (layer.setMany) {
1899
+ await layer.setMany(layerEntries);
1900
+ return;
1901
+ }
1902
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1903
+ } catch (error) {
1904
+ await this.handleLayerFailure(layer, "write", error);
1905
+ }
1906
+ };
1907
+ if (this.shouldWriteBehind(layer)) {
1908
+ deferredOperations.push(operation);
1909
+ } else {
1910
+ immediateOperations.push(operation);
1911
+ }
1912
+ }
1913
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1914
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1915
+ for (const entry of entries) {
1916
+ if (entry.options?.tags) {
1917
+ await this.tagIndex.track(entry.key, entry.options.tags);
1918
+ } else {
1919
+ await this.tagIndex.touch(entry.key);
1920
+ }
1921
+ this.metricsCollector.increment("sets");
1922
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1923
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1924
+ }
1925
+ if (this.shouldBroadcastL1Invalidation()) {
1926
+ await this.publishInvalidation({
1927
+ scope: "keys",
1928
+ keys: entries.map((entry) => entry.key),
1929
+ sourceId: this.instanceId,
1930
+ operation: "write"
1931
+ });
1932
+ }
1933
+ }
1472
1934
  async readFromLayers(key, options, mode) {
1473
1935
  let sawRetainableValue = false;
1474
1936
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1552,33 +2014,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1552
2014
  }
1553
2015
  async writeAcrossLayers(key, kind, value, options) {
1554
2016
  const now = Date.now();
1555
- const operations = this.layers.map((layer) => async () => {
1556
- if (this.shouldSkipLayer(layer)) {
1557
- return;
1558
- }
1559
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1560
- const staleWhileRevalidate = this.resolveLayerSeconds(
1561
- layer.name,
1562
- options?.staleWhileRevalidate,
1563
- this.options.staleWhileRevalidate
1564
- );
1565
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1566
- const payload = createStoredValueEnvelope({
1567
- kind,
1568
- value,
1569
- freshTtlSeconds: freshTtl,
1570
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1571
- staleIfErrorSeconds: staleIfError,
1572
- now
1573
- });
1574
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1575
- try {
1576
- await layer.set(key, payload, ttl);
1577
- } catch (error) {
1578
- await this.handleLayerFailure(layer, "write", error);
2017
+ const immediateOperations = [];
2018
+ const deferredOperations = [];
2019
+ for (const layer of this.layers) {
2020
+ const operation = async () => {
2021
+ if (this.shouldSkipLayer(layer)) {
2022
+ return;
2023
+ }
2024
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
2025
+ try {
2026
+ await layer.set(entry.key, entry.value, entry.ttl);
2027
+ } catch (error) {
2028
+ await this.handleLayerFailure(layer, "write", error);
2029
+ }
2030
+ };
2031
+ if (this.shouldWriteBehind(layer)) {
2032
+ deferredOperations.push(operation);
2033
+ } else {
2034
+ immediateOperations.push(operation);
1579
2035
  }
1580
- });
1581
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
2036
+ }
2037
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
2038
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1582
2039
  }
1583
2040
  async executeLayerOperations(operations, context) {
1584
2041
  if (this.options.writePolicy !== "best-effort") {
@@ -1602,8 +2059,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1602
2059
  );
1603
2060
  }
1604
2061
  }
1605
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1606
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
2062
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2063
+ return this.ttlResolver.resolveFreshTtl(
2064
+ key,
2065
+ layerName,
2066
+ kind,
2067
+ options,
2068
+ fallbackTtl,
2069
+ this.options.negativeTtl,
2070
+ void 0,
2071
+ value
2072
+ );
1607
2073
  }
1608
2074
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1609
2075
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1632,7 +2098,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1632
2098
  return {
1633
2099
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1634
2100
  waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1635
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
2101
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
2102
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1636
2103
  };
1637
2104
  }
1638
2105
  async deleteKeys(keys) {
@@ -1697,6 +2164,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
1697
2164
  shouldBroadcastL1Invalidation() {
1698
2165
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1699
2166
  }
2167
+ initializeWriteBehind(options) {
2168
+ if (this.options.writeStrategy !== "write-behind") {
2169
+ return;
2170
+ }
2171
+ const flushIntervalMs = options?.flushIntervalMs;
2172
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2173
+ return;
2174
+ }
2175
+ this.writeBehindTimer = setInterval(() => {
2176
+ void this.flushWriteBehindQueue();
2177
+ }, flushIntervalMs);
2178
+ this.writeBehindTimer.unref?.();
2179
+ }
2180
+ shouldWriteBehind(layer) {
2181
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2182
+ }
2183
+ async enqueueWriteBehind(operation) {
2184
+ this.writeBehindQueue.push(operation);
2185
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2186
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2187
+ if (this.writeBehindQueue.length >= batchSize) {
2188
+ await this.flushWriteBehindQueue();
2189
+ return;
2190
+ }
2191
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2192
+ await this.flushWriteBehindQueue();
2193
+ }
2194
+ }
2195
+ async flushWriteBehindQueue() {
2196
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2197
+ await this.writeBehindFlushPromise;
2198
+ return;
2199
+ }
2200
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2201
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2202
+ this.writeBehindFlushPromise = (async () => {
2203
+ await Promise.allSettled(batch.map((operation) => operation()));
2204
+ })();
2205
+ await this.writeBehindFlushPromise;
2206
+ this.writeBehindFlushPromise = void 0;
2207
+ if (this.writeBehindQueue.length > 0) {
2208
+ await this.flushWriteBehindQueue();
2209
+ }
2210
+ }
2211
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2212
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2213
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2214
+ layer.name,
2215
+ options?.staleWhileRevalidate,
2216
+ this.options.staleWhileRevalidate
2217
+ );
2218
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2219
+ const payload = createStoredValueEnvelope({
2220
+ kind,
2221
+ value,
2222
+ freshTtlSeconds: freshTtl,
2223
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2224
+ staleIfErrorSeconds: staleIfError,
2225
+ now
2226
+ });
2227
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2228
+ return {
2229
+ key,
2230
+ value: payload,
2231
+ ttl
2232
+ };
2233
+ }
2234
+ intersectKeys(groups) {
2235
+ if (groups.length === 0) {
2236
+ return [];
2237
+ }
2238
+ const [firstGroup, ...rest] = groups;
2239
+ if (!firstGroup) {
2240
+ return [];
2241
+ }
2242
+ const restSets = rest.map((group) => new Set(group));
2243
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2244
+ }
2245
+ qualifyKey(key) {
2246
+ const prefix = this.generationPrefix();
2247
+ return prefix ? `${prefix}${key}` : key;
2248
+ }
2249
+ qualifyPattern(pattern) {
2250
+ const prefix = this.generationPrefix();
2251
+ return prefix ? `${prefix}${pattern}` : pattern;
2252
+ }
2253
+ stripQualifiedKey(key) {
2254
+ const prefix = this.generationPrefix();
2255
+ if (!prefix || !key.startsWith(prefix)) {
2256
+ return key;
2257
+ }
2258
+ return key.slice(prefix.length);
2259
+ }
2260
+ generationPrefix() {
2261
+ if (this.currentGeneration === void 0) {
2262
+ return "";
2263
+ }
2264
+ return `v${this.currentGeneration}:`;
2265
+ }
1700
2266
  async deleteKeysFromLayers(layers, keys) {
1701
2267
  await Promise.all(
1702
2268
  layers.map(async (layer) => {
@@ -1738,8 +2304,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1738
2304
  this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1739
2305
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1740
2306
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2307
+ this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2308
+ this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
1741
2309
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1742
2310
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2311
+ if (this.options.generation !== void 0) {
2312
+ this.validateNonNegativeNumber("generation", this.options.generation);
2313
+ }
1743
2314
  }
1744
2315
  validateWriteOptions(options) {
1745
2316
  if (!options) {
@@ -1751,8 +2322,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1751
2322
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1752
2323
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1753
2324
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2325
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1754
2326
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1755
2327
  this.validateCircuitBreakerOptions(options.circuitBreaker);
2328
+ this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
1756
2329
  }
1757
2330
  validateLayerNumberOption(name, value) {
1758
2331
  if (value === void 0) {
@@ -1777,6 +2350,20 @@ var CacheStack = class extends import_node_events.EventEmitter {
1777
2350
  throw new Error(`${name} must be a positive finite number.`);
1778
2351
  }
1779
2352
  }
2353
+ validateRateLimitOptions(name, options) {
2354
+ if (!options) {
2355
+ return;
2356
+ }
2357
+ this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2358
+ this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2359
+ this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2360
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2361
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2362
+ }
2363
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2364
+ throw new Error(`${name}.bucketKey must not be empty.`);
2365
+ }
2366
+ }
1780
2367
  validateNonNegativeNumber(name, value) {
1781
2368
  if (!Number.isFinite(value) || value < 0) {
1782
2369
  throw new Error(`${name} must be a non-negative finite number.`);
@@ -1794,6 +2381,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1794
2381
  }
1795
2382
  return key;
1796
2383
  }
2384
+ validateTtlPolicy(name, policy) {
2385
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2386
+ return;
2387
+ }
2388
+ if ("alignTo" in policy) {
2389
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2390
+ return;
2391
+ }
2392
+ throw new Error(`${name} is invalid.`);
2393
+ }
2394
+ assertActive(operation) {
2395
+ if (this.isDisconnecting) {
2396
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2397
+ }
2398
+ }
2399
+ async awaitStartup(operation) {
2400
+ this.assertActive(operation);
2401
+ await this.startup;
2402
+ this.assertActive(operation);
2403
+ }
1797
2404
  serializeOptions(options) {
1798
2405
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1799
2406
  }
@@ -1899,6 +2506,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1899
2506
  return value;
1900
2507
  }
1901
2508
  };
2509
+ function createInstanceId() {
2510
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2511
+ }
1902
2512
 
1903
2513
  // src/module.ts
1904
2514
  var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);