layercache 1.0.0 → 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 +371 -13
- 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 +1214 -98
- package/dist/index.d.cts +245 -7
- package/dist/index.d.ts +245 -7
- package/dist/index.js +1200 -185
- package/package.json +9 -2
- package/packages/nestjs/dist/index.cjs +971 -89
- package/packages/nestjs/dist/index.d.cts +227 -2
- package/packages/nestjs/dist/index.d.ts +227 -2
- package/packages/nestjs/dist/index.js +970 -89
|
@@ -30,6 +30,7 @@ var index_exports = {};
|
|
|
30
30
|
__export(index_exports, {
|
|
31
31
|
CACHE_STACK: () => CACHE_STACK,
|
|
32
32
|
CacheStackModule: () => CacheStackModule,
|
|
33
|
+
Cacheable: () => Cacheable,
|
|
33
34
|
InjectCacheStack: () => InjectCacheStack
|
|
34
35
|
});
|
|
35
36
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -37,11 +38,199 @@ module.exports = __toCommonJS(index_exports);
|
|
|
37
38
|
// src/constants.ts
|
|
38
39
|
var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
|
|
39
40
|
|
|
41
|
+
// ../../src/decorators/createCachedMethodDecorator.ts
|
|
42
|
+
function createCachedMethodDecorator(options) {
|
|
43
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
44
|
+
return ((_, propertyKey, descriptor) => {
|
|
45
|
+
const original = descriptor.value;
|
|
46
|
+
if (typeof original !== "function") {
|
|
47
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
48
|
+
}
|
|
49
|
+
descriptor.value = async function(...args) {
|
|
50
|
+
const instance = this;
|
|
51
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
52
|
+
if (!wrapped) {
|
|
53
|
+
const cache = options.cache(instance);
|
|
54
|
+
wrapped = cache.wrap(
|
|
55
|
+
options.prefix ?? String(propertyKey),
|
|
56
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
57
|
+
options
|
|
58
|
+
);
|
|
59
|
+
wrappedByInstance.set(instance, wrapped);
|
|
60
|
+
}
|
|
61
|
+
return wrapped(...args);
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/decorators.ts
|
|
67
|
+
function Cacheable(options) {
|
|
68
|
+
return createCachedMethodDecorator(options);
|
|
69
|
+
}
|
|
70
|
+
|
|
40
71
|
// src/module.ts
|
|
41
72
|
var import_common = require("@nestjs/common");
|
|
42
73
|
|
|
43
74
|
// ../../src/CacheStack.ts
|
|
44
75
|
var import_node_crypto = require("crypto");
|
|
76
|
+
var import_node_fs = require("fs");
|
|
77
|
+
var import_node_events = require("events");
|
|
78
|
+
|
|
79
|
+
// ../../src/internal/StoredValue.ts
|
|
80
|
+
function isStoredValueEnvelope(value) {
|
|
81
|
+
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
82
|
+
}
|
|
83
|
+
function createStoredValueEnvelope(options) {
|
|
84
|
+
const now = options.now ?? Date.now();
|
|
85
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
86
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
87
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
88
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
89
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
90
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
91
|
+
return {
|
|
92
|
+
__layercache: 1,
|
|
93
|
+
kind: options.kind,
|
|
94
|
+
value: options.value,
|
|
95
|
+
freshUntil,
|
|
96
|
+
staleUntil,
|
|
97
|
+
errorUntil,
|
|
98
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
99
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
100
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
104
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
105
|
+
return { state: "fresh", value: stored, stored };
|
|
106
|
+
}
|
|
107
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
108
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
109
|
+
}
|
|
110
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
111
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
112
|
+
}
|
|
113
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
114
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
115
|
+
}
|
|
116
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
117
|
+
}
|
|
118
|
+
function unwrapStoredValue(stored) {
|
|
119
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
120
|
+
return stored;
|
|
121
|
+
}
|
|
122
|
+
if (stored.kind === "empty") {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return stored.value ?? null;
|
|
126
|
+
}
|
|
127
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
128
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
const expiry = maxExpiry(stored);
|
|
132
|
+
if (expiry === null) {
|
|
133
|
+
return void 0;
|
|
134
|
+
}
|
|
135
|
+
const remainingMs = expiry - now;
|
|
136
|
+
if (remainingMs <= 0) {
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
140
|
+
}
|
|
141
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
142
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
143
|
+
return void 0;
|
|
144
|
+
}
|
|
145
|
+
const remainingMs = stored.freshUntil - now;
|
|
146
|
+
if (remainingMs <= 0) {
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
149
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
150
|
+
}
|
|
151
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
152
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
153
|
+
return stored;
|
|
154
|
+
}
|
|
155
|
+
return createStoredValueEnvelope({
|
|
156
|
+
kind: stored.kind,
|
|
157
|
+
value: stored.value,
|
|
158
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
159
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
160
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
161
|
+
now
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
function maxExpiry(stored) {
|
|
165
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
166
|
+
(value) => value !== null
|
|
167
|
+
);
|
|
168
|
+
if (values.length === 0) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
return Math.max(...values);
|
|
172
|
+
}
|
|
173
|
+
function normalizePositiveSeconds(value) {
|
|
174
|
+
if (!value || value <= 0) {
|
|
175
|
+
return void 0;
|
|
176
|
+
}
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ../../src/CacheNamespace.ts
|
|
181
|
+
var CacheNamespace = class {
|
|
182
|
+
constructor(cache, prefix) {
|
|
183
|
+
this.cache = cache;
|
|
184
|
+
this.prefix = prefix;
|
|
185
|
+
}
|
|
186
|
+
cache;
|
|
187
|
+
prefix;
|
|
188
|
+
async get(key, fetcher, options) {
|
|
189
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
190
|
+
}
|
|
191
|
+
async set(key, value, options) {
|
|
192
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
193
|
+
}
|
|
194
|
+
async delete(key) {
|
|
195
|
+
await this.cache.delete(this.qualify(key));
|
|
196
|
+
}
|
|
197
|
+
async clear() {
|
|
198
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
199
|
+
}
|
|
200
|
+
async mget(entries) {
|
|
201
|
+
return this.cache.mget(entries.map((entry) => ({
|
|
202
|
+
...entry,
|
|
203
|
+
key: this.qualify(entry.key)
|
|
204
|
+
})));
|
|
205
|
+
}
|
|
206
|
+
async mset(entries) {
|
|
207
|
+
await this.cache.mset(entries.map((entry) => ({
|
|
208
|
+
...entry,
|
|
209
|
+
key: this.qualify(entry.key)
|
|
210
|
+
})));
|
|
211
|
+
}
|
|
212
|
+
async invalidateByTag(tag) {
|
|
213
|
+
await this.cache.invalidateByTag(tag);
|
|
214
|
+
}
|
|
215
|
+
async invalidateByPattern(pattern) {
|
|
216
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
217
|
+
}
|
|
218
|
+
wrap(keyPrefix, fetcher, options) {
|
|
219
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
220
|
+
}
|
|
221
|
+
warm(entries, options) {
|
|
222
|
+
return this.cache.warm(entries.map((entry) => ({
|
|
223
|
+
...entry,
|
|
224
|
+
key: this.qualify(entry.key)
|
|
225
|
+
})), options);
|
|
226
|
+
}
|
|
227
|
+
getMetrics() {
|
|
228
|
+
return this.cache.getMetrics();
|
|
229
|
+
}
|
|
230
|
+
qualify(key) {
|
|
231
|
+
return `${this.prefix}:${key}`;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
45
234
|
|
|
46
235
|
// ../../src/invalidation/PatternMatcher.ts
|
|
47
236
|
var PatternMatcher = class {
|
|
@@ -289,26 +478,33 @@ var Mutex = class {
|
|
|
289
478
|
var StampedeGuard = class {
|
|
290
479
|
mutexes = /* @__PURE__ */ new Map();
|
|
291
480
|
async execute(key, task) {
|
|
292
|
-
const
|
|
481
|
+
const entry = this.getMutexEntry(key);
|
|
293
482
|
try {
|
|
294
|
-
return await mutex.runExclusive(task);
|
|
483
|
+
return await entry.mutex.runExclusive(task);
|
|
295
484
|
} finally {
|
|
296
|
-
|
|
485
|
+
entry.references -= 1;
|
|
486
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
297
487
|
this.mutexes.delete(key);
|
|
298
488
|
}
|
|
299
489
|
}
|
|
300
490
|
}
|
|
301
|
-
|
|
302
|
-
let
|
|
303
|
-
if (!
|
|
304
|
-
|
|
305
|
-
this.mutexes.set(key,
|
|
491
|
+
getMutexEntry(key) {
|
|
492
|
+
let entry = this.mutexes.get(key);
|
|
493
|
+
if (!entry) {
|
|
494
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
495
|
+
this.mutexes.set(key, entry);
|
|
306
496
|
}
|
|
307
|
-
|
|
497
|
+
entry.references += 1;
|
|
498
|
+
return entry;
|
|
308
499
|
}
|
|
309
500
|
};
|
|
310
501
|
|
|
311
502
|
// ../../src/CacheStack.ts
|
|
503
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
504
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
505
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
506
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
507
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
312
508
|
var EMPTY_METRICS = () => ({
|
|
313
509
|
hits: 0,
|
|
314
510
|
misses: 0,
|
|
@@ -316,7 +512,17 @@ var EMPTY_METRICS = () => ({
|
|
|
316
512
|
sets: 0,
|
|
317
513
|
deletes: 0,
|
|
318
514
|
backfills: 0,
|
|
319
|
-
invalidations: 0
|
|
515
|
+
invalidations: 0,
|
|
516
|
+
staleHits: 0,
|
|
517
|
+
refreshes: 0,
|
|
518
|
+
refreshErrors: 0,
|
|
519
|
+
writeFailures: 0,
|
|
520
|
+
singleFlightWaits: 0,
|
|
521
|
+
negativeCacheHits: 0,
|
|
522
|
+
circuitBreakerTrips: 0,
|
|
523
|
+
degradedOperations: 0,
|
|
524
|
+
hitsByLayer: {},
|
|
525
|
+
missesByLayer: {}
|
|
320
526
|
});
|
|
321
527
|
var DebugLogger = class {
|
|
322
528
|
enabled;
|
|
@@ -324,21 +530,35 @@ var DebugLogger = class {
|
|
|
324
530
|
this.enabled = enabled;
|
|
325
531
|
}
|
|
326
532
|
debug(message, context) {
|
|
533
|
+
this.write("debug", message, context);
|
|
534
|
+
}
|
|
535
|
+
info(message, context) {
|
|
536
|
+
this.write("info", message, context);
|
|
537
|
+
}
|
|
538
|
+
warn(message, context) {
|
|
539
|
+
this.write("warn", message, context);
|
|
540
|
+
}
|
|
541
|
+
error(message, context) {
|
|
542
|
+
this.write("error", message, context);
|
|
543
|
+
}
|
|
544
|
+
write(level, message, context) {
|
|
327
545
|
if (!this.enabled) {
|
|
328
546
|
return;
|
|
329
547
|
}
|
|
330
548
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
331
|
-
console
|
|
549
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
332
550
|
}
|
|
333
551
|
};
|
|
334
|
-
var CacheStack = class {
|
|
552
|
+
var CacheStack = class extends import_node_events.EventEmitter {
|
|
335
553
|
constructor(layers, options = {}) {
|
|
554
|
+
super();
|
|
336
555
|
this.layers = layers;
|
|
337
556
|
this.options = options;
|
|
338
557
|
if (layers.length === 0) {
|
|
339
558
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
340
559
|
}
|
|
341
|
-
|
|
560
|
+
this.validateConfiguration();
|
|
561
|
+
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
342
562
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
343
563
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
344
564
|
this.startup = this.initialize();
|
|
@@ -352,68 +572,193 @@ var CacheStack = class {
|
|
|
352
572
|
unsubscribeInvalidation;
|
|
353
573
|
logger;
|
|
354
574
|
tagIndex;
|
|
575
|
+
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
576
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
577
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
578
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
579
|
+
isDisconnecting = false;
|
|
580
|
+
disconnectPromise;
|
|
355
581
|
async get(key, fetcher, options) {
|
|
582
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
583
|
+
this.validateWriteOptions(options);
|
|
356
584
|
await this.startup;
|
|
357
|
-
const hit = await this.
|
|
585
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
358
586
|
if (hit.found) {
|
|
359
|
-
this.
|
|
360
|
-
|
|
587
|
+
this.recordAccess(normalizedKey);
|
|
588
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
589
|
+
this.metrics.negativeCacheHits += 1;
|
|
590
|
+
}
|
|
591
|
+
if (hit.state === "fresh") {
|
|
592
|
+
this.metrics.hits += 1;
|
|
593
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
594
|
+
return hit.value;
|
|
595
|
+
}
|
|
596
|
+
if (hit.state === "stale-while-revalidate") {
|
|
597
|
+
this.metrics.hits += 1;
|
|
598
|
+
this.metrics.staleHits += 1;
|
|
599
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
600
|
+
if (fetcher) {
|
|
601
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
602
|
+
}
|
|
603
|
+
return hit.value;
|
|
604
|
+
}
|
|
605
|
+
if (!fetcher) {
|
|
606
|
+
this.metrics.hits += 1;
|
|
607
|
+
this.metrics.staleHits += 1;
|
|
608
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
609
|
+
return hit.value;
|
|
610
|
+
}
|
|
611
|
+
try {
|
|
612
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
this.metrics.staleHits += 1;
|
|
615
|
+
this.metrics.refreshErrors += 1;
|
|
616
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
617
|
+
return hit.value;
|
|
618
|
+
}
|
|
361
619
|
}
|
|
362
620
|
this.metrics.misses += 1;
|
|
363
621
|
if (!fetcher) {
|
|
364
622
|
return null;
|
|
365
623
|
}
|
|
366
|
-
|
|
367
|
-
const secondHit = await this.getFromLayers(key, options);
|
|
368
|
-
if (secondHit.found) {
|
|
369
|
-
this.metrics.hits += 1;
|
|
370
|
-
return secondHit.value;
|
|
371
|
-
}
|
|
372
|
-
this.metrics.fetches += 1;
|
|
373
|
-
const fetched = await fetcher();
|
|
374
|
-
if (fetched === null || fetched === void 0) {
|
|
375
|
-
return null;
|
|
376
|
-
}
|
|
377
|
-
await this.set(key, fetched, options);
|
|
378
|
-
return fetched;
|
|
379
|
-
};
|
|
380
|
-
if (this.options.stampedePrevention === false) {
|
|
381
|
-
return runFetch();
|
|
382
|
-
}
|
|
383
|
-
return this.stampedeGuard.execute(key, runFetch);
|
|
624
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
384
625
|
}
|
|
385
626
|
async set(key, value, options) {
|
|
627
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
628
|
+
this.validateWriteOptions(options);
|
|
386
629
|
await this.startup;
|
|
387
|
-
await this.
|
|
388
|
-
if (options?.tags) {
|
|
389
|
-
await this.tagIndex.track(key, options.tags);
|
|
390
|
-
} else {
|
|
391
|
-
await this.tagIndex.touch(key);
|
|
392
|
-
}
|
|
393
|
-
this.metrics.sets += 1;
|
|
394
|
-
this.logger.debug("set", { key, tags: options?.tags });
|
|
395
|
-
if (this.options.publishSetInvalidation !== false) {
|
|
396
|
-
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
397
|
-
}
|
|
630
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
398
631
|
}
|
|
399
632
|
async delete(key) {
|
|
633
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
400
634
|
await this.startup;
|
|
401
|
-
await this.deleteKeys([
|
|
402
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
635
|
+
await this.deleteKeys([normalizedKey]);
|
|
636
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
403
637
|
}
|
|
404
638
|
async clear() {
|
|
405
639
|
await this.startup;
|
|
406
640
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
407
641
|
await this.tagIndex.clear();
|
|
642
|
+
this.accessProfiles.clear();
|
|
408
643
|
this.metrics.invalidations += 1;
|
|
409
|
-
this.logger.debug("clear");
|
|
644
|
+
this.logger.debug?.("clear");
|
|
410
645
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
411
646
|
}
|
|
412
647
|
async mget(entries) {
|
|
413
|
-
|
|
648
|
+
if (entries.length === 0) {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
652
|
+
...entry,
|
|
653
|
+
key: this.validateCacheKey(entry.key)
|
|
654
|
+
}));
|
|
655
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
656
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
657
|
+
if (!canFastPath) {
|
|
658
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
659
|
+
return Promise.all(
|
|
660
|
+
normalizedEntries.map((entry) => {
|
|
661
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
662
|
+
const existing = pendingReads.get(entry.key);
|
|
663
|
+
if (!existing) {
|
|
664
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
665
|
+
pendingReads.set(entry.key, {
|
|
666
|
+
promise,
|
|
667
|
+
fetch: entry.fetch,
|
|
668
|
+
optionsSignature
|
|
669
|
+
});
|
|
670
|
+
return promise;
|
|
671
|
+
}
|
|
672
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
673
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
674
|
+
}
|
|
675
|
+
return existing.promise;
|
|
676
|
+
})
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
await this.startup;
|
|
680
|
+
const pending = /* @__PURE__ */ new Set();
|
|
681
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
682
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
683
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
684
|
+
const key = normalizedEntries[index].key;
|
|
685
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
686
|
+
indexes.push(index);
|
|
687
|
+
indexesByKey.set(key, indexes);
|
|
688
|
+
pending.add(key);
|
|
689
|
+
}
|
|
690
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
691
|
+
const layer = this.layers[layerIndex];
|
|
692
|
+
const keys = [...pending];
|
|
693
|
+
if (keys.length === 0) {
|
|
694
|
+
break;
|
|
695
|
+
}
|
|
696
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
697
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
698
|
+
const key = keys[offset];
|
|
699
|
+
const stored = values[offset];
|
|
700
|
+
if (stored === null) {
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
const resolved = resolveStoredValue(stored);
|
|
704
|
+
if (resolved.state === "expired") {
|
|
705
|
+
await layer.delete(key);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
await this.tagIndex.touch(key);
|
|
709
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
710
|
+
resultsByKey.set(key, resolved.value);
|
|
711
|
+
pending.delete(key);
|
|
712
|
+
this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (pending.size > 0) {
|
|
716
|
+
for (const key of pending) {
|
|
717
|
+
await this.tagIndex.remove(key);
|
|
718
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
414
722
|
}
|
|
415
723
|
async mset(entries) {
|
|
416
|
-
|
|
724
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
725
|
+
...entry,
|
|
726
|
+
key: this.validateCacheKey(entry.key)
|
|
727
|
+
}));
|
|
728
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
729
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
730
|
+
}
|
|
731
|
+
async warm(entries, options = {}) {
|
|
732
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
733
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
734
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
735
|
+
while (queue.length > 0) {
|
|
736
|
+
const entry = queue.shift();
|
|
737
|
+
if (!entry) {
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
try {
|
|
741
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
742
|
+
this.emit("warm", { key: entry.key });
|
|
743
|
+
} catch (error) {
|
|
744
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
745
|
+
if (!options.continueOnError) {
|
|
746
|
+
throw error;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
await Promise.all(workers);
|
|
752
|
+
}
|
|
753
|
+
wrap(prefix, fetcher, options = {}) {
|
|
754
|
+
return (...args) => {
|
|
755
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
756
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
757
|
+
return this.get(key, () => fetcher(...args), options);
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
namespace(prefix) {
|
|
761
|
+
return new CacheNamespace(this, prefix);
|
|
417
762
|
}
|
|
418
763
|
async invalidateByTag(tag) {
|
|
419
764
|
await this.startup;
|
|
@@ -430,12 +775,74 @@ var CacheStack = class {
|
|
|
430
775
|
getMetrics() {
|
|
431
776
|
return { ...this.metrics };
|
|
432
777
|
}
|
|
778
|
+
getStats() {
|
|
779
|
+
return {
|
|
780
|
+
metrics: this.getMetrics(),
|
|
781
|
+
layers: this.layers.map((layer) => ({
|
|
782
|
+
name: layer.name,
|
|
783
|
+
isLocal: Boolean(layer.isLocal),
|
|
784
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
785
|
+
})),
|
|
786
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
787
|
+
};
|
|
788
|
+
}
|
|
433
789
|
resetMetrics() {
|
|
434
790
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
435
791
|
}
|
|
436
|
-
async
|
|
792
|
+
async exportState() {
|
|
793
|
+
await this.startup;
|
|
794
|
+
const exported = /* @__PURE__ */ new Map();
|
|
795
|
+
for (const layer of this.layers) {
|
|
796
|
+
if (!layer.keys) {
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
799
|
+
const keys = await layer.keys();
|
|
800
|
+
for (const key of keys) {
|
|
801
|
+
if (exported.has(key)) {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
805
|
+
if (stored === null) {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
exported.set(key, {
|
|
809
|
+
key,
|
|
810
|
+
value: stored,
|
|
811
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return [...exported.values()];
|
|
816
|
+
}
|
|
817
|
+
async importState(entries) {
|
|
437
818
|
await this.startup;
|
|
438
|
-
await
|
|
819
|
+
await Promise.all(entries.map(async (entry) => {
|
|
820
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
821
|
+
await this.tagIndex.touch(entry.key);
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
async persistToFile(filePath) {
|
|
825
|
+
const snapshot = await this.exportState();
|
|
826
|
+
await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
827
|
+
}
|
|
828
|
+
async restoreFromFile(filePath) {
|
|
829
|
+
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
830
|
+
const snapshot = JSON.parse(raw);
|
|
831
|
+
if (!this.isCacheSnapshotEntries(snapshot)) {
|
|
832
|
+
throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
|
|
833
|
+
}
|
|
834
|
+
await this.importState(snapshot);
|
|
835
|
+
}
|
|
836
|
+
async disconnect() {
|
|
837
|
+
if (!this.disconnectPromise) {
|
|
838
|
+
this.isDisconnecting = true;
|
|
839
|
+
this.disconnectPromise = (async () => {
|
|
840
|
+
await this.startup;
|
|
841
|
+
await this.unsubscribeInvalidation?.();
|
|
842
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
843
|
+
})();
|
|
844
|
+
}
|
|
845
|
+
await this.disconnectPromise;
|
|
439
846
|
}
|
|
440
847
|
async initialize() {
|
|
441
848
|
if (!this.options.invalidationBus) {
|
|
@@ -445,66 +852,286 @@ var CacheStack = class {
|
|
|
445
852
|
await this.handleInvalidationMessage(message);
|
|
446
853
|
});
|
|
447
854
|
}
|
|
448
|
-
async
|
|
855
|
+
async fetchWithGuards(key, fetcher, options) {
|
|
856
|
+
const fetchTask = async () => {
|
|
857
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
858
|
+
if (secondHit.found) {
|
|
859
|
+
this.metrics.hits += 1;
|
|
860
|
+
return secondHit.value;
|
|
861
|
+
}
|
|
862
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
863
|
+
};
|
|
864
|
+
const singleFlightTask = async () => {
|
|
865
|
+
if (!this.options.singleFlightCoordinator) {
|
|
866
|
+
return fetchTask();
|
|
867
|
+
}
|
|
868
|
+
return this.options.singleFlightCoordinator.execute(
|
|
869
|
+
key,
|
|
870
|
+
this.resolveSingleFlightOptions(),
|
|
871
|
+
fetchTask,
|
|
872
|
+
() => this.waitForFreshValue(key, fetcher, options)
|
|
873
|
+
);
|
|
874
|
+
};
|
|
875
|
+
if (this.options.stampedePrevention === false) {
|
|
876
|
+
return singleFlightTask();
|
|
877
|
+
}
|
|
878
|
+
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
879
|
+
}
|
|
880
|
+
async waitForFreshValue(key, fetcher, options) {
|
|
881
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
882
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
883
|
+
const deadline = Date.now() + timeoutMs;
|
|
884
|
+
this.metrics.singleFlightWaits += 1;
|
|
885
|
+
this.emit("stampede-dedupe", { key });
|
|
886
|
+
while (Date.now() < deadline) {
|
|
887
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
888
|
+
if (hit.found) {
|
|
889
|
+
this.metrics.hits += 1;
|
|
890
|
+
return hit.value;
|
|
891
|
+
}
|
|
892
|
+
await this.sleep(pollIntervalMs);
|
|
893
|
+
}
|
|
894
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
895
|
+
}
|
|
896
|
+
async fetchAndPopulate(key, fetcher, options) {
|
|
897
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
898
|
+
this.metrics.fetches += 1;
|
|
899
|
+
let fetched;
|
|
900
|
+
try {
|
|
901
|
+
fetched = await fetcher();
|
|
902
|
+
this.resetCircuitBreaker(key);
|
|
903
|
+
} catch (error) {
|
|
904
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
905
|
+
throw error;
|
|
906
|
+
}
|
|
907
|
+
if (fetched === null || fetched === void 0) {
|
|
908
|
+
if (!this.shouldNegativeCache(options)) {
|
|
909
|
+
return null;
|
|
910
|
+
}
|
|
911
|
+
await this.storeEntry(key, "empty", null, options);
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
await this.storeEntry(key, "value", fetched, options);
|
|
915
|
+
return fetched;
|
|
916
|
+
}
|
|
917
|
+
async storeEntry(key, kind, value, options) {
|
|
918
|
+
await this.writeAcrossLayers(key, kind, value, options);
|
|
919
|
+
if (options?.tags) {
|
|
920
|
+
await this.tagIndex.track(key, options.tags);
|
|
921
|
+
} else {
|
|
922
|
+
await this.tagIndex.touch(key);
|
|
923
|
+
}
|
|
924
|
+
this.metrics.sets += 1;
|
|
925
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
926
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
927
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
928
|
+
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
async readFromLayers(key, options, mode) {
|
|
932
|
+
let sawRetainableValue = false;
|
|
449
933
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
450
934
|
const layer = this.layers[index];
|
|
451
|
-
const
|
|
452
|
-
if (
|
|
935
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
936
|
+
if (stored === null) {
|
|
937
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
const resolved = resolveStoredValue(stored);
|
|
941
|
+
if (resolved.state === "expired") {
|
|
942
|
+
await layer.delete(key);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
sawRetainableValue = true;
|
|
946
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
453
947
|
continue;
|
|
454
948
|
}
|
|
455
949
|
await this.tagIndex.touch(key);
|
|
456
|
-
await this.backfill(key,
|
|
457
|
-
this.
|
|
458
|
-
|
|
950
|
+
await this.backfill(key, stored, index - 1, options);
|
|
951
|
+
this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
|
|
952
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
953
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
954
|
+
return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
|
|
955
|
+
}
|
|
956
|
+
if (!sawRetainableValue) {
|
|
957
|
+
await this.tagIndex.remove(key);
|
|
459
958
|
}
|
|
460
|
-
|
|
461
|
-
this.
|
|
462
|
-
return { found: false, value: null };
|
|
959
|
+
this.logger.debug?.("miss", { key, mode });
|
|
960
|
+
this.emit("miss", { key, mode });
|
|
961
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
463
962
|
}
|
|
464
|
-
async
|
|
963
|
+
async readLayerEntry(layer, key) {
|
|
964
|
+
if (this.shouldSkipLayer(layer)) {
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
if (layer.getEntry) {
|
|
968
|
+
try {
|
|
969
|
+
return await layer.getEntry(key);
|
|
970
|
+
} catch (error) {
|
|
971
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
try {
|
|
975
|
+
return await layer.get(key);
|
|
976
|
+
} catch (error) {
|
|
977
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async backfill(key, stored, upToIndex, options) {
|
|
465
981
|
if (upToIndex < 0) {
|
|
466
982
|
return;
|
|
467
983
|
}
|
|
468
984
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
469
985
|
const layer = this.layers[index];
|
|
470
|
-
|
|
986
|
+
if (this.shouldSkipLayer(layer)) {
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
990
|
+
try {
|
|
991
|
+
await layer.set(key, stored, ttl);
|
|
992
|
+
} catch (error) {
|
|
993
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
471
996
|
this.metrics.backfills += 1;
|
|
472
|
-
this.logger.debug("backfill", { key, layer: layer.name });
|
|
997
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
998
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
473
999
|
}
|
|
474
1000
|
}
|
|
475
|
-
async
|
|
476
|
-
|
|
477
|
-
|
|
1001
|
+
async writeAcrossLayers(key, kind, value, options) {
|
|
1002
|
+
const now = Date.now();
|
|
1003
|
+
const operations = this.layers.map((layer) => async () => {
|
|
1004
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
1008
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
1009
|
+
layer.name,
|
|
1010
|
+
options?.staleWhileRevalidate,
|
|
1011
|
+
this.options.staleWhileRevalidate
|
|
1012
|
+
);
|
|
1013
|
+
const staleIfError = this.resolveLayerSeconds(
|
|
1014
|
+
layer.name,
|
|
1015
|
+
options?.staleIfError,
|
|
1016
|
+
this.options.staleIfError
|
|
1017
|
+
);
|
|
1018
|
+
const payload = createStoredValueEnvelope({
|
|
1019
|
+
kind,
|
|
1020
|
+
value,
|
|
1021
|
+
freshTtlSeconds: freshTtl,
|
|
1022
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1023
|
+
staleIfErrorSeconds: staleIfError,
|
|
1024
|
+
now
|
|
1025
|
+
});
|
|
1026
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1027
|
+
try {
|
|
1028
|
+
await layer.set(key, payload, ttl);
|
|
1029
|
+
} catch (error) {
|
|
1030
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1034
|
+
}
|
|
1035
|
+
async executeLayerOperations(operations, context) {
|
|
1036
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1037
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1041
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1042
|
+
if (failures.length === 0) {
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
this.metrics.writeFailures += failures.length;
|
|
1046
|
+
this.logger.debug?.("write-failure", {
|
|
1047
|
+
...context,
|
|
1048
|
+
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
1049
|
+
});
|
|
1050
|
+
if (failures.length === operations.length) {
|
|
1051
|
+
throw new AggregateError(
|
|
1052
|
+
failures.map((failure) => failure.reason),
|
|
1053
|
+
`${context.action} failed for every cache layer`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1058
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
1059
|
+
layerName,
|
|
1060
|
+
options?.negativeTtl,
|
|
1061
|
+
this.options.negativeTtl,
|
|
1062
|
+
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
1063
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
1064
|
+
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
1065
|
+
key,
|
|
1066
|
+
layerName,
|
|
1067
|
+
baseTtl,
|
|
1068
|
+
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
478
1069
|
);
|
|
1070
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
1071
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
1072
|
+
}
|
|
1073
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1074
|
+
if (override !== void 0) {
|
|
1075
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
1076
|
+
}
|
|
1077
|
+
if (globalDefault !== void 0) {
|
|
1078
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
1079
|
+
}
|
|
1080
|
+
return fallback;
|
|
1081
|
+
}
|
|
1082
|
+
readLayerNumber(layerName, value) {
|
|
1083
|
+
if (typeof value === "number") {
|
|
1084
|
+
return value;
|
|
1085
|
+
}
|
|
1086
|
+
return value[layerName];
|
|
479
1087
|
}
|
|
480
|
-
|
|
481
|
-
if (
|
|
482
|
-
return
|
|
1088
|
+
applyJitter(ttl, jitter) {
|
|
1089
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1090
|
+
return ttl;
|
|
483
1091
|
}
|
|
484
|
-
|
|
485
|
-
|
|
1092
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
1093
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
1094
|
+
}
|
|
1095
|
+
shouldNegativeCache(options) {
|
|
1096
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1097
|
+
}
|
|
1098
|
+
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
1099
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
1100
|
+
return;
|
|
486
1101
|
}
|
|
487
|
-
|
|
1102
|
+
const refresh = (async () => {
|
|
1103
|
+
this.metrics.refreshes += 1;
|
|
1104
|
+
try {
|
|
1105
|
+
await this.fetchWithGuards(key, fetcher, options);
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
this.metrics.refreshErrors += 1;
|
|
1108
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
1109
|
+
} finally {
|
|
1110
|
+
this.backgroundRefreshes.delete(key);
|
|
1111
|
+
}
|
|
1112
|
+
})();
|
|
1113
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
1114
|
+
}
|
|
1115
|
+
resolveSingleFlightOptions() {
|
|
1116
|
+
return {
|
|
1117
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1118
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1119
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
1120
|
+
};
|
|
488
1121
|
}
|
|
489
1122
|
async deleteKeys(keys) {
|
|
490
1123
|
if (keys.length === 0) {
|
|
491
1124
|
return;
|
|
492
1125
|
}
|
|
493
|
-
await
|
|
494
|
-
this.layers.map(async (layer) => {
|
|
495
|
-
if (layer.deleteMany) {
|
|
496
|
-
await layer.deleteMany(keys);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
500
|
-
})
|
|
501
|
-
);
|
|
1126
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
502
1127
|
for (const key of keys) {
|
|
503
1128
|
await this.tagIndex.remove(key);
|
|
1129
|
+
this.accessProfiles.delete(key);
|
|
504
1130
|
}
|
|
505
1131
|
this.metrics.deletes += keys.length;
|
|
506
1132
|
this.metrics.invalidations += 1;
|
|
507
|
-
this.logger.debug("delete", { keys });
|
|
1133
|
+
this.logger.debug?.("delete", { keys });
|
|
1134
|
+
this.emit("delete", { keys });
|
|
508
1135
|
}
|
|
509
1136
|
async publishInvalidation(message) {
|
|
510
1137
|
if (!this.options.invalidationBus) {
|
|
@@ -523,23 +1150,277 @@ var CacheStack = class {
|
|
|
523
1150
|
if (message.scope === "clear") {
|
|
524
1151
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
525
1152
|
await this.tagIndex.clear();
|
|
1153
|
+
this.accessProfiles.clear();
|
|
526
1154
|
return;
|
|
527
1155
|
}
|
|
528
1156
|
const keys = message.keys ?? [];
|
|
1157
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
1158
|
+
if (message.operation !== "write") {
|
|
1159
|
+
for (const key of keys) {
|
|
1160
|
+
await this.tagIndex.remove(key);
|
|
1161
|
+
this.accessProfiles.delete(key);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
formatError(error) {
|
|
1166
|
+
if (error instanceof Error) {
|
|
1167
|
+
return error.message;
|
|
1168
|
+
}
|
|
1169
|
+
return String(error);
|
|
1170
|
+
}
|
|
1171
|
+
sleep(ms) {
|
|
1172
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1173
|
+
}
|
|
1174
|
+
shouldBroadcastL1Invalidation() {
|
|
1175
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1176
|
+
}
|
|
1177
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
529
1178
|
await Promise.all(
|
|
530
|
-
|
|
1179
|
+
layers.map(async (layer) => {
|
|
1180
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
531
1183
|
if (layer.deleteMany) {
|
|
532
|
-
|
|
1184
|
+
try {
|
|
1185
|
+
await layer.deleteMany(keys);
|
|
1186
|
+
} catch (error) {
|
|
1187
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1188
|
+
}
|
|
533
1189
|
return;
|
|
534
1190
|
}
|
|
535
|
-
await Promise.all(keys.map((key) =>
|
|
1191
|
+
await Promise.all(keys.map(async (key) => {
|
|
1192
|
+
try {
|
|
1193
|
+
await layer.delete(key);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1196
|
+
}
|
|
1197
|
+
}));
|
|
536
1198
|
})
|
|
537
1199
|
);
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
1200
|
+
}
|
|
1201
|
+
validateConfiguration() {
|
|
1202
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
1203
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
1204
|
+
}
|
|
1205
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1206
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1207
|
+
}
|
|
1208
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1209
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1210
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1211
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1212
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1213
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1214
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1215
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1216
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1217
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1218
|
+
}
|
|
1219
|
+
validateWriteOptions(options) {
|
|
1220
|
+
if (!options) {
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1224
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1225
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1226
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1227
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1228
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1229
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1230
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1231
|
+
}
|
|
1232
|
+
validateLayerNumberOption(name, value) {
|
|
1233
|
+
if (value === void 0) {
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
if (typeof value === "number") {
|
|
1237
|
+
this.validateNonNegativeNumber(name, value);
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1241
|
+
if (layerValue === void 0) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
validatePositiveNumber(name, value) {
|
|
1248
|
+
if (value === void 0) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1252
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
validateNonNegativeNumber(name, value) {
|
|
1256
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1257
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
validateCacheKey(key) {
|
|
1261
|
+
if (key.length === 0) {
|
|
1262
|
+
throw new Error("Cache key must not be empty.");
|
|
1263
|
+
}
|
|
1264
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1265
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1266
|
+
}
|
|
1267
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1268
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1269
|
+
}
|
|
1270
|
+
return key;
|
|
1271
|
+
}
|
|
1272
|
+
serializeOptions(options) {
|
|
1273
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1274
|
+
}
|
|
1275
|
+
validateAdaptiveTtlOptions(options) {
|
|
1276
|
+
if (!options || options === true) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1280
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1281
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1282
|
+
}
|
|
1283
|
+
validateCircuitBreakerOptions(options) {
|
|
1284
|
+
if (!options) {
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1288
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1289
|
+
}
|
|
1290
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1291
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1292
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1293
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1294
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1295
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1296
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1297
|
+
const layer = this.layers[index];
|
|
1298
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
try {
|
|
1302
|
+
await layer.set(key, refreshed, ttl);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1305
|
+
}
|
|
541
1306
|
}
|
|
542
1307
|
}
|
|
1308
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1309
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1313
|
+
if (!ttl || !adaptiveTtl) {
|
|
1314
|
+
return ttl;
|
|
1315
|
+
}
|
|
1316
|
+
const profile = this.accessProfiles.get(key);
|
|
1317
|
+
if (!profile) {
|
|
1318
|
+
return ttl;
|
|
1319
|
+
}
|
|
1320
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1321
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
1322
|
+
if (profile.hits < hotAfter) {
|
|
1323
|
+
return ttl;
|
|
1324
|
+
}
|
|
1325
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1326
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1327
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1328
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1329
|
+
}
|
|
1330
|
+
recordAccess(key) {
|
|
1331
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1332
|
+
profile.hits += 1;
|
|
1333
|
+
profile.lastAccessAt = Date.now();
|
|
1334
|
+
this.accessProfiles.set(key, profile);
|
|
1335
|
+
}
|
|
1336
|
+
incrementMetricMap(target, key) {
|
|
1337
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
1338
|
+
}
|
|
1339
|
+
shouldSkipLayer(layer) {
|
|
1340
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1341
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1342
|
+
}
|
|
1343
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1344
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1348
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1349
|
+
this.metrics.degradedOperations += 1;
|
|
1350
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1351
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1352
|
+
return null;
|
|
1353
|
+
}
|
|
1354
|
+
isGracefulDegradationEnabled() {
|
|
1355
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1356
|
+
}
|
|
1357
|
+
assertCircuitClosed(key, options) {
|
|
1358
|
+
const state = this.circuitBreakers.get(key);
|
|
1359
|
+
if (!state?.openUntil) {
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (state.openUntil <= Date.now()) {
|
|
1363
|
+
state.openUntil = null;
|
|
1364
|
+
state.failures = 0;
|
|
1365
|
+
this.circuitBreakers.set(key, state);
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1369
|
+
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1370
|
+
}
|
|
1371
|
+
recordCircuitFailure(key, options, error) {
|
|
1372
|
+
if (!options) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
1376
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1377
|
+
const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1378
|
+
state.failures += 1;
|
|
1379
|
+
if (state.failures >= failureThreshold) {
|
|
1380
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
1381
|
+
this.metrics.circuitBreakerTrips += 1;
|
|
1382
|
+
}
|
|
1383
|
+
this.circuitBreakers.set(key, state);
|
|
1384
|
+
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1385
|
+
}
|
|
1386
|
+
resetCircuitBreaker(key) {
|
|
1387
|
+
this.circuitBreakers.delete(key);
|
|
1388
|
+
}
|
|
1389
|
+
isNegativeStoredValue(stored) {
|
|
1390
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1391
|
+
}
|
|
1392
|
+
emitError(operation, context) {
|
|
1393
|
+
this.logger.error?.(operation, context);
|
|
1394
|
+
if (this.listenerCount("error") > 0) {
|
|
1395
|
+
this.emit("error", { operation, ...context });
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
serializeKeyPart(value) {
|
|
1399
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1400
|
+
return String(value);
|
|
1401
|
+
}
|
|
1402
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1403
|
+
}
|
|
1404
|
+
isCacheSnapshotEntries(value) {
|
|
1405
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1406
|
+
if (!entry || typeof entry !== "object") {
|
|
1407
|
+
return false;
|
|
1408
|
+
}
|
|
1409
|
+
const candidate = entry;
|
|
1410
|
+
return typeof candidate.key === "string";
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
normalizeForSerialization(value) {
|
|
1414
|
+
if (Array.isArray(value)) {
|
|
1415
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1416
|
+
}
|
|
1417
|
+
if (value && typeof value === "object") {
|
|
1418
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1419
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1420
|
+
return normalized;
|
|
1421
|
+
}, {});
|
|
1422
|
+
}
|
|
1423
|
+
return value;
|
|
543
1424
|
}
|
|
544
1425
|
};
|
|
545
1426
|
|
|
@@ -567,5 +1448,6 @@ CacheStackModule = __decorateClass([
|
|
|
567
1448
|
0 && (module.exports = {
|
|
568
1449
|
CACHE_STACK,
|
|
569
1450
|
CacheStackModule,
|
|
1451
|
+
Cacheable,
|
|
570
1452
|
InjectCacheStack
|
|
571
1453
|
});
|