layercache 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var __decorateClass = (decorators, target, key, kind) => {
20
30
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
@@ -72,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,95 @@ var CircuitBreakerManager = class {
269
596
  }
270
597
  };
271
598
 
599
+ // ../../src/internal/FetchRateLimiter.ts
600
+ var FetchRateLimiter = class {
601
+ active = 0;
602
+ queue = [];
603
+ startedAt = [];
604
+ drainTimer;
605
+ async schedule(options, task) {
606
+ if (!options) {
607
+ return task();
608
+ }
609
+ const normalized = this.normalize(options);
610
+ if (!normalized) {
611
+ return task();
612
+ }
613
+ return new Promise((resolve, reject) => {
614
+ this.queue.push({ options: normalized, task, resolve, reject });
615
+ this.drain();
616
+ });
617
+ }
618
+ normalize(options) {
619
+ const maxConcurrent = options.maxConcurrent;
620
+ const intervalMs = options.intervalMs;
621
+ const maxPerInterval = options.maxPerInterval;
622
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
623
+ return void 0;
624
+ }
625
+ return {
626
+ maxConcurrent,
627
+ intervalMs,
628
+ maxPerInterval
629
+ };
630
+ }
631
+ drain() {
632
+ if (this.drainTimer) {
633
+ clearTimeout(this.drainTimer);
634
+ this.drainTimer = void 0;
635
+ }
636
+ while (this.queue.length > 0) {
637
+ const next = this.queue[0];
638
+ if (!next) {
639
+ return;
640
+ }
641
+ const waitMs = this.waitTime(next.options);
642
+ if (waitMs > 0) {
643
+ this.drainTimer = setTimeout(() => {
644
+ this.drainTimer = void 0;
645
+ this.drain();
646
+ }, waitMs);
647
+ this.drainTimer.unref?.();
648
+ return;
649
+ }
650
+ this.queue.shift();
651
+ this.active += 1;
652
+ this.startedAt.push(Date.now());
653
+ void next.task().then(next.resolve, next.reject).finally(() => {
654
+ this.active -= 1;
655
+ this.drain();
656
+ });
657
+ }
658
+ }
659
+ waitTime(options) {
660
+ const now = Date.now();
661
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
662
+ return 1;
663
+ }
664
+ if (!options.intervalMs || !options.maxPerInterval) {
665
+ return 0;
666
+ }
667
+ this.prune(now, options.intervalMs);
668
+ if (this.startedAt.length < options.maxPerInterval) {
669
+ return 0;
670
+ }
671
+ const oldest = this.startedAt[0];
672
+ if (!oldest) {
673
+ return 0;
674
+ }
675
+ return Math.max(1, options.intervalMs - (now - oldest));
676
+ }
677
+ prune(now, intervalMs) {
678
+ while (this.startedAt.length > 0) {
679
+ const startedAt = this.startedAt[0];
680
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
681
+ break;
682
+ }
683
+ this.startedAt.shift();
684
+ }
685
+ }
686
+ };
687
+
272
688
  // ../../src/internal/MetricsCollector.ts
