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.
@@ -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,95 @@ var CircuitBreakerManager = class {
243
560
  }
244
561
  };
245
562
 
563
+ // ../../src/internal/FetchRateLimiter.ts
564
+ var FetchRateLimiter = class {
565
+ active = 0;
566
+ queue = [];
567
+ startedAt = [];
568
+ drainTimer;
569
+ async schedule(options, task) {
570
+ if (!options) {
571
+ return task();
572
+ }
573
+ const normalized = this.normalize(options);
574
+ if (!normalized) {
575
+ return task();
576
+ }
577
+ return new Promise((resolve, reject) => {
578
+ this.queue.push({ options: normalized, task, resolve, reject });
579
+ this.drain();
580
+ });
581
+ }
582
+ normalize(options) {
583
+ const maxConcurrent = options.maxConcurrent;
584
+ const intervalMs = options.intervalMs;
585
+ const maxPerInterval = options.maxPerInterval;
586
+ if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
587
+ return void 0;
588
+ }
589
+ return {
590
+ maxConcurrent,
591
+ intervalMs,
592
+ maxPerInterval
593
+ };
594
+ }
595
+ drain() {
596
+ if (this.drainTimer) {
597
+ clearTimeout(this.drainTimer);
598
+ this.drainTimer = void 0;
599
+ }
600
+ while (this.queue.length > 0) {
601
+ const next = this.queue[0];
602
+ if (!next) {
603
+ return;
604
+ }
605
+ const waitMs = this.waitTime(next.options);
606
+ if (waitMs > 0) {
607
+ this.drainTimer = setTimeout(() => {
608
+ this.drainTimer = void 0;
609
+ this.drain();
610
+ }, waitMs);
611
+ this.drainTimer.unref?.();
612
+ return;
613
+ }
614
+ this.queue.shift();
615
+ this.active += 1;
616
+ this.startedAt.push(Date.now());
617
+ void next.task().then(next.resolve, next.reject).finally(() => {
618
+ this.active -= 1;
619
+ this.drain();
620
+ });
621
+ }
622
+ }
623
+ waitTime(options) {
624
+ const now = Date.now();
625
+ if (options.maxConcurrent && this.active >= options.maxConcurrent) {
626
+ return 1;
627
+ }
628
+ if (!options.intervalMs || !options.maxPerInterval) {
629
+ return 0;
630
+ }
631
+ this.prune(now, options.intervalMs);
632
+ if (this.startedAt.length < options.maxPerInterval) {
633
+ return 0;
634
+ }
635
+ const oldest = this.startedAt[0];
636
+ if (!oldest) {
637
+ return 0;
638
+ }
639
+ return Math.max(1, options.intervalMs - (now - oldest));
640
+ }
641
+ prune(now, intervalMs) {
642
+ while (this.startedAt.length > 0) {
643
+ const startedAt = this.startedAt[0];
644
+ if (startedAt === void 0 || now - startedAt < intervalMs) {
645
+ break;
646
+ }
647
+ this.startedAt.shift();
648
+ }
649
+ }
650
+ };
651
+
246
652
  // ../../src/internal/MetricsCollector.ts
247
653
  var MetricsCollector = class {
248
654
  data = this.empty();
@@ -439,13 +845,14 @@ var TtlResolver = class {
439
845
  clearProfiles() {
440
846
  this.accessProfiles.clear();
441
847
  }
442
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
848
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
849
+ const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
443
850
  const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
444
851
  layerName,
445
852
  options?.negativeTtl,
446
853
  globalNegativeTtl,
447
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
448
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
854
+ this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
855
+ ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
449
856
  const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
450
857
  const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
451
858
  return this.applyJitter(adaptiveTtl, jitter);
@@ -484,6 +891,29 @@ var TtlResolver = class {
484
891
  const delta = (Math.random() * 2 - 1) * jitter;
485
892
  return Math.max(1, Math.round(ttl + delta));
486
893
  }
894
+ resolvePolicyTtl(key, value, policy) {
895
+ if (!policy) {
896
+ return void 0;
897
+ }
898
+ if (typeof policy === "function") {
899
+ return policy({ key, value });
900
+ }
901
+ const now = /* @__PURE__ */ new Date();
902
+ if (policy === "until-midnight") {
903
+ const nextMidnight = new Date(now);
904
+ nextMidnight.setHours(24, 0, 0, 0);
905
+ return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
906
+ }
907
+ if (policy === "next-hour") {
908
+ const nextHour = new Date(now);
909
+ nextHour.setMinutes(60, 0, 0);
910
+ return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
911
+ }
912
+ const alignToSeconds = policy.alignTo;
913
+ const currentSeconds = Math.floor(Date.now() / 1e3);
914
+ const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
915
+ return Math.max(1, nextBoundary - currentSeconds);
916
+ }
487
917
  readLayerNumber(layerName, value) {
488
918
  if (typeof value === "number") {
489
919
  return value;
@@ -498,306 +928,146 @@ var TtlResolver = class {
498
928
  let removed = 0;
499
929
  for (const key of this.accessProfiles.keys()) {
500
930
  if (removed >= toRemove) {
501
- break;
502
- }
503
- this.accessProfiles.delete(key);
504
- removed += 1;
505
- }
506
- }
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
- };
931
+ break;
932
+ }
933
+ this.accessProfiles.delete(key);
934
+ removed += 1;
935
+ }
739
936
  }
