layercache 1.3.2 → 1.3.4
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 +157 -81
- package/dist/{chunk-GJBKCFE6.js → chunk-5RCAX2BQ.js} +9 -9
- package/dist/{chunk-BQLL6IM5.js → chunk-BORDQ3LA.js} +135 -0
- package/dist/cli.cjs +77 -5
- package/dist/cli.js +37 -7
- package/dist/{edge-CUHTP9Bc.d.cts → edge-DKkrQ_Ky.d.cts} +3 -14
- package/dist/{edge-CUHTP9Bc.d.ts → edge-DKkrQ_Ky.d.ts} +3 -14
- package/dist/edge.cjs +9 -9
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +1 -1
- package/dist/index.cjs +511 -387
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +491 -480
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
RedisTagIndex
|
|
3
|
-
|
|
2
|
+
RedisTagIndex,
|
|
3
|
+
validateAdaptiveTtlOptions,
|
|
4
|
+
validateCacheKey,
|
|
5
|
+
validateCircuitBreakerOptions,
|
|
6
|
+
validateLayerNumberOption,
|
|
7
|
+
validateNonNegativeNumber,
|
|
8
|
+
validatePattern,
|
|
9
|
+
validatePositiveNumber,
|
|
10
|
+
validateRateLimitOptions,
|
|
11
|
+
validateTag,
|
|
12
|
+
validateTags,
|
|
13
|
+
validateTtlPolicy
|
|
14
|
+
} from "./chunk-BORDQ3LA.js";
|
|
4
15
|
import {
|
|
5
16
|
MemoryLayer,
|
|
6
17
|
TagIndex,
|
|
7
18
|
createHonoCacheMiddleware
|
|
8
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-5RCAX2BQ.js";
|
|
9
20
|
import {
|
|
10
21
|
PatternMatcher,
|
|
11
22
|
createStoredValueEnvelope,
|
|
@@ -715,6 +726,7 @@ var CacheStackLayerWriter = class {
|
|
|
715
726
|
};
|
|
716
727
|
|
|
717
728
|
// src/internal/CacheStackMaintenance.ts
|
|
729
|
+
var MAX_KEY_EPOCHS = 5e4;
|
|
718
730
|
var CacheStackMaintenance = class {
|
|
719
731
|
keyEpochs = /* @__PURE__ */ new Map();
|
|
720
732
|
writeBehindQueue = [];
|
|
@@ -758,6 +770,7 @@ var CacheStackMaintenance = class {
|
|
|
758
770
|
for (const key of keys) {
|
|
759
771
|
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
760
772
|
}
|
|
773
|
+
this.pruneKeyEpochsIfNeeded();
|
|
761
774
|
}
|
|
762
775
|
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
763
776
|
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
@@ -810,6 +823,16 @@ var CacheStackMaintenance = class {
|
|
|
810
823
|
async waitForGenerationCleanup() {
|
|
811
824
|
await this.generationCleanupPromise;
|
|
812
825
|
}
|
|
826
|
+
pruneKeyEpochsIfNeeded() {
|
|
827
|
+
if (this.keyEpochs.size <= MAX_KEY_EPOCHS) {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const sorted = [...this.keyEpochs.entries()].sort((a, b) => a[1] - b[1]);
|
|
831
|
+
const toDelete = Math.ceil(sorted.length * 0.1);
|
|
832
|
+
for (let i = 0; i < toDelete; i++) {
|
|
833
|
+
this.keyEpochs.delete(sorted[i][0]);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
813
836
|
};
|
|
814
837
|
|
|
815
838
|
// src/internal/CacheStackRuntimePolicy.ts
|
|
@@ -848,17 +871,377 @@ function planFreshReadPolicies({
|
|
|
848
871
|
};
|
|
849
872
|
}
|
|
850
873
|
|
|
874
|
+
// src/internal/CacheStackReader.ts
|
|
875
|
+
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
876
|
+
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
877
|
+
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
878
|
+
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
879
|
+
var CacheStackReader = class {
|
|
880
|
+
constructor(options) {
|
|
881
|
+
this.options = options;
|
|
882
|
+
}
|
|
883
|
+
options;
|
|
884
|
+
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
885
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
886
|
+
get activeRefreshCount() {
|
|
887
|
+
return this.backgroundRefreshes.size;
|
|
888
|
+
}
|
|
889
|
+
async getPrepared(normalizedKey, fetcher, options) {
|
|
890
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
891
|
+
if (hit.found) {
|
|
892
|
+
this.options.ttlResolver.recordAccess(normalizedKey);
|
|
893
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
894
|
+
this.options.metricsCollector.increment("negativeCacheHits");
|
|
895
|
+
}
|
|
896
|
+
if (hit.state === "fresh") {
|
|
897
|
+
this.options.metricsCollector.increment("hits");
|
|
898
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
899
|
+
return hit.value;
|
|
900
|
+
}
|
|
901
|
+
if (hit.state === "stale-while-revalidate") {
|
|
902
|
+
this.options.metricsCollector.increment("hits");
|
|
903
|
+
this.options.metricsCollector.increment("staleHits");
|
|
904
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
905
|
+
if (fetcher) {
|
|
906
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
907
|
+
}
|
|
908
|
+
return hit.value;
|
|
909
|
+
}
|
|
910
|
+
if (!fetcher) {
|
|
911
|
+
this.options.metricsCollector.increment("hits");
|
|
912
|
+
this.options.metricsCollector.increment("staleHits");
|
|
913
|
+
this.options.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
914
|
+
return hit.value;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
918
|
+
} catch (error) {
|
|
919
|
+
this.options.metricsCollector.increment("staleHits");
|
|
920
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
921
|
+
this.options.logger.debug?.("stale-if-error", {
|
|
922
|
+
key: normalizedKey,
|
|
923
|
+
error: this.options.formatError(error)
|
|
924
|
+
});
|
|
925
|
+
return hit.value;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
this.options.metricsCollector.increment("misses");
|
|
929
|
+
if (!fetcher) {
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
933
|
+
}
|
|
934
|
+
async readLayerEntry(layer, key) {
|
|
935
|
+
if (this.options.shouldSkipLayer(layer)) {
|
|
936
|
+
return null;
|
|
937
|
+
}
|
|
938
|
+
if (layer.getEntry) {
|
|
939
|
+
try {
|
|
940
|
+
return await layer.getEntry(key);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
try {
|
|
946
|
+
return await layer.get(key);
|
|
947
|
+
} catch (error) {
|
|
948
|
+
return this.options.handleLayerFailure(layer, "read", error);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
async backfill(key, stored, upToIndex, options) {
|
|
952
|
+
if (upToIndex < 0) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
for (let index = 0; index <= upToIndex; index += 1) {
|
|
956
|
+
const layer = this.options.layers[index];
|
|
957
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
const ttl = remainingStoredTtlSeconds(stored) ?? this.options.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
961
|
+
try {
|
|
962
|
+
await layer.set(key, stored, ttl);
|
|
963
|
+
} catch (error) {
|
|
964
|
+
await this.options.handleLayerFailure(layer, "backfill", error);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
this.options.metricsCollector.increment("backfills");
|
|
968
|
+
this.options.logger.debug?.("backfill", { key, layer: layer.name });
|
|
969
|
+
this.options.emit("backfill", { key, layer: layer.name });
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
abortAllRefreshes() {
|
|
973
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
974
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
getAllRefreshPromises() {
|
|
978
|
+
return [...this.backgroundRefreshes.values()];
|
|
979
|
+
}
|
|
980
|
+
async readFromLayers(key, options, mode) {
|
|
981
|
+
let sawRetainableValue = false;
|
|
982
|
+
for (let index = 0; index < this.options.layers.length; index += 1) {
|
|
983
|
+
const layer = this.options.layers[index];
|
|
984
|
+
if (!layer) continue;
|
|
985
|
+
const readStart = performance.now();
|
|
986
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
987
|
+
const readDuration = performance.now() - readStart;
|
|
988
|
+
this.options.metricsCollector.recordLatency(layer.name, readDuration);
|
|
989
|
+
if (stored === null) {
|
|
990
|
+
this.options.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const resolved = resolveStoredValue(stored);
|
|
994
|
+
if (resolved.state === "expired") {
|
|
995
|
+
await layer.delete(key);
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
sawRetainableValue = true;
|
|
999
|
+
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
await this.options.tagIndex.touch(key);
|
|
1003
|
+
await this.backfill(key, stored, index - 1, options);
|
|
1004
|
+
this.options.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
1005
|
+
this.options.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
1006
|
+
this.options.emit("hit", {
|
|
1007
|
+
key,
|
|
1008
|
+
layer: layer.name,
|
|
1009
|
+
state: resolved.state
|
|
1010
|
+
});
|
|
1011
|
+
return {
|
|
1012
|
+
found: true,
|
|
1013
|
+
value: resolved.value,
|
|
1014
|
+
stored,
|
|
1015
|
+
state: resolved.state,
|
|
1016
|
+
layerIndex: index,
|
|
1017
|
+
layerName: layer.name
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
if (!sawRetainableValue) {
|
|
1021
|
+
await this.options.tagIndex.remove(key);
|
|
1022
|
+
}
|
|
1023
|
+
this.options.logger.debug?.("miss", { key, mode });
|
|
1024
|
+
this.options.emit("miss", { key, mode });
|
|
1025
|
+
return { found: false, value: null, stored: null, state: "miss" };
|
|
1026
|
+
}
|
|
1027
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
1028
|
+
const fetchTask = async () => {
|
|
1029
|
+
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
1030
|
+
if (shouldRecheckFreshLayers) {
|
|
1031
|
+
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1032
|
+
if (secondHit.found) {
|
|
1033
|
+
this.options.metricsCollector.increment("hits");
|
|
1034
|
+
return secondHit.value;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1038
|
+
};
|
|
1039
|
+
const singleFlightTask = async () => {
|
|
1040
|
+
if (!this.options.singleFlightCoordinator) {
|
|
1041
|
+
return fetchTask();
|
|
1042
|
+
}
|
|
1043
|
+
try {
|
|
1044
|
+
return await this.options.singleFlightCoordinator.execute(
|
|
1045
|
+
key,
|
|
1046
|
+
this.resolveSingleFlightOptions(),
|
|
1047
|
+
fetchTask,
|
|
1048
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1049
|
+
);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (!this.options.isGracefulDegradationEnabled()) {
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
this.options.metricsCollector.increment("degradedOperations");
|
|
1055
|
+
this.options.logger.warn?.("single-flight-coordinator-degraded", {
|
|
1056
|
+
key,
|
|
1057
|
+
error: this.options.formatError(error)
|
|
1058
|
+
});
|
|
1059
|
+
this.options.emitError("single-flight", {
|
|
1060
|
+
key,
|
|
1061
|
+
degraded: true,
|
|
1062
|
+
error: this.options.formatError(error)
|
|
1063
|
+
});
|
|
1064
|
+
return fetchTask();
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
if (this.options.stampedePrevention === false) {
|
|
1068
|
+
return singleFlightTask();
|
|
1069
|
+
}
|
|
1070
|
+
return this.options.stampedeGuard.execute(key, singleFlightTask);
|
|
1071
|
+
}
|
|
1072
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1073
|
+
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1074
|
+
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1075
|
+
const deadline = Date.now() + timeoutMs;
|
|
1076
|
+
this.options.metricsCollector.increment("singleFlightWaits");
|
|
1077
|
+
this.options.emit("stampede-dedupe", { key });
|
|
1078
|
+
while (Date.now() < deadline) {
|
|
1079
|
+
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
1080
|
+
if (hit.found) {
|
|
1081
|
+
this.options.metricsCollector.increment("hits");
|
|
1082
|
+
return hit.value;
|
|
1083
|
+
}
|
|
1084
|
+
await this.options.sleep(pollIntervalMs);
|
|
1085
|
+
}
|
|
1086
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1087
|
+
}
|
|
1088
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1089
|
+
this.options.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1090
|
+
this.options.metricsCollector.increment("fetches");
|
|
1091
|
+
const fetchStart = Date.now();
|
|
1092
|
+
let fetched;
|
|
1093
|
+
try {
|
|
1094
|
+
fetched = await this.options.fetchRateLimiter.schedule(
|
|
1095
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1096
|
+
{ key, fetcher },
|
|
1097
|
+
fetcher
|
|
1098
|
+
);
|
|
1099
|
+
this.options.circuitBreakerManager.recordSuccess(key);
|
|
1100
|
+
this.options.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1101
|
+
} catch (error) {
|
|
1102
|
+
this.options.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
1103
|
+
throw error;
|
|
1104
|
+
}
|
|
1105
|
+
if (fetched === null || fetched === void 0) {
|
|
1106
|
+
if (!this.shouldNegativeCache(options)) {
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1110
|
+
this.options.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
1111
|
+
key,
|
|
1112
|
+
expectedClearEpoch,
|
|
1113
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1114
|
+
expectedKeyEpoch,
|
|
1115
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1116
|
+
});
|
|
1117
|
+
return null;
|
|
1118
|
+
}
|
|
1119
|
+
await this.options.storeEntry(key, "empty", null, options);
|
|
1120
|
+
return null;
|
|
1121
|
+
}
|
|
1122
|
+
if (options?.shouldCache) {
|
|
1123
|
+
try {
|
|
1124
|
+
if (!options.shouldCache(fetched)) {
|
|
1125
|
+
return fetched;
|
|
1126
|
+
}
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
this.options.logger.warn?.("shouldCache-error", {
|
|
1129
|
+
key,
|
|
1130
|
+
error: this.options.formatError(error)
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
if (this.options.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
1135
|
+
this.options.logger.debug?.("skip-store-after-invalidation", {
|
|
1136
|
+
key,
|
|
1137
|
+
expectedClearEpoch,
|
|
1138
|
+
clearEpoch: this.options.maintenance.currentClearEpoch(),
|
|
1139
|
+
expectedKeyEpoch,
|
|
1140
|
+
keyEpoch: this.options.maintenance.currentKeyEpoch(key)
|
|
1141
|
+
});
|
|
1142
|
+
return fetched;
|
|
1143
|
+
}
|
|
1144
|
+
await this.options.storeEntry(key, "value", fetched, options);
|
|
1145
|
+
return fetched;
|
|
1146
|
+
}
|
|
1147
|
+
runScheduleBackgroundRefresh(key, fetcher, options) {
|
|
1148
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1149
|
+
}
|
|
1150
|
+
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
1151
|
+
if (!shouldStartBackgroundRefresh({
|
|
1152
|
+
isDisconnecting: this.options.isDisconnecting(),
|
|
1153
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
1154
|
+
})) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const clearEpoch = this.options.maintenance.currentClearEpoch();
|
|
1158
|
+
const keyEpoch = this.options.maintenance.currentKeyEpoch(key);
|
|
1159
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
1160
|
+
const refresh = (async () => {
|
|
1161
|
+
this.options.metricsCollector.increment("refreshes");
|
|
1162
|
+
try {
|
|
1163
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1164
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
1167
|
+
this.options.metricsCollector.increment("refreshErrors");
|
|
1168
|
+
this.options.logger.warn?.("background-refresh-error", {
|
|
1169
|
+
key,
|
|
1170
|
+
error: this.options.formatError(error)
|
|
1171
|
+
});
|
|
1172
|
+
} finally {
|
|
1173
|
+
this.backgroundRefreshes.delete(key);
|
|
1174
|
+
this.backgroundRefreshAbort.delete(key);
|
|
1175
|
+
}
|
|
1176
|
+
})();
|
|
1177
|
+
this.backgroundRefreshes.set(key, refresh);
|
|
1178
|
+
}
|
|
1179
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1180
|
+
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
1181
|
+
await this.fetchWithGuards(
|
|
1182
|
+
key,
|
|
1183
|
+
() => this.options.withTimeout(fetcher(), timeoutMs, () => {
|
|
1184
|
+
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
1185
|
+
}),
|
|
1186
|
+
options,
|
|
1187
|
+
expectedClearEpoch,
|
|
1188
|
+
expectedKeyEpoch
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
async runApplyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1192
|
+
return this.applyFreshReadPolicies(key, hit, options, fetcher);
|
|
1193
|
+
}
|
|
1194
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1195
|
+
const plan = planFreshReadPolicies({
|
|
1196
|
+
stored: hit.stored,
|
|
1197
|
+
hasFetcher: Boolean(fetcher),
|
|
1198
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
1199
|
+
refreshAheadSeconds: this.options.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
1200
|
+
});
|
|
1201
|
+
if (plan.refreshedStored) {
|
|
1202
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1203
|
+
const layer = this.options.layers[index];
|
|
1204
|
+
if (!layer || this.options.shouldSkipLayer(layer)) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
try {
|
|
1208
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
await this.options.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
1215
|
+
this.options.scheduleBackgroundRefreshDispatch(key, fetcher, options);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
resolveSingleFlightOptions() {
|
|
1219
|
+
return {
|
|
1220
|
+
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1221
|
+
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1222
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
1223
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
shouldNegativeCache(options) {
|
|
1227
|
+
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1228
|
+
}
|
|
1229
|
+
isNegativeStoredValue(stored) {
|
|
1230
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
|
|
851
1234
|
// src/internal/CacheStackSnapshotManager.ts
|
|
852
|
-
import { randomBytes } from "crypto";
|
|
853
1235
|
import { constants, promises as fs } from "fs";
|
|
854
|
-
import path from "path";
|
|
855
1236
|
|
|
856
1237
|
// src/internal/CacheSnapshotFile.ts
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1238
|
+
import { randomBytes } from "crypto";
|
|
1239
|
+
import { rename, unlink } from "fs/promises";
|
|
1240
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
1241
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
1242
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
860
1243
|
}
|
|
861
|
-
async function findExistingAncestor(directory, fs3,
|
|
1244
|
+
async function findExistingAncestor(directory, fs3, path) {
|
|
862
1245
|
let current = directory;
|
|
863
1246
|
while (true) {
|
|
864
1247
|
try {
|
|
@@ -869,7 +1252,7 @@ async function findExistingAncestor(directory, fs3, path2) {
|
|
|
869
1252
|
throw error;
|
|
870
1253
|
}
|
|
871
1254
|
}
|
|
872
|
-
const parent =
|
|
1255
|
+
const parent = path.dirname(current);
|
|
873
1256
|
if (parent === current) {
|
|
874
1257
|
return current;
|
|
875
1258
|
}
|
|
@@ -884,36 +1267,36 @@ async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = p
|
|
|
884
1267
|
throw new Error("filePath must not contain null bytes.");
|
|
885
1268
|
}
|
|
886
1269
|
const { promises: fs3 } = await import("fs");
|
|
887
|
-
const
|
|
888
|
-
const resolved =
|
|
889
|
-
const baseDir = snapshotBaseDir === false ? false :
|
|
1270
|
+
const path = await import("path");
|
|
1271
|
+
const resolved = path.resolve(filePath);
|
|
1272
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
890
1273
|
if (baseDir === false) {
|
|
891
1274
|
return resolved;
|
|
892
1275
|
}
|
|
893
1276
|
await fs3.mkdir(baseDir, { recursive: true });
|
|
894
1277
|
const realBaseDir = await fs3.realpath(baseDir);
|
|
895
|
-
if (!isWithinSnapshotBase(realBaseDir, resolved,
|
|
1278
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
896
1279
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
897
1280
|
}
|
|
898
1281
|
if (mode === "read") {
|
|
899
1282
|
const realTarget = await fs3.realpath(resolved);
|
|
900
|
-
if (!isWithinSnapshotBase(realBaseDir, realTarget,
|
|
1283
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
901
1284
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
902
1285
|
}
|
|
903
1286
|
return realTarget;
|
|
904
1287
|
}
|
|
905
|
-
const parentDir =
|
|
906
|
-
const existingAncestor = await findExistingAncestor(parentDir, fs3,
|
|
1288
|
+
const parentDir = path.dirname(resolved);
|
|
1289
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs3, path);
|
|
907
1290
|
const realExistingAncestor = await fs3.realpath(existingAncestor);
|
|
908
|
-
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor,
|
|
1291
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
909
1292
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
910
1293
|
}
|
|
911
1294
|
await fs3.mkdir(parentDir, { recursive: true });
|
|
912
1295
|
const realParentDir = await fs3.realpath(parentDir);
|
|
913
|
-
if (!isWithinSnapshotBase(realBaseDir, realParentDir,
|
|
1296
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
914
1297
|
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
915
1298
|
}
|
|
916
|
-
const targetPath =
|
|
1299
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
917
1300
|
try {
|
|
918
1301
|
const existing = await fs3.lstat(targetPath);
|
|
919
1302
|
if (existing.isSymbolicLink()) {
|
|
@@ -948,6 +1331,17 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
948
1331
|
}
|
|
949
1332
|
return Buffer.concat(chunks).toString("utf8");
|
|
950
1333
|
}
|
|
1334
|
+
function atomicWriteTempPath(targetPath) {
|
|
1335
|
+
return `${targetPath}.tmp-${randomBytes(8).toString("hex")}`;
|
|
1336
|
+
}
|
|
1337
|
+
async function commitAtomicWrite(tempPath, targetPath) {
|
|
1338
|
+
try {
|
|
1339
|
+
await rename(tempPath, targetPath);
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
await unlink(tempPath).catch(() => void 0);
|
|
1342
|
+
throw error;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
951
1345
|
|
|
952
1346
|
// src/internal/StructuredDataSanitizer.ts
|
|
953
1347
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
@@ -1026,10 +1420,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1026
1420
|
}
|
|
1027
1421
|
async persistToFile(filePath, snapshotBaseDir, maxEntries) {
|
|
1028
1422
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1029
|
-
const tempPath =
|
|
1030
|
-
path.dirname(targetPath),
|
|
1031
|
-
`.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
|
|
1032
|
-
);
|
|
1423
|
+
const tempPath = atomicWriteTempPath(targetPath);
|
|
1033
1424
|
let handle;
|
|
1034
1425
|
try {
|
|
1035
1426
|
handle = await fs.open(tempPath, "wx");
|
|
@@ -1044,7 +1435,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1044
1435
|
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
1045
1436
|
await openedHandle.close();
|
|
1046
1437
|
handle = void 0;
|
|
1047
|
-
await
|
|
1438
|
+
await commitAtomicWrite(tempPath, targetPath);
|
|
1048
1439
|
} catch (error) {
|
|
1049
1440
|
await handle?.close().catch(() => void 0);
|
|
1050
1441
|
await fs.unlink(tempPath).catch(() => void 0);
|
|
@@ -1138,130 +1529,6 @@ var CacheStackSnapshotManager = class {
|
|
|
1138
1529
|
}
|
|
1139
1530
|
};
|
|
1140
1531
|
|
|
1141
|
-
// src/internal/CacheStackValidation.ts
|
|
1142
|
-
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1143
|
-
var MAX_PATTERN_LENGTH = 1024;
|
|
1144
|
-
var MAX_TAGS_PER_OPERATION = 128;
|
|
1145
|
-
function validatePositiveNumber(name, value) {
|
|
1146
|
-
if (value === void 0) {
|
|
1147
|
-
return;
|
|
1148
|
-
}
|
|
1149
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
1150
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
function validateNonNegativeNumber(name, value) {
|
|
1154
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
1155
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
function validateLayerNumberOption(name, value) {
|
|
1159
|
-
if (value === void 0) {
|
|
1160
|
-
return;
|
|
1161
|
-
}
|
|
1162
|
-
if (typeof value === "number") {
|
|
1163
|
-
validateNonNegativeNumber(name, value);
|
|
1164
|
-
return;
|
|
1165
|
-
}
|
|
1166
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1167
|
-
if (layerValue === void 0) {
|
|
1168
|
-
continue;
|
|
1169
|
-
}
|
|
1170
|
-
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
function validateRateLimitOptions(name, options) {
|
|
1174
|
-
if (!options) {
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1178
|
-
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1179
|
-
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1180
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1181
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1182
|
-
}
|
|
1183
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1184
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
function validateCacheKey(key) {
|
|
1188
|
-
if (key.length === 0) {
|
|
1189
|
-
throw new Error("Cache key must not be empty.");
|
|
1190
|
-
}
|
|
1191
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1192
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1193
|
-
}
|
|
1194
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1195
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
1196
|
-
}
|
|
1197
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
1198
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
1199
|
-
}
|
|
1200
|
-
return key;
|
|
1201
|
-
}
|
|
1202
|
-
function validateTag(tag) {
|
|
1203
|
-
if (tag.length === 0) {
|
|
1204
|
-
throw new Error("Cache tag must not be empty.");
|
|
1205
|
-
}
|
|
1206
|
-
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
1207
|
-
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1208
|
-
}
|
|
1209
|
-
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
1210
|
-
throw new Error("Cache tag contains unsupported control characters.");
|
|
1211
|
-
}
|
|
1212
|
-
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
1213
|
-
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
1214
|
-
}
|
|
1215
|
-
return tag;
|
|
1216
|
-
}
|
|
1217
|
-
function validateTags(tags) {
|
|
1218
|
-
if (!tags) {
|
|
1219
|
-
return;
|
|
1220
|
-
}
|
|
1221
|
-
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
1222
|
-
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
1223
|
-
}
|
|
1224
|
-
for (const tag of tags) {
|
|
1225
|
-
validateTag(tag);
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
function validatePattern(pattern) {
|
|
1229
|
-
if (pattern.length === 0) {
|
|
1230
|
-
throw new Error("Pattern must not be empty.");
|
|
1231
|
-
}
|
|
1232
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1233
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
1234
|
-
}
|
|
1235
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
1236
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
function validateTtlPolicy(name, policy) {
|
|
1240
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
if ("alignTo" in policy) {
|
|
1244
|
-
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
throw new Error(`${name} is invalid.`);
|
|
1248
|
-
}
|
|
1249
|
-
function validateAdaptiveTtlOptions(options) {
|
|
1250
|
-
if (!options || options === true) {
|
|
1251
|
-
return;
|
|
1252
|
-
}
|
|
1253
|
-
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1254
|
-
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1255
|
-
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1256
|
-
}
|
|
1257
|
-
function validateCircuitBreakerOptions(options) {
|
|
1258
|
-
if (!options) {
|
|
1259
|
-
return;
|
|
1260
|
-
}
|
|
1261
|
-
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1262
|
-
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
1532
|
// src/internal/CircuitBreakerManager.ts
|
|
1266
1533
|
var CircuitBreakerManager = class {
|
|
1267
1534
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -1359,6 +1626,7 @@ var CircuitBreakerManager = class {
|
|
|
1359
1626
|
|
|
1360
1627
|
// src/internal/FetchRateLimiter.ts
|
|
1361
1628
|
var MAX_BUCKETS = 1e4;
|
|
1629
|
+
var MAX_QUEUE_PER_BUCKET = 1e4;
|
|
1362
1630
|
var FetchRateLimiter = class {
|
|
1363
1631
|
buckets = /* @__PURE__ */ new Map();
|
|
1364
1632
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -1367,6 +1635,7 @@ var FetchRateLimiter = class {
|
|
|
1367
1635
|
nextFetcherBucketId = 0;
|
|
1368
1636
|
drainTimer;
|
|
1369
1637
|
isDisposed = false;
|
|
1638
|
+
rateLimitBypasses = 0;
|
|
1370
1639
|
async schedule(options, context, task) {
|
|
1371
1640
|
if (this.isDisposed) {
|
|
1372
1641
|
throw new Error("FetchRateLimiter has been disposed.");
|
|
@@ -1381,6 +1650,11 @@ var FetchRateLimiter = class {
|
|
|
1381
1650
|
return new Promise((resolve2, reject) => {
|
|
1382
1651
|
const bucketKey = this.resolveBucketKey(normalized, context);
|
|
1383
1652
|
const queue = this.queuesByBucket.get(bucketKey) ?? [];
|
|
1653
|
+
if (queue.length >= MAX_QUEUE_PER_BUCKET) {
|
|
1654
|
+
this.rateLimitBypasses += 1;
|
|
1655
|
+
task().then(resolve2, reject);
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1384
1658
|
queue.push({
|
|
1385
1659
|
bucketKey,
|
|
1386
1660
|
options: normalized,
|
|
@@ -1685,7 +1959,13 @@ var MetricsCollector = class {
|
|
|
1685
1959
|
};
|
|
1686
1960
|
|
|
1687
1961
|
// src/internal/TtlResolver.ts
|
|
1962
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
1688
1963
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
1964
|
+
var secureRandom = {
|
|
1965
|
+
value() {
|
|
1966
|
+
return randomBytes2(4).readUInt32BE(0) / 4294967296;
|
|
1967
|
+
}
|
|
1968
|
+
};
|
|
1689
1969
|
var TtlResolver = class {
|
|
1690
1970
|
accessProfiles = /* @__PURE__ */ new Map();
|
|
1691
1971
|
maxProfileEntries;
|
|
@@ -1748,7 +2028,7 @@ var TtlResolver = class {
|
|
|
1748
2028
|
if (!ttl || ttl <= 0 || !jitter || jitter <= 0) {
|
|
1749
2029
|
return ttl;
|
|
1750
2030
|
}
|
|
1751
|
-
const delta = (
|
|
2031
|
+
const delta = (secureRandom.value() * 2 - 1) * jitter;
|
|
1752
2032
|
return Math.max(1, Math.round(ttl + delta));
|
|
1753
2033
|
}
|
|
1754
2034
|
resolvePolicyTtl(key, value, policy) {
|
|
@@ -1895,10 +2175,6 @@ var CacheMissError = class extends Error {
|
|
|
1895
2175
|
};
|
|
1896
2176
|
|
|
1897
2177
|
// src/CacheStack.ts
|
|
1898
|
-
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
1899
|
-
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1900
|
-
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1901
|
-
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1902
2178
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1903
2179
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1904
2180
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
@@ -2009,7 +2285,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2009
2285
|
layers: this.layers,
|
|
2010
2286
|
tagIndex: this.tagIndex,
|
|
2011
2287
|
snapshotSerializer: this.snapshotSerializer,
|
|
2012
|
-
readLayerEntry: this.readLayerEntry
|
|
2288
|
+
readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
|
|
2013
2289
|
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2014
2290
|
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2015
2291
|
qualifyKey: this.qualifyKey.bind(this),
|
|
@@ -2017,6 +2293,41 @@ var CacheStack = class extends EventEmitter {
|
|
|
2017
2293
|
validateCacheKey,
|
|
2018
2294
|
formatError: this.formatError.bind(this)
|
|
2019
2295
|
});
|
|
2296
|
+
this.reader = new CacheStackReader({
|
|
2297
|
+
layers: this.layers,
|
|
2298
|
+
metricsCollector: this.metricsCollector,
|
|
2299
|
+
maintenance: this.maintenance,
|
|
2300
|
+
tagIndex: this.tagIndex,
|
|
2301
|
+
circuitBreakerManager: this.circuitBreakerManager,
|
|
2302
|
+
fetchRateLimiter: this.fetchRateLimiter,
|
|
2303
|
+
stampedeGuard: this.stampedeGuard,
|
|
2304
|
+
ttlResolver: this.ttlResolver,
|
|
2305
|
+
logger: this.logger,
|
|
2306
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2307
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2308
|
+
emit: (event, data) => this.emit(event, data),
|
|
2309
|
+
emitError: (operation, context) => this.emitError(operation, context),
|
|
2310
|
+
formatError: (error) => this.formatError(error),
|
|
2311
|
+
storeEntry: (key, kind, value, options2) => this.storeEntry(key, kind, value, options2),
|
|
2312
|
+
recordCircuitFailure: (key, options2, error) => this.recordCircuitFailure(key, options2, error),
|
|
2313
|
+
resolveLayerSeconds: (layerName, override, globalDefault, fallback) => this.resolveLayerSeconds(layerName, override, globalDefault, fallback),
|
|
2314
|
+
sleep: (ms) => this.sleep(ms),
|
|
2315
|
+
withTimeout: (promise, ms, createError) => this.withTimeout(promise, ms, createError),
|
|
2316
|
+
isDisconnecting: () => this.isDisconnecting,
|
|
2317
|
+
isGracefulDegradationEnabled: () => this.isGracefulDegradationEnabled(),
|
|
2318
|
+
scheduleBackgroundRefreshDispatch: (key, fetcher, options2) => this.scheduleBackgroundRefresh(key, fetcher, options2),
|
|
2319
|
+
stampedePrevention: options.stampedePrevention,
|
|
2320
|
+
singleFlightCoordinator: options.singleFlightCoordinator,
|
|
2321
|
+
singleFlightLeaseMs: options.singleFlightLeaseMs,
|
|
2322
|
+
singleFlightTimeoutMs: options.singleFlightTimeoutMs,
|
|
2323
|
+
singleFlightPollMs: options.singleFlightPollMs,
|
|
2324
|
+
singleFlightRenewIntervalMs: options.singleFlightRenewIntervalMs,
|
|
2325
|
+
backgroundRefreshTimeoutMs: options.backgroundRefreshTimeoutMs,
|
|
2326
|
+
negativeCaching: options.negativeCaching,
|
|
2327
|
+
refreshAhead: options.refreshAhead,
|
|
2328
|
+
circuitBreaker: options.circuitBreaker,
|
|
2329
|
+
fetcherRateLimit: options.fetcherRateLimit
|
|
2330
|
+
});
|
|
2020
2331
|
this.initializeWriteBehind(options.writeBehind);
|
|
2021
2332
|
this.startup = this.initialize();
|
|
2022
2333
|
}
|
|
@@ -2035,8 +2346,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2035
2346
|
invalidation;
|
|
2036
2347
|
layerWriter;
|
|
2037
2348
|
snapshots;
|
|
2038
|
-
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2039
|
-
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2040
2349
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2041
2350
|
maintenance = new CacheStackMaintenance();
|
|
2042
2351
|
ttlResolver;
|
|
@@ -2044,6 +2353,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2044
2353
|
nextOperationId = 0;
|
|
2045
2354
|
currentGeneration;
|
|
2046
2355
|
isDisconnecting = false;
|
|
2356
|
+
reader;
|
|
2047
2357
|
disconnectPromise;
|
|
2048
2358
|
/**
|
|
2049
2359
|
* Read-through cache get.
|
|
@@ -2056,51 +2366,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2056
2366
|
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
2057
2367
|
this.validateWriteOptions(options);
|
|
2058
2368
|
await this.awaitStartup("get");
|
|
2059
|
-
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2369
|
+
return this.reader.getPrepared(normalizedKey, fetcher, options);
|
|
2060
2370
|
});
|
|
2061
2371
|
}
|
|
2062
|
-
async getPrepared(normalizedKey, fetcher, options) {
|
|
2063
|
-
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
2064
|
-
if (hit.found) {
|
|
2065
|
-
this.ttlResolver.recordAccess(normalizedKey);
|
|
2066
|
-
if (this.isNegativeStoredValue(hit.stored)) {
|
|
2067
|
-
this.metricsCollector.increment("negativeCacheHits");
|
|
2068
|
-
}
|
|
2069
|
-
if (hit.state === "fresh") {
|
|
2070
|
-
this.metricsCollector.increment("hits");
|
|
2071
|
-
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
2072
|
-
return hit.value;
|
|
2073
|
-
}
|
|
2074
|
-
if (hit.state === "stale-while-revalidate") {
|
|
2075
|
-
this.metricsCollector.increment("hits");
|
|
2076
|
-
this.metricsCollector.increment("staleHits");
|
|
2077
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
2078
|
-
if (fetcher) {
|
|
2079
|
-
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
2080
|
-
}
|
|
2081
|
-
return hit.value;
|
|
2082
|
-
}
|
|
2083
|
-
if (!fetcher) {
|
|
2084
|
-
this.metricsCollector.increment("hits");
|
|
2085
|
-
this.metricsCollector.increment("staleHits");
|
|
2086
|
-
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
2087
|
-
return hit.value;
|
|
2088
|
-
}
|
|
2089
|
-
try {
|
|
2090
|
-
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
2091
|
-
} catch (error) {
|
|
2092
|
-
this.metricsCollector.increment("staleHits");
|
|
2093
|
-
this.metricsCollector.increment("refreshErrors");
|
|
2094
|
-
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
2095
|
-
return hit.value;
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
|
-
this.metricsCollector.increment("misses");
|
|
2099
|
-
if (!fetcher) {
|
|
2100
|
-
return null;
|
|
2101
|
-
}
|
|
2102
|
-
return this.fetchWithGuards(normalizedKey, fetcher, options, void 0, void 0, true);
|
|
2103
|
-
}
|
|
2104
2372
|
/**
|
|
2105
2373
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
2106
2374
|
* Fetches and caches the value if not already present.
|
|
@@ -2251,7 +2519,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2251
2519
|
const optionsSignature = serializeOptions(entry.options);
|
|
2252
2520
|
const existing = pendingReads.get(entry.key);
|
|
2253
2521
|
if (!existing) {
|
|
2254
|
-
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2522
|
+
const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2255
2523
|
pendingReads.set(entry.key, {
|
|
2256
2524
|
promise,
|
|
2257
2525
|
fetch: entry.fetch,
|
|
@@ -2287,7 +2555,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2287
2555
|
if (keys.length === 0) {
|
|
2288
2556
|
break;
|
|
2289
2557
|
}
|
|
2290
|
-
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
2558
|
+
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.reader.readLayerEntry(layer, key)));
|
|
2291
2559
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2292
2560
|
const key = keys[offset];
|
|
2293
2561
|
const stored = values[offset];
|
|
@@ -2303,7 +2571,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2303
2571
|
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2304
2572
|
}
|
|
2305
2573
|
await this.tagIndex.touch(key);
|
|
2306
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2574
|
+
await this.reader.backfill(key, stored, layerIndex - 1);
|
|
2307
2575
|
resultsByKey.set(key, resolved.value);
|
|
2308
2576
|
pending.delete(key);
|
|
2309
2577
|
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
@@ -2437,7 +2705,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2437
2705
|
isLocal: Boolean(layer.isLocal),
|
|
2438
2706
|
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
2439
2707
|
})),
|
|
2440
|
-
backgroundRefreshes: this.
|
|
2708
|
+
backgroundRefreshes: this.reader.activeRefreshCount
|
|
2441
2709
|
};
|
|
2442
2710
|
}
|
|
2443
2711
|
resetMetrics() {
|
|
@@ -2557,11 +2825,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2557
2825
|
await this.unsubscribeInvalidation?.();
|
|
2558
2826
|
await this.flushWriteBehindQueue();
|
|
2559
2827
|
await this.maintenance.waitForGenerationCleanup();
|
|
2560
|
-
|
|
2561
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
2562
|
-
}
|
|
2828
|
+
this.reader.abortAllRefreshes();
|
|
2563
2829
|
await Promise.allSettled(
|
|
2564
|
-
|
|
2830
|
+
this.reader.getAllRefreshPromises().map((promise) => {
|
|
2565
2831
|
let timer;
|
|
2566
2832
|
return Promise.race([
|
|
2567
2833
|
promise,
|
|
@@ -2574,8 +2840,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2574
2840
|
});
|
|
2575
2841
|
})
|
|
2576
2842
|
);
|
|
2577
|
-
this.backgroundRefreshes.clear();
|
|
2578
|
-
this.backgroundRefreshAbort.clear();
|
|
2579
2843
|
this.maintenance.disposeWriteBehindTimer();
|
|
2580
2844
|
this.fetchRateLimiter.dispose();
|
|
2581
2845
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -2591,116 +2855,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2591
2855
|
await this.handleInvalidationMessage(message);
|
|
2592
2856
|
});
|
|
2593
2857
|
}
|
|
2594
|
-
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch, initialMissConfirmed = false) {
|
|
2595
|
-
const fetchTask = async () => {
|
|
2596
|
-
const shouldRecheckFreshLayers = !(initialMissConfirmed && this.options.singleFlightCoordinator);
|
|
2597
|
-
if (shouldRecheckFreshLayers) {
|
|
2598
|
-
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2599
|
-
if (secondHit.found) {
|
|
2600
|
-
this.metricsCollector.increment("hits");
|
|
2601
|
-
return secondHit.value;
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2605
|
-
};
|
|
2606
|
-
const singleFlightTask = async () => {
|
|
2607
|
-
if (!this.options.singleFlightCoordinator) {
|
|
2608
|
-
return fetchTask();
|
|
2609
|
-
}
|
|
2610
|
-
try {
|
|
2611
|
-
return await this.options.singleFlightCoordinator.execute(
|
|
2612
|
-
key,
|
|
2613
|
-
this.resolveSingleFlightOptions(),
|
|
2614
|
-
fetchTask,
|
|
2615
|
-
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2616
|
-
);
|
|
2617
|
-
} catch (error) {
|
|
2618
|
-
if (!this.isGracefulDegradationEnabled()) {
|
|
2619
|
-
throw error;
|
|
2620
|
-
}
|
|
2621
|
-
this.metricsCollector.increment("degradedOperations");
|
|
2622
|
-
this.logger.warn?.("single-flight-coordinator-degraded", { key, error: this.formatError(error) });
|
|
2623
|
-
this.emitError("single-flight", { key, degraded: true, error: this.formatError(error) });
|
|
2624
|
-
return fetchTask();
|
|
2625
|
-
}
|
|
2626
|
-
};
|
|
2627
|
-
if (this.options.stampedePrevention === false) {
|
|
2628
|
-
return singleFlightTask();
|
|
2629
|
-
}
|
|
2630
|
-
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2631
|
-
}
|
|
2632
|
-
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2633
|
-
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2634
|
-
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2635
|
-
const deadline = Date.now() + timeoutMs;
|
|
2636
|
-
this.metricsCollector.increment("singleFlightWaits");
|
|
2637
|
-
this.emit("stampede-dedupe", { key });
|
|
2638
|
-
while (Date.now() < deadline) {
|
|
2639
|
-
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
2640
|
-
if (hit.found) {
|
|
2641
|
-
this.metricsCollector.increment("hits");
|
|
2642
|
-
return hit.value;
|
|
2643
|
-
}
|
|
2644
|
-
await this.sleep(pollIntervalMs);
|
|
2645
|
-
}
|
|
2646
|
-
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2647
|
-
}
|
|
2648
|
-
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2649
|
-
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2650
|
-
this.metricsCollector.increment("fetches");
|
|
2651
|
-
const fetchStart = Date.now();
|
|
2652
|
-
let fetched;
|
|
2653
|
-
try {
|
|
2654
|
-
fetched = await this.fetchRateLimiter.schedule(
|
|
2655
|
-
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
2656
|
-
{ key, fetcher },
|
|
2657
|
-
fetcher
|
|
2658
|
-
);
|
|
2659
|
-
this.circuitBreakerManager.recordSuccess(key);
|
|
2660
|
-
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
2661
|
-
} catch (error) {
|
|
2662
|
-
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
2663
|
-
throw error;
|
|
2664
|
-
}
|
|
2665
|
-
if (fetched === null || fetched === void 0) {
|
|
2666
|
-
if (!this.shouldNegativeCache(options)) {
|
|
2667
|
-
return null;
|
|
2668
|
-
}
|
|
2669
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2670
|
-
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2671
|
-
key,
|
|
2672
|
-
expectedClearEpoch,
|
|
2673
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2674
|
-
expectedKeyEpoch,
|
|
2675
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2676
|
-
});
|
|
2677
|
-
return null;
|
|
2678
|
-
}
|
|
2679
|
-
await this.storeEntry(key, "empty", null, options);
|
|
2680
|
-
return null;
|
|
2681
|
-
}
|
|
2682
|
-
if (options?.shouldCache) {
|
|
2683
|
-
try {
|
|
2684
|
-
if (!options.shouldCache(fetched)) {
|
|
2685
|
-
return fetched;
|
|
2686
|
-
}
|
|
2687
|
-
} catch (error) {
|
|
2688
|
-
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2692
|
-
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2693
|
-
key,
|
|
2694
|
-
expectedClearEpoch,
|
|
2695
|
-
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2696
|
-
expectedKeyEpoch,
|
|
2697
|
-
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2698
|
-
});
|
|
2699
|
-
return fetched;
|
|
2700
|
-
}
|
|
2701
|
-
await this.storeEntry(key, "value", fetched, options);
|
|
2702
|
-
return fetched;
|
|
2703
|
-
}
|
|
2704
2858
|
async storeEntry(key, kind, value, options) {
|
|
2705
2859
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2706
2860
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
@@ -2747,87 +2901,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2747
2901
|
});
|
|
2748
2902
|
}
|
|
2749
2903
|
}
|
|
2750
|
-
async readFromLayers(key, options, mode) {
|
|
2751
|
-
let sawRetainableValue = false;
|
|
2752
|
-
for (let index = 0; index < this.layers.length; index += 1) {
|
|
2753
|
-
const layer = this.layers[index];
|
|
2754
|
-
if (!layer) continue;
|
|
2755
|
-
const readStart = performance.now();
|
|
2756
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2757
|
-
const readDuration = performance.now() - readStart;
|
|
2758
|
-
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
2759
|
-
if (stored === null) {
|
|
2760
|
-
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
2761
|
-
continue;
|
|
2762
|
-
}
|
|
2763
|
-
const resolved = resolveStoredValue(stored);
|
|
2764
|
-
if (resolved.state === "expired") {
|
|
2765
|
-
await layer.delete(key);
|
|
2766
|
-
continue;
|
|
2767
|
-
}
|
|
2768
|
-
sawRetainableValue = true;
|
|
2769
|
-
if (mode === "fresh-only" && resolved.state !== "fresh") {
|
|
2770
|
-
continue;
|
|
2771
|
-
}
|
|
2772
|
-
await this.tagIndex.touch(key);
|
|
2773
|
-
await this.backfill(key, stored, index - 1, options);
|
|
2774
|
-
this.metricsCollector.incrementLayer("hitsByLayer", layer.name);
|
|
2775
|
-
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
2776
|
-
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
2777
|
-
return {
|
|
2778
|
-
found: true,
|
|
2779
|
-
value: resolved.value,
|
|
2780
|
-
stored,
|
|
2781
|
-
state: resolved.state,
|
|
2782
|
-
layerIndex: index,
|
|
2783
|
-
layerName: layer.name
|
|
2784
|
-
};
|
|
2785
|
-
}
|
|
2786
|
-
if (!sawRetainableValue) {
|
|
2787
|
-
await this.tagIndex.remove(key);
|
|
2788
|
-
}
|
|
2789
|
-
this.logger.debug?.("miss", { key, mode });
|
|
2790
|
-
this.emit("miss", { key, mode });
|
|
2791
|
-
return { found: false, value: null, stored: null, state: "miss" };
|
|
2792
|
-
}
|
|
2793
|
-
async readLayerEntry(layer, key) {
|
|
2794
|
-
if (this.shouldSkipLayer(layer)) {
|
|
2795
|
-
return null;
|
|
2796
|
-
}
|
|
2797
|
-
if (layer.getEntry) {
|
|
2798
|
-
try {
|
|
2799
|
-
return await layer.getEntry(key);
|
|
2800
|
-
} catch (error) {
|
|
2801
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2804
|
-
try {
|
|
2805
|
-
return await layer.get(key);
|
|
2806
|
-
} catch (error) {
|
|
2807
|
-
return this.handleLayerFailure(layer, "read", error);
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
async backfill(key, stored, upToIndex, options) {
|
|
2811
|
-
if (upToIndex < 0) {
|
|
2812
|
-
return;
|
|
2813
|
-
}
|
|
2814
|
-
for (let index = 0; index <= upToIndex; index += 1) {
|
|
2815
|
-
const layer = this.layers[index];
|
|
2816
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2817
|
-
continue;
|
|
2818
|
-
}
|
|
2819
|
-
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
2820
|
-
try {
|
|
2821
|
-
await layer.set(key, stored, ttl);
|
|
2822
|
-
} catch (error) {
|
|
2823
|
-
await this.handleLayerFailure(layer, "backfill", error);
|
|
2824
|
-
continue;
|
|
2825
|
-
}
|
|
2826
|
-
this.metricsCollector.increment("backfills");
|
|
2827
|
-
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
2828
|
-
this.emit("backfill", { key, layer: layer.name });
|
|
2829
|
-
}
|
|
2830
|
-
}
|
|
2831
2904
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2832
2905
|
return this.ttlResolver.resolveFreshTtl(
|
|
2833
2906
|
key,
|
|
@@ -2843,55 +2916,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2843
2916
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
2844
2917
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
2845
2918
|
}
|
|
2846
|
-
shouldNegativeCache(options) {
|
|
2847
|
-
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2848
|
-
}
|
|
2849
|
-
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2850
|
-
if (!shouldStartBackgroundRefresh({
|
|
2851
|
-
isDisconnecting: this.isDisconnecting,
|
|
2852
|
-
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
2853
|
-
})) {
|
|
2854
|
-
return;
|
|
2855
|
-
}
|
|
2856
|
-
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2857
|
-
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2858
|
-
this.backgroundRefreshAbort.set(key, false);
|
|
2859
|
-
const refresh = (async () => {
|
|
2860
|
-
this.metricsCollector.increment("refreshes");
|
|
2861
|
-
try {
|
|
2862
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2863
|
-
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2864
|
-
} catch (error) {
|
|
2865
|
-
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2866
|
-
this.metricsCollector.increment("refreshErrors");
|
|
2867
|
-
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
2868
|
-
} finally {
|
|
2869
|
-
this.backgroundRefreshes.delete(key);
|
|
2870
|
-
this.backgroundRefreshAbort.delete(key);
|
|
2871
|
-
}
|
|
2872
|
-
})();
|
|
2873
|
-
this.backgroundRefreshes.set(key, refresh);
|
|
2874
|
-
}
|
|
2875
|
-
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2876
|
-
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2877
|
-
await this.fetchWithGuards(
|
|
2878
|
-
key,
|
|
2879
|
-
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2880
|
-
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2881
|
-
}),
|
|
2882
|
-
options,
|
|
2883
|
-
expectedClearEpoch,
|
|
2884
|
-
expectedKeyEpoch
|
|
2885
|
-
);
|
|
2886
|
-
}
|
|
2887
|
-
resolveSingleFlightOptions() {
|
|
2888
|
-
return {
|
|
2889
|
-
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
2890
|
-
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
2891
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2892
|
-
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
2893
|
-
};
|
|
2894
|
-
}
|
|
2895
2919
|
async deleteKeys(keys) {
|
|
2896
2920
|
if (keys.length === 0) {
|
|
2897
2921
|
return;
|
|
@@ -3137,32 +3161,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
3137
3161
|
await this.startup;
|
|
3138
3162
|
this.assertActive(operation);
|
|
3139
3163
|
}
|
|
3164
|
+
async readLayerEntry(layer, key) {
|
|
3165
|
+
return this.reader.readLayerEntry(layer, key);
|
|
3166
|
+
}
|
|
3167
|
+
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
3168
|
+
this.reader.runScheduleBackgroundRefresh(key, fetcher, options);
|
|
3169
|
+
}
|
|
3140
3170
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
3141
|
-
|
|
3142
|
-
stored: hit.stored,
|
|
3143
|
-
hasFetcher: Boolean(fetcher),
|
|
3144
|
-
slidingTtl: options?.slidingTtl ?? false,
|
|
3145
|
-
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3146
|
-
});
|
|
3147
|
-
if (plan.refreshedStored) {
|
|
3148
|
-
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
3149
|
-
const layer = this.layers[index];
|
|
3150
|
-
if (!layer || this.shouldSkipLayer(layer)) {
|
|
3151
|
-
continue;
|
|
3152
|
-
}
|
|
3153
|
-
try {
|
|
3154
|
-
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
3155
|
-
} catch (error) {
|
|
3156
|
-
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
3157
|
-
}
|
|
3158
|
-
}
|
|
3159
|
-
}
|
|
3160
|
-
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
3161
|
-
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
3162
|
-
}
|
|
3171
|
+
return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
|
|
3163
3172
|
}
|
|
3164
3173
|
shouldSkipLayer(layer) {
|
|
3165
|
-
|
|
3174
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
3175
|
+
const skip = shouldSkipLayer(degradedUntil);
|
|
3176
|
+
if (!skip && degradedUntil !== void 0) {
|
|
3177
|
+
this.layerDegradedUntil.delete(layer.name);
|
|
3178
|
+
}
|
|
3179
|
+
return skip;
|
|
3166
3180
|
}
|
|
3167
3181
|
async handleLayerFailure(layer, operation, error) {
|
|
3168
3182
|
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
@@ -3196,9 +3210,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3196
3210
|
}
|
|
3197
3211
|
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
3198
3212
|
}
|
|
3199
|
-
isNegativeStoredValue(stored) {
|
|
3200
|
-
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
3201
|
-
}
|
|
3202
3213
|
emitError(operation, context) {
|
|
3203
3214
|
this.logger.error?.(operation, context);
|
|
3204
3215
|
if (this.listenerCount("error") > 0) {
|
|
@@ -3964,12 +3975,12 @@ var RedisLayer = class {
|
|
|
3964
3975
|
};
|
|
3965
3976
|
|
|
3966
3977
|
// src/layers/DiskLayer.ts
|
|
3967
|
-
import { createHash as createHash2, randomBytes as
|
|
3978
|
+
import { createHash as createHash2, randomBytes as randomBytes4 } from "crypto";
|
|
3968
3979
|
import { promises as fs2 } from "fs";
|
|
3969
3980
|
import { join, resolve } from "path";
|
|
3970
3981
|
|
|
3971
3982
|
// src/internal/PayloadProtection.ts
|
|
3972
|
-
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as
|
|
3983
|
+
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
3973
3984
|
var MAGIC_ENCRYPTED = Buffer.from("LCP1:");
|
|
3974
3985
|
var MAGIC_SIGNED = Buffer.from("LCS1:");
|
|
3975
3986
|
var ALGORITHM = "aes-256-gcm";
|
|
@@ -4033,7 +4044,7 @@ var PayloadProtection = class {
|
|
|
4033
4044
|
}
|
|
4034
4045
|
// ── Encryption (AES-256-GCM) ──────────────────────────────────────────
|
|
4035
4046
|
encrypt(plaintext, key) {
|
|
4036
|
-
const iv =
|
|
4047
|
+
const iv = randomBytes3(IV_LENGTH);
|
|
4037
4048
|
const cipher = createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
4038
4049
|
const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
4039
4050
|
const authTag = cipher.getAuthTag();
|
|
@@ -4146,7 +4157,7 @@ var DiskLayer = class {
|
|
|
4146
4157
|
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "utf8");
|
|
4147
4158
|
const protectedPayload = this.protection.protect(raw);
|
|
4148
4159
|
const targetPath = this.keyToPath(key);
|
|
4149
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
4160
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomBytes4(8).toString("hex")}.tmp`;
|
|
4150
4161
|
try {
|
|
4151
4162
|
await fs2.writeFile(tempPath, protectedPayload);
|
|
4152
4163
|
await fs2.rename(tempPath, targetPath);
|