layercache 1.3.4 → 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-DKkrQ_Ky.d.cts → edge-D2FpRlyS.d.cts} +71 -22
- package/dist/{edge-DKkrQ_Ky.d.ts → edge-D2FpRlyS.d.ts} +71 -22
- 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 +399 -164
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +294 -81
- package/package.json +5 -5
- 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,15 +1151,15 @@ 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
|
|
|
@@ -1138,7 +1195,7 @@ var CacheStackReader = class {
|
|
|
1138
1195
|
this.options.metricsCollector.increment("staleHits");
|
|
1139
1196
|
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
1140
1197
|
if (fetcher) {
|
|
1141
|
-
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
1198
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
|
|
1142
1199
|
}
|
|
1143
1200
|
return hit.value;
|
|
1144
1201
|
}
|
|
@@ -1149,7 +1206,15 @@ var CacheStackReader = class {
|
|
|
1149
1206
|
return hit.value;
|
|
1150
1207
|
}
|
|
1151
1208
|
try {
|
|
1152
|
-
return await this.fetchWithGuards(
|
|
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
|
+
);
|
|
1153
1218
|
} catch (error) {
|
|
1154
1219
|
this.options.metricsCollector.increment("staleHits");
|
|
1155
1220
|
this.options.metricsCollector.increment("refreshErrors");
|
|
@@ -1164,7 +1229,11 @@ var CacheStackReader = class {
|
|
|
1164
1229
|
if (!fetcher) {
|
|
1165
1230
|
return null;
|
|
1166
1231
|
}
|
|
1167
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true
|
|
1232
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
|
|
1233
|
+
key: normalizedKey,
|
|
1234
|
+
currentValue: void 0,
|
|
1235
|
+
state: "miss"
|
|
1236
|
+
});
|
|
1168
1237
|
}
|
|
1169
1238
|
async readLayerEntry(layer, key) {
|
|
1170
1239
|
if (this.options.shouldSkipLayer(layer)) {
|
|
@@ -1192,7 +1261,7 @@ var CacheStackReader = class {
|
|
|
1192
1261
|
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1193
1262
|
continue;
|
|
1194
1263
|
}
|
|
1195
|
-
const ttl =
|
|
1264
|
+
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1196
1265
|
try {
|
|
1197
1266
|
await layer.set(key, stored, ttl);
|
|
1198
1267
|
} catch (error) {
|
|
@@ -1259,7 +1328,11 @@ var CacheStackReader = class {
|
|
|
1259
1328
|
this.options.emit("miss", { key, mode });
|
|
1260
1329
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
1261
1330
|
}
|
|
1262
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false
|
|
1331
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
|
|
1332
|
+
key,
|
|
1333
|
+
currentValue: void 0,
|
|
1334
|
+
state: "miss"
|
|
1335
|
+
}) {
|
|
1263
1336
|
const fetchTask = async () => {
|
|
1264
1337
|
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
1265
1338
|
if (shouldRecheckFreshLayers) {
|
|
@@ -1269,7 +1342,7 @@ var CacheStackReader = class {
|
|
|
1269
1342
|
return secondHit.value;
|
|
1270
1343
|
}
|
|
1271
1344
|
}
|
|
1272
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1345
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1273
1346
|
};
|
|
1274
1347
|
const singleFlightTask = async () => {
|
|
1275
1348
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -1280,7 +1353,7 @@ var CacheStackReader = class {
|
|
|
1280
1353
|
key,
|
|
1281
1354
|
this.resolveSingleFlightOptions(),
|
|
1282
1355
|
fetchTask,
|
|
1283
|
-
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1356
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1284
1357
|
);
|
|
1285
1358
|
} catch (error) {
|
|
1286
1359
|
if (!this.options.isGracefulDegradationEnabled()) {
|
|
@@ -1304,7 +1377,11 @@ var CacheStackReader = class {
|
|
|
1304
1377
|
}
|
|
1305
1378
|
return this.options.stampedeGuard.execute(key, singleFlightTask);
|
|
1306
1379
|
}
|
|
1307
|
-
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch
|
|
1380
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1381
|
+
key,
|
|
1382
|
+
currentValue: void 0,
|
|
1383
|
+
state: "miss"
|
|
1384
|
+
}) {
|
|
1308
1385
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1309
1386
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1310
1387
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1318,9 +1395,13 @@ var CacheStackReader = class {
|
|
|
1318
1395
|
}
|
|
1319
1396
|
await this.options.sleep(pollIntervalMs);
|
|
1320
1397
|
}
|
|
1321
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1398
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1322
1399
|
}
|
|
1323
|
-
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch
|
|
1400
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1401
|
+
key,
|
|
1402
|
+
currentValue: void 0,
|
|
1403
|
+
state: "miss"
|
|
1404
|
+
}) {
|
|
1324
1405
|
this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1325
1406
|
this.options.metricsCollector.increment("fetches");
|
|
1326
1407
|
const fetchStart = Date.now();
|
|
@@ -1329,7 +1410,7 @@ var CacheStackReader = class {
|
|
|
1329
1410
|
fetched = await this.options.fetchRateLimiter.schedule(
|
|
1330
1411
|
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1331
1412
|
{ key, fetcher },
|
|
1332
|
-
fetcher
|
|
1413
|
+
() => fetcher(fetcherContext)
|
|
1333
1414
|
);
|
|
1334
1415
|
this.options.circuitBreakerManager.recordSuccess(key);
|
|
1335
1416
|
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
@@ -1379,10 +1460,14 @@ var CacheStackReader = class {
|
|
|
1379
1460
|
await this.options.storeEntry(key, "value", fetched, options);
|
|
1380
1461
|
return fetched;
|
|
1381
1462
|
}
|
|
1382
|
-
runScheduleBackgroundRefresh(key, fetcher, options) {
|
|
1383
|
-
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1463
|
+
runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
1464
|
+
this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
1384
1465
|
}
|
|
1385
|
-
scheduleBackgroundRefresh(key, fetcher, options
|
|
1466
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
|
|
1467
|
+
key,
|
|
1468
|
+
currentValue: void 0,
|
|
1469
|
+
state: "miss"
|
|
1470
|
+
}) {
|
|
1386
1471
|
if (!shouldStartBackgroundRefresh({
|
|
1387
1472
|
isDisconnecting: this.options.isDisconnecting(),
|
|
1388
1473
|
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
@@ -1396,7 +1481,7 @@ var CacheStackReader = class {
|
|
|
1396
1481
|
this.options.metricsCollector.increment("refreshes");
|
|
1397
1482
|
try {
|
|
1398
1483
|
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1399
|
-
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
1484
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
|
|
1400
1485
|
} catch (error) {
|
|
1401
1486
|
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1402
1487
|
this.options.metricsCollector.increment("refreshErrors");
|
|
@@ -1411,16 +1496,22 @@ var CacheStackReader = class {
|
|
|
1411
1496
|
})();
|
|
1412
1497
|
this.backgroundRefreshes.set(key, refresh);
|
|
1413
1498
|
}
|
|
1414
|
-
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch
|
|
1499
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1500
|
+
key,
|
|
1501
|
+
currentValue: void 0,
|
|
1502
|
+
state: "miss"
|
|
1503
|
+
}) {
|
|
1415
1504
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
1416
1505
|
await this.fetchWithGuards(
|
|
1417
1506
|
key,
|
|
1418
|
-
() => this.options.withTimeout(fetcher(), timeoutMs, () => {
|
|
1507
|
+
(context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
|
|
1419
1508
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
1420
1509
|
}),
|
|
1421
1510
|
options,
|
|
1422
1511
|
expectedClearEpoch,
|
|
1423
|
-
expectedKeyEpoch
|
|
1512
|
+
expectedKeyEpoch,
|
|
1513
|
+
false,
|
|
1514
|
+
fetcherContext
|
|
1424
1515
|
);
|
|
1425
1516
|
}
|
|
1426
1517
|
async runApplyFreshReadPolicies(key, hit, options, fetcher) {
|
|
@@ -1431,7 +1522,7 @@ var CacheStackReader = class {
|
|
|
1431
1522
|
stored: hit.stored,
|
|
1432
1523
|
hasFetcher: Boolean(fetcher),
|
|
1433
1524
|
slidingTtl: options?.slidingTtl ?? false,
|
|
1434
|
-
|
|
1525
|
+
refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
1435
1526
|
});
|
|
1436
1527
|
if (plan.refreshedStored) {
|
|
1437
1528
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
@@ -1447,9 +1538,17 @@ var CacheStackReader = class {
|
|
|
1447
1538
|
}
|
|
1448
1539
|
}
|
|
1449
1540
|
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
1450
|
-
this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options);
|
|
1541
|
+
this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
|
|
1451
1542
|
}
|
|
1452
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
|
+
}
|
|
1453
1552
|
resolveSingleFlightOptions() {
|
|
1454
1553
|
return {
|
|
1455
1554
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
@@ -1731,7 +1830,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1731
1830
|
await visitor({
|
|
1732
1831
|
key: exportedKey,
|
|
1733
1832
|
value: stored,
|
|
1734
|
-
ttl:
|
|
1833
|
+
ttl: remainingStoredTtlMs(stored)
|
|
1735
1834
|
});
|
|
1736
1835
|
};
|
|
1737
1836
|
if (layer.forEachKey) {
|
|
@@ -1887,6 +1986,19 @@ function validateCircuitBreakerOptions(options) {
|
|
|
1887
1986
|
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1888
1987
|
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1889
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
|
+
}
|
|
1890
2002
|
|
|
1891
2003
|
// src/internal/CircuitBreakerManager.ts
|
|
1892
2004
|
var CircuitBreakerManager = class {
|
|
@@ -2319,7 +2431,7 @@ var MetricsCollector = class {
|
|
|
2319
2431
|
|
|
2320
2432
|
// src/internal/TtlResolver.ts
|
|
2321
2433
|
var import_node_crypto2 = require("crypto");
|
|
2322
|
-
var
|
|
2434
|
+
var DEFAULT_NEGATIVE_TTL_MS = 6e4;
|
|
2323
2435
|
var secureRandom = {
|
|
2324
2436
|
value() {
|
|
2325
2437
|
return (0, import_node_crypto2.randomBytes)(4).readUInt32BE(0) / 4294967296;
|
|
@@ -2346,17 +2458,17 @@ var TtlResolver = class {
|
|
|
2346
2458
|
}
|
|
2347
2459
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
2348
2460
|
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
2349
|
-
const baseTtl = kind === "empty" ? this.
|
|
2461
|
+
const baseTtl = kind === "empty" ? this.resolveLayerMs(
|
|
2350
2462
|
layerName,
|
|
2351
2463
|
options?.negativeTtl,
|
|
2352
2464
|
globalNegativeTtl,
|
|
2353
|
-
this.
|
|
2354
|
-
) : this.
|
|
2465
|
+
this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_MS
|
|
2466
|
+
) : this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
2355
2467
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
2356
|
-
const jitter = this.
|
|
2468
|
+
const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
|
|
2357
2469
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
2358
2470
|
}
|
|
2359
|
-
|
|
2471
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
2360
2472
|
if (override !== void 0) {
|
|
2361
2473
|
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
2362
2474
|
}
|
|
@@ -2378,8 +2490,8 @@ var TtlResolver = class {
|
|
|
2378
2490
|
if (profile.hits < hotAfter) {
|
|
2379
2491
|
return ttl;
|
|
2380
2492
|
}
|
|
2381
|
-
const step = this.
|
|
2382
|
-
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;
|
|
2383
2495
|
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
2384
2496
|
return Math.min(maxTtl, ttl + step * multiplier);
|
|
2385
2497
|
}
|
|
@@ -2401,17 +2513,17 @@ var TtlResolver = class {
|
|
|
2401
2513
|
if (policy === "until-midnight") {
|
|
2402
2514
|
const nextMidnight = new Date(now);
|
|
2403
2515
|
nextMidnight.setHours(24, 0, 0, 0);
|
|
2404
|
-
return Math.max(1, Math.ceil(
|
|
2516
|
+
return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
|
|
2405
2517
|
}
|
|
2406
2518
|
if (policy === "next-hour") {
|
|
2407
2519
|
const nextHour = new Date(now);
|
|
2408
2520
|
nextHour.setMinutes(60, 0, 0);
|
|
2409
|
-
return Math.max(1, Math.ceil(
|
|
2521
|
+
return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
|
|
2410
2522
|
}
|
|
2411
|
-
const
|
|
2412
|
-
const
|
|
2413
|
-
const nextBoundary = Math.ceil((
|
|
2414
|
-
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);
|
|
2415
2527
|
}
|
|
2416
2528
|
readLayerNumber(layerName, value) {
|
|
2417
2529
|
if (typeof value === "number") {
|
|
@@ -2862,7 +2974,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2862
2974
|
},
|
|
2863
2975
|
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
2864
2976
|
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
2865
|
-
|
|
2977
|
+
resolveLayerMs: this.resolveLayerMs.bind(this),
|
|
2866
2978
|
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
2867
2979
|
globalStaleIfError: this.options.staleIfError,
|
|
2868
2980
|
writePolicy: this.options.writePolicy,
|
|
@@ -2918,12 +3030,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2918
3030
|
formatError: (error) => this.formatError(error),
|
|
2919
3031
|
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
2920
3032
|
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
2921
|
-
|
|
3033
|
+
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
2922
3034
|
sleep: (ms) => this.sleep(ms),
|
|
2923
3035
|
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
2924
3036
|
isDisconnecting: () => this.isDisconnecting,
|
|
2925
3037
|
isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
|
|
2926
|
-
scheduleBackgroundRefreshDispatch: (key, fetcher, options2) => this.scheduleBackgroundRefresh(key, fetcher, options2),
|
|
3038
|
+
scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
|
|
2927
3039
|
stampedePrevention: options.stampedePrevention,
|
|
2928
3040
|
singleFlightCoordinator: options.singleFlightCoordinator,
|
|
2929
3041
|
singleFlightLeaseMs: options.singleFlightLeaseMs,
|
|
@@ -3029,7 +3141,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3029
3141
|
return false;
|
|
3030
3142
|
}
|
|
3031
3143
|
/**
|
|
3032
|
-
* Returns the remaining TTL in
|
|
3144
|
+
* Returns the remaining TTL in milliseconds for the key in the fastest layer
|
|
3033
3145
|
* that has it, or null if the key is not found / has no TTL.
|
|
3034
3146
|
*/
|
|
3035
3147
|
async ttl(key) {
|
|
@@ -3265,6 +3377,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3265
3377
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3266
3378
|
});
|
|
3267
3379
|
}
|
|
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
|
+
}
|
|
3268
3389
|
async invalidateByTags(tags, mode = "any") {
|
|
3269
3390
|
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
3270
3391
|
if (tags.length === 0) {
|
|
@@ -3281,6 +3402,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3281
3402
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3282
3403
|
});
|
|
3283
3404
|
}
|
|
3405
|
+
async expireByTags(tags, mode = "any") {
|
|
3406
|
+
await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
|
|
3407
|
+
if (tags.length === 0) {
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
validateTags(tags);
|
|
3411
|
+
await this.awaitStartup("expireByTags");
|
|
3412
|
+
const keysByTag = await Promise.all(
|
|
3413
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
3414
|
+
);
|
|
3415
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
3416
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
3417
|
+
await this.expireKeys(keys);
|
|
3418
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
3419
|
+
});
|
|
3420
|
+
}
|
|
3284
3421
|
async invalidateByPattern(pattern) {
|
|
3285
3422
|
await this.observeOperation("layercache.invalidate_by_pattern", void 0, async () => {
|
|
3286
3423
|
validatePattern(pattern);
|
|
@@ -3293,6 +3430,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3293
3430
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3294
3431
|
});
|
|
3295
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
|
+
}
|
|
3296
3445
|
async invalidateByPrefix(prefix) {
|
|
3297
3446
|
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
3298
3447
|
await this.awaitStartup("invalidateByPrefix");
|
|
@@ -3302,6 +3451,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3302
3451
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
3303
3452
|
});
|
|
3304
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
|
+
}
|
|
3305
3463
|
getMetrics() {
|
|
3306
3464
|
return this.metricsCollector.snapshot;
|
|
3307
3465
|
}
|
|
@@ -3378,9 +3536,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3378
3536
|
const normalizedKey = this.qualifyKey(userKey);
|
|
3379
3537
|
await this.awaitStartup("inspect");
|
|
3380
3538
|
const foundInLayers = [];
|
|
3381
|
-
let
|
|
3382
|
-
let
|
|
3383
|
-
let
|
|
3539
|
+
let freshTtlMs = null;
|
|
3540
|
+
let staleTtlMs = null;
|
|
3541
|
+
let errorTtlMs = null;
|
|
3384
3542
|
let isStale = false;
|
|
3385
3543
|
for (const layer of this.layers) {
|
|
3386
3544
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -3397,9 +3555,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3397
3555
|
foundInLayers.push(layer.name);
|
|
3398
3556
|
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
3399
3557
|
const now = Date.now();
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
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;
|
|
3403
3561
|
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
3404
3562
|
}
|
|
3405
3563
|
}
|
|
@@ -3407,7 +3565,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3407
3565
|
return null;
|
|
3408
3566
|
}
|
|
3409
3567
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
3410
|
-
return { key: userKey, foundInLayers,
|
|
3568
|
+
return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
|
|
3411
3569
|
}
|
|
3412
3570
|
async exportState() {
|
|
3413
3571
|
await this.awaitStartup("exportState");
|
|
@@ -3464,30 +3622,35 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3464
3622
|
});
|
|
3465
3623
|
}
|
|
3466
3624
|
async storeEntry(key, kind, value, options) {
|
|
3625
|
+
const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
|
|
3467
3626
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3468
3627
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3469
|
-
await this.layerWriter.writeAcrossLayers(key, kind, value,
|
|
3628
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
|
|
3470
3629
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3471
3630
|
return;
|
|
3472
3631
|
}
|
|
3473
|
-
if (
|
|
3474
|
-
await this.tagIndex.track(key,
|
|
3632
|
+
if (resolvedOptions?.tags) {
|
|
3633
|
+
await this.tagIndex.track(key, resolvedOptions.tags);
|
|
3475
3634
|
} else {
|
|
3476
3635
|
await this.tagIndex.touch(key);
|
|
3477
3636
|
}
|
|
3478
3637
|
this.metricsCollector.increment("sets");
|
|
3479
|
-
this.logger.debug?.("set", { key, kind, tags:
|
|
3480
|
-
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 });
|
|
3481
3640
|
if (this.shouldBroadcastL1Invalidation()) {
|
|
3482
3641
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
3483
3642
|
}
|
|
3484
3643
|
}
|
|
3485
3644
|
async writeBatch(entries) {
|
|
3486
|
-
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);
|
|
3487
3650
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
3488
3651
|
return;
|
|
3489
3652
|
}
|
|
3490
|
-
for (const entry of
|
|
3653
|
+
for (const entry of resolvedEntries) {
|
|
3491
3654
|
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
3492
3655
|
continue;
|
|
3493
3656
|
}
|
|
@@ -3521,8 +3684,46 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3521
3684
|
value
|
|
3522
3685
|
);
|
|
3523
3686
|
}
|
|
3524
|
-
|
|
3525
|
-
return this.ttlResolver.
|
|
3687
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
3688
|
+
return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
|
|
3689
|
+
}
|
|
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
|
+
);
|
|
3715
|
+
}
|
|
3716
|
+
return {
|
|
3717
|
+
...baseOptions,
|
|
3718
|
+
...overrides
|
|
3719
|
+
};
|
|
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;
|
|
3526
3727
|
}
|
|
3527
3728
|
async deleteKeys(keys) {
|
|
3528
3729
|
if (keys.length === 0) {
|
|
@@ -3540,6 +3741,30 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3540
3741
|
this.logger.debug?.("delete", { keys });
|
|
3541
3742
|
this.emit("delete", { keys });
|
|
3542
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
|
+
}
|
|
3543
3768
|
async publishInvalidation(message) {
|
|
3544
3769
|
if (!this.options.invalidationBus) {
|
|
3545
3770
|
return;
|
|
@@ -3561,6 +3786,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3561
3786
|
}
|
|
3562
3787
|
const keys = message.keys ?? [];
|
|
3563
3788
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3789
|
+
if (message.operation === "expire") {
|
|
3790
|
+
await this.expireKeysInLayers(keys, localLayers);
|
|
3791
|
+
return;
|
|
3792
|
+
}
|
|
3564
3793
|
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
3565
3794
|
if (message.operation !== "write") {
|
|
3566
3795
|
for (const key of keys) {
|
|
@@ -3758,6 +3987,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3758
3987
|
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3759
3988
|
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3760
3989
|
validateTags(options.tags);
|
|
3990
|
+
if (options.contextOptions && typeof options.contextOptions !== "function") {
|
|
3991
|
+
throw new Error("options.contextOptions must be a function.");
|
|
3992
|
+
}
|
|
3761
3993
|
}
|
|
3762
3994
|
assertActive(operation) {
|
|
3763
3995
|
if (this.isDisconnecting) {
|
|
@@ -3772,8 +4004,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3772
4004
|
async readLayerEntry(layer, key) {
|
|
3773
4005
|
return this.reader.readLayerEntry(layer, key);
|
|
3774
4006
|
}
|
|
3775
|
-
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
3776
|
-
this.reader.runScheduleBackgroundRefresh(key, fetcher, options);
|
|
4007
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
4008
|
+
this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
3777
4009
|
}
|
|
3778
4010
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3779
4011
|
return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
|
|
@@ -3917,7 +4149,7 @@ var RedisInvalidationBus = class {
|
|
|
3917
4149
|
}
|
|
3918
4150
|
const candidate = value;
|
|
3919
4151
|
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
3920
|
-
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";
|
|
3921
4153
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3922
4154
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3923
4155
|
}
|
|
@@ -4440,7 +4672,7 @@ var MemoryLayer = class {
|
|
|
4440
4672
|
this.entries.delete(key);
|
|
4441
4673
|
this.entries.set(key, {
|
|
4442
4674
|
value,
|
|
4443
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
4675
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null,
|
|
4444
4676
|
accessCount: 0,
|
|
4445
4677
|
insertedAt: Date.now()
|
|
4446
4678
|
});
|
|
@@ -4471,7 +4703,7 @@ var MemoryLayer = class {
|
|
|
4471
4703
|
if (entry.expiresAt === null) {
|
|
4472
4704
|
return null;
|
|
4473
4705
|
}
|
|
4474
|
-
return Math.max(0, Math.ceil(
|
|
4706
|
+
return Math.max(0, Math.ceil(entry.expiresAt - Date.now()));
|
|
4475
4707
|
}
|
|
4476
4708
|
async size() {
|
|
4477
4709
|
this.pruneExpired();
|
|
@@ -4661,7 +4893,7 @@ var RedisLayer = class {
|
|
|
4661
4893
|
const payload = await this.encodePayload(serialized);
|
|
4662
4894
|
const normalizedKey = this.withPrefix(entry.key);
|
|
4663
4895
|
if (entry.ttl && entry.ttl > 0) {
|
|
4664
|
-
pipeline.set(normalizedKey, payload, "
|
|
4896
|
+
pipeline.set(normalizedKey, payload, "PX", entry.ttl);
|
|
4665
4897
|
} else {
|
|
4666
4898
|
pipeline.set(normalizedKey, payload);
|
|
4667
4899
|
}
|
|
@@ -4676,7 +4908,7 @@ var RedisLayer = class {
|
|
|
4676
4908
|
if (ttl && ttl > 0) {
|
|
4677
4909
|
await this.runCommand(
|
|
4678
4910
|
`set(${this.displayKey(key)})`,
|
|
4679
|
-
() => this.client.set(normalizedKey, payload, "
|
|
4911
|
+
() => this.client.set(normalizedKey, payload, "PX", ttl)
|
|
4680
4912
|
);
|
|
4681
4913
|
return;
|
|
4682
4914
|
}
|
|
@@ -4705,7 +4937,10 @@ var RedisLayer = class {
|
|
|
4705
4937
|
}
|
|
4706
4938
|
async ttl(key) {
|
|
4707
4939
|
this.validateKey(key);
|
|
4708
|
-
const remaining = await this.runCommand(
|
|
4940
|
+
const remaining = await this.runCommand(
|
|
4941
|
+
`ttl(${this.displayKey(key)})`,
|
|
4942
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
4943
|
+
);
|
|
4709
4944
|
if (remaining < 0) {
|
|
4710
4945
|
return null;
|
|
4711
4946
|
}
|
|
@@ -4856,12 +5091,12 @@ var RedisLayer = class {
|
|
|
4856
5091
|
const payload = await this.encodePayload(serialized);
|
|
4857
5092
|
const ttl = await this.runCommand(
|
|
4858
5093
|
`rewrite-ttl(${this.displayKey(key)})`,
|
|
4859
|
-
() => this.client.
|
|
5094
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
4860
5095
|
);
|
|
4861
5096
|
if (ttl > 0) {
|
|
4862
5097
|
await this.runCommand(
|
|
4863
5098
|
`rewrite-set(${this.displayKey(key)})`,
|
|
4864
|
-
() => this.client.set(this.withPrefix(key), payload, "
|
|
5099
|
+
() => this.client.set(this.withPrefix(key), payload, "PX", ttl)
|
|
4865
5100
|
);
|
|
4866
5101
|
return;
|
|
4867
5102
|
}
|
|
@@ -5172,7 +5407,7 @@ var DiskLayer = class {
|
|
|
5172
5407
|
const entry = {
|
|
5173
5408
|
key,
|
|
5174
5409
|
value,
|
|
5175
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
5410
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
|
|
5176
5411
|
};
|
|
5177
5412
|
const payload = this.serializer.serialize(entry);
|
|
5178
5413
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
@@ -5217,7 +5452,7 @@ var DiskLayer = class {
|
|
|
5217
5452
|
if (entry.expiresAt === null) {
|
|
5218
5453
|
return null;
|
|
5219
5454
|
}
|
|
5220
|
-
const remaining = Math.ceil(
|
|
5455
|
+
const remaining = Math.ceil(entry.expiresAt - Date.now());
|
|
5221
5456
|
if (remaining <= 0) {
|
|
5222
5457
|
return null;
|
|
5223
5458
|
}
|
|
@@ -5507,7 +5742,7 @@ var MemcachedLayer = class {
|
|
|
5507
5742
|
this.validateKey(key);
|
|
5508
5743
|
const payload = this.serializer.serialize(value);
|
|
5509
5744
|
await this.client.set(this.withPrefix(key), payload, {
|
|
5510
|
-
expires: ttl && ttl > 0 ? ttl : void 0
|
|
5745
|
+
expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
|
|
5511
5746
|
});
|
|
5512
5747
|
}
|
|
5513
5748
|
async has(key) {
|