273
689
  var MetricsCollector = class {
274
690
  data = this.empty();
@@ -465,13 +881,14 @@ var TtlResolver = class {
465
881
  clearProfiles() {
466
882
  this.accessProfiles.clear();
467
883
  }
468
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
884
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
885
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
469
886
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
470
887
  layerName,
471
888
  options?.negativeTtl,
472
889
  globalNegativeTtl,
473
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
474
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
890
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
891
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
475
892
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
476
893
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
477
894
  return this.applyJitter(adaptiveTtl, jitter);
@@ -510,6 +927,29 @@ var TtlResolver = class {
510
927
  const delta = (Math.random() * 2 - 1) * jitter;
511
928
  return Math.max(1, Math.round(ttl + delta));
512
929
  }
930
+ resolvePolicyTtl(key, value, policy) {
931
+ if (!policy) {
932
+ return void 0;
933
+ }
934
+ if (typeof policy === "function") {
935
+ return policy({ key, value });
936
+ }
937
+ const now = /* @__PURE__ */ new Date();
938
+ if (policy === "until-midnight") {
939
+ const nextMidnight = new Date(now);
940
+ nextMidnight.setHours(24, 0, 0, 0);
941
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
942
+ }
943
+ if (policy === "next-hour") {
944
+ const nextHour = new Date(now);
945
+ nextHour.setMinutes(60, 0, 0);
946
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
947
+ }
948
+ const alignToSeconds = policy.alignTo;
949
+ const currentSeconds = Math.floor(Date.now() / 1e3);
950
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
951
+ return Math.max(1, nextBoundary - currentSeconds);
952
+ }
513
953
  readLayerNumber(layerName, value) {
514
954
  if (typeof value === "number") {
515
955
  return value;
@@ -524,306 +964,146 @@ var TtlResolver = class {
524
964
  let removed = 0;
525
965
  for (const key of this.accessProfiles.keys()) {
526
966
  if (removed >= toRemove) {
527
- break;
528
- }
529
- this.accessProfiles.delete(key);
530
- removed += 1;
531
- }
532
- }
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
- };
967
+ break;
968
+ }
969
+ this.accessProfiles.delete(key);
970
+ removed += 1;
971
+ }
765
972
  }