740
- _drainUnlockWaiters() {
741
- for (let weight = this._value; weight > 0; weight--) {
742
- if (!this._weightedWaiters[weight - 1])
937
+ };
938
+
939
+ // ../../src/invalidation/PatternMatcher.ts
940
+ var PatternMatcher = class _PatternMatcher {
941
+ /**
942
+ * Tests whether a glob-style pattern matches a value.
943
+ * Supports `*` (any sequence of characters) and `?` (any single character).
944
+ * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
945
+ * quadratic memory usage on long patterns/keys.
946
+ */
947
+ static matches(pattern, value) {
948
+ return _PatternMatcher.matchLinear(pattern, value);
949
+ }
950
+ /**
951
+ * Linear-time glob matching with O(1) extra memory.
952
+ */
953
+ static matchLinear(pattern, value) {
954
+ let patternIndex = 0;
955
+ let valueIndex = 0;
956
+ let starIndex = -1;
957
+ let backtrackValueIndex = 0;
958
+ while (valueIndex < value.length) {
959
+ const patternChar = pattern[patternIndex];
960
+ const valueChar = value[valueIndex];
961
+ if (patternChar === "*" && patternIndex < pattern.length) {
962
+ starIndex = patternIndex;
963
+ patternIndex += 1;
964
+ backtrackValueIndex = valueIndex;
743
965
  continue;
744
- this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
745
- this._weightedWaiters[weight - 1] = [];
966
+ }
967
+ if (patternChar === "?" || patternChar === valueChar) {
968
+ patternIndex += 1;
969
+ valueIndex += 1;
970
+ continue;
971
+ }
972
+ if (starIndex !== -1) {
973
+ patternIndex = starIndex + 1;
974
+ backtrackValueIndex += 1;
975
+ valueIndex = backtrackValueIndex;
976
+ continue;
977
+ }
978
+ return false;
746
979
  }
980
+ while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
981
+ patternIndex += 1;
982
+ }
983
+ return patternIndex === pattern.length;
747
984
  }
748
985
  };
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
- });
986
+
987
+ // ../../src/invalidation/TagIndex.ts
988
+ var TagIndex = class {
989
+ tagToKeys = /* @__PURE__ */ new Map();
990
+ keyToTags = /* @__PURE__ */ new Map();
991
+ knownKeys = /* @__PURE__ */ new Set();
992
+ maxKnownKeys;
993
+ constructor(options = {}) {
994
+ this.maxKnownKeys = options.maxKnownKeys;
754
995
  }
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
- }
996
+ async touch(key) {
997
+ this.knownKeys.add(key);
998
+ this.pruneKnownKeysIfNeeded();
999
+ }
1000
+ async track(key, tags) {
1001
+ this.knownKeys.add(key);
1002
+ this.pruneKnownKeysIfNeeded();
1003
+ if (tags.length === 0) {
1004
+ return;
762
1005
  }
763
- function rejected(value) {
764
- try {
765
- step(generator["throw"](value));
766
- } catch (e) {
767
- reject(e);
1006
+ const existingTags = this.keyToTags.get(key);
1007
+ if (existingTags) {
1008
+ for (const tag of existingTags) {
1009
+ this.tagToKeys.get(tag)?.delete(key);
768
1010
  }
769
1011
  }
770
- function step(result) {
771
- result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
1012
+ const tagSet = new Set(tags);
1013
+ this.keyToTags.set(key, tagSet);
1014
+ for (const tag of tagSet) {
1015
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
1016
+ keys.add(key);
1017
+ this.tagToKeys.set(tag, keys);
772
1018
  }
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
1019
  }
780
- acquire() {
781
- return __awaiter$1(this, void 0, void 0, function* () {
782
- const [, releaser] = yield this._semaphore.acquire();
783
- return releaser;
784
- });
1020
+ async remove(key) {
1021
+ this.removeKey(key);
785
1022
  }
786
- runExclusive(callback) {
787
- return this._semaphore.runExclusive(() => callback());
1023
+ async keysForTag(tag) {
1024
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
788
1025
  }
789
- isLocked() {
790
- return this._semaphore.isLocked();
1026
+ async keysForPrefix(prefix) {
1027
+ return [...this.knownKeys].filter((key) => key.startsWith(prefix));
791
1028
  }
792
- waitForUnlock() {
793
- return this._semaphore.waitForUnlock();
1029
+ async tagsForKey(key) {
1030
+ return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
794
1031
  }
795
- release() {
796
- if (this._semaphore.isLocked())
797
- this._semaphore.release();
1032
+ async matchPattern(pattern) {
1033
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
798
1034
  }
799
- cancel() {
800
- return this._semaphore.cancel();
1035
+ async clear() {
1036
+ this.tagToKeys.clear();
1037
+ this.keyToTags.clear();
1038
+ this.knownKeys.clear();
1039
+ }
1040
+ pruneKnownKeysIfNeeded() {
1041
+ if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
1042
+ return;
1043
+ }
1044
+ const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
1045
+ let removed = 0;
1046
+ for (const key of this.knownKeys) {
1047
+ if (removed >= toRemove) {
1048
+ break;
1049
+ }
1050
+ this.removeKey(key);
1051
+ removed += 1;
1052
+ }
1053
+ }
1054
+ removeKey(key) {
1055
+ this.knownKeys.delete(key);
1056
+ const tags = this.keyToTags.get(key);
1057
+ if (!tags) {
1058
+ return;
1059
+ }
1060
+ for (const tag of tags) {
1061
+ const keys = this.tagToKeys.get(tag);
1062
+ if (!keys) {
1063
+ continue;
1064
+ }
1065
+ keys.delete(key);
1066
+ if (keys.size === 0) {
1067
+ this.tagToKeys.delete(tag);
1068
+ }
1069
+ }
1070
+ this.keyToTags.delete(key);
801
1071
  }
802
1072
  };
803
1073
 
@@ -879,6 +1149,7 @@ var CacheStack = class extends EventEmitter {
879
1149
  const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
880
1150
  this.ttlResolver = new TtlResolver({ maxProfileEntries });
881
1151
  this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
1152
+ this.currentGeneration = options.generation;
882
1153
  if (options.publishSetInvalidation !== void 0) {
883
1154
  console.warn(
884
1155
  "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
@@ -887,21 +1158,27 @@ var CacheStack = class extends EventEmitter {
887
1158
  const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
888
1159
  this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
889
1160
  this.tagIndex = options.tagIndex ?? new TagIndex();
1161
+ this.initializeWriteBehind(options.writeBehind);
890
1162
  this.startup = this.initialize();
891
1163
  }
892
1164
  layers;
893
1165
  options;
894
1166
  stampedeGuard = new StampedeGuard();
895
1167
  metricsCollector = new MetricsCollector();
896
- instanceId = randomUUID();
1168
+ instanceId = createInstanceId();
897
1169
  startup;
898
1170
  unsubscribeInvalidation;
899
1171
  logger;
900
1172
  tagIndex;
1173
+ fetchRateLimiter = new FetchRateLimiter();
901
1174
  backgroundRefreshes = /* @__PURE__ */ new Map();
902
1175
  layerDegradedUntil = /* @__PURE__ */ new Map();
903
1176
  ttlResolver;
904
1177
  circuitBreakerManager;
1178
+ currentGeneration;
1179
+ writeBehindQueue = [];
1180
+ writeBehindTimer;
1181
+ writeBehindFlushPromise;
905
1182
  isDisconnecting = false;
906
1183
  disconnectPromise;
907
1184
  /**
@@ -911,9 +1188,9 @@ var CacheStack = class extends EventEmitter {
911
1188
  * and no `fetcher` is provided.
912
1189
  */
913
1190
  async get(key, fetcher, options) {
914
- const normalizedKey = this.validateCacheKey(key);
1191
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
915
1192
  this.validateWriteOptions(options);
916
- await this.startup;
1193
+ await this.awaitStartup("get");
917
1194
  const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
918
1195
  if (hit.found) {
919
1196
  this.ttlResolver.recordAccess(normalizedKey);
@@ -978,8 +1255,8 @@ var CacheStack = class extends EventEmitter {
978
1255
  * Returns true if the given key exists and is not expired in any layer.
979
1256
  */
980
1257
  async has(key) {
981
- const normalizedKey = this.validateCacheKey(key);
982
- await this.startup;
1258
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1259
+ await this.awaitStartup("has");
983
1260
  for (const layer of this.layers) {
984
1261
  if (this.shouldSkipLayer(layer)) {
985
1262
  continue;
@@ -1009,8 +1286,8 @@ var CacheStack = class extends EventEmitter {
1009
1286
  * that has it, or null if the key is not found / has no TTL.
1010
1287
  */
1011
1288
  async ttl(key) {
1012
- const normalizedKey = this.validateCacheKey(key);
1013
- await this.startup;
1289
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1290
+ await this.awaitStartup("ttl");
1014
1291
  for (const layer of this.layers) {
1015
1292
  if (this.shouldSkipLayer(layer)) {
1016
1293
  continue;
@@ -1031,17 +1308,17 @@ var CacheStack = class extends EventEmitter {
1031
1308
  * Stores a value in all cache layers. Overwrites any existing value.
1032
1309
  */
1033
1310
  async set(key, value, options) {
1034
- const normalizedKey = this.validateCacheKey(key);
1311
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1035
1312
  this.validateWriteOptions(options);
1036
- await this.startup;
1313
+ await this.awaitStartup("set");
1037
1314
  await this.storeEntry(normalizedKey, "value", value, options);
1038
1315
  }
1039
1316
  /**
1040
1317
  * Deletes the key from all layers and publishes an invalidation message.
1041
1318
  */
1042
1319
  async delete(key) {
1043
- const normalizedKey = this.validateCacheKey(key);
1044
- await this.startup;
1320
+ const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
1321
+ await this.awaitStartup("delete");
1045
1322
  await this.deleteKeys([normalizedKey]);
1046
1323
  await this.publishInvalidation({
1047
1324
  scope: "key",
@@ -1051,7 +1328,7 @@ var CacheStack = class extends EventEmitter {
1051
1328
  });
1052
1329
  }
1053
1330
  async clear() {
1054
- await this.startup;
1331
+ await this.awaitStartup("clear");
1055
1332
  await Promise.all(this.layers.map((layer) => layer.clear()));
1056
1333
  await this.tagIndex.clear();
1057
1334
  this.ttlResolver.clearProfiles();
@@ -1067,23 +1344,25 @@ var CacheStack = class extends EventEmitter {
1067
1344
  if (keys.length === 0) {
1068
1345
  return;
1069
1346
  }
1070
- await this.startup;
1347
+ await this.awaitStartup("mdelete");
1071
1348
  const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
1072
- await this.deleteKeys(normalizedKeys);
1349
+ const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
1350
+ await this.deleteKeys(cacheKeys);
1073
1351
  await this.publishInvalidation({
1074
1352
  scope: "keys",
1075
- keys: normalizedKeys,
1353
+ keys: cacheKeys,
1076
1354
  sourceId: this.instanceId,
1077
1355
  operation: "delete"
1078
1356
  });
1079
1357
  }
1080
1358
  async mget(entries) {
1359
+ this.assertActive("mget");
1081
1360
  if (entries.length === 0) {
1082
1361
  return [];
1083
1362
  }
1084
1363
  const normalizedEntries = entries.map((entry) => ({
1085
1364
  ...entry,
1086
- key: this.validateCacheKey(entry.key)
1365
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1087
1366
  }));
1088
1367
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1089
1368
  const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
@@ -1109,7 +1388,7 @@ var CacheStack = class extends EventEmitter {
1109
1388
  })
1110
1389
  );
1111
1390
  }
1112
- await this.startup;
1391
+ await this.awaitStartup("mget");
1113
1392
  const pending = /* @__PURE__ */ new Set();
1114
1393
  const indexesByKey = /* @__PURE__ */ new Map();
1115
1394
  const resultsByKey = /* @__PURE__ */ new Map();
@@ -1157,14 +1436,17 @@ var CacheStack = class extends EventEmitter {
1157
1436
  return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
1158
1437
  }
1159
1438
  async mset(entries) {
1439
+ this.assertActive("mset");
1160
1440
  const normalizedEntries = entries.map((entry) => ({
1161
1441
  ...entry,
1162
- key: this.validateCacheKey(entry.key)
1442
+ key: this.qualifyKey(this.validateCacheKey(entry.key))
1163
1443
  }));
1164
1444
  normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
1165
- await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
1445
+ await this.awaitStartup("mset");
1446
+ await this.writeBatch(normalizedEntries);
1166
1447
  }
1167
1448
  async warm(entries, options = {}) {
1449
+ this.assertActive("warm");
1168
1450
  const concurrency = Math.max(1, options.concurrency ?? 4);
1169
1451
  const total = entries.length;
1170
1452
  let completed = 0;
@@ -1213,14 +1495,31 @@ var CacheStack = class extends EventEmitter {
1213
1495
  return new CacheNamespace(this, prefix);
1214
1496
  }
1215
1497
  async invalidateByTag(tag) {
1216
- await this.startup;
1498
+ await this.awaitStartup("invalidateByTag");
1217
1499
  const keys = await this.tagIndex.keysForTag(tag);
1218
1500
  await this.deleteKeys(keys);
1219
1501
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1220
1502
  }
1503
+ async invalidateByTags(tags, mode = "any") {
1504
+ if (tags.length === 0) {
1505
+ return;
1506
+ }
1507
+ await this.awaitStartup("invalidateByTags");
1508
+ const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
1509
+ const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
1510
+ await this.deleteKeys(keys);
1511
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1512
+ }
1221
1513
  async invalidateByPattern(pattern) {
1222
- await this.startup;
1223
- const keys = await this.tagIndex.matchPattern(pattern);
1514
+ await this.awaitStartup("invalidateByPattern");
1515
+ const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
1516
+ await this.deleteKeys(keys);
1517
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1518
+ }
1519
+ async invalidateByPrefix(prefix) {
1520
+ await this.awaitStartup("invalidateByPrefix");
1521
+ const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
1522
+ const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
1224
1523
  await this.deleteKeys(keys);
1225
1524
  await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
1226
1525
  }
@@ -1247,14 +1546,43 @@ var CacheStack = class extends EventEmitter {
1247
1546
  getHitRate() {
1248
1547
  return this.metricsCollector.hitRate();
1249
1548
  }
1549
+ async healthCheck() {
1550
+ await this.startup;
1551
+ return Promise.all(
1552
+ this.layers.map(async (layer) => {
1553
+ const startedAt = performance.now();
1554
+ try {
1555
+ const healthy = layer.ping ? await layer.ping() : true;
1556
+ return {
1557
+ layer: layer.name,
1558
+ healthy,
1559
+ latencyMs: performance.now() - startedAt
1560
+ };
1561
+ } catch (error) {
1562
+ return {
1563
+ layer: layer.name,
1564
+ healthy: false,
1565
+ latencyMs: performance.now() - startedAt,
1566
+ error: this.formatError(error)
1567
+ };
1568
+ }
1569
+ })
1570
+ );
1571
+ }
1572
+ bumpGeneration(nextGeneration) {
1573
+ const current = this.currentGeneration ?? 0;
1574
+ this.currentGeneration = nextGeneration ?? current + 1;
1575
+ return this.currentGeneration;
1576
+ }
1250
1577
  /**
1251
1578
  * Returns detailed metadata about a single cache key: which layers contain it,
1252
1579
  * remaining fresh/stale/error TTLs, and associated tags.
1253
1580
  * Returns `null` if the key does not exist in any layer.
1254
1581
  */
1255
1582
  async inspect(key) {
1256
- const normalizedKey = this.validateCacheKey(key);
1257
- await this.startup;
1583
+ const userKey = this.validateCacheKey(key);
1584
+ const normalizedKey = this.qualifyKey(userKey);
1585
+ await this.awaitStartup("inspect");
1258
1586
  const foundInLayers = [];
1259
1587
  let freshTtlSeconds = null;
1260
1588
  let staleTtlSeconds = null;
@@ -1285,10 +1613,10 @@ var CacheStack = class extends EventEmitter {
1285
1613
  return null;
1286
1614
  }
1287
1615
  const tags = await this.getTagsForKey(normalizedKey);
1288
- return { key: normalizedKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1616
+ return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
1289
1617
  }
1290
1618
  async exportState() {
1291
- await this.startup;
1619
+ await this.awaitStartup("exportState");
1292
1620
  const exported = /* @__PURE__ */ new Map();
1293
1621
  for (const layer of this.layers) {
1294
1622
  if (!layer.keys) {
@@ -1296,15 +1624,16 @@ var CacheStack = class extends EventEmitter {
1296
1624
  }
1297
1625
  const keys = await layer.keys();
1298
1626
  for (const key of keys) {
1299
- if (exported.has(key)) {
1627
+ const exportedKey = this.stripQualifiedKey(key);
1628
+ if (exported.has(exportedKey)) {
1300
1629
  continue;
1301
1630
  }
1302
1631
  const stored = await this.readLayerEntry(layer, key);
1303
1632
  if (stored === null) {
1304
1633
  continue;
1305
1634
  }
1306
- exported.set(key, {
1307
- key,
1635
+ exported.set(exportedKey, {
1636
+ key: exportedKey,
1308
1637
  value: stored,
1309
1638
  ttl: remainingStoredTtlSeconds(stored)
1310
1639
  });
@@ -1313,19 +1642,24 @@ var CacheStack = class extends EventEmitter {
1313
1642
  return [...exported.values()];
1314
1643
  }
1315
1644
  async importState(entries) {
1316
- await this.startup;
1645
+ await this.awaitStartup("importState");
1317
1646
  await Promise.all(
1318
1647
  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);
1648
+ const qualifiedKey = this.qualifyKey(entry.key);
1649
+ await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
1650
+ await this.tagIndex.touch(qualifiedKey);
1321
1651
  })
1322
1652
  );
1323
1653
  }
1324
1654
  async persistToFile(filePath) {
1655
+ this.assertActive("persistToFile");
1325
1656
  const snapshot = await this.exportState();
1657
+ const { promises: fs } = await import("fs");
1326
1658
  await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
1327
1659
  }
1328
1660
  async restoreFromFile(filePath) {
1661
+ this.assertActive("restoreFromFile");
1662
+ const { promises: fs } = await import("fs");
1329
1663
  const raw = await fs.readFile(filePath, "utf8");
1330
1664
  let parsed;
1331
1665
  try {
@@ -1349,7 +1683,13 @@ var CacheStack = class extends EventEmitter {
1349
1683
  this.disconnectPromise = (async () => {
1350
1684
  await this.startup;
1351
1685
  await this.unsubscribeInvalidation?.();
1686
+ await this.flushWriteBehindQueue();
1352
1687
  await Promise.allSettled([...this.backgroundRefreshes.values()]);
1688
+ if (this.writeBehindTimer) {
1689
+ clearInterval(this.writeBehindTimer);
1690
+ this.writeBehindTimer = void 0;
1691
+ }
1692
+ await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
1353
1693
  })();
1354
1694
  }
1355
1695
  await this.disconnectPromise;
@@ -1409,7 +1749,10 @@ var CacheStack = class extends EventEmitter {
1409
1749
  const fetchStart = Date.now();
1410
1750
  let fetched;
1411
1751
  try {
1412
- fetched = await fetcher();
1752
+ fetched = await this.fetchRateLimiter.schedule(
1753
+ options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
1754
+ fetcher
1755
+ );
1413
1756
  this.circuitBreakerManager.recordSuccess(key);
1414
1757
  this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
1415
1758
  } catch (error) {
@@ -1443,6 +1786,61 @@ var CacheStack = class extends EventEmitter {
1443
1786
  await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
1444
1787
  }
1445
1788
  }
1789
+ async writeBatch(entries) {
1790
+ const now = Date.now();
1791
+ const entriesByLayer = /* @__PURE__ */ new Map();
1792
+ const immediateOperations = [];
1793
+ const deferredOperations = [];
1794
+ for (const entry of entries) {
1795
+ for (const layer of this.layers) {
1796
+ if (this.shouldSkipLayer(layer)) {
1797
+ continue;
1798
+ }
1799
+ const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1800
+ const bucket = entriesByLayer.get(layer) ?? [];
1801
+ bucket.push(layerEntry);
1802
+ entriesByLayer.set(layer, bucket);
1803
+ }
1804
+ }
1805
+ for (const [layer, layerEntries] of entriesByLayer.entries()) {
1806
+ const operation = async () => {
1807
+ try {
1808
+ if (layer.setMany) {
1809
+ await layer.setMany(layerEntries);
1810
+ return;
1811
+ }
1812
+ await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1813
+ } catch (error) {
1814
+ await this.handleLayerFailure(layer, "write", error);
1815
+ }
1816
+ };
1817
+ if (this.shouldWriteBehind(layer)) {
1818
+ deferredOperations.push(operation);
1819
+ } else {
1820
+ immediateOperations.push(operation);
1821
+ }
1822
+ }
1823
+ await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1824
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1825
+ for (const entry of entries) {
1826
+ if (entry.options?.tags) {
1827
+ await this.tagIndex.track(entry.key, entry.options.tags);
1828
+ } else {
1829
+ await this.tagIndex.touch(entry.key);
1830
+ }
1831
+ this.metricsCollector.increment("sets");
1832
+ this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1833
+ this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
1834
+ }
1835
+ if (this.shouldBroadcastL1Invalidation()) {
1836
+ await this.publishInvalidation({
1837
+ scope: "keys",
1838
+ keys: entries.map((entry) => entry.key),
1839
+ sourceId: this.instanceId,
1840
+ operation: "write"
1841
+ });
1842
+ }
1843
+ }
1446
1844
  async readFromLayers(key, options, mode) {
1447
1845
  let sawRetainableValue = false;
1448
1846
  for (let index = 0; index < this.layers.length; index += 1) {
@@ -1526,33 +1924,28 @@ var CacheStack = class extends EventEmitter {
1526
1924
  }
1527
1925
  async writeAcrossLayers(key, kind, value, options) {
1528
1926
  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);
1927
+ const immediateOperations = [];
1928
+ const deferredOperations = [];
1929
+ for (const layer of this.layers) {
1930
+ const operation = async () => {
1931
+ if (this.shouldSkipLayer(layer)) {
1932
+ return;
1933
+ }
1934
+ const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
1935
+ try {
1936
+ await layer.set(entry.key, entry.value, entry.ttl);
1937
+ } catch (error) {
1938
+ await this.handleLayerFailure(layer, "write", error);
1939
+ }
1940
+ };
1941
+ if (this.shouldWriteBehind(layer)) {
1942
+ deferredOperations.push(operation);
1943
+ } else {
1944
+ immediateOperations.push(operation);
1553
1945
  }
1554
- });
1555
- await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
1946
+ }
1947
+ await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1948
+ await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
1556
1949
  }
1557
1950
  async executeLayerOperations(operations, context) {
1558
1951
  if (this.options.writePolicy !== "best-effort") {
@@ -1576,8 +1969,17 @@ var CacheStack = class extends EventEmitter {
1576
1969
  );
1577
1970
  }
1578
1971
  }
1579
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
1580
- return this.ttlResolver.resolveFreshTtl(key, layerName, kind, options, fallbackTtl, this.options.negativeTtl);
1972
+ resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
1973
+ return this.ttlResolver.resolveFreshTtl(
1974
+ key,
1975
+ layerName,
1976
+ kind,
1977
+ options,
1978
+ fallbackTtl,
1979
+ this.options.negativeTtl,
1980
+ void 0,
1981
+ value
1982
+ );
1581
1983
  }
1582
1984
  resolveLayerSeconds(layerName, override, globalDefault, fallback) {
1583
1985
  return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
@@ -1671,6 +2073,105 @@ var CacheStack = class extends EventEmitter {
1671
2073
  shouldBroadcastL1Invalidation() {
1672
2074
  return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
1673
2075
  }
2076
+ initializeWriteBehind(options) {
2077
+ if (this.options.writeStrategy !== "write-behind") {
2078
+ return;
2079
+ }
2080
+ const flushIntervalMs = options?.flushIntervalMs;
2081
+ if (!flushIntervalMs || flushIntervalMs <= 0) {
2082
+ return;
2083
+ }
2084
+ this.writeBehindTimer = setInterval(() => {
2085
+ void this.flushWriteBehindQueue();
2086
+ }, flushIntervalMs);
2087
+ this.writeBehindTimer.unref?.();
2088
+ }
2089
+ shouldWriteBehind(layer) {
2090
+ return this.options.writeStrategy === "write-behind" && !layer.isLocal;
2091
+ }
2092
+ async enqueueWriteBehind(operation) {
2093
+ this.writeBehindQueue.push(operation);
2094
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2095
+ const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
2096
+ if (this.writeBehindQueue.length >= batchSize) {
2097
+ await this.flushWriteBehindQueue();
2098
+ return;
2099
+ }
2100
+ if (this.writeBehindQueue.length >= maxQueueSize) {
2101
+ await this.flushWriteBehindQueue();
2102
+ }
2103
+ }
2104
+ async flushWriteBehindQueue() {
2105
+ if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
2106
+ await this.writeBehindFlushPromise;
2107
+ return;
2108
+ }
2109
+ const batchSize = this.options.writeBehind?.batchSize ?? 100;
2110
+ const batch = this.writeBehindQueue.splice(0, batchSize);
2111
+ this.writeBehindFlushPromise = (async () => {
2112
+ await Promise.allSettled(batch.map((operation) => operation()));
2113
+ })();
2114
+ await this.writeBehindFlushPromise;
2115
+ this.writeBehindFlushPromise = void 0;
2116
+ if (this.writeBehindQueue.length > 0) {
2117
+ await this.flushWriteBehindQueue();
2118
+ }
2119
+ }
2120
+ buildLayerSetEntry(layer, key, kind, value, options, now) {
2121
+ const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
2122
+ const staleWhileRevalidate = this.resolveLayerSeconds(
2123
+ layer.name,
2124
+ options?.staleWhileRevalidate,
2125
+ this.options.staleWhileRevalidate
2126
+ );
2127
+ const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
2128
+ const payload = createStoredValueEnvelope({
2129
+ kind,
2130
+ value,
2131
+ freshTtlSeconds: freshTtl,
2132
+ staleWhileRevalidateSeconds: staleWhileRevalidate,
2133
+ staleIfErrorSeconds: staleIfError,
2134
+ now
2135
+ });
2136
+ const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
2137
+ return {
2138
+ key,
2139
+ value: payload,
2140
+ ttl
2141
+ };
2142
+ }
2143
+ intersectKeys(groups) {
2144
+ if (groups.length === 0) {
2145
+ return [];
2146
+ }
2147
+ const [firstGroup, ...rest] = groups;
2148
+ if (!firstGroup) {
2149
+ return [];
2150
+ }
2151
+ const restSets = rest.map((group) => new Set(group));
2152
+ return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
2153
+ }
2154
+ qualifyKey(key) {
2155
+ const prefix = this.generationPrefix();
2156
+ return prefix ? `${prefix}${key}` : key;
2157
+ }
2158
+ qualifyPattern(pattern) {
2159
+ const prefix = this.generationPrefix();
2160
+ return prefix ? `${prefix}${pattern}` : pattern;
2161
+ }
2162
+ stripQualifiedKey(key) {
2163
+ const prefix = this.generationPrefix();
2164
+ if (!prefix || !key.startsWith(prefix)) {
2165
+ return key;
2166
+ }
2167
+ return key.slice(prefix.length);
2168
+ }
2169
+ generationPrefix() {
2170
+ if (this.currentGeneration === void 0) {
2171
+ return "";
2172
+ }
2173
+ return `v${this.currentGeneration}:`;
2174
+ }
1674
2175
  async deleteKeysFromLayers(layers, keys) {
1675
2176
  await Promise.all(
1676
2177
  layers.map(async (layer) => {
@@ -1714,6 +2215,9 @@ var CacheStack = class extends EventEmitter {
1714
2215
  this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
1715
2216
  this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
1716
2217
  this.validateCircuitBreakerOptions(this.options.circuitBreaker);
2218
+ if (this.options.generation !== void 0) {
2219
+ this.validateNonNegativeNumber("generation", this.options.generation);
2220
+ }
1717
2221
  }
1718
2222
  validateWriteOptions(options) {
1719
2223
  if (!options) {
@@ -1725,6 +2229,7 @@ var CacheStack = class extends EventEmitter {
1725
2229
  this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
1726
2230
  this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
1727
2231
  this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
2232
+ this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
1728
2233
  this.validateAdaptiveTtlOptions(options.adaptiveTtl);
1729
2234
  this.validateCircuitBreakerOptions(options.circuitBreaker);
1730
2235
  }
@@ -1768,6 +2273,26 @@ var CacheStack = class extends EventEmitter {
1768
2273
  }
1769
2274
  return key;
1770
2275
  }
2276
+ validateTtlPolicy(name, policy) {
2277
+ if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
2278
+ return;
2279
+ }
2280
+ if ("alignTo" in policy) {
2281
+ this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
2282
+ return;
2283
+ }
2284
+ throw new Error(`${name} is invalid.`);
2285
+ }
2286
+ assertActive(operation) {
2287
+ if (this.isDisconnecting) {
2288
+ throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
2289
+ }
2290
+ }
2291
+ async awaitStartup(operation) {
2292
+ this.assertActive(operation);
2293
+ await this.startup;
2294
+ this.assertActive(operation);
2295
+ }
1771
2296
  serializeOptions(options) {
1772
2297
  return JSON.stringify(this.normalizeForSerialization(options) ?? null);
1773
2298
  }
@@ -1873,6 +2398,9 @@ var CacheStack = class extends EventEmitter {
1873
2398
  return value;
1874
2399
  }
1875
2400
  };
2401
+ function createInstanceId() {
2402
+ return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2403
+ }
1876
2404
 
1877
2405
  // src/module.ts
1878
2406
  var InjectCacheStack = () => Inject(CACHE_STACK);