layercache 1.3.3 → 1.4.0
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 +42 -41
- package/dist/{chunk-BORDQ3LA.js → chunk-7KMKQ6QZ.js} +15 -1
- package/dist/{chunk-5RCAX2BQ.js → chunk-FFZCC7EQ.js} +3 -3
- package/dist/{chunk-4PPBOOXT.js → chunk-KJDFYE5T.js} +38 -26
- package/dist/cli.cjs +9 -9
- package/dist/cli.js +4 -4
- package/dist/{edge-CUHTP9Bc.d.cts → edge-D2FpRlyS.d.cts} +74 -36
- package/dist/{edge-CUHTP9Bc.d.ts → edge-D2FpRlyS.d.ts} +74 -36
- package/dist/edge.cjs +9 -9
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +787 -466
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +682 -383
- package/package.json +6 -6
- package/benchmarks/direct.ts +0 -221
- package/benchmarks/edge-utils.ts +0 -28
- package/benchmarks/edge.ts +0 -491
- package/benchmarks/http.ts +0 -99
- package/benchmarks/latency.ts +0 -45
- package/benchmarks/memory-pressure.ts +0 -144
- package/benchmarks/multi-process-fanout.ts +0 -231
- package/benchmarks/multi-process-worker.ts +0 -151
- package/benchmarks/paths.ts +0 -25
- package/benchmarks/queue-amplification-utils.ts +0 -48
- package/benchmarks/queue-amplification.ts +0 -230
- package/benchmarks/redis-latency-proxy.ts +0 -100
- package/benchmarks/redis.ts +0 -107
- package/benchmarks/scenario-utils.ts +0 -38
- package/benchmarks/server.ts +0 -157
- package/benchmarks/slow-redis-latency.ts +0 -309
- package/benchmarks/slow-redis-utils.ts +0 -29
- package/benchmarks/slow-redis.ts +0 -47
- package/benchmarks/stampede.ts +0 -26
- package/benchmarks/stats.ts +0 -46
- package/benchmarks/workload.ts +0 -77
- package/examples/express-api/index.ts +0 -31
- package/examples/nextjs-api-routes/route.ts +0 -16
package/dist/index.cjs
CHANGED
|
@@ -248,6 +248,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
248
248
|
async invalidateByTag(tag) {
|
|
249
249
|
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
250
250
|
}
|
|
251
|
+
async expireByTag(tag) {
|
|
252
|
+
await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
|
|
253
|
+
}
|
|
251
254
|
async invalidateByTags(tags, mode = "any") {
|
|
252
255
|
await this.trackMetrics(
|
|
253
256
|
() => this.cache.invalidateByTags(
|
|
@@ -256,12 +259,26 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
256
259
|
)
|
|
257
260
|
);
|
|
258
261
|
}
|
|
262
|
+
async expireByTags(tags, mode = "any") {
|
|
263
|
+
await this.trackMetrics(
|
|
264
|
+
() => this.cache.expireByTags(
|
|
265
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
266
|
+
mode
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
}
|
|
259
270
|
async invalidateByPattern(pattern) {
|
|
260
271
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
261
272
|
}
|
|
273
|
+
async expireByPattern(pattern) {
|
|
274
|
+
await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
|
|
275
|
+
}
|
|
262
276
|
async invalidateByPrefix(prefix) {
|
|
263
277
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
264
278
|
}
|
|
279
|
+
async expireByPrefix(prefix) {
|
|
280
|
+
await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
|
|
281
|
+
}
|
|
265
282
|
/**
|
|
266
283
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
267
284
|
*/
|
|
@@ -602,68 +619,6 @@ function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
|
602
619
|
return batches;
|
|
603
620
|
}
|
|
604
621
|
|
|
605
|
-
// src/internal/CacheStackInvalidationSupport.ts
|
|
606
|
-
var CacheStackInvalidationSupport = class {
|
|
607
|
-
constructor(options) {
|
|
608
|
-
this.options = options;
|
|
609
|
-
}
|
|
610
|
-
options;
|
|
611
|
-
async collectKeysForTag(tag, maxKeys) {
|
|
612
|
-
const keys = /* @__PURE__ */ new Set();
|
|
613
|
-
if (this.options.tagIndex.forEachKeyForTag) {
|
|
614
|
-
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
615
|
-
keys.add(key);
|
|
616
|
-
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
617
|
-
});
|
|
618
|
-
return [...keys];
|
|
619
|
-
}
|
|
620
|
-
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
621
|
-
keys.add(key);
|
|
622
|
-
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
623
|
-
}
|
|
624
|
-
return [...keys];
|
|
625
|
-
}
|
|
626
|
-
intersectKeys(groups) {
|
|
627
|
-
if (groups.length === 0) {
|
|
628
|
-
return [];
|
|
629
|
-
}
|
|
630
|
-
const [firstGroup, ...rest] = groups;
|
|
631
|
-
const restSets = rest.map((group) => new Set(group));
|
|
632
|
-
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
633
|
-
}
|
|
634
|
-
async deleteKeysFromLayers(layers, keys) {
|
|
635
|
-
await Promise.all(
|
|
636
|
-
layers.map(async (layer) => {
|
|
637
|
-
if (this.options.shouldSkipLayer(layer)) {
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
if (layer.deleteMany) {
|
|
641
|
-
try {
|
|
642
|
-
await layer.deleteMany(keys);
|
|
643
|
-
} catch (error) {
|
|
644
|
-
await this.options.handleLayerFailure(layer, "delete", error);
|
|
645
|
-
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
await Promise.all(
|
|
649
|
-
keys.map(async (key) => {
|
|
650
|
-
try {
|
|
651
|
-
await layer.delete(key);
|
|
652
|
-
} catch (error) {
|
|
653
|
-
await this.options.handleLayerFailure(layer, "delete", error);
|
|
654
|
-
}
|
|
655
|
-
})
|
|
656
|
-
);
|
|
657
|
-
})
|
|
658
|
-
);
|
|
659
|
-
}
|
|
660
|
-
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
661
|
-
if (maxKeys !== false && size > maxKeys) {
|
|
662
|
-
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
};
|
|
666
|
-
|
|
667
622
|
// src/internal/StoredValue.ts
|
|
668
623
|
function isStoredValueEnvelope(value) {
|
|
669
624
|
if (typeof value !== "object" || value === null) {
|
|
@@ -704,29 +659,29 @@ function isStoredValueEnvelope(value) {
|
|
|
704
659
|
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
705
660
|
return false;
|
|
706
661
|
}
|
|
707
|
-
const
|
|
708
|
-
if (!
|
|
662
|
+
const maxTtlMs = 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
663
|
+
if (!isValidEnvelopeTtlMs(v.freshTtlMs, maxTtlMs)) {
|
|
709
664
|
return false;
|
|
710
665
|
}
|
|
711
|
-
if (!
|
|
666
|
+
if (!isValidEnvelopeTtlMs(v.staleWhileRevalidateMs, maxTtlMs)) {
|
|
712
667
|
return false;
|
|
713
668
|
}
|
|
714
|
-
if (!
|
|
669
|
+
if (!isValidEnvelopeTtlMs(v.staleIfErrorMs, maxTtlMs)) {
|
|
715
670
|
return false;
|
|
716
671
|
}
|
|
717
|
-
if (v.
|
|
672
|
+
if (v.freshTtlMs == null && (v.staleWhileRevalidateMs != null || v.staleIfErrorMs != null)) {
|
|
718
673
|
return false;
|
|
719
674
|
}
|
|
720
675
|
return true;
|
|
721
676
|
}
|
|
722
677
|
function createStoredValueEnvelope(options) {
|
|
723
678
|
const now = options.now ?? Date.now();
|
|
724
|
-
const
|
|
725
|
-
const
|
|
726
|
-
const
|
|
727
|
-
const freshUntil =
|
|
728
|
-
const staleUntil = freshUntil &&
|
|
729
|
-
const errorUntil = freshUntil &&
|
|
679
|
+
const freshTtlMs = normalizePositiveMs(options.freshTtlMs);
|
|
680
|
+
const staleWhileRevalidateMs = normalizePositiveMs(options.staleWhileRevalidateMs);
|
|
681
|
+
const staleIfErrorMs = normalizePositiveMs(options.staleIfErrorMs);
|
|
682
|
+
const freshUntil = freshTtlMs ? now + freshTtlMs : null;
|
|
683
|
+
const staleUntil = freshUntil && staleWhileRevalidateMs ? freshUntil + staleWhileRevalidateMs : null;
|
|
684
|
+
const errorUntil = freshUntil && staleIfErrorMs ? freshUntil + staleIfErrorMs : null;
|
|
730
685
|
return {
|
|
731
686
|
__layercache: 1,
|
|
732
687
|
kind: options.kind,
|
|
@@ -734,9 +689,9 @@ function createStoredValueEnvelope(options) {
|
|
|
734
689
|
freshUntil,
|
|
735
690
|
staleUntil,
|
|
736
691
|
errorUntil,
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
692
|
+
freshTtlMs: freshTtlMs ?? null,
|
|
693
|
+
staleWhileRevalidateMs: staleWhileRevalidateMs ?? null,
|
|
694
|
+
staleIfErrorMs: staleIfErrorMs ?? null
|
|
740
695
|
};
|
|
741
696
|
}
|
|
742
697
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -763,7 +718,7 @@ function unwrapStoredValue(stored) {
|
|
|
763
718
|
}
|
|
764
719
|
return stored.value ?? null;
|
|
765
720
|
}
|
|
766
|
-
function
|
|
721
|
+
function remainingStoredTtlMs(stored, now = Date.now()) {
|
|
767
722
|
if (!isStoredValueEnvelope(stored)) {
|
|
768
723
|
return void 0;
|
|
769
724
|
}
|
|
@@ -775,9 +730,9 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
775
730
|
if (remainingMs <= 0) {
|
|
776
731
|
return 1;
|
|
777
732
|
}
|
|
778
|
-
return Math.max(1, Math.ceil(remainingMs
|
|
733
|
+
return Math.max(1, Math.ceil(remainingMs));
|
|
779
734
|
}
|
|
780
|
-
function
|
|
735
|
+
function remainingFreshTtlMs(stored, now = Date.now()) {
|
|
781
736
|
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
782
737
|
return void 0;
|
|
783
738
|
}
|
|
@@ -785,7 +740,7 @@ function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
|
785
740
|
if (remainingMs <= 0) {
|
|
786
741
|
return 0;
|
|
787
742
|
}
|
|
788
|
-
return Math.max(1, Math.ceil(remainingMs
|
|
743
|
+
return Math.max(1, Math.ceil(remainingMs));
|
|
789
744
|
}
|
|
790
745
|
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
791
746
|
if (!isStoredValueEnvelope(stored)) {
|
|
@@ -794,12 +749,23 @@ function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
|
794
749
|
return createStoredValueEnvelope({
|
|
795
750
|
kind: stored.kind,
|
|
796
751
|
value: stored.value,
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
752
|
+
freshTtlMs: stored.freshTtlMs ?? void 0,
|
|
753
|
+
staleWhileRevalidateMs: stored.staleWhileRevalidateMs ?? void 0,
|
|
754
|
+
staleIfErrorMs: stored.staleIfErrorMs ?? void 0,
|
|
800
755
|
now
|
|
801
756
|
});
|
|
802
757
|
}
|
|
758
|
+
function expireStoredEnvelope(stored, now = Date.now()) {
|
|
759
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
760
|
+
return stored;
|
|
761
|
+
}
|
|
762
|
+
const futureDeadlines = [stored.staleUntil, stored.errorUntil].filter((value) => value !== null);
|
|
763
|
+
const freshUntil = futureDeadlines.length > 0 ? Math.min(now, ...futureDeadlines) : now;
|
|
764
|
+
return {
|
|
765
|
+
...stored,
|
|
766
|
+
freshUntil
|
|
767
|
+
};
|
|
768
|
+
}
|
|
803
769
|
function maxExpiry(stored) {
|
|
804
770
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
805
771
|
(value) => value !== null
|
|
@@ -809,19 +775,110 @@ function maxExpiry(stored) {
|
|
|
809
775
|
}
|
|
810
776
|
return Math.max(...values);
|
|
811
777
|
}
|
|
812
|
-
function
|
|
778
|
+
function normalizePositiveMs(value) {
|
|
813
779
|
if (!value || value <= 0) {
|
|
814
780
|
return void 0;
|
|
815
781
|
}
|
|
816
782
|
return value;
|
|
817
783
|
}
|
|
818
|
-
function
|
|
784
|
+
function isValidEnvelopeTtlMs(value, maxTtlMs) {
|
|
819
785
|
if (value == null) {
|
|
820
786
|
return true;
|
|
821
787
|
}
|
|
822
|
-
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <=
|
|
788
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlMs;
|
|
823
789
|
}
|
|
824
790
|
|
|
791
|
+
// src/internal/CacheStackInvalidationSupport.ts
|
|
792
|
+
var CacheStackInvalidationSupport = class {
|
|
793
|
+
constructor(options) {
|
|
794
|
+
this.options = options;
|
|
795
|
+
}
|
|
796
|
+
options;
|
|
797
|
+
async collectKeysForTag(tag, maxKeys) {
|
|
798
|
+
const keys = /* @__PURE__ */ new Set();
|
|
799
|
+
if (this.options.tagIndex.forEachKeyForTag) {
|
|
800
|
+
await this.options.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
801
|
+
keys.add(key);
|
|
802
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
803
|
+
});
|
|
804
|
+
return [...keys];
|
|
805
|
+
}
|
|
806
|
+
for (const key of await this.options.tagIndex.keysForTag(tag)) {
|
|
807
|
+
keys.add(key);
|
|
808
|
+
this.assertWithinInvalidationKeyLimit(keys.size, maxKeys);
|
|
809
|
+
}
|
|
810
|
+
return [...keys];
|
|
811
|
+
}
|
|
812
|
+
intersectKeys(groups) {
|
|
813
|
+
if (groups.length === 0) {
|
|
814
|
+
return [];
|
|
815
|
+
}
|
|
816
|
+
const [firstGroup, ...rest] = groups;
|
|
817
|
+
const restSets = rest.map((group) => new Set(group));
|
|
818
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
819
|
+
}
|
|
820
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
821
|
+
await Promise.all(
|
|
822
|
+
layers.map(async (layer) => {
|
|
823
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
if (layer.deleteMany) {
|
|
827
|
+
try {
|
|
828
|
+
await layer.deleteMany(keys);
|
|
829
|
+
} catch (error) {
|
|
830
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
831
|
+
}
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
await Promise.all(
|
|
835
|
+
keys.map(async (key) => {
|
|
836
|
+
try {
|
|
837
|
+
await layer.delete(key);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
await this.options.handleLayerFailure(layer, "delete", error);
|
|
840
|
+
}
|
|
841
|
+
})
|
|
842
|
+
);
|
|
843
|
+
})
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
async expireKeysInLayers(layers, keys) {
|
|
847
|
+
const foundKeys = /* @__PURE__ */ new Set();
|
|
848
|
+
await Promise.all(
|
|
849
|
+
layers.map(async (layer) => {
|
|
850
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
await Promise.all(
|
|
854
|
+
keys.map(async (key) => {
|
|
855
|
+
try {
|
|
856
|
+
const stored = layer.getEntry ? await layer.getEntry(key) : await layer.get(key);
|
|
857
|
+
if (stored === null) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
foundKeys.add(key);
|
|
861
|
+
const expired = expireStoredEnvelope(stored);
|
|
862
|
+
if (expired === stored) {
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
await layer.set(key, expired, remainingStoredTtlMs(expired));
|
|
866
|
+
} catch (error) {
|
|
867
|
+
await this.options.handleLayerFailure(layer, "expire", error);
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
})
|
|
872
|
+
);
|
|
873
|
+
return foundKeys;
|
|
874
|
+
}
|
|
875
|
+
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
876
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
877
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
|
|
825
882
|
// src/internal/CacheStackLayerWriter.ts
|
|
826
883
|
var CacheStackLayerWriter = class {
|
|
827
884
|
constructor(options) {
|
|
@@ -933,12 +990,12 @@ var CacheStackLayerWriter = class {
|
|
|
933
990
|
}
|
|
934
991
|
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
935
992
|
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
936
|
-
const staleWhileRevalidate = this.options.
|
|
993
|
+
const staleWhileRevalidate = this.options.resolveLayerMs(
|
|
937
994
|
layer.name,
|
|
938
995
|
writeOptions?.staleWhileRevalidate,
|
|
939
996
|
this.options.globalStaleWhileRevalidate
|
|
940
997
|
);
|
|
941
|
-
const staleIfError = this.options.
|
|
998
|
+
const staleIfError = this.options.resolveLayerMs(
|
|
942
999
|
layer.name,
|
|
943
1000
|
writeOptions?.staleIfError,
|
|
944
1001
|
this.options.globalStaleIfError
|
|
@@ -946,12 +1003,12 @@ var CacheStackLayerWriter = class {
|
|
|
946
1003
|
const payload = createStoredValueEnvelope({
|
|
947
1004
|
kind,
|
|
948
1005
|
value,
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1006
|
+
freshTtlMs: freshTtl,
|
|
1007
|
+
staleWhileRevalidateMs: staleWhileRevalidate,
|
|
1008
|
+
staleIfErrorMs: staleIfError,
|
|
952
1009
|
now
|
|
953
1010
|
});
|
|
954
|
-
const ttl =
|
|
1011
|
+
const ttl = remainingStoredTtlMs(payload, now) ?? freshTtl;
|
|
955
1012
|
return {
|
|
956
1013
|
key,
|
|
957
1014
|
value: payload,
|
|
@@ -1094,18 +1151,420 @@ function planFreshReadPolicies({
|
|
|
1094
1151
|
stored,
|
|
1095
1152
|
hasFetcher,
|
|
1096
1153
|
slidingTtl,
|
|
1097
|
-
|
|
1154
|
+
refreshAheadMs
|
|
1098
1155
|
}) {
|
|
1099
1156
|
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1100
|
-
const refreshedStoredTtl = refreshedStored ?
|
|
1101
|
-
const remainingFreshTtl =
|
|
1157
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlMs(refreshedStored) ?? void 0 : void 0;
|
|
1158
|
+
const remainingFreshTtl = remainingFreshTtlMs(stored) ?? 0;
|
|
1102
1159
|
return {
|
|
1103
1160
|
refreshedStored,
|
|
1104
1161
|
refreshedStoredTtl,
|
|
1105
|
-
shouldScheduleBackgroundRefresh: hasFetcher &&
|
|
1162
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadMs > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadMs
|
|
1106
1163
|
};
|
|
1107
1164
|
}
|
|
1108
1165
|
|
|
1166
|
+
// src/internal/CacheStackReader.ts
|
|
1167
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1168
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1169
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1170
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1171
|
+
var CacheStackReader = class {
|
|
1172
|
+
constructor(options) {
|
|
1173
|
+
this.options = options;
|
|
1174
|
+
}
|
|
1175
|
+
options;
|
|
1176
|
+
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1177
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
1178
|
+
get activeRefreshCount() {
|
|
1179
|
+
return this.backgroundRefreshes.size;
|
|
1180
|
+
}
|
|
1181
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
1182
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1183
|
+
if (hit.found) {
|
|
1184
|
+
this.options.ttlResolver.recordAccess(normalizedKey);
|
|
1185
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
1186
|
+
this.options.metricsCollector.increment("negativeCacheHits");
|
|
1187
|
+
}
|
|
1188
|
+
if (hit.state === "fresh") {
|
|
1189
|
+
this.options.metricsCollector.increment("hits");
|
|
1190
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
1191
|
+
return hit.value;
|
|
1192
|
+
}
|
|
1193
|
+
if (hit.state === "stale-while-revalidate") {
|
|
1194
|
+
this.options.metricsCollector.increment("hits");
|
|
1195
|
+
this.options.metricsCollector.increment("staleHits");
|
|
1196
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
1197
|
+
if (fetcher) {
|
|
1198
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
|
|
1199
|
+
}
|
|
1200
|
+
return hit.value;
|
|
1201
|
+
}
|
|
1202
|
+
if (!fetcher) {
|
|
1203
|
+
this.options.metricsCollector.increment("hits");
|
|
1204
|
+
this.options.metricsCollector.increment("staleHits");
|
|
1205
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
1206
|
+
return hit.value;
|
|
1207
|
+
}
|
|
1208
|
+
try {
|
|
1209
|
+
return await this.fetchWithGuards(
|
|
1210
|
+
normalizedKey,
|
|
1211
|
+
fetcher,
|
|
1212
|
+
options,
|
|
1213
|
+
void 0,
|
|
1214
|
+
void 0,
|
|
1215
|
+
false,
|
|
1216
|
+
this.createFetcherContext(normalizedKey, hit)
|
|
1217
|
+
);
|
|
1218
|
+
} catch (error) {
|
|
1219
|
+
this.options.metricsCollector.increment("staleHits");
|
|
1220
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
1221
|
+
this.options.logger.debug?.("stale-if-error", {
|
|
1222
|
+
key: normalizedKey,
|
|
1223
|
+
error: this.options.formatError(error)
|
|
1224
|
+
});
|
|
1225
|
+
return hit.value;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
this.options.metricsCollector.increment("misses");
|
|
1229
|
+
if (!fetcher) {
|
|
1230
|
+
return null;
|
|
1231
|
+
}
|
|
1232
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
|
|
1233
|
+
key: normalizedKey,
|
|
1234
|
+
currentValue: void 0,
|
|
1235
|
+
state: "miss"
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
async readLayerEntry(layer, key) {
|
|
1239
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
1240
|
+
return null;
|
|
1241
|
+
}
|
|
1242
|
+
if (layer.getEntry) {
|
|
1243
|
+
try {
|
|
1244
|
+
return await layer.getEntry(key);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
try {
|
|
1250
|
+
return await layer.get(key);
|
|
1251
|
+
} catch (error) {
|
|
1252
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
async backfill(key, stored, upToIndex, options) {
|
|
1256
|
+
if (upToIndex < 0) {
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1260
|
+
const layer = this.options.layers[index];
|
|
1261
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1265
|
+
try {
|
|
1266
|
+
await layer.set(key, stored, ttl);
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1271
|
+
this.options.metricsCollector.increment("backfills");
|
|
1272
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1273
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
abortAllRefreshes() {
|
|
1277
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
1278
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
getAllRefreshPromises() {
|
|
1282
|
+
return [...this.backgroundRefreshes.values()];
|
|
1283
|
+
}
|
|
1284
|
+
async readFromLayers(key, options, mode) {
|
|
1285
|
+
let sawRetainableValue = false;
|
|
1286
|
+
for (let index = 0; index < this.options.layers.length; index += 1) {
|
|
1287
|
+
const layer = this.options.layers[index];
|
|
1288
|
+
if (!layer) continue;
|
|
1289
|
+
const readStart = performance.now();
|
|
1290
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
1291
|
+
const readDuration = performance.now() - readStart;
|
|
1292
|
+
this.options.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1293
|
+
if (stored === null) {
|
|
1294
|
+
this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
const resolved = resolveStoredValue(stored);
|
|
1298
|
+
if (resolved.state === "expired") {
|
|
1299
|
+
await layer.delete(key);
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
sawRetainableValue = true;
|
|
1303
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
await this.options.tagIndex.touch(key);
|
|
1307
|
+
await this.backfill(key, stored, index - 1, options);
|
|
1308
|
+
this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1309
|
+
this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1310
|
+
this.options.emit("hit", {
|
|
1311
|
+
key,
|
|
1312
|
+
layer: layer.name,
|
|
1313
|
+
state: resolved.state
|
|
1314
|
+
});
|
|
1315
|
+
return {
|
|
1316
|
+
found: true,
|
|
1317
|
+
value: resolved.value,
|
|
1318
|
+
stored,
|
|
1319
|
+
state: resolved.state,
|
|
1320
|
+
layerIndex: index,
|
|
1321
|
+
layerName: layer.name
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
if (!sawRetainableValue) {
|
|
1325
|
+
await this.options.tagIndex.remove(key);
|
|
1326
|
+
}
|
|
1327
|
+
this.options.logger.debug?.("miss", { key, mode });
|
|
1328
|
+
this.options.emit("miss", { key, mode });
|
|
1329
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
1330
|
+
}
|
|
1331
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
|
|
1332
|
+
key,
|
|
1333
|
+
currentValue: void 0,
|
|
1334
|
+
state: "miss"
|
|
1335
|
+
}) {
|
|
1336
|
+
const fetchTask = async () => {
|
|
1337
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
1338
|
+
if (shouldRecheckFreshLayers) {
|
|
1339
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1340
|
+
if (secondHit.found) {
|
|
1341
|
+
this.options.metricsCollector.increment("hits");
|
|
1342
|
+
return secondHit.value;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1346
|
+
};
|
|
1347
|
+
const singleFlightTask = async () => {
|
|
1348
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1349
|
+
return fetchTask();
|
|
1350
|
+
}
|
|
1351
|
+
try {
|
|
1352
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
1353
|
+
key,
|
|
1354
|
+
this.resolveSingleFlightOptions(),
|
|
1355
|
+
fetchTask,
|
|
1356
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1357
|
+
);
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
if (!this.options.isGracefulDegradationEnabled()) {
|
|
1360
|
+
throw error;
|
|
1361
|
+
}
|
|
1362
|
+
this.options.metricsCollector.increment("degradedOperations");
|
|
1363
|
+
this.options.logger.warn?.("single-flight-coordinator-degraded", {
|
|
1364
|
+
key,
|
|
1365
|
+
error: this.options.formatError(error)
|
|
1366
|
+
});
|
|
1367
|
+
this.options.emitError("single-flight", {
|
|
1368
|
+
key,
|
|
1369
|
+
degraded: true,
|
|
1370
|
+
error: this.options.formatError(error)
|
|
1371
|
+
});
|
|
1372
|
+
return fetchTask();
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
if (this.options.stampedePrevention === false) {
|
|
1376
|
+
return singleFlightTask();
|
|
1377
|
+
}
|
|
1378
|
+
return this.options.stampedeGuard.execute(key, singleFlightTask);
|
|
1379
|
+
}
|
|
1380
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1381
|
+
key,
|
|
1382
|
+
currentValue: void 0,
|
|
1383
|
+
state: "miss"
|
|
1384
|
+
}) {
|
|
1385
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1386
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1387
|
+
const deadline = Date.now() + timeoutMs;
|
|
1388
|
+
this.options.metricsCollector.increment("singleFlightWaits");
|
|
1389
|
+
this.options.emit("stampede-dedupe", { key });
|
|
1390
|
+
while (Date.now() < deadline) {
|
|
1391
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
1392
|
+
if (hit.found) {
|
|
1393
|
+
this.options.metricsCollector.increment("hits");
|
|
1394
|
+
return hit.value;
|
|
1395
|
+
}
|
|
1396
|
+
await this.options.sleep(pollIntervalMs);
|
|
1397
|
+
}
|
|
1398
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1399
|
+
}
|
|
1400
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1401
|
+
key,
|
|
1402
|
+
currentValue: void 0,
|
|
1403
|
+
state: "miss"
|
|
1404
|
+
}) {
|
|
1405
|
+
this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1406
|
+
this.options.metricsCollector.increment("fetches");
|
|
1407
|
+
const fetchStart = Date.now();
|
|
1408
|
+
let fetched;
|
|
1409
|
+
try {
|
|
1410
|
+
fetched = await this.options.fetchRateLimiter.schedule(
|
|
1411
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1412
|
+
{ key, fetcher },
|
|
1413
|
+
() => fetcher(fetcherContext)
|
|
1414
|
+
);
|
|
1415
|
+
this.options.circuitBreakerManager.recordSuccess(key);
|
|
1416
|
+
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1419
|
+
throw error;
|
|
1420
|
+
}
|
|
1421
|
+
if (fetched === null || fetched === void 0) {
|
|
1422
|
+
if (!this.shouldNegativeCache(options)) {
|
|
1423
|
+
return null;
|
|
1424
|
+
}
|
|
1425
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1426
|
+
this.options.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
1427
|
+
key,
|
|
1428
|
+
expectedClearEpoch,
|
|
1429
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1430
|
+
expectedKeyEpoch,
|
|
1431
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1432
|
+
});
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
await this.options.storeEntry(key, "empty", null, options);
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
if (options?.shouldCache) {
|
|
1439
|
+
try {
|
|
1440
|
+
if (!options.shouldCache(fetched)) {
|
|
1441
|
+
return fetched;
|
|
1442
|
+
}
|
|
1443
|
+
} catch (error) {
|
|
1444
|
+
this.options.logger.warn?.("shouldCache-error", {
|
|
1445
|
+
key,
|
|
1446
|
+
error: this.options.formatError(error)
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1451
|
+
this.options.logger.debug?.("skip-store-after-invalidation", {
|
|
1452
|
+
key,
|
|
1453
|
+
expectedClearEpoch,
|
|
1454
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1455
|
+
expectedKeyEpoch,
|
|
1456
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1457
|
+
});
|
|
1458
|
+
return fetched;
|
|
1459
|
+
}
|
|
1460
|
+
await this.options.storeEntry(key, "value", fetched, options);
|
|
1461
|
+
return fetched;
|
|
1462
|
+
}
|
|
1463
|
+
runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
1464
|
+
this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
1465
|
+
}
|
|
1466
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
|
|
1467
|
+
key,
|
|
1468
|
+
currentValue: void 0,
|
|
1469
|
+
state: "miss"
|
|
1470
|
+
}) {
|
|
1471
|
+
if (!shouldStartBackgroundRefresh({
|
|
1472
|
+
isDisconnecting: this.options.isDisconnecting(),
|
|
1473
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
1474
|
+
})) {
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1478
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
1479
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
1480
|
+
const refresh = (async () => {
|
|
1481
|
+
this.options.metricsCollector.increment("refreshes");
|
|
1482
|
+
try {
|
|
1483
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1484
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1487
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
1488
|
+
this.options.logger.warn?.("background-refresh-error", {
|
|
1489
|
+
key,
|
|
1490
|
+
error: this.options.formatError(error)
|
|
1491
|
+
});
|
|
1492
|
+
} finally {
|
|
1493
|
+
this.backgroundRefreshes.delete(key);
|
|
1494
|
+
this.backgroundRefreshAbort.delete(key);
|
|
1495
|
+
}
|
|
1496
|
+
})();
|
|
1497
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
1498
|
+
}
|
|
1499
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1500
|
+
key,
|
|
1501
|
+
currentValue: void 0,
|
|
1502
|
+
state: "miss"
|
|
1503
|
+
}) {
|
|
1504
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
1505
|
+
await this.fetchWithGuards(
|
|
1506
|
+
key,
|
|
1507
|
+
(context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
|
|
1508
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
1509
|
+
}),
|
|
1510
|
+
options,
|
|
1511
|
+
expectedClearEpoch,
|
|
1512
|
+
expectedKeyEpoch,
|
|
1513
|
+
false,
|
|
1514
|
+
fetcherContext
|
|
1515
|
+
);
|
|
1516
|
+
}
|
|
1517
|
+
async runApplyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1518
|
+
return this.applyFreshReadPolicies(key, hit, options, fetcher);
|
|
1519
|
+
}
|
|
1520
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1521
|
+
const plan = planFreshReadPolicies({
|
|
1522
|
+
stored: hit.stored,
|
|
1523
|
+
hasFetcher: Boolean(fetcher),
|
|
1524
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
1525
|
+
refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
1526
|
+
});
|
|
1527
|
+
if (plan.refreshedStored) {
|
|
1528
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1529
|
+
const layer = this.options.layers[index];
|
|
1530
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
try {
|
|
1534
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
1535
|
+
} catch (error) {
|
|
1536
|
+
await this.options.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
1541
|
+
this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
createFetcherContext(key, hit) {
|
|
1545
|
+
return {
|
|
1546
|
+
key,
|
|
1547
|
+
currentValue: hit.value === null ? void 0 : hit.value,
|
|
1548
|
+
state: hit.state,
|
|
1549
|
+
layer: hit.layerName
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
resolveSingleFlightOptions() {
|
|
1553
|
+
return {
|
|
1554
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1555
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1556
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
1557
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
shouldNegativeCache(options) {
|
|
1561
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1562
|
+
}
|
|
1563
|
+
isNegativeStoredValue(stored) {
|
|
1564
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1565
|
+
}
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1109
1568
|
// src/internal/CacheStackSnapshotManager.ts
|
|
1110
1569
|
var import_node_fs = require("fs");
|
|
1111
1570
|
|
|
@@ -1371,7 +1830,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1371
1830
|
await visitor({
|
|
1372
1831
|
key: exportedKey,
|
|
1373
1832
|
value: stored,
|
|
1374
|
-
ttl:
|
|
1833
|
+
ttl: remainingStoredTtlMs(stored)
|
|
1375
1834
|
});
|
|
1376
1835
|
};
|
|
1377
1836
|
if (layer.forEachKey) {
|
|
@@ -1527,6 +1986,19 @@ function validateCircuitBreakerOptions(options) {
|
|
|
1527
1986
|
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1528
1987
|
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1529
1988
|
}
|
|
1989
|
+
function validateContextEntryOptions(name, options) {
|
|
1990
|
+
if (!options) {
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
validateLayerNumberOption(`${name}.ttl`, options.ttl);
|
|
1994
|
+
validateLayerNumberOption(`${name}.negativeTtl`, options.negativeTtl);
|
|
1995
|
+
validateLayerNumberOption(`${name}.staleWhileRevalidate`, options.staleWhileRevalidate);
|
|
1996
|
+
validateLayerNumberOption(`${name}.staleIfError`, options.staleIfError);
|
|
1997
|
+
validateLayerNumberOption(`${name}.ttlJitter`, options.ttlJitter);
|
|
1998
|
+
validateTtlPolicy(`${name}.ttlPolicy`, options.ttlPolicy);
|
|
1999
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
2000
|
+
validateTags(options.tags);
|
|
2001
|
+
}
|
|
1530
2002
|
|
|
1531
2003
|
// src/internal/CircuitBreakerManager.ts
|
|
1532
2004
|
var CircuitBreakerManager = class {
|
|
@@ -1959,7 +2431,7 @@ var MetricsCollector = class {
|
|
|
1959
2431
|
|
|
1960
2432
|
// src/internal/TtlResolver.ts
|
|
1961
2433
|
var import_node_crypto2 = require("crypto");
|
|
1962
|
-
var
|
|
2434
|
+
var DEFAULT_NEGATIVE_TTL_MS = 6e4;
|
|
1963
2435
|
var secureRandom = {
|
|
1964
2436
|
value() {
|
|
1965
2437
|
return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
|
|
@@ -1986,17 +2458,17 @@ var TtlResolver = class {
|
|
|
1986
2458
|
}
|
|
1987
2459
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
1988
2460
|
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
1989
|
-
const baseTtl = kind === "empty" ? this.
|
|
2461
|
+
const baseTtl = kind === "empty" ? this.resolveLayerMs(
|
|
1990
2462
|
layerName,
|
|
1991
2463
|
options?.negativeTtl,
|
|
1992
2464
|
globalNegativeTtl,
|
|
1993
|
-
this.
|
|
1994
|
-
) : this.
|
|
2465
|
+
this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_MS
|
|
2466
|
+
) : this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
1995
2467
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
1996
|
-
const jitter = this.
|
|
2468
|
+
const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
|
|
1997
2469
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
1998
2470
|
}
|
|
1999
|
-
|
|
2471
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
2000
2472
|
if (override !== void 0) {
|
|
2001
2473
|
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
2002
2474
|
}
|
|
@@ -2018,8 +2490,8 @@ var TtlResolver = class {
|
|
|
2018
2490
|
if (profile.hits < hotAfter) {
|
|
2019
2491
|
return ttl;
|
|
2020
2492
|
}
|
|
2021
|
-
const step = this.
|
|
2022
|
-
const maxTtl = this.
|
|
2493
|
+
const step = this.resolveLayerMs(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
2494
|
+
const maxTtl = this.resolveLayerMs(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
2023
2495
|
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
2024
2496
|
return Math.min(maxTtl, ttl + step * multiplier);
|
|
2025
2497
|
}
|
|
@@ -2041,17 +2513,17 @@ var TtlResolver = class {
|
|
|
2041
2513
|
if (policy === "until-midnight") {
|
|
2042
2514
|
const nextMidnight = new Date(now);
|
|
2043
2515
|
nextMidnight.setHours(24, 0, 0, 0);
|
|
2044
|
-
return Math.max(1, Math.ceil(
|
|
2516
|
+
return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
|
|
2045
2517
|
}
|
|
2046
2518
|
if (policy === "next-hour") {
|
|
2047
2519
|
const nextHour = new Date(now);
|
|
2048
2520
|
nextHour.setMinutes(60, 0, 0);
|
|
2049
|
-
return Math.max(1, Math.ceil(
|
|
2521
|
+
return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
|
|
2050
2522
|
}
|
|
2051
|
-
const
|
|
2052
|
-
const
|
|
2053
|
-
const nextBoundary = Math.ceil((
|
|
2054
|
-
return Math.max(1, nextBoundary -
|
|
2523
|
+
const alignToMs = policy.alignTo;
|
|
2524
|
+
const currentMs = Date.now();
|
|
2525
|
+
const nextBoundary = Math.ceil((currentMs + 1) / alignToMs) * alignToMs;
|
|
2526
|
+
return Math.max(1, nextBoundary - currentMs);
|
|
2055
2527
|
}
|
|
2056
2528
|
readLayerNumber(layerName, value) {
|
|
2057
2529
|
if (typeof value === "number") {
|
|
@@ -2423,10 +2895,6 @@ var CacheMissError = class extends Error {
|
|
|
2423
2895
|
};
|
|
2424
2896
|
|
|
2425
2897
|
// src/CacheStack.ts
|
|
2426
|
-
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
2427
|
-
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
2428
|
-
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
2429
|
-
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
2430
2898
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2431
2899
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
2432
2900
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
@@ -2506,7 +2974,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2506
2974
|
},
|
|
2507
2975
|
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2508
2976
|
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2509
|
-
|
|
2977
|
+
resolveLayerMs: this.resolveLayerMs.bind(this),
|
|
2510
2978
|
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2511
2979
|
globalStaleIfError: this.options.staleIfError,
|
|
2512
2980
|
writePolicy: this.options.writePolicy,
|
|
@@ -2537,7 +3005,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2537
3005
|
layers: this.layers,
|
|
2538
3006
|
tagIndex: this.tagIndex,
|
|
2539
3007
|
snapshotSerializer: this.snapshotSerializer,
|
|
2540
|
-
readLayerEntry: this.readLayerEntry
|
|
3008
|
+
readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
|
|
2541
3009
|
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2542
3010
|
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2543
3011
|
qualifyKey: this.qualifyKey.bind(this),
|
|
@@ -2545,6 +3013,41 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2545
3013
|
validateCacheKey,
|
|
2546
3014
|
formatError: this.formatError.bind(this)
|
|
2547
3015
|
});
|
|
3016
|
+
this.reader = new CacheStackReader({
|
|
3017
|
+
layers: this.layers,
|
|
3018
|
+
metricsCollector: this.metricsCollector,
|
|
3019
|
+
maintenance: this.maintenance,
|
|
3020
|
+
tagIndex: this.tagIndex,
|
|
3021
|
+
circuitBreakerManager: this.circuitBreakerManager,
|
|
3022
|
+
fetchRateLimiter: this.fetchRateLimiter,
|
|
3023
|
+
stampedeGuard: this.stampedeGuard,
|
|
3024
|
+
ttlResolver: this.ttlResolver,
|
|
3025
|
+
logger: this.logger,
|
|
3026
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
3027
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
3028
|
+
emit: (event, data) => this.emit(event, data),
|
|
3029
|
+
emitError: (operation, context) => this.emitError(operation, context),
|
|
3030
|
+
formatError: (error) => this.formatError(error),
|
|
3031
|
+
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
3032
|
+
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
3033
|
+
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
3034
|
+
sleep: (ms) => this.sleep(ms),
|
|
3035
|
+
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
3036
|
+
isDisconnecting: () => this.isDisconnecting,
|
|
3037
|
+
isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
|
|
3038
|
+
scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
|
|
3039
|
+
stampedePrevention: options.stampedePrevention,
|
|
3040
|
+
singleFlightCoordinator: options.singleFlightCoordinator,
|
|
3041
|
+
singleFlightLeaseMs: options.singleFlightLeaseMs,
|
|
3042
|
+
singleFlightTimeoutMs: options.singleFlightTimeoutMs,
|
|
3043
|
+
singleFlightPollMs: options.singleFlightPollMs,
|
|
3044
|
+
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
3045
|
+
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
3046
|
+
negativeCaching: options.negativeCaching,
|
|
3047
|
+
refreshAhead: options.refreshAhead,
|
|
3048
|
+
circuitBreaker: options.circuitBreaker,
|
|
3049
|
+
fetcherRateLimit: options.fetcherRateLimit
|
|
3050
|
+
});
|
|
2548
3051
|
this.initializeWriteBehind(options.writeBehind);
|
|
2549
3052
|
this.startup = this.initialize();
|
|
2550
3053
|
}
|
|
@@ -2563,8 +3066,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2563
3066
|
invalidation;
|
|
2564
3067
|
layerWriter;
|
|
2565
3068
|
snapshots;
|
|
2566
|
-
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2567
|
-
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2568
3069
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2569
3070
|
maintenance = new CacheStackMaintenance();
|
|
2570
3071
|
ttlResolver;
|
|
@@ -2572,6 +3073,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2572
3073
|
nextOperationId = 0;
|
|
2573
3074
|
currentGeneration;
|
|
2574
3075
|
isDisconnecting = false;
|
|
3076
|
+
reader;
|
|
2575
3077
|
disconnectPromise;
|
|
2576
3078
|
/**
|
|
2577
3079
|
* Read-through cache get.
|
|
@@ -2584,51 +3086,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2584
3086
|
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2585
3087
|
this.validateWriteOptions(options);
|
|
2586
3088
|
await this.awaitStartup("get");
|
|
2587
|
-
return this.getPrepared(normalizedKey, fetcher, options);
|
|
3089
|
+
return this.reader.getPrepared(normalizedKey, fetcher, options);
|
|
2588
3090
|
});
|
|
2589
3091
|
}
|
|
2590
|
-
async getPrepared(normalizedKey, fetcher, options) {
|
|
2591
|
-
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
2592
|
-
if (hit.found) {
|
|
2593
|
-
this.ttlResolver.recordAccess(normalizedKey);
|
|
2594
|
-
if (this.isNegativeStoredValue(hit.stored)) {
|
|
2595
|
-
this.metricsCollector.increment("negativeCacheHits");
|
|
2596
|
-
}
|
|
2597
|
-
if (hit.state === "fresh") {
|
|
2598
|
-
this.metricsCollector.increment("hits");
|
|
2599
|
-
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
2600
|
-
return hit.value;
|
|
2601
|
-
}
|
|
2602
|
-
if (hit.state === "stale-while-revalidate") {
|
|
2603
|
-
this.metricsCollector.increment("hits");
|
|
2604
|
-
this.metricsCollector.increment("staleHits");
|
|
2605
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
2606
|
-
if (fetcher) {
|
|
2607
|
-
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
2608
|
-
}
|
|
2609
|
-
return hit.value;
|
|
2610
|
-
}
|
|
2611
|
-
if (!fetcher) {
|
|
2612
|
-
this.metricsCollector.increment("hits");
|
|
2613
|
-
this.metricsCollector.increment("staleHits");
|
|
2614
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
2615
|
-
return hit.value;
|
|
2616
|
-
}
|
|
2617
|
-
try {
|
|
2618
|
-
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2619
|
-
} catch (error) {
|
|
2620
|
-
this.metricsCollector.increment("staleHits");
|
|
2621
|
-
this.metricsCollector.increment("refreshErrors");
|
|
2622
|
-
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
2623
|
-
return hit.value;
|
|
2624
|
-
}
|
|
2625
|
-
}
|
|
2626
|
-
this.metricsCollector.increment("misses");
|
|
2627
|
-
if (!fetcher) {
|
|
2628
|
-
return null;
|
|
2629
|
-
}
|
|
2630
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2631
|
-
}
|
|
2632
3092
|
/**
|
|
2633
3093
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
2634
3094
|
* Fetches and caches the value if not already present.
|
|
@@ -2681,7 +3141,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2681
3141
|
return false;
|
|
2682
3142
|
}
|
|
2683
3143
|
/**
|
|
2684
|
-
* Returns the remaining TTL in
|
|
3144
|
+
* Returns the remaining TTL in milliseconds for the key in the fastest layer
|
|
2685
3145
|
* that has it, or null if the key is not found / has no TTL.
|
|
2686
3146
|
*/
|
|
2687
3147
|
async ttl(key) {
|
|
@@ -2779,7 +3239,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2779
3239
|
const optionsSignature = serializeOptions(entry.options);
|
|
2780
3240
|
const existing = pendingReads.get(entry.key);
|
|
2781
3241
|
if (!existing) {
|
|
2782
|
-
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
3242
|
+
const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2783
3243
|
pendingReads.set(entry.key, {
|
|
2784
3244
|
promise,
|
|
2785
3245
|
fetch: entry.fetch,
|
|
@@ -2815,7 +3275,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2815
3275
|
if (keys.length === 0) {
|
|
2816
3276
|
break;
|
|
2817
3277
|
}
|
|
2818
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
3278
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
|
|
2819
3279
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2820
3280
|
const key = keys[offset];
|
|
2821
3281
|
const stored = values[offset];
|
|
@@ -2831,7 +3291,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2831
3291
|
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2832
3292
|
}
|
|
2833
3293
|
await this.tagIndex.touch(key);
|
|
2834
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
3294
|
+
await this.reader.backfill(key, stored, layerIndex - 1);
|
|
2835
3295
|
resultsByKey.set(key, resolved.value);
|
|
2836
3296
|
pending.delete(key);
|
|
2837
3297
|
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
@@ -2917,20 +3377,45 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2917
3377
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2918
3378
|
});
|
|
2919
3379
|
}
|
|
2920
|
-
async
|
|
2921
|
-
await this.observeOperation("layercache.
|
|
3380
|
+
async expireByTag(tag) {
|
|
3381
|
+
await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
|
|
3382
|
+
validateTag(tag);
|
|
3383
|
+
await this.awaitStartup("expireByTag");
|
|
3384
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
3385
|
+
await this.expireKeys(keys);
|
|
3386
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
async invalidateByTags(tags, mode = "any") {
|
|
3390
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
3391
|
+
if (tags.length === 0) {
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
validateTags(tags);
|
|
3395
|
+
await this.awaitStartup("invalidateByTags");
|
|
3396
|
+
const keysByTag = await Promise.all(
|
|
3397
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
3398
|
+
);
|
|
3399
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
3400
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
3401
|
+
await this.deleteKeys(keys);
|
|
3402
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
async expireByTags(tags, mode = "any") {
|
|
3406
|
+
await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
|
|
2922
3407
|
if (tags.length === 0) {
|
|
2923
3408
|
return;
|
|
2924
3409
|
}
|
|
2925
3410
|
validateTags(tags);
|
|
2926
|
-
await this.awaitStartup("
|
|
3411
|
+
await this.awaitStartup("expireByTags");
|
|
2927
3412
|
const keysByTag = await Promise.all(
|
|
2928
3413
|
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2929
3414
|
);
|
|
2930
3415
|
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2931
3416
|
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2932
|
-
await this.
|
|
2933
|
-
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "
|
|
3417
|
+
await this.expireKeys(keys);
|
|
3418
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2934
3419
|
});
|
|
2935
3420
|
}
|
|
2936
3421
|
async invalidateByPattern(pattern) {
|
|
@@ -2945,6 +3430,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2945
3430
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2946
3431
|
});
|
|
2947
3432
|
}
|
|
3433
|
+
async expireByPattern(pattern) {
|
|
3434
|
+
await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
|
|
3435
|
+
validatePattern(pattern);
|
|
3436
|
+
await this.awaitStartup("expireByPattern");
|
|
3437
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
3438
|
+
this.qualifyPattern(pattern),
|
|
3439
|
+
this.invalidationMaxKeys()
|
|
3440
|
+
);
|
|
3441
|
+
await this.expireKeys(keys);
|
|
3442
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3443
|
+
});
|
|
3444
|
+
}
|
|
2948
3445
|
async invalidateByPrefix(prefix) {
|
|
2949
3446
|
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2950
3447
|
await this.awaitStartup("invalidateByPrefix");
|
|
@@ -2954,6 +3451,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2954
3451
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2955
3452
|
});
|
|
2956
3453
|
}
|
|
3454
|
+
async expireByPrefix(prefix) {
|
|
3455
|
+
await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
|
|
3456
|
+
await this.awaitStartup("expireByPrefix");
|
|
3457
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
3458
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
3459
|
+
await this.expireKeys(keys);
|
|
3460
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3461
|
+
});
|
|
3462
|
+
}
|
|
2957
3463
|
getMetrics() {
|
|
2958
3464
|
return this.metricsCollector.snapshot;
|
|
2959
3465
|
}
|
|
@@ -2965,7 +3471,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2965
3471
|
isLocal: Boolean(layer.isLocal),
|
|
2966
3472
|
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
2967
3473
|
})),
|
|
2968
|
-
backgroundRefreshes: this.
|
|
3474
|
+
backgroundRefreshes: this.reader.activeRefreshCount
|
|
2969
3475
|
};
|
|
2970
3476
|
}
|
|
2971
3477
|
resetMetrics() {
|
|
@@ -3030,9 +3536,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3030
3536
|
const normalizedKey = this.qualifyKey(userKey);
|
|
3031
3537
|
await this.awaitStartup("inspect");
|
|
3032
3538
|
const foundInLayers = [];
|
|
3033
|
-
let
|
|
3034
|
-
let
|
|
3035
|
-
let
|
|
3539
|
+
let freshTtlMs = null;
|
|
3540
|
+
let staleTtlMs = null;
|
|
3541
|
+
let errorTtlMs = null;
|
|
3036
3542
|
let isStale = false;
|
|
3037
3543
|
for (const layer of this.layers) {
|
|
3038
3544
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -3049,9 +3555,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3049
3555
|
foundInLayers.push(layer.name);
|
|
3050
3556
|
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
3051
3557
|
const now = Date.now();
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3558
|
+
freshTtlMs = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.freshUntil - now)) : null;
|
|
3559
|
+
staleTtlMs = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.staleUntil - now)) : null;
|
|
3560
|
+
errorTtlMs = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.errorUntil - now)) : null;
|
|
3055
3561
|
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
3056
3562
|
}
|
|
3057
3563
|
}
|
|
@@ -3059,7 +3565,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3059
3565
|
return null;
|
|
3060
3566
|
}
|
|
3061
3567
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
3062
|
-
return { key: userKey, foundInLayers,
|
|
3568
|
+
return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
|
|
3063
3569
|
}
|
|
3064
3570
|
async exportState() {
|
|
3065
3571
|
await this.awaitStartup("exportState");
|
|
@@ -3085,11 +3591,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3085
3591
|
await this.unsubscribeInvalidation?.();
|
|
3086
3592
|
await this.flushWriteBehindQueue();
|
|
3087
3593
|
await this.maintenance.waitForGenerationCleanup();
|
|
3088
|
-
|
|
3089
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
3090
|
-
}
|
|
3594
|
+
this.reader.abortAllRefreshes();
|
|
3091
3595
|
await Promise.allSettled(
|
|
3092
|
-
|
|
3596
|
+
this.reader.getAllRefreshPromises().map((promise) => {
|
|
3093
3597
|
let timer;
|
|
3094
3598
|
return Promise.race([
|
|
3095
3599
|
promise,
|
|
@@ -3102,8 +3606,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3102
3606
|
});
|
|
3103
3607
|
})
|
|
3104
3608
|
);
|
|
3105
|
-
this.backgroundRefreshes.clear();
|
|
3106
|
-
this.backgroundRefreshAbort.clear();
|
|
3107
3609
|
this.maintenance.disposeWriteBehindTimer();
|
|
3108
3610
|
this.fetchRateLimiter.dispose();
|
|
3109
3611
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -3119,141 +3621,36 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3119
3621
|
await this.handleInvalidationMessage(message);
|
|
3120
3622
|
});
|
|
3121
3623
|
}
|
|
3122
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
3123
|
-
const fetchTask = async () => {
|
|
3124
|
-
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
3125
|
-
if (shouldRecheckFreshLayers) {
|
|
3126
|
-
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
3127
|
-
if (secondHit.found) {
|
|
3128
|
-
this.metricsCollector.increment("hits");
|
|
3129
|
-
return secondHit.value;
|
|
3130
|
-
}
|
|
3131
|
-
}
|
|
3132
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
3133
|
-
};
|
|
3134
|
-
const singleFlightTask = async () => {
|
|
3135
|
-
if (!this.options.singleFlightCoordinator) {
|
|
3136
|
-
return fetchTask();
|
|
3137
|
-
}
|
|
3138
|
-
try {
|
|
3139
|
-
return await this.options.singleFlightCoordinator.execute(
|
|
3140
|
-
key,
|
|
3141
|
-
this.resolveSingleFlightOptions(),
|
|
3142
|
-
fetchTask,
|
|
3143
|
-
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
3144
|
-
);
|
|
3145
|
-
} catch (error) {
|
|
3146
|
-
if (!this.isGracefulDegradationEnabled()) {
|
|
3147
|
-
throw error;
|
|
3148
|
-
}
|
|
3149
|
-
this.metricsCollector.increment("degradedOperations");
|
|
3150
|
-
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
3151
|
-
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
3152
|
-
return fetchTask();
|
|
3153
|
-
}
|
|
3154
|
-
};
|
|
3155
|
-
if (this.options.stampedePrevention === false) {
|
|
3156
|
-
return singleFlightTask();
|
|
3157
|
-
}
|
|
3158
|
-
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
3159
|
-
}
|
|
3160
|
-
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3161
|
-
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
3162
|
-
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
3163
|
-
const deadline = Date.now() + timeoutMs;
|
|
3164
|
-
this.metricsCollector.increment("singleFlightWaits");
|
|
3165
|
-
this.emit("stampede-dedupe", { key });
|
|
3166
|
-
while (Date.now() < deadline) {
|
|
3167
|
-
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
3168
|
-
if (hit.found) {
|
|
3169
|
-
this.metricsCollector.increment("hits");
|
|
3170
|
-
return hit.value;
|
|
3171
|
-
}
|
|
3172
|
-
await this.sleep(pollIntervalMs);
|
|
3173
|
-
}
|
|
3174
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
3175
|
-
}
|
|
3176
|
-
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3177
|
-
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
3178
|
-
this.metricsCollector.increment("fetches");
|
|
3179
|
-
const fetchStart = Date.now();
|
|
3180
|
-
let fetched;
|
|
3181
|
-
try {
|
|
3182
|
-
fetched = await this.fetchRateLimiter.schedule(
|
|
3183
|
-
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
3184
|
-
{ key, fetcher },
|
|
3185
|
-
fetcher
|
|
3186
|
-
);
|
|
3187
|
-
this.circuitBreakerManager.recordSuccess(key);
|
|
3188
|
-
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
3189
|
-
} catch (error) {
|
|
3190
|
-
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
3191
|
-
throw error;
|
|
3192
|
-
}
|
|
3193
|
-
if (fetched === null || fetched === void 0) {
|
|
3194
|
-
if (!this.shouldNegativeCache(options)) {
|
|
3195
|
-
return null;
|
|
3196
|
-
}
|
|
3197
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3198
|
-
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
3199
|
-
key,
|
|
3200
|
-
expectedClearEpoch,
|
|
3201
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
3202
|
-
expectedKeyEpoch,
|
|
3203
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
3204
|
-
});
|
|
3205
|
-
return null;
|
|
3206
|
-
}
|
|
3207
|
-
await this.storeEntry(key, "empty", null, options);
|
|
3208
|
-
return null;
|
|
3209
|
-
}
|
|
3210
|
-
if (options?.shouldCache) {
|
|
3211
|
-
try {
|
|
3212
|
-
if (!options.shouldCache(fetched)) {
|
|
3213
|
-
return fetched;
|
|
3214
|
-
}
|
|
3215
|
-
} catch (error) {
|
|
3216
|
-
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
3217
|
-
}
|
|
3218
|
-
}
|
|
3219
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
3220
|
-
this.logger.debug?.("skip-store-after-invalidation", {
|
|
3221
|
-
key,
|
|
3222
|
-
expectedClearEpoch,
|
|
3223
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
3224
|
-
expectedKeyEpoch,
|
|
3225
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
3226
|
-
});
|
|
3227
|
-
return fetched;
|
|
3228
|
-
}
|
|
3229
|
-
await this.storeEntry(key, "value", fetched, options);
|
|
3230
|
-
return fetched;
|
|
3231
|
-
}
|
|
3232
3624
|
async storeEntry(key, kind, value, options) {
|
|
3625
|
+
const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
|
|
3233
3626
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3234
3627
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3235
|
-
await this.layerWriter.writeAcrossLayers(key, kind, value,
|
|
3628
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
|
|
3236
3629
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3237
3630
|
return;
|
|
3238
3631
|
}
|
|
3239
|
-
if (
|
|
3240
|
-
await this.tagIndex.track(key,
|
|
3632
|
+
if (resolvedOptions?.tags) {
|
|
3633
|
+
await this.tagIndex.track(key, resolvedOptions.tags);
|
|
3241
3634
|
} else {
|
|
3242
3635
|
await this.tagIndex.touch(key);
|
|
3243
3636
|
}
|
|
3244
3637
|
this.metricsCollector.increment("sets");
|
|
3245
|
-
this.logger.debug?.("set", { key, kind, tags:
|
|
3246
|
-
this.emit("set", { key, kind, tags:
|
|
3638
|
+
this.logger.debug?.("set", { key, kind, tags: resolvedOptions?.tags });
|
|
3639
|
+
this.emit("set", { key, kind, tags: resolvedOptions?.tags });
|
|
3247
3640
|
if (this.shouldBroadcastL1Invalidation()) {
|
|
3248
3641
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
3249
3642
|
}
|
|
3250
3643
|
}
|
|
3251
3644
|
async writeBatch(entries) {
|
|
3252
|
-
const
|
|
3645
|
+
const resolvedEntries = entries.map((entry) => ({
|
|
3646
|
+
...entry,
|
|
3647
|
+
options: this.resolveContextOptions(entry.key, "value", entry.value, entry.options)
|
|
3648
|
+
}));
|
|
3649
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(resolvedEntries);
|
|
3253
3650
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
3254
3651
|
return;
|
|
3255
3652
|
}
|
|
3256
|
-
for (const entry of
|
|
3653
|
+
for (const entry of resolvedEntries) {
|
|
3257
3654
|
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
3258
3655
|
continue;
|
|
3259
3656
|
}
|
|
@@ -3275,87 +3672,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3275
3672
|
});
|
|
3276
3673
|
}
|
|
3277
3674
|
}
|
|
3278
|
-
async readFromLayers(key, options, mode) {
|
|
3279
|
-
let sawRetainableValue = false;
|
|
3280
|
-
for (let index = 0; index < this.layers.length; index += 1) {
|
|
3281
|
-
const layer = this.layers[index];
|
|
3282
|
-
if (!layer) continue;
|
|
3283
|
-
const readStart = performance.now();
|
|
3284
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
3285
|
-
const readDuration = performance.now() - readStart;
|
|
3286
|
-
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
3287
|
-
if (stored === null) {
|
|
3288
|
-
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
3289
|
-
continue;
|
|
3290
|
-
}
|
|
3291
|
-
const resolved = resolveStoredValue(stored);
|
|
3292
|
-
if (resolved.state === "expired") {
|
|
3293
|
-
await layer.delete(key);
|
|
3294
|
-
continue;
|
|
3295
|
-
}
|
|
3296
|
-
sawRetainableValue = true;
|
|
3297
|
-
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
3298
|
-
continue;
|
|
3299
|
-
}
|
|
3300
|
-
await this.tagIndex.touch(key);
|
|
3301
|
-
await this.backfill(key, stored, index - 1, options);
|
|
3302
|
-
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
3303
|
-
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
3304
|
-
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
3305
|
-
return {
|
|
3306
|
-
found: true,
|
|
3307
|
-
value: resolved.value,
|
|
3308
|
-
stored,
|
|
3309
|
-
state: resolved.state,
|
|
3310
|
-
layerIndex: index,
|
|
3311
|
-
layerName: layer.name
|
|
3312
|
-
};
|
|
3313
|
-
}
|
|
3314
|
-
if (!sawRetainableValue) {
|
|
3315
|
-
await this.tagIndex.remove(key);
|
|
3316
|
-
}
|
|
3317
|
-
this.logger.debug?.("miss", { key, mode });
|
|
3318
|
-
this.emit("miss", { key, mode });
|
|
3319
|
-
return { found: false, value: null, stored: null, state: "miss" };
|
|
3320
|
-
}
|
|
3321
|
-
async readLayerEntry(layer, key) {
|
|
3322
|
-
if (this.shouldSkipLayer(layer)) {
|
|
3323
|
-
return null;
|
|
3324
|
-
}
|
|
3325
|
-
if (layer.getEntry) {
|
|
3326
|
-
try {
|
|
3327
|
-
return await layer.getEntry(key);
|
|
3328
|
-
} catch (error) {
|
|
3329
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
3330
|
-
}
|
|
3331
|
-
}
|
|
3332
|
-
try {
|
|
3333
|
-
return await layer.get(key);
|
|
3334
|
-
} catch (error) {
|
|
3335
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
3336
|
-
}
|
|
3337
|
-
}
|
|
3338
|
-
async backfill(key, stored, upToIndex, options) {
|
|
3339
|
-
if (upToIndex < 0) {
|
|
3340
|
-
return;
|
|
3341
|
-
}
|
|
3342
|
-
for (let index = 0; index <= upToIndex; index += 1) {
|
|
3343
|
-
const layer = this.layers[index];
|
|
3344
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3345
|
-
continue;
|
|
3346
|
-
}
|
|
3347
|
-
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
3348
|
-
try {
|
|
3349
|
-
await layer.set(key, stored, ttl);
|
|
3350
|
-
} catch (error) {
|
|
3351
|
-
await this.handleLayerFailure(layer, "backfill", error);
|
|
3352
|
-
continue;
|
|
3353
|
-
}
|
|
3354
|
-
this.metricsCollector.increment("backfills");
|
|
3355
|
-
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
3356
|
-
this.emit("backfill", { key, layer: layer.name });
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
3675
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
3360
3676
|
return this.ttlResolver.resolveFreshTtl(
|
|
3361
3677
|
key,
|
|
@@ -3368,58 +3684,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3368
3684
|
value
|
|
3369
3685
|
);
|
|
3370
3686
|
}
|
|
3371
|
-
|
|
3372
|
-
return this.ttlResolver.
|
|
3373
|
-
}
|
|
3374
|
-
shouldNegativeCache(options) {
|
|
3375
|
-
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
3687
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
3688
|
+
return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
|
|
3376
3689
|
}
|
|
3377
|
-
|
|
3378
|
-
if (!
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
}
|
|
3382
|
-
|
|
3690
|
+
resolveContextOptions(key, kind, value, options) {
|
|
3691
|
+
if (!options?.contextOptions) {
|
|
3692
|
+
return options;
|
|
3693
|
+
}
|
|
3694
|
+
const { contextOptions, ...baseOptions } = options;
|
|
3695
|
+
let overrides;
|
|
3696
|
+
try {
|
|
3697
|
+
overrides = contextOptions({ key, value, kind });
|
|
3698
|
+
} catch (error) {
|
|
3699
|
+
throw new Error(`options.contextOptions() failed for key "${key}": ${this.formatError(error)}`);
|
|
3700
|
+
}
|
|
3701
|
+
if (!overrides) {
|
|
3702
|
+
return baseOptions;
|
|
3703
|
+
}
|
|
3704
|
+
if (!this.isPlainObject(overrides)) {
|
|
3705
|
+
throw new Error(
|
|
3706
|
+
`options.contextOptions() must return a plain object or undefined for key "${key}". Async resolvers are not supported.`
|
|
3707
|
+
);
|
|
3708
|
+
}
|
|
3709
|
+
try {
|
|
3710
|
+
validateContextEntryOptions("options.contextOptions()", overrides);
|
|
3711
|
+
} catch (error) {
|
|
3712
|
+
throw new Error(
|
|
3713
|
+
`options.contextOptions() returned invalid entry options for key "${key}": ${this.formatError(error)}`
|
|
3714
|
+
);
|
|
3383
3715
|
}
|
|
3384
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3385
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3386
|
-
this.backgroundRefreshAbort.set(key, false);
|
|
3387
|
-
const refresh = (async () => {
|
|
3388
|
-
this.metricsCollector.increment("refreshes");
|
|
3389
|
-
try {
|
|
3390
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3391
|
-
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3392
|
-
} catch (error) {
|
|
3393
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3394
|
-
this.metricsCollector.increment("refreshErrors");
|
|
3395
|
-
this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
|
|
3396
|
-
} finally {
|
|
3397
|
-
this.backgroundRefreshes.delete(key);
|
|
3398
|
-
this.backgroundRefreshAbort.delete(key);
|
|
3399
|
-
}
|
|
3400
|
-
})();
|
|
3401
|
-
this.backgroundRefreshes.set(key, refresh);
|
|
3402
|
-
}
|
|
3403
|
-
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
3404
|
-
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
3405
|
-
await this.fetchWithGuards(
|
|
3406
|
-
key,
|
|
3407
|
-
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
3408
|
-
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
3409
|
-
}),
|
|
3410
|
-
options,
|
|
3411
|
-
expectedClearEpoch,
|
|
3412
|
-
expectedKeyEpoch
|
|
3413
|
-
);
|
|
3414
|
-
}
|
|
3415
|
-
resolveSingleFlightOptions() {
|
|
3416
3716
|
return {
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
3420
|
-
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
3717
|
+
...baseOptions,
|
|
3718
|
+
...overrides
|
|
3421
3719
|
};
|
|
3422
3720
|
}
|
|
3721
|
+
isPlainObject(value) {
|
|
3722
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3723
|
+
return false;
|
|
3724
|
+
}
|
|
3725
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3726
|
+
return prototype === Object.prototype || prototype === null;
|
|
3727
|
+
}
|
|
3423
3728
|
async deleteKeys(keys) {
|
|
3424
3729
|
if (keys.length === 0) {
|
|
3425
3730
|
return;
|
|
@@ -3436,6 +3741,30 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3436
3741
|
this.logger.debug?.("delete", { keys });
|
|
3437
3742
|
this.emit("delete", { keys });
|
|
3438
3743
|
}
|
|
3744
|
+
async expireKeys(keys) {
|
|
3745
|
+
if (keys.length === 0) {
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3749
|
+
const foundKeys = await this.expireKeysInLayers(keys, this.layers);
|
|
3750
|
+
for (const key of keys) {
|
|
3751
|
+
if (foundKeys.has(key)) {
|
|
3752
|
+
continue;
|
|
3753
|
+
}
|
|
3754
|
+
await this.tagIndex.remove(key);
|
|
3755
|
+
this.ttlResolver.deleteProfile(key);
|
|
3756
|
+
this.circuitBreakerManager.delete(key);
|
|
3757
|
+
}
|
|
3758
|
+
this.metricsCollector.increment("invalidations");
|
|
3759
|
+
this.logger.debug?.("expire", { keys });
|
|
3760
|
+
this.emit("expire", { keys });
|
|
3761
|
+
}
|
|
3762
|
+
async expireKeysInLayers(keys, layers) {
|
|
3763
|
+
if (keys.length === 0) {
|
|
3764
|
+
return /* @__PURE__ */ new Set();
|
|
3765
|
+
}
|
|
3766
|
+
return this.invalidation.expireKeysInLayers(layers, keys);
|
|
3767
|
+
}
|
|
3439
3768
|
async publishInvalidation(message) {
|
|
3440
3769
|
if (!this.options.invalidationBus) {
|
|
3441
3770
|
return;
|
|
@@ -3457,6 +3786,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3457
3786
|
}
|
|
3458
3787
|
const keys = message.keys ?? [];
|
|
3459
3788
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3789
|
+
if (message.operation === "expire") {
|
|
3790
|
+
await this.expireKeysInLayers(keys, localLayers);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3460
3793
|
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3461
3794
|
if (message.operation !== "write") {
|
|
3462
3795
|
for (const key of keys) {
|
|
@@ -3654,6 +3987,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3654
3987
|
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3655
3988
|
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3656
3989
|
validateTags(options.tags);
|
|
3990
|
+
if (options.contextOptions && typeof options.contextOptions !== "function") {
|
|
3991
|
+
throw new Error("options.contextOptions must be a function.");
|
|
3992
|
+
}
|
|
3657
3993
|
}
|
|
3658
3994
|
assertActive(operation) {
|
|
3659
3995
|
if (this.isDisconnecting) {
|
|
@@ -3665,29 +4001,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3665
4001
|
await this.startup;
|
|
3666
4002
|
this.assertActive(operation);
|
|
3667
4003
|
}
|
|
4004
|
+
async readLayerEntry(layer, key) {
|
|
4005
|
+
return this.reader.readLayerEntry(layer, key);
|
|
4006
|
+
}
|
|
4007
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
4008
|
+
this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
4009
|
+
}
|
|
3668
4010
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3669
|
-
|
|
3670
|
-
stored: hit.stored,
|
|
3671
|
-
hasFetcher: Boolean(fetcher),
|
|
3672
|
-
slidingTtl: options?.slidingTtl ?? false,
|
|
3673
|
-
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3674
|
-
});
|
|
3675
|
-
if (plan.refreshedStored) {
|
|
3676
|
-
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3677
|
-
const layer = this.layers[index];
|
|
3678
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3679
|
-
continue;
|
|
3680
|
-
}
|
|
3681
|
-
try {
|
|
3682
|
-
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3683
|
-
} catch (error) {
|
|
3684
|
-
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3685
|
-
}
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3689
|
-
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3690
|
-
}
|
|
4011
|
+
return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
|
|
3691
4012
|
}
|
|
3692
4013
|
shouldSkipLayer(layer) {
|
|
3693
4014
|
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
@@ -3729,9 +4050,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3729
4050
|
}
|
|
3730
4051
|
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
3731
4052
|
}
|
|
3732
|
-
isNegativeStoredValue(stored) {
|
|
3733
|
-
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
3734
|
-
}
|
|
3735
4053
|
emitError(operation, context) {
|
|
3736
4054
|
this.logger.error?.(operation, context);
|
|
3737
4055
|
if (this.listenerCount("error") > 0) {
|
|
@@ -3831,7 +4149,7 @@ var RedisInvalidationBus = class {
|
|
|
3831
4149
|
}
|
|
3832
4150
|
const candidate = value;
|
|
3833
4151
|
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
3834
|
-
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
4152
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "expire" || candidate.operation === "clear";
|
|
3835
4153
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3836
4154
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3837
4155
|
}
|
|
@@ -4354,7 +4672,7 @@ var MemoryLayer = class {
|
|
|
4354
4672
|
this.entries.delete(key);
|
|
4355
4673
|
this.entries.set(key, {
|
|
4356
4674
|
value,
|
|
4357
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
4675
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null,
|
|
4358
4676
|
accessCount: 0,
|
|
4359
4677
|
insertedAt: Date.now()
|
|
4360
4678
|
});
|
|
@@ -4385,7 +4703,7 @@ var MemoryLayer = class {
|
|
|
4385
4703
|
if (entry.expiresAt === null) {
|
|
4386
4704
|
return null;
|
|
4387
4705
|
}
|
|
4388
|
-
return Math.max(0, Math.ceil(
|
|
4706
|
+
return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
|
|
4389
4707
|
}
|
|
4390
4708
|
async size() {
|
|
4391
4709
|
this.pruneExpired();
|
|
@@ -4575,7 +4893,7 @@ var RedisLayer = class {
|
|
|
4575
4893
|
const payload = await this.encodePayload(serialized);
|
|
4576
4894
|
const normalizedKey = this.withPrefix(entry.key);
|
|
4577
4895
|
if (entry.ttl && entry.ttl > 0) {
|
|
4578
|
-
pipeline.set(normalizedKey, payload, "
|
|
4896
|
+
pipeline.set(normalizedKey, payload, "PX", entry.ttl);
|
|
4579
4897
|
} else {
|
|
4580
4898
|
pipeline.set(normalizedKey, payload);
|
|
4581
4899
|
}
|
|
@@ -4590,7 +4908,7 @@ var RedisLayer = class {
|
|
|
4590
4908
|
if (ttl && ttl > 0) {
|
|
4591
4909
|
await this.runCommand(
|
|
4592
4910
|
`set(${this.displayKey(key)})`,
|
|
4593
|
-
() => this.client.set(normalizedKey, payload, "
|
|
4911
|
+
() => this.client.set(normalizedKey, payload, "PX", ttl)
|
|
4594
4912
|
);
|
|
4595
4913
|
return;
|
|
4596
4914
|
}
|
|
@@ -4619,7 +4937,10 @@ var RedisLayer = class {
|
|
|
4619
4937
|
}
|
|
4620
4938
|
async ttl(key) {
|
|
4621
4939
|
this.validateKey(key);
|
|
4622
|
-
const remaining = await this.runCommand(
|
|
4940
|
+
const remaining = await this.runCommand(
|
|
4941
|
+
`ttl(${this.displayKey(key)})`,
|
|
4942
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
4943
|
+
);
|
|
4623
4944
|
if (remaining < 0) {
|
|
4624
4945
|
return null;
|
|
4625
4946
|
}
|
|
@@ -4770,12 +5091,12 @@ var RedisLayer = class {
|
|
|
4770
5091
|
const payload = await this.encodePayload(serialized);
|
|
4771
5092
|
const ttl = await this.runCommand(
|
|
4772
5093
|
`rewrite-ttl(${this.displayKey(key)})`,
|
|
4773
|
-
() => this.client.
|
|
5094
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
4774
5095
|
);
|
|
4775
5096
|
if (ttl > 0) {
|
|
4776
5097
|
await this.runCommand(
|
|
4777
5098
|
`rewrite-set(${this.displayKey(key)})`,
|
|
4778
|
-
() => this.client.set(this.withPrefix(key), payload, "
|
|
5099
|
+
() => this.client.set(this.withPrefix(key), payload, "PX", ttl)
|
|
4779
5100
|
);
|
|
4780
5101
|
return;
|
|
4781
5102
|
}
|
|
@@ -5086,7 +5407,7 @@ var DiskLayer = class {
|
|
|
5086
5407
|
const entry = {
|
|
5087
5408
|
key,
|
|
5088
5409
|
value,
|
|
5089
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
5410
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
|
|
5090
5411
|
};
|
|
5091
5412
|
const payload = this.serializer.serialize(entry);
|
|
5092
5413
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
@@ -5131,7 +5452,7 @@ var DiskLayer = class {
|
|
|
5131
5452
|
if (entry.expiresAt === null) {
|
|
5132
5453
|
return null;
|
|
5133
5454
|
}
|
|
5134
|
-
const remaining = Math.ceil(
|
|
5455
|
+
const remaining = Math.ceil(entry.expiresAt - Date.now());
|
|
5135
5456
|
if (remaining <= 0) {
|
|
5136
5457
|
return null;
|
|
5137
5458
|
}
|
|
@@ -5421,7 +5742,7 @@ var MemcachedLayer = class {
|
|
|
5421
5742
|
this.validateKey(key);
|
|
5422
5743
|
const payload = this.serializer.serialize(value);
|
|
5423
5744
|
await this.client.set(this.withPrefix(key), payload, {
|
|
5424
|
-
expires: ttl && ttl > 0 ? ttl : void 0
|
|
5745
|
+
expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
|
|
5425
5746
|
});
|
|
5426
5747
|
}
|
|
5427
5748
|
async has(key) {
|