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
|
@@ -12,11 +12,199 @@ 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";
|
|
52
|
+
|
|
53
|
+
// ../../src/internal/StoredValue.ts
|
|
54
|
+
function isStoredValueEnvelope(value) {
|
|
55
|
+
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
56
|
+
}
|
|
57
|
+
function createStoredValueEnvelope(options) {
|
|
58
|
+
const now = options.now ?? Date.now();
|
|
59
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
60
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
61
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
62
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
63
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
64
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
65
|
+
return {
|
|
66
|
+
__layercache: 1,
|
|
67
|
+
kind: options.kind,
|
|
68
|
+
value: options.value,
|
|
69
|
+
freshUntil,
|
|
70
|
+
staleUntil,
|
|
71
|
+
errorUntil,
|
|
72
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
73
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
74
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
78
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
79
|
+
return { state: "fresh", value: stored, stored };
|
|
80
|
+
}
|
|
81
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
82
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
83
|
+
}
|
|
84
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
85
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
86
|
+
}
|
|
87
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
88
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
89
|
+
}
|
|
90
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
91
|
+
}
|
|
92
|
+
function unwrapStoredValue(stored) {
|
|
93
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
94
|
+
return stored;
|
|
95
|
+
}
|
|
96
|
+
if (stored.kind === "empty") {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
return stored.value ?? null;
|
|
100
|
+
}
|
|
101
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
102
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
103
|
+
return void 0;
|
|
104
|
+
}
|
|
105
|
+
const expiry = maxExpiry(stored);
|
|
106
|
+
if (expiry === null) {
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
const remainingMs = expiry - now;
|
|
110
|
+
if (remainingMs <= 0) {
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
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
|
+
}
|
|
138
|
+
function maxExpiry(stored) {
|
|
139
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
140
|
+
(value) => value !== null
|
|
141
|
+
);
|
|
142
|
+
if (values.length === 0) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return Math.max(...values);
|
|
146
|
+
}
|
|
147
|
+
function normalizePositiveSeconds(value) {
|
|
148
|
+
if (!value || value <= 0) {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
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
|
+
};
|
|
20
208
|
|
|
21
209
|
// ../../src/invalidation/PatternMatcher.ts
|
|
22
210
|
var PatternMatcher = class {
|
|
@@ -264,26 +452,33 @@ var Mutex = class {
|
|
|
264
452
|
var StampedeGuard = class {
|
|
265
453
|
mutexes = /* @__PURE__ */ new Map();
|
|
266
454
|
async execute(key, task) {
|
|
267
|
-
const
|
|
455
|
+
const entry = this.getMutexEntry(key);
|
|
268
456
|
try {
|
|
269
|
-
return await mutex.runExclusive(task);
|
|
457
|
+
return await entry.mutex.runExclusive(task);
|
|
270
458
|
} finally {
|
|
271
|
-
|
|
459
|
+
entry.references -= 1;
|
|
460
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
272
461
|
this.mutexes.delete(key);
|
|
273
462
|
}
|
|
274
463
|
}
|
|
275
464
|
}
|
|
276
|
-
|
|
277
|
-
let
|
|
278
|
-
if (!
|
|
279
|
-
|
|
280
|
-
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);
|
|
281
470
|
}
|
|
282
|
-
|
|
471
|
+
entry.references += 1;
|
|
472
|
+
return entry;
|
|
283
473
|
}
|
|
284
474
|
};
|
|
285
475
|
|
|
286
476
|
// ../../src/CacheStack.ts
|
|
477
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
478
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
479
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
480
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
481
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
287
482
|
var EMPTY_METRICS = () => ({
|
|
288
483
|
hits: 0,
|
|
289
484
|
misses: 0,
|
|
@@ -291,7 +486,17 @@ var EMPTY_METRICS = () => ({
|
|
|
291
486
|
sets: 0,
|
|
292
487
|
deletes: 0,
|
|
293
488
|
backfills: 0,
|
|
294
|
-
invalidations: 0
|
|
489
|
+
invalidations: 0,
|
|
490
|
+
staleHits: 0,
|
|
491
|
+
refreshes: 0,
|
|
492
|
+
refreshErrors: 0,
|
|
493
|
+
writeFailures: 0,
|
|
494
|
+
singleFlightWaits: 0,
|
|
495
|
+
negativeCacheHits: 0,
|
|
496
|
+
circuitBreakerTrips: 0,
|
|
497
|
+
degradedOperations: 0,
|
|
498
|
+
hitsByLayer: {},
|
|
499
|
+
missesByLayer: {}
|
|
295
500
|
});
|
|
296
501
|
var DebugLogger = class {
|
|
297
502
|
enabled;
|
|
@@ -299,21 +504,35 @@ var DebugLogger = class {
|
|
|
299
504
|
this.enabled = enabled;
|
|
300
505
|
}
|
|
301
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) {
|
|
302
519
|
if (!this.enabled) {
|
|
303
520
|
return;
|
|
304
521
|
}
|
|
305
522
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
306
|
-
console
|
|
523
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
307
524
|
}
|
|
308
525
|
};
|
|
309
|
-
var CacheStack = class {
|
|
526
|
+
var CacheStack = class extends EventEmitter {
|
|
310
527
|
constructor(layers, options = {}) {
|
|
528
|
+
super();
|
|
311
529
|
this.layers = layers;
|
|
312
530
|
this.options = options;
|
|
313
531
|
if (layers.length === 0) {
|
|
314
532
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
315
533
|
}
|
|
316
|
-
|
|
534
|
+
this.validateConfiguration();
|
|
535
|
+
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
317
536
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
318
537
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
319
538
|
this.startup = this.initialize();
|
|
@@ -327,68 +546,193 @@ var CacheStack = class {
|
|
|
327
546
|
unsubscribeInvalidation;
|
|
328
547
|
logger;
|
|
329
548
|
tagIndex;
|
|
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;
|
|
330
555
|
async get(key, fetcher, options) {
|
|
556
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
557
|
+
this.validateWriteOptions(options);
|
|
331
558
|
await this.startup;
|
|
332
|
-
const hit = await this.
|
|
559
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
333
560
|
if (hit.found) {
|
|
334
|
-
this.
|
|
335
|
-
|
|
561
|
+
this.recordAccess(normalizedKey);
|
|
562
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
563
|
+
this.metrics.negativeCacheHits += 1;
|
|
564
|
+
}
|
|
565
|
+
if (hit.state === "fresh") {
|
|
566
|
+
this.metrics.hits += 1;
|
|
567
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
568
|
+
return hit.value;
|
|
569
|
+
}
|
|
570
|
+
if (hit.state === "stale-while-revalidate") {
|
|
571
|
+
this.metrics.hits += 1;
|
|
572
|
+
this.metrics.staleHits += 1;
|
|
573
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
574
|
+
if (fetcher) {
|
|
575
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
576
|
+
}
|
|
577
|
+
return hit.value;
|
|
578
|
+
}
|
|
579
|
+
if (!fetcher) {
|
|
580
|
+
this.metrics.hits += 1;
|
|
581
|
+
this.metrics.staleHits += 1;
|
|
582
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
583
|
+
return hit.value;
|
|
584
|
+
}
|
|
585
|
+
try {
|
|
586
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
587
|
+
} catch (error) {
|
|
588
|
+
this.metrics.staleHits += 1;
|
|
589
|
+
this.metrics.refreshErrors += 1;
|
|
590
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
591
|
+
return hit.value;
|
|
592
|
+
}
|
|
336
593
|
}
|
|
337
594
|
this.metrics.misses += 1;
|
|
338
595
|
if (!fetcher) {
|
|
339
596
|
return null;
|
|
340
597
|
}
|
|
341
|
-
|
|
342
|
-
const secondHit = await this.getFromLayers(key, options);
|
|
343
|
-
if (secondHit.found) {
|
|
344
|
-
this.metrics.hits += 1;
|
|
345
|
-
return secondHit.value;
|
|
346
|
-
}
|
|
347
|
-
this.metrics.fetches += 1;
|
|
348
|
-
const fetched = await fetcher();
|
|
349
|
-
if (fetched === null || fetched === void 0) {
|
|
350
|
-
return null;
|
|
351
|
-
}
|
|
352
|
-
await this.set(key, fetched, options);
|
|
353
|
-
return fetched;
|
|
354
|
-
};
|
|
355
|
-
if (this.options.stampedePrevention === false) {
|
|
356
|
-
return runFetch();
|
|
357
|
-
}
|
|
358
|
-
return this.stampedeGuard.execute(key, runFetch);
|
|
598
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
359
599
|
}
|
|
360
600
|
async set(key, value, options) {
|
|
601
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
602
|
+
this.validateWriteOptions(options);
|
|
361
603
|
await this.startup;
|
|
362
|
-
await this.
|
|
363
|
-
if (options?.tags) {
|
|
364
|
-
await this.tagIndex.track(key, options.tags);
|
|
365
|
-
} else {
|
|
366
|
-
await this.tagIndex.touch(key);
|
|
367
|
-
}
|
|
368
|
-
this.metrics.sets += 1;
|
|
369
|
-
this.logger.debug("set", { key, tags: options?.tags });
|
|
370
|
-
if (this.options.publishSetInvalidation !== false) {
|
|
371
|
-
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
372
|
-
}
|
|
604
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
373
605
|
}
|
|
374
606
|
async delete(key) {
|
|
607
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
375
608
|
await this.startup;
|
|
376
|
-
await this.deleteKeys([
|
|
377
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
609
|
+
await this.deleteKeys([normalizedKey]);
|
|
610
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
378
611
|
}
|
|
379
612
|
async clear() {
|
|
380
613
|
await this.startup;
|
|
381
614
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
382
615
|
await this.tagIndex.clear();
|
|
616
|
+
this.accessProfiles.clear();
|
|
383
617
|
this.metrics.invalidations += 1;
|
|
384
|
-
this.logger.debug("clear");
|
|
618
|
+
this.logger.debug?.("clear");
|
|
385
619
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
386
620
|
}
|
|
387
621
|
async mget(entries) {
|
|
388
|
-
|
|
622
|
+
if (entries.length === 0) {
|
|
623
|
+
return [];
|
|
624
|
+
}
|
|
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);
|
|
631
|
+
if (!canFastPath) {
|
|
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
|
+
);
|
|
652
|
+
}
|
|
653
|
+
await this.startup;
|
|
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) {
|
|
668
|
+
break;
|
|
669
|
+
}
|
|
670
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
671
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
672
|
+
const key = keys[offset];
|
|
673
|
+
const stored = values[offset];
|
|
674
|
+
if (stored === null) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const resolved = resolveStoredValue(stored);
|
|
678
|
+
if (resolved.state === "expired") {
|
|
679
|
+
await layer.delete(key);
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
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;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (pending.size > 0) {
|
|
690
|
+
for (const key of pending) {
|
|
691
|
+
await this.tagIndex.remove(key);
|
|
692
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
389
696
|
}
|
|
390
697
|
async mset(entries) {
|
|
391
|
-
|
|
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);
|
|
392
736
|
}
|
|
393
737
|
async invalidateByTag(tag) {
|
|
394
738
|
await this.startup;
|
|
@@ -405,12 +749,74 @@ var CacheStack = class {
|
|
|
405
749
|
getMetrics() {
|
|
406
750
|
return { ...this.metrics };
|
|
407
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
|
+
}
|
|
408
763
|
resetMetrics() {
|
|
409
764
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
410
765
|
}
|
|
411
|
-
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) {
|
|
412
792
|
await this.startup;
|
|
413
|
-
await
|
|
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;
|
|
414
820
|
}
|
|
415
821
|
async initialize() {
|
|
416
822
|
if (!this.options.invalidationBus) {
|
|
@@ -420,66 +826,286 @@ var CacheStack = class {
|
|
|
420
826
|
await this.handleInvalidationMessage(message);
|
|
421
827
|
});
|
|
422
828
|
}
|
|
423
|
-
async
|
|
829
|
+
async fetchWithGuards(key, fetcher, options) {
|
|
830
|
+
const fetchTask = async () => {
|
|
831
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
832
|
+
if (secondHit.found) {
|
|
833
|
+
this.metrics.hits += 1;
|
|
834
|
+
return secondHit.value;
|
|
835
|
+
}
|
|
836
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
837
|
+
};
|
|
838
|
+
const singleFlightTask = async () => {
|
|
839
|
+
if (!this.options.singleFlightCoordinator) {
|
|
840
|
+
return fetchTask();
|
|
841
|
+
}
|
|
842
|
+
return this.options.singleFlightCoordinator.execute(
|
|
843
|
+
key,
|
|
844
|
+
this.resolveSingleFlightOptions(),
|
|
845
|
+
fetchTask,
|
|
846
|
+
() => this.waitForFreshValue(key, fetcher, options)
|
|
847
|
+
);
|
|
848
|
+
};
|
|
849
|
+
if (this.options.stampedePrevention === false) {
|
|
850
|
+
return singleFlightTask();
|
|
851
|
+
}
|
|
852
|
+
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
853
|
+
}
|
|
854
|
+
async waitForFreshValue(key, fetcher, options) {
|
|
855
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
856
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
857
|
+
const deadline = Date.now() + timeoutMs;
|
|
858
|
+
this.metrics.singleFlightWaits += 1;
|
|
859
|
+
this.emit("stampede-dedupe", { key });
|
|
860
|
+
while (Date.now() < deadline) {
|
|
861
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
862
|
+
if (hit.found) {
|
|
863
|
+
this.metrics.hits += 1;
|
|
864
|
+
return hit.value;
|
|
865
|
+
}
|
|
866
|
+
await this.sleep(pollIntervalMs);
|
|
867
|
+
}
|
|
868
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
869
|
+
}
|
|
870
|
+
async fetchAndPopulate(key, fetcher, options) {
|
|
871
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
872
|
+
this.metrics.fetches += 1;
|
|
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
|
+
}
|
|
881
|
+
if (fetched === null || fetched === void 0) {
|
|
882
|
+
if (!this.shouldNegativeCache(options)) {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
await this.storeEntry(key, "empty", null, options);
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
await this.storeEntry(key, "value", fetched, options);
|
|
889
|
+
return fetched;
|
|
890
|
+
}
|
|
891
|
+
async storeEntry(key, kind, value, options) {
|
|
892
|
+
await this.writeAcrossLayers(key, kind, value, options);
|
|
893
|
+
if (options?.tags) {
|
|
894
|
+
await this.tagIndex.track(key, options.tags);
|
|
895
|
+
} else {
|
|
896
|
+
await this.tagIndex.touch(key);
|
|
897
|
+
}
|
|
898
|
+
this.metrics.sets += 1;
|
|
899
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
900
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
901
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
902
|
+
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
async readFromLayers(key, options, mode) {
|
|
906
|
+
let sawRetainableValue = false;
|
|
424
907
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
425
908
|
const layer = this.layers[index];
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
909
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
910
|
+
if (stored === null) {
|
|
911
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
const resolved = resolveStoredValue(stored);
|
|
915
|
+
if (resolved.state === "expired") {
|
|
916
|
+
await layer.delete(key);
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
sawRetainableValue = true;
|
|
920
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
428
921
|
continue;
|
|
429
922
|
}
|
|
430
923
|
await this.tagIndex.touch(key);
|
|
431
|
-
await this.backfill(key,
|
|
432
|
-
this.
|
|
433
|
-
|
|
924
|
+
await this.backfill(key, stored, index - 1, options);
|
|
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 };
|
|
929
|
+
}
|
|
930
|
+
if (!sawRetainableValue) {
|
|
931
|
+
await this.tagIndex.remove(key);
|
|
434
932
|
}
|
|
435
|
-
|
|
436
|
-
this.
|
|
437
|
-
return { found: false, value: null };
|
|
933
|
+
this.logger.debug?.("miss", { key, mode });
|
|
934
|
+
this.emit("miss", { key, mode });
|
|
935
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
438
936
|
}
|
|
439
|
-
async
|
|
937
|
+
async readLayerEntry(layer, key) {
|
|
938
|
+
if (this.shouldSkipLayer(layer)) {
|
|
939
|
+
return null;
|
|
940
|
+
}
|
|
941
|
+
if (layer.getEntry) {
|
|
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);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async backfill(key, stored, upToIndex, options) {
|
|
440
955
|
if (upToIndex < 0) {
|
|
441
956
|
return;
|
|
442
957
|
}
|
|
443
958
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
444
959
|
const layer = this.layers[index];
|
|
445
|
-
|
|
960
|
+
if (this.shouldSkipLayer(layer)) {
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
964
|
+
try {
|
|
965
|
+
await layer.set(key, stored, ttl);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
446
970
|
this.metrics.backfills += 1;
|
|
447
|
-
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 });
|
|
448
973
|
}
|
|
449
974
|
}
|
|
450
|
-
async
|
|
451
|
-
|
|
452
|
-
|
|
975
|
+
async writeAcrossLayers(key, kind, value, options) {
|
|
976
|
+
const now = Date.now();
|
|
977
|
+
const operations = this.layers.map((layer) => async () => {
|
|
978
|
+
if (this.shouldSkipLayer(layer)) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
982
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
983
|
+
layer.name,
|
|
984
|
+
options?.staleWhileRevalidate,
|
|
985
|
+
this.options.staleWhileRevalidate
|
|
986
|
+
);
|
|
987
|
+
const staleIfError = this.resolveLayerSeconds(
|
|
988
|
+
layer.name,
|
|
989
|
+
options?.staleIfError,
|
|
990
|
+
this.options.staleIfError
|
|
991
|
+
);
|
|
992
|
+
const payload = createStoredValueEnvelope({
|
|
993
|
+
kind,
|
|
994
|
+
value,
|
|
995
|
+
freshTtlSeconds: freshTtl,
|
|
996
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
997
|
+
staleIfErrorSeconds: staleIfError,
|
|
998
|
+
now
|
|
999
|
+
});
|
|
1000
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1001
|
+
try {
|
|
1002
|
+
await layer.set(key, payload, ttl);
|
|
1003
|
+
} catch (error) {
|
|
1004
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1008
|
+
}
|
|
1009
|
+
async executeLayerOperations(operations, context) {
|
|
1010
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
1011
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1015
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
1016
|
+
if (failures.length === 0) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
this.metrics.writeFailures += failures.length;
|
|
1020
|
+
this.logger.debug?.("write-failure", {
|
|
1021
|
+
...context,
|
|
1022
|
+
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
1023
|
+
});
|
|
1024
|
+
if (failures.length === operations.length) {
|
|
1025
|
+
throw new AggregateError(
|
|
1026
|
+
failures.map((failure) => failure.reason),
|
|
1027
|
+
`${context.action} failed for every cache layer`
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1032
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
1033
|
+
layerName,
|
|
1034
|
+
options?.negativeTtl,
|
|
1035
|
+
this.options.negativeTtl,
|
|
1036
|
+
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
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
|
|
453
1043
|
);
|
|
1044
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
1045
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
1046
|
+
}
|
|
1047
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1048
|
+
if (override !== void 0) {
|
|
1049
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
1050
|
+
}
|
|
1051
|
+
if (globalDefault !== void 0) {
|
|
1052
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
1053
|
+
}
|
|
1054
|
+
return fallback;
|
|
1055
|
+
}
|
|
1056
|
+
readLayerNumber(layerName, value) {
|
|
1057
|
+
if (typeof value === "number") {
|
|
1058
|
+
return value;
|
|
1059
|
+
}
|
|
1060
|
+
return value[layerName];
|
|
454
1061
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
return
|
|
1062
|
+
applyJitter(ttl, jitter) {
|
|
1063
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1064
|
+
return ttl;
|
|
458
1065
|
}
|
|
459
|
-
|
|
460
|
-
|
|
1066
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
1067
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
1068
|
+
}
|
|
1069
|
+
shouldNegativeCache(options) {
|
|
1070
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1071
|
+
}
|
|
1072
|
+
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
1073
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
1074
|
+
return;
|
|
461
1075
|
}
|
|
462
|
-
|
|
1076
|
+
const refresh = (async () => {
|
|
1077
|
+
this.metrics.refreshes += 1;
|
|
1078
|
+
try {
|
|
1079
|
+
await this.fetchWithGuards(key, fetcher, options);
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
this.metrics.refreshErrors += 1;
|
|
1082
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
1083
|
+
} finally {
|
|
1084
|
+
this.backgroundRefreshes.delete(key);
|
|
1085
|
+
}
|
|
1086
|
+
})();
|
|
1087
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
1088
|
+
}
|
|
1089
|
+
resolveSingleFlightOptions() {
|
|
1090
|
+
return {
|
|
1091
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1092
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1093
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
1094
|
+
};
|
|
463
1095
|
}
|
|
464
1096
|
async deleteKeys(keys) {
|
|
465
1097
|
if (keys.length === 0) {
|
|
466
1098
|
return;
|
|
467
1099
|
}
|
|
468
|
-
await
|
|
469
|
-
this.layers.map(async (layer) => {
|
|
470
|
-
if (layer.deleteMany) {
|
|
471
|
-
await layer.deleteMany(keys);
|
|
472
|
-
return;
|
|
473
|
-
}
|
|
474
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
475
|
-
})
|
|
476
|
-
);
|
|
1100
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
477
1101
|
for (const key of keys) {
|
|
478
1102
|
await this.tagIndex.remove(key);
|
|
1103
|
+
this.accessProfiles.delete(key);
|
|
479
1104
|
}
|
|
480
1105
|
this.metrics.deletes += keys.length;
|
|
481
1106
|
this.metrics.invalidations += 1;
|
|
482
|
-
this.logger.debug("delete", { keys });
|
|
1107
|
+
this.logger.debug?.("delete", { keys });
|
|
1108
|
+
this.emit("delete", { keys });
|
|
483
1109
|
}
|
|
484
1110
|
async publishInvalidation(message) {
|
|
485
1111
|
if (!this.options.invalidationBus) {
|
|
@@ -498,23 +1124,277 @@ var CacheStack = class {
|
|
|
498
1124
|
if (message.scope === "clear") {
|
|
499
1125
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
500
1126
|
await this.tagIndex.clear();
|
|
1127
|
+
this.accessProfiles.clear();
|
|
501
1128
|
return;
|
|
502
1129
|
}
|
|
503
1130
|
const keys = message.keys ?? [];
|
|
1131
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
1132
|
+
if (message.operation !== "write") {
|
|
1133
|
+
for (const key of keys) {
|
|
1134
|
+
await this.tagIndex.remove(key);
|
|
1135
|
+
this.accessProfiles.delete(key);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
formatError(error) {
|
|
1140
|
+
if (error instanceof Error) {
|
|
1141
|
+
return error.message;
|
|
1142
|
+
}
|
|
1143
|
+
return String(error);
|
|
1144
|
+
}
|
|
1145
|
+
sleep(ms) {
|
|
1146
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1147
|
+
}
|
|
1148
|
+
shouldBroadcastL1Invalidation() {
|
|
1149
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1150
|
+
}
|
|
1151
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
504
1152
|
await Promise.all(
|
|
505
|
-
|
|
1153
|
+
layers.map(async (layer) => {
|
|
1154
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
506
1157
|
if (layer.deleteMany) {
|
|
507
|
-
|
|
1158
|
+
try {
|
|
1159
|
+
await layer.deleteMany(keys);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
1162
|
+
}
|
|
508
1163
|
return;
|
|
509
1164
|
}
|
|
510
|
-
await Promise.all(keys.map((key) =>
|
|
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
|
+
}));
|
|
511
1172
|
})
|
|
512
1173
|
);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
+
}
|
|
516
1280
|
}
|
|
517
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;
|
|
518
1398
|
}
|
|
519
1399
|
};
|
|
520
1400
|
|
|
@@ -541,5 +1421,6 @@ CacheStackModule = __decorateClass([
|
|
|
541
1421
|
export {
|
|
542
1422
|
CACHE_STACK,
|
|
543
1423
|
CacheStackModule,
|
|
1424
|
+
Cacheable,
|
|
544
1425
|
InjectCacheStack
|
|
545
1426
|
};
|