layercache 1.0.1 → 1.0.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.
- package/README.md +286 -7
- package/dist/chunk-IILH5XTS.js +103 -0
- package/dist/cli.cjs +228 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +96 -0
- package/dist/index.cjs +833 -95
- package/dist/index.d.cts +182 -4
- package/dist/index.d.ts +182 -4
- package/dist/index.js +821 -183
- package/package.json +5 -2
- package/packages/nestjs/dist/index.cjs +652 -81
- package/packages/nestjs/dist/index.d.cts +204 -2
- package/packages/nestjs/dist/index.d.ts +204 -2
- package/packages/nestjs/dist/index.js +651 -81
|
@@ -12,11 +12,43 @@ var __decorateClass = (decorators, target, key, kind) => {
|
|
|
12
12
|
// src/constants.ts
|
|
13
13
|
var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
|
|
14
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
|
+
|
|
15
45
|
// src/module.ts
|
|
16
46
|
import { Global, Inject, Module } from "@nestjs/common";
|
|
17
47
|
|
|
18
48
|
// ../../src/CacheStack.ts
|
|
19
49
|
import { randomUUID } from "crypto";
|
|
50
|
+
import { promises as fs } from "fs";
|
|
51
|
+
import { EventEmitter } from "events";
|
|
20
52
|
|
|
21
53
|
// ../../src/internal/StoredValue.ts
|
|
22
54
|
function isStoredValueEnvelope(value) {
|
|
@@ -36,7 +68,10 @@ function createStoredValueEnvelope(options) {
|
|
|
36
68
|
value: options.value,
|
|
37
69
|
freshUntil,
|
|
38
70
|
staleUntil,
|
|
39
|
-
errorUntil
|
|
71
|
+
errorUntil,
|
|
72
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
73
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
74
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
40
75
|
};
|
|
41
76
|
}
|
|
42
77
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -77,6 +112,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
77
112
|
}
|
|
78
113
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
79
114
|
}
|
|
115
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
116
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
const remainingMs = stored.freshUntil - now;
|
|
120
|
+
if (remainingMs <= 0) {
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
124
|
+
}
|
|
125
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
126
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
127
|
+
return stored;
|
|
128
|
+
}
|
|
129
|
+
return createStoredValueEnvelope({
|
|
130
|
+
kind: stored.kind,
|
|
131
|
+
value: stored.value,
|
|
132
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
133
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
134
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
135
|
+
now
|
|
136
|
+
});
|
|
137
|
+
}
|
|
80
138
|
function maxExpiry(stored) {
|
|
81
139
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
82
140
|
(value) => value !== null
|
|
@@ -93,6 +151,61 @@ function normalizePositiveSeconds(value) {
|
|
|
93
151
|
return value;
|
|
94
152
|
}
|
|
95
153
|
|
|
154
|
+
// ../../src/CacheNamespace.ts
|
|
155
|
+
var CacheNamespace = class {
|
|
156
|
+
constructor(cache, prefix) {
|
|
157
|
+
this.cache = cache;
|
|
158
|
+
this.prefix = prefix;
|
|
159
|
+
}
|
|
160
|
+
cache;
|
|
161
|
+
prefix;
|
|
162
|
+
async get(key, fetcher, options) {
|
|
163
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
164
|
+
}
|
|
165
|
+
async set(key, value, options) {
|
|
166
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
167
|
+
}
|
|
168
|
+
async delete(key) {
|
|
169
|
+
await this.cache.delete(this.qualify(key));
|
|
170
|
+
}
|
|
171
|
+
async clear() {
|
|
172
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
173
|
+
}
|
|
174
|
+
async mget(entries) {
|
|
175
|
+
return this.cache.mget(entries.map((entry) => ({
|
|
176
|
+
...entry,
|
|
177
|
+
key: this.qualify(entry.key)
|
|
178
|
+
})));
|
|
179
|
+
}
|
|
180
|
+
async mset(entries) {
|
|
181
|
+
await this.cache.mset(entries.map((entry) => ({
|
|
182
|
+
...entry,
|
|
183
|
+
key: this.qualify(entry.key)
|
|
184
|
+
})));
|
|
185
|
+
}
|
|
186
|
+
async invalidateByTag(tag) {
|
|
187
|
+
await this.cache.invalidateByTag(tag);
|
|
188
|
+
}
|
|
189
|
+
async invalidateByPattern(pattern) {
|
|
190
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
191
|
+
}
|
|
192
|
+
wrap(keyPrefix, fetcher, options) {
|
|
193
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
194
|
+
}
|
|
195
|
+
warm(entries, options) {
|
|
196
|
+
return this.cache.warm(entries.map((entry) => ({
|
|
197
|
+
...entry,
|
|
198
|
+
key: this.qualify(entry.key)
|
|
199
|
+
})), options);
|
|
200
|
+
}
|
|
201
|
+
getMetrics() {
|
|
202
|
+
return this.cache.getMetrics();
|
|
203
|
+
}
|
|
204
|
+
qualify(key) {
|
|
205
|
+
return `${this.prefix}:${key}`;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
96
209
|
// ../../src/invalidation/PatternMatcher.ts
|
|
97
210
|
var PatternMatcher = class {
|
|
98
211
|
static matches(pattern, value) {
|
|
@@ -339,22 +452,24 @@ var Mutex = class {
|
|
|
339
452
|
var StampedeGuard = class {
|
|
340
453
|
mutexes = /* @__PURE__ */ new Map();
|
|
341
454
|
async execute(key, task) {
|
|
342
|
-
const
|
|
455
|
+
const entry = this.getMutexEntry(key);
|
|
343
456
|
try {
|
|
344
|
-
return await mutex.runExclusive(task);
|
|
457
|
+
return await entry.mutex.runExclusive(task);
|
|
345
458
|
} finally {
|
|
346
|
-
|
|
459
|
+
entry.references -= 1;
|
|
460
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
347
461
|
this.mutexes.delete(key);
|
|
348
462
|
}
|
|
349
463
|
}
|
|
350
464
|
}
|
|
351
|
-
|
|
352
|
-
let
|
|
353
|
-
if (!
|
|
354
|
-
|
|
355
|
-
this.mutexes.set(key,
|
|
465
|
+
getMutexEntry(key) {
|
|
466
|
+
let entry = this.mutexes.get(key);
|
|
467
|
+
if (!entry) {
|
|
468
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
469
|
+
this.mutexes.set(key, entry);
|
|
356
470
|
}
|
|
357
|
-
|
|
471
|
+
entry.references += 1;
|
|
472
|
+
return entry;
|
|
358
473
|
}
|
|
359
474
|
};
|
|
360
475
|
|
|
@@ -363,6 +478,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
|
363
478
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
364
479
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
365
480
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
481
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
366
482
|
var EMPTY_METRICS = () => ({
|
|
367
483
|
hits: 0,
|
|
368
484
|
misses: 0,
|
|
@@ -375,7 +491,12 @@ var EMPTY_METRICS = () => ({
|
|
|
375
491
|
refreshes: 0,
|
|
376
492
|
refreshErrors: 0,
|
|
377
493
|
writeFailures: 0,
|
|
378
|
-
singleFlightWaits: 0
|
|
494
|
+
singleFlightWaits: 0,
|
|
495
|
+
negativeCacheHits: 0,
|
|
496
|
+
circuitBreakerTrips: 0,
|
|
497
|
+
degradedOperations: 0,
|
|
498
|
+
hitsByLayer: {},
|
|
499
|
+
missesByLayer: {}
|
|
379
500
|
});
|
|
380
501
|
var DebugLogger = class {
|
|
381
502
|
enabled;
|
|
@@ -383,20 +504,34 @@ var DebugLogger = class {
|
|
|
383
504
|
this.enabled = enabled;
|
|
384
505
|
}
|
|
385
506
|
debug(message, context) {
|
|
507
|
+
this.write("debug", message, context);
|
|
508
|
+
}
|
|
509
|
+
info(message, context) {
|
|
510
|
+
this.write("info", message, context);
|
|
511
|
+
}
|
|
512
|
+
warn(message, context) {
|
|
513
|
+
this.write("warn", message, context);
|
|
514
|
+
}
|
|
515
|
+
error(message, context) {
|
|
516
|
+
this.write("error", message, context);
|
|
517
|
+
}
|
|
518
|
+
write(level, message, context) {
|
|
386
519
|
if (!this.enabled) {
|
|
387
520
|
return;
|
|
388
521
|
}
|
|
389
522
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
390
|
-
console
|
|
523
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
391
524
|
}
|
|
392
525
|
};
|
|
393
|
-
var CacheStack = class {
|
|
526
|
+
var CacheStack = class extends EventEmitter {
|
|
394
527
|
constructor(layers, options = {}) {
|
|
528
|
+
super();
|
|
395
529
|
this.layers = layers;
|
|
396
530
|
this.options = options;
|
|
397
531
|
if (layers.length === 0) {
|
|
398
532
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
399
533
|
}
|
|
534
|
+
this.validateConfiguration();
|
|
400
535
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
401
536
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
402
537
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -412,33 +547,47 @@ var CacheStack = class {
|
|
|
412
547
|
logger;
|
|
413
548
|
tagIndex;
|
|
414
549
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
550
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
551
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
552
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
553
|
+
isDisconnecting = false;
|
|
554
|
+
disconnectPromise;
|
|
415
555
|
async get(key, fetcher, options) {
|
|
556
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
557
|
+
this.validateWriteOptions(options);
|
|
416
558
|
await this.startup;
|
|
417
|
-
const hit = await this.readFromLayers(
|
|
559
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
418
560
|
if (hit.found) {
|
|
561
|
+
this.recordAccess(normalizedKey);
|
|
562
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
563
|
+
this.metrics.negativeCacheHits += 1;
|
|
564
|
+
}
|
|
419
565
|
if (hit.state === "fresh") {
|
|
420
566
|
this.metrics.hits += 1;
|
|
567
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
421
568
|
return hit.value;
|
|
422
569
|
}
|
|
423
570
|
if (hit.state === "stale-while-revalidate") {
|
|
424
571
|
this.metrics.hits += 1;
|
|
425
572
|
this.metrics.staleHits += 1;
|
|
573
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
426
574
|
if (fetcher) {
|
|
427
|
-
this.scheduleBackgroundRefresh(
|
|
575
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
428
576
|
}
|
|
429
577
|
return hit.value;
|
|
430
578
|
}
|
|
431
579
|
if (!fetcher) {
|
|
432
580
|
this.metrics.hits += 1;
|
|
433
581
|
this.metrics.staleHits += 1;
|
|
582
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
434
583
|
return hit.value;
|
|
435
584
|
}
|
|
436
585
|
try {
|
|
437
|
-
return await this.fetchWithGuards(
|
|
586
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
438
587
|
} catch (error) {
|
|
439
588
|
this.metrics.staleHits += 1;
|
|
440
589
|
this.metrics.refreshErrors += 1;
|
|
441
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
590
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
442
591
|
return hit.value;
|
|
443
592
|
}
|
|
444
593
|
}
|
|
@@ -446,71 +595,144 @@ var CacheStack = class {
|
|
|
446
595
|
if (!fetcher) {
|
|
447
596
|
return null;
|
|
448
597
|
}
|
|
449
|
-
return this.fetchWithGuards(
|
|
598
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
450
599
|
}
|
|
451
600
|
async set(key, value, options) {
|
|
601
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
602
|
+
this.validateWriteOptions(options);
|
|
452
603
|
await this.startup;
|
|
453
|
-
await this.storeEntry(
|
|
604
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
454
605
|
}
|
|
455
606
|
async delete(key) {
|
|
607
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
456
608
|
await this.startup;
|
|
457
|
-
await this.deleteKeys([
|
|
458
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
609
|
+
await this.deleteKeys([normalizedKey]);
|
|
610
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
459
611
|
}
|
|
460
612
|
async clear() {
|
|
461
613
|
await this.startup;
|
|
462
614
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
463
615
|
await this.tagIndex.clear();
|
|
616
|
+
this.accessProfiles.clear();
|
|
464
617
|
this.metrics.invalidations += 1;
|
|
465
|
-
this.logger.debug("clear");
|
|
618
|
+
this.logger.debug?.("clear");
|
|
466
619
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
467
620
|
}
|
|
468
621
|
async mget(entries) {
|
|
469
622
|
if (entries.length === 0) {
|
|
470
623
|
return [];
|
|
471
624
|
}
|
|
472
|
-
const
|
|
625
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
626
|
+
...entry,
|
|
627
|
+
key: this.validateCacheKey(entry.key)
|
|
628
|
+
}));
|
|
629
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
630
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
473
631
|
if (!canFastPath) {
|
|
474
|
-
|
|
632
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
633
|
+
return Promise.all(
|
|
634
|
+
normalizedEntries.map((entry) => {
|
|
635
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
636
|
+
const existing = pendingReads.get(entry.key);
|
|
637
|
+
if (!existing) {
|
|
638
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
639
|
+
pendingReads.set(entry.key, {
|
|
640
|
+
promise,
|
|
641
|
+
fetch: entry.fetch,
|
|
642
|
+
optionsSignature
|
|
643
|
+
});
|
|
644
|
+
return promise;
|
|
645
|
+
}
|
|
646
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
647
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
648
|
+
}
|
|
649
|
+
return existing.promise;
|
|
650
|
+
})
|
|
651
|
+
);
|
|
475
652
|
}
|
|
476
653
|
await this.startup;
|
|
477
|
-
const pending = new Set(
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
654
|
+
const pending = /* @__PURE__ */ new Set();
|
|
655
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
656
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
657
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
658
|
+
const key = normalizedEntries[index].key;
|
|
659
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
660
|
+
indexes.push(index);
|
|
661
|
+
indexesByKey.set(key, indexes);
|
|
662
|
+
pending.add(key);
|
|
663
|
+
}
|
|
664
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
665
|
+
const layer = this.layers[layerIndex];
|
|
666
|
+
const keys = [...pending];
|
|
667
|
+
if (keys.length === 0) {
|
|
482
668
|
break;
|
|
483
669
|
}
|
|
484
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
485
670
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
486
671
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
487
|
-
const
|
|
672
|
+
const key = keys[offset];
|
|
488
673
|
const stored = values[offset];
|
|
489
674
|
if (stored === null) {
|
|
490
675
|
continue;
|
|
491
676
|
}
|
|
492
677
|
const resolved = resolveStoredValue(stored);
|
|
493
678
|
if (resolved.state === "expired") {
|
|
494
|
-
await layer.delete(
|
|
679
|
+
await layer.delete(key);
|
|
495
680
|
continue;
|
|
496
681
|
}
|
|
497
|
-
await this.tagIndex.touch(
|
|
498
|
-
await this.backfill(
|
|
499
|
-
|
|
500
|
-
pending.delete(
|
|
501
|
-
this.metrics.hits += 1;
|
|
682
|
+
await this.tagIndex.touch(key);
|
|
683
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
684
|
+
resultsByKey.set(key, resolved.value);
|
|
685
|
+
pending.delete(key);
|
|
686
|
+
this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
|
|
502
687
|
}
|
|
503
688
|
}
|
|
504
689
|
if (pending.size > 0) {
|
|
505
|
-
for (const
|
|
506
|
-
await this.tagIndex.remove(
|
|
507
|
-
this.metrics.misses += 1;
|
|
690
|
+
for (const key of pending) {
|
|
691
|
+
await this.tagIndex.remove(key);
|
|
692
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
508
693
|
}
|
|
509
694
|
}
|
|
510
|
-
return
|
|
695
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
511
696
|
}
|
|
512
697
|
async mset(entries) {
|
|
513
|
-
|
|
698
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
699
|
+
...entry,
|
|
700
|
+
key: this.validateCacheKey(entry.key)
|
|
701
|
+
}));
|
|
702
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
703
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
704
|
+
}
|
|
705
|
+
async warm(entries, options = {}) {
|
|
706
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
707
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
708
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
709
|
+
while (queue.length > 0) {
|
|
710
|
+
const entry = queue.shift();
|
|
711
|
+
if (!entry) {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
716
|
+
this.emit("warm", { key: entry.key });
|
|
717
|
+
} catch (error) {
|
|
718
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
719
|
+
if (!options.continueOnError) {
|
|
720
|
+
throw error;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
});
|
|
725
|
+
await Promise.all(workers);
|
|
726
|
+
}
|
|
727
|
+
wrap(prefix, fetcher, options = {}) {
|
|
728
|
+
return (...args) => {
|
|
729
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
730
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
731
|
+
return this.get(key, () => fetcher(...args), options);
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
namespace(prefix) {
|
|
735
|
+
return new CacheNamespace(this, prefix);
|
|
514
736
|
}
|
|
515
737
|
async invalidateByTag(tag) {
|
|
516
738
|
await this.startup;
|
|
@@ -527,13 +749,74 @@ var CacheStack = class {
|
|
|
527
749
|
getMetrics() {
|
|
528
750
|
return { ...this.metrics };
|
|
529
751
|
}
|
|
752
|
+
getStats() {
|
|
753
|
+
return {
|
|
754
|
+
metrics: this.getMetrics(),
|
|
755
|
+
layers: this.layers.map((layer) => ({
|
|
756
|
+
name: layer.name,
|
|
757
|
+
isLocal: Boolean(layer.isLocal),
|
|
758
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
759
|
+
})),
|
|
760
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
761
|
+
};
|
|
762
|
+
}
|
|
530
763
|
resetMetrics() {
|
|
531
764
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
532
765
|
}
|
|
533
|
-
async
|
|
766
|
+
async exportState() {
|
|
767
|
+
await this.startup;
|
|
768
|
+
const exported = /* @__PURE__ */ new Map();
|
|
769
|
+
for (const layer of this.layers) {
|
|
770
|
+
if (!layer.keys) {
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const keys = await layer.keys();
|
|
774
|
+
for (const key of keys) {
|
|
775
|
+
if (exported.has(key)) {
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
779
|
+
if (stored === null) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
exported.set(key, {
|
|
783
|
+
key,
|
|
784
|
+
value: stored,
|
|
785
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return [...exported.values()];
|
|
790
|
+
}
|
|
791
|
+
async importState(entries) {
|
|
534
792
|
await this.startup;
|
|
535
|
-
await
|
|
536
|
-
|
|
793
|
+
await Promise.all(entries.map(async (entry) => {
|
|
794
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
795
|
+
await this.tagIndex.touch(entry.key);
|
|
796
|
+
}));
|
|
797
|
+
}
|
|
798
|
+
async persistToFile(filePath) {
|
|
799
|
+
const snapshot = await this.exportState();
|
|
800
|
+
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
801
|
+
}
|
|
802
|
+
async restoreFromFile(filePath) {
|
|
803
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
804
|
+
const snapshot = JSON.parse(raw);
|
|
805
|
+
if (!this.isCacheSnapshotEntries(snapshot)) {
|
|
806
|
+
throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
|
|
807
|
+
}
|
|
808
|
+
await this.importState(snapshot);
|
|
809
|
+
}
|
|
810
|
+
async disconnect() {
|
|
811
|
+
if (!this.disconnectPromise) {
|
|
812
|
+
this.isDisconnecting = true;
|
|
813
|
+
this.disconnectPromise = (async () => {
|
|
814
|
+
await this.startup;
|
|
815
|
+
await this.unsubscribeInvalidation?.();
|
|
816
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
817
|
+
})();
|
|
818
|
+
}
|
|
819
|
+
await this.disconnectPromise;
|
|
537
820
|
}
|
|
538
821
|
async initialize() {
|
|
539
822
|
if (!this.options.invalidationBus) {
|
|
@@ -573,6 +856,7 @@ var CacheStack = class {
|
|
|
573
856
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
574
857
|
const deadline = Date.now() + timeoutMs;
|
|
575
858
|
this.metrics.singleFlightWaits += 1;
|
|
859
|
+
this.emit("stampede-dedupe", { key });
|
|
576
860
|
while (Date.now() < deadline) {
|
|
577
861
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
578
862
|
if (hit.found) {
|
|
@@ -584,8 +868,16 @@ var CacheStack = class {
|
|
|
584
868
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
585
869
|
}
|
|
586
870
|
async fetchAndPopulate(key, fetcher, options) {
|
|
871
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
587
872
|
this.metrics.fetches += 1;
|
|
588
|
-
|
|
873
|
+
let fetched;
|
|
874
|
+
try {
|
|
875
|
+
fetched = await fetcher();
|
|
876
|
+
this.resetCircuitBreaker(key);
|
|
877
|
+
} catch (error) {
|
|
878
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
589
881
|
if (fetched === null || fetched === void 0) {
|
|
590
882
|
if (!this.shouldNegativeCache(options)) {
|
|
591
883
|
return null;
|
|
@@ -604,8 +896,9 @@ var CacheStack = class {
|
|
|
604
896
|
await this.tagIndex.touch(key);
|
|
605
897
|
}
|
|
606
898
|
this.metrics.sets += 1;
|
|
607
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
608
|
-
|
|
899
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
900
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
901
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
609
902
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
610
903
|
}
|
|
611
904
|
}
|
|
@@ -615,6 +908,7 @@ var CacheStack = class {
|
|
|
615
908
|
const layer = this.layers[index];
|
|
616
909
|
const stored = await this.readLayerEntry(layer, key);
|
|
617
910
|
if (stored === null) {
|
|
911
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
618
912
|
continue;
|
|
619
913
|
}
|
|
620
914
|
const resolved = resolveStoredValue(stored);
|
|
@@ -628,20 +922,34 @@ var CacheStack = class {
|
|
|
628
922
|
}
|
|
629
923
|
await this.tagIndex.touch(key);
|
|
630
924
|
await this.backfill(key, stored, index - 1, options);
|
|
631
|
-
this.
|
|
632
|
-
|
|
925
|
+
this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
|
|
926
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
927
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
928
|
+
return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
|
|
633
929
|
}
|
|
634
930
|
if (!sawRetainableValue) {
|
|
635
931
|
await this.tagIndex.remove(key);
|
|
636
932
|
}
|
|
637
|
-
this.logger.debug("miss", { key, mode });
|
|
933
|
+
this.logger.debug?.("miss", { key, mode });
|
|
934
|
+
this.emit("miss", { key, mode });
|
|
638
935
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
639
936
|
}
|
|
640
937
|
async readLayerEntry(layer, key) {
|
|
938
|
+
if (this.shouldSkipLayer(layer)) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
641
941
|
if (layer.getEntry) {
|
|
642
|
-
|
|
942
|
+
try {
|
|
943
|
+
return await layer.getEntry(key);
|
|
944
|
+
} catch (error) {
|
|
945
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
try {
|
|
949
|
+
return await layer.get(key);
|
|
950
|
+
} catch (error) {
|
|
951
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
643
952
|
}
|
|
644
|
-
return layer.get(key);
|
|
645
953
|
}
|
|
646
954
|
async backfill(key, stored, upToIndex, options) {
|
|
647
955
|
if (upToIndex < 0) {
|
|
@@ -649,16 +957,28 @@ var CacheStack = class {
|
|
|
649
957
|
}
|
|
650
958
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
651
959
|
const layer = this.layers[index];
|
|
960
|
+
if (this.shouldSkipLayer(layer)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
652
963
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
653
|
-
|
|
964
|
+
try {
|
|
965
|
+
await layer.set(key, stored, ttl);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
654
970
|
this.metrics.backfills += 1;
|
|
655
|
-
this.logger.debug("backfill", { key, layer: layer.name });
|
|
971
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
972
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
656
973
|
}
|
|
657
974
|
}
|
|
658
975
|
async writeAcrossLayers(key, kind, value, options) {
|
|
659
976
|
const now = Date.now();
|
|
660
977
|
const operations = this.layers.map((layer) => async () => {
|
|
661
|
-
|
|
978
|
+
if (this.shouldSkipLayer(layer)) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
662
982
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
663
983
|
layer.name,
|
|
664
984
|
options?.staleWhileRevalidate,
|
|
@@ -678,7 +998,11 @@ var CacheStack = class {
|
|
|
678
998
|
now
|
|
679
999
|
});
|
|
680
1000
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
681
|
-
|
|
1001
|
+
try {
|
|
1002
|
+
await layer.set(key, payload, ttl);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1005
|
+
}
|
|
682
1006
|
});
|
|
683
1007
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
684
1008
|
}
|
|
@@ -693,7 +1017,7 @@ var CacheStack = class {
|
|
|
693
1017
|
return;
|
|
694
1018
|
}
|
|
695
1019
|
this.metrics.writeFailures += failures.length;
|
|
696
|
-
this.logger.debug("write-failure", {
|
|
1020
|
+
this.logger.debug?.("write-failure", {
|
|
697
1021
|
...context,
|
|
698
1022
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
699
1023
|
});
|
|
@@ -704,15 +1028,21 @@ var CacheStack = class {
|
|
|
704
1028
|
);
|
|
705
1029
|
}
|
|
706
1030
|
}
|
|
707
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
1031
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
708
1032
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
709
1033
|
layerName,
|
|
710
1034
|
options?.negativeTtl,
|
|
711
1035
|
this.options.negativeTtl,
|
|
712
1036
|
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
713
1037
|
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
1038
|
+
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
1039
|
+
key,
|
|
1040
|
+
layerName,
|
|
1041
|
+
baseTtl,
|
|
1042
|
+
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
1043
|
+
);
|
|
714
1044
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
715
|
-
return this.applyJitter(
|
|
1045
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
716
1046
|
}
|
|
717
1047
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
718
1048
|
if (override !== void 0) {
|
|
@@ -740,7 +1070,7 @@ var CacheStack = class {
|
|
|
740
1070
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
741
1071
|
}
|
|
742
1072
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
743
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
1073
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
744
1074
|
return;
|
|
745
1075
|
}
|
|
746
1076
|
const refresh = (async () => {
|
|
@@ -749,7 +1079,7 @@ var CacheStack = class {
|
|
|
749
1079
|
await this.fetchWithGuards(key, fetcher, options);
|
|
750
1080
|
} catch (error) {
|
|
751
1081
|
this.metrics.refreshErrors += 1;
|
|
752
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
1082
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
753
1083
|
} finally {
|
|
754
1084
|
this.backgroundRefreshes.delete(key);
|
|
755
1085
|
}
|
|
@@ -767,21 +1097,15 @@ var CacheStack = class {
|
|
|
767
1097
|
if (keys.length === 0) {
|
|
768
1098
|
return;
|
|
769
1099
|
}
|
|
770
|
-
await
|
|
771
|
-
this.layers.map(async (layer) => {
|
|
772
|
-
if (layer.deleteMany) {
|
|
773
|
-
await layer.deleteMany(keys);
|
|
774
|
-
return;
|
|
775
|
-
}
|
|
776
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
777
|
-
})
|
|
778
|
-
);
|
|
1100
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
779
1101
|
for (const key of keys) {
|
|
780
1102
|
await this.tagIndex.remove(key);
|
|
1103
|
+
this.accessProfiles.delete(key);
|
|
781
1104
|
}
|
|
782
1105
|
this.metrics.deletes += keys.length;
|
|
783
1106
|
this.metrics.invalidations += 1;
|
|
784
|
-
this.logger.debug("delete", { keys });
|
|
1107
|
+
this.logger.debug?.("delete", { keys });
|
|
1108
|
+
this.emit("delete", { keys });
|
|
785
1109
|
}
|
|
786
1110
|
async publishInvalidation(message) {
|
|
787
1111
|
if (!this.options.invalidationBus) {
|
|
@@ -800,21 +1124,15 @@ var CacheStack = class {
|
|
|
800
1124
|
if (message.scope === "clear") {
|
|
801
1125
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
802
1126
|
await this.tagIndex.clear();
|
|
1127
|
+
this.accessProfiles.clear();
|
|
803
1128
|
return;
|
|
804
1129
|
}
|
|
805
1130
|
const keys = message.keys ?? [];
|
|
806
|
-
await
|
|
807
|
-
localLayers.map(async (layer) => {
|
|
808
|
-
if (layer.deleteMany) {
|
|
809
|
-
await layer.deleteMany(keys);
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
813
|
-
})
|
|
814
|
-
);
|
|
1131
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
815
1132
|
if (message.operation !== "write") {
|
|
816
1133
|
for (const key of keys) {
|
|
817
1134
|
await this.tagIndex.remove(key);
|
|
1135
|
+
this.accessProfiles.delete(key);
|
|
818
1136
|
}
|
|
819
1137
|
}
|
|
820
1138
|
}
|
|
@@ -827,6 +1145,257 @@ var CacheStack = class {
|
|
|
827
1145
|
sleep(ms) {
|
|
828
1146
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
829
1147
|
}
|
|
1148
|
+
shouldBroadcastL1Invalidation() {
|
|
1149
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1150
|
+
}
|
|
1151
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1152
|
+
await Promise.all(
|
|
1153
|
+
layers.map(async (layer) => {
|
|
1154
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (layer.deleteMany) {
|
|
1158
|
+
try {
|
|
1159
|
+
await layer.deleteMany(keys);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
await Promise.all(keys.map(async (key) => {
|
|
1166
|
+
try {
|
|
1167
|
+
await layer.delete(key);
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1170
|
+
}
|
|
1171
|
+
}));
|
|
1172
|
+
})
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
validateConfiguration() {
|
|
1176
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1177
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1178
|
+
}
|
|
1179
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1180
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1181
|
+
}
|
|
1182
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1183
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1184
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1185
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1186
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1187
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1188
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1189
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1190
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1191
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1192
|
+
}
|
|
1193
|
+
validateWriteOptions(options) {
|
|
1194
|
+
if (!options) {
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1198
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1199
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1200
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1201
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1202
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1203
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1204
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1205
|
+
}
|
|
1206
|
+
validateLayerNumberOption(name, value) {
|
|
1207
|
+
if (value === void 0) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof value === "number") {
|
|
1211
|
+
this.validateNonNegativeNumber(name, value);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1215
|
+
if (layerValue === void 0) {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
validatePositiveNumber(name, value) {
|
|
1222
|
+
if (value === void 0) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1226
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
validateNonNegativeNumber(name, value) {
|
|
1230
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1231
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
validateCacheKey(key) {
|
|
1235
|
+
if (key.length === 0) {
|
|
1236
|
+
throw new Error("Cache key must not be empty.");
|
|
1237
|
+
}
|
|
1238
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1239
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1240
|
+
}
|
|
1241
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1242
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1243
|
+
}
|
|
1244
|
+
return key;
|
|
1245
|
+
}
|
|
1246
|
+
serializeOptions(options) {
|
|
1247
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1248
|
+
}
|
|
1249
|
+
validateAdaptiveTtlOptions(options) {
|
|
1250
|
+
if (!options || options === true) {
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1254
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1255
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1256
|
+
}
|
|
1257
|
+
validateCircuitBreakerOptions(options) {
|
|
1258
|
+
if (!options) {
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1262
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1263
|
+
}
|
|
1264
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1265
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1266
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1267
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1268
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1269
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1270
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1271
|
+
const layer = this.layers[index];
|
|
1272
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
try {
|
|
1276
|
+
await layer.set(key, refreshed, ttl);
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1283
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1287
|
+
if (!ttl || !adaptiveTtl) {
|
|
1288
|
+
return ttl;
|
|
1289
|
+
}
|
|
1290
|
+
const profile = this.accessProfiles.get(key);
|
|
1291
|
+
if (!profile) {
|
|
1292
|
+
return ttl;
|
|
1293
|
+
}
|
|
1294
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1295
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
1296
|
+
if (profile.hits < hotAfter) {
|
|
1297
|
+
return ttl;
|
|
1298
|
+
}
|
|
1299
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1300
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1301
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1302
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1303
|
+
}
|
|
1304
|
+
recordAccess(key) {
|
|
1305
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1306
|
+
profile.hits += 1;
|
|
1307
|
+
profile.lastAccessAt = Date.now();
|
|
1308
|
+
this.accessProfiles.set(key, profile);
|
|
1309
|
+
}
|
|
1310
|
+
incrementMetricMap(target, key) {
|
|
1311
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
1312
|
+
}
|
|
1313
|
+
shouldSkipLayer(layer) {
|
|
1314
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1315
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1316
|
+
}
|
|
1317
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1318
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1319
|
+
throw error;
|
|
1320
|
+
}
|
|
1321
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1322
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1323
|
+
this.metrics.degradedOperations += 1;
|
|
1324
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1325
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
isGracefulDegradationEnabled() {
|
|
1329
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1330
|
+
}
|
|
1331
|
+
assertCircuitClosed(key, options) {
|
|
1332
|
+
const state = this.circuitBreakers.get(key);
|
|
1333
|
+
if (!state?.openUntil) {
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
if (state.openUntil <= Date.now()) {
|
|
1337
|
+
state.openUntil = null;
|
|
1338
|
+
state.failures = 0;
|
|
1339
|
+
this.circuitBreakers.set(key, state);
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1343
|
+
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1344
|
+
}
|
|
1345
|
+
recordCircuitFailure(key, options, error) {
|
|
1346
|
+
if (!options) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
1350
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1351
|
+
const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1352
|
+
state.failures += 1;
|
|
1353
|
+
if (state.failures >= failureThreshold) {
|
|
1354
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
1355
|
+
this.metrics.circuitBreakerTrips += 1;
|
|
1356
|
+
}
|
|
1357
|
+
this.circuitBreakers.set(key, state);
|
|
1358
|
+
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1359
|
+
}
|
|
1360
|
+
resetCircuitBreaker(key) {
|
|
1361
|
+
this.circuitBreakers.delete(key);
|
|
1362
|
+
}
|
|
1363
|
+
isNegativeStoredValue(stored) {
|
|
1364
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1365
|
+
}
|
|
1366
|
+
emitError(operation, context) {
|
|
1367
|
+
this.logger.error?.(operation, context);
|
|
1368
|
+
if (this.listenerCount("error") > 0) {
|
|
1369
|
+
this.emit("error", { operation, ...context });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
serializeKeyPart(value) {
|
|
1373
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1374
|
+
return String(value);
|
|
1375
|
+
}
|
|
1376
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1377
|
+
}
|
|
1378
|
+
isCacheSnapshotEntries(value) {
|
|
1379
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1380
|
+
if (!entry || typeof entry !== "object") {
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
const candidate = entry;
|
|
1384
|
+
return typeof candidate.key === "string";
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
normalizeForSerialization(value) {
|
|
1388
|
+
if (Array.isArray(value)) {
|
|
1389
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1390
|
+
}
|
|
1391
|
+
if (value && typeof value === "object") {
|
|
1392
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1393
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1394
|
+
return normalized;
|
|
1395
|
+
}, {});
|
|
1396
|
+
}
|
|
1397
|
+
return value;
|
|
1398
|
+
}
|
|
830
1399
|
};
|
|
831
1400
|
|
|
832
1401
|
// src/module.ts
|
|
@@ -852,5 +1421,6 @@ CacheStackModule = __decorateClass([
|
|
|
852
1421
|
export {
|
|
853
1422
|
CACHE_STACK,
|
|
854
1423
|
CacheStackModule,
|
|
1424
|
+
Cacheable,
|
|
855
1425
|
InjectCacheStack
|
|
856
1426
|
};
|