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.
@@ -46,9 +46,182 @@ function Cacheable(options) {
46
46
  import { Global, Inject, Module } from "@nestjs/common";
47
47
 
48
48
  // ../../src/CacheStack.ts
49
- import { randomUUID } from "crypto";
50
49
  import { EventEmitter } from "events";
51
- import { promises as fs } from "fs";
50
+
51
+ // ../../node_modules/async-mutex/index.mjs
52
+ var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
53
+ var E_ALREADY_LOCKED = new Error("mutex already locked");
54
+ var E_CANCELED = new Error("request for lock canceled");
55
+ var __awaiter$2 = function(thisArg, _arguments, P, generator) {
56
+ function adopt(value) {
57
+ return value instanceof P ? value : new P(function(resolve) {
58
+ resolve(value);
59
+ });
60
+ }
61
+ return new (P || (P = Promise))(function(resolve, reject) {
62
+ function fulfilled(value) {
63
+ try {
64
+ step(generator.next(value));
65
+ } catch (e) {
66
+ reject(e);
67
+ }
68
+ }
69
+ function rejected(value) {
70
+ try {
71
+ step(generator["throw"](value));
72
+ } catch (e) {
73
+ reject(e);
74
+ }
75
+ }
76
+ function step(result) {
77
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
78
+ }
79
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
80
+ });
81
+ };
82
+ var Semaphore = class {
83
+ constructor(_value, _cancelError = E_CANCELED) {
84
+ this._value = _value;
85
+ this._cancelError = _cancelError;
86
+ this._weightedQueues = [];
87
+ this._weightedWaiters = [];
88
+ }
89
+ acquire(weight = 1) {
90
+ if (weight <= 0)
91
+ throw new Error(`invalid weight ${weight}: must be positive`);
92
+ return new Promise((resolve, reject) => {
93
+ if (!this._weightedQueues[weight - 1])
94
+ this._weightedQueues[weight - 1] = [];
95
+ this._weightedQueues[weight - 1].push({ resolve, reject });
96
+ this._dispatch();
97
+ });
98
+ }
99
+ runExclusive(callback, weight = 1) {
100
+ return __awaiter$2(this, void 0, void 0, function* () {
101
+ const [value, release] = yield this.acquire(weight);
102
+ try {
103
+ return yield callback(value);
104
+ } finally {
105
+ release();
106
+ }
107
+ });
108
+ }
109
+ waitForUnlock(weight = 1) {
110
+ if (weight <= 0)
111
+ throw new Error(`invalid weight ${weight}: must be positive`);
112
+ return new Promise((resolve) => {
113
+ if (!this._weightedWaiters[weight - 1])
114
+ this._weightedWaiters[weight - 1] = [];
115
+ this._weightedWaiters[weight - 1].push(resolve);
116
+ this._dispatch();
117
+ });
118
+ }
119
+ isLocked() {
120
+ return this._value <= 0;
121
+ }
122
+ getValue() {
123
+ return this._value;
124
+ }
125
+ setValue(value) {
126
+ this._value = value;
127
+ this._dispatch();
128
+ }
129
+ release(weight = 1) {
130
+ if (weight <= 0)
131
+ throw new Error(`invalid weight ${weight}: must be positive`);
132
+ this._value += weight;
133
+ this._dispatch();
134
+ }
135
+ cancel() {
136
+ this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
137
+ this._weightedQueues = [];
138
+ }
139
+ _dispatch() {
140
+ var _a;
141
+ for (let weight = this._value; weight > 0; weight--) {
142
+ const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
143
+ if (!queueEntry)
144
+ continue;
145
+ const previousValue = this._value;
146
+ const previousWeight = weight;
147
+ this._value -= weight;
148
+ weight = this._value + 1;
149
+ queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
150
+ }
151
+ this._drainUnlockWaiters();
152
+ }
153
+ _newReleaser(weight) {
154
+ let called = false;
155
+ return () => {
156
+ if (called)
157
+ return;
158
+ called = true;
159
+ this.release(weight);
160
+ };
161
+ }
162
+ _drainUnlockWaiters() {
163
+ for (let weight = this._value; weight > 0; weight--) {
164
+ if (!this._weightedWaiters[weight - 1])
165
+ continue;
166
+ this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
167
+ this._weightedWaiters[weight - 1] = [];
168
+ }
169
+ }
170
+ };
171
+ var __awaiter$1 = function(thisArg, _arguments, P, generator) {
172
+ function adopt(value) {
173
+ return value instanceof P ? value : new P(function(resolve) {
174
+ resolve(value);
175
+ });
176
+ }
177
+ return new (P || (P = Promise))(function(resolve, reject) {
178
+ function fulfilled(value) {
179
+ try {
180
+ step(generator.next(value));
181
+ } catch (e) {
182
+ reject(e);
183
+ }
184
+ }
185
+ function rejected(value) {
186
+ try {
187
+ step(generator["throw"](value));
188
+ } catch (e) {
189
+ reject(e);
190
+ }
191
+ }
192
+ function step(result) {
193
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
194
+ }
195
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
196
+ });
197
+ };
198
+ var Mutex = class {
199
+ constructor(cancelError) {
200
+ this._semaphore = new Semaphore(1, cancelError);
201
+ }
202
+ acquire() {
203
+ return __awaiter$1(this, void 0, void 0, function* () {
204
+ const [, releaser] = yield this._semaphore.acquire();
205
+ return releaser;
206
+ });
207
+ }
208
+ runExclusive(callback) {
209
+ return this._semaphore.runExclusive(() => callback());
210
+ }
211
+ isLocked() {
212
+ return this._semaphore.isLocked();
213
+ }
214
+ waitForUnlock() {
215
+ return this._semaphore.waitForUnlock();
216
+ }
217
+ release() {
218
+ if (this._semaphore.isLocked())
219
+ this._semaphore.release();
220
+ }
221
+ cancel() {
222
+ return this._semaphore.cancel();
223
+ }
224
+ };
52
225
 
53
226
  // ../../src/CacheNamespace.ts
54
227
  var CacheNamespace = class _CacheNamespace {
@@ -58,57 +231,69 @@ var CacheNamespace = class _CacheNamespace {
58
231
  }
59
232
  cache;
60
233
  prefix;
234
+ static metricsMutexes = /* @__PURE__ */ new WeakMap();
235
+ metrics = emptyMetrics();
61
236
  async get(key, fetcher, options) {
62
- return this.cache.get(this.qualify(key), fetcher, options);
237
+ return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
63
238
  }
64
239
  async getOrSet(key, fetcher, options) {
65
- return this.cache.getOrSet(this.qualify(key), fetcher, options);
240
+ return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
66
241
  }
67
242
  /**
68
243
  * Like `get()`, but throws `CacheMissError` instead of returning `null`.
69
244
  */
70
245
  async getOrThrow(key, fetcher, options) {
71
- return this.cache.getOrThrow(this.qualify(key), fetcher, options);
246
+ return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
72
247
  }
73
248
  async has(key) {
74
- return this.cache.has(this.qualify(key));
249
+ return this.trackMetrics(() => this.cache.has(this.qualify(key)));
75
250
  }
76
251
  async ttl(key) {
77
- return this.cache.ttl(this.qualify(key));
252
+ return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
78
253
  }
79
254
  async set(key, value, options) {
80
- await this.cache.set(this.qualify(key), value, options);
255
+ await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
81
256
  }
82
257
  async delete(key) {
83
- await this.cache.delete(this.qualify(key));
258
+ await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
84
259
  }
85
260
  async mdelete(keys) {
86
- await this.cache.mdelete(keys.map((k) => this.qualify(k)));
261
+ await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
87
262
  }
88
263
  async clear() {
89
- await this.cache.invalidateByPattern(`${this.prefix}:*`);
264
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
90
265
  }
91
266
  async mget(entries) {
92
- return this.cache.mget(
93
- entries.map((entry) => ({
94
- ...entry,
95
- key: this.qualify(entry.key)
96
- }))
267
+ return this.trackMetrics(
268
+ () => this.cache.mget(
269
+ entries.map((entry) => ({
270
+ ...entry,
271
+ key: this.qualify(entry.key)
272
+ }))
273
+ )
97
274
  );
98
275
  }
99
276
  async mset(entries) {
100
- await this.cache.mset(
101
- entries.map((entry) => ({
102
- ...entry,
103
- key: this.qualify(entry.key)
104
- }))
277
+ await this.trackMetrics(
278
+ () => this.cache.mset(
279
+ entries.map((entry) => ({
280
+ ...entry,
281
+ key: this.qualify(entry.key)
282
+ }))
283
+ )
105
284
  );
106
285
  }
107
286
  async invalidateByTag(tag) {
108
- await this.cache.invalidateByTag(tag);
287
+ await this.trackMetrics(() => this.cache.invalidateByTag(tag));
288
+ }
289
+ async invalidateByTags(tags, mode = "any") {
290
+ await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
109
291
  }
110
292
  async invalidateByPattern(pattern) {
111
- await this.cache.invalidateByPattern(this.qualify(pattern));
293
+ await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
294
+ }
295
+ async invalidateByPrefix(prefix) {
296
+ await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
112
297
  }
113
298
  /**
114
299
  * Returns detailed metadata about a single cache key within this namespace.
@@ -129,10 +314,19 @@ var CacheNamespace = class _CacheNamespace {
129
314
  );
130
315
  }
131
316
  getMetrics() {
132
- return this.cache.getMetrics();
317
+ return cloneMetrics(this.metrics);
133
318
  }
134
319
  getHitRate() {
135
- return this.cache.getHitRate();
320
+ const total = this.metrics.hits + this.metrics.misses;
321
+ const overall = total === 0 ? 0 : this.metrics.hits / total;
322
+ const byLayer = {};
323
+ const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
324
+ for (const layer of layers) {
325
+ const hits = this.metrics.hitsByLayer[layer] ?? 0;
326
+ const misses = this.metrics.missesByLayer[layer] ?? 0;
327
+ byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
328
+ }
329
+ return { overall, byLayer };
136
330
  }
137
331
  /**
138
332
  * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
@@ -149,7 +343,130 @@ var CacheNamespace = class _CacheNamespace {
149
343
  qualify(key) {
150
344
  return `${this.prefix}:${key}`;
151
345
  }
346
+ async trackMetrics(operation) {
347
+ return this.getMetricsMutex().runExclusive(async () => {
348
+ const before = this.cache.getMetrics();
349
+ const result = await operation();
350
+ const after = this.cache.getMetrics();
351
+ this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
352
+ return result;
353
+ });
354
+ }
355
+ getMetricsMutex() {
356
+ const existing = _CacheNamespace.metricsMutexes.get(this.cache);
357
+ if (existing) {
358
+ return existing;
359
+ }
360
+ const mutex = new Mutex();
361
+ _CacheNamespace.metricsMutexes.set(this.cache, mutex);
362
+ return mutex;
363
+ }
152
364
  };
365
+ function emptyMetrics() {
366
+ return {
367
+ hits: 0,
368
+ misses: 0,
369
+ fetches: 0,
370
+ sets: 0,
371
+ deletes: 0,
372
+ backfills: 0,
373
+ invalidations: 0,
374
+ staleHits: 0,
375
+ refreshes: 0,
376
+ refreshErrors: 0,
377
+ writeFailures: 0,
378
+ singleFlightWaits: 0,
379
+ negativeCacheHits: 0,
380
+ circuitBreakerTrips: 0,
381
+ degradedOperations: 0,
382
+ hitsByLayer: {},
383
+ missesByLayer: {},
384
+ latencyByLayer: {},
385
+ resetAt: Date.now()
386
+ };
387
+ }
388
+ function cloneMetrics(metrics) {
389
+ return {
390
+ ...metrics,
391
+ hitsByLayer: { ...metrics.hitsByLayer },
392
+ missesByLayer: { ...metrics.missesByLayer },
393
+ latencyByLayer: Object.fromEntries(
394
+ Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
395
+ )
396
+ };
397
+ }
398
+ function diffMetrics(before, after) {
399
+ const latencyByLayer = Object.fromEntries(
400
+ Object.entries(after.latencyByLayer).map(([layer, value]) => [
401
+ layer,
402
+ {
403
+ avgMs: value.avgMs,
404
+ maxMs: value.maxMs,
405
+ count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
406
+ }
407
+ ])
408
+ );
409
+ return {
410
+ hits: after.hits - before.hits,
411
+ misses: after.misses - before.misses,
412
+ fetches: after.fetches - before.fetches,
413
+ sets: after.sets - before.sets,
414
+ deletes: after.deletes - before.deletes,
415
+ backfills: after.backfills - before.backfills,
416
+ invalidations: after.invalidations - before.invalidations,
417
+ staleHits: after.staleHits - before.staleHits,
418
+ refreshes: after.refreshes - before.refreshes,
419
+ refreshErrors: after.refreshErrors - before.refreshErrors,
420
+ writeFailures: after.writeFailures - before.writeFailures,
421
+ singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
422
+ negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
423
+ circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
424
+ degradedOperations: after.degradedOperations - before.degradedOperations,
425
+ hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
426
+ missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
427
+ latencyByLayer,
428
+ resetAt: after.resetAt
429
+ };
430
+ }
431
+ function addMetrics(base, delta) {
432
+ return {
433
+ hits: base.hits + delta.hits,
434
+ misses: base.misses + delta.misses,
435
+ fetches: base.fetches + delta.fetches,
436
+ sets: base.sets + delta.sets,
437
+ deletes: base.deletes + delta.deletes,
438
+ backfills: base.backfills + delta.backfills,
439
+ invalidations: base.invalidations + delta.invalidations,
440
+ staleHits: base.staleHits + delta.staleHits,
441
+ refreshes: base.refreshes + delta.refreshes,
442
+ refreshErrors: base.refreshErrors + delta.refreshErrors,
443
+ writeFailures: base.writeFailures + delta.writeFailures,
444
+ singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
445
+ negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
446
+ circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
447
+ degradedOperations: base.degradedOperations + delta.degradedOperations,
448
+ hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
449
+ missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
450
+ latencyByLayer: cloneMetrics(delta).latencyByLayer,
451
+ resetAt: base.resetAt
452
+ };
453
+ }
454
+ function diffMap(before, after) {
455
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
456
+ const result = {};
457
+ for (const key of keys) {
458
+ result[key] = (after[key] ?? 0) - (before[key] ?? 0);
459
+ }
460
+ return result;
461
+ }
462
+ function addMap(base, delta) {
463
+ const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
464
+ const result = {};
465
+ for (const key of keys) {
466
+ result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
467
+ }
468
+ return result;
469
+ }
153
470
 
154
471
  // ../../src/internal/CircuitBreakerManager.ts
155
472
  var CircuitBreakerManager = class {
@@ -243,6 +560,148 @@ var CircuitBreakerManager = class {
243
560
  }
244
561
  };
245
562
 
563
+ // ../../src/internal/FetchRateLimiter.ts
564
+ var FetchRateLimiter = class {
565
+ queue = [];
566
+ buckets = /* @__PURE__ */ new Map();
567
+ fetcherBuckets = /* @__PURE__ */ new WeakMap();
568
+ nextFetcherBucketId = 0;
569
+ drainTimer;
570
+ async schedule(options, context, task) {
571
+ if (!options) {
572
+ return task();
573
+ }
574
+ const normalized = this.normalize(options);
575
+ if (!normalized) {
576
+ return task();
577
+ }
578
+ return new Promise((resolve, reject) => {
579
+ this.queue.push({
580
+ bucketKey: this.resolveBucketKey(normalized, context),
581
+ options: normalized,
582
+ task,
583
+ resolve,
584
+ reject
585
+ });
586
+ this.drain();
587
+ });
588
+ }
589
+ normalize(options) {
590
+ const maxConcurrent = options.maxConcurrent;
591
+ const intervalMs = options.intervalMs;
592
+ const maxPerInterval = options.maxPerInterval;
593
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
594
+ return void 0;
595
+ }
596
+ return {
597
+ maxConcurrent,
598
+ intervalMs,
599
+ maxPerInterval,
600
+ scope: options.scope ?? "global",
601
+ bucketKey: options.bucketKey
602
+ };
603
+ }
604
+ resolveBucketKey(options, context) {
605
+ if (options.bucketKey) {
606
+ return `custom:${options.bucketKey}`;
607
+ }
608
+ if (options.scope === "key") {
609
+ return `key:${context.key}`;
610
+ }
611
+ if (options.scope === "fetcher") {
612
+ const existing = this.fetcherBuckets.get(context.fetcher);
613
+ if (existing) {
614
+ return existing;
615
+ }
616
+ const bucket = `fetcher:${this.nextFetcherBucketId}`;
617
+ this.nextFetcherBucketId += 1;
618
+ this.fetcherBuckets.set(context.fetcher, bucket);
619
+ return bucket;
620
+ }
621
+ return "global";
622
+ }
623
+ drain() {
624
+ if (this.drainTimer) {
625
+ clearTimeout(this.drainTimer);
626
+ this.drainTimer = void 0;
627
+ }
628
+ while (this.queue.length > 0) {
629
+ let nextIndex = -1;
630
+ let nextWaitMs = Number.POSITIVE_INFINITY;
631
+ for (let index = 0; index < this.queue.length; index += 1) {
632
+ const next2 = this.queue[index];
633
+ if (!next2) {
634
+ continue;
635
+ }
636
+ const waitMs = this.waitTime(next2.bucketKey, next2.options);
637
+ if (waitMs <= 0) {
638
+ nextIndex = index;
639
+ break;
640
+ }
641
+ nextWaitMs = Math.min(nextWaitMs, waitMs);
642
+ }
643
+ if (nextIndex < 0) {
644
+ if (Number.isFinite(nextWaitMs)) {
645
+ this.drainTimer = setTimeout(() => {
646
+ this.drainTimer = void 0;
647
+ this.drain();
648
+ }, nextWaitMs);
649
+ this.drainTimer.unref?.();
650
+ }
651
+ return;
652
+ }
653
+ const next = this.queue.splice(nextIndex, 1)[0];
654
+ if (!next) {
655
+ return;
656
+ }
657
+ const bucket = this.bucketState(next.bucketKey);
658
+ bucket.active += 1;
659
+ bucket.startedAt.push(Date.now());
660
+ void next.task().then(next.resolve, next.reject).finally(() => {
661
+ bucket.active -= 1;
662
+ this.drain();
663
+ });
664
+ }
665
+ }
666
+ waitTime(bucketKey, options) {
667
+ const bucket = this.bucketState(bucketKey);
668
+ const now = Date.now();
669
+ if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
670
+ return 1;
671
+ }
672
+ if (!options.intervalMs || !options.maxPerInterval) {
673
+ return 0;
674
+ }
675
+ this.prune(bucket, now, options.intervalMs);
676
+ if (bucket.startedAt.length < options.maxPerInterval) {
677
+ return 0;
678
+ }
679
+ const oldest = bucket.startedAt[0];
680
+ if (!oldest) {
681
+ return 0;
682
+ }
683
+ return Math.max(1, options.intervalMs - (now - oldest));
684
+ }
685
+ prune(bucket, now, intervalMs) {
686
+ while (bucket.startedAt.length > 0) {
687
+ const startedAt = bucket.startedAt[0];
688
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
689
+ break;
690
+ }
691
+ bucket.startedAt.shift();
692
+ }
693
+ }
694
+ bucketState(bucketKey) {
695
+ const existing = this.buckets.get(bucketKey);
696
+ if (existing) {
697
+ return existing;
698
+ }
699
+ const bucket = { active: 0, startedAt: [] };
700
+ this.buckets.set(bucketKey, bucket);
701
+ return bucket;
702
+ }
703
+ };
704
+
246
705
  // ../../src/internal/MetricsCollector.ts
