layercache 1.3.1 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3915 +0,0 @@
1
- var __defProp = Object.defineProperty;
2
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
- var __decorateClass = (decorators, target, key, kind) => {
4
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
5
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
6
- if (decorator = decorators[i])
7
- result = (kind ? decorator(target, key, result) : decorator(result)) || result;
8
- if (kind && result) __defProp(target, key, result);
9
- return result;
10
- };
11
-
12
- // src/constants.ts
13
- var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
14
-
15
- // ../../src/decorators/createCachedMethodDecorator.ts
16
- function createCachedMethodDecorator(options) {
17
- const wrappedByInstance = /* @__PURE__ */ new WeakMap();
18
- return ((_, propertyKey, descriptor) => {
19
- const original = descriptor.value;
20
- if (typeof original !== "function") {
21
- throw new Error("createCachedMethodDecorator can only be applied to methods.");
22
- }
23
- descriptor.value = async function(...args) {
24
- const instance = this;
25
- let wrapped = wrappedByInstance.get(instance);
26
- if (!wrapped) {
27
- const cache = options.cache(instance);
28
- wrapped = cache.wrap(
29
- options.prefix ?? String(propertyKey),
30
- (...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
31
- options
32
- );
33
- wrappedByInstance.set(instance, wrapped);
34
- }
35
- return wrapped(...args);
36
- };
37
- });
38
- }
39
-
40
- // src/decorators.ts
41
- function Cacheable(options) {
42
- return createCachedMethodDecorator(options);
43
- }
44
-
45
- // src/module.ts
46
- import { Global, Inject, Module } from "@nestjs/common";
47
-
48
- // ../../src/CacheStack.ts
49
- import { EventEmitter } from "events";
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
- };
225
-
226
- // ../../src/internal/CacheNamespaceMetrics.ts
227
- function createEmptyNamespaceMetrics(resetAt = Date.now()) {
228
- return {
229
- hits: 0,
230
- misses: 0,
231
- fetches: 0,
232
- sets: 0,
233
- deletes: 0,
234
- backfills: 0,
235
- invalidations: 0,
236
- staleHits: 0,
237
- refreshes: 0,
238
- refreshErrors: 0,
239
- writeFailures: 0,
240
- singleFlightWaits: 0,
241
- negativeCacheHits: 0,
242
- circuitBreakerTrips: 0,
243
- degradedOperations: 0,
244
- hitsByLayer: {},
245
- missesByLayer: {},
246
- latencyByLayer: {},
247
- resetAt
248
- };
249
- }
250
- function cloneNamespaceMetrics(metrics) {
251
- return {
252
- ...metrics,
253
- hitsByLayer: { ...metrics.hitsByLayer },
254
- missesByLayer: { ...metrics.missesByLayer },
255
- latencyByLayer: Object.fromEntries(
256
- Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
257
- )
258
- };
259
- }
260
- function diffNamespaceMetrics(before, after) {
261
- const latencyByLayer = Object.fromEntries(
262
- Object.entries(after.latencyByLayer).map(([layer, value]) => [
263
- layer,
264
- {
265
- avgMs: value.avgMs,
266
- maxMs: value.maxMs,
267
- count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
268
- }
269
- ])
270
- );
271
- return {
272
- hits: after.hits - before.hits,
273
- misses: after.misses - before.misses,
274
- fetches: after.fetches - before.fetches,
275
- sets: after.sets - before.sets,
276
- deletes: after.deletes - before.deletes,
277
- backfills: after.backfills - before.backfills,
278
- invalidations: after.invalidations - before.invalidations,
279
- staleHits: after.staleHits - before.staleHits,
280
- refreshes: after.refreshes - before.refreshes,
281
- refreshErrors: after.refreshErrors - before.refreshErrors,
282
- writeFailures: after.writeFailures - before.writeFailures,
283
- singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
284
- negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
285
- circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
286
- degradedOperations: after.degradedOperations - before.degradedOperations,
287
- hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
288
- missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
289
- latencyByLayer,
290
- resetAt: after.resetAt
291
- };
292
- }
293
- function addNamespaceMetrics(base, delta) {
294
- return {
295
- hits: base.hits + delta.hits,
296
- misses: base.misses + delta.misses,
297
- fetches: base.fetches + delta.fetches,
298
- sets: base.sets + delta.sets,
299
- deletes: base.deletes + delta.deletes,
300
- backfills: base.backfills + delta.backfills,
301
- invalidations: base.invalidations + delta.invalidations,
302
- staleHits: base.staleHits + delta.staleHits,
303
- refreshes: base.refreshes + delta.refreshes,
304
- refreshErrors: base.refreshErrors + delta.refreshErrors,
305
- writeFailures: base.writeFailures + delta.writeFailures,
306
- singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
307
- negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
308
- circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
309
- degradedOperations: base.degradedOperations + delta.degradedOperations,
310
- hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
311
- missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
312
- latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
313
- resetAt: base.resetAt
314
- };
315
- }
316
- function computeNamespaceHitRate(metrics) {
317
- const total = metrics.hits + metrics.misses;
318
- const overall = total === 0 ? 0 : metrics.hits / total;
319
- const byLayer = {};
320
- const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
321
- for (const layer of layers) {
322
- const hits = metrics.hitsByLayer[layer] ?? 0;
323
- const misses = metrics.missesByLayer[layer] ?? 0;
324
- byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
325
- }
326
- return { overall, byLayer };
327
- }
328
- function diffMetricMap(before, after) {
329
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
330
- const result = {};
331
- for (const key of keys) {
332
- result[key] = (after[key] ?? 0) - (before[key] ?? 0);
333
- }
334
- return result;
335
- }
336
- function addMetricMap(base, delta) {
337
- const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
338
- const result = {};
339
- for (const key of keys) {
340
- result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
341
- }
342
- return result;
343
- }
344
-
345
- // ../../src/CacheNamespace.ts
346
- var CacheNamespace = class _CacheNamespace {
347
- constructor(cache, prefix) {
348
- this.cache = cache;
349
- this.prefix = prefix;
350
- validateNamespaceKey(prefix);
351
- }
352
- cache;
353
- prefix;
354
- static metricsMutexes = /* @__PURE__ */ new WeakMap();
355
- metrics = createEmptyNamespaceMetrics();
356
- async get(key, fetcher, options) {
357
- return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
358
- }
359
- async getOrSet(key, fetcher, options) {
360
- return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
361
- }
362
- /**
363
- * Like `get()`, but throws `CacheMissError` instead of returning `null`.
364
- */
365
- async getOrThrow(key, fetcher, options) {
366
- return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
367
- }
368
- async has(key) {
369
- return this.trackMetrics(() => this.cache.has(this.qualify(key)));
370
- }
371
- async ttl(key) {
372
- return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
373
- }
374
- async set(key, value, options) {
375
- await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
376
- }
377
- async delete(key) {
378
- await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
379
- }
380
- async mdelete(keys) {
381
- await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
382
- }
383
- async clear() {
384
- await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
385
- }
386
- async mget(entries) {
387
- return this.trackMetrics(
388
- () => this.cache.mget(
389
- entries.map((entry) => ({
390
- ...entry,
391
- key: this.qualify(entry.key),
392
- options: this.qualifyGetOptions(entry.options)
393
- }))
394
- )
395
- );
396
- }
397
- async mset(entries) {
398
- await this.trackMetrics(
399
- () => this.cache.mset(
400
- entries.map((entry) => ({
401
- ...entry,
402
- key: this.qualify(entry.key),
403
- options: this.qualifyWriteOptions(entry.options)
404
- }))
405
- )
406
- );
407
- }
408
- async invalidateByTag(tag) {
409
- await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
410
- }
411
- async invalidateByTags(tags, mode = "any") {
412
- await this.trackMetrics(
413
- () => this.cache.invalidateByTags(
414
- tags.map((tag) => this.qualifyTag(tag)),
415
- mode
416
- )
417
- );
418
- }
419
- async invalidateByPattern(pattern) {
420
- await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
421
- }
422
- async invalidateByPrefix(prefix) {
423
- await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
424
- }
425
- /**
426
- * Returns detailed metadata about a single cache key within this namespace.
427
- */
428
- async inspect(key) {
429
- const result = await this.cache.inspect(this.qualify(key));
430
- if (result === null) {
431
- return null;
432
- }
433
- return {
434
- ...result,
435
- tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
436
- };
437
- }
438
- wrap(keyPrefix, fetcher, options) {
439
- return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
440
- }
441
- warm(entries, options) {
442
- return this.cache.warm(
443
- entries.map((entry) => ({
444
- ...entry,
445
- key: this.qualify(entry.key),
446
- options: this.qualifyGetOptions(entry.options)
447
- })),
448
- options
449
- );
450
- }
451
- getMetrics() {
452
- return cloneNamespaceMetrics(this.metrics);
453
- }
454
- getHitRate() {
455
- return computeNamespaceHitRate(this.metrics);
456
- }
457
- /**
458
- * Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
459
- *
460
- * ```ts
461
- * const tenant = cache.namespace('tenant:abc')
462
- * const posts = tenant.namespace('posts')
463
- * // keys become: "tenant:abc:posts:mykey"
464
- * ```
465
- */
466
- namespace(childPrefix) {
467
- validateNamespaceKey(childPrefix);
468
- return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
469
- }
470
- qualify(key) {
471
- return `${this.prefix}:${key}`;
472
- }
473
- qualifyTag(tag) {
474
- return `${this.prefix}:${tag}`;
475
- }
476
- qualifyGetOptions(options) {
477
- return this.qualifyWriteOptions(options);
478
- }
479
- qualifyWrapOptions(options) {
480
- return this.qualifyWriteOptions(options);
481
- }
482
- qualifyWriteOptions(options) {
483
- if (!options?.tags || options.tags.length === 0) {
484
- return options;
485
- }
486
- return {
487
- ...options,
488
- tags: options.tags.map((tag) => this.qualifyTag(tag))
489
- };
490
- }
491
- async trackMetrics(operation) {
492
- return this.getMetricsMutex().runExclusive(async () => {
493
- const before = this.cache.getMetrics();
494
- const result = await operation();
495
- const after = this.cache.getMetrics();
496
- this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
497
- return result;
498
- });
499
- }
500
- getMetricsMutex() {
501
- const existing = _CacheNamespace.metricsMutexes.get(this.cache);
502
- if (existing) {
503
- return existing;
504
- }
505
- const mutex = new Mutex();
506
- _CacheNamespace.metricsMutexes.set(this.cache, mutex);
507
- return mutex;
508
- }
509
- };
510
- function validateNamespaceKey(key) {
511
- if (key.length === 0) {
512
- throw new Error("Namespace prefix must not be empty.");
513
- }
514
- if (key.length > 256) {
515
- throw new Error("Namespace prefix must be at most 256 characters.");
516
- }
517
- if (/[\u0000-\u001F\u007F]/.test(key)) {
518
- throw new Error("Namespace prefix contains unsupported control characters.");
519
- }
520
- if (/[\uD800-\uDFFF]/.test(key)) {
521
- throw new Error("Namespace prefix contains unsupported surrogate code points.");
522
- }
523
- }
524
-
525
- // ../../src/invalidation/PatternMatcher.ts
526
- var PatternMatcher = class _PatternMatcher {
527
- /**
528
- * Tests whether a glob-style pattern matches a value.
529
- * Supports `*` (any sequence of characters) and `?` (any single character).
530
- * Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
531
- * quadratic memory usage on long patterns/keys.
532
- */
533
- static matches(pattern, value) {
534
- return _PatternMatcher.matchLinear(pattern, value);
535
- }
536
- /**
537
- * Linear-time glob matching with O(1) extra memory.
538
- */
539
- static matchLinear(pattern, value) {
540
- let patternIndex = 0;
541
- let valueIndex = 0;
542
- let starIndex = -1;
543
- let backtrackValueIndex = 0;
544
- while (valueIndex < value.length) {
545
- const patternChar = pattern[patternIndex];
546
- const valueChar = value[valueIndex];
547
- if (patternChar === "*" && patternIndex < pattern.length) {
548
- starIndex = patternIndex;
549
- patternIndex += 1;
550
- backtrackValueIndex = valueIndex;
551
- continue;
552
- }
553
- if (patternChar === "?" || patternChar === valueChar) {
554
- patternIndex += 1;
555
- valueIndex += 1;
556
- continue;
557
- }
558
- if (starIndex !== -1) {
559
- patternIndex = starIndex + 1;
560
- backtrackValueIndex += 1;
561
- valueIndex = backtrackValueIndex;
562
- continue;
563
- }
564
- return false;
565
- }
566
- while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
567
- patternIndex += 1;
568
- }
569
- return patternIndex === pattern.length;
570
- }
571
- };
572
-
573
- // ../../src/internal/CacheKeyDiscovery.ts
574
- var CacheKeyDiscovery = class {
575
- constructor(options) {
576
- this.options = options;
577
- }
578
- options;
579
- async collectKeysWithPrefix(prefix, maxMatches = false) {
580
- const { tagIndex } = this.options;
581
- const matches = /* @__PURE__ */ new Set();
582
- if (tagIndex.forEachKeyForPrefix) {
583
- await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
584
- matches.add(key);
585
- this.assertWithinMatchLimit(matches, maxMatches);
586
- });
587
- } else {
588
- const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
589
- for (const key of initialMatches) {
590
- matches.add(key);
591
- this.assertWithinMatchLimit(matches, maxMatches);
592
- }
593
- }
594
- await Promise.all(
595
- this.options.layers.map(async (layer) => {
596
- if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
597
- return;
598
- }
599
- try {
600
- if (layer.forEachKey) {
601
- await layer.forEachKey(async (key) => {
602
- if (key.startsWith(prefix)) {
603
- matches.add(key);
604
- this.assertWithinMatchLimit(matches, maxMatches);
605
- }
606
- });
607
- return;
608
- }
609
- const keys = await layer.keys?.();
610
- for (const key of keys ?? []) {
611
- if (key.startsWith(prefix)) {
612
- matches.add(key);
613
- this.assertWithinMatchLimit(matches, maxMatches);
614
- }
615
- }
616
- } catch (error) {
617
- await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
618
- }
619
- })
620
- );
621
- return [...matches];
622
- }
623
- async collectKeysMatchingPattern(pattern, maxMatches = false) {
624
- const matches = /* @__PURE__ */ new Set();
625
- if (this.options.tagIndex.forEachKeyMatchingPattern) {
626
- await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
627
- matches.add(key);
628
- this.assertWithinMatchLimit(matches, maxMatches);
629
- });
630
- } else {
631
- for (const key of await this.options.tagIndex.matchPattern(pattern)) {
632
- matches.add(key);
633
- this.assertWithinMatchLimit(matches, maxMatches);
634
- }
635
- }
636
- await Promise.all(
637
- this.options.layers.map(async (layer) => {
638
- if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
639
- return;
640
- }
641
- try {
642
- if (layer.forEachKey) {
643
- await layer.forEachKey(async (key) => {
644
- if (PatternMatcher.matches(pattern, key)) {
645
- matches.add(key);
646
- this.assertWithinMatchLimit(matches, maxMatches);
647
- }
648
- });
649
- return;
650
- }
651
- const keys = await layer.keys?.();
652
- for (const key of keys ?? []) {
653
- if (PatternMatcher.matches(pattern, key)) {
654
- matches.add(key);
655
- this.assertWithinMatchLimit(matches, maxMatches);
656
- }
657
- }
658
- } catch (error) {
659
- await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
660
- }
661
- })
662
- );
663
- return [...matches];
664
- }
665
- assertWithinMatchLimit(matches, maxMatches) {
666
- if (maxMatches !== false && matches.size > maxMatches) {
667
- throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
668
- }
669
- }
670
- };
671
-
672
- // ../../src/internal/CacheKeySerialization.ts
673
- var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
674
- function normalizeForSerialization(value) {
675
- if (Array.isArray(value)) {
676
- return value.map((entry) => normalizeForSerialization(entry));
677
- }
678
- if (value && typeof value === "object") {
679
- return Object.keys(value).sort().reduce((normalized, key) => {
680
- if (DANGEROUS_OBJECT_KEYS.has(key)) {
681
- return normalized;
682
- }
683
- normalized[key] = normalizeForSerialization(value[key]);
684
- return normalized;
685
- }, {});
686
- }
687
- return value;
688
- }
689
- function serializeKeyPart(value) {
690
- if (typeof value === "string") {
691
- return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
692
- }
693
- if (typeof value === "number") {
694
- return `n:${value}`;
695
- }
696
- if (typeof value === "boolean") {
697
- return `b:${value}`;
698
- }
699
- return `j:${JSON.stringify(normalizeForSerialization(value))}`;
700
- }
701
- function serializeOptions(options) {
702
- return JSON.stringify(normalizeForSerialization(options) ?? null);
703
- }
704
- function createInstanceId() {
705
- if (globalThis.crypto?.randomUUID) {
706
- return globalThis.crypto.randomUUID();
707
- }
708
- if (globalThis.crypto?.getRandomValues) {
709
- const bytes = new Uint8Array(16);
710
- globalThis.crypto.getRandomValues(bytes);
711
- return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
712
- }
713
- throw new Error(
714
- "layercache requires a cryptographic random source. Neither crypto.randomUUID nor crypto.getRandomValues is available in this runtime."
715
- );
716
- }
717
-
718
- // ../../src/internal/CacheStackGeneration.ts
719
- var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
720
- function generationPrefix(generation) {
721
- return generation === void 0 ? "" : `v${generation}:`;
722
- }
723
- function qualifyGenerationKey(key, generation) {
724
- const prefix = generationPrefix(generation);
725
- return prefix ? `${prefix}${key}` : key;
726
- }
727
- function qualifyGenerationPattern(pattern, generation) {
728
- return qualifyGenerationKey(pattern, generation);
729
- }
730
- function stripGenerationPrefix(key, generation) {
731
- const prefix = generationPrefix(generation);
732
- if (!prefix || !key.startsWith(prefix)) {
733
- return key;
734
- }
735
- return key.slice(prefix.length);
736
- }
737
- function resolveGenerationCleanupTarget({
738
- previousGeneration,
739
- nextGeneration,
740
- generationCleanup
741
- }) {
742
- if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
743
- return null;
744
- }
745
- return previousGeneration;
746
- }
747
- function resolveGenerationCleanupBatchSize(generationCleanup) {
748
- if (typeof generationCleanup !== "object" || generationCleanup === null) {
749
- return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
750
- }
751
- return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
752
- }
753
- function planGenerationCleanupBatches(keys, generationCleanup) {
754
- if (keys.length === 0) {
755
- return [];
756
- }
757
- const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
758
- const batches = [];
759
- for (let index = 0; index < keys.length; index += batchSize) {
760
- batches.push(keys.slice(index, index + batchSize));
761
- }
762
- return batches;
763
- }
764
-
765
- // ../../src/internal/CacheStackInvalidationSupport.ts
766
- var CacheStackInvalidationSupport = class {
767
- constructor(options) {
768
- this.options = options;
769
- }
770
- options;
771
- async collectKeysForTag(tag, maxKeys) {
772
- const keys = /* @__PURE__ */ new Set();
773
- if (this.options.tagIndex.forEachKeyForTag) {
774
- await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
775
- keys.add(key);
776
- this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
777
- });
778
- return [...keys];
779
- }
780
- for (const key of await this.options.tagIndex.keysForTag(tag)) {
781
- keys.add(key);
782
- this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
783
- }
784
- return [...keys];
785
- }
786
- intersectKeys(groups) {
787
- if (groups.length === 0) {
788
- return [];
789
- }
790
- const [firstGroup, ...rest] = groups;
791
- const restSets = rest.map((group) => new Set(group));
792
- return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
793
- }
794
- async deleteKeysFromLayers(layers, keys) {
795
- await Promise.all(
796
- layers.map(async (layer) => {
797
- if (this.options.shouldSkipLayer(layer)) {
798
- return;
799
- }
800
- if (layer.deleteMany) {
801
- try {
802
- await layer.deleteMany(keys);
803
- } catch (error) {
804
- await this.options.handleLayerFailure(layer, "delete", error);
805
- }
806
- return;
807
- }
808
- await Promise.all(
809
- keys.map(async (key) => {
810
- try {
811
- await layer.delete(key);
812
- } catch (error) {
813
- await this.options.handleLayerFailure(layer, "delete", error);
814
- }
815
- })
816
- );
817
- })
818
- );
819
- }
820
- assertWithinInvalidationKeyLimit(size, maxKeys) {
821
- if (maxKeys !== false && size > maxKeys) {
822
- throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
823
- }
824
- }
825
- };
826
-
827
- // ../../src/internal/StoredValue.ts
828
- function isStoredValueEnvelope(value) {
829
- if (typeof value !== "object" || value === null) {
830
- return false;
831
- }
832
- const v = value;
833
- if (v.__layercache !== 1) {
834
- return false;
835
- }
836
- if (v.kind !== "value" && v.kind !== "empty") {
837
- return false;
838
- }
839
- if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
840
- return false;
841
- }
842
- if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
843
- return false;
844
- }
845
- if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
846
- return false;
847
- }
848
- const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
849
- if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
850
- return false;
851
- }
852
- if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
853
- return false;
854
- }
855
- if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
856
- return false;
857
- }
858
- if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
859
- return false;
860
- }
861
- if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
862
- return false;
863
- }
864
- if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
865
- return false;
866
- }
867
- const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
868
- if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
869
- return false;
870
- }
871
- if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
872
- return false;
873
- }
874
- if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
875
- return false;
876
- }
877
- if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
878
- return false;
879
- }
880
- return true;
881
- }
882
- function createStoredValueEnvelope(options) {
883
- const now = options.now ?? Date.now();
884
- const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
885
- const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
886
- const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
887
- const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
888
- const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
889
- const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
890
- return {
891
- __layercache: 1,
892
- kind: options.kind,
893
- value: options.value,
894
- freshUntil,
895
- staleUntil,
896
- errorUntil,
897
- freshTtlSeconds: freshTtlSeconds ?? null,
898
- staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
899
- staleIfErrorSeconds: staleIfErrorSeconds ?? null
900
- };
901
- }
902
- function resolveStoredValue(stored, now = Date.now()) {
903
- if (!isStoredValueEnvelope(stored)) {
904
- return { state: "fresh", value: stored, stored };
905
- }
906
- if (stored.freshUntil === null || stored.freshUntil > now) {
907
- return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
908
- }
909
- if (stored.staleUntil !== null && stored.staleUntil > now) {
910
- return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
911
- }
912
- if (stored.errorUntil !== null && stored.errorUntil > now) {
913
- return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
914
- }
915
- return { state: "expired", value: null, stored, envelope: stored };
916
- }
917
- function unwrapStoredValue(stored) {
918
- if (!isStoredValueEnvelope(stored)) {
919
- return stored;
920
- }
921
- if (stored.kind === "empty") {
922
- return null;
923
- }
924
- return stored.value ?? null;
925
- }
926
- function remainingStoredTtlSeconds(stored, now = Date.now()) {
927
- if (!isStoredValueEnvelope(stored)) {
928
- return void 0;
929
- }
930
- const expiry = maxExpiry(stored);
931
- if (expiry === null) {
932
- return void 0;
933
- }
934
- const remainingMs = expiry - now;
935
- if (remainingMs <= 0) {
936
- return 1;
937
- }
938
- return Math.max(1, Math.ceil(remainingMs / 1e3));
939
- }
940
- function remainingFreshTtlSeconds(stored, now = Date.now()) {
941
- if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
942
- return void 0;
943
- }
944
- const remainingMs = stored.freshUntil - now;
945
- if (remainingMs <= 0) {
946
- return 0;
947
- }
948
- return Math.max(1, Math.ceil(remainingMs / 1e3));
949
- }
950
- function refreshStoredEnvelope(stored, now = Date.now()) {
951
- if (!isStoredValueEnvelope(stored)) {
952
- return stored;
953
- }
954
- return createStoredValueEnvelope({
955
- kind: stored.kind,
956
- value: stored.value,
957
- freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
958
- staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
959
- staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
960
- now
961
- });
962
- }
963
- function maxExpiry(stored) {
964
- const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
965
- (value) => value !== null
966
- );
967
- if (values.length === 0) {
968
- return null;
969
- }
970
- return Math.max(...values);
971
- }
972
- function normalizePositiveSeconds(value) {
973
- if (!value || value <= 0) {
974
- return void 0;
975
- }
976
- return value;
977
- }
978
- function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
979
- if (value == null) {
980
- return true;
981
- }
982
- return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
983
- }
984
-
985
- // ../../src/internal/CacheStackLayerWriter.ts
986
- var CacheStackLayerWriter = class {
987
- constructor(options) {
988
- this.options = options;
989
- }
990
- options;
991
- async writeAcrossLayers(key, kind, value, writeOptions) {
992
- const now = Date.now();
993
- const clearEpoch = this.options.maintenance.currentClearEpoch();
994
- const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
995
- const immediateOperations = [];
996
- const deferredOperations = [];
997
- for (const layer of this.options.layers) {
998
- const operation = async () => {
999
- if (this.options.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
1000
- return;
1001
- }
1002
- if (this.options.shouldSkipLayer(layer)) {
1003
- return;
1004
- }
1005
- const entry = this.buildLayerSetEntry(layer, key, kind, value, writeOptions, now);
1006
- try {
1007
- await layer.set(entry.key, entry.value, entry.ttl);
1008
- } catch (error) {
1009
- await this.options.handleLayerFailure(layer, "write", error);
1010
- }
1011
- };
1012
- if (this.options.shouldWriteBehind(layer)) {
1013
- deferredOperations.push(operation);
1014
- } else {
1015
- immediateOperations.push(operation);
1016
- }
1017
- }
1018
- await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
1019
- await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1020
- }
1021
- async writeBatch(entries) {
1022
- const now = Date.now();
1023
- const clearEpoch = this.options.maintenance.currentClearEpoch();
1024
- const entryEpochs = new Map(
1025
- entries.map((entry) => [entry.key, this.options.maintenance.currentKeyEpoch(entry.key)])
1026
- );
1027
- const entriesByLayer = /* @__PURE__ */ new Map();
1028
- const immediateOperations = [];
1029
- const deferredOperations = [];
1030
- for (const entry of entries) {
1031
- for (const layer of this.options.layers) {
1032
- if (this.options.shouldSkipLayer(layer)) {
1033
- continue;
1034
- }
1035
- const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
1036
- const bucket = entriesByLayer.get(layer) ?? [];
1037
- bucket.push(layerEntry);
1038
- entriesByLayer.set(layer, bucket);
1039
- }
1040
- }
1041
- for (const [layer, layerEntries] of entriesByLayer.entries()) {
1042
- const operation = async () => {
1043
- if (clearEpoch !== this.options.maintenance.currentClearEpoch()) {
1044
- return;
1045
- }
1046
- const activeEntries = layerEntries.filter(
1047
- (entry) => (entryEpochs.get(entry.key) ?? 0) === this.options.maintenance.currentKeyEpoch(entry.key)
1048
- );
1049
- if (activeEntries.length === 0) {
1050
- return;
1051
- }
1052
- try {
1053
- if (layer.setMany) {
1054
- await layer.setMany(activeEntries);
1055
- return;
1056
- }
1057
- await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
1058
- } catch (error) {
1059
- await this.options.handleLayerFailure(layer, "write", error);
1060
- }
1061
- };
1062
- if (this.options.shouldWriteBehind(layer)) {
1063
- deferredOperations.push(operation);
1064
- } else {
1065
- immediateOperations.push(operation);
1066
- }
1067
- }
1068
- await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
1069
- await Promise.all(deferredOperations.map((operation) => this.options.enqueueWriteBehind(operation)));
1070
- return { clearEpoch, entryEpochs };
1071
- }
1072
- async executeLayerOperations(operations, context) {
1073
- if (this.options.writePolicy !== "best-effort") {
1074
- await Promise.all(operations.map((operation) => operation()));
1075
- return;
1076
- }
1077
- const results = await Promise.allSettled(operations.map((operation) => operation()));
1078
- const failures = results.filter((result) => result.status === "rejected");
1079
- const degraded = results.filter((result) => result.status === "fulfilled");
1080
- if (failures.length === 0) {
1081
- return;
1082
- }
1083
- this.options.onWriteFailures(
1084
- context,
1085
- failures.map((failure) => failure.reason)
1086
- );
1087
- if (failures.length === operations.length) {
1088
- throw new AggregateError(
1089
- failures.map((failure) => failure.reason),
1090
- `${context.action} failed for every cache layer`
1091
- );
1092
- }
1093
- }
1094
- buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
1095
- const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
1096
- const staleWhileRevalidate = this.options.resolveLayerSeconds(
1097
- layer.name,
1098
- writeOptions?.staleWhileRevalidate,
1099
- this.options.globalStaleWhileRevalidate
1100
- );
1101
- const staleIfError = this.options.resolveLayerSeconds(
1102
- layer.name,
1103
- writeOptions?.staleIfError,
1104
- this.options.globalStaleIfError
1105
- );
1106
- const payload = createStoredValueEnvelope({
1107
- kind,
1108
- value,
1109
- freshTtlSeconds: freshTtl,
1110
- staleWhileRevalidateSeconds: staleWhileRevalidate,
1111
- staleIfErrorSeconds: staleIfError,
1112
- now
1113
- });
1114
- const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
1115
- return {
1116
- key,
1117
- value: payload,
1118
- ttl
1119
- };
1120
- }
1121
- };
1122
-
1123
- // ../../src/internal/CacheStackMaintenance.ts
1124
- var CacheStackMaintenance = class {
1125
- keyEpochs = /* @__PURE__ */ new Map();
1126
- writeBehindQueue = [];
1127
- writeBehindTimer;
1128
- writeBehindFlushPromise;
1129
- generationCleanupPromise;
1130
- clearEpoch = 0;
1131
- initializeWriteBehindTimer(writeStrategy, options, flush) {
1132
- if (writeStrategy !== "write-behind") {
1133
- return;
1134
- }
1135
- const flushIntervalMs = options?.flushIntervalMs;
1136
- if (!flushIntervalMs || flushIntervalMs <= 0) {
1137
- return;
1138
- }
1139
- this.disposeWriteBehindTimer();
1140
- this.writeBehindTimer = setInterval(() => {
1141
- void flush();
1142
- }, flushIntervalMs);
1143
- this.writeBehindTimer.unref?.();
1144
- }
1145
- disposeWriteBehindTimer() {
1146
- if (!this.writeBehindTimer) {
1147
- return;
1148
- }
1149
- clearInterval(this.writeBehindTimer);
1150
- this.writeBehindTimer = void 0;
1151
- }
1152
- beginClearEpoch() {
1153
- this.clearEpoch += 1;
1154
- this.keyEpochs.clear();
1155
- this.writeBehindQueue.length = 0;
1156
- }
1157
- currentClearEpoch() {
1158
- return this.clearEpoch;
1159
- }
1160
- currentKeyEpoch(key) {
1161
- return this.keyEpochs.get(key) ?? 0;
1162
- }
1163
- bumpKeyEpochs(keys) {
1164
- for (const key of keys) {
1165
- this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
1166
- }
1167
- }
1168
- isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
1169
- if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
1170
- return true;
1171
- }
1172
- if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
1173
- return true;
1174
- }
1175
- return false;
1176
- }
1177
- async enqueueWriteBehind(operation, options, flushBatch) {
1178
- this.writeBehindQueue.push(operation);
1179
- const batchSize = options?.batchSize ?? 100;
1180
- const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
1181
- if (this.writeBehindQueue.length >= batchSize) {
1182
- await this.flushWriteBehindQueue(options, flushBatch);
1183
- return;
1184
- }
1185
- if (this.writeBehindQueue.length >= maxQueueSize) {
1186
- await this.flushWriteBehindQueue(options, flushBatch);
1187
- }
1188
- }
1189
- async flushWriteBehindQueue(options, flushBatch) {
1190
- if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
1191
- await this.writeBehindFlushPromise;
1192
- return;
1193
- }
1194
- const batchSize = options?.batchSize ?? 100;
1195
- const batch = this.writeBehindQueue.splice(0, batchSize);
1196
- this.writeBehindFlushPromise = flushBatch(batch);
1197
- try {
1198
- await this.writeBehindFlushPromise;
1199
- } finally {
1200
- this.writeBehindFlushPromise = void 0;
1201
- }
1202
- if (this.writeBehindQueue.length > 0) {
1203
- await this.flushWriteBehindQueue(options, flushBatch);
1204
- }
1205
- }
1206
- scheduleGenerationCleanup(generation, task, onError) {
1207
- const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
1208
- onError(generation, error);
1209
- });
1210
- this.generationCleanupPromise = scheduledTask.finally(() => {
1211
- if (this.generationCleanupPromise === scheduledTask) {
1212
- this.generationCleanupPromise = void 0;
1213
- }
1214
- });
1215
- }
1216
- async waitForGenerationCleanup() {
1217
- await this.generationCleanupPromise;
1218
- }
1219
- };
1220
-
1221
- // ../../src/internal/CacheStackRuntimePolicy.ts
1222
- function shouldSkipLayer(degradedUntil, now = Date.now()) {
1223
- return degradedUntil !== void 0 && degradedUntil > now;
1224
- }
1225
- function shouldStartBackgroundRefresh({
1226
- isDisconnecting,
1227
- hasRefreshInFlight
1228
- }) {
1229
- return !isDisconnecting && !hasRefreshInFlight;
1230
- }
1231
- function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
1232
- if (!gracefulDegradation) {
1233
- return { degrade: false };
1234
- }
1235
- const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
1236
- return {
1237
- degrade: true,
1238
- degradedUntil: now + retryAfterMs
1239
- };
1240
- }
1241
- function planFreshReadPolicies({
1242
- stored,
1243
- hasFetcher,
1244
- slidingTtl,
1245
- refreshAheadSeconds
1246
- }) {
1247
- const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
1248
- const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
1249
- const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
1250
- return {
1251
- refreshedStored,
1252
- refreshedStoredTtl,
1253
- shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
1254
- };
1255
- }
1256
-
1257
- // ../../src/internal/CacheStackSnapshotManager.ts
1258
- import { randomBytes } from "crypto";
1259
- import { constants, promises as fs } from "fs";
1260
- import path from "path";
1261
-
1262
- // ../../src/internal/CacheSnapshotFile.ts
1263
- function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path2) {
1264
- const relative = path2.relative(realBaseDir, candidatePath);
1265
- return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path2.isAbsolute(relative));
1266
- }
1267
- async function findExistingAncestor(directory, fs2, path2) {
1268
- let current = directory;
1269
- while (true) {
1270
- try {
1271
- await fs2.lstat(current);
1272
- return current;
1273
- } catch (error) {
1274
- if (error.code !== "ENOENT") {
1275
- throw error;
1276
- }
1277
- }
1278
- const parent = path2.dirname(current);
1279
- if (parent === current) {
1280
- return current;
1281
- }
1282
- current = parent;
1283
- }
1284
- }
1285
- async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
1286
- if (filePath.length === 0) {
1287
- throw new Error("filePath must not be empty.");
1288
- }
1289
- if (filePath.includes("\0")) {
1290
- throw new Error("filePath must not contain null bytes.");
1291
- }
1292
- const { promises: fs2 } = await import("fs");
1293
- const path2 = await import("path");
1294
- const resolved = path2.resolve(filePath);
1295
- const baseDir = snapshotBaseDir === false ? false : path2.resolve(snapshotBaseDir ?? cwd);
1296
- if (baseDir === false) {
1297
- return resolved;
1298
- }
1299
- await fs2.mkdir(baseDir, { recursive: true });
1300
- const realBaseDir = await fs2.realpath(baseDir);
1301
- if (!isWithinSnapshotBase(realBaseDir, resolved, path2.sep, path2)) {
1302
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1303
- }
1304
- if (mode === "read") {
1305
- const realTarget = await fs2.realpath(resolved);
1306
- if (!isWithinSnapshotBase(realBaseDir, realTarget, path2.sep, path2)) {
1307
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1308
- }
1309
- return realTarget;
1310
- }
1311
- const parentDir = path2.dirname(resolved);
1312
- const existingAncestor = await findExistingAncestor(parentDir, fs2, path2);
1313
- const realExistingAncestor = await fs2.realpath(existingAncestor);
1314
- if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path2.sep, path2)) {
1315
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1316
- }
1317
- await fs2.mkdir(parentDir, { recursive: true });
1318
- const realParentDir = await fs2.realpath(parentDir);
1319
- if (!isWithinSnapshotBase(realBaseDir, realParentDir, path2.sep, path2)) {
1320
- throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
1321
- }
1322
- const targetPath = path2.join(realParentDir, path2.basename(resolved));
1323
- try {
1324
- const existing = await fs2.lstat(targetPath);
1325
- if (existing.isSymbolicLink()) {
1326
- throw new Error("filePath must not point to a symbolic link.");
1327
- }
1328
- } catch (error) {
1329
- if (error.code !== "ENOENT") {
1330
- throw error;
1331
- }
1332
- }
1333
- return targetPath;
1334
- }
1335
- async function readUtf8HandleWithLimit(handle, byteLimit) {
1336
- if (byteLimit === false) {
1337
- return handle.readFile({ encoding: "utf8" });
1338
- }
1339
- const chunks = [];
1340
- let totalBytes = 0;
1341
- let position = 0;
1342
- while (true) {
1343
- const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
1344
- const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
1345
- if (bytesRead === 0) {
1346
- break;
1347
- }
1348
- totalBytes += bytesRead;
1349
- if (totalBytes > byteLimit) {
1350
- throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
1351
- }
1352
- chunks.push(buffer.subarray(0, bytesRead));
1353
- position += bytesRead;
1354
- }
1355
- return Buffer.concat(chunks).toString("utf8");
1356
- }
1357
-
1358
- // ../../src/internal/StructuredDataSanitizer.ts
1359
- var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
1360
- function sanitizeStructuredData(value, options) {
1361
- return sanitizeValue(value, 0, { count: 0 }, options);
1362
- }
1363
- function sanitizeValue(value, depth, state, options) {
1364
- state.count += 1;
1365
- if (state.count > options.maxNodes) {
1366
- throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
1367
- }
1368
- if (depth > options.maxDepth) {
1369
- throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
1370
- }
1371
- if (Array.isArray(value)) {
1372
- const sanitized2 = [];
1373
- for (const entry of value) {
1374
- sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
1375
- }
1376
- return sanitized2;
1377
- }
1378
- if (!isPlainObject(value)) {
1379
- return value;
1380
- }
1381
- const sanitized = options.createObject?.() ?? {};
1382
- for (const [key, entry] of Object.entries(value)) {
1383
- if (DANGEROUS_KEYS.has(key)) {
1384
- continue;
1385
- }
1386
- sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
1387
- }
1388
- return sanitized;
1389
- }
1390
- function isPlainObject(value) {
1391
- return Object.prototype.toString.call(value) === "[object Object]";
1392
- }
1393
-
1394
- // ../../src/internal/CacheStackSnapshotManager.ts
1395
- var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
1396
- var CacheStackSnapshotManager = class {
1397
- constructor(options) {
1398
- this.options = options;
1399
- }
1400
- options;
1401
- async exportState(maxEntries) {
1402
- const entries = [];
1403
- await this.visitExportEntries(maxEntries, async (entry) => {
1404
- entries.push(entry);
1405
- });
1406
- return entries;
1407
- }
1408
- async importState(entries) {
1409
- const normalizedEntries = entries.map((entry) => ({
1410
- key: this.options.qualifyKey(this.options.validateCacheKey(entry.key)),
1411
- value: entry.value,
1412
- ttl: entry.ttl
1413
- }));
1414
- for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
1415
- const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
1416
- await Promise.all(
1417
- batch.map(async (entry) => {
1418
- await Promise.all(
1419
- this.options.layers.map(async (layer) => {
1420
- if (this.options.shouldSkipLayer(layer)) return;
1421
- try {
1422
- await layer.set(entry.key, entry.value, entry.ttl);
1423
- } catch (error) {
1424
- await this.options.handleLayerFailure(layer, "write", error);
1425
- }
1426
- })
1427
- );
1428
- await this.options.tagIndex.touch(entry.key);
1429
- })
1430
- );
1431
- }
1432
- }
1433
- async persistToFile(filePath, snapshotBaseDir, maxEntries) {
1434
- const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
1435
- const tempPath = path.join(
1436
- path.dirname(targetPath),
1437
- `.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
1438
- );
1439
- let handle;
1440
- try {
1441
- handle = await fs.open(tempPath, "wx");
1442
- const openedHandle = handle;
1443
- await openedHandle.writeFile("[", "utf8");
1444
- let wroteAny = false;
1445
- await this.visitExportEntries(maxEntries, async (entry) => {
1446
- await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
1447
- await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
1448
- wroteAny = true;
1449
- });
1450
- await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
1451
- await openedHandle.close();
1452
- handle = void 0;
1453
- await fs.rename(tempPath, targetPath);
1454
- } catch (error) {
1455
- await handle?.close().catch(() => void 0);
1456
- await fs.unlink(tempPath).catch(() => void 0);
1457
- throw error;
1458
- }
1459
- }
1460
- async restoreFromFile(filePath, snapshotBaseDir, maxBytes) {
1461
- const validatedPath = await validateSnapshotFilePath(filePath, "read", snapshotBaseDir);
1462
- const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1463
- let raw;
1464
- try {
1465
- if (maxBytes !== false) {
1466
- const stat = await handle.stat();
1467
- if (stat.size > maxBytes) {
1468
- throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${maxBytes} bytes).`);
1469
- }
1470
- }
1471
- raw = await readUtf8HandleWithLimit(handle, maxBytes);
1472
- } finally {
1473
- await handle.close();
1474
- }
1475
- let parsed;
1476
- try {
1477
- parsed = JSON.parse(raw);
1478
- } catch (cause) {
1479
- throw new Error(`Invalid snapshot file: could not parse JSON (${this.options.formatError(cause)})`);
1480
- }
1481
- if (!this.isCacheSnapshotEntries(parsed)) {
1482
- throw new Error("Invalid snapshot file: expected an array of { key: string, value, ttl? } entries");
1483
- }
1484
- await this.importState(
1485
- parsed.map((entry) => ({
1486
- key: entry.key,
1487
- value: this.sanitizeSnapshotValue(entry.value),
1488
- ttl: entry.ttl
1489
- }))
1490
- );
1491
- }
1492
- async visitExportEntries(maxEntries, visitor) {
1493
- const exported = /* @__PURE__ */ new Set();
1494
- for (const layer of this.options.layers) {
1495
- if (!layer.keys && !layer.forEachKey) {
1496
- continue;
1497
- }
1498
- const visitKey = async (key) => {
1499
- const exportedKey = this.options.stripQualifiedKey(key);
1500
- if (exported.has(exportedKey)) {
1501
- return;
1502
- }
1503
- const stored = await this.options.readLayerEntry(layer, key);
1504
- if (stored === null) {
1505
- return;
1506
- }
1507
- exported.add(exportedKey);
1508
- if (maxEntries !== false && exported.size > maxEntries) {
1509
- throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
1510
- }
1511
- await visitor({
1512
- key: exportedKey,
1513
- value: stored,
1514
- ttl: remainingStoredTtlSeconds(stored)
1515
- });
1516
- };
1517
- if (layer.forEachKey) {
1518
- await layer.forEachKey(visitKey);
1519
- continue;
1520
- }
1521
- const keys = await layer.keys?.();
1522
- for (const key of keys ?? []) {
1523
- await visitKey(key);
1524
- }
1525
- }
1526
- }
1527
- isCacheSnapshotEntries(value) {
1528
- return Array.isArray(value) && value.every((entry) => {
1529
- if (!entry || typeof entry !== "object") {
1530
- return false;
1531
- }
1532
- const candidate = entry;
1533
- return typeof candidate.key === "string" && (candidate.ttl === void 0 || typeof candidate.ttl === "number" && Number.isFinite(candidate.ttl) && candidate.ttl >= 0);
1534
- });
1535
- }
1536
- sanitizeSnapshotValue(value) {
1537
- const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
1538
- return sanitizeStructuredData(roundTripped, {
1539
- label: "Snapshot value",
1540
- maxDepth: 64,
1541
- maxNodes: 1e4,
1542
- createObject: () => /* @__PURE__ */ Object.create(null)
1543
- });
1544
- }
1545
- };
1546
-
1547
- // ../../src/internal/CacheStackValidation.ts
1548
- var MAX_CACHE_KEY_LENGTH = 1024;
1549
- var MAX_PATTERN_LENGTH = 1024;
1550
- var MAX_TAGS_PER_OPERATION = 128;
1551
- function validatePositiveNumber(name, value) {
1552
- if (value === void 0) {
1553
- return;
1554
- }
1555
- if (!Number.isFinite(value) || value <= 0) {
1556
- throw new Error(`${name} must be a positive finite number.`);
1557
- }
1558
- }
1559
- function validateNonNegativeNumber(name, value) {
1560
- if (!Number.isFinite(value) || value < 0) {
1561
- throw new Error(`${name} must be a non-negative finite number.`);
1562
- }
1563
- }
1564
- function validateLayerNumberOption(name, value) {
1565
- if (value === void 0) {
1566
- return;
1567
- }
1568
- if (typeof value === "number") {
1569
- validateNonNegativeNumber(name, value);
1570
- return;
1571
- }
1572
- for (const [layerName, layerValue] of Object.entries(value)) {
1573
- if (layerValue === void 0) {
1574
- continue;
1575
- }
1576
- validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
1577
- }
1578
- }
1579
- function validateRateLimitOptions(name, options) {
1580
- if (!options) {
1581
- return;
1582
- }
1583
- validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
1584
- validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
1585
- validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
1586
- if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
1587
- throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
1588
- }
1589
- if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
1590
- throw new Error(`${name}.bucketKey must not be empty.`);
1591
- }
1592
- }
1593
- function validateCacheKey(key) {
1594
- if (key.length === 0) {
1595
- throw new Error("Cache key must not be empty.");
1596
- }
1597
- if (key.length > MAX_CACHE_KEY_LENGTH) {
1598
- throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1599
- }
1600
- if (/[\u0000-\u001F\u007F]/.test(key)) {
1601
- throw new Error("Cache key contains unsupported control characters.");
1602
- }
1603
- if (/[\uD800-\uDFFF]/.test(key)) {
1604
- throw new Error("Cache key contains unsupported surrogate code points.");
1605
- }
1606
- return key;
1607
- }
1608
- function validateTag(tag) {
1609
- if (tag.length === 0) {
1610
- throw new Error("Cache tag must not be empty.");
1611
- }
1612
- if (tag.length > MAX_CACHE_KEY_LENGTH) {
1613
- throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
1614
- }
1615
- if (/[\u0000-\u001F\u007F]/.test(tag)) {
1616
- throw new Error("Cache tag contains unsupported control characters.");
1617
- }
1618
- if (/[\uD800-\uDFFF]/.test(tag)) {
1619
- throw new Error("Cache tag contains unsupported surrogate code points.");
1620
- }
1621
- return tag;
1622
- }
1623
- function validateTags(tags) {
1624
- if (!tags) {
1625
- return;
1626
- }
1627
- if (tags.length > MAX_TAGS_PER_OPERATION) {
1628
- throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
1629
- }
1630
- for (const tag of tags) {
1631
- validateTag(tag);
1632
- }
1633
- }
1634
- function validatePattern(pattern) {
1635
- if (pattern.length === 0) {
1636
- throw new Error("Pattern must not be empty.");
1637
- }
1638
- if (pattern.length > MAX_PATTERN_LENGTH) {
1639
- throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
1640
- }
1641
- if (/[\u0000-\u001F\u007F]/.test(pattern)) {
1642
- throw new Error("Pattern contains unsupported control characters.");
1643
- }
1644
- }
1645
- function validateTtlPolicy(name, policy) {
1646
- if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
1647
- return;
1648
- }
1649
- if ("alignTo" in policy) {
1650
- validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
1651
- return;
1652
- }
1653
- throw new Error(`${name} is invalid.`);
1654
- }
1655
- function validateAdaptiveTtlOptions(options) {
1656
- if (!options || options === true) {
1657
- return;
1658
- }
1659
- validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
1660
- validateLayerNumberOption("adaptiveTtl.step", options.step);
1661
- validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
1662
- }
1663
- function validateCircuitBreakerOptions(options) {
1664
- if (!options) {
1665
- return;
1666
- }
1667
- validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
1668
- validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
1669
- }
1670
-
1671
- // ../../src/internal/CircuitBreakerManager.ts
1672
- var CircuitBreakerManager = class {
1673
- breakers = /* @__PURE__ */ new Map();
1674
- maxEntries;
1675
- constructor(options) {
1676
- this.maxEntries = options.maxEntries;
1677
- }
1678
- /**
1679
- * Throws if the circuit is open for the given key.
1680
- * Automatically resets if the cooldown has elapsed.
1681
- */
1682
- assertClosed(key, options) {
1683
- const state = this.breakers.get(key);
1684
- if (!state?.openUntil) {
1685
- return;
1686
- }
1687
- const now = Date.now();
1688
- if (state.openUntil <= now) {
1689
- this.breakers.delete(key);
1690
- return;
1691
- }
1692
- const remainingMs = state.openUntil - now;
1693
- const remainingSecs = Math.ceil(remainingMs / 1e3);
1694
- const displayKey = key.length > 64 ? `${key.slice(0, 64)}...` : key;
1695
- throw new Error(`Circuit breaker is open for key "${displayKey}" (resets in ${remainingSecs}s).`);
1696
- }
1697
- recordFailure(key, options) {
1698
- if (!options) {
1699
- return;
1700
- }
1701
- const failureThreshold = options.failureThreshold ?? 3;
1702
- const cooldownMs = options.cooldownMs ?? 3e4;
1703
- const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
1704
- state.failures += 1;
1705
- if (state.failures >= failureThreshold) {
1706
- state.openUntil = Date.now() + cooldownMs;
1707
- }
1708
- this.breakers.set(key, state);
1709
- this.pruneIfNeeded();
1710
- }
1711
- recordSuccess(key) {
1712
- this.breakers.delete(key);
1713
- }
1714
- isOpen(key) {
1715
- const state = this.breakers.get(key);
1716
- if (!state?.openUntil) {
1717
- return false;
1718
- }
1719
- if (state.openUntil <= Date.now()) {
1720
- this.breakers.delete(key);
1721
- return false;
1722
- }
1723
- return true;
1724
- }
1725
- delete(key) {
1726
- this.breakers.delete(key);
1727
- }
1728
- clear() {
1729
- this.breakers.clear();
1730
- }
1731
- tripCount() {
1732
- let count = 0;
1733
- for (const state of this.breakers.values()) {
1734
- if (state.openUntil !== null) {
1735
- count += 1;
1736
- }
1737
- }
1738
- return count;
1739
- }
1740
- pruneIfNeeded() {
1741
- if (this.breakers.size <= this.maxEntries) {
1742
- return;
1743
- }
1744
- const now = Date.now();
1745
- for (const [key, state] of this.breakers.entries()) {
1746
- if (this.breakers.size <= this.maxEntries) {
1747
- return;
1748
- }
1749
- if (!state.openUntil || state.openUntil <= now) {
1750
- this.breakers.delete(key);
1751
- }
1752
- }
1753
- if (this.breakers.size <= this.maxEntries) {
1754
- return;
1755
- }
1756
- const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
1757
- for (const [key] of sorted) {
1758
- if (this.breakers.size <= this.maxEntries) {
1759
- break;
1760
- }
1761
- this.breakers.delete(key);
1762
- }
1763
- }
1764
- };
1765
-
1766
- // ../../src/internal/FetchRateLimiter.ts
1767
- var MAX_BUCKETS = 1e4;
1768
- var FetchRateLimiter = class {
1769
- buckets = /* @__PURE__ */ new Map();
1770
- queuesByBucket = /* @__PURE__ */ new Map();
1771
- pendingBuckets = /* @__PURE__ */ new Set();
1772
- fetcherBuckets = /* @__PURE__ */ new WeakMap();
1773
- nextFetcherBucketId = 0;
1774
- drainTimer;
1775
- isDisposed = false;
1776
- async schedule(options, context, task) {
1777
- if (this.isDisposed) {
1778
- throw new Error("FetchRateLimiter has been disposed.");
1779
- }
1780
- if (!options) {
1781
- return task();
1782
- }
1783
- const normalized = this.normalize(options);
1784
- if (!normalized) {
1785
- return task();
1786
- }
1787
- return new Promise((resolve, reject) => {
1788
- const bucketKey = this.resolveBucketKey(normalized, context);
1789
- const queue = this.queuesByBucket.get(bucketKey) ?? [];
1790
- queue.push({
1791
- bucketKey,
1792
- options: normalized,
1793
- task,
1794
- resolve,
1795
- reject
1796
- });
1797
- this.queuesByBucket.set(bucketKey, queue);
1798
- this.pendingBuckets.add(bucketKey);
1799
- this.drain();
1800
- });
1801
- }
1802
- dispose() {
1803
- this.isDisposed = true;
1804
- if (this.drainTimer) {
1805
- clearTimeout(this.drainTimer);
1806
- this.drainTimer = void 0;
1807
- }
1808
- for (const bucket of this.buckets.values()) {
1809
- if (bucket.cleanupTimer) {
1810
- clearTimeout(bucket.cleanupTimer);
1811
- bucket.cleanupTimer = void 0;
1812
- }
1813
- }
1814
- for (const queue of this.queuesByBucket.values()) {
1815
- for (const item of queue) {
1816
- item.reject(new Error("FetchRateLimiter has been disposed."));
1817
- }
1818
- }
1819
- this.queuesByBucket.clear();
1820
- this.pendingBuckets.clear();
1821
- this.buckets.clear();
1822
- }
1823
- normalize(options) {
1824
- const maxConcurrent = options.maxConcurrent;
1825
- const intervalMs = options.intervalMs;
1826
- const maxPerInterval = options.maxPerInterval;
1827
- if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
1828
- return void 0;
1829
- }
1830
- return {
1831
- maxConcurrent,
1832
- intervalMs,
1833
- maxPerInterval,
1834
- scope: options.scope ?? "global",
1835
- bucketKey: options.bucketKey
1836
- };
1837
- }
1838
- resolveBucketKey(options, context) {
1839
- if (options.bucketKey) {
1840
- return `custom:${options.bucketKey}`;
1841
- }
1842
- if (options.scope === "key") {
1843
- return `key:${context.key}`;
1844
- }
1845
- if (options.scope === "fetcher") {
1846
- const existing = this.fetcherBuckets.get(context.fetcher);
1847
- if (existing) {
1848
- return existing;
1849
- }
1850
- const bucket = `fetcher:${this.nextFetcherBucketId}`;
1851
- this.nextFetcherBucketId += 1;
1852
- this.fetcherBuckets.set(context.fetcher, bucket);
1853
- return bucket;
1854
- }
1855
- return "global";
1856
- }
1857
- drain() {
1858
- if (this.isDisposed) {
1859
- return;
1860
- }
1861
- if (this.drainTimer) {
1862
- clearTimeout(this.drainTimer);
1863
- this.drainTimer = void 0;
1864
- }
1865
- while (this.pendingBuckets.size > 0) {
1866
- let nextBucketKey;
1867
- let nextWaitMs = Number.POSITIVE_INFINITY;
1868
- for (const bucketKey of this.pendingBuckets) {
1869
- const queue2 = this.queuesByBucket.get(bucketKey);
1870
- if (!queue2 || queue2.length === 0) {
1871
- this.pendingBuckets.delete(bucketKey);
1872
- this.queuesByBucket.delete(bucketKey);
1873
- continue;
1874
- }
1875
- const next2 = queue2[0];
1876
- if (!next2) {
1877
- this.pendingBuckets.delete(bucketKey);
1878
- this.queuesByBucket.delete(bucketKey);
1879
- continue;
1880
- }
1881
- const waitMs = this.waitTime(bucketKey, next2.options);
1882
- if (waitMs <= 0) {
1883
- nextBucketKey = bucketKey;
1884
- break;
1885
- }
1886
- nextWaitMs = Math.min(nextWaitMs, waitMs);
1887
- }
1888
- if (!nextBucketKey) {
1889
- if (Number.isFinite(nextWaitMs)) {
1890
- this.drainTimer = setTimeout(() => {
1891
- this.drainTimer = void 0;
1892
- this.drain();
1893
- }, nextWaitMs);
1894
- this.drainTimer.unref?.();
1895
- }
1896
- return;
1897
- }
1898
- const queue = this.queuesByBucket.get(nextBucketKey);
1899
- const next = queue?.shift();
1900
- if (!next) {
1901
- this.pendingBuckets.delete(nextBucketKey);
1902
- this.queuesByBucket.delete(nextBucketKey);
1903
- continue;
1904
- }
1905
- if (!queue || queue.length === 0) {
1906
- this.pendingBuckets.delete(nextBucketKey);
1907
- this.queuesByBucket.delete(nextBucketKey);
1908
- }
1909
- const bucket = this.bucketState(next.bucketKey);
1910
- if (bucket.cleanupTimer) {
1911
- clearTimeout(bucket.cleanupTimer);
1912
- bucket.cleanupTimer = void 0;
1913
- }
1914
- bucket.active += 1;
1915
- if (next.options.intervalMs && next.options.maxPerInterval) {
1916
- bucket.startedAt.push(Date.now());
1917
- }
1918
- void next.task().then(next.resolve, next.reject).finally(() => {
1919
- bucket.active -= 1;
1920
- if ((this.queuesByBucket.get(next.bucketKey)?.length ?? 0) > 0) {
1921
- this.pendingBuckets.add(next.bucketKey);
1922
- }
1923
- this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
1924
- if (!this.drainTimer) {
1925
- this.drainTimer = setTimeout(() => {
1926
- this.drainTimer = void 0;
1927
- this.drain();
1928
- }, 0);
1929
- this.drainTimer.unref?.();
1930
- }
1931
- });
1932
- }
1933
- }
1934
- waitTime(bucketKey, options) {
1935
- const bucket = this.bucketState(bucketKey);
1936
- const now = Date.now();
1937
- if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
1938
- return 1;
1939
- }
1940
- if (!options.intervalMs || !options.maxPerInterval) {
1941
- return 0;
1942
- }
1943
- this.prune(bucket, now, options.intervalMs);
1944
- if (bucket.startedAt.length < options.maxPerInterval) {
1945
- return 0;
1946
- }
1947
- const oldest = bucket.startedAt[0];
1948
- if (!oldest) {
1949
- return 0;
1950
- }
1951
- return Math.max(1, options.intervalMs - (now - oldest));
1952
- }
1953
- prune(bucket, now, intervalMs) {
1954
- while (bucket.startedAt.length > 0) {
1955
- const startedAt = bucket.startedAt[0];
1956
- if (startedAt === void 0 || now - startedAt < intervalMs) {
1957
- break;
1958
- }
1959
- bucket.startedAt.shift();
1960
- }
1961
- }
1962
- bucketState(bucketKey) {
1963
- if (this.isDisposed) {
1964
- throw new Error("FetchRateLimiter has been disposed.");
1965
- }
1966
- const existing = this.buckets.get(bucketKey);
1967
- if (existing) {
1968
- return existing;
1969
- }
1970
- if (this.buckets.size >= MAX_BUCKETS) {
1971
- this.evictIdleBuckets();
1972
- if (this.buckets.size >= MAX_BUCKETS) {
1973
- throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
1974
- }
1975
- }
1976
- const bucket = { active: 0, startedAt: [] };
1977
- this.buckets.set(bucketKey, bucket);
1978
- return bucket;
1979
- }
1980
- evictIdleBuckets() {
1981
- for (const [key, bucket] of this.buckets.entries()) {
1982
- if (this.buckets.size <= MAX_BUCKETS * 0.9) {
1983
- break;
1984
- }
1985
- if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
1986
- this.buckets.delete(key);
1987
- this.queuesByBucket.delete(key);
1988
- this.pendingBuckets.delete(key);
1989
- }
1990
- }
1991
- }
1992
- cleanupBucket(bucketKey, bucket, intervalMs) {
1993
- const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
1994
- if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
1995
- this.buckets.delete(bucketKey);
1996
- this.queuesByBucket.delete(bucketKey);
1997
- this.pendingBuckets.delete(bucketKey);
1998
- return;
1999
- }
2000
- if (!intervalMs || bucket.active > 0 || queued > 0) {
2001
- return;
2002
- }
2003
- if (bucket.cleanupTimer) {
2004
- clearTimeout(bucket.cleanupTimer);
2005
- }
2006
- bucket.cleanupTimer = setTimeout(() => {
2007
- bucket.cleanupTimer = void 0;
2008
- this.prune(bucket, Date.now(), intervalMs);
2009
- if (bucket.active === 0 && bucket.startedAt.length === 0 && (this.queuesByBucket.get(bucketKey)?.length ?? 0) === 0) {
2010
- this.buckets.delete(bucketKey);
2011
- this.queuesByBucket.delete(bucketKey);
2012
- this.pendingBuckets.delete(bucketKey);
2013
- }
2014
- }, intervalMs);
2015
- bucket.cleanupTimer.unref?.();
2016
- }
2017
- };
2018
-
2019
- // ../../src/internal/MetricsCollector.ts
2020
- var MetricsCollector = class {
2021
- data = this.empty();
2022
- get snapshot() {
2023
- return {
2024
- ...this.data,
2025
- hitsByLayer: { ...this.data.hitsByLayer },
2026
- missesByLayer: { ...this.data.missesByLayer },
2027
- latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
2028
- };
2029
- }
2030
- increment(field, amount = 1) {
2031
- ;
2032
- this.data[field] += amount;
2033
- }
2034
- incrementLayer(map, layerName) {
2035
- this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
2036
- }
2037
- /**
2038
- * Records a read latency sample for the given layer.
2039
- * Maintains a rolling average and max using Welford's online algorithm.
2040
- */
2041
- recordLatency(layerName, durationMs) {
2042
- const existing = this.data.latencyByLayer[layerName];
2043
- if (!existing) {
2044
- this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
2045
- return;
2046
- }
2047
- existing.count += 1;
2048
- existing.avgMs += (durationMs - existing.avgMs) / existing.count;
2049
- if (durationMs > existing.maxMs) {
2050
- existing.maxMs = durationMs;
2051
- }
2052
- }
2053
- reset() {
2054
- this.data = this.empty();
2055
- }
2056
- hitRate() {
2057
- const total = this.data.hits + this.data.misses;
2058
- const overall = total === 0 ? 0 : this.data.hits / total;
2059
- const byLayer = {};
2060
- const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
2061
- for (const layer of allLayers) {
2062
- const h = this.data.hitsByLayer[layer] ?? 0;
2063
- const m = this.data.missesByLayer[layer] ?? 0;
2064
- byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
2065
- }
2066
- return { overall, byLayer };
2067
- }
2068
- empty() {
2069
- return {
2070
- hits: 0,
2071
- misses: 0,
2072
- fetches: 0,
2073
- sets: 0,
2074
- deletes: 0,
2075
- backfills: 0,
2076
- invalidations: 0,
2077
- staleHits: 0,
2078
- refreshes: 0,
2079
- refreshErrors: 0,
2080
- writeFailures: 0,
2081
- singleFlightWaits: 0,
2082
- negativeCacheHits: 0,
2083
- circuitBreakerTrips: 0,
2084
- degradedOperations: 0,
2085
- hitsByLayer: {},
2086
- missesByLayer: {},
2087
- latencyByLayer: {},
2088
- resetAt: Date.now()
2089
- };
2090
- }
2091
- };
2092
-
2093
- // ../../src/internal/TtlResolver.ts
2094
- var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
2095
- var TtlResolver = class {
2096
- accessProfiles = /* @__PURE__ */ new Map();
2097
- maxProfileEntries;
2098
- constructor(options) {
2099
- this.maxProfileEntries = options.maxProfileEntries;
2100
- }
2101
- recordAccess(key) {
2102
- const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
2103
- profile.hits += 1;
2104
- profile.lastAccessAt = Date.now();
2105
- this.accessProfiles.set(key, profile);
2106
- this.pruneIfNeeded();
2107
- }
2108
- deleteProfile(key) {
2109
- this.accessProfiles.delete(key);
2110
- }
2111
- clearProfiles() {
2112
- this.accessProfiles.clear();
2113
- }
2114
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
2115
- const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
2116
- const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
2117
- layerName,
2118
- options?.negativeTtl,
2119
- globalNegativeTtl,
2120
- this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
2121
- ) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
2122
- const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
2123
- const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
2124
- return this.applyJitter(adaptiveTtl, jitter);
2125
- }
2126
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
2127
- if (override !== void 0) {
2128
- return this.readLayerNumber(layerName, override) ?? fallback;
2129
- }
2130
- if (globalDefault !== void 0) {
2131
- return this.readLayerNumber(layerName, globalDefault) ?? fallback;
2132
- }
2133
- return fallback;
2134
- }
2135
- applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
2136
- if (!ttl || !adaptiveTtl) {
2137
- return ttl;
2138
- }
2139
- const profile = this.accessProfiles.get(key);
2140
- if (!profile) {
2141
- return ttl;
2142
- }
2143
- const config = adaptiveTtl === true ? {} : adaptiveTtl;
2144
- const hotAfter = config.hotAfter ?? 3;
2145
- if (profile.hits < hotAfter) {
2146
- return ttl;
2147
- }
2148
- const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
2149
- const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
2150
- const multiplier = Math.floor(profile.hits / hotAfter);
2151
- return Math.min(maxTtl, ttl + step * multiplier);
2152
- }
2153
- applyJitter(ttl, jitter) {
2154
- if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
2155
- return ttl;
2156
- }
2157
- const delta = (Math.random() * 2 - 1) * jitter;
2158
- return Math.max(1, Math.round(ttl + delta));
2159
- }
2160
- resolvePolicyTtl(key, value, policy) {
2161
- if (!policy) {
2162
- return void 0;
2163
- }
2164
- if (typeof policy === "function") {
2165
- return policy({ key, value });
2166
- }
2167
- const now = /* @__PURE__ */ new Date();
2168
- if (policy === "until-midnight") {
2169
- const nextMidnight = new Date(now);
2170
- nextMidnight.setHours(24, 0, 0, 0);
2171
- return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
2172
- }
2173
- if (policy === "next-hour") {
2174
- const nextHour = new Date(now);
2175
- nextHour.setMinutes(60, 0, 0);
2176
- return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
2177
- }
2178
- const alignToSeconds = policy.alignTo;
2179
- const currentSeconds = Math.floor(Date.now() / 1e3);
2180
- const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
2181
- return Math.max(1, nextBoundary - currentSeconds);
2182
- }
2183
- readLayerNumber(layerName, value) {
2184
- if (typeof value === "number") {
2185
- return value;
2186
- }
2187
- return value[layerName];
2188
- }
2189
- pruneIfNeeded() {
2190
- if (this.accessProfiles.size <= this.maxProfileEntries) {
2191
- return;
2192
- }
2193
- const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
2194
- const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
2195
- for (let i = 0; i < toRemove && i < sorted.length; i++) {
2196
- const entry = sorted[i];
2197
- if (entry) {
2198
- this.accessProfiles.delete(entry[0]);
2199
- }
2200
- }
2201
- }
2202
- };
2203
-
2204
- // ../../src/invalidation/TagIndex.ts
2205
- var MAX_PATTERN_RECURSION_DEPTH = 500;
2206
- var TagIndex = class {
2207
- tagToKeys = /* @__PURE__ */ new Map();
2208
- keyToTags = /* @__PURE__ */ new Map();
2209
- knownKeys = /* @__PURE__ */ new Set();
2210
- maxKnownKeys;
2211
- nextNodeId = 1;
2212
- root = this.createTrieNode();
2213
- constructor(options = {}) {
2214
- this.maxKnownKeys = options.maxKnownKeys ?? 1e5;
2215
- }
2216
- async touch(key) {
2217
- this.insertKnownKey(key);
2218
- this.pruneKnownKeysIfNeeded();
2219
- }
2220
- async track(key, tags) {
2221
- this.insertKnownKey(key);
2222
- this.pruneKnownKeysIfNeeded();
2223
- if (tags.length === 0) {
2224
- return;
2225
- }
2226
- const existingTags = this.keyToTags.get(key);
2227
- if (existingTags) {
2228
- for (const tag of existingTags) {
2229
- this.tagToKeys.get(tag)?.delete(key);
2230
- }
2231
- }
2232
- const tagSet = new Set(tags);
2233
- this.keyToTags.set(key, tagSet);
2234
- for (const tag of tagSet) {
2235
- const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
2236
- keys.add(key);
2237
- this.tagToKeys.set(tag, keys);
2238
- }
2239
- }
2240
- async remove(key) {
2241
- this.removeKey(key);
2242
- }
2243
- async keysForTag(tag) {
2244
- return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
2245
- }
2246
- async forEachKeyForTag(tag, visitor) {
2247
- for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
2248
- await visitor(key);
2249
- }
2250
- }
2251
- async keysForPrefix(prefix) {
2252
- const node = this.findNode(prefix);
2253
- if (!node) {
2254
- return [];
2255
- }
2256
- const matches = [];
2257
- this.collectFromNode(node, prefix, matches);
2258
- return matches;
2259
- }
2260
- async forEachKeyForPrefix(prefix, visitor) {
2261
- const node = this.findNode(prefix);
2262
- if (!node) {
2263
- return;
2264
- }
2265
- await this.visitFromNode(node, prefix, visitor);
2266
- }
2267
- async tagsForKey(key) {
2268
- return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
2269
- }
2270
- async matchPattern(pattern) {
2271
- const matches = /* @__PURE__ */ new Set();
2272
- this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
2273
- return [...matches];
2274
- }
2275
- async forEachKeyMatchingPattern(pattern, visitor) {
2276
- const matches = await this.matchPattern(pattern);
2277
- for (const key of matches) {
2278
- await visitor(key);
2279
- }
2280
- }
2281
- async clear() {
2282
- this.tagToKeys.clear();
2283
- this.keyToTags.clear();
2284
- this.knownKeys.clear();
2285
- this.root.children.clear();
2286
- this.root.terminal = false;
2287
- this.nextNodeId = this.root.id + 1;
2288
- }
2289
- createTrieNode() {
2290
- return {
2291
- id: this.nextNodeId++,
2292
- terminal: false,
2293
- children: /* @__PURE__ */ new Map()
2294
- };
2295
- }
2296
- insertKnownKey(key) {
2297
- if (this.knownKeys.has(key)) {
2298
- return;
2299
- }
2300
- this.knownKeys.add(key);
2301
- let node = this.root;
2302
- for (const character of key) {
2303
- let child = node.children.get(character);
2304
- if (!child) {
2305
- child = this.createTrieNode();
2306
- node.children.set(character, child);
2307
- }
2308
- node = child;
2309
- }
2310
- node.terminal = true;
2311
- }
2312
- findNode(prefix) {
2313
- let node = this.root;
2314
- for (const character of prefix) {
2315
- node = node.children.get(character);
2316
- if (!node) {
2317
- return void 0;
2318
- }
2319
- }
2320
- return node;
2321
- }
2322
- collectFromNode(node, prefix, matches) {
2323
- if (node.terminal) {
2324
- matches.push(prefix);
2325
- }
2326
- for (const [character, child] of node.children) {
2327
- this.collectFromNode(child, `${prefix}${character}`, matches);
2328
- }
2329
- }
2330
- async visitFromNode(node, prefix, visitor) {
2331
- if (node.terminal) {
2332
- await visitor(prefix);
2333
- }
2334
- for (const [character, child] of node.children) {
2335
- await this.visitFromNode(child, `${prefix}${character}`, visitor);
2336
- }
2337
- }
2338
- collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
2339
- if (depth > MAX_PATTERN_RECURSION_DEPTH) {
2340
- return;
2341
- }
2342
- const stateKey = `${node.id}:${patternIndex}`;
2343
- if (visited.has(stateKey)) {
2344
- return;
2345
- }
2346
- visited.add(stateKey);
2347
- if (patternIndex === pattern.length) {
2348
- if (node.terminal) {
2349
- matches.add(prefix);
2350
- }
2351
- return;
2352
- }
2353
- const patternChar = pattern[patternIndex];
2354
- if (patternChar === void 0) {
2355
- return;
2356
- }
2357
- if (patternChar === "*") {
2358
- this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
2359
- for (const [character, child2] of node.children) {
2360
- this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
2361
- }
2362
- return;
2363
- }
2364
- if (patternChar === "?") {
2365
- for (const [character, child2] of node.children) {
2366
- this.collectPatternMatches(
2367
- child2,
2368
- `${prefix}${character}`,
2369
- pattern,
2370
- patternIndex + 1,
2371
- matches,
2372
- visited,
2373
- depth + 1
2374
- );
2375
- }
2376
- return;
2377
- }
2378
- const child = node.children.get(patternChar);
2379
- if (child) {
2380
- this.collectPatternMatches(
2381
- child,
2382
- `${prefix}${patternChar}`,
2383
- pattern,
2384
- patternIndex + 1,
2385
- matches,
2386
- visited,
2387
- depth + 1
2388
- );
2389
- }
2390
- }
2391
- pruneKnownKeysIfNeeded() {
2392
- if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
2393
- return;
2394
- }
2395
- const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
2396
- let removed = 0;
2397
- for (const key of this.knownKeys) {
2398
- if (removed >= toRemove) {
2399
- break;
2400
- }
2401
- this.removeKey(key);
2402
- removed += 1;
2403
- }
2404
- }
2405
- removeKey(key) {
2406
- this.removeKnownKey(key);
2407
- const tags = this.keyToTags.get(key);
2408
- if (!tags) {
2409
- return;
2410
- }
2411
- for (const tag of tags) {
2412
- const keys = this.tagToKeys.get(tag);
2413
- if (!keys) {
2414
- continue;
2415
- }
2416
- keys.delete(key);
2417
- if (keys.size === 0) {
2418
- this.tagToKeys.delete(tag);
2419
- }
2420
- }
2421
- this.keyToTags.delete(key);
2422
- }
2423
- removeKnownKey(key) {
2424
- if (!this.knownKeys.delete(key)) {
2425
- return;
2426
- }
2427
- const path2 = [];
2428
- let node = this.root;
2429
- for (const character of key) {
2430
- const child = node.children.get(character);
2431
- if (!child) {
2432
- return;
2433
- }
2434
- path2.push([node, character]);
2435
- node = child;
2436
- }
2437
- node.terminal = false;
2438
- for (let index = path2.length - 1; index >= 0; index -= 1) {
2439
- const entry = path2[index];
2440
- if (!entry) {
2441
- continue;
2442
- }
2443
- const [parent, character] = entry;
2444
- const child = parent.children.get(character);
2445
- if (!child || child.terminal || child.children.size > 0) {
2446
- break;
2447
- }
2448
- parent.children.delete(character);
2449
- }
2450
- }
2451
- };
2452
-
2453
- // ../../src/serialization/JsonSerializer.ts
2454
- var JsonSerializer = class {
2455
- serialize(value) {
2456
- return JSON.stringify(value);
2457
- }
2458
- deserialize(payload) {
2459
- const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
2460
- let parsed;
2461
- try {
2462
- parsed = JSON.parse(normalized);
2463
- } catch (error) {
2464
- const message = error instanceof Error ? error.message : String(error);
2465
- throw new Error(`JsonSerializer: failed to parse JSON payload: ${message}`);
2466
- }
2467
- return sanitizeStructuredData(parsed, {
2468
- label: "JSON payload",
2469
- maxDepth: 200,
2470
- maxNodes: 1e4
2471
- });
2472
- }
2473
- };
2474
-
2475
- // ../../src/stampede/StampedeGuard.ts
2476
- var StampedeGuard = class {
2477
- inFlight = /* @__PURE__ */ new Map();
2478
- maxInFlight;
2479
- entryTimeoutMs;
2480
- constructor(options = {}) {
2481
- this.maxInFlight = options.maxInFlight ?? 1e4;
2482
- this.entryTimeoutMs = options.entryTimeoutMs;
2483
- }
2484
- async execute(key, task) {
2485
- const existing = this.inFlight.get(key);
2486
- if (existing) {
2487
- existing.references += 1;
2488
- try {
2489
- return await existing.promise;
2490
- } finally {
2491
- this.releaseEntry(key, existing);
2492
- }
2493
- }
2494
- if (this.inFlight.size >= this.maxInFlight) {
2495
- throw new Error(
2496
- `StampedeGuard: in-flight limit of ${this.maxInFlight} exceeded. Rejecting new key to prevent memory exhaustion.`
2497
- );
2498
- }
2499
- const taskPromise = Promise.resolve().then(task);
2500
- const guardedPromise = this.entryTimeoutMs ? this.withTimeout(key, taskPromise, this.entryTimeoutMs) : taskPromise;
2501
- const entry = {
2502
- promise: guardedPromise,
2503
- references: 1
2504
- };
2505
- this.inFlight.set(key, entry);
2506
- try {
2507
- return await entry.promise;
2508
- } finally {
2509
- this.releaseEntry(key, entry);
2510
- }
2511
- }
2512
- withTimeout(key, promise, timeoutMs) {
2513
- return new Promise((resolve, reject) => {
2514
- const timer = setTimeout(() => {
2515
- reject(
2516
- new Error(
2517
- `StampedeGuard: task for key "${key.slice(0, 64)}${key.length > 64 ? "..." : ""}" timed out after ${timeoutMs}ms.`
2518
- )
2519
- );
2520
- }, timeoutMs);
2521
- promise.then(
2522
- (value) => {
2523
- clearTimeout(timer);
2524
- resolve(value);
2525
- },
2526
- (error) => {
2527
- clearTimeout(timer);
2528
- reject(error);
2529
- }
2530
- );
2531
- });
2532
- }
2533
- releaseEntry(key, entry) {
2534
- entry.references -= 1;
2535
- const current = this.inFlight.get(key);
2536
- if (current === entry && entry.references === 0) {
2537
- this.inFlight.delete(key);
2538
- }
2539
- }
2540
- };
2541
-
2542
- // ../../src/types.ts
2543
- var CacheMissError = class extends Error {
2544
- key;
2545
- constructor(key) {
2546
- super(`Cache miss for key "${key}".`);
2547
- this.name = "CacheMissError";
2548
- this.key = key;
2549
- }
2550
- };
2551
-
2552
- // ../../src/CacheStack.ts
2553
- var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
2554
- var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
2555
- var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
2556
- var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
2557
- var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
2558
- var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
2559
- var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
2560
- var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
2561
- var DebugLogger = class {
2562
- enabled;
2563
- constructor(enabled) {
2564
- this.enabled = enabled;
2565
- }
2566
- debug(message, context) {
2567
- this.write("debug", message, context);
2568
- }
2569
- info(message, context) {
2570
- this.write("info", message, context);
2571
- }
2572
- warn(message, context) {
2573
- this.write("warn", message, context);
2574
- }
2575
- error(message, context) {
2576
- this.write("error", message, context);
2577
- }
2578
- write(level, message, context) {
2579
- if (!this.enabled) {
2580
- return;
2581
- }
2582
- const suffix = context ? ` ${JSON.stringify(context)}` : "";
2583
- console[level](`[layercache] ${message}${suffix}`);
2584
- }
2585
- };
2586
- var CacheStack = class extends EventEmitter {
2587
- constructor(layers, options = {}) {
2588
- super();
2589
- this.layers = layers;
2590
- this.options = options;
2591
- if (layers.length === 0) {
2592
- throw new Error("CacheStack requires at least one cache layer.");
2593
- }
2594
- this.validateConfiguration();
2595
- const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
2596
- this.ttlResolver = new TtlResolver({ maxProfileEntries });
2597
- this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
2598
- this.stampedeGuard = new StampedeGuard({
2599
- maxInFlight: options.stampedeMaxInFlight,
2600
- entryTimeoutMs: options.stampedeEntryTimeoutMs
2601
- });
2602
- this.currentGeneration = options.generation;
2603
- if (options.publishSetInvalidation !== void 0) {
2604
- console.warn(
2605
- "[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
2606
- );
2607
- }
2608
- const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
2609
- this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
2610
- this.tagIndex = options.tagIndex ?? new TagIndex();
2611
- this.keyDiscovery = new CacheKeyDiscovery({
2612
- layers: this.layers,
2613
- tagIndex: this.tagIndex,
2614
- shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2615
- handleLayerFailure: async (layer, operation, error) => {
2616
- await this.handleLayerFailure(layer, operation, error);
2617
- }
2618
- });
2619
- this.invalidation = new CacheStackInvalidationSupport({
2620
- tagIndex: this.tagIndex,
2621
- shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2622
- handleLayerFailure: async (layer, operation, error) => {
2623
- await this.handleLayerFailure(layer, operation, error);
2624
- }
2625
- });
2626
- this.layerWriter = new CacheStackLayerWriter({
2627
- layers: this.layers,
2628
- maintenance: this.maintenance,
2629
- shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2630
- shouldWriteBehind: (layer) => this.shouldWriteBehind(layer),
2631
- handleLayerFailure: async (layer, operation, error) => {
2632
- await this.handleLayerFailure(layer, operation, error);
2633
- },
2634
- enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
2635
- resolveFreshTtl: this.resolveFreshTtl.bind(this),
2636
- resolveLayerSeconds: this.resolveLayerSeconds.bind(this),
2637
- globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
2638
- globalStaleIfError: this.options.staleIfError,
2639
- writePolicy: this.options.writePolicy,
2640
- onWriteFailures: (context, failures) => {
2641
- this.metricsCollector.increment("writeFailures", failures.length);
2642
- this.logger.debug?.("write-failure", {
2643
- ...context,
2644
- failures: failures.map((failure) => this.formatError(failure))
2645
- });
2646
- }
2647
- });
2648
- if (!options.tagIndex && layers.some((layer) => layer.isLocal === false)) {
2649
- this.logger.warn?.(
2650
- "Using the default in-memory TagIndex with a shared cache layer only tracks keys seen by this process. Use RedisTagIndex for cross-instance tag invalidation."
2651
- );
2652
- }
2653
- if (!options.tagIndex && layers.some((layer) => layer.isLocal === false && !layer.keys)) {
2654
- this.logger.warn?.(
2655
- "Using the default in-memory TagIndex with a shared cache layer that does not implement keys() can leave invalidateByPattern() and invalidateByPrefix() incomplete after restarts. Use RedisTagIndex or implement keys() on the shared layer."
2656
- );
2657
- }
2658
- if (options.invalidationBus && options.broadcastL1Invalidation === void 0 && options.publishSetInvalidation === void 0) {
2659
- this.logger.warn?.(
2660
- "broadcastL1Invalidation defaults to false when an invalidation bus is configured; opt in explicitly if write-triggered L1 invalidation is desired."
2661
- );
2662
- }
2663
- this.snapshots = new CacheStackSnapshotManager({
2664
- layers: this.layers,
2665
- tagIndex: this.tagIndex,
2666
- snapshotSerializer: this.snapshotSerializer,
2667
- readLayerEntry: this.readLayerEntry.bind(this),
2668
- shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
2669
- handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
2670
- qualifyKey: this.qualifyKey.bind(this),
2671
- stripQualifiedKey: this.stripQualifiedKey.bind(this),
2672
- validateCacheKey,
2673
- formatError: this.formatError.bind(this)
2674
- });
2675
- this.initializeWriteBehind(options.writeBehind);
2676
- this.startup = this.initialize();
2677
- }
2678
- layers;
2679
- options;
2680
- stampedeGuard;
2681
- metricsCollector = new MetricsCollector();
2682
- instanceId = createInstanceId();
2683
- startup;
2684
- unsubscribeInvalidation;
2685
- logger;
2686
- tagIndex;
2687
- keyDiscovery;
2688
- fetchRateLimiter = new FetchRateLimiter();
2689
- snapshotSerializer = new JsonSerializer();
2690
- invalidation;
2691
- layerWriter;
2692
- snapshots;
2693
- backgroundRefreshes = /* @__PURE__ */ new Map();
2694
- backgroundRefreshAbort = /* @__PURE__ */ new Map();
2695
- layerDegradedUntil = /* @__PURE__ */ new Map();
2696
- maintenance = new CacheStackMaintenance();
2697
- ttlResolver;
2698
- circuitBreakerManager;
2699
- nextOperationId = 0;
2700
- currentGeneration;
2701
- isDisconnecting = false;
2702
- disconnectPromise;
2703
- /**
2704
- * Read-through cache get.
2705
- * Returns the cached value if present and fresh, or invokes `fetcher` on a miss
2706
- * and stores the result across all layers. Returns `null` if the key is not found
2707
- * and no `fetcher` is provided.
2708
- */
2709
- async get(key, fetcher, options) {
2710
- return this.observeOperation("layercache.get", { "layercache.key": String(key ?? "") }, async () => {
2711
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2712
- this.validateWriteOptions(options);
2713
- await this.awaitStartup("get");
2714
- return this.getPrepared(normalizedKey, fetcher, options);
2715
- });
2716
- }
2717
- async getPrepared(normalizedKey, fetcher, options) {
2718
- const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
2719
- if (hit.found) {
2720
- this.ttlResolver.recordAccess(normalizedKey);
2721
- if (this.isNegativeStoredValue(hit.stored)) {
2722
- this.metricsCollector.increment("negativeCacheHits");
2723
- }
2724
- if (hit.state === "fresh") {
2725
- this.metricsCollector.increment("hits");
2726
- await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
2727
- return hit.value;
2728
- }
2729
- if (hit.state === "stale-while-revalidate") {
2730
- this.metricsCollector.increment("hits");
2731
- this.metricsCollector.increment("staleHits");
2732
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2733
- if (fetcher) {
2734
- this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
2735
- }
2736
- return hit.value;
2737
- }
2738
- if (!fetcher) {
2739
- this.metricsCollector.increment("hits");
2740
- this.metricsCollector.increment("staleHits");
2741
- this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
2742
- return hit.value;
2743
- }
2744
- try {
2745
- return await this.fetchWithGuards(normalizedKey, fetcher, options);
2746
- } catch (error) {
2747
- this.metricsCollector.increment("staleHits");
2748
- this.metricsCollector.increment("refreshErrors");
2749
- this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
2750
- return hit.value;
2751
- }
2752
- }
2753
- this.metricsCollector.increment("misses");
2754
- if (!fetcher) {
2755
- return null;
2756
- }
2757
- return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
2758
- }
2759
- /**
2760
- * Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
2761
- * Fetches and caches the value if not already present.
2762
- */
2763
- async getOrSet(key, fetcher, options) {
2764
- return this.get(key, fetcher, options);
2765
- }
2766
- /**
2767
- * Like `get()`, but throws `CacheMissError` instead of returning `null`.
2768
- * Useful when the value is expected to exist or the fetcher is expected to
2769
- * return non-null.
2770
- */
2771
- async getOrThrow(key, fetcher, options) {
2772
- const value = await this.get(key, fetcher, options);
2773
- if (value === null) {
2774
- throw new CacheMissError(key);
2775
- }
2776
- return value;
2777
- }
2778
- /**
2779
- * Returns true if the given key exists and is not expired in any layer.
2780
- */
2781
- async has(key) {
2782
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2783
- await this.awaitStartup("has");
2784
- for (const layer of this.layers) {
2785
- if (this.shouldSkipLayer(layer)) {
2786
- continue;
2787
- }
2788
- if (layer.has) {
2789
- try {
2790
- const exists = await layer.has(normalizedKey);
2791
- if (exists) {
2792
- return true;
2793
- }
2794
- } catch {
2795
- await this.reportRecoverableLayerFailure(layer, "has", new Error(`has() failed for layer "${layer.name}"`));
2796
- }
2797
- } else {
2798
- try {
2799
- const value = await layer.get(normalizedKey);
2800
- if (value !== null) {
2801
- return true;
2802
- }
2803
- } catch (error) {
2804
- await this.reportRecoverableLayerFailure(layer, "has", error);
2805
- }
2806
- }
2807
- }
2808
- return false;
2809
- }
2810
- /**
2811
- * Returns the remaining TTL in seconds for the key in the fastest layer
2812
- * that has it, or null if the key is not found / has no TTL.
2813
- */
2814
- async ttl(key) {
2815
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2816
- await this.awaitStartup("ttl");
2817
- for (const layer of this.layers) {
2818
- if (this.shouldSkipLayer(layer)) {
2819
- continue;
2820
- }
2821
- if (layer.ttl) {
2822
- try {
2823
- const remaining = await layer.ttl(normalizedKey);
2824
- if (remaining !== null) {
2825
- return remaining;
2826
- }
2827
- } catch {
2828
- }
2829
- }
2830
- }
2831
- return null;
2832
- }
2833
- /**
2834
- * Stores a value in all cache layers. Overwrites any existing value.
2835
- */
2836
- async set(key, value, options) {
2837
- await this.observeOperation("layercache.set", { "layercache.key": String(key ?? "") }, async () => {
2838
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2839
- this.validateWriteOptions(options);
2840
- await this.awaitStartup("set");
2841
- await this.storeEntry(normalizedKey, "value", value, options);
2842
- });
2843
- }
2844
- /**
2845
- * Deletes the key from all layers and publishes an invalidation message.
2846
- */
2847
- async delete(key) {
2848
- await this.observeOperation("layercache.delete", { "layercache.key": String(key ?? "") }, async () => {
2849
- const normalizedKey = this.qualifyKey(validateCacheKey(key));
2850
- await this.awaitStartup("delete");
2851
- await this.deleteKeys([normalizedKey]);
2852
- await this.publishInvalidation({
2853
- scope: "key",
2854
- keys: [normalizedKey],
2855
- sourceId: this.instanceId,
2856
- operation: "delete"
2857
- });
2858
- });
2859
- }
2860
- async clear() {
2861
- await this.awaitStartup("clear");
2862
- this.maintenance.beginClearEpoch();
2863
- await Promise.all(this.layers.map((layer) => layer.clear()));
2864
- await this.tagIndex.clear();
2865
- this.ttlResolver.clearProfiles();
2866
- this.circuitBreakerManager.clear();
2867
- this.metricsCollector.increment("invalidations");
2868
- this.logger.debug?.("clear");
2869
- await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
2870
- }
2871
- /**
2872
- * Deletes multiple keys at once. More efficient than calling `delete()` in a loop.
2873
- */
2874
- async mdelete(keys) {
2875
- if (keys.length === 0) {
2876
- return;
2877
- }
2878
- await this.awaitStartup("mdelete");
2879
- const normalizedKeys = keys.map((k) => validateCacheKey(k));
2880
- const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
2881
- await this.deleteKeys(cacheKeys);
2882
- await this.publishInvalidation({
2883
- scope: "keys",
2884
- keys: cacheKeys,
2885
- sourceId: this.instanceId,
2886
- operation: "delete"
2887
- });
2888
- }
2889
- async mget(entries) {
2890
- return this.observeOperation("layercache.mget", void 0, async () => {
2891
- this.assertActive("mget");
2892
- if (entries.length === 0) {
2893
- return [];
2894
- }
2895
- const normalizedEntries = entries.map((entry) => ({
2896
- ...entry,
2897
- key: this.qualifyKey(validateCacheKey(entry.key))
2898
- }));
2899
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2900
- const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
2901
- if (!canFastPath) {
2902
- await this.awaitStartup("mget");
2903
- const pendingReads = /* @__PURE__ */ new Map();
2904
- return Promise.all(
2905
- normalizedEntries.map((entry) => {
2906
- const optionsSignature = serializeOptions(entry.options);
2907
- const existing = pendingReads.get(entry.key);
2908
- if (!existing) {
2909
- const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
2910
- pendingReads.set(entry.key, {
2911
- promise,
2912
- fetch: entry.fetch,
2913
- optionsSignature
2914
- });
2915
- return promise;
2916
- }
2917
- if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
2918
- const displayKey = entry.key.length > 64 ? `${entry.key.slice(0, 64)}...` : entry.key;
2919
- throw new Error(`mget received conflicting entries for key "${displayKey}".`);
2920
- }
2921
- return existing.promise;
2922
- })
2923
- );
2924
- }
2925
- await this.awaitStartup("mget");
2926
- const pending = /* @__PURE__ */ new Set();
2927
- const indexesByKey = /* @__PURE__ */ new Map();
2928
- const resultsByKey = /* @__PURE__ */ new Map();
2929
- for (let index = 0; index < normalizedEntries.length; index += 1) {
2930
- const entry = normalizedEntries[index];
2931
- if (!entry) continue;
2932
- const key = entry.key;
2933
- const indexes = indexesByKey.get(key) ?? [];
2934
- indexes.push(index);
2935
- indexesByKey.set(key, indexes);
2936
- pending.add(key);
2937
- }
2938
- for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
2939
- const layer = this.layers[layerIndex];
2940
- if (!layer || this.shouldSkipLayer(layer)) continue;
2941
- const keys = [...pending];
2942
- if (keys.length === 0) {
2943
- break;
2944
- }
2945
- const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
2946
- for (let offset = 0; offset < values.length; offset += 1) {
2947
- const key = keys[offset];
2948
- const stored = values[offset];
2949
- if (!key || stored === null) {
2950
- continue;
2951
- }
2952
- const resolved = resolveStoredValue(stored);
2953
- if (resolved.state === "expired") {
2954
- await layer.delete(key);
2955
- continue;
2956
- }
2957
- if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
2958
- this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
2959
- }
2960
- await this.tagIndex.touch(key);
2961
- await this.backfill(key, stored, layerIndex - 1);
2962
- resultsByKey.set(key, resolved.value);
2963
- pending.delete(key);
2964
- this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
2965
- }
2966
- }
2967
- if (pending.size > 0) {
2968
- for (const key of pending) {
2969
- await this.tagIndex.remove(key);
2970
- this.metricsCollector.increment("misses", indexesByKey.get(key)?.length ?? 1);
2971
- }
2972
- }
2973
- return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
2974
- });
2975
- }
2976
- async mset(entries) {
2977
- await this.observeOperation("layercache.mset", void 0, async () => {
2978
- this.assertActive("mset");
2979
- const normalizedEntries = entries.map((entry) => ({
2980
- ...entry,
2981
- key: this.qualifyKey(validateCacheKey(entry.key))
2982
- }));
2983
- normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
2984
- await this.awaitStartup("mset");
2985
- await this.writeBatch(normalizedEntries);
2986
- });
2987
- }
2988
- async warm(entries, options = {}) {
2989
- this.assertActive("warm");
2990
- const concurrency = Math.max(1, options.concurrency ?? 4);
2991
- const total = entries.length;
2992
- let completed = 0;
2993
- const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
2994
- const workers = Array.from({ length: Math.min(concurrency, queue.length || 1) }, async () => {
2995
- while (queue.length > 0) {
2996
- const entry = queue.shift();
2997
- if (!entry) {
2998
- return;
2999
- }
3000
- let success = false;
3001
- try {
3002
- await this.get(entry.key, entry.fetcher, entry.options);
3003
- this.emit("warm", { key: entry.key });
3004
- success = true;
3005
- } catch (error) {
3006
- this.emitError("warm", { key: entry.key, error: this.formatError(error) });
3007
- if (!options.continueOnError) {
3008
- throw error;
3009
- }
3010
- } finally {
3011
- completed += 1;
3012
- const progress = { completed, total, key: entry.key, success };
3013
- options.onProgress?.(progress);
3014
- }
3015
- }
3016
- });
3017
- await Promise.all(workers);
3018
- }
3019
- /**
3020
- * Returns a cached version of `fetcher`. The cache key is derived from
3021
- * `prefix` plus the serialized arguments unless a `keyResolver` is provided.
3022
- */
3023
- wrap(prefix, fetcher, options = {}) {
3024
- return (...args) => {
3025
- const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
3026
- const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
3027
- return this.get(key, () => fetcher(...args), options);
3028
- };
3029
- }
3030
- /**
3031
- * Creates a `CacheNamespace` that automatically prefixes all keys with
3032
- * `prefix:`. Useful for multi-tenant or module-level isolation.
3033
- */
3034
- namespace(prefix) {
3035
- validateNamespaceKey(prefix);
3036
- return new CacheNamespace(this, prefix);
3037
- }
3038
- async invalidateByTag(tag) {
3039
- await this.observeOperation("layercache.invalidate_by_tag", void 0, async () => {
3040
- validateTag(tag);
3041
- await this.awaitStartup("invalidateByTag");
3042
- const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
3043
- await this.deleteKeys(keys);
3044
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3045
- });
3046
- }
3047
- async invalidateByTags(tags, mode = "any") {
3048
- await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
3049
- if (tags.length === 0) {
3050
- return;
3051
- }
3052
- validateTags(tags);
3053
- await this.awaitStartup("invalidateByTags");
3054
- const keysByTag = await Promise.all(
3055
- tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
3056
- );
3057
- const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
3058
- this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
3059
- await this.deleteKeys(keys);
3060
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3061
- });
3062
- }
3063
- async invalidateByPattern(pattern) {
3064
- await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
3065
- validatePattern(pattern);
3066
- await this.awaitStartup("invalidateByPattern");
3067
- const keys = await this.keyDiscovery.collectKeysMatchingPattern(
3068
- this.qualifyPattern(pattern),
3069
- this.invalidationMaxKeys()
3070
- );
3071
- await this.deleteKeys(keys);
3072
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3073
- });
3074
- }
3075
- async invalidateByPrefix(prefix) {
3076
- await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
3077
- await this.awaitStartup("invalidateByPrefix");
3078
- const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
3079
- const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
3080
- await this.deleteKeys(keys);
3081
- await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
3082
- });
3083
- }
3084
- getMetrics() {
3085
- return this.metricsCollector.snapshot;
3086
- }
3087
- getStats() {
3088
- return {
3089
- metrics: this.getMetrics(),
3090
- layers: this.layers.map((layer) => ({
3091
- name: layer.name,
3092
- isLocal: Boolean(layer.isLocal),
3093
- degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
3094
- })),
3095
- backgroundRefreshes: this.backgroundRefreshes.size
3096
- };
3097
- }
3098
- resetMetrics() {
3099
- this.metricsCollector.reset();
3100
- }
3101
- /**
3102
- * Returns computed hit-rate statistics (overall and per-layer).
3103
- */
3104
- getHitRate() {
3105
- return this.metricsCollector.hitRate();
3106
- }
3107
- async healthCheck() {
3108
- await this.startup;
3109
- return Promise.all(
3110
- this.layers.map(async (layer) => {
3111
- const startedAt = performance.now();
3112
- try {
3113
- const healthy = layer.ping ? await layer.ping() : true;
3114
- return {
3115
- layer: layer.name,
3116
- healthy,
3117
- latencyMs: performance.now() - startedAt
3118
- };
3119
- } catch (error) {
3120
- return {
3121
- layer: layer.name,
3122
- healthy: false,
3123
- latencyMs: performance.now() - startedAt,
3124
- error: this.formatError(error)
3125
- };
3126
- }
3127
- })
3128
- );
3129
- }
3130
- /**
3131
- * Rotates the active generation prefix used for all future cache keys.
3132
- * Previous-generation keys remain in the underlying layers until they expire,
3133
- * unless `generationCleanup` is enabled to prune them in the background.
3134
- */
3135
- bumpGeneration(nextGeneration) {
3136
- const current = this.currentGeneration ?? 0;
3137
- const previousGeneration = this.currentGeneration;
3138
- const updatedGeneration = nextGeneration ?? current + 1;
3139
- const generationToCleanup = resolveGenerationCleanupTarget({
3140
- previousGeneration,
3141
- nextGeneration: updatedGeneration,
3142
- generationCleanup: this.options.generationCleanup
3143
- });
3144
- this.currentGeneration = updatedGeneration;
3145
- if (generationToCleanup !== null) {
3146
- this.scheduleGenerationCleanup(generationToCleanup);
3147
- }
3148
- return this.currentGeneration;
3149
- }
3150
- /**
3151
- * Returns detailed metadata about a single cache key: which layers contain it,
3152
- * remaining fresh/stale/error TTLs, and associated tags.
3153
- * Returns `null` if the key does not exist in any layer.
3154
- */
3155
- async inspect(key) {
3156
- const userKey = validateCacheKey(key);
3157
- const normalizedKey = this.qualifyKey(userKey);
3158
- await this.awaitStartup("inspect");
3159
- const foundInLayers = [];
3160
- let freshTtlSeconds = null;
3161
- let staleTtlSeconds = null;
3162
- let errorTtlSeconds = null;
3163
- let isStale = false;
3164
- for (const layer of this.layers) {
3165
- if (this.shouldSkipLayer(layer)) {
3166
- continue;
3167
- }
3168
- const stored = await this.readLayerEntry(layer, normalizedKey);
3169
- if (stored === null) {
3170
- continue;
3171
- }
3172
- const resolved = resolveStoredValue(stored);
3173
- if (resolved.state === "expired") {
3174
- continue;
3175
- }
3176
- foundInLayers.push(layer.name);
3177
- if (foundInLayers.length === 1 && resolved.envelope) {
3178
- const now = Date.now();
3179
- freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
3180
- staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
3181
- errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
3182
- isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
3183
- }
3184
- }
3185
- if (foundInLayers.length === 0) {
3186
- return null;
3187
- }
3188
- const tags = await this.getTagsForKey(normalizedKey);
3189
- return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
3190
- }
3191
- async exportState() {
3192
- await this.awaitStartup("exportState");
3193
- return this.snapshots.exportState(this.snapshotMaxEntries());
3194
- }
3195
- async importState(entries) {
3196
- await this.awaitStartup("importState");
3197
- await this.snapshots.importState(entries);
3198
- }
3199
- async persistToFile(filePath) {
3200
- this.assertActive("persistToFile");
3201
- await this.snapshots.persistToFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxEntries());
3202
- }
3203
- async restoreFromFile(filePath) {
3204
- this.assertActive("restoreFromFile");
3205
- await this.snapshots.restoreFromFile(filePath, this.options.snapshotBaseDir, this.snapshotMaxBytes());
3206
- }
3207
- async disconnect() {
3208
- if (!this.disconnectPromise) {
3209
- this.isDisconnecting = true;
3210
- this.disconnectPromise = (async () => {
3211
- await this.startup;
3212
- await this.unsubscribeInvalidation?.();
3213
- await this.flushWriteBehindQueue();
3214
- await this.maintenance.waitForGenerationCleanup();
3215
- for (const key of this.backgroundRefreshAbort.keys()) {
3216
- this.backgroundRefreshAbort.set(key, true);
3217
- }
3218
- await Promise.allSettled(
3219
- [...this.backgroundRefreshes.values()].map((promise) => {
3220
- let timer;
3221
- return Promise.race([
3222
- promise,
3223
- new Promise((resolve) => {
3224
- timer = setTimeout(resolve, 5e3);
3225
- timer.unref?.();
3226
- })
3227
- ]).finally(() => {
3228
- if (timer) clearTimeout(timer);
3229
- });
3230
- })
3231
- );
3232
- this.backgroundRefreshes.clear();
3233
- this.backgroundRefreshAbort.clear();
3234
- this.maintenance.disposeWriteBehindTimer();
3235
- this.fetchRateLimiter.dispose();
3236
- await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
3237
- })();
3238
- }
3239
- await this.disconnectPromise;
3240
- }
3241
- async initialize() {
3242
- if (!this.options.invalidationBus) {
3243
- return;
3244
- }
3245
- this.unsubscribeInvalidation = await this.options.invalidationBus.subscribe(async (message) => {
3246
- await this.handleInvalidationMessage(message);
3247
- });
3248
- }
3249
- async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
3250
- const fetchTask = async () => {
3251
- const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
3252
- if (shouldRecheckFreshLayers) {
3253
- const secondHit = await this.readFromLayers(key, options, "fresh-only");
3254
- if (secondHit.found) {
3255
- this.metricsCollector.increment("hits");
3256
- return secondHit.value;
3257
- }
3258
- }
3259
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3260
- };
3261
- const singleFlightTask = async () => {
3262
- if (!this.options.singleFlightCoordinator) {
3263
- return fetchTask();
3264
- }
3265
- try {
3266
- return await this.options.singleFlightCoordinator.execute(
3267
- key,
3268
- this.resolveSingleFlightOptions(),
3269
- fetchTask,
3270
- () => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
3271
- );
3272
- } catch (error) {
3273
- if (!this.isGracefulDegradationEnabled()) {
3274
- throw error;
3275
- }
3276
- this.metricsCollector.increment("degradedOperations");
3277
- this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
3278
- this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
3279
- return fetchTask();
3280
- }
3281
- };
3282
- if (this.options.stampedePrevention === false) {
3283
- return singleFlightTask();
3284
- }
3285
- return this.stampedeGuard.execute(key, singleFlightTask);
3286
- }
3287
- async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3288
- const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
3289
- const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
3290
- const deadline = Date.now() + timeoutMs;
3291
- this.metricsCollector.increment("singleFlightWaits");
3292
- this.emit("stampede-dedupe", { key });
3293
- while (Date.now() < deadline) {
3294
- const hit = await this.readFromLayers(key, options, "fresh-only");
3295
- if (hit.found) {
3296
- this.metricsCollector.increment("hits");
3297
- return hit.value;
3298
- }
3299
- await this.sleep(pollIntervalMs);
3300
- }
3301
- return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
3302
- }
3303
- async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3304
- this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
3305
- this.metricsCollector.increment("fetches");
3306
- const fetchStart = Date.now();
3307
- let fetched;
3308
- try {
3309
- fetched = await this.fetchRateLimiter.schedule(
3310
- options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
3311
- { key, fetcher },
3312
- fetcher
3313
- );
3314
- this.circuitBreakerManager.recordSuccess(key);
3315
- this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
3316
- } catch (error) {
3317
- this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
3318
- throw error;
3319
- }
3320
- if (fetched === null || fetched === void 0) {
3321
- if (!this.shouldNegativeCache(options)) {
3322
- return null;
3323
- }
3324
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3325
- this.logger.debug?.("skip-negative-store-after-invalidation", {
3326
- key,
3327
- expectedClearEpoch,
3328
- clearEpoch: this.maintenance.currentClearEpoch(),
3329
- expectedKeyEpoch,
3330
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3331
- });
3332
- return null;
3333
- }
3334
- await this.storeEntry(key, "empty", null, options);
3335
- return null;
3336
- }
3337
- if (options?.shouldCache) {
3338
- try {
3339
- if (!options.shouldCache(fetched)) {
3340
- return fetched;
3341
- }
3342
- } catch (error) {
3343
- this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
3344
- }
3345
- }
3346
- if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
3347
- this.logger.debug?.("skip-store-after-invalidation", {
3348
- key,
3349
- expectedClearEpoch,
3350
- clearEpoch: this.maintenance.currentClearEpoch(),
3351
- expectedKeyEpoch,
3352
- keyEpoch: this.maintenance.currentKeyEpoch(key)
3353
- });
3354
- return fetched;
3355
- }
3356
- await this.storeEntry(key, "value", fetched, options);
3357
- return fetched;
3358
- }
3359
- async storeEntry(key, kind, value, options) {
3360
- const clearEpoch = this.maintenance.currentClearEpoch();
3361
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3362
- await this.layerWriter.writeAcrossLayers(key, kind, value, options);
3363
- if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
3364
- return;
3365
- }
3366
- if (options?.tags) {
3367
- await this.tagIndex.track(key, options.tags);
3368
- } else {
3369
- await this.tagIndex.touch(key);
3370
- }
3371
- this.metricsCollector.increment("sets");
3372
- this.logger.debug?.("set", { key, kind, tags: options?.tags });
3373
- this.emit("set", { key, kind, tags: options?.tags });
3374
- if (this.shouldBroadcastL1Invalidation()) {
3375
- await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
3376
- }
3377
- }
3378
- async writeBatch(entries) {
3379
- const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(entries);
3380
- if (clearEpoch !== this.maintenance.currentClearEpoch()) {
3381
- return;
3382
- }
3383
- for (const entry of entries) {
3384
- if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
3385
- continue;
3386
- }
3387
- if (entry.options?.tags) {
3388
- await this.tagIndex.track(entry.key, entry.options.tags);
3389
- } else {
3390
- await this.tagIndex.touch(entry.key);
3391
- }
3392
- this.metricsCollector.increment("sets");
3393
- this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
3394
- this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
3395
- }
3396
- if (this.shouldBroadcastL1Invalidation()) {
3397
- await this.publishInvalidation({
3398
- scope: "keys",
3399
- keys: entries.map((entry) => entry.key),
3400
- sourceId: this.instanceId,
3401
- operation: "write"
3402
- });
3403
- }
3404
- }
3405
- async readFromLayers(key, options, mode) {
3406
- let sawRetainableValue = false;
3407
- for (let index = 0; index < this.layers.length; index += 1) {
3408
- const layer = this.layers[index];
3409
- if (!layer) continue;
3410
- const readStart = performance.now();
3411
- const stored = await this.readLayerEntry(layer, key);
3412
- const readDuration = performance.now() - readStart;
3413
- this.metricsCollector.recordLatency(layer.name, readDuration);
3414
- if (stored === null) {
3415
- this.metricsCollector.incrementLayer("missesByLayer", layer.name);
3416
- continue;
3417
- }
3418
- const resolved = resolveStoredValue(stored);
3419
- if (resolved.state === "expired") {
3420
- await layer.delete(key);
3421
- continue;
3422
- }
3423
- sawRetainableValue = true;
3424
- if (mode === "fresh-only" && resolved.state !== "fresh") {
3425
- continue;
3426
- }
3427
- await this.tagIndex.touch(key);
3428
- await this.backfill(key, stored, index - 1, options);
3429
- this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
3430
- this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
3431
- this.emit("hit", { key, layer: layer.name, state: resolved.state });
3432
- return {
3433
- found: true,
3434
- value: resolved.value,
3435
- stored,
3436
- state: resolved.state,
3437
- layerIndex: index,
3438
- layerName: layer.name
3439
- };
3440
- }
3441
- if (!sawRetainableValue) {
3442
- await this.tagIndex.remove(key);
3443
- }
3444
- this.logger.debug?.("miss", { key, mode });
3445
- this.emit("miss", { key, mode });
3446
- return { found: false, value: null, stored: null, state: "miss" };
3447
- }
3448
- async readLayerEntry(layer, key) {
3449
- if (this.shouldSkipLayer(layer)) {
3450
- return null;
3451
- }
3452
- if (layer.getEntry) {
3453
- try {
3454
- return await layer.getEntry(key);
3455
- } catch (error) {
3456
- return this.handleLayerFailure(layer, "read", error);
3457
- }
3458
- }
3459
- try {
3460
- return await layer.get(key);
3461
- } catch (error) {
3462
- return this.handleLayerFailure(layer, "read", error);
3463
- }
3464
- }
3465
- async backfill(key, stored, upToIndex, options) {
3466
- if (upToIndex < 0) {
3467
- return;
3468
- }
3469
- for (let index = 0; index <= upToIndex; index += 1) {
3470
- const layer = this.layers[index];
3471
- if (!layer || this.shouldSkipLayer(layer)) {
3472
- continue;
3473
- }
3474
- const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
3475
- try {
3476
- await layer.set(key, stored, ttl);
3477
- } catch (error) {
3478
- await this.handleLayerFailure(layer, "backfill", error);
3479
- continue;
3480
- }
3481
- this.metricsCollector.increment("backfills");
3482
- this.logger.debug?.("backfill", { key, layer: layer.name });
3483
- this.emit("backfill", { key, layer: layer.name });
3484
- }
3485
- }
3486
- resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
3487
- return this.ttlResolver.resolveFreshTtl(
3488
- key,
3489
- layerName,
3490
- kind,
3491
- options,
3492
- fallbackTtl,
3493
- this.options.negativeTtl,
3494
- void 0,
3495
- value
3496
- );
3497
- }
3498
- resolveLayerSeconds(layerName, override, globalDefault, fallback) {
3499
- return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
3500
- }
3501
- shouldNegativeCache(options) {
3502
- return options?.negativeCache ?? this.options.negativeCaching ?? false;
3503
- }
3504
- scheduleBackgroundRefresh(key, fetcher, options) {
3505
- if (!shouldStartBackgroundRefresh({
3506
- isDisconnecting: this.isDisconnecting,
3507
- hasRefreshInFlight: this.backgroundRefreshes.has(key)
3508
- })) {
3509
- return;
3510
- }
3511
- const clearEpoch = this.maintenance.currentClearEpoch();
3512
- const keyEpoch = this.maintenance.currentKeyEpoch(key);
3513
- this.backgroundRefreshAbort.set(key, false);
3514
- const refresh = (async () => {
3515
- this.metricsCollector.increment("refreshes");
3516
- try {
3517
- if (this.backgroundRefreshAbort.get(key)) return;
3518
- await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
3519
- } catch (error) {
3520
- if (this.backgroundRefreshAbort.get(key)) return;
3521
- this.metricsCollector.increment("refreshErrors");
3522
- this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
3523
- } finally {
3524
- this.backgroundRefreshes.delete(key);
3525
- this.backgroundRefreshAbort.delete(key);
3526
- }
3527
- })();
3528
- this.backgroundRefreshes.set(key, refresh);
3529
- }
3530
- async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
3531
- const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
3532
- await this.fetchWithGuards(
3533
- key,
3534
- () => this.withTimeout(fetcher(), timeoutMs, () => {
3535
- return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
3536
- }),
3537
- options,
3538
- expectedClearEpoch,
3539
- expectedKeyEpoch
3540
- );
3541
- }
3542
- resolveSingleFlightOptions() {
3543
- return {
3544
- leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
3545
- waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
3546
- pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
3547
- renewIntervalMs: this.options.singleFlightRenewIntervalMs
3548
- };
3549
- }
3550
- async deleteKeys(keys) {
3551
- if (keys.length === 0) {
3552
- return;
3553
- }
3554
- this.maintenance.bumpKeyEpochs(keys);
3555
- await this.invalidation.deleteKeysFromLayers(this.layers, keys);
3556
- for (const key of keys) {
3557
- await this.tagIndex.remove(key);
3558
- this.ttlResolver.deleteProfile(key);
3559
- this.circuitBreakerManager.delete(key);
3560
- }
3561
- this.metricsCollector.increment("deletes", keys.length);
3562
- this.metricsCollector.increment("invalidations");
3563
- this.logger.debug?.("delete", { keys });
3564
- this.emit("delete", { keys });
3565
- }
3566
- async publishInvalidation(message) {
3567
- if (!this.options.invalidationBus) {
3568
- return;
3569
- }
3570
- await this.options.invalidationBus.publish(message);
3571
- }
3572
- async handleInvalidationMessage(message) {
3573
- if (message.sourceId === this.instanceId) {
3574
- return;
3575
- }
3576
- const localLayers = this.layers.filter((layer) => layer.isLocal);
3577
- if (message.scope === "clear") {
3578
- this.maintenance.beginClearEpoch();
3579
- await Promise.all(localLayers.map((layer) => layer.clear()));
3580
- await this.tagIndex.clear();
3581
- this.ttlResolver.clearProfiles();
3582
- this.circuitBreakerManager.clear();
3583
- return;
3584
- }
3585
- const keys = message.keys ?? [];
3586
- this.maintenance.bumpKeyEpochs(keys);
3587
- await this.invalidation.deleteKeysFromLayers(localLayers, keys);
3588
- if (message.operation !== "write") {
3589
- for (const key of keys) {
3590
- await this.tagIndex.remove(key);
3591
- this.ttlResolver.deleteProfile(key);
3592
- this.circuitBreakerManager.delete(key);
3593
- }
3594
- }
3595
- }
3596
- async getTagsForKey(key) {
3597
- if (this.tagIndex.tagsForKey) {
3598
- return this.tagIndex.tagsForKey(key);
3599
- }
3600
- return [];
3601
- }
3602
- formatError(error) {
3603
- if (error instanceof Error) {
3604
- return error.message;
3605
- }
3606
- return String(error);
3607
- }
3608
- sleep(ms) {
3609
- return new Promise((resolve) => setTimeout(resolve, ms));
3610
- }
3611
- async withTimeout(promise, timeoutMs, onTimeout) {
3612
- if (timeoutMs <= 0) {
3613
- return promise;
3614
- }
3615
- let timer;
3616
- const observedPromise = promise.then(
3617
- (value) => ({ kind: "value", value }),
3618
- (error) => ({ kind: "error", error })
3619
- );
3620
- try {
3621
- const result = await Promise.race([
3622
- observedPromise,
3623
- new Promise((_, reject) => {
3624
- timer = setTimeout(() => reject(onTimeout()), timeoutMs);
3625
- timer.unref?.();
3626
- })
3627
- ]);
3628
- if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
3629
- if (result.kind === "error") {
3630
- throw result.error;
3631
- }
3632
- return result.value;
3633
- }
3634
- return result;
3635
- } finally {
3636
- if (timer) {
3637
- clearTimeout(timer);
3638
- }
3639
- }
3640
- }
3641
- shouldBroadcastL1Invalidation() {
3642
- return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
3643
- }
3644
- async observeOperation(name, attributes, execute) {
3645
- const id = this.nextOperationId;
3646
- this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
3647
- this.emit("operation-start", { id, name, attributes });
3648
- try {
3649
- const result = await execute();
3650
- this.emit("operation-end", {
3651
- id,
3652
- name,
3653
- attributes,
3654
- success: true,
3655
- result: result === null ? "null" : void 0
3656
- });
3657
- return result;
3658
- } catch (error) {
3659
- this.emit("operation-end", {
3660
- id,
3661
- name,
3662
- attributes,
3663
- success: false,
3664
- error
3665
- });
3666
- throw error;
3667
- }
3668
- }
3669
- scheduleGenerationCleanup(generation) {
3670
- this.maintenance.scheduleGenerationCleanup(
3671
- generation,
3672
- async (generationToClean) => this.cleanupGeneration(generationToClean),
3673
- (failedGeneration, error) => {
3674
- this.logger.warn?.("generation-cleanup-error", {
3675
- generation: failedGeneration,
3676
- error: this.formatError(error)
3677
- });
3678
- }
3679
- );
3680
- }
3681
- async cleanupGeneration(generation) {
3682
- const prefix = `v${generation}:`;
3683
- const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
3684
- for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
3685
- await this.deleteKeys(batch);
3686
- await this.publishInvalidation({
3687
- scope: "keys",
3688
- keys: batch,
3689
- sourceId: this.instanceId,
3690
- operation: "invalidate"
3691
- });
3692
- }
3693
- }
3694
- initializeWriteBehind(options) {
3695
- this.maintenance.initializeWriteBehindTimer(
3696
- this.options.writeStrategy,
3697
- options,
3698
- this.flushWriteBehindQueue.bind(this)
3699
- );
3700
- }
3701
- shouldWriteBehind(layer) {
3702
- return this.options.writeStrategy === "write-behind" && !layer.isLocal;
3703
- }
3704
- async enqueueWriteBehind(operation) {
3705
- await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3706
- }
3707
- async flushWriteBehindQueue() {
3708
- await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
3709
- }
3710
- async runWriteBehindBatch(batch) {
3711
- const results = await Promise.allSettled(batch.map((operation) => operation()));
3712
- const failures = results.filter((result) => result.status === "rejected");
3713
- if (failures.length === 0) {
3714
- return;
3715
- }
3716
- this.metricsCollector.increment("writeFailures", failures.length);
3717
- this.logger.error?.("write-behind-flush-failure", {
3718
- failed: failures.length,
3719
- total: batch.length,
3720
- errors: failures.map((failure) => this.formatError(failure.reason))
3721
- });
3722
- this.emitError("write-behind", { failed: failures.length, total: batch.length });
3723
- }
3724
- qualifyKey(key) {
3725
- return qualifyGenerationKey(key, this.currentGeneration);
3726
- }
3727
- qualifyPattern(pattern) {
3728
- return qualifyGenerationPattern(pattern, this.currentGeneration);
3729
- }
3730
- stripQualifiedKey(key) {
3731
- return stripGenerationPrefix(key, this.currentGeneration);
3732
- }
3733
- validateConfiguration() {
3734
- if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
3735
- throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
3736
- }
3737
- if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
3738
- throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
3739
- }
3740
- validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
3741
- validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
3742
- validateLayerNumberOption("staleIfError", this.options.staleIfError);
3743
- validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
3744
- validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
3745
- validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
3746
- validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
3747
- validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
3748
- validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
3749
- validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
3750
- if (this.options.snapshotMaxBytes !== false) {
3751
- validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
3752
- }
3753
- if (this.options.snapshotMaxEntries !== false) {
3754
- validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
3755
- }
3756
- if (this.options.invalidationMaxKeys !== false) {
3757
- validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
3758
- }
3759
- validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
3760
- validateAdaptiveTtlOptions(this.options.adaptiveTtl);
3761
- validateCircuitBreakerOptions(this.options.circuitBreaker);
3762
- if (typeof this.options.generationCleanup === "object") {
3763
- validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
3764
- }
3765
- if (this.options.generation !== void 0) {
3766
- validateNonNegativeNumber("generation", this.options.generation);
3767
- }
3768
- }
3769
- validateWriteOptions(options) {
3770
- if (!options) {
3771
- return;
3772
- }
3773
- validateLayerNumberOption("options.ttl", options.ttl);
3774
- validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
3775
- validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
3776
- validateLayerNumberOption("options.staleIfError", options.staleIfError);
3777
- validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
3778
- validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
3779
- validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
3780
- validateAdaptiveTtlOptions(options.adaptiveTtl);
3781
- validateCircuitBreakerOptions(options.circuitBreaker);
3782
- validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
3783
- validateTags(options.tags);
3784
- }
3785
- assertActive(operation) {
3786
- if (this.isDisconnecting) {
3787
- throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
3788
- }
3789
- }
3790
- async awaitStartup(operation) {
3791
- this.assertActive(operation);
3792
- await this.startup;
3793
- this.assertActive(operation);
3794
- }
3795
- async applyFreshReadPolicies(key, hit, options, fetcher) {
3796
- const plan = planFreshReadPolicies({
3797
- stored: hit.stored,
3798
- hasFetcher: Boolean(fetcher),
3799
- slidingTtl: options?.slidingTtl ?? false,
3800
- refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
3801
- });
3802
- if (plan.refreshedStored) {
3803
- for (let index = 0; index <= hit.layerIndex; index += 1) {
3804
- const layer = this.layers[index];
3805
- if (!layer || this.shouldSkipLayer(layer)) {
3806
- continue;
3807
- }
3808
- try {
3809
- await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
3810
- } catch (error) {
3811
- await this.handleLayerFailure(layer, "sliding-ttl", error);
3812
- }
3813
- }
3814
- }
3815
- if (fetcher && plan.shouldScheduleBackgroundRefresh) {
3816
- this.scheduleBackgroundRefresh(key, fetcher, options);
3817
- }
3818
- }
3819
- shouldSkipLayer(layer) {
3820
- return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
3821
- }
3822
- async handleLayerFailure(layer, operation, error) {
3823
- const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
3824
- if (!recovery.degrade) {
3825
- throw error;
3826
- }
3827
- this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
3828
- this.metricsCollector.increment("degradedOperations");
3829
- this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
3830
- this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
3831
- return null;
3832
- }
3833
- async reportRecoverableLayerFailure(layer, operation, error) {
3834
- if (this.isGracefulDegradationEnabled()) {
3835
- await this.handleLayerFailure(layer, operation, error);
3836
- return;
3837
- }
3838
- this.logger.warn?.("layer-operation-failed", { layer: layer.name, operation, error: this.formatError(error) });
3839
- this.emitError(operation, { layer: layer.name, degraded: false, error: this.formatError(error) });
3840
- }
3841
- isGracefulDegradationEnabled() {
3842
- return Boolean(this.options.gracefulDegradation);
3843
- }
3844
- recordCircuitFailure(key, options, error) {
3845
- if (!options) {
3846
- return;
3847
- }
3848
- this.circuitBreakerManager.recordFailure(key, options);
3849
- if (this.circuitBreakerManager.isOpen(key)) {
3850
- this.metricsCollector.increment("circuitBreakerTrips");
3851
- }
3852
- this.emitError("fetch", { key, error: this.formatError(error) });
3853
- }
3854
- isNegativeStoredValue(stored) {
3855
- return isStoredValueEnvelope(stored) && stored.kind === "empty";
3856
- }
3857
- emitError(operation, context) {
3858
- this.logger.error?.(operation, context);
3859
- if (this.listenerCount("error") > 0) {
3860
- this.emit("error", { operation, ...context });
3861
- }
3862
- }
3863
- snapshotMaxBytes() {
3864
- return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
3865
- }
3866
- snapshotMaxEntries() {
3867
- return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
3868
- }
3869
- invalidationMaxKeys() {
3870
- return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
3871
- }
3872
- };
3873
-
3874
- // src/module.ts
3875
- var InjectCacheStack = () => Inject(CACHE_STACK);
3876
- var CacheStackModule = class {
3877
- static forRoot(options) {
3878
- const provider = {
3879
- provide: CACHE_STACK,
3880
- useFactory: () => new CacheStack(options.layers, options.bridgeOptions)
3881
- };
3882
- return {
3883
- global: true,
3884
- module: CacheStackModule,
3885
- providers: [provider],
3886
- exports: [provider]
3887
- };
3888
- }
3889
- static forRootAsync(options) {
3890
- const provider = {
3891
- provide: CACHE_STACK,
3892
- inject: options.inject ?? [],
3893
- useFactory: async (...args) => {
3894
- const resolved = await options.useFactory(...args);
3895
- return new CacheStack(resolved.layers, resolved.bridgeOptions);
3896
- }
3897
- };
3898
- return {
3899
- global: true,
3900
- module: CacheStackModule,
3901
- providers: [provider],
3902
- exports: [provider]
3903
- };
3904
- }
3905
- };
3906
- CacheStackModule = __decorateClass([
3907
- Global(),
3908
- Module({})
3909
- ], CacheStackModule);
3910
- export {
3911
- CACHE_STACK,
3912
- CacheStackModule,
3913
- Cacheable,
3914
- InjectCacheStack
3915
- };