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.js
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
validateAdaptiveTtlOptions,
|
|
4
4
|
validateCacheKey,
|
|
5
5
|
validateCircuitBreakerOptions,
|
|
6
|
+
validateContextEntryOptions,
|
|
6
7
|
validateLayerNumberOption,
|
|
7
8
|
validateNonNegativeNumber,
|
|
8
9
|
validatePattern,
|
|
@@ -11,22 +12,23 @@ import {
|
|
|
11
12
|
validateTag,
|
|
12
13
|
validateTags,
|
|
13
14
|
validateTtlPolicy
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-7KMKQ6QZ.js";
|
|
15
16
|
import {
|
|
16
17
|
MemoryLayer,
|
|
17
18
|
TagIndex,
|
|
18
19
|
createHonoCacheMiddleware
|
|
19
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-FFZCC7EQ.js";
|
|
20
21
|
import {
|
|
21
22
|
PatternMatcher,
|
|
22
23
|
createStoredValueEnvelope,
|
|
24
|
+
expireStoredEnvelope,
|
|
23
25
|
isStoredValueEnvelope,
|
|
24
26
|
refreshStoredEnvelope,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
remainingFreshTtlMs,
|
|
28
|
+
remainingStoredTtlMs,
|
|
27
29
|
resolveStoredValue,
|
|
28
30
|
unwrapStoredValue
|
|
29
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-KJDFYE5T.js";
|
|
30
32
|
|
|
31
33
|
// src/CacheStack.ts
|
|
32
34
|
import { EventEmitter } from "events";
|
|
@@ -219,6 +221,9 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
219
221
|
async invalidateByTag(tag) {
|
|
220
222
|
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
221
223
|
}
|
|
224
|
+
async expireByTag(tag) {
|
|
225
|
+
await this.trackMetrics(() => this.cache.expireByTag(this.qualifyTag(tag)));
|
|
226
|
+
}
|
|
222
227
|
async invalidateByTags(tags, mode = "any") {
|
|
223
228
|
await this.trackMetrics(
|
|
224
229
|
() => this.cache.invalidateByTags(
|
|
@@ -227,12 +232,26 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
227
232
|
)
|
|
228
233
|
);
|
|
229
234
|
}
|
|
235
|
+
async expireByTags(tags, mode = "any") {
|
|
236
|
+
await this.trackMetrics(
|
|
237
|
+
() => this.cache.expireByTags(
|
|
238
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
239
|
+
mode
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
}
|
|
230
243
|
async invalidateByPattern(pattern) {
|
|
231
244
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
232
245
|
}
|
|
246
|
+
async expireByPattern(pattern) {
|
|
247
|
+
await this.trackMetrics(() => this.cache.expireByPattern(this.qualify(pattern)));
|
|
248
|
+
}
|
|
233
249
|
async invalidateByPrefix(prefix) {
|
|
234
250
|
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
235
251
|
}
|
|
252
|
+
async expireByPrefix(prefix) {
|
|
253
|
+
await this.trackMetrics(() => this.cache.expireByPrefix(this.qualify(prefix)));
|
|
254
|
+
}
|
|
236
255
|
/**
|
|
237
256
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
238
257
|
*/
|
|
@@ -580,6 +599,35 @@ var CacheStackInvalidationSupport = class {
|
|
|
580
599
|
})
|
|
581
600
|
);
|
|
582
601
|
}
|
|
602
|
+
async expireKeysInLayers(layers, keys) {
|
|
603
|
+
const foundKeys = /* @__PURE__ */ new Set();
|
|
604
|
+
await Promise.all(
|
|
605
|
+
layers.map(async (layer) => {
|
|
606
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
await Promise.all(
|
|
610
|
+
keys.map(async (key) => {
|
|
611
|
+
try {
|
|
612
|
+
const stored = layer.getEntry ? await layer.getEntry(key) : await layer.get(key);
|
|
613
|
+
if (stored === null) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
foundKeys.add(key);
|
|
617
|
+
const expired = expireStoredEnvelope(stored);
|
|
618
|
+
if (expired === stored) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
await layer.set(key, expired, remainingStoredTtlMs(expired));
|
|
622
|
+
} catch (error) {
|
|
623
|
+
await this.options.handleLayerFailure(layer, "expire", error);
|
|
624
|
+
}
|
|
625
|
+
})
|
|
626
|
+
);
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
return foundKeys;
|
|
630
|
+
}
|
|
583
631
|
assertWithinInvalidationKeyLimit(size, maxKeys) {
|
|
584
632
|
if (maxKeys !== false && size > maxKeys) {
|
|
585
633
|
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
@@ -698,12 +746,12 @@ var CacheStackLayerWriter = class {
|
|
|
698
746
|
}
|
|
699
747
|
buildLayerSetEntry(layer, key, kind, value, writeOptions, now) {
|
|
700
748
|
const freshTtl = this.options.resolveFreshTtl(key, layer.name, kind, writeOptions, layer.defaultTtl, value);
|
|
701
|
-
const staleWhileRevalidate = this.options.
|
|
749
|
+
const staleWhileRevalidate = this.options.resolveLayerMs(
|
|
702
750
|
layer.name,
|
|
703
751
|
writeOptions?.staleWhileRevalidate,
|
|
704
752
|
this.options.globalStaleWhileRevalidate
|
|
705
753
|
);
|
|
706
|
-
const staleIfError = this.options.
|
|
754
|
+
const staleIfError = this.options.resolveLayerMs(
|
|
707
755
|
layer.name,
|
|
708
756
|
writeOptions?.staleIfError,
|
|
709
757
|
this.options.globalStaleIfError
|
|
@@ -711,12 +759,12 @@ var CacheStackLayerWriter = class {
|
|
|
711
759
|
const payload = createStoredValueEnvelope({
|
|
712
760
|
kind,
|
|
713
761
|
value,
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
762
|
+
freshTtlMs: freshTtl,
|
|
763
|
+
staleWhileRevalidateMs: staleWhileRevalidate,
|
|
764
|
+
staleIfErrorMs: staleIfError,
|
|
717
765
|
now
|
|
718
766
|
});
|
|
719
|
-
const ttl =
|
|
767
|
+
const ttl = remainingStoredTtlMs(payload, now) ?? freshTtl;
|
|
720
768
|
return {
|
|
721
769
|
key,
|
|
722
770
|
value: payload,
|
|
@@ -859,18 +907,420 @@ function planFreshReadPolicies({
|
|
|
859
907
|
stored,
|
|
860
908
|
hasFetcher,
|
|
861
909
|
slidingTtl,
|
|
862
|
-
|
|
910
|
+
refreshAheadMs
|
|
863
911
|
}) {
|
|
864
912
|
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
865
|
-
const refreshedStoredTtl = refreshedStored ?
|
|
866
|
-
const remainingFreshTtl =
|
|
913
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlMs(refreshedStored) ?? void 0 : void 0;
|
|
914
|
+
const remainingFreshTtl = remainingFreshTtlMs(stored) ?? 0;
|
|
867
915
|
return {
|
|
868
916
|
refreshedStored,
|
|
869
917
|
refreshedStoredTtl,
|
|
870
|
-
shouldScheduleBackgroundRefresh: hasFetcher &&
|
|
918
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadMs > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadMs
|
|
871
919
|
};
|
|
872
920
|
}
|
|
873
921
|
|
|
922
|
+
// src/internal/CacheStackReader.ts
|
|
923
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
924
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
925
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
926
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
927
|
+
var CacheStackReader = class {
|
|
928
|
+
constructor(options) {
|
|
929
|
+
this.options = options;
|
|
930
|
+
}
|
|
931
|
+
options;
|
|
932
|
+
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
933
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
934
|
+
get activeRefreshCount() {
|
|
935
|
+
return this.backgroundRefreshes.size;
|
|
936
|
+
}
|
|
937
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
938
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
939
|
+
if (hit.found) {
|
|
940
|
+
this.options.ttlResolver.recordAccess(normalizedKey);
|
|
941
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
942
|
+
this.options.metricsCollector.increment("negativeCacheHits");
|
|
943
|
+
}
|
|
944
|
+
if (hit.state === "fresh") {
|
|
945
|
+
this.options.metricsCollector.increment("hits");
|
|
946
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
947
|
+
return hit.value;
|
|
948
|
+
}
|
|
949
|
+
if (hit.state === "stale-while-revalidate") {
|
|
950
|
+
this.options.metricsCollector.increment("hits");
|
|
951
|
+
this.options.metricsCollector.increment("staleHits");
|
|
952
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
953
|
+
if (fetcher) {
|
|
954
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options, this.createFetcherContext(normalizedKey, hit));
|
|
955
|
+
}
|
|
956
|
+
return hit.value;
|
|
957
|
+
}
|
|
958
|
+
if (!fetcher) {
|
|
959
|
+
this.options.metricsCollector.increment("hits");
|
|
960
|
+
this.options.metricsCollector.increment("staleHits");
|
|
961
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
962
|
+
return hit.value;
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
return await this.fetchWithGuards(
|
|
966
|
+
normalizedKey,
|
|
967
|
+
fetcher,
|
|
968
|
+
options,
|
|
969
|
+
void 0,
|
|
970
|
+
void 0,
|
|
971
|
+
false,
|
|
972
|
+
this.createFetcherContext(normalizedKey, hit)
|
|
973
|
+
);
|
|
974
|
+
} catch (error) {
|
|
975
|
+
this.options.metricsCollector.increment("staleHits");
|
|
976
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
977
|
+
this.options.logger.debug?.("stale-if-error", {
|
|
978
|
+
key: normalizedKey,
|
|
979
|
+
error: this.options.formatError(error)
|
|
980
|
+
});
|
|
981
|
+
return hit.value;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
this.options.metricsCollector.increment("misses");
|
|
985
|
+
if (!fetcher) {
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true, {
|
|
989
|
+
key: normalizedKey,
|
|
990
|
+
currentValue: void 0,
|
|
991
|
+
state: "miss"
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
async readLayerEntry(layer, key) {
|
|
995
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
if (layer.getEntry) {
|
|
999
|
+
try {
|
|
1000
|
+
return await layer.getEntry(key);
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
try {
|
|
1006
|
+
return await layer.get(key);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
async backfill(key, stored, upToIndex, options) {
|
|
1012
|
+
if (upToIndex < 0) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
for (let index = 0; index <= upToIndex; index += 1) {
|
|
1016
|
+
const layer = this.options.layers[index];
|
|
1017
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
const ttl = remainingStoredTtlMs(stored) ?? this.options.resolveLayerMs(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
1021
|
+
try {
|
|
1022
|
+
await layer.set(key, stored, ttl);
|
|
1023
|
+
} catch (error) {
|
|
1024
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
this.options.metricsCollector.increment("backfills");
|
|
1028
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
1029
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
abortAllRefreshes() {
|
|
1033
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
1034
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
getAllRefreshPromises() {
|
|
1038
|
+
return [...this.backgroundRefreshes.values()];
|
|
1039
|
+
}
|
|
1040
|
+
async readFromLayers(key, options, mode) {
|
|
1041
|
+
let sawRetainableValue = false;
|
|
1042
|
+
for (let index = 0; index < this.options.layers.length; index += 1) {
|
|
1043
|
+
const layer = this.options.layers[index];
|
|
1044
|
+
if (!layer) continue;
|
|
1045
|
+
const readStart = performance.now();
|
|
1046
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
1047
|
+
const readDuration = performance.now() - readStart;
|
|
1048
|
+
this.options.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1049
|
+
if (stored === null) {
|
|
1050
|
+
this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
const resolved = resolveStoredValue(stored);
|
|
1054
|
+
if (resolved.state === "expired") {
|
|
1055
|
+
await layer.delete(key);
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
sawRetainableValue = true;
|
|
1059
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
await this.options.tagIndex.touch(key);
|
|
1063
|
+
await this.backfill(key, stored, index - 1, options);
|
|
1064
|
+
this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1065
|
+
this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1066
|
+
this.options.emit("hit", {
|
|
1067
|
+
key,
|
|
1068
|
+
layer: layer.name,
|
|
1069
|
+
state: resolved.state
|
|
1070
|
+
});
|
|
1071
|
+
return {
|
|
1072
|
+
found: true,
|
|
1073
|
+
value: resolved.value,
|
|
1074
|
+
stored,
|
|
1075
|
+
state: resolved.state,
|
|
1076
|
+
layerIndex: index,
|
|
1077
|
+
layerName: layer.name
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
if (!sawRetainableValue) {
|
|
1081
|
+
await this.options.tagIndex.remove(key);
|
|
1082
|
+
}
|
|
1083
|
+
this.options.logger.debug?.("miss", { key, mode });
|
|
1084
|
+
this.options.emit("miss", { key, mode });
|
|
1085
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
1086
|
+
}
|
|
1087
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false, fetcherContext = {
|
|
1088
|
+
key,
|
|
1089
|
+
currentValue: void 0,
|
|
1090
|
+
state: "miss"
|
|
1091
|
+
}) {
|
|
1092
|
+
const fetchTask = async () => {
|
|
1093
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
1094
|
+
if (shouldRecheckFreshLayers) {
|
|
1095
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1096
|
+
if (secondHit.found) {
|
|
1097
|
+
this.options.metricsCollector.increment("hits");
|
|
1098
|
+
return secondHit.value;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1102
|
+
};
|
|
1103
|
+
const singleFlightTask = async () => {
|
|
1104
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1105
|
+
return fetchTask();
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
1109
|
+
key,
|
|
1110
|
+
this.resolveSingleFlightOptions(),
|
|
1111
|
+
fetchTask,
|
|
1112
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext)
|
|
1113
|
+
);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
if (!this.options.isGracefulDegradationEnabled()) {
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
this.options.metricsCollector.increment("degradedOperations");
|
|
1119
|
+
this.options.logger.warn?.("single-flight-coordinator-degraded", {
|
|
1120
|
+
key,
|
|
1121
|
+
error: this.options.formatError(error)
|
|
1122
|
+
});
|
|
1123
|
+
this.options.emitError("single-flight", {
|
|
1124
|
+
key,
|
|
1125
|
+
degraded: true,
|
|
1126
|
+
error: this.options.formatError(error)
|
|
1127
|
+
});
|
|
1128
|
+
return fetchTask();
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
if (this.options.stampedePrevention === false) {
|
|
1132
|
+
return singleFlightTask();
|
|
1133
|
+
}
|
|
1134
|
+
return this.options.stampedeGuard.execute(key, singleFlightTask);
|
|
1135
|
+
}
|
|
1136
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1137
|
+
key,
|
|
1138
|
+
currentValue: void 0,
|
|
1139
|
+
state: "miss"
|
|
1140
|
+
}) {
|
|
1141
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1142
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1143
|
+
const deadline = Date.now() + timeoutMs;
|
|
1144
|
+
this.options.metricsCollector.increment("singleFlightWaits");
|
|
1145
|
+
this.options.emit("stampede-dedupe", { key });
|
|
1146
|
+
while (Date.now() < deadline) {
|
|
1147
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
1148
|
+
if (hit.found) {
|
|
1149
|
+
this.options.metricsCollector.increment("hits");
|
|
1150
|
+
return hit.value;
|
|
1151
|
+
}
|
|
1152
|
+
await this.options.sleep(pollIntervalMs);
|
|
1153
|
+
}
|
|
1154
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext);
|
|
1155
|
+
}
|
|
1156
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1157
|
+
key,
|
|
1158
|
+
currentValue: void 0,
|
|
1159
|
+
state: "miss"
|
|
1160
|
+
}) {
|
|
1161
|
+
this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1162
|
+
this.options.metricsCollector.increment("fetches");
|
|
1163
|
+
const fetchStart = Date.now();
|
|
1164
|
+
let fetched;
|
|
1165
|
+
try {
|
|
1166
|
+
fetched = await this.options.fetchRateLimiter.schedule(
|
|
1167
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1168
|
+
{ key, fetcher },
|
|
1169
|
+
() => fetcher(fetcherContext)
|
|
1170
|
+
);
|
|
1171
|
+
this.options.circuitBreakerManager.recordSuccess(key);
|
|
1172
|
+
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1175
|
+
throw error;
|
|
1176
|
+
}
|
|
1177
|
+
if (fetched === null || fetched === void 0) {
|
|
1178
|
+
if (!this.shouldNegativeCache(options)) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1182
|
+
this.options.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
1183
|
+
key,
|
|
1184
|
+
expectedClearEpoch,
|
|
1185
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1186
|
+
expectedKeyEpoch,
|
|
1187
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1188
|
+
});
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
await this.options.storeEntry(key, "empty", null, options);
|
|
1192
|
+
return null;
|
|
1193
|
+
}
|
|
1194
|
+
if (options?.shouldCache) {
|
|
1195
|
+
try {
|
|
1196
|
+
if (!options.shouldCache(fetched)) {
|
|
1197
|
+
return fetched;
|
|
1198
|
+
}
|
|
1199
|
+
} catch (error) {
|
|
1200
|
+
this.options.logger.warn?.("shouldCache-error", {
|
|
1201
|
+
key,
|
|
1202
|
+
error: this.options.formatError(error)
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1207
|
+
this.options.logger.debug?.("skip-store-after-invalidation", {
|
|
1208
|
+
key,
|
|
1209
|
+
expectedClearEpoch,
|
|
1210
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1211
|
+
expectedKeyEpoch,
|
|
1212
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1213
|
+
});
|
|
1214
|
+
return fetched;
|
|
1215
|
+
}
|
|
1216
|
+
await this.options.storeEntry(key, "value", fetched, options);
|
|
1217
|
+
return fetched;
|
|
1218
|
+
}
|
|
1219
|
+
runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
1220
|
+
this.scheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
1221
|
+
}
|
|
1222
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext = {
|
|
1223
|
+
key,
|
|
1224
|
+
currentValue: void 0,
|
|
1225
|
+
state: "miss"
|
|
1226
|
+
}) {
|
|
1227
|
+
if (!shouldStartBackgroundRefresh({
|
|
1228
|
+
isDisconnecting: this.options.isDisconnecting(),
|
|
1229
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
1230
|
+
})) {
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1234
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
1235
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
1236
|
+
const refresh = (async () => {
|
|
1237
|
+
this.options.metricsCollector.increment("refreshes");
|
|
1238
|
+
try {
|
|
1239
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1240
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch, fetcherContext);
|
|
1241
|
+
} catch (error) {
|
|
1242
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1243
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
1244
|
+
this.options.logger.warn?.("background-refresh-error", {
|
|
1245
|
+
key,
|
|
1246
|
+
error: this.options.formatError(error)
|
|
1247
|
+
});
|
|
1248
|
+
} finally {
|
|
1249
|
+
this.backgroundRefreshes.delete(key);
|
|
1250
|
+
this.backgroundRefreshAbort.delete(key);
|
|
1251
|
+
}
|
|
1252
|
+
})();
|
|
1253
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
1254
|
+
}
|
|
1255
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, fetcherContext = {
|
|
1256
|
+
key,
|
|
1257
|
+
currentValue: void 0,
|
|
1258
|
+
state: "miss"
|
|
1259
|
+
}) {
|
|
1260
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
1261
|
+
await this.fetchWithGuards(
|
|
1262
|
+
key,
|
|
1263
|
+
(context) => this.options.withTimeout(fetcher(context), timeoutMs, () => {
|
|
1264
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
1265
|
+
}),
|
|
1266
|
+
options,
|
|
1267
|
+
expectedClearEpoch,
|
|
1268
|
+
expectedKeyEpoch,
|
|
1269
|
+
false,
|
|
1270
|
+
fetcherContext
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
async runApplyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1274
|
+
return this.applyFreshReadPolicies(key, hit, options, fetcher);
|
|
1275
|
+
}
|
|
1276
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1277
|
+
const plan = planFreshReadPolicies({
|
|
1278
|
+
stored: hit.stored,
|
|
1279
|
+
hasFetcher: Boolean(fetcher),
|
|
1280
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
1281
|
+
refreshAheadMs: this.options.resolveLayerMs(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
1282
|
+
});
|
|
1283
|
+
if (plan.refreshedStored) {
|
|
1284
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1285
|
+
const layer = this.options.layers[index];
|
|
1286
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
try {
|
|
1290
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
1291
|
+
} catch (error) {
|
|
1292
|
+
await this.options.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
1297
|
+
this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options, this.createFetcherContext(key, hit));
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
createFetcherContext(key, hit) {
|
|
1301
|
+
return {
|
|
1302
|
+
key,
|
|
1303
|
+
currentValue: hit.value === null ? void 0 : hit.value,
|
|
1304
|
+
state: hit.state,
|
|
1305
|
+
layer: hit.layerName
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
resolveSingleFlightOptions() {
|
|
1309
|
+
return {
|
|
1310
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1311
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1312
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
1313
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
shouldNegativeCache(options) {
|
|
1317
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1318
|
+
}
|
|
1319
|
+
isNegativeStoredValue(stored) {
|
|
1320
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1321
|
+
}
|
|
1322
|
+
};
|
|
1323
|
+
|
|
874
1324
|
// src/internal/CacheStackSnapshotManager.ts
|
|
875
1325
|
import { constants, promises as fs } from "fs";
|
|
876
1326
|
|
|
@@ -1136,7 +1586,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1136
1586
|
await visitor({
|
|
1137
1587
|
key: exportedKey,
|
|
1138
1588
|
value: stored,
|
|
1139
|
-
ttl:
|
|
1589
|
+
ttl: remainingStoredTtlMs(stored)
|
|
1140
1590
|
});
|
|
1141
1591
|
};
|
|
1142
1592
|
if (layer.forEachKey) {
|
|
@@ -1600,7 +2050,7 @@ var MetricsCollector = class {
|
|
|
1600
2050
|
|
|
1601
2051
|
// src/internal/TtlResolver.ts
|
|
1602
2052
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
1603
|
-
var
|
|
2053
|
+
var DEFAULT_NEGATIVE_TTL_MS = 6e4;
|
|
1604
2054
|
var secureRandom = {
|
|
1605
2055
|
value() {
|
|
1606
2056
|
return randomBytes2(4).readUInt32BE(0) / 4294967296;
|
|
@@ -1627,17 +2077,17 @@ var TtlResolver = class {
|
|
|
1627
2077
|
}
|
|
1628
2078
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
1629
2079
|
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
1630
|
-
const baseTtl = kind === "empty" ? this.
|
|
2080
|
+
const baseTtl = kind === "empty" ? this.resolveLayerMs(
|
|
1631
2081
|
layerName,
|
|
1632
2082
|
options?.negativeTtl,
|
|
1633
2083
|
globalNegativeTtl,
|
|
1634
|
-
this.
|
|
1635
|
-
) : this.
|
|
2084
|
+
this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_MS
|
|
2085
|
+
) : this.resolveLayerMs(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
1636
2086
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
1637
|
-
const jitter = this.
|
|
2087
|
+
const jitter = this.resolveLayerMs(layerName, options?.ttlJitter, void 0);
|
|
1638
2088
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
1639
2089
|
}
|
|
1640
|
-
|
|
2090
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
1641
2091
|
if (override !== void 0) {
|
|
1642
2092
|
return this.readLayerNumber(layerName, override) ?? fallback;
|
|
1643
2093
|
}
|
|
@@ -1659,8 +2109,8 @@ var TtlResolver = class {
|
|
|
1659
2109
|
if (profile.hits < hotAfter) {
|
|
1660
2110
|
return ttl;
|
|
1661
2111
|
}
|
|
1662
|
-
const step = this.
|
|
1663
|
-
const maxTtl = this.
|
|
2112
|
+
const step = this.resolveLayerMs(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
2113
|
+
const maxTtl = this.resolveLayerMs(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1664
2114
|
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1665
2115
|
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1666
2116
|
}
|
|
@@ -1682,17 +2132,17 @@ var TtlResolver = class {
|
|
|
1682
2132
|
if (policy === "until-midnight") {
|
|
1683
2133
|
const nextMidnight = new Date(now);
|
|
1684
2134
|
nextMidnight.setHours(24, 0, 0, 0);
|
|
1685
|
-
return Math.max(1, Math.ceil(
|
|
2135
|
+
return Math.max(1, Math.ceil(nextMidnight.getTime() - now.getTime()));
|
|
1686
2136
|
}
|
|
1687
2137
|
if (policy === "next-hour") {
|
|
1688
2138
|
const nextHour = new Date(now);
|
|
1689
2139
|
nextHour.setMinutes(60, 0, 0);
|
|
1690
|
-
return Math.max(1, Math.ceil(
|
|
2140
|
+
return Math.max(1, Math.ceil(nextHour.getTime() - now.getTime()));
|
|
1691
2141
|
}
|
|
1692
|
-
const
|
|
1693
|
-
const
|
|
1694
|
-
const nextBoundary = Math.ceil((
|
|
1695
|
-
return Math.max(1, nextBoundary -
|
|
2142
|
+
const alignToMs = policy.alignTo;
|
|
2143
|
+
const currentMs = Date.now();
|
|
2144
|
+
const nextBoundary = Math.ceil((currentMs + 1) / alignToMs) * alignToMs;
|
|
2145
|
+
return Math.max(1, nextBoundary - currentMs);
|
|
1696
2146
|
}
|
|
1697
2147
|
readLayerNumber(layerName, value) {
|
|
1698
2148
|
if (typeof value === "number") {
|
|
@@ -1815,10 +2265,6 @@ var CacheMissError = class extends Error {
|
|
|
1815
2265
|
};
|
|
1816
2266
|
|
|
1817
2267
|
// src/CacheStack.ts
|
|
1818
|
-
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1819
|
-
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1820
|
-
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1821
|
-
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1822
2268
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1823
2269
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1824
2270
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
@@ -1898,7 +2344,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1898
2344
|
},
|
|
1899
2345
|
enqueueWriteBehind: this.enqueueWriteBehind.bind(this),
|
|
1900
2346
|
resolveFreshTtl: this.resolveFreshTtl.bind(this),
|
|
1901
|
-
|
|
2347
|
+
resolveLayerMs: this.resolveLayerMs.bind(this),
|
|
1902
2348
|
globalStaleWhileRevalidate: this.options.staleWhileRevalidate,
|
|
1903
2349
|
globalStaleIfError: this.options.staleIfError,
|
|
1904
2350
|
writePolicy: this.options.writePolicy,
|
|
@@ -1929,7 +2375,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1929
2375
|
layers: this.layers,
|
|
1930
2376
|
tagIndex: this.tagIndex,
|
|
1931
2377
|
snapshotSerializer: this.snapshotSerializer,
|
|
1932
|
-
readLayerEntry: this.readLayerEntry
|
|
2378
|
+
readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
|
|
1933
2379
|
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1934
2380
|
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
1935
2381
|
qualifyKey: this.qualifyKey.bind(this),
|
|
@@ -1937,6 +2383,41 @@ var CacheStack = class extends EventEmitter {
|
|
|
1937
2383
|
validateCacheKey,
|
|
1938
2384
|
formatError: this.formatError.bind(this)
|
|
1939
2385
|
});
|
|
2386
|
+
this.reader = new CacheStackReader({
|
|
2387
|
+
layers: this.layers,
|
|
2388
|
+
metricsCollector: this.metricsCollector,
|
|
2389
|
+
maintenance: this.maintenance,
|
|
2390
|
+
tagIndex: this.tagIndex,
|
|
2391
|
+
circuitBreakerManager: this.circuitBreakerManager,
|
|
2392
|
+
fetchRateLimiter: this.fetchRateLimiter,
|
|
2393
|
+
stampedeGuard: this.stampedeGuard,
|
|
2394
|
+
ttlResolver: this.ttlResolver,
|
|
2395
|
+
logger: this.logger,
|
|
2396
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2397
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2398
|
+
emit: (event, data) => this.emit(event, data),
|
|
2399
|
+
emitError: (operation, context) => this.emitError(operation, context),
|
|
2400
|
+
formatError: (error) => this.formatError(error),
|
|
2401
|
+
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
2402
|
+
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
2403
|
+
resolveLayerMs: (layerName, override, globalDefault, fallback) => this.resolveLayerMs(layerName, override, globalDefault, fallback),
|
|
2404
|
+
sleep: (ms) => this.sleep(ms),
|
|
2405
|
+
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
2406
|
+
isDisconnecting: () => this.isDisconnecting,
|
|
2407
|
+
isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
|
|
2408
|
+
scheduleBackgroundRefreshDispatch: (key, fetcher, options2, fetcherContext) => this.scheduleBackgroundRefresh(key, fetcher, options2, fetcherContext),
|
|
2409
|
+
stampedePrevention: options.stampedePrevention,
|
|
2410
|
+
singleFlightCoordinator: options.singleFlightCoordinator,
|
|
2411
|
+
singleFlightLeaseMs: options.singleFlightLeaseMs,
|
|
2412
|
+
singleFlightTimeoutMs: options.singleFlightTimeoutMs,
|
|
2413
|
+
singleFlightPollMs: options.singleFlightPollMs,
|
|
2414
|
+
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
2415
|
+
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
2416
|
+
negativeCaching: options.negativeCaching,
|
|
2417
|
+
refreshAhead: options.refreshAhead,
|
|
2418
|
+
circuitBreaker: options.circuitBreaker,
|
|
2419
|
+
fetcherRateLimit: options.fetcherRateLimit
|
|
2420
|
+
});
|
|
1940
2421
|
this.initializeWriteBehind(options.writeBehind);
|
|
1941
2422
|
this.startup = this.initialize();
|
|
1942
2423
|
}
|
|
@@ -1955,8 +2436,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
1955
2436
|
invalidation;
|
|
1956
2437
|
layerWriter;
|
|
1957
2438
|
snapshots;
|
|
1958
|
-
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1959
|
-
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
1960
2439
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1961
2440
|
maintenance = new CacheStackMaintenance();
|
|
1962
2441
|
ttlResolver;
|
|
@@ -1964,6 +2443,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1964
2443
|
nextOperationId = 0;
|
|
1965
2444
|
currentGeneration;
|
|
1966
2445
|
isDisconnecting = false;
|
|
2446
|
+
reader;
|
|
1967
2447
|
disconnectPromise;
|
|
1968
2448
|
/**
|
|
1969
2449
|
* Read-through cache get.
|
|
@@ -1976,51 +2456,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1976
2456
|
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1977
2457
|
this.validateWriteOptions(options);
|
|
1978
2458
|
await this.awaitStartup("get");
|
|
1979
|
-
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2459
|
+
return this.reader.getPrepared(normalizedKey, fetcher, options);
|
|
1980
2460
|
});
|
|
1981
2461
|
}
|
|
1982
|
-
async getPrepared(normalizedKey, fetcher, options) {
|
|
1983
|
-
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
1984
|
-
if (hit.found) {
|
|
1985
|
-
this.ttlResolver.recordAccess(normalizedKey);
|
|
1986
|
-
if (this.isNegativeStoredValue(hit.stored)) {
|
|
1987
|
-
this.metricsCollector.increment("negativeCacheHits");
|
|
1988
|
-
}
|
|
1989
|
-
if (hit.state === "fresh") {
|
|
1990
|
-
this.metricsCollector.increment("hits");
|
|
1991
|
-
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
1992
|
-
return hit.value;
|
|
1993
|
-
}
|
|
1994
|
-
if (hit.state === "stale-while-revalidate") {
|
|
1995
|
-
this.metricsCollector.increment("hits");
|
|
1996
|
-
this.metricsCollector.increment("staleHits");
|
|
1997
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
1998
|
-
if (fetcher) {
|
|
1999
|
-
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
2000
|
-
}
|
|
2001
|
-
return hit.value;
|
|
2002
|
-
}
|
|
2003
|
-
if (!fetcher) {
|
|
2004
|
-
this.metricsCollector.increment("hits");
|
|
2005
|
-
this.metricsCollector.increment("staleHits");
|
|
2006
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
2007
|
-
return hit.value;
|
|
2008
|
-
}
|
|
2009
|
-
try {
|
|
2010
|
-
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2011
|
-
} catch (error) {
|
|
2012
|
-
this.metricsCollector.increment("staleHits");
|
|
2013
|
-
this.metricsCollector.increment("refreshErrors");
|
|
2014
|
-
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
2015
|
-
return hit.value;
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
this.metricsCollector.increment("misses");
|
|
2019
|
-
if (!fetcher) {
|
|
2020
|
-
return null;
|
|
2021
|
-
}
|
|
2022
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2023
|
-
}
|
|
2024
2462
|
/**
|
|
2025
2463
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
2026
2464
|
* Fetches and caches the value if not already present.
|
|
@@ -2073,7 +2511,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2073
2511
|
return false;
|
|
2074
2512
|
}
|
|
2075
2513
|
/**
|
|
2076
|
-
* Returns the remaining TTL in
|
|
2514
|
+
* Returns the remaining TTL in milliseconds for the key in the fastest layer
|
|
2077
2515
|
* that has it, or null if the key is not found / has no TTL.
|
|
2078
2516
|
*/
|
|
2079
2517
|
async ttl(key) {
|
|
@@ -2171,7 +2609,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2171
2609
|
const optionsSignature = serializeOptions(entry.options);
|
|
2172
2610
|
const existing = pendingReads.get(entry.key);
|
|
2173
2611
|
if (!existing) {
|
|
2174
|
-
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2612
|
+
const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2175
2613
|
pendingReads.set(entry.key, {
|
|
2176
2614
|
promise,
|
|
2177
2615
|
fetch: entry.fetch,
|
|
@@ -2207,7 +2645,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2207
2645
|
if (keys.length === 0) {
|
|
2208
2646
|
break;
|
|
2209
2647
|
}
|
|
2210
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2648
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
|
|
2211
2649
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2212
2650
|
const key = keys[offset];
|
|
2213
2651
|
const stored = values[offset];
|
|
@@ -2223,7 +2661,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2223
2661
|
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2224
2662
|
}
|
|
2225
2663
|
await this.tagIndex.touch(key);
|
|
2226
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2664
|
+
await this.reader.backfill(key, stored, layerIndex - 1);
|
|
2227
2665
|
resultsByKey.set(key, resolved.value);
|
|
2228
2666
|
pending.delete(key);
|
|
2229
2667
|
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
@@ -2309,20 +2747,45 @@ var CacheStack = class extends EventEmitter {
|
|
|
2309
2747
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2310
2748
|
});
|
|
2311
2749
|
}
|
|
2312
|
-
async
|
|
2313
|
-
await this.observeOperation("layercache.
|
|
2750
|
+
async expireByTag(tag) {
|
|
2751
|
+
await this.observeOperation("layercache.expire_by_tag", void 0, async () => {
|
|
2752
|
+
validateTag(tag);
|
|
2753
|
+
await this.awaitStartup("expireByTag");
|
|
2754
|
+
const keys = await this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys());
|
|
2755
|
+
await this.expireKeys(keys);
|
|
2756
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
async invalidateByTags(tags, mode = "any") {
|
|
2760
|
+
await this.observeOperation("layercache.invalidate_by_tags", void 0, async () => {
|
|
2761
|
+
if (tags.length === 0) {
|
|
2762
|
+
return;
|
|
2763
|
+
}
|
|
2764
|
+
validateTags(tags);
|
|
2765
|
+
await this.awaitStartup("invalidateByTags");
|
|
2766
|
+
const keysByTag = await Promise.all(
|
|
2767
|
+
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2768
|
+
);
|
|
2769
|
+
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2770
|
+
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2771
|
+
await this.deleteKeys(keys);
|
|
2772
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2775
|
+
async expireByTags(tags, mode = "any") {
|
|
2776
|
+
await this.observeOperation("layercache.expire_by_tags", void 0, async () => {
|
|
2314
2777
|
if (tags.length === 0) {
|
|
2315
2778
|
return;
|
|
2316
2779
|
}
|
|
2317
2780
|
validateTags(tags);
|
|
2318
|
-
await this.awaitStartup("
|
|
2781
|
+
await this.awaitStartup("expireByTags");
|
|
2319
2782
|
const keysByTag = await Promise.all(
|
|
2320
2783
|
tags.map((tag) => this.invalidation.collectKeysForTag(tag, this.invalidationMaxKeys()))
|
|
2321
2784
|
);
|
|
2322
2785
|
const keys = mode === "all" ? this.invalidation.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2323
2786
|
this.invalidation.assertWithinInvalidationKeyLimit(keys.length, this.invalidationMaxKeys());
|
|
2324
|
-
await this.
|
|
2325
|
-
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "
|
|
2787
|
+
await this.expireKeys(keys);
|
|
2788
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2326
2789
|
});
|
|
2327
2790
|
}
|
|
2328
2791
|
async invalidateByPattern(pattern) {
|
|
@@ -2337,6 +2800,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
2337
2800
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2338
2801
|
});
|
|
2339
2802
|
}
|
|
2803
|
+
async expireByPattern(pattern) {
|
|
2804
|
+
await this.observeOperation("layercache.expire_by_pattern", void 0, async () => {
|
|
2805
|
+
validatePattern(pattern);
|
|
2806
|
+
await this.awaitStartup("expireByPattern");
|
|
2807
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2808
|
+
this.qualifyPattern(pattern),
|
|
2809
|
+
this.invalidationMaxKeys()
|
|
2810
|
+
);
|
|
2811
|
+
await this.expireKeys(keys);
|
|
2812
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2813
|
+
});
|
|
2814
|
+
}
|
|
2340
2815
|
async invalidateByPrefix(prefix) {
|
|
2341
2816
|
await this.observeOperation("layercache.invalidate_by_prefix", void 0, async () => {
|
|
2342
2817
|
await this.awaitStartup("invalidateByPrefix");
|
|
@@ -2346,6 +2821,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2346
2821
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
2347
2822
|
});
|
|
2348
2823
|
}
|
|
2824
|
+
async expireByPrefix(prefix) {
|
|
2825
|
+
await this.observeOperation("layercache.expire_by_prefix", void 0, async () => {
|
|
2826
|
+
await this.awaitStartup("expireByPrefix");
|
|
2827
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2828
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
2829
|
+
await this.expireKeys(keys);
|
|
2830
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "expire" });
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2349
2833
|
getMetrics() {
|
|
2350
2834
|
return this.metricsCollector.snapshot;
|
|
2351
2835
|
}
|
|
@@ -2357,7 +2841,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2357
2841
|
isLocal: Boolean(layer.isLocal),
|
|
2358
2842
|
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
2359
2843
|
})),
|
|
2360
|
-
backgroundRefreshes: this.
|
|
2844
|
+
backgroundRefreshes: this.reader.activeRefreshCount
|
|
2361
2845
|
};
|
|
2362
2846
|
}
|
|
2363
2847
|
resetMetrics() {
|
|
@@ -2422,9 +2906,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2422
2906
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2423
2907
|
await this.awaitStartup("inspect");
|
|
2424
2908
|
const foundInLayers = [];
|
|
2425
|
-
let
|
|
2426
|
-
let
|
|
2427
|
-
let
|
|
2909
|
+
let freshTtlMs = null;
|
|
2910
|
+
let staleTtlMs = null;
|
|
2911
|
+
let errorTtlMs = null;
|
|
2428
2912
|
let isStale = false;
|
|
2429
2913
|
for (const layer of this.layers) {
|
|
2430
2914
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -2441,9 +2925,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2441
2925
|
foundInLayers.push(layer.name);
|
|
2442
2926
|
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
2443
2927
|
const now = Date.now();
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2928
|
+
freshTtlMs = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.freshUntil - now)) : null;
|
|
2929
|
+
staleTtlMs = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.staleUntil - now)) : null;
|
|
2930
|
+
errorTtlMs = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil(resolved.envelope.errorUntil - now)) : null;
|
|
2447
2931
|
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
2448
2932
|
}
|
|
2449
2933
|
}
|
|
@@ -2451,7 +2935,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2451
2935
|
return null;
|
|
2452
2936
|
}
|
|
2453
2937
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
2454
|
-
return { key: userKey, foundInLayers,
|
|
2938
|
+
return { key: userKey, foundInLayers, freshTtlMs, staleTtlMs, errorTtlMs, isStale, tags };
|
|
2455
2939
|
}
|
|
2456
2940
|
async exportState() {
|
|
2457
2941
|
await this.awaitStartup("exportState");
|
|
@@ -2477,11 +2961,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2477
2961
|
await this.unsubscribeInvalidation?.();
|
|
2478
2962
|
await this.flushWriteBehindQueue();
|
|
2479
2963
|
await this.maintenance.waitForGenerationCleanup();
|
|
2480
|
-
|
|
2481
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
2482
|
-
}
|
|
2964
|
+
this.reader.abortAllRefreshes();
|
|
2483
2965
|
await Promise.allSettled(
|
|
2484
|
-
|
|
2966
|
+
this.reader.getAllRefreshPromises().map((promise) => {
|
|
2485
2967
|
let timer;
|
|
2486
2968
|
return Promise.race([
|
|
2487
2969
|
promise,
|
|
@@ -2494,8 +2976,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2494
2976
|
});
|
|
2495
2977
|
})
|
|
2496
2978
|
);
|
|
2497
|
-
this.backgroundRefreshes.clear();
|
|
2498
|
-
this.backgroundRefreshAbort.clear();
|
|
2499
2979
|
this.maintenance.disposeWriteBehindTimer();
|
|
2500
2980
|
this.fetchRateLimiter.dispose();
|
|
2501
2981
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -2511,141 +2991,36 @@ var CacheStack = class extends EventEmitter {
|
|
|
2511
2991
|
await this.handleInvalidationMessage(message);
|
|
2512
2992
|
});
|
|
2513
2993
|
}
|
|
2514
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
2515
|
-
const fetchTask = async () => {
|
|
2516
|
-
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
2517
|
-
if (shouldRecheckFreshLayers) {
|
|
2518
|
-
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2519
|
-
if (secondHit.found) {
|
|
2520
|
-
this.metricsCollector.increment("hits");
|
|
2521
|
-
return secondHit.value;
|
|
2522
|
-
}
|
|
2523
|
-
}
|
|
2524
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2525
|
-
};
|
|
2526
|
-
const singleFlightTask = async () => {
|
|
2527
|
-
if (!this.options.singleFlightCoordinator) {
|
|
2528
|
-
return fetchTask();
|
|
2529
|
-
}
|
|
2530
|
-
try {
|
|
2531
|
-
return await this.options.singleFlightCoordinator.execute(
|
|
2532
|
-
key,
|
|
2533
|
-
this.resolveSingleFlightOptions(),
|
|
2534
|
-
fetchTask,
|
|
2535
|
-
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2536
|
-
);
|
|
2537
|
-
} catch (error) {
|
|
2538
|
-
if (!this.isGracefulDegradationEnabled()) {
|
|
2539
|
-
throw error;
|
|
2540
|
-
}
|
|
2541
|
-
this.metricsCollector.increment("degradedOperations");
|
|
2542
|
-
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
2543
|
-
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
2544
|
-
return fetchTask();
|
|
2545
|
-
}
|
|
2546
|
-
};
|
|
2547
|
-
if (this.options.stampedePrevention === false) {
|
|
2548
|
-
return singleFlightTask();
|
|
2549
|
-
}
|
|
2550
|
-
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2551
|
-
}
|
|
2552
|
-
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2553
|
-
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2554
|
-
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2555
|
-
const deadline = Date.now() + timeoutMs;
|
|
2556
|
-
this.metricsCollector.increment("singleFlightWaits");
|
|
2557
|
-
this.emit("stampede-dedupe", { key });
|
|
2558
|
-
while (Date.now() < deadline) {
|
|
2559
|
-
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
2560
|
-
if (hit.found) {
|
|
2561
|
-
this.metricsCollector.increment("hits");
|
|
2562
|
-
return hit.value;
|
|
2563
|
-
}
|
|
2564
|
-
await this.sleep(pollIntervalMs);
|
|
2565
|
-
}
|
|
2566
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2567
|
-
}
|
|
2568
|
-
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2569
|
-
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2570
|
-
this.metricsCollector.increment("fetches");
|
|
2571
|
-
const fetchStart = Date.now();
|
|
2572
|
-
let fetched;
|
|
2573
|
-
try {
|
|
2574
|
-
fetched = await this.fetchRateLimiter.schedule(
|
|
2575
|
-
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
2576
|
-
{ key, fetcher },
|
|
2577
|
-
fetcher
|
|
2578
|
-
);
|
|
2579
|
-
this.circuitBreakerManager.recordSuccess(key);
|
|
2580
|
-
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
2581
|
-
} catch (error) {
|
|
2582
|
-
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
2583
|
-
throw error;
|
|
2584
|
-
}
|
|
2585
|
-
if (fetched === null || fetched === void 0) {
|
|
2586
|
-
if (!this.shouldNegativeCache(options)) {
|
|
2587
|
-
return null;
|
|
2588
|
-
}
|
|
2589
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2590
|
-
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2591
|
-
key,
|
|
2592
|
-
expectedClearEpoch,
|
|
2593
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2594
|
-
expectedKeyEpoch,
|
|
2595
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2596
|
-
});
|
|
2597
|
-
return null;
|
|
2598
|
-
}
|
|
2599
|
-
await this.storeEntry(key, "empty", null, options);
|
|
2600
|
-
return null;
|
|
2601
|
-
}
|
|
2602
|
-
if (options?.shouldCache) {
|
|
2603
|
-
try {
|
|
2604
|
-
if (!options.shouldCache(fetched)) {
|
|
2605
|
-
return fetched;
|
|
2606
|
-
}
|
|
2607
|
-
} catch (error) {
|
|
2608
|
-
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2609
|
-
}
|
|
2610
|
-
}
|
|
2611
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2612
|
-
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2613
|
-
key,
|
|
2614
|
-
expectedClearEpoch,
|
|
2615
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2616
|
-
expectedKeyEpoch,
|
|
2617
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2618
|
-
});
|
|
2619
|
-
return fetched;
|
|
2620
|
-
}
|
|
2621
|
-
await this.storeEntry(key, "value", fetched, options);
|
|
2622
|
-
return fetched;
|
|
2623
|
-
}
|
|
2624
2994
|
async storeEntry(key, kind, value, options) {
|
|
2995
|
+
const resolvedOptions = this.resolveContextOptions(key, kind, value, options);
|
|
2625
2996
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2626
2997
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2627
|
-
await this.layerWriter.writeAcrossLayers(key, kind, value,
|
|
2998
|
+
await this.layerWriter.writeAcrossLayers(key, kind, value, resolvedOptions);
|
|
2628
2999
|
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2629
3000
|
return;
|
|
2630
3001
|
}
|
|
2631
|
-
if (
|
|
2632
|
-
await this.tagIndex.track(key,
|
|
3002
|
+
if (resolvedOptions?.tags) {
|
|
3003
|
+
await this.tagIndex.track(key, resolvedOptions.tags);
|
|
2633
3004
|
} else {
|
|
2634
3005
|
await this.tagIndex.touch(key);
|
|
2635
3006
|
}
|
|
2636
3007
|
this.metricsCollector.increment("sets");
|
|
2637
|
-
this.logger.debug?.("set", { key, kind, tags:
|
|
2638
|
-
this.emit("set", { key, kind, tags:
|
|
3008
|
+
this.logger.debug?.("set", { key, kind, tags: resolvedOptions?.tags });
|
|
3009
|
+
this.emit("set", { key, kind, tags: resolvedOptions?.tags });
|
|
2639
3010
|
if (this.shouldBroadcastL1Invalidation()) {
|
|
2640
3011
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
2641
3012
|
}
|
|
2642
3013
|
}
|
|
2643
3014
|
async writeBatch(entries) {
|
|
2644
|
-
const
|
|
3015
|
+
const resolvedEntries = entries.map((entry) => ({
|
|
3016
|
+
...entry,
|
|
3017
|
+
options: this.resolveContextOptions(entry.key, "value", entry.value, entry.options)
|
|
3018
|
+
}));
|
|
3019
|
+
const { clearEpoch, entryEpochs } = await this.layerWriter.writeBatch(resolvedEntries);
|
|
2645
3020
|
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2646
3021
|
return;
|
|
2647
3022
|
}
|
|
2648
|
-
for (const entry of
|
|
3023
|
+
for (const entry of resolvedEntries) {
|
|
2649
3024
|
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2650
3025
|
continue;
|
|
2651
3026
|
}
|
|
@@ -2667,87 +3042,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2667
3042
|
});
|
|
2668
3043
|
}
|
|
2669
3044
|
}
|
|
2670
|
-
async readFromLayers(key, options, mode) {
|
|
2671
|
-
let sawRetainableValue = false;
|
|
2672
|
-
for (let index = 0; index < this.layers.length; index += 1) {
|
|
2673
|
-
const layer = this.layers[index];
|
|
2674
|
-
if (!layer) continue;
|
|
2675
|
-
const readStart = performance.now();
|
|
2676
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2677
|
-
const readDuration = performance.now() - readStart;
|
|
2678
|
-
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
2679
|
-
if (stored === null) {
|
|
2680
|
-
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
2681
|
-
continue;
|
|
2682
|
-
}
|
|
2683
|
-
const resolved = resolveStoredValue(stored);
|
|
2684
|
-
if (resolved.state === "expired") {
|
|
2685
|
-
await layer.delete(key);
|
|
2686
|
-
continue;
|
|
2687
|
-
}
|
|
2688
|
-
sawRetainableValue = true;
|
|
2689
|
-
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
2690
|
-
continue;
|
|
2691
|
-
}
|
|
2692
|
-
await this.tagIndex.touch(key);
|
|
2693
|
-
await this.backfill(key, stored, index - 1, options);
|
|
2694
|
-
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
2695
|
-
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
2696
|
-
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
2697
|
-
return {
|
|
2698
|
-
found: true,
|
|
2699
|
-
value: resolved.value,
|
|
2700
|
-
stored,
|
|
2701
|
-
state: resolved.state,
|
|
2702
|
-
layerIndex: index,
|
|
2703
|
-
layerName: layer.name
|
|
2704
|
-
};
|
|
2705
|
-
}
|
|
2706
|
-
if (!sawRetainableValue) {
|
|
2707
|
-
await this.tagIndex.remove(key);
|
|
2708
|
-
}
|
|
2709
|
-
this.logger.debug?.("miss", { key, mode });
|
|
2710
|
-
this.emit("miss", { key, mode });
|
|
2711
|
-
return { found: false, value: null, stored: null, state: "miss" };
|
|
2712
|
-
}
|
|
2713
|
-
async readLayerEntry(layer, key) {
|
|
2714
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2715
|
-
return null;
|
|
2716
|
-
}
|
|
2717
|
-
if (layer.getEntry) {
|
|
2718
|
-
try {
|
|
2719
|
-
return await layer.getEntry(key);
|
|
2720
|
-
} catch (error) {
|
|
2721
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
2722
|
-
}
|
|
2723
|
-
}
|
|
2724
|
-
try {
|
|
2725
|
-
return await layer.get(key);
|
|
2726
|
-
} catch (error) {
|
|
2727
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
async backfill(key, stored, upToIndex, options) {
|
|
2731
|
-
if (upToIndex < 0) {
|
|
2732
|
-
return;
|
|
2733
|
-
}
|
|
2734
|
-
for (let index = 0; index <= upToIndex; index += 1) {
|
|
2735
|
-
const layer = this.layers[index];
|
|
2736
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2737
|
-
continue;
|
|
2738
|
-
}
|
|
2739
|
-
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
2740
|
-
try {
|
|
2741
|
-
await layer.set(key, stored, ttl);
|
|
2742
|
-
} catch (error) {
|
|
2743
|
-
await this.handleLayerFailure(layer, "backfill", error);
|
|
2744
|
-
continue;
|
|
2745
|
-
}
|
|
2746
|
-
this.metricsCollector.increment("backfills");
|
|
2747
|
-
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
2748
|
-
this.emit("backfill", { key, layer: layer.name });
|
|
2749
|
-
}
|
|
2750
|
-
}
|
|
2751
3045
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2752
3046
|
return this.ttlResolver.resolveFreshTtl(
|
|
2753
3047
|
key,
|
|
@@ -2760,58 +3054,47 @@ var CacheStack = class extends EventEmitter {
|
|
|
2760
3054
|
value
|
|
2761
3055
|
);
|
|
2762
3056
|
}
|
|
2763
|
-
|
|
2764
|
-
return this.ttlResolver.
|
|
2765
|
-
}
|
|
2766
|
-
shouldNegativeCache(options) {
|
|
2767
|
-
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
3057
|
+
resolveLayerMs(layerName, override, globalDefault, fallback) {
|
|
3058
|
+
return this.ttlResolver.resolveLayerMs(layerName, override, globalDefault, fallback);
|
|
2768
3059
|
}
|
|
2769
|
-
|
|
2770
|
-
if (!
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
}
|
|
2774
|
-
|
|
3060
|
+
resolveContextOptions(key, kind, value, options) {
|
|
3061
|
+
if (!options?.contextOptions) {
|
|
3062
|
+
return options;
|
|
3063
|
+
}
|
|
3064
|
+
const { contextOptions, ...baseOptions } = options;
|
|
3065
|
+
let overrides;
|
|
3066
|
+
try {
|
|
3067
|
+
overrides = contextOptions({ key, value, kind });
|
|
3068
|
+
} catch (error) {
|
|
3069
|
+
throw new Error(`options.contextOptions() failed for key "${key}": ${this.formatError(error)}`);
|
|
3070
|
+
}
|
|
3071
|
+
if (!overrides) {
|
|
3072
|
+
return baseOptions;
|
|
3073
|
+
}
|
|
3074
|
+
if (!this.isPlainObject(overrides)) {
|
|
3075
|
+
throw new Error(
|
|
3076
|
+
`options.contextOptions() must return a plain object or undefined for key "${key}". Async resolvers are not supported.`
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
try {
|
|
3080
|
+
validateContextEntryOptions("options.contextOptions()", overrides);
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`options.contextOptions() returned invalid entry options for key "${key}": ${this.formatError(error)}`
|
|
3084
|
+
);
|
|
2775
3085
|
}
|
|
2776
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2777
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2778
|
-
this.backgroundRefreshAbort.set(key, false);
|
|
2779
|
-
const refresh = (async () => {
|
|
2780
|
-
this.metricsCollector.increment("refreshes");
|
|
2781
|
-
try {
|
|
2782
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2783
|
-
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2784
|
-
} catch (error) {
|
|
2785
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2786
|
-
this.metricsCollector.increment("refreshErrors");
|
|
2787
|
-
this.logger.warn?.("background-refresh-error", { key, error: this.formatError(error) });
|
|
2788
|
-
} finally {
|
|
2789
|
-
this.backgroundRefreshes.delete(key);
|
|
2790
|
-
this.backgroundRefreshAbort.delete(key);
|
|
2791
|
-
}
|
|
2792
|
-
})();
|
|
2793
|
-
this.backgroundRefreshes.set(key, refresh);
|
|
2794
|
-
}
|
|
2795
|
-
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2796
|
-
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2797
|
-
await this.fetchWithGuards(
|
|
2798
|
-
key,
|
|
2799
|
-
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2800
|
-
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2801
|
-
}),
|
|
2802
|
-
options,
|
|
2803
|
-
expectedClearEpoch,
|
|
2804
|
-
expectedKeyEpoch
|
|
2805
|
-
);
|
|
2806
|
-
}
|
|
2807
|
-
resolveSingleFlightOptions() {
|
|
2808
3086
|
return {
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2812
|
-
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
3087
|
+
...baseOptions,
|
|
3088
|
+
...overrides
|
|
2813
3089
|
};
|
|
2814
3090
|
}
|
|
3091
|
+
isPlainObject(value) {
|
|
3092
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3093
|
+
return false;
|
|
3094
|
+
}
|
|
3095
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3096
|
+
return prototype === Object.prototype || prototype === null;
|
|
3097
|
+
}
|
|
2815
3098
|
async deleteKeys(keys) {
|
|
2816
3099
|
if (keys.length === 0) {
|
|
2817
3100
|
return;
|
|
@@ -2828,6 +3111,30 @@ var CacheStack = class extends EventEmitter {
|
|
|
2828
3111
|
this.logger.debug?.("delete", { keys });
|
|
2829
3112
|
this.emit("delete", { keys });
|
|
2830
3113
|
}
|
|
3114
|
+
async expireKeys(keys) {
|
|
3115
|
+
if (keys.length === 0) {
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
3119
|
+
const foundKeys = await this.expireKeysInLayers(keys, this.layers);
|
|
3120
|
+
for (const key of keys) {
|
|
3121
|
+
if (foundKeys.has(key)) {
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
await this.tagIndex.remove(key);
|
|
3125
|
+
this.ttlResolver.deleteProfile(key);
|
|
3126
|
+
this.circuitBreakerManager.delete(key);
|
|
3127
|
+
}
|
|
3128
|
+
this.metricsCollector.increment("invalidations");
|
|
3129
|
+
this.logger.debug?.("expire", { keys });
|
|
3130
|
+
this.emit("expire", { keys });
|
|
3131
|
+
}
|
|
3132
|
+
async expireKeysInLayers(keys, layers) {
|
|
3133
|
+
if (keys.length === 0) {
|
|
3134
|
+
return /* @__PURE__ */ new Set();
|
|
3135
|
+
}
|
|
3136
|
+
return this.invalidation.expireKeysInLayers(layers, keys);
|
|
3137
|
+
}
|
|
2831
3138
|
async publishInvalidation(message) {
|
|
2832
3139
|
if (!this.options.invalidationBus) {
|
|
2833
3140
|
return;
|
|
@@ -2849,6 +3156,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
2849
3156
|
}
|
|
2850
3157
|
const keys = message.keys ?? [];
|
|
2851
3158
|
this.maintenance.bumpKeyEpochs(keys);
|
|
3159
|
+
if (message.operation === "expire") {
|
|
3160
|
+
await this.expireKeysInLayers(keys, localLayers);
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
2852
3163
|
await this.invalidation.deleteKeysFromLayers(localLayers, keys);
|
|
2853
3164
|
if (message.operation !== "write") {
|
|
2854
3165
|
for (const key of keys) {
|
|
@@ -3046,6 +3357,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
3046
3357
|
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3047
3358
|
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3048
3359
|
validateTags(options.tags);
|
|
3360
|
+
if (options.contextOptions && typeof options.contextOptions !== "function") {
|
|
3361
|
+
throw new Error("options.contextOptions must be a function.");
|
|
3362
|
+
}
|
|
3049
3363
|
}
|
|
3050
3364
|
assertActive(operation) {
|
|
3051
3365
|
if (this.isDisconnecting) {
|
|
@@ -3057,29 +3371,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
3057
3371
|
await this.startup;
|
|
3058
3372
|
this.assertActive(operation);
|
|
3059
3373
|
}
|
|
3374
|
+
async readLayerEntry(layer, key) {
|
|
3375
|
+
return this.reader.readLayerEntry(layer, key);
|
|
3376
|
+
}
|
|
3377
|
+
scheduleBackgroundRefresh(key, fetcher, options, fetcherContext) {
|
|
3378
|
+
this.reader.runScheduleBackgroundRefresh(key, fetcher, options, fetcherContext);
|
|
3379
|
+
}
|
|
3060
3380
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3061
|
-
|
|
3062
|
-
stored: hit.stored,
|
|
3063
|
-
hasFetcher: Boolean(fetcher),
|
|
3064
|
-
slidingTtl: options?.slidingTtl ?? false,
|
|
3065
|
-
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3066
|
-
});
|
|
3067
|
-
if (plan.refreshedStored) {
|
|
3068
|
-
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3069
|
-
const layer = this.layers[index];
|
|
3070
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3071
|
-
continue;
|
|
3072
|
-
}
|
|
3073
|
-
try {
|
|
3074
|
-
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3075
|
-
} catch (error) {
|
|
3076
|
-
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
}
|
|
3080
|
-
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3081
|
-
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3082
|
-
}
|
|
3381
|
+
return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
|
|
3083
3382
|
}
|
|
3084
3383
|
shouldSkipLayer(layer) {
|
|
3085
3384
|
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
@@ -3121,9 +3420,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3121
3420
|
}
|
|
3122
3421
|
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
3123
3422
|
}
|
|
3124
|
-
isNegativeStoredValue(stored) {
|
|
3125
|
-
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
3126
|
-
}
|
|
3127
3423
|
emitError(operation, context) {
|
|
3128
3424
|
this.logger.error?.(operation, context);
|
|
3129
3425
|
if (this.listenerCount("error") > 0) {
|
|
@@ -3223,7 +3519,7 @@ var RedisInvalidationBus = class {
|
|
|
3223
3519
|
}
|
|
3224
3520
|
const candidate = value;
|
|
3225
3521
|
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
3226
|
-
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
3522
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "expire" || candidate.operation === "clear";
|
|
3227
3523
|
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
3228
3524
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
3229
3525
|
}
|
|
@@ -3554,7 +3850,7 @@ var RedisLayer = class {
|
|
|
3554
3850
|
const payload = await this.encodePayload(serialized);
|
|
3555
3851
|
const normalizedKey = this.withPrefix(entry.key);
|
|
3556
3852
|
if (entry.ttl && entry.ttl > 0) {
|
|
3557
|
-
pipeline.set(normalizedKey, payload, "
|
|
3853
|
+
pipeline.set(normalizedKey, payload, "PX", entry.ttl);
|
|
3558
3854
|
} else {
|
|
3559
3855
|
pipeline.set(normalizedKey, payload);
|
|
3560
3856
|
}
|
|
@@ -3569,7 +3865,7 @@ var RedisLayer = class {
|
|
|
3569
3865
|
if (ttl && ttl > 0) {
|
|
3570
3866
|
await this.runCommand(
|
|
3571
3867
|
`set(${this.displayKey(key)})`,
|
|
3572
|
-
() => this.client.set(normalizedKey, payload, "
|
|
3868
|
+
() => this.client.set(normalizedKey, payload, "PX", ttl)
|
|
3573
3869
|
);
|
|
3574
3870
|
return;
|
|
3575
3871
|
}
|
|
@@ -3598,7 +3894,10 @@ var RedisLayer = class {
|
|
|
3598
3894
|
}
|
|
3599
3895
|
async ttl(key) {
|
|
3600
3896
|
this.validateKey(key);
|
|
3601
|
-
const remaining = await this.runCommand(
|
|
3897
|
+
const remaining = await this.runCommand(
|
|
3898
|
+
`ttl(${this.displayKey(key)})`,
|
|
3899
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
3900
|
+
);
|
|
3602
3901
|
if (remaining < 0) {
|
|
3603
3902
|
return null;
|
|
3604
3903
|
}
|
|
@@ -3749,12 +4048,12 @@ var RedisLayer = class {
|
|
|
3749
4048
|
const payload = await this.encodePayload(serialized);
|
|
3750
4049
|
const ttl = await this.runCommand(
|
|
3751
4050
|
`rewrite-ttl(${this.displayKey(key)})`,
|
|
3752
|
-
() => this.client.
|
|
4051
|
+
() => this.client.pttl(this.withPrefix(key))
|
|
3753
4052
|
);
|
|
3754
4053
|
if (ttl > 0) {
|
|
3755
4054
|
await this.runCommand(
|
|
3756
4055
|
`rewrite-set(${this.displayKey(key)})`,
|
|
3757
|
-
() => this.client.set(this.withPrefix(key), payload, "
|
|
4056
|
+
() => this.client.set(this.withPrefix(key), payload, "PX", ttl)
|
|
3758
4057
|
);
|
|
3759
4058
|
return;
|
|
3760
4059
|
}
|
|
@@ -4065,7 +4364,7 @@ var DiskLayer = class {
|
|
|
4065
4364
|
const entry = {
|
|
4066
4365
|
key,
|
|
4067
4366
|
value,
|
|
4068
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl
|
|
4367
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl : null
|
|
4069
4368
|
};
|
|
4070
4369
|
const payload = this.serializer.serialize(entry);
|
|
4071
4370
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
@@ -4110,7 +4409,7 @@ var DiskLayer = class {
|
|
|
4110
4409
|
if (entry.expiresAt === null) {
|
|
4111
4410
|
return null;
|
|
4112
4411
|
}
|
|
4113
|
-
const remaining = Math.ceil(
|
|
4412
|
+
const remaining = Math.ceil(entry.expiresAt - Date.now());
|
|
4114
4413
|
if (remaining <= 0) {
|
|
4115
4414
|
return null;
|
|
4116
4415
|
}
|
|
@@ -4400,7 +4699,7 @@ var MemcachedLayer = class {
|
|
|
4400
4699
|
this.validateKey(key);
|
|
4401
4700
|
const payload = this.serializer.serialize(value);
|
|
4402
4701
|
await this.client.set(this.withPrefix(key), payload, {
|
|
4403
|
-
expires: ttl && ttl > 0 ? ttl : void 0
|
|
4702
|
+
expires: ttl && ttl > 0 ? Math.ceil(ttl / 1e3) : void 0
|
|
4404
4703
|
});
|
|
4405
4704
|
}
|
|
4406
4705
|
async has(key) {
|