247
706
  var MetricsCollector = class {
248
707
  data = this.empty();
@@ -439,13 +898,14 @@ var TtlResolver = class {
439
898
  clearProfiles() {
440
899
  this.accessProfiles.clear();
441
900
  }
442
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
901
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
902
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
443
903
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
444
904
  layerName,
445
905
  options?.negativeTtl,
446
906
  globalNegativeTtl,
447
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
448
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
907
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
908
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
449
909
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
450
910
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
451
911
  return this.applyJitter(adaptiveTtl, jitter);
@@ -484,6 +944,29 @@ var TtlResolver = class {
484
944
  const delta = (Math.random() * 2 - 1) * jitter;
485
945
  return Math.max(1, Math.round(ttl + delta));
486
946
  }
947
+ resolvePolicyTtl(key, value, policy) {
948
+ if (!policy) {
949
+ return void 0;
950
+ }
951
+ if (typeof policy === "function") {
952
+ return policy({ key, value });
953
+ }
954
+ const now = /* @__PURE__ */ new Date();
955
+ if (policy === "until-midnight") {
956
+ const nextMidnight = new Date(now);
957
+ nextMidnight.setHours(24, 0, 0, 0);
958
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
959
+ }
960
+ if (policy === "next-hour") {
961
+ const nextHour = new Date(now);
962
+ nextHour.setMinutes(60, 0, 0);
963
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
964
+ }
965
+ const alignToSeconds = policy.alignTo;
966
+ const currentSeconds = Math.floor(Date.now() / 1e3);
967
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
968
+ return Math.max(1, nextBoundary - currentSeconds);
969
+ }
487
970
  readLayerNumber(layerName, value) {
488
971
  if (typeof value === "number") {
489
972
  return value;
@@ -504,300 +987,140 @@ var TtlResolver = class {
504
987
  removed += 1;
505
988
  }
506
989
  }
507
- };
508
-
509
- // ../../src/invalidation/PatternMatcher.ts
510
- var PatternMatcher = class _PatternMatcher {
511
- /**
512
- * Tests whether a glob-style pattern matches a value.
513
- * Supports `*` (any sequence of characters) and `?` (any single character).
514
- * Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
515
- */
516
- static matches(pattern, value) {
517
- return _PatternMatcher.matchLinear(pattern, value);
518
- }
519
- /**
520
- * Linear-time glob matching using dynamic programming.
521
- * Avoids catastrophic backtracking that RegExp-based glob matching can cause.
522
- */
523
- static matchLinear(pattern, value) {
524
- const m = pattern.length;
525
- const n = value.length;
526
- const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
527
- dp[0][0] = true;
528
- for (let i = 1; i <= m; i++) {
529
- if (pattern[i - 1] === "*") {
530
- dp[i][0] = dp[i - 1]?.[0];
531
- }
532
- }
533
- for (let i = 1; i <= m; i++) {
534
- for (let j = 1; j <= n; j++) {
535
- const pc = pattern[i - 1];
536
- if (pc === "*") {
537
- dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
538
- } else if (pc === "?" || pc === value[j - 1]) {
539
- dp[i][j] = dp[i - 1]?.[j - 1];
540
- }
541
- }
542
- }
543
- return dp[m]?.[n];
544
- }
545
- };
546
-
547
- // ../../src/invalidation/TagIndex.ts
548
- var TagIndex = class {
549
- tagToKeys = /* @__PURE__ */ new Map();
550
- keyToTags = /* @__PURE__ */ new Map();
551
- knownKeys = /* @__PURE__ */ new Set();
552
- maxKnownKeys;
553
- constructor(options = {}) {
554
- this.maxKnownKeys = options.maxKnownKeys;
555
- }
556
- async touch(key) {
557
- this.knownKeys.add(key);
558
- this.pruneKnownKeysIfNeeded();
559
- }
560
- async track(key, tags) {
561
- this.knownKeys.add(key);
562
- this.pruneKnownKeysIfNeeded();
563
- if (tags.length === 0) {
564
- return;
565
- }
566
- const existingTags = this.keyToTags.get(key);
567
- if (existingTags) {
568
- for (const tag of existingTags) {
569
- this.tagToKeys.get(tag)?.delete(key);
570
- }
571
- }
572
- const tagSet = new Set(tags);
573
- this.keyToTags.set(key, tagSet);
574
- for (const tag of tagSet) {
575
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
576
- keys.add(key);
577
- this.tagToKeys.set(tag, keys);
578
- }
579
- }
580
- async remove(key) {
581
- this.knownKeys.delete(key);
582
- const tags = this.keyToTags.get(key);
583
- if (!tags) {
584
- return;
585
- }
586
- for (const tag of tags) {
587
- const keys = this.tagToKeys.get(tag);
588
- if (!keys) {
589
- continue;
590
- }
591
- keys.delete(key);
592
- if (keys.size === 0) {
593
- this.tagToKeys.delete(tag);
594
- }
595
- }
596
- this.keyToTags.delete(key);
597
- }
598
- async keysForTag(tag) {
599
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
600
- }
601
- async tagsForKey(key) {
602
- return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
603
- }
604
- async matchPattern(pattern) {
605
- return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
606
- }
607
- async clear() {
608
- this.tagToKeys.clear();
609
- this.keyToTags.clear();
610
- this.knownKeys.clear();
611
- }
612
- pruneKnownKeysIfNeeded() {
613
- if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
614
- return;
615
- }
616
- const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
617
- let removed = 0;
618
- for (const key of this.knownKeys) {
619
- if (removed >= toRemove) {
620
- break;
621
- }
622
- this.knownKeys.delete(key);
623
- this.keyToTags.delete(key);
624
- removed += 1;
625
- }
626
- }
627
- };
628
-
629
- // ../../node_modules/async-mutex/index.mjs
630
- var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
631
- var E_ALREADY_LOCKED = new Error("mutex already locked");
632
- var E_CANCELED = new Error("request for lock canceled");
633
- var __awaiter$2 = function(thisArg, _arguments, P, generator) {
634
- function adopt(value) {
635
- return value instanceof P ? value : new P(function(resolve) {
636
- resolve(value);
637
- });
638
- }
639
- return new (P || (P = Promise))(function(resolve, reject) {
640
- function fulfilled(value) {
641
- try {
642
- step(generator.next(value));
643
- } catch (e) {
644
- reject(e);
645
- }
646
- }
647
- function rejected(value) {
648
- try {
649
- step(generator["throw"](value));
650
- } catch (e) {
651
- reject(e);
652
- }
653
- }
654
- function step(result) {
655
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
656
- }
657
- step((generator = generator.apply(thisArg, _arguments || [])).next());
658
- });
659
- };
660
- var Semaphore = class {
661
- constructor(_value, _cancelError = E_CANCELED) {
662
- this._value = _value;
663
- this._cancelError = _cancelError;
664
- this._weightedQueues = [];
665
- this._weightedWaiters = [];
666
- }
667
- acquire(weight = 1) {
668
- if (weight <= 0)
669
- throw new Error(`invalid weight ${weight}: must be positive`);
670
- return new Promise((resolve, reject) => {
671
- if (!this._weightedQueues[weight - 1])
672
- this._weightedQueues[weight - 1] = [];
673
- this._weightedQueues[weight - 1].push({ resolve, reject });
674
- this._dispatch();
675
- });
676
- }
677
- runExclusive(callback, weight = 1) {
678
- return __awaiter$2(this, void 0, void 0, function* () {
679
- const [value, release] = yield this.acquire(weight);
680
- try {
681
- return yield callback(value);
682
- } finally {
683
- release();
684
- }
685
- });
686
- }
687
- waitForUnlock(weight = 1) {
688
- if (weight <= 0)
689
- throw new Error(`invalid weight ${weight}: must be positive`);
690
- return new Promise((resolve) => {
691
- if (!this._weightedWaiters[weight - 1])
692
- this._weightedWaiters[weight - 1] = [];
693
- this._weightedWaiters[weight - 1].push(resolve);
694
- this._dispatch();
695
- });
696
- }
697
- isLocked() {
698
- return this._value <= 0;
699
- }
700
- getValue() {
701
- return this._value;
702
- }
703
- setValue(value) {
704
- this._value = value;
705
- this._dispatch();
706
- }
707
- release(weight = 1) {
708
- if (weight <= 0)
709
- throw new Error(`invalid weight ${weight}: must be positive`);
710
- this._value += weight;
711
- this._dispatch();
712
- }
713
- cancel() {
714
- this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
715
- this._weightedQueues = [];
716
- }
717
- _dispatch() {
718
- var _a;
719
- for (let weight = this._value; weight > 0; weight--) {
720
- const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
721
- if (!queueEntry)
722
- continue;
723
- const previousValue = this._value;
724
- const previousWeight = weight;
725
- this._value -= weight;
726
- weight = this._value + 1;
727
- queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
728
- }
729
- this._drainUnlockWaiters();
730
- }
731
- _newReleaser(weight) {
732
- let called = false;
733
- return () => {
734
- if (called)
735
- return;
736
- called = true;
737
- this.release(weight);
738
- };
990
+ };
991
+
992
+ // ../../src/invalidation/PatternMatcher.ts
993
+ var PatternMatcher = class _PatternMatcher {
994
+ /**
995
+ * Tests whether a glob-style pattern matches a value.
996
+ * Supports `*` (any sequence of characters) and `?` (any single character).
997
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
998
+ * quadratic memory usage on long patterns/keys.
999
+ */
1000
+ static matches(pattern, value) {
1001
+ return _PatternMatcher.matchLinear(pattern, value);
739
1002
  }
740
- _drainUnlockWaiters() {
741
- for (let weight = this._value; weight > 0; weight--) {
742
- if (!this._weightedWaiters[weight - 1])
1003
+ /**
1004
+ * Linear-time glob matching with O(1) extra memory.
1005
+ */
1006
+ static matchLinear(pattern, value) {
1007
+ let patternIndex = 0;
1008
+ let valueIndex = 0;
1009
+ let starIndex = -1;
1010
+ let backtrackValueIndex = 0;
1011
+ while (valueIndex < value.length) {
1012
+ const patternChar = pattern[patternIndex];
1013
+ const valueChar = value[valueIndex];
1014
+ if (patternChar === "*" && patternIndex < pattern.length) {
1015
+ starIndex = patternIndex;
1016
+ patternIndex += 1;
1017
+ backtrackValueIndex = valueIndex;
743
1018
  continue;
744
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
745
- this._weightedWaiters[weight - 1] = [];
1019
+ }
1020
+ if (patternChar === "?" || patternChar === valueChar) {
1021
+ patternIndex += 1;
1022
+ valueIndex += 1;
1023
+ continue;
1024
+ }
1025
+ if (starIndex !== -1) {
1026
+ patternIndex = starIndex + 1;
1027
+ backtrackValueIndex += 1;
1028
+ valueIndex = backtrackValueIndex;
1029
+ continue;
1030
+ }
1031
+ return false;
746
1032
  }
1033
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
1034
+ patternIndex += 1;
1035
+ }
1036
+ return patternIndex === pattern.length;
747
1037
  }
748
1038
  };
749
- var __awaiter$1 = function(thisArg, _arguments, P, generator) {
750
- function adopt(value) {
751
- return value instanceof P ? value : new P(function(resolve) {
752
- resolve(value);
753
- });
1039
+
1040
+ // ../../src/invalidation/TagIndex.ts
1041
+ var TagIndex = class {
1042
+ tagToKeys = /* @__PURE__ */ new Map();
1043
+ keyToTags = /* @__PURE__ */ new Map();
1044
+ knownKeys = /* @__PURE__ */ new Set();
1045
+ maxKnownKeys;
1046
+ constructor(options = {}) {
1047
+ this.maxKnownKeys = options.maxKnownKeys;
754
1048
  }
755
- return new (P || (P = Promise))(function(resolve, reject) {
756
- function fulfilled(value) {
757
- try {
758
- step(generator.next(value));
759
- } catch (e) {
760
- reject(e);
761
- }
1049
+ async touch(key) {
1050
+ this.knownKeys.add(key);
1051
+ this.pruneKnownKeysIfNeeded();
1052
+ }
1053
+ async track(key, tags) {
1054
+ this.knownKeys.add(key);
1055
+ this.pruneKnownKeysIfNeeded();
1056
+ if (tags.length === 0) {
1057
+ return;
762
1058
  }
763
- function rejected(value) {
764
- try {
765
- step(generator["throw"](value));
766
- } catch (e) {
767
- reject(e);
1059
+ const existingTags = this.keyToTags.get(key);
1060
+ if (existingTags) {
1061
+ for (const tag of existingTags) {
1062
+ this.tagToKeys.get(tag)?.delete(key);
768
1063
  }
769
1064
  }
770
- function step(result) {
771
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1065
+ const tagSet = new Set(tags);
1066
+ this.keyToTags.set(key, tagSet);
1067
+ for (const tag of tagSet) {
1068
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1069
+ keys.add(key);
1070
+ this.tagToKeys.set(tag, keys);
772
1071
  }
773
- step((generator = generator.apply(thisArg, _arguments || [])).next());
774
- });
775
- };
776
- var Mutex = class {
777
- constructor(cancelError) {
778
- this._semaphore = new Semaphore(1, cancelError);
779
1072
  }
780
- acquire() {
781
- return __awaiter$1(this, void 0, void 0, function* () {
782
- const [, releaser] = yield this._semaphore.acquire();
783
- return releaser;
784
- });
1073
+ async remove(key) {
1074
+ this.removeKey(key);
785
1075
  }
786
- runExclusive(callback) {
787
- return this._semaphore.runExclusive(() => callback());
1076
+ async keysForTag(tag) {
1077
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
788
1078
  }
789
- isLocked() {
790
- return this._semaphore.isLocked();
1079
+ async keysForPrefix(prefix) {
1080
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
791
1081
  }
792
- waitForUnlock() {
793
- return this._semaphore.waitForUnlock();
1082
+ async tagsForKey(key) {
1083
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
794
1084
  }
795
- release() {
796
- if (this._semaphore.isLocked())
797
- this._semaphore.release();
1085
+ async matchPattern(pattern) {
1086
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
798
1087
  }
799
- cancel() {
800
- return this._semaphore.cancel();
1088
+ async clear() {
1089
+ this.tagToKeys.clear();
1090
+ this.keyToTags.clear();
1091
+ this.knownKeys.clear();
1092
+ }
1093
+ pruneKnownKeysIfNeeded() {
1094
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1095
+ return;
1096
+ }
1097
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1098
+ let removed = 0;
1099
+ for (const key of this.knownKeys) {
1100
+ if (removed >= toRemove) {
1101
+ break;
1102
+ }
1103
+ this.removeKey(key);
1104
+ removed += 1;
1105
+ }
1106
+ }
1107
+ removeKey(key) {
1108
+ this.knownKeys.delete(key);
1109
+ const tags = this.keyToTags.get(key);
1110
+ if (!tags) {
1111
+ return;
1112
+ }
1113
+ for (const tag of tags) {
1114
+ const keys = this.tagToKeys.get(tag);
1115
+ if (!keys) {
1116
+ continue;
1117
+ }
1118
+ keys.delete(key);
1119
+ if (keys.size === 0) {
1120
+ this.tagToKeys.delete(tag);
1121
+ }
1122
+ }
1123
+ this.keyToTags.delete(key);
801
1124
  }
802
1125
  };
803
1126
 
@@ -879,6 +1202,7 @@ var CacheStack = class extends EventEmitter {
879
1202
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
880
1203
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
881
1204
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1205
+ this.currentGeneration = options.generation;
882
1206
  if (options.publishSetInvalidation !== void 0) {
883
1207
  console.warn(
884
1208
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -887,21 +1211,27 @@ var CacheStack = class extends EventEmitter {
887
1211
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
888
1212
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
889
1213
  this.tagIndex = options.tagIndex ?? new TagIndex();
1214
+ this.initializeWriteBehind(options.writeBehind);
890
1215
  this.startup = this.initialize();
891
1216
  }
892
1217
  layers;
893
1218
  options;
894
1219
  stampedeGuard = new StampedeGuard();
895
1220
  metricsCollector = new MetricsCollector();
896
- instanceId = randomUUID();
1221
+ instanceId = createInstanceId();
897
1222
  startup;
898
1223
  unsubscribeInvalidation;
899
1224
  logger;
900
1225
  tagIndex;
1226
+ fetchRateLimiter = new FetchRateLimiter();
901
1227
  backgroundRefreshes = /* @__PURE__ */ new Map();
902
1228
  layerDegradedUntil = /* @__PURE__ */ new Map();
903
1229
  ttlResolver;
904
1230
  circuitBreakerManager;
1231
+ currentGeneration;
1232
+ writeBehindQueue = [];
1233
+ writeBehindTimer;
1234
+ writeBehindFlushPromise;
905
1235
  isDisconnecting = false;
906
1236
  disconnectPromise;
907
1237
  /**
@@ -911,9 +1241,9 @@ var CacheStack = class extends EventEmitter {
911
1241
  * and no `fetcher` is provided.
912
1242
  */
913
1243
  async get(key, fetcher, options) {
914
- const normalizedKey = this.validateCacheKey(key);
1244
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
915
1245
  this.validateWriteOptions(options);
916
- await this.startup;
1246
+ await this.awaitStartup("get");
917
1247
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
918
1248
  if (hit.found) {
919
1249
  this.ttlResolver.recordAccess(normalizedKey);
@@ -978,8 +1308,8 @@ var CacheStack = class extends EventEmitter {
978
1308
  * Returns true if the given key exists and is not expired in any layer.
979
1309
  */
980
1310
  async has(key) {
981
- const normalizedKey = this.validateCacheKey(key);
982
- await this.startup;
1311
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1312
+ await this.awaitStartup("has");
983
1313
  for (const layer of this.layers) {
984
1314
  if (this.shouldSkipLayer(layer)) {
985
1315
  continue;
@@ -1009,8 +1339,8 @@ var CacheStack = class extends EventEmitter {
1009
1339
  * that has it, or null if the key is not found / has no TTL.
1010
1340
  */
1011
1341
  async ttl(key) {
1012
- const normalizedKey = this.validateCacheKey(key);
1013
- await this.startup;
1342
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1343
+ await this.awaitStartup("ttl");
1014
1344
  for (const layer of this.layers) {
1015
1345
  if (this.shouldSkipLayer(layer)) {
1016
1346
  continue;
@@ -1031,17 +1361,17 @@ var CacheStack = class extends EventEmitter {
1031
1361
  * Stores a value in all cache layers. Overwrites any existing value.
1032
1362
  */
1033
1363
  async set(key, value, options) {
1034
- const normalizedKey = this.validateCacheKey(key);
1364
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1035
1365
  this.validateWriteOptions(options);
1036
- await this.startup;
1366
+ await this.awaitStartup("set");
1037
1367
  await this.storeEntry(normalizedKey, "value", value, options);
1038
1368
  }
1039
1369
  /**
1040
1370
  * Deletes the key from all layers and publishes an invalidation message.
1041
1371
  */
1042
1372
  async delete(key) {
1043
- const normalizedKey = this.validateCacheKey(key);
1044
- await this.startup;
1373
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1374
+ await this.awaitStartup("delete");
1045
1375
  await this.deleteKeys([normalizedKey]);
1046
1376
  await this.publishInvalidation({
1047
1377
  scope: "key",
@@ -1051,7 +1381,7 @@ var CacheStack = class extends EventEmitter {
1051
1381
  });
1052
1382
  }
1053
1383
  async clear() {
1054
- await this.startup;
1384
+ await this.awaitStartup("clear");
1055
1385
  await Promise.all(this.layers.map((layer) => layer.clear()));
1056
1386
  await this.tagIndex.clear();
1057
1387
  this.ttlResolver.clearProfiles();
@@ -1067,23 +1397,25 @@ var CacheStack = class extends EventEmitter {
1067
1397
  if (keys.length === 0) {
1068
1398
  return;
1069
1399
  }
1070
- await this.startup;
1400
+ await this.awaitStartup("mdelete");
1071
1401
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1072
- await this.deleteKeys(normalizedKeys);
1402
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1403
+ await this.deleteKeys(cacheKeys);
1073
1404
  await this.publishInvalidation({
1074
1405
  scope: "keys",
1075
- keys: normalizedKeys,
1406
+ keys: cacheKeys,
1076
1407
  sourceId: this.instanceId,
1077
1408
  operation: "delete"
1078
1409
  });
1079
1410
  }
1080
1411
  async mget(entries) {
1412
+ this.assertActive("mget");
1081
1413
  if (entries.length === 0) {
1082
1414
  return [];
1083
1415
  }
1084
1416
  const normalizedEntries = entries.map((entry) => ({
1085
1417
  ...entry,
1086
- key: this.validateCacheKey(entry.key)
1418
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1087
1419
  }));
1088
1420
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1089
1421
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1109,7 +1441,7 @@ var CacheStack = class extends EventEmitter {
1109
1441
  })
1110
1442
  );
1111
1443
  }
1112
- await this.startup;
1444
+ await this.awaitStartup("mget");
1113
1445
  const pending = /* @__PURE__ */ new Set();
1114
1446
  const indexesByKey = /* @__PURE__ */ new Map();
1115
1447
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1157,14 +1489,17 @@ var CacheStack = class extends EventEmitter {
1157
1489
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1158
1490
  }
1159
1491
  async mset(entries) {
1492
+ this.assertActive("mset");
1160
1493
  const normalizedEntries = entries.map((entry) => ({
1161
1494
  ...entry,
1162
- key: this.validateCacheKey(entry.key)
1495
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1163
1496
  }));
1164
1497
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1165
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1498
+ await this.awaitStartup("mset");
1499
+ await this.writeBatch(normalizedEntries);
1166
1500
  }
1167
1501
  async warm(entries, options = {}) {
1502
+ this.assertActive("warm");
1168
1503
  const concurrency = Math.max(1, options.concurrency ?? 4);
1169
1504
  const total = entries.length;
1170
1505
  let completed = 0;
@@ -1213,14 +1548,31 @@ var CacheStack = class extends EventEmitter {
1213
1548
  return new CacheNamespace(this, prefix);
1214
1549
  }
1215
1550
  async invalidateByTag(tag) {
1216
- await this.startup;
1551
+ await this.awaitStartup("invalidateByTag");
1217
1552
  const keys = await this.tagIndex.keysForTag(tag);
1218
1553
  await this.deleteKeys(keys);
1219
1554
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1220
1555
  }
1556
+ async invalidateByTags(tags, mode = "any") {
1557
+ if (tags.length === 0) {
1558
+ return;
1559
+ }
1560
+ await this.awaitStartup("invalidateByTags");
1561
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1562
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1563
+ await this.deleteKeys(keys);
1564
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1565
+ }
1221
1566
  async invalidateByPattern(pattern) {
1222
- await this.startup;
1223
- const keys = await this.tagIndex.matchPattern(pattern);
1567
+ await this.awaitStartup("invalidateByPattern");
1568
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1569
+ await this.deleteKeys(keys);
1570
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1571
+ }
1572
+ async invalidateByPrefix(prefix) {
1573
+ await this.awaitStartup("invalidateByPrefix");
1574
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1575
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1224
1576
  await this.deleteKeys(keys);
1225
1577
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1226
1578
  }
@@ -1247,14 +1599,43 @@ var CacheStack = class extends EventEmitter {
1247
1599
  getHitRate() {
1248
1600
  return this.metricsCollector.hitRate();
1249
1601
  }
1602
+ async healthCheck() {
1603
+ await this.startup;
1604
+ return Promise.all(
1605
+ this.layers.map(async (layer) => {
1606
+ const startedAt = performance.now();
1607
+ try {
1608
+ const healthy = layer.ping ? await layer.ping() : true;
1609
+ return {
1610
+ layer: layer.name,
1611
+ healthy,
1612
+ latencyMs: performance.now() - startedAt
1613
+ };
1614
+ } catch (error) {
1615
+ return {
1616
+ layer: layer.name,
1617
+ healthy: false,
1618
+ latencyMs: performance.now() - startedAt,
1619
+ error: this.formatError(error)
1620
+ };
1621
+ }
1622
+ })
1623
+ );
1624
+ }
1625
+ bumpGeneration(nextGeneration) {
1626
+ const current = this.currentGeneration ?? 0;
1627
+ this.currentGeneration = nextGeneration ?? current + 1;
1628
+ return this.currentGeneration;
1629
+ }
1250
1630
  /**
1251
1631
  * Returns detailed metadata about a single cache key: which layers contain it,
1252
1632
  * remaining fresh/stale/error TTLs, and associated tags.
1253
1633
  * Returns `null` if the key does not exist in any layer.
1254
1634
  */
1255
1635
  async inspect(key) {
1256
- const normalizedKey = this.validateCacheKey(key);
1257
- await this.startup;
1636
+ const userKey = this.validateCacheKey(key);
1637
+ const normalizedKey = this.qualifyKey(userKey);
1638
+ await this.awaitStartup("inspect");
1258
1639
  const foundInLayers = [];
1259
1640
  let freshTtlSeconds = null;
1260
1641
  let staleTtlSeconds = null;
@@ -1285,10 +1666,10 @@ var CacheStack = class extends EventEmitter {
1285
1666
  return null;
1286
1667
  }
1287
1668
  const tags = await this.getTagsForKey(normalizedKey);
1288
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1669
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1289
1670
  }
1290
1671
  async exportState() {
1291
- await this.startup;
1672
+ await this.awaitStartup("exportState");
1292
1673
  const exported = /* @__PURE__ */ new Map();
1293
1674
  for (const layer of this.layers) {
1294
1675
  if (!layer.keys) {
@@ -1296,15 +1677,16 @@ var CacheStack = class extends EventEmitter {
1296
1677
  }
1297
1678
  const keys = await layer.keys();
1298
1679
  for (const key of keys) {
1299
- if (exported.has(key)) {
1680
+ const exportedKey = this.stripQualifiedKey(key);
1681
+ if (exported.has(exportedKey)) {
1300
1682
  continue;
1301
1683
  }
1302
1684
  const stored = await this.readLayerEntry(layer, key);
1303
1685
  if (stored === null) {
1304
1686
  continue;
1305
1687
  }
1306
- exported.set(key, {
1307
- key,
1688
+ exported.set(exportedKey, {
1689
+ key: exportedKey,
1308
1690
  value: stored,
1309
1691
  ttl: remainingStoredTtlSeconds(stored)
1310
1692
  });
@@ -1313,19 +1695,24 @@ var CacheStack = class extends EventEmitter {
1313
1695
  return [...exported.values()];
1314
1696
  }
1315
1697
  async importState(entries) {
1316
- await this.startup;
1698
+ await this.awaitStartup("importState");
1317
1699
  await Promise.all(
1318
1700
  entries.map(async (entry) => {
1319
- await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
1320
- await this.tagIndex.touch(entry.key);
1701
+ const qualifiedKey = this.qualifyKey(entry.key);
1702
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1703
+ await this.tagIndex.touch(qualifiedKey);
1321
1704
  })
1322
1705
  );
1323
1706
  }
1324
1707
  async persistToFile(filePath) {
1708
+ this.assertActive("persistToFile");
1325
1709
  const snapshot = await this.exportState();
1710
+ const { promises: fs } = await import("fs");
1326
1711
  await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1327
1712
  }
1328
1713
  async restoreFromFile(filePath) {
1714
+ this.assertActive("restoreFromFile");
1715
+ const { promises: fs } = await import("fs");
1329
1716
  const raw = await fs.readFile(filePath, "utf8");
1330
1717
  let parsed;
1331
1718
  try {
@@ -1349,7 +1736,13 @@ var CacheStack = class extends EventEmitter {
1349
1736
  this.disconnectPromise = (async () => {
1350
1737
  await this.startup;
1351
1738
  await this.unsubscribeInvalidation?.();
1739
+ await this.flushWriteBehindQueue();
1352
1740
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1741
+ if (this.writeBehindTimer) {
1742
+ clearInterval(this.writeBehindTimer);
1743
+ this.writeBehindTimer = void 0;
1744
+ }
1745
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1353
1746
  })();
1354
1747
  }
1355
1748
  await this.disconnectPromise;
@@ -1409,7 +1802,11 @@ var CacheStack = class extends EventEmitter {
1409
1802
  const fetchStart = Date.now();
1410
1803
  let fetched;
1411
1804
  try {
1412
- fetched = await fetcher();
1805
+ fetched = await this.fetchRateLimiter.schedule(
1806
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1807
+ { key, fetcher },
1808
+ fetcher
1809
+ );
1413
1810
  this.circuitBreakerManager.recordSuccess(key);
1414
1811
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1415
1812
  } catch (error) {
@@ -1443,6 +1840,61 @@ var CacheStack = class extends EventEmitter {
1443
1840
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1444
1841
  }
1445
1842
  }
1843
+ async writeBatch(entries) {
1844
+ const now = Date.now();
1845
+ const entriesByLayer = /* @__PURE__ */ new Map();
1846
+ const immediateOperations = [];
1847
+ const deferredOperations = [];
1848
+ for (const entry of entries) {
1849
+ for (const layer of this.layers) {
1850
+ if (this.shouldSkipLayer(layer)) {
1851
+ continue;
1852
+ }
1853
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1854
+ const bucket = entriesByLayer.get(layer) ?? [];
1855
+ bucket.push(layerEntry);
1856
+ entriesByLayer.set(layer, bucket);
1857
+ }
1858
+ }
1859
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1860
+ const operation = async () => {
1861
+ try {
1862
+ if (layer.setMany) {
1863
+ await layer.setMany(layerEntries);
1864
+ return;
1865
+ }
1866
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1867
+ } catch (error) {
1868
+ await this.handleLayerFailure(layer, "write", error);
1869
+ }
1870
+ };
1871
+ if (this.shouldWriteBehind(layer)) {
1872
+ deferredOperations.push(operation);
1873
+ } else {
1874
+ immediateOperations.push(operation);
1875
+ }
1876
+ }
1877
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1878
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1879
+ for (const entry of entries) {
1880
+ if (entry.options?.tags) {
1881
+ await this.tagIndex.track(entry.key, entry.options.tags);
1882
+ } else {
1883
+ await this.tagIndex.touch(entry.key);
1884
+ }
1885
+ this.metricsCollector.increment("sets");
1886
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1887
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1888
+ }
1889
+ if (this.shouldBroadcastL1Invalidation()) {
1890
+ await this.publishInvalidation({
1891
+ scope: "keys",
1892
+ keys: entries.map((entry) => entry.key),
1893
+ sourceId: this.instanceId,
1894
+ operation: "write"
1895
+ });
1896
+ }
1897
+ }
1446
1898
  async readFromLayers(key, options, mode) {
1447
1899
  let sawRetainableValue = false;
1448
1900
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1526,33 +1978,28 @@ var CacheStack = class extends EventEmitter {
1526
1978
  }
1527
1979
  async writeAcrossLayers(key, kind, value, options) {
1528
1980
  const now = Date.now();
1529
- const operations = this.layers.map((layer) => async () => {
1530
- if (this.shouldSkipLayer(layer)) {
1531
- return;
1532
- }
1533
- const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
1534
- const staleWhileRevalidate = this.resolveLayerSeconds(
1535
- layer.name,
1536
- options?.staleWhileRevalidate,
1537
- this.options.staleWhileRevalidate
1538
- );
1539
- const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
1540
- const payload = createStoredValueEnvelope({
1541
- kind,
1542
- value,
1543
- freshTtlSeconds: freshTtl,
1544
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1545
- staleIfErrorSeconds: staleIfError,
1546
- now
1547
- });
1548
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1549
- try {
1550
- await layer.set(key, payload, ttl);
1551
- } catch (error) {
1552
- await this.handleLayerFailure(layer, "write", error);
1981
+ const immediateOperations = [];
1982
+ const deferredOperations = [];
1983
+ for (const layer of this.layers) {
1984
+ const operation = async () => {
1985
+ if (this.shouldSkipLayer(layer)) {
1986
+ return;
1987
+ }
1988
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1989
+ try {
1990
+ await layer.set(entry.key, entry.value, entry.ttl);
1991
+ } catch (error) {
1992
+ await this.handleLayerFailure(layer, "write", error);
1993
+ }
1994
+ };
1995
+ if (this.shouldWriteBehind(layer)) {
1996
+ deferredOperations.push(operation);
1997
+ } else {
1998
+ immediateOperations.push(operation);
1553
1999
  }
1554
- });
1555
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
2000
+ }
2001
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
2002
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1556
2003
  }
1557
2004
  async executeLayerOperations(operations, context) {
1558
2005
  if (this.options.writePolicy !== "best-effort") {
@@ -1576,8 +2023,17 @@ var CacheStack = class extends EventEmitter {
1576
2023
  );
1577
2024
  }
1578
2025
  }
1579
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1580
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
2026
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2027
+ return this.ttlResolver.resolveFreshTtl(
2028
+ key,
2029
+ layerName,
2030
+ kind,
2031
+ options,
2032
+ fallbackTtl,
2033
+ this.options.negativeTtl,
2034
+ void 0,
2035
+ value
2036
+ );
1581
2037
  }
1582
2038
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1583
2039
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1606,7 +2062,8 @@ var CacheStack = class extends EventEmitter {
1606
2062
  return {
1607
2063
  leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
1608
2064
  waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
1609
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
2065
+ pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
2066
+ renewIntervalMs: this.options.singleFlightRenewIntervalMs
1610
2067
  };
1611
2068
  }
1612
2069
  async deleteKeys(keys) {
@@ -1671,6 +2128,105 @@ var CacheStack = class extends EventEmitter {
1671
2128
  shouldBroadcastL1Invalidation() {
1672
2129
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1673
2130
  }
2131
+ initializeWriteBehind(options) {
2132
+ if (this.options.writeStrategy !== "write-behind") {
2133
+ return;
2134
+ }
2135
+ const flushIntervalMs = options?.flushIntervalMs;
2136
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2137
+ return;
2138
+ }
2139
+ this.writeBehindTimer = setInterval(() => {
2140
+ void this.flushWriteBehindQueue();
2141
+ }, flushIntervalMs);
2142
+ this.writeBehindTimer.unref?.();
2143
+ }
2144
+ shouldWriteBehind(layer) {
2145
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2146
+ }
2147
+ async enqueueWriteBehind(operation) {
2148
+ this.writeBehindQueue.push(operation);
2149
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2150
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2151
+ if (this.writeBehindQueue.length >= batchSize) {
2152
+ await this.flushWriteBehindQueue();
2153
+ return;
2154
+ }
2155
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2156
+ await this.flushWriteBehindQueue();
2157
+ }
2158
+ }
2159
+ async flushWriteBehindQueue() {
2160
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2161
+ await this.writeBehindFlushPromise;
2162
+ return;
2163
+ }
2164
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2165
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2166
+ this.writeBehindFlushPromise = (async () => {
2167
+ await Promise.allSettled(batch.map((operation) => operation()));
2168
+ })();
2169
+ await this.writeBehindFlushPromise;
2170
+ this.writeBehindFlushPromise = void 0;
2171
+ if (this.writeBehindQueue.length > 0) {
2172
+ await this.flushWriteBehindQueue();
2173
+ }
2174
+ }
2175
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2176
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2177
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2178
+ layer.name,
2179
+ options?.staleWhileRevalidate,
2180
+ this.options.staleWhileRevalidate
2181
+ );
2182
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2183
+ const payload = createStoredValueEnvelope({
2184
+ kind,
2185
+ value,
2186
+ freshTtlSeconds: freshTtl,
2187
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2188
+ staleIfErrorSeconds: staleIfError,
2189
+ now
2190
+ });
2191
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2192
+ return {
2193
+ key,
2194
+ value: payload,
2195
+ ttl
2196
+ };
2197
+ }
2198
+ intersectKeys(groups) {
2199
+ if (groups.length === 0) {
2200
+ return [];
2201
+ }
2202
+ const [firstGroup, ...rest] = groups;
2203
+ if (!firstGroup) {
2204
+ return [];
2205
+ }
2206
+ const restSets = rest.map((group) => new Set(group));
2207
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2208
+ }
2209
+ qualifyKey(key) {
2210
+ const prefix = this.generationPrefix();
2211
+ return prefix ? `${prefix}${key}` : key;
2212
+ }
2213
+ qualifyPattern(pattern) {
2214
+ const prefix = this.generationPrefix();
2215
+ return prefix ? `${prefix}${pattern}` : pattern;
2216
+ }
2217
+ stripQualifiedKey(key) {
2218
+ const prefix = this.generationPrefix();
2219
+ if (!prefix || !key.startsWith(prefix)) {
2220
+ return key;
2221
+ }
2222
+ return key.slice(prefix.length);
2223
+ }
2224
+ generationPrefix() {
2225
+ if (this.currentGeneration === void 0) {
2226
+ return "";
2227
+ }
2228
+ return `v${this.currentGeneration}:`;
2229
+ }
1674
2230
  async deleteKeysFromLayers(layers, keys) {
1675
2231
  await Promise.all(
1676
2232
  layers.map(async (layer) => {
@@ -1712,8 +2268,13 @@ var CacheStack = class extends EventEmitter {
1712
2268
  this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
1713
2269
  this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
1714
2270
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
2271
+ this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
2272
+ this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
1715
2273
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1716
2274
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2275
+ if (this.options.generation !== void 0) {
2276
+ this.validateNonNegativeNumber("generation", this.options.generation);
2277
+ }
1717
2278
  }
1718
2279
  validateWriteOptions(options) {
1719
2280
  if (!options) {
@@ -1725,8 +2286,10 @@ var CacheStack = class extends EventEmitter {
1725
2286
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1726
2287
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1727
2288
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2289
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1728
2290
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1729
2291
  this.validateCircuitBreakerOptions(options.circuitBreaker);
2292
+ this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
1730
2293
  }
1731
2294
  validateLayerNumberOption(name, value) {
1732
2295
  if (value === void 0) {
@@ -1751,6 +2314,20 @@ var CacheStack = class extends EventEmitter {
1751
2314
  throw new Error(`${name} must be a positive finite number.`);
1752
2315
  }
1753
2316
  }
2317
+ validateRateLimitOptions(name, options) {
2318
+ if (!options) {
2319
+ return;
2320
+ }
2321
+ this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
2322
+ this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
2323
+ this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
2324
+ if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
2325
+ throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
2326
+ }
2327
+ if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
2328
+ throw new Error(`${name}.bucketKey must not be empty.`);
2329
+ }
2330
+ }
1754
2331
  validateNonNegativeNumber(name, value) {
1755
2332
  if (!Number.isFinite(value) || value < 0) {
1756
2333
  throw new Error(`${name} must be a non-negative finite number.`);
@@ -1768,6 +2345,26 @@ var CacheStack = class extends EventEmitter {
1768
2345
  }
1769
2346
  return key;
1770
2347
  }
2348
+ validateTtlPolicy(name, policy) {
2349
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2350
+ return;
2351
+ }
2352
+ if ("alignTo" in policy) {
2353
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2354
+ return;
2355
+ }
2356
+ throw new Error(`${name} is invalid.`);
2357
+ }
2358
+ assertActive(operation) {
2359
+ if (this.isDisconnecting) {
2360
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2361
+ }
2362
+ }
2363
+ async awaitStartup(operation) {
2364
+ this.assertActive(operation);
2365
+ await this.startup;
2366
+ this.assertActive(operation);
2367
+ }
1771
2368
  serializeOptions(options) {
1772
2369
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1773
2370
  }
@@ -1873,6 +2470,9 @@ var CacheStack = class extends EventEmitter {
1873
2470
  return value;
1874
2471
  }
1875
2472
  };
2473
+ function createInstanceId() {
2474
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2475
+ }
1876
2476
 
1877
2477
  // src/module.ts
1878
2478
  var InjectCacheStack = () => Inject(CACHE_STACK);