layercache 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +85 -6
- package/dist/index.cjs +437 -59
- package/dist/index.d.cts +64 -4
- package/dist/index.d.ts +64 -4
- package/dist/index.js +436 -59
- package/package.json +5 -1
- package/packages/nestjs/dist/index.cjs +368 -57
- package/packages/nestjs/dist/index.d.cts +23 -0
- package/packages/nestjs/dist/index.d.ts +23 -0
- package/packages/nestjs/dist/index.js +368 -57
|
@@ -18,6 +18,81 @@ import { Global, Inject, Module } from "@nestjs/common";
|
|
|
18
18
|
// ../../src/CacheStack.ts
|
|
19
19
|
import { randomUUID } from "crypto";
|
|
20
20
|
|
|
21
|
+
// ../../src/internal/StoredValue.ts
|
|
22
|
+
function isStoredValueEnvelope(value) {
|
|
23
|
+
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
24
|
+
}
|
|
25
|
+
function createStoredValueEnvelope(options) {
|
|
26
|
+
const now = options.now ?? Date.now();
|
|
27
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
28
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
29
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
30
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
31
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
32
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
33
|
+
return {
|
|
34
|
+
__layercache: 1,
|
|
35
|
+
kind: options.kind,
|
|
36
|
+
value: options.value,
|
|
37
|
+
freshUntil,
|
|
38
|
+
staleUntil,
|
|
39
|
+
errorUntil
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
43
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
44
|
+
return { state: "fresh", value: stored, stored };
|
|
45
|
+
}
|
|
46
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
47
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
48
|
+
}
|
|
49
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
50
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
51
|
+
}
|
|
52
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
53
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
54
|
+
}
|
|
55
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
56
|
+
}
|
|
57
|
+
function unwrapStoredValue(stored) {
|
|
58
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
59
|
+
return stored;
|
|
60
|
+
}
|
|
61
|
+
if (stored.kind === "empty") {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return stored.value ?? null;
|
|
65
|
+
}
|
|
66
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
67
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
const expiry = maxExpiry(stored);
|
|
71
|
+
if (expiry === null) {
|
|
72
|
+
return void 0;
|
|
73
|
+
}
|
|
74
|
+
const remainingMs = expiry - now;
|
|
75
|
+
if (remainingMs <= 0) {
|
|
76
|
+
return 1;
|
|
77
|
+
}
|
|
78
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
79
|
+
}
|
|
80
|
+
function maxExpiry(stored) {
|
|
81
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
82
|
+
(value) => value !== null
|
|
83
|
+
);
|
|
84
|
+
if (values.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
return Math.max(...values);
|
|
88
|
+
}
|
|
89
|
+
function normalizePositiveSeconds(value) {
|
|
90
|
+
if (!value || value <= 0) {
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
21
96
|
// ../../src/invalidation/PatternMatcher.ts
|
|
22
97
|
var PatternMatcher = class {
|
|
23
98
|
static matches(pattern, value) {
|
|
@@ -284,6 +359,10 @@ var StampedeGuard = class {
|
|
|
284
359
|
};
|
|
285
360
|
|
|
286
361
|
// ../../src/CacheStack.ts
|
|
362
|
+
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
363
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
364
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
365
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
287
366
|
var EMPTY_METRICS = () => ({
|
|
288
367
|
hits: 0,
|
|
289
368
|
misses: 0,
|
|
@@ -291,7 +370,12 @@ var EMPTY_METRICS = () => ({
|
|
|
291
370
|
sets: 0,
|
|
292
371
|
deletes: 0,
|
|
293
372
|
backfills: 0,
|
|
294
|
-
invalidations: 0
|
|
373
|
+
invalidations: 0,
|
|
374
|
+
staleHits: 0,
|
|
375
|
+
refreshes: 0,
|
|
376
|
+
refreshErrors: 0,
|
|
377
|
+
writeFailures: 0,
|
|
378
|
+
singleFlightWaits: 0
|
|
295
379
|
});
|
|
296
380
|
var DebugLogger = class {
|
|
297
381
|
enabled;
|
|
@@ -303,7 +387,7 @@ var DebugLogger = class {
|
|
|
303
387
|
return;
|
|
304
388
|
}
|
|
305
389
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
306
|
-
console.debug(`[
|
|
390
|
+
console.debug(`[layercache] ${message}${suffix}`);
|
|
307
391
|
}
|
|
308
392
|
};
|
|
309
393
|
var CacheStack = class {
|
|
@@ -313,7 +397,7 @@ var CacheStack = class {
|
|
|
313
397
|
if (layers.length === 0) {
|
|
314
398
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
315
399
|
}
|
|
316
|
-
const debugEnv = process.env.DEBUG?.split(",").includes("
|
|
400
|
+
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
317
401
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
318
402
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
319
403
|
this.startup = this.initialize();
|
|
@@ -327,49 +411,46 @@ var CacheStack = class {
|
|
|
327
411
|
unsubscribeInvalidation;
|
|
328
412
|
logger;
|
|
329
413
|
tagIndex;
|
|
414
|
+
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
330
415
|
async get(key, fetcher, options) {
|
|
331
416
|
await this.startup;
|
|
332
|
-
const hit = await this.
|
|
417
|
+
const hit = await this.readFromLayers(key, options, "allow-stale");
|
|
333
418
|
if (hit.found) {
|
|
334
|
-
|
|
335
|
-
|
|
419
|
+
if (hit.state === "fresh") {
|
|
420
|
+
this.metrics.hits += 1;
|
|
421
|
+
return hit.value;
|
|
422
|
+
}
|
|
423
|
+
if (hit.state === "stale-while-revalidate") {
|
|
424
|
+
this.metrics.hits += 1;
|
|
425
|
+
this.metrics.staleHits += 1;
|
|
426
|
+
if (fetcher) {
|
|
427
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
428
|
+
}
|
|
429
|
+
return hit.value;
|
|
430
|
+
}
|
|
431
|
+
if (!fetcher) {
|
|
432
|
+
this.metrics.hits += 1;
|
|
433
|
+
this.metrics.staleHits += 1;
|
|
434
|
+
return hit.value;
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
return await this.fetchWithGuards(key, fetcher, options);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.metrics.staleHits += 1;
|
|
440
|
+
this.metrics.refreshErrors += 1;
|
|
441
|
+
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
442
|
+
return hit.value;
|
|
443
|
+
}
|
|
336
444
|
}
|
|
337
445
|
this.metrics.misses += 1;
|
|
338
446
|
if (!fetcher) {
|
|
339
447
|
return null;
|
|
340
448
|
}
|
|
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);
|
|
449
|
+
return this.fetchWithGuards(key, fetcher, options);
|
|
359
450
|
}
|
|
360
451
|
async set(key, value, options) {
|
|
361
452
|
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
|
-
}
|
|
453
|
+
await this.storeEntry(key, "value", value, options);
|
|
373
454
|
}
|
|
374
455
|
async delete(key) {
|
|
375
456
|
await this.startup;
|
|
@@ -385,7 +466,48 @@ var CacheStack = class {
|
|
|
385
466
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
386
467
|
}
|
|
387
468
|
async mget(entries) {
|
|
388
|
-
|
|
469
|
+
if (entries.length === 0) {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
const canFastPath = entries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
473
|
+
if (!canFastPath) {
|
|
474
|
+
return Promise.all(entries.map((entry) => this.get(entry.key, entry.fetch, entry.options)));
|
|
475
|
+
}
|
|
476
|
+
await this.startup;
|
|
477
|
+
const pending = new Set(entries.map((_, index) => index));
|
|
478
|
+
const results = Array(entries.length).fill(null);
|
|
479
|
+
for (const layer of this.layers) {
|
|
480
|
+
const indexes = [...pending];
|
|
481
|
+
if (indexes.length === 0) {
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
const keys = indexes.map((index) => entries[index].key);
|
|
485
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
486
|
+
for (let offset = 0; offset < values.length; offset += 1) {
|
|
487
|
+
const index = indexes[offset];
|
|
488
|
+
const stored = values[offset];
|
|
489
|
+
if (stored === null) {
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const resolved = resolveStoredValue(stored);
|
|
493
|
+
if (resolved.state === "expired") {
|
|
494
|
+
await layer.delete(entries[index].key);
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
await this.tagIndex.touch(entries[index].key);
|
|
498
|
+
await this.backfill(entries[index].key, stored, this.layers.indexOf(layer) - 1, entries[index].options);
|
|
499
|
+
results[index] = resolved.value;
|
|
500
|
+
pending.delete(index);
|
|
501
|
+
this.metrics.hits += 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (pending.size > 0) {
|
|
505
|
+
for (const index of pending) {
|
|
506
|
+
await this.tagIndex.remove(entries[index].key);
|
|
507
|
+
this.metrics.misses += 1;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return results;
|
|
389
511
|
}
|
|
390
512
|
async mset(entries) {
|
|
391
513
|
await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
@@ -411,6 +533,7 @@ var CacheStack = class {
|
|
|
411
533
|
async disconnect() {
|
|
412
534
|
await this.startup;
|
|
413
535
|
await this.unsubscribeInvalidation?.();
|
|
536
|
+
await Promise.allSettled(this.backgroundRefreshes.values());
|
|
414
537
|
}
|
|
415
538
|
async initialize() {
|
|
416
539
|
if (!this.options.invalidationBus) {
|
|
@@ -420,46 +543,225 @@ var CacheStack = class {
|
|
|
420
543
|
await this.handleInvalidationMessage(message);
|
|
421
544
|
});
|
|
422
545
|
}
|
|
423
|
-
async
|
|
546
|
+
async fetchWithGuards(key, fetcher, options) {
|
|
547
|
+
const fetchTask = async () => {
|
|
548
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
549
|
+
if (secondHit.found) {
|
|
550
|
+
this.metrics.hits += 1;
|
|
551
|
+
return secondHit.value;
|
|
552
|
+
}
|
|
553
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
554
|
+
};
|
|
555
|
+
const singleFlightTask = async () => {
|
|
556
|
+
if (!this.options.singleFlightCoordinator) {
|
|
557
|
+
return fetchTask();
|
|
558
|
+
}
|
|
559
|
+
return this.options.singleFlightCoordinator.execute(
|
|
560
|
+
key,
|
|
561
|
+
this.resolveSingleFlightOptions(),
|
|
562
|
+
fetchTask,
|
|
563
|
+
() => this.waitForFreshValue(key, fetcher, options)
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
if (this.options.stampedePrevention === false) {
|
|
567
|
+
return singleFlightTask();
|
|
568
|
+
}
|
|
569
|
+
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
570
|
+
}
|
|
571
|
+
async waitForFreshValue(key, fetcher, options) {
|
|
572
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
573
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
574
|
+
const deadline = Date.now() + timeoutMs;
|
|
575
|
+
this.metrics.singleFlightWaits += 1;
|
|
576
|
+
while (Date.now() < deadline) {
|
|
577
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
578
|
+
if (hit.found) {
|
|
579
|
+
this.metrics.hits += 1;
|
|
580
|
+
return hit.value;
|
|
581
|
+
}
|
|
582
|
+
await this.sleep(pollIntervalMs);
|
|
583
|
+
}
|
|
584
|
+
return this.fetchAndPopulate(key, fetcher, options);
|
|
585
|
+
}
|
|
586
|
+
async fetchAndPopulate(key, fetcher, options) {
|
|
587
|
+
this.metrics.fetches += 1;
|
|
588
|
+
const fetched = await fetcher();
|
|
589
|
+
if (fetched === null || fetched === void 0) {
|
|
590
|
+
if (!this.shouldNegativeCache(options)) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
await this.storeEntry(key, "empty", null, options);
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
await this.storeEntry(key, "value", fetched, options);
|
|
597
|
+
return fetched;
|
|
598
|
+
}
|
|
599
|
+
async storeEntry(key, kind, value, options) {
|
|
600
|
+
await this.writeAcrossLayers(key, kind, value, options);
|
|
601
|
+
if (options?.tags) {
|
|
602
|
+
await this.tagIndex.track(key, options.tags);
|
|
603
|
+
} else {
|
|
604
|
+
await this.tagIndex.touch(key);
|
|
605
|
+
}
|
|
606
|
+
this.metrics.sets += 1;
|
|
607
|
+
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
608
|
+
if (this.options.publishSetInvalidation !== false) {
|
|
609
|
+
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async readFromLayers(key, options, mode) {
|
|
613
|
+
let sawRetainableValue = false;
|
|
424
614
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
425
615
|
const layer = this.layers[index];
|
|
426
|
-
const
|
|
427
|
-
if (
|
|
616
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
617
|
+
if (stored === null) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const resolved = resolveStoredValue(stored);
|
|
621
|
+
if (resolved.state === "expired") {
|
|
622
|
+
await layer.delete(key);
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
sawRetainableValue = true;
|
|
626
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
428
627
|
continue;
|
|
429
628
|
}
|
|
430
629
|
await this.tagIndex.touch(key);
|
|
431
|
-
await this.backfill(key,
|
|
432
|
-
this.logger.debug("hit", { key, layer: layer.name });
|
|
433
|
-
return { found: true, value };
|
|
630
|
+
await this.backfill(key, stored, index - 1, options);
|
|
631
|
+
this.logger.debug("hit", { key, layer: layer.name, state: resolved.state });
|
|
632
|
+
return { found: true, value: resolved.value, stored, state: resolved.state };
|
|
434
633
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
634
|
+
if (!sawRetainableValue) {
|
|
635
|
+
await this.tagIndex.remove(key);
|
|
636
|
+
}
|
|
637
|
+
this.logger.debug("miss", { key, mode });
|
|
638
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
639
|
+
}
|
|
640
|
+
async readLayerEntry(layer, key) {
|
|
641
|
+
if (layer.getEntry) {
|
|
642
|
+
return layer.getEntry(key);
|
|
643
|
+
}
|
|
644
|
+
return layer.get(key);
|
|
438
645
|
}
|
|
439
|
-
async backfill(key,
|
|
646
|
+
async backfill(key, stored, upToIndex, options) {
|
|
440
647
|
if (upToIndex < 0) {
|
|
441
648
|
return;
|
|
442
649
|
}
|
|
443
650
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
444
651
|
const layer = this.layers[index];
|
|
445
|
-
|
|
652
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
653
|
+
await layer.set(key, stored, ttl);
|
|
446
654
|
this.metrics.backfills += 1;
|
|
447
655
|
this.logger.debug("backfill", { key, layer: layer.name });
|
|
448
656
|
}
|
|
449
657
|
}
|
|
450
|
-
async
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
658
|
+
async writeAcrossLayers(key, kind, value, options) {
|
|
659
|
+
const now = Date.now();
|
|
660
|
+
const operations = this.layers.map((layer) => async () => {
|
|
661
|
+
const freshTtl = this.resolveFreshTtl(layer.name, kind, options, layer.defaultTtl);
|
|
662
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
663
|
+
layer.name,
|
|
664
|
+
options?.staleWhileRevalidate,
|
|
665
|
+
this.options.staleWhileRevalidate
|
|
666
|
+
);
|
|
667
|
+
const staleIfError = this.resolveLayerSeconds(
|
|
668
|
+
layer.name,
|
|
669
|
+
options?.staleIfError,
|
|
670
|
+
this.options.staleIfError
|
|
671
|
+
);
|
|
672
|
+
const payload = createStoredValueEnvelope({
|
|
673
|
+
kind,
|
|
674
|
+
value,
|
|
675
|
+
freshTtlSeconds: freshTtl,
|
|
676
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
677
|
+
staleIfErrorSeconds: staleIfError,
|
|
678
|
+
now
|
|
679
|
+
});
|
|
680
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
681
|
+
await layer.set(key, payload, ttl);
|
|
682
|
+
});
|
|
683
|
+
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
454
684
|
}
|
|
455
|
-
|
|
456
|
-
if (
|
|
457
|
-
|
|
685
|
+
async executeLayerOperations(operations, context) {
|
|
686
|
+
if (this.options.writePolicy !== "best-effort") {
|
|
687
|
+
await Promise.all(operations.map((operation) => operation()));
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
691
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
692
|
+
if (failures.length === 0) {
|
|
693
|
+
return;
|
|
458
694
|
}
|
|
459
|
-
|
|
460
|
-
|
|
695
|
+
this.metrics.writeFailures += failures.length;
|
|
696
|
+
this.logger.debug("write-failure", {
|
|
697
|
+
...context,
|
|
698
|
+
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
699
|
+
});
|
|
700
|
+
if (failures.length === operations.length) {
|
|
701
|
+
throw new AggregateError(
|
|
702
|
+
failures.map((failure) => failure.reason),
|
|
703
|
+
`${context.action} failed for every cache layer`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
708
|
+
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
709
|
+
layerName,
|
|
710
|
+
options?.negativeTtl,
|
|
711
|
+
this.options.negativeTtl,
|
|
712
|
+
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
713
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
714
|
+
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
715
|
+
return this.applyJitter(baseTtl, jitter);
|
|
716
|
+
}
|
|
717
|
+
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
718
|
+
if (override !== void 0) {
|
|
719
|
+
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
461
720
|
}
|
|
462
|
-
|
|
721
|
+
if (globalDefault !== void 0) {
|
|
722
|
+
return this.readLayerNumber(layerName, globalDefault) ?? fallback;
|
|
723
|
+
}
|
|
724
|
+
return fallback;
|
|
725
|
+
}
|
|
726
|
+
readLayerNumber(layerName, value) {
|
|
727
|
+
if (typeof value === "number") {
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
return value[layerName];
|
|
731
|
+
}
|
|
732
|
+
applyJitter(ttl, jitter) {
|
|
733
|
+
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
734
|
+
return ttl;
|
|
735
|
+
}
|
|
736
|
+
const delta = (Math.random() * 2 - 1) * jitter;
|
|
737
|
+
return Math.max(1, Math.round(ttl + delta));
|
|
738
|
+
}
|
|
739
|
+
shouldNegativeCache(options) {
|
|
740
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
741
|
+
}
|
|
742
|
+
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
743
|
+
if (this.backgroundRefreshes.has(key)) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const refresh = (async () => {
|
|
747
|
+
this.metrics.refreshes += 1;
|
|
748
|
+
try {
|
|
749
|
+
await this.fetchWithGuards(key, fetcher, options);
|
|
750
|
+
} catch (error) {
|
|
751
|
+
this.metrics.refreshErrors += 1;
|
|
752
|
+
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
753
|
+
} finally {
|
|
754
|
+
this.backgroundRefreshes.delete(key);
|
|
755
|
+
}
|
|
756
|
+
})();
|
|
757
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
758
|
+
}
|
|
759
|
+
resolveSingleFlightOptions() {
|
|
760
|
+
return {
|
|
761
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
762
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
763
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
764
|
+
};
|
|
463
765
|
}
|
|
464
766
|
async deleteKeys(keys) {
|
|
465
767
|
if (keys.length === 0) {
|
|
@@ -516,6 +818,15 @@ var CacheStack = class {
|
|
|
516
818
|
}
|
|
517
819
|
}
|
|
518
820
|
}
|
|
821
|
+
formatError(error) {
|
|
822
|
+
if (error instanceof Error) {
|
|
823
|
+
return error.message;
|
|
824
|
+
}
|
|
825
|
+
return String(error);
|
|
826
|
+
}
|
|
827
|
+
sleep(ms) {
|
|
828
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
829
|
+
}
|
|
519
830
|
};
|
|
520
831
|
|
|
521
832
|
// src/module.ts
|