layercache 1.3.3 → 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 +1 -1
- 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.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +411 -325
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +411 -325
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -871,6 +871,366 @@ function planFreshReadPolicies({
|
|
|
871
871
|
};
|
|
872
872
|
}
|
|
873
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
|
+
|
|
874
1234
|
// src/internal/CacheStackSnapshotManager.ts
|
|
875
1235
|
import { constants, promises as fs } from "fs";
|
|
876
1236
|
|
|
@@ -1815,10 +2175,6 @@ var CacheMissError = class extends Error {
|
|
|
1815
2175
|
};
|
|
1816
2176
|
|
|
1817
2177
|
// 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
2178
|
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1823
2179
|
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1824
2180
|
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
@@ -1929,7 +2285,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1929
2285
|
layers: this.layers,
|
|
1930
2286
|
tagIndex: this.tagIndex,
|
|
1931
2287
|
snapshotSerializer: this.snapshotSerializer,
|
|
1932
|
-
readLayerEntry: this.readLayerEntry
|
|
2288
|
+
readLayerEntry: (layer, key) => this.reader.readLayerEntry(layer, key),
|
|
1933
2289
|
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1934
2290
|
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
1935
2291
|
qualifyKey: this.qualifyKey.bind(this),
|
|
@@ -1937,6 +2293,41 @@ var CacheStack = class extends EventEmitter {
|
|
|
1937
2293
|
validateCacheKey,
|
|
1938
2294
|
formatError: this.formatError.bind(this)
|
|
1939
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
|
+
});
|
|
1940
2331
|
this.initializeWriteBehind(options.writeBehind);
|
|
1941
2332
|
this.startup = this.initialize();
|
|
1942
2333
|
}
|
|
@@ -1955,8 +2346,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
1955
2346
|
invalidation;
|
|
1956
2347
|
layerWriter;
|
|
1957
2348
|
snapshots;
|
|
1958
|
-
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1959
|
-
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
1960
2349
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1961
2350
|
maintenance = new CacheStackMaintenance();
|
|
1962
2351
|
ttlResolver;
|
|
@@ -1964,6 +2353,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1964
2353
|
nextOperationId = 0;
|
|
1965
2354
|
currentGeneration;
|
|
1966
2355
|
isDisconnecting = false;
|
|
2356
|
+
reader;
|
|
1967
2357
|
disconnectPromise;
|
|
1968
2358
|
/**
|
|
1969
2359
|
* Read-through cache get.
|
|
@@ -1976,51 +2366,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1976
2366
|
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1977
2367
|
this.validateWriteOptions(options);
|
|
1978
2368
|
await this.awaitStartup("get");
|
|
1979
|
-
return this.getPrepared(normalizedKey, fetcher, options);
|
|
2369
|
+
return this.reader.getPrepared(normalizedKey, fetcher, options);
|
|
1980
2370
|
});
|
|
1981
2371
|
}
|
|
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
2372
|
/**
|
|
2025
2373
|
* Alias for `get(key, fetcher, options)` — explicit get-or-set pattern.
|
|
2026
2374
|
* Fetches and caches the value if not already present.
|
|
@@ -2171,7 +2519,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2171
2519
|
const optionsSignature = serializeOptions(entry.options);
|
|
2172
2520
|
const existing = pendingReads.get(entry.key);
|
|
2173
2521
|
if (!existing) {
|
|
2174
|
-
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2522
|
+
const promise = this.reader.getPrepared(entry.key, entry.fetch, entry.options);
|
|
2175
2523
|
pendingReads.set(entry.key, {
|
|
2176
2524
|
promise,
|
|
2177
2525
|
fetch: entry.fetch,
|
|
@@ -2207,7 +2555,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2207
2555
|
if (keys.length === 0) {
|
|
2208
2556
|
break;
|
|
2209
2557
|
}
|
|
2210
|
-
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)));
|
|
2211
2559
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
2212
2560
|
const key = keys[offset];
|
|
2213
2561
|
const stored = values[offset];
|
|
@@ -2223,7 +2571,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2223
2571
|
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2224
2572
|
}
|
|
2225
2573
|
await this.tagIndex.touch(key);
|
|
2226
|
-
await this.backfill(key, stored, layerIndex - 1);
|
|
2574
|
+
await this.reader.backfill(key, stored, layerIndex - 1);
|
|
2227
2575
|
resultsByKey.set(key, resolved.value);
|
|
2228
2576
|
pending.delete(key);
|
|
2229
2577
|
this.metricsCollector.increment("hits", indexesByKey.get(key)?.length ?? 1);
|
|
@@ -2357,7 +2705,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2357
2705
|
isLocal: Boolean(layer.isLocal),
|
|
2358
2706
|
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
2359
2707
|
})),
|
|
2360
|
-
backgroundRefreshes: this.
|
|
2708
|
+
backgroundRefreshes: this.reader.activeRefreshCount
|
|
2361
2709
|
};
|
|
2362
2710
|
}
|
|
2363
2711
|
resetMetrics() {
|
|
@@ -2477,11 +2825,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2477
2825
|
await this.unsubscribeInvalidation?.();
|
|
2478
2826
|
await this.flushWriteBehindQueue();
|
|
2479
2827
|
await this.maintenance.waitForGenerationCleanup();
|
|
2480
|
-
|
|
2481
|
-
this.backgroundRefreshAbort.set(key, true);
|
|
2482
|
-
}
|
|
2828
|
+
this.reader.abortAllRefreshes();
|
|
2483
2829
|
await Promise.allSettled(
|
|
2484
|
-
|
|
2830
|
+
this.reader.getAllRefreshPromises().map((promise) => {
|
|
2485
2831
|
let timer;
|
|
2486
2832
|
return Promise.race([
|
|
2487
2833
|
promise,
|
|
@@ -2494,8 +2840,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2494
2840
|
});
|
|
2495
2841
|
})
|
|
2496
2842
|
);
|
|
2497
|
-
this.backgroundRefreshes.clear();
|
|
2498
|
-
this.backgroundRefreshAbort.clear();
|
|
2499
2843
|
this.maintenance.disposeWriteBehindTimer();
|
|
2500
2844
|
this.fetchRateLimiter.dispose();
|
|
2501
2845
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -2511,116 +2855,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2511
2855
|
await this.handleInvalidationMessage(message);
|
|
2512
2856
|
});
|
|
2513
2857
|
}
|
|
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
2858
|
async storeEntry(key, kind, value, options) {
|
|
2625
2859
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2626
2860
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
@@ -2667,87 +2901,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2667
2901
|
});
|
|
2668
2902
|
}
|
|
2669
2903
|
}
|
|
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
2904
|
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2752
2905
|
return this.ttlResolver.resolveFreshTtl(
|
|
2753
2906
|
key,
|
|
@@ -2763,55 +2916,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2763
2916
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
2764
2917
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
2765
2918
|
}
|
|
2766
|
-
shouldNegativeCache(options) {
|
|
2767
|
-
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2768
|
-
}
|
|
2769
|
-
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2770
|
-
if (!shouldStartBackgroundRefresh({
|
|
2771
|
-
isDisconnecting: this.isDisconnecting,
|
|
2772
|
-
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
2773
|
-
})) {
|
|
2774
|
-
return;
|
|
2775
|
-
}
|
|
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
|
-
return {
|
|
2809
|
-
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
2810
|
-
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
2811
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2812
|
-
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
2813
|
-
};
|
|
2814
|
-
}
|
|
2815
2919
|
async deleteKeys(keys) {
|
|
2816
2920
|
if (keys.length === 0) {
|
|
2817
2921
|
return;
|
|
@@ -3057,29 +3161,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
3057
3161
|
await this.startup;
|
|
3058
3162
|
this.assertActive(operation);
|
|
3059
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
|
+
}
|
|
3060
3170
|
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
|
-
}
|
|
3171
|
+
return this.reader.runApplyFreshReadPolicies(key, hit, options, fetcher);
|
|
3083
3172
|
}
|
|
3084
3173
|
shouldSkipLayer(layer) {
|
|
3085
3174
|
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
@@ -3121,9 +3210,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
3121
3210
|
}
|
|
3122
3211
|
this.emitError("fetch", { key, error: this.formatError(error) });
|
|
3123
3212
|
}
|
|
3124
|
-
isNegativeStoredValue(stored) {
|
|
3125
|
-
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
3126
|
-
}
|
|
3127
3213
|
emitError(operation, context) {
|
|
3128
3214
|
this.logger.error?.(operation, context);
|
|
3129
3215
|
if (this.listenerCount("error") > 0) {
|