766
- _drainUnlockWaiters() {
767
- for (let weight = this._value; weight > 0; weight--) {
768
- if (!this._weightedWaiters[weight - 1])
973
+ };
974
+
975
+ // ../../src/invalidation/PatternMatcher.ts
976
+ var PatternMatcher = class _PatternMatcher {
977
+ /**
978
+ * Tests whether a glob-style pattern matches a value.
979
+ * Supports `*` (any sequence of characters) and `?` (any single character).
980
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
981
+ * quadratic memory usage on long patterns/keys.
982
+ */
983
+ static matches(pattern, value) {
984
+ return _PatternMatcher.matchLinear(pattern, value);
985
+ }
986
+ /**
987
+ * Linear-time glob matching with O(1) extra memory.
988
+ */
989
+ static matchLinear(pattern, value) {
990
+ let patternIndex = 0;
991
+ let valueIndex = 0;
992
+ let starIndex = -1;
993
+ let backtrackValueIndex = 0;
994
+ while (valueIndex < value.length) {
995
+ const patternChar = pattern[patternIndex];
996
+ const valueChar = value[valueIndex];
997
+ if (patternChar === "*" && patternIndex < pattern.length) {
998
+ starIndex = patternIndex;
999
+ patternIndex += 1;
1000
+ backtrackValueIndex = valueIndex;
769
1001
  continue;
770
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
771
- this._weightedWaiters[weight - 1] = [];
1002
+ }
1003
+ if (patternChar === "?" || patternChar === valueChar) {
1004
+ patternIndex += 1;
1005
+ valueIndex += 1;
1006
+ continue;
1007
+ }
1008
+ if (starIndex !== -1) {
1009
+ patternIndex = starIndex + 1;
1010
+ backtrackValueIndex += 1;
1011
+ valueIndex = backtrackValueIndex;
1012
+ continue;
1013
+ }
1014
+ return false;
772
1015
  }
1016
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
1017
+ patternIndex += 1;
1018
+ }
1019
+ return patternIndex === pattern.length;
773
1020
  }
774
1021
  };
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
- });
1022
+
1023
+ // ../../src/invalidation/TagIndex.ts
1024
+ var TagIndex = class {
1025
+ tagToKeys = /* @__PURE__ */ new Map();
1026
+ keyToTags = /* @__PURE__ */ new Map();
1027
+ knownKeys = /* @__PURE__ */ new Set();
1028
+ maxKnownKeys;
1029
+ constructor(options = {}) {
1030
+ this.maxKnownKeys = options.maxKnownKeys;
780
1031
  }
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
- }
1032
+ async touch(key) {
1033
+ this.knownKeys.add(key);
1034
+ this.pruneKnownKeysIfNeeded();
1035
+ }
1036
+ async track(key, tags) {
1037
+ this.knownKeys.add(key);
1038
+ this.pruneKnownKeysIfNeeded();
1039
+ if (tags.length === 0) {
1040
+ return;
788
1041
  }
789
- function rejected(value) {
790
- try {
791
- step(generator["throw"](value));
792
- } catch (e) {
793
- reject(e);
1042
+ const existingTags = this.keyToTags.get(key);
1043
+ if (existingTags) {
1044
+ for (const tag of existingTags) {
1045
+ this.tagToKeys.get(tag)?.delete(key);
794
1046
  }
795
1047
  }
796
- function step(result) {
797
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1048
+ const tagSet = new Set(tags);
1049
+ this.keyToTags.set(key, tagSet);
1050
+ for (const tag of tagSet) {
1051
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1052
+ keys.add(key);
1053
+ this.tagToKeys.set(tag, keys);
798
1054
  }
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
1055
  }
806
- acquire() {
807
- return __awaiter$1(this, void 0, void 0, function* () {
808
- const [, releaser] = yield this._semaphore.acquire();
809
- return releaser;
810
- });
1056
+ async remove(key) {
1057
+ this.removeKey(key);
811
1058
  }
812
- runExclusive(callback) {
813
- return this._semaphore.runExclusive(() => callback());
1059
+ async keysForTag(tag) {
1060
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
814
1061
  }
815
- isLocked() {
816
- return this._semaphore.isLocked();
1062
+ async keysForPrefix(prefix) {
1063
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
817
1064
  }
818
- waitForUnlock() {
819
- return this._semaphore.waitForUnlock();
1065
+ async tagsForKey(key) {
1066
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
820
1067
  }
821
- release() {
822
- if (this._semaphore.isLocked())
823
- this._semaphore.release();
1068
+ async matchPattern(pattern) {
1069
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
824
1070
  }
825
- cancel() {
826
- return this._semaphore.cancel();
1071
+ async clear() {
1072
+ this.tagToKeys.clear();
1073
+ this.keyToTags.clear();
1074
+ this.knownKeys.clear();
1075
+ }
1076
+ pruneKnownKeysIfNeeded() {
1077
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1078
+ return;
1079
+ }
1080
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1081
+ let removed = 0;
1082
+ for (const key of this.knownKeys) {
1083
+ if (removed >= toRemove) {
1084
+ break;
1085
+ }
1086
+ this.removeKey(key);
1087
+ removed += 1;
1088
+ }
1089
+ }
1090
+ removeKey(key) {
1091
+ this.knownKeys.delete(key);
1092
+ const tags = this.keyToTags.get(key);
1093
+ if (!tags) {
1094
+ return;
1095
+ }
1096
+ for (const tag of tags) {
1097
+ const keys = this.tagToKeys.get(tag);
1098
+ if (!keys) {
1099
+ continue;
1100
+ }
1101
+ keys.delete(key);
1102
+ if (keys.size === 0) {
1103
+ this.tagToKeys.delete(tag);
1104
+ }
1105
+ }
1106
+ this.keyToTags.delete(key);
827
1107
  }
828
1108
  };
829
1109
 
@@ -905,6 +1185,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
905
1185
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
906
1186
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
907
1187
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1188
+ this.currentGeneration = options.generation;
908
1189
  if (options.publishSetInvalidation !== void 0) {
909
1190
  console.warn(
910
1191
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -913,21 +1194,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
913
1194
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
914
1195
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
915
1196
  this.tagIndex = options.tagIndex ?? new TagIndex();
1197
+ this.initializeWriteBehind(options.writeBehind);
916
1198
  this.startup = this.initialize();
917
1199
  }
918
1200
  layers;
919
1201
  options;
920
1202
  stampedeGuard = new StampedeGuard();
921
1203
  metricsCollector = new MetricsCollector();
922
- instanceId = (0, import_node_crypto.randomUUID)();
1204
+ instanceId = createInstanceId();
923
1205
  startup;
924
1206
  unsubscribeInvalidation;
925
1207
  logger;
926
1208
  tagIndex;
1209
+ fetchRateLimiter = new FetchRateLimiter();
927
1210
  backgroundRefreshes = /* @__PURE__ */ new Map();
928
1211
  layerDegradedUntil = /* @__PURE__ */ new Map();
929
1212
  ttlResolver;
930
1213
  circuitBreakerManager;
1214
+ currentGeneration;
1215
+ writeBehindQueue = [];
1216
+ writeBehindTimer;
1217
+ writeBehindFlushPromise;
931
1218
  isDisconnecting = false;
932
1219
  disconnectPromise;
933
1220
  /**
@@ -937,9 +1224,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
937
1224
  * and no `fetcher` is provided.
938
1225
  */
939
1226
  async get(key, fetcher, options) {
940
- const normalizedKey = this.validateCacheKey(key);
1227
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
941
1228
  this.validateWriteOptions(options);
942
- await this.startup;
1229
+ await this.awaitStartup("get");
943
1230
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
944
1231
  if (hit.found) {
945
1232
  this.ttlResolver.recordAccess(normalizedKey);
@@ -1004,8 +1291,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1004
1291
  * Returns true if the given key exists and is not expired in any layer.
1005
1292
  */
1006
1293
  async has(key) {
1007
- const normalizedKey = this.validateCacheKey(key);
1008
- await this.startup;
1294
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1295
+ await this.awaitStartup("has");
1009
1296
  for (const layer of this.layers) {
1010
1297
  if (this.shouldSkipLayer(layer)) {
1011
1298
  continue;
@@ -1035,8 +1322,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
1035
1322
  * that has it, or null if the key is not found / has no TTL.
1036
1323
  */
1037
1324
  async ttl(key) {
1038
- const normalizedKey = this.validateCacheKey(key);
1039
- await this.startup;
1325
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1326
+ await this.awaitStartup("ttl");
1040
1327
  for (const layer of this.layers) {
1041
1328
  if (this.shouldSkipLayer(layer)) {
1042
1329
  continue;
@@ -1057,17 +1344,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1057
1344
  * Stores a value in all cache layers. Overwrites any existing value.
1058
1345
  */
1059
1346
  async set(key, value, options) {
1060
- const normalizedKey = this.validateCacheKey(key);
1347
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1061
1348
  this.validateWriteOptions(options);
1062
- await this.startup;
1349
+ await this.awaitStartup("set");
1063
1350
  await this.storeEntry(normalizedKey, "value", value, options);
1064
1351
  }
1065
1352
  /**
1066
1353
  * Deletes the key from all layers and publishes an invalidation message.
1067
1354
  */
1068
1355
  async delete(key) {
1069
- const normalizedKey = this.validateCacheKey(key);
1070
- await this.startup;
1356
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1357
+ await this.awaitStartup("delete");
1071
1358
  await this.deleteKeys([normalizedKey]);
1072
1359
  await this.publishInvalidation({
1073
1360
  scope: "key",
@@ -1077,7 +1364,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1077
1364
  });
1078
1365
  }
1079
1366
  async clear() {
1080
- await this.startup;
1367
+ await this.awaitStartup("clear");
1081
1368
  await Promise.all(this.layers.map((layer) => layer.clear()));
1082
1369
  await this.tagIndex.clear();
1083
1370
  this.ttlResolver.clearProfiles();
@@ -1093,23 +1380,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1093
1380
  if (keys.length === 0) {
1094
1381
  return;
1095
1382
  }
1096
- await this.startup;
1383
+ await this.awaitStartup("mdelete");
1097
1384
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1098
- await this.deleteKeys(normalizedKeys);
1385
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1386
+ await this.deleteKeys(cacheKeys);
1099
1387
  await this.publishInvalidation({
1100
1388
  scope: "keys",
1101
- keys: normalizedKeys,
1389
+ keys: cacheKeys,
1102
1390
  sourceId: this.instanceId,
1103
1391
  operation: "delete"
1104
1392
  });
1105
1393
  }
1106
1394
  async mget(entries) {
1395
+ this.assertActive("mget");
1107
1396
  if (entries.length === 0) {
1108
1397
  return [];
1109
1398
  }
1110
1399
  const normalizedEntries = entries.map((entry) => ({
1111
1400
  ...entry,
1112
- key: this.validateCacheKey(entry.key)
1401
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1113
1402
  }));
1114
1403
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1115
1404
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1135,7 +1424,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1135
1424
  })
1136
1425
  );
1137
1426
  }
1138
- await this.startup;
1427
+ await this.awaitStartup("mget");
1139
1428
  const pending = /* @__PURE__ */ new Set();
1140
1429
  const indexesByKey = /* @__PURE__ */ new Map();
1141
1430
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1183,14 +1472,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1183
1472
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1184
1473
  }
1185
1474
  async mset(entries) {
1475
+ this.assertActive("mset");
1186
1476
  const normalizedEntries = entries.map((entry) => ({
1187
1477
  ...entry,
1188
- key: this.validateCacheKey(entry.key)
1478
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1189
1479
  }));
1190
1480
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1191
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1481
+ await this.awaitStartup("mset");
1482
+ await this.writeBatch(normalizedEntries);
1192
1483
  }
1193
1484
  async warm(entries, options = {}) {
1485
+ this.assertActive("warm");
1194
1486
  const concurrency = Math.max(1, options.concurrency ?? 4);
1195
1487
  const total = entries.length;
1196
1488
  let completed = 0;
@@ -1239,14 +1531,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
1239
1531
  return new CacheNamespace(this, prefix);
1240
1532
  }
1241
1533
  async invalidateByTag(tag) {
1242
- await this.startup;
1534
+ await this.awaitStartup("invalidateByTag");
1243
1535
  const keys = await this.tagIndex.keysForTag(tag);
1244
1536
  await this.deleteKeys(keys);
1245
1537
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1246
1538
  }
1539
+ async invalidateByTags(tags, mode = "any") {
1540
+ if (tags.length === 0) {
1541
+ return;
1542
+ }
1543
+ await this.awaitStartup("invalidateByTags");
1544
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1545
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1546
+ await this.deleteKeys(keys);
1547
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1548
+ }
1247
1549
  async invalidateByPattern(pattern) {
1248
- await this.startup;
1249
- const keys = await this.tagIndex.matchPattern(pattern);
1550
+ await this.awaitStartup("invalidateByPattern");
1551
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1552
+ await this.deleteKeys(keys);
1553
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1554
+ }
1555
+ async invalidateByPrefix(prefix) {
1556
+ await this.awaitStartup("invalidateByPrefix");
1557
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1558
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1250
1559
  await this.deleteKeys(keys);
1251
1560
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1252
1561
  }
@@ -1273,14 +1582,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
1273
1582
  getHitRate() {
1274
1583
  return this.metricsCollector.hitRate();
1275
1584
  }
1585
+ async healthCheck() {
1586
+ await this.startup;
1587
+ return Promise.all(
1588
+ this.layers.map(async (layer) => {
1589
+ const startedAt = performance.now();
1590
+ try {
1591
+ const healthy = layer.ping ? await layer.ping() : true;
1592
+ return {
1593
+ layer: layer.name,
1594
+ healthy,
1595
+ latencyMs: performance.now() - startedAt
1596
+ };
1597
+ } catch (error) {
1598
+ return {
1599
+ layer: layer.name,
1600
+ healthy: false,
1601
+ latencyMs: performance.now() - startedAt,
1602
+ error: this.formatError(error)
1603
+ };
1604
+ }
1605
+ })
1606
+ );
1607
+ }
1608
+ bumpGeneration(nextGeneration) {
1609
+ const current = this.currentGeneration ?? 0;
1610
+ this.currentGeneration = nextGeneration ?? current + 1;
1611
+ return this.currentGeneration;
1612
+ }
1276
1613
  /**
1277
1614
  * Returns detailed metadata about a single cache key: which layers contain it,
1278
1615
  * remaining fresh/stale/error TTLs, and associated tags.
1279
1616
  * Returns `null` if the key does not exist in any layer.
1280
1617
  */
1281
1618
  async inspect(key) {
1282
- const normalizedKey = this.validateCacheKey(key);
1283
- await this.startup;
1619
+ const userKey = this.validateCacheKey(key);
1620
+ const normalizedKey = this.qualifyKey(userKey);
1621
+ await this.awaitStartup("inspect");
1284
1622
  const foundInLayers = [];
1285
1623
  let freshTtlSeconds = null;
1286
1624
  let staleTtlSeconds = null;
@@ -1311,10 +1649,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1311
1649
  return null;
1312
1650
  }
1313
1651
  const tags = await this.getTagsForKey(normalizedKey);
1314
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1652
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1315
1653
  }
1316
1654
  async exportState() {
1317
- await this.startup;
1655
+ await this.awaitStartup("exportState");
1318
1656
  const exported = /* @__PURE__ */ new Map();
1319
1657
  for (const layer of this.layers) {
1320
1658
  if (!layer.keys) {
@@ -1322,15 +1660,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
1322
1660
  }
1323
1661
  const keys = await layer.keys();
1324
1662
  for (const key of keys) {
1325
- if (exported.has(key)) {
1663
+ const exportedKey = this.stripQualifiedKey(key);
1664
+ if (exported.has(exportedKey)) {
1326
1665
  continue;
1327
1666
  }
1328
1667
  const stored = await this.readLayerEntry(layer, key);
1329
1668
  if (stored === null) {
1330
1669
  continue;
1331
1670
  }
1332
- exported.set(key, {
1333
- key,
1671
+ exported.set(exportedKey, {
1672
+ key: exportedKey,
1334
1673
  value: stored,
1335
1674
  ttl: remainingStoredTtlSeconds(stored)
1336
1675
  });
@@ -1339,20 +1678,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
1339
1678
  return [...exported.values()];
1340
1679
  }
1341
1680
  async importState(entries) {
1342
- await this.startup;
1681
+ await this.awaitStartup("importState");
1343
1682
  await Promise.all(
1344
1683
  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);
1684
+ const qualifiedKey = this.qualifyKey(entry.key);
1685
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1686
+ await this.tagIndex.touch(qualifiedKey);
1347
1687
  })
1348
1688
  );
1349
1689
  }
1350
1690
  async persistToFile(filePath) {
1691
+ this.assertActive("persistToFile");
1351
1692
  const snapshot = await this.exportState();
1352
- await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1693
+ const { promises: fs } = await import("fs");
1694
+ await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1353
1695
  }
1354
1696
  async restoreFromFile(filePath) {
1355
- const raw = await import_node_fs.promises.readFile(filePath, "utf8");
1697
+ this.assertActive("restoreFromFile");
1698
+ const { promises: fs } = await import("fs");
1699
+ const raw = await fs.readFile(filePath, "utf8");
1356
1700
  let parsed;
1357
1701
  try {
1358
1702
  parsed = JSON.parse(raw, (_key, value) => {
@@ -1375,7 +1719,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
1375
1719
  this.disconnectPromise = (async () => {
1376
1720
  await this.startup;
1377
1721
  await this.unsubscribeInvalidation?.();
1722
+ await this.flushWriteBehindQueue();
1378
1723
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1724
+ if (this.writeBehindTimer) {
1725
+ clearInterval(this.writeBehindTimer);
1726
+ this.writeBehindTimer = void 0;
1727
+ }
1728
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1379
1729
  })();
1380
1730
  }
1381
1731
  await this.disconnectPromise;
@@ -1435,7 +1785,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
1435
1785
  const fetchStart = Date.now();
1436
1786
  let fetched;
1437
1787
  try {
1438
- fetched = await fetcher();
1788
+ fetched = await this.fetchRateLimiter.schedule(
1789
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1790
+ fetcher
1791
+ );
1439
1792
  this.circuitBreakerManager.recordSuccess(key);
1440
1793
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1441
1794
  } catch (error) {
@@ -1469,6 +1822,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
1469
1822
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1470
1823
  }
1471
1824
  }
1825
+ async writeBatch(entries) {
1826
+ const now = Date.now();
1827
+ const entriesByLayer = /* @__PURE__ */ new Map();
1828
+ const immediateOperations = [];
1829
+ const deferredOperations = [];
1830
+ for (const entry of entries) {
1831
+ for (const layer of this.layers) {
1832
+ if (this.shouldSkipLayer(layer)) {
1833
+ continue;
1834
+ }
1835
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1836
+ const bucket = entriesByLayer.get(layer) ?? [];
1837
+ bucket.push(layerEntry);
1838
+ entriesByLayer.set(layer, bucket);
1839
+ }
1840
+ }
1841
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1842
+ const operation = async () => {
1843
+ try {
1844
+ if (layer.setMany) {
1845
+ await layer.setMany(layerEntries);
1846
+ return;
1847
+ }
1848
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1849
+ } catch (error) {
1850
+ await this.handleLayerFailure(layer, "write", error);
1851
+ }
1852
+ };
1853
+ if (this.shouldWriteBehind(layer)) {
1854
+ deferredOperations.push(operation);
1855
+ } else {
1856
+ immediateOperations.push(operation);
1857
+ }
1858
+ }
1859
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1860
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1861
+ for (const entry of entries) {
1862
+ if (entry.options?.tags) {
1863
+ await this.tagIndex.track(entry.key, entry.options.tags);
1864
+ } else {
1865
+ await this.tagIndex.touch(entry.key);
1866
+ }
1867
+ this.metricsCollector.increment("sets");
1868
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1869
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1870
+ }
1871
+ if (this.shouldBroadcastL1Invalidation()) {
1872
+ await this.publishInvalidation({
1873
+ scope: "keys",
1874
+ keys: entries.map((entry) => entry.key),
1875
+ sourceId: this.instanceId,
1876
+ operation: "write"
1877
+ });
1878
+ }
1879
+ }
1472
1880
  async readFromLayers(key, options, mode) {
1473
1881
  let sawRetainableValue = false;
1474
1882
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1552,33 +1960,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
1552
1960
  }
1553
1961
  async writeAcrossLayers(key, kind, value, options) {
1554
1962
  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);
1963
+ const immediateOperations = [];
1964
+ const deferredOperations = [];
1965
+ for (const layer of this.layers) {
1966
+ const operation = async () => {
1967
+ if (this.shouldSkipLayer(layer)) {
1968
+ return;
1969
+ }
1970
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1971
+ try {
1972
+ await layer.set(entry.key, entry.value, entry.ttl);
1973
+ } catch (error) {
1974
+ await this.handleLayerFailure(layer, "write", error);
1975
+ }
1976
+ };
1977
+ if (this.shouldWriteBehind(layer)) {
1978
+ deferredOperations.push(operation);
1979
+ } else {
1980
+ immediateOperations.push(operation);
1579
1981
  }
1580
- });
1581
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1982
+ }
1983
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1984
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1582
1985
  }
1583
1986
  async executeLayerOperations(operations, context) {
1584
1987
  if (this.options.writePolicy !== "best-effort") {
@@ -1602,8 +2005,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
1602
2005
  );
1603
2006
  }
1604
2007
  }
1605
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1606
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
2008
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
2009
+ return this.ttlResolver.resolveFreshTtl(
2010
+ key,
2011
+ layerName,
2012
+ kind,
2013
+ options,
2014
+ fallbackTtl,
2015
+ this.options.negativeTtl,
2016
+ void 0,
2017
+ value
2018
+ );
1607
2019
  }
1608
2020
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1609
2021
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1697,6 +2109,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
1697
2109
  shouldBroadcastL1Invalidation() {
1698
2110
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1699
2111
  }
2112
+ initializeWriteBehind(options) {
2113
+ if (this.options.writeStrategy !== "write-behind") {
2114
+ return;
2115
+ }
2116
+ const flushIntervalMs = options?.flushIntervalMs;
2117
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2118
+ return;
2119
+ }
2120
+ this.writeBehindTimer = setInterval(() => {
2121
+ void this.flushWriteBehindQueue();
2122
+ }, flushIntervalMs);
2123
+ this.writeBehindTimer.unref?.();
2124
+ }
2125
+ shouldWriteBehind(layer) {
2126
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2127
+ }
2128
+ async enqueueWriteBehind(operation) {
2129
+ this.writeBehindQueue.push(operation);
2130
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2131
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2132
+ if (this.writeBehindQueue.length >= batchSize) {
2133
+ await this.flushWriteBehindQueue();
2134
+ return;
2135
+ }
2136
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2137
+ await this.flushWriteBehindQueue();
2138
+ }
2139
+ }
2140
+ async flushWriteBehindQueue() {
2141
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2142
+ await this.writeBehindFlushPromise;
2143
+ return;
2144
+ }
2145
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2146
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2147
+ this.writeBehindFlushPromise = (async () => {
2148
+ await Promise.allSettled(batch.map((operation) => operation()));
2149
+ })();
2150
+ await this.writeBehindFlushPromise;
2151
+ this.writeBehindFlushPromise = void 0;
2152
+ if (this.writeBehindQueue.length > 0) {
2153
+ await this.flushWriteBehindQueue();
2154
+ }
2155
+ }
2156
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2157
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2158
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2159
+ layer.name,
2160
+ options?.staleWhileRevalidate,
2161
+ this.options.staleWhileRevalidate
2162
+ );
2163
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2164
+ const payload = createStoredValueEnvelope({
2165
+ kind,
2166
+ value,
2167
+ freshTtlSeconds: freshTtl,
2168
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2169
+ staleIfErrorSeconds: staleIfError,
2170
+ now
2171
+ });
2172
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2173
+ return {
2174
+ key,
2175
+ value: payload,
2176
+ ttl
2177
+ };
2178
+ }
2179
+ intersectKeys(groups) {
2180
+ if (groups.length === 0) {
2181
+ return [];
2182
+ }
2183
+ const [firstGroup, ...rest] = groups;
2184
+ if (!firstGroup) {
2185
+ return [];
2186
+ }
2187
+ const restSets = rest.map((group) => new Set(group));
2188
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2189
+ }
2190
+ qualifyKey(key) {
2191
+ const prefix = this.generationPrefix();
2192
+ return prefix ? `${prefix}${key}` : key;
2193
+ }
2194
+ qualifyPattern(pattern) {
2195
+ const prefix = this.generationPrefix();
2196
+ return prefix ? `${prefix}${pattern}` : pattern;
2197
+ }
2198
+ stripQualifiedKey(key) {
2199
+ const prefix = this.generationPrefix();
2200
+ if (!prefix || !key.startsWith(prefix)) {
2201
+ return key;
2202
+ }
2203
+ return key.slice(prefix.length);
2204
+ }
2205
+ generationPrefix() {
2206
+ if (this.currentGeneration === void 0) {
2207
+ return "";
2208
+ }
2209
+ return `v${this.currentGeneration}:`;
2210
+ }
1700
2211
  async deleteKeysFromLayers(layers, keys) {
1701
2212
  await Promise.all(
1702
2213
  layers.map(async (layer) => {
@@ -1740,6 +2251,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1740
2251
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1741
2252
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1742
2253
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2254
+ if (this.options.generation !== void 0) {
2255
+ this.validateNonNegativeNumber("generation", this.options.generation);
2256
+ }
1743
2257
  }
1744
2258
  validateWriteOptions(options) {
1745
2259
  if (!options) {
@@ -1751,6 +2265,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
1751
2265
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1752
2266
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1753
2267
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2268
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1754
2269
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1755
2270
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1756
2271
  }
@@ -1794,6 +2309,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
1794
2309
  }
1795
2310
  return key;
1796
2311
  }
2312
+ validateTtlPolicy(name, policy) {
2313
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2314
+ return;
2315
+ }
2316
+ if ("alignTo" in policy) {
2317
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2318
+ return;
2319
+ }
2320
+ throw new Error(`${name} is invalid.`);
2321
+ }
2322
+ assertActive(operation) {
2323
+ if (this.isDisconnecting) {
2324
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2325
+ }
2326
+ }
2327
+ async awaitStartup(operation) {
2328
+ this.assertActive(operation);
2329
+ await this.startup;
2330
+ this.assertActive(operation);
2331
+ }
1797
2332
  serializeOptions(options) {
1798
2333
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1799
2334
  }
@@ -1899,6 +2434,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
1899
2434
  return value;
1900
2435
  }
1901
2436
  };
2437
+ function createInstanceId() {
2438
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2439
+ }
1902
2440
 
1903
2441
  // src/module.ts
1904
2442
  var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);