layercache 1.2.8 → 1.2.9
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 +2 -2
- package/dist/cli.cjs +14 -1
- package/dist/cli.js +14 -1
- package/dist/{edge-DBs8Ko5W.d.cts → edge-BXWTKlI1.d.cts} +1 -0
- package/dist/{edge-DBs8Ko5W.d.ts → edge-BXWTKlI1.d.ts} +1 -0
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/index.cjs +146 -61
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +143 -58
- package/package.json +1 -1
- package/packages/nestjs/dist/index.cjs +99 -41
- package/packages/nestjs/dist/index.d.cts +1 -0
- package/packages/nestjs/dist/index.d.ts +1 -0
- package/packages/nestjs/dist/index.js +99 -41
package/dist/index.js
CHANGED
|
@@ -440,7 +440,7 @@ function normalizeForSerialization(value) {
|
|
|
440
440
|
}
|
|
441
441
|
function serializeKeyPart(value) {
|
|
442
442
|
if (typeof value === "string") {
|
|
443
|
-
return `s:${value}`;
|
|
443
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
444
444
|
}
|
|
445
445
|
if (typeof value === "number") {
|
|
446
446
|
return `n:${value}`;
|
|
@@ -671,6 +671,7 @@ var CacheStackLayerWriter = class {
|
|
|
671
671
|
}
|
|
672
672
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
673
673
|
const failures = results.filter((result) => result.status === "rejected");
|
|
674
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
674
675
|
if (failures.length === 0) {
|
|
675
676
|
return;
|
|
676
677
|
}
|
|
@@ -849,6 +850,7 @@ function planFreshReadPolicies({
|
|
|
849
850
|
}
|
|
850
851
|
|
|
851
852
|
// src/internal/CacheStackSnapshotManager.ts
|
|
853
|
+
import { randomBytes } from "crypto";
|
|
852
854
|
import { constants, promises as fs } from "fs";
|
|
853
855
|
import path from "path";
|
|
854
856
|
|
|
@@ -948,6 +950,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
948
950
|
return Buffer.concat(chunks).toString("utf8");
|
|
949
951
|
}
|
|
950
952
|
|
|
953
|
+
// src/internal/StructuredDataSanitizer.ts
|
|
954
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
955
|
+
function sanitizeStructuredData(value, options) {
|
|
956
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
957
|
+
}
|
|
958
|
+
function sanitizeValue(value, depth, state, options) {
|
|
959
|
+
state.count += 1;
|
|
960
|
+
if (state.count > options.maxNodes) {
|
|
961
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
962
|
+
}
|
|
963
|
+
if (depth > options.maxDepth) {
|
|
964
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
965
|
+
}
|
|
966
|
+
if (Array.isArray(value)) {
|
|
967
|
+
const sanitized2 = [];
|
|
968
|
+
for (const entry of value) {
|
|
969
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
970
|
+
}
|
|
971
|
+
return sanitized2;
|
|
972
|
+
}
|
|
973
|
+
if (!isPlainObject(value)) {
|
|
974
|
+
return value;
|
|
975
|
+
}
|
|
976
|
+
const sanitized = options.createObject?.() ?? {};
|
|
977
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
978
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
982
|
+
}
|
|
983
|
+
return sanitized;
|
|
984
|
+
}
|
|
985
|
+
function isPlainObject(value) {
|
|
986
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
987
|
+
}
|
|
988
|
+
|
|
951
989
|
// src/internal/CacheStackSnapshotManager.ts
|
|
952
990
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
953
991
|
var CacheStackSnapshotManager = class {
|
|
@@ -972,7 +1010,16 @@ var CacheStackSnapshotManager = class {
|
|
|
972
1010
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
973
1011
|
await Promise.all(
|
|
974
1012
|
batch.map(async (entry) => {
|
|
975
|
-
await Promise.all(
|
|
1013
|
+
await Promise.all(
|
|
1014
|
+
this.options.layers.map(async (layer) => {
|
|
1015
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1016
|
+
try {
|
|
1017
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1020
|
+
}
|
|
1021
|
+
})
|
|
1022
|
+
);
|
|
976
1023
|
await this.options.tagIndex.touch(entry.key);
|
|
977
1024
|
})
|
|
978
1025
|
);
|
|
@@ -982,7 +1029,7 @@ var CacheStackSnapshotManager = class {
|
|
|
982
1029
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
983
1030
|
const tempPath = path.join(
|
|
984
1031
|
path.dirname(targetPath),
|
|
985
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1032
|
+
`.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
|
|
986
1033
|
);
|
|
987
1034
|
let handle;
|
|
988
1035
|
try {
|
|
@@ -1082,7 +1129,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1082
1129
|
});
|
|
1083
1130
|
}
|
|
1084
1131
|
sanitizeSnapshotValue(value) {
|
|
1085
|
-
|
|
1132
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1133
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1134
|
+
label: "Snapshot value",
|
|
1135
|
+
maxDepth: 64,
|
|
1136
|
+
maxNodes: 1e4,
|
|
1137
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1138
|
+
});
|
|
1086
1139
|
}
|
|
1087
1140
|
};
|
|
1088
1141
|
|
|
@@ -1462,7 +1515,13 @@ var FetchRateLimiter = class {
|
|
|
1462
1515
|
this.pendingBuckets.add(next.bucketKey);
|
|
1463
1516
|
}
|
|
1464
1517
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1465
|
-
this.
|
|
1518
|
+
if (!this.drainTimer) {
|
|
1519
|
+
this.drainTimer = setTimeout(() => {
|
|
1520
|
+
this.drainTimer = void 0;
|
|
1521
|
+
this.drain();
|
|
1522
|
+
}, 0);
|
|
1523
|
+
this.drainTimer.unref?.();
|
|
1524
|
+
}
|
|
1466
1525
|
});
|
|
1467
1526
|
}
|
|
1468
1527
|
}
|
|
@@ -1504,6 +1563,9 @@ var FetchRateLimiter = class {
|
|
|
1504
1563
|
}
|
|
1505
1564
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1506
1565
|
this.evictIdleBuckets();
|
|
1566
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1567
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1568
|
+
}
|
|
1507
1569
|
}
|
|
1508
1570
|
const bucket = { active: 0, startedAt: [] };
|
|
1509
1571
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -1733,38 +1795,6 @@ var TtlResolver = class {
|
|
|
1733
1795
|
}
|
|
1734
1796
|
};
|
|
1735
1797
|
|
|
1736
|
-
// src/internal/StructuredDataSanitizer.ts
|
|
1737
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1738
|
-
function sanitizeStructuredData(value, options) {
|
|
1739
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1740
|
-
}
|
|
1741
|
-
function sanitizeValue(value, depth, state, options) {
|
|
1742
|
-
state.count += 1;
|
|
1743
|
-
if (state.count > options.maxNodes) {
|
|
1744
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1745
|
-
}
|
|
1746
|
-
if (depth > options.maxDepth) {
|
|
1747
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1748
|
-
}
|
|
1749
|
-
if (Array.isArray(value)) {
|
|
1750
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
1751
|
-
}
|
|
1752
|
-
if (!isPlainObject(value)) {
|
|
1753
|
-
return value;
|
|
1754
|
-
}
|
|
1755
|
-
const sanitized = options.createObject?.() ?? {};
|
|
1756
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
1757
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
1758
|
-
continue;
|
|
1759
|
-
}
|
|
1760
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1761
|
-
}
|
|
1762
|
-
return sanitized;
|
|
1763
|
-
}
|
|
1764
|
-
function isPlainObject(value) {
|
|
1765
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1766
|
-
}
|
|
1767
|
-
|
|
1768
1798
|
// src/serialization/JsonSerializer.ts
|
|
1769
1799
|
var JsonSerializer = class {
|
|
1770
1800
|
serialize(value) {
|
|
@@ -1929,6 +1959,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1929
1959
|
tagIndex: this.tagIndex,
|
|
1930
1960
|
snapshotSerializer: this.snapshotSerializer,
|
|
1931
1961
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
1962
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
1963
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
1932
1964
|
qualifyKey: this.qualifyKey.bind(this),
|
|
1933
1965
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
1934
1966
|
validateCacheKey,
|
|
@@ -1953,6 +1985,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1953
1985
|
layerWriter;
|
|
1954
1986
|
snapshots;
|
|
1955
1987
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1988
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
1956
1989
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1957
1990
|
maintenance = new CacheStackMaintenance();
|
|
1958
1991
|
ttlResolver;
|
|
@@ -2197,7 +2230,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2197
2230
|
}
|
|
2198
2231
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2199
2232
|
const layer = this.layers[layerIndex];
|
|
2200
|
-
if (!layer) continue;
|
|
2233
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2201
2234
|
const keys = [...pending];
|
|
2202
2235
|
if (keys.length === 0) {
|
|
2203
2236
|
break;
|
|
@@ -2214,6 +2247,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2214
2247
|
await layer.delete(key);
|
|
2215
2248
|
continue;
|
|
2216
2249
|
}
|
|
2250
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2251
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2252
|
+
}
|
|
2217
2253
|
await this.tagIndex.touch(key);
|
|
2218
2254
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2219
2255
|
resultsByKey.set(key, resolved.value);
|
|
@@ -2469,7 +2505,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
2469
2505
|
await this.unsubscribeInvalidation?.();
|
|
2470
2506
|
await this.flushWriteBehindQueue();
|
|
2471
2507
|
await this.maintenance.waitForGenerationCleanup();
|
|
2472
|
-
|
|
2508
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
2509
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
2510
|
+
}
|
|
2511
|
+
await Promise.allSettled(
|
|
2512
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
2513
|
+
let timer;
|
|
2514
|
+
return Promise.race([
|
|
2515
|
+
promise,
|
|
2516
|
+
new Promise((resolve2) => {
|
|
2517
|
+
timer = setTimeout(resolve2, 5e3);
|
|
2518
|
+
timer.unref?.();
|
|
2519
|
+
})
|
|
2520
|
+
]).finally(() => {
|
|
2521
|
+
if (timer) clearTimeout(timer);
|
|
2522
|
+
});
|
|
2523
|
+
})
|
|
2524
|
+
);
|
|
2525
|
+
this.backgroundRefreshes.clear();
|
|
2526
|
+
this.backgroundRefreshAbort.clear();
|
|
2473
2527
|
this.maintenance.disposeWriteBehindTimer();
|
|
2474
2528
|
this.fetchRateLimiter.dispose();
|
|
2475
2529
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -2736,15 +2790,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
2736
2790
|
}
|
|
2737
2791
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2738
2792
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2793
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
2739
2794
|
const refresh = (async () => {
|
|
2740
2795
|
this.metricsCollector.increment("refreshes");
|
|
2741
2796
|
try {
|
|
2797
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2742
2798
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2743
2799
|
} catch (error) {
|
|
2800
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
2744
2801
|
this.metricsCollector.increment("refreshErrors");
|
|
2745
2802
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
2746
2803
|
} finally {
|
|
2747
2804
|
this.backgroundRefreshes.delete(key);
|
|
2805
|
+
this.backgroundRefreshAbort.delete(key);
|
|
2748
2806
|
}
|
|
2749
2807
|
})();
|
|
2750
2808
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -2847,7 +2905,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2847
2905
|
timer.unref?.();
|
|
2848
2906
|
})
|
|
2849
2907
|
]);
|
|
2850
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
2908
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
2851
2909
|
if (result.kind === "error") {
|
|
2852
2910
|
throw result.error;
|
|
2853
2911
|
}
|
|
@@ -2865,7 +2923,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2865
2923
|
}
|
|
2866
2924
|
async observeOperation(name, attributes, execute) {
|
|
2867
2925
|
const id = this.nextOperationId;
|
|
2868
|
-
this.nextOperationId
|
|
2926
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
2869
2927
|
this.emit("operation-start", { id, name, attributes });
|
|
2870
2928
|
try {
|
|
2871
2929
|
const result = await execute();
|
|
@@ -3101,6 +3159,7 @@ var RedisInvalidationBus = class {
|
|
|
3101
3159
|
logger;
|
|
3102
3160
|
handlers = /* @__PURE__ */ new Set();
|
|
3103
3161
|
sharedListener;
|
|
3162
|
+
subscribePromise;
|
|
3104
3163
|
constructor(options) {
|
|
3105
3164
|
this.publisher = options.publisher;
|
|
3106
3165
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
@@ -3108,15 +3167,27 @@ var RedisInvalidationBus = class {
|
|
|
3108
3167
|
this.logger = options.logger;
|
|
3109
3168
|
}
|
|
3110
3169
|
async subscribe(handler) {
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
await
|
|
3170
|
+
const previousPromise = this.subscribePromise;
|
|
3171
|
+
let resolveThis;
|
|
3172
|
+
this.subscribePromise = new Promise((resolve2) => {
|
|
3173
|
+
resolveThis = resolve2;
|
|
3174
|
+
});
|
|
3175
|
+
if (previousPromise) {
|
|
3176
|
+
await previousPromise;
|
|
3177
|
+
}
|
|
3178
|
+
try {
|
|
3179
|
+
if (this.handlers.size === 0) {
|
|
3180
|
+
const listener = (_channel, payload) => {
|
|
3181
|
+
void this.dispatchToHandlers(payload);
|
|
3182
|
+
};
|
|
3183
|
+
this.sharedListener = listener;
|
|
3184
|
+
this.subscriber.on("message", listener);
|
|
3185
|
+
await this.subscriber.subscribe(this.channel);
|
|
3186
|
+
}
|
|
3187
|
+
this.handlers.add(handler);
|
|
3188
|
+
} finally {
|
|
3189
|
+
resolveThis();
|
|
3118
3190
|
}
|
|
3119
|
-
this.handlers.add(handler);
|
|
3120
3191
|
return async () => {
|
|
3121
3192
|
this.handlers.delete(handler);
|
|
3122
3193
|
if (this.handlers.size === 0 && this.sharedListener) {
|
|
@@ -3317,10 +3388,21 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
3317
3388
|
}
|
|
3318
3389
|
|
|
3319
3390
|
// src/integrations/opentelemetry.ts
|
|
3391
|
+
var MAX_SPANS = 1e4;
|
|
3320
3392
|
function createOpenTelemetryPlugin(cache, tracer) {
|
|
3321
3393
|
const spans = /* @__PURE__ */ new Map();
|
|
3322
3394
|
const onStart = (event) => {
|
|
3323
|
-
|
|
3395
|
+
try {
|
|
3396
|
+
if (spans.size >= MAX_SPANS) {
|
|
3397
|
+
const oldest = spans.keys().next().value;
|
|
3398
|
+
if (oldest !== void 0) {
|
|
3399
|
+
spans.get(oldest)?.end();
|
|
3400
|
+
spans.delete(oldest);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
spans.set(event.id, tracer.startSpan(event.name, { attributes: event.attributes }));
|
|
3404
|
+
} catch {
|
|
3405
|
+
}
|
|
3324
3406
|
};
|
|
3325
3407
|
const onEnd = (event) => {
|
|
3326
3408
|
const span = spans.get(event.id);
|
|
@@ -3328,12 +3410,15 @@ function createOpenTelemetryPlugin(cache, tracer) {
|
|
|
3328
3410
|
return;
|
|
3329
3411
|
}
|
|
3330
3412
|
spans.delete(event.id);
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3413
|
+
try {
|
|
3414
|
+
span.setAttribute?.("layercache.success", event.success);
|
|
3415
|
+
if (event.result) {
|
|
3416
|
+
span.setAttribute?.("layercache.result", event.result);
|
|
3417
|
+
}
|
|
3418
|
+
if (event.error !== void 0) {
|
|
3419
|
+
span.recordException?.(event.error);
|
|
3420
|
+
}
|
|
3421
|
+
} catch {
|
|
3337
3422
|
}
|
|
3338
3423
|
span.end();
|
|
3339
3424
|
};
|
|
@@ -3711,7 +3796,7 @@ var RedisLayer = class {
|
|
|
3711
3796
|
};
|
|
3712
3797
|
|
|
3713
3798
|
// src/layers/DiskLayer.ts
|
|
3714
|
-
import { createHash } from "crypto";
|
|
3799
|
+
import { createHash, randomBytes as randomBytes2 } from "crypto";
|
|
3715
3800
|
import { promises as fs2 } from "fs";
|
|
3716
3801
|
import { join, resolve } from "path";
|
|
3717
3802
|
var FILE_SCAN_CONCURRENCY = 32;
|
|
@@ -3764,7 +3849,7 @@ var DiskLayer = class {
|
|
|
3764
3849
|
};
|
|
3765
3850
|
const payload = this.serializer.serialize(entry);
|
|
3766
3851
|
const targetPath = this.keyToPath(key);
|
|
3767
|
-
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${
|
|
3852
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${randomBytes2(8).toString("hex")}.tmp`;
|
|
3768
3853
|
try {
|
|
3769
3854
|
await fs2.writeFile(tempPath, payload);
|
|
3770
3855
|
await fs2.rename(tempPath, targetPath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "layercache",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Production-ready multi-layer caching for Node.js. Stack memory + Redis + disk behind one API with stampede prevention, tag invalidation, stale serving, and full observability.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cache",
|
|
@@ -724,7 +724,7 @@ function normalizeForSerialization(value) {
|
|
|
724
724
|
}
|
|
725
725
|
function serializeKeyPart(value) {
|
|
726
726
|
if (typeof value === "string") {
|
|
727
|
-
return `s:${value}`;
|
|
727
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
728
728
|
}
|
|
729
729
|
if (typeof value === "number") {
|
|
730
730
|
return `n:${value}`;
|
|
@@ -1113,6 +1113,7 @@ var CacheStackLayerWriter = class {
|
|
|
1113
1113
|
}
|
|
1114
1114
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1115
1115
|
const failures = results.filter((result) => result.status === "rejected");
|
|
1116
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
1116
1117
|
if (failures.length === 0) {
|
|
1117
1118
|
return;
|
|
1118
1119
|
}
|
|
@@ -1291,6 +1292,7 @@ function planFreshReadPolicies({
|
|
|
1291
1292
|
}
|
|
1292
1293
|
|
|
1293
1294
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1295
|
+
var import_node_crypto = require("crypto");
|
|
1294
1296
|
var import_node_fs = require("fs");
|
|
1295
1297
|
var import_node_path = __toESM(require("path"), 1);
|
|
1296
1298
|
|
|
@@ -1390,6 +1392,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1390
1392
|
return Buffer.concat(chunks).toString("utf8");
|
|
1391
1393
|
}
|
|
1392
1394
|
|
|
1395
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
1396
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1397
|
+
function sanitizeStructuredData(value, options) {
|
|
1398
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1399
|
+
}
|
|
1400
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1401
|
+
state.count += 1;
|
|
1402
|
+
if (state.count > options.maxNodes) {
|
|
1403
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1404
|
+
}
|
|
1405
|
+
if (depth > options.maxDepth) {
|
|
1406
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1407
|
+
}
|
|
1408
|
+
if (Array.isArray(value)) {
|
|
1409
|
+
const sanitized2 = [];
|
|
1410
|
+
for (const entry of value) {
|
|
1411
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1412
|
+
}
|
|
1413
|
+
return sanitized2;
|
|
1414
|
+
}
|
|
1415
|
+
if (!isPlainObject(value)) {
|
|
1416
|
+
return value;
|
|
1417
|
+
}
|
|
1418
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1419
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1420
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1424
|
+
}
|
|
1425
|
+
return sanitized;
|
|
1426
|
+
}
|
|
1427
|
+
function isPlainObject(value) {
|
|
1428
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1393
1431
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1394
1432
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1395
1433
|
var CacheStackSnapshotManager = class {
|
|
@@ -1414,7 +1452,16 @@ var CacheStackSnapshotManager = class {
|
|
|
1414
1452
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1415
1453
|
await Promise.all(
|
|
1416
1454
|
batch.map(async (entry) => {
|
|
1417
|
-
await Promise.all(
|
|
1455
|
+
await Promise.all(
|
|
1456
|
+
this.options.layers.map(async (layer) => {
|
|
1457
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1458
|
+
try {
|
|
1459
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1460
|
+
} catch (error) {
|
|
1461
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1462
|
+
}
|
|
1463
|
+
})
|
|
1464
|
+
);
|
|
1418
1465
|
await this.options.tagIndex.touch(entry.key);
|
|
1419
1466
|
})
|
|
1420
1467
|
);
|
|
@@ -1424,7 +1471,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1424
1471
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1425
1472
|
const tempPath = import_node_path.default.join(
|
|
1426
1473
|
import_node_path.default.dirname(targetPath),
|
|
1427
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1474
|
+
`.layercache-${process.pid}-${Date.now()}-${(0, import_node_crypto.randomBytes)(8).toString("hex")}.tmp`
|
|
1428
1475
|
);
|
|
1429
1476
|
let handle;
|
|
1430
1477
|
try {
|
|
@@ -1524,7 +1571,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1524
1571
|
});
|
|
1525
1572
|
}
|
|
1526
1573
|
sanitizeSnapshotValue(value) {
|
|
1527
|
-
|
|
1574
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1575
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1576
|
+
label: "Snapshot value",
|
|
1577
|
+
maxDepth: 64,
|
|
1578
|
+
maxNodes: 1e4,
|
|
1579
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1580
|
+
});
|
|
1528
1581
|
}
|
|
1529
1582
|
};
|
|
1530
1583
|
|
|
@@ -1904,7 +1957,13 @@ var FetchRateLimiter = class {
|
|
|
1904
1957
|
this.pendingBuckets.add(next.bucketKey);
|
|
1905
1958
|
}
|
|
1906
1959
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1907
|
-
this.
|
|
1960
|
+
if (!this.drainTimer) {
|
|
1961
|
+
this.drainTimer = setTimeout(() => {
|
|
1962
|
+
this.drainTimer = void 0;
|
|
1963
|
+
this.drain();
|
|
1964
|
+
}, 0);
|
|
1965
|
+
this.drainTimer.unref?.();
|
|
1966
|
+
}
|
|
1908
1967
|
});
|
|
1909
1968
|
}
|
|
1910
1969
|
}
|
|
@@ -1946,6 +2005,9 @@ var FetchRateLimiter = class {
|
|
|
1946
2005
|
}
|
|
1947
2006
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1948
2007
|
this.evictIdleBuckets();
|
|
2008
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
2009
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
2010
|
+
}
|
|
1949
2011
|
}
|
|
1950
2012
|
const bucket = { active: 0, startedAt: [] };
|
|
1951
2013
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -2424,38 +2486,6 @@ var TagIndex = class {
|
|
|
2424
2486
|
}
|
|
2425
2487
|
};
|
|
2426
2488
|
|
|
2427
|
-
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2428
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2429
|
-
function sanitizeStructuredData(value, options) {
|
|
2430
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2431
|
-
}
|
|
2432
|
-
function sanitizeValue(value, depth, state, options) {
|
|
2433
|
-
state.count += 1;
|
|
2434
|
-
if (state.count > options.maxNodes) {
|
|
2435
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2436
|
-
}
|
|
2437
|
-
if (depth > options.maxDepth) {
|
|
2438
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2439
|
-
}
|
|
2440
|
-
if (Array.isArray(value)) {
|
|
2441
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2442
|
-
}
|
|
2443
|
-
if (!isPlainObject(value)) {
|
|
2444
|
-
return value;
|
|
2445
|
-
}
|
|
2446
|
-
const sanitized = options.createObject?.() ?? {};
|
|
2447
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2448
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
2449
|
-
continue;
|
|
2450
|
-
}
|
|
2451
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2452
|
-
}
|
|
2453
|
-
return sanitized;
|
|
2454
|
-
}
|
|
2455
|
-
function isPlainObject(value) {
|
|
2456
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2457
|
-
}
|
|
2458
|
-
|
|
2459
2489
|
// ../../src/serialization/JsonSerializer.ts
|
|
2460
2490
|
var JsonSerializer = class {
|
|
2461
2491
|
serialize(value) {
|
|
@@ -2619,6 +2649,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2619
2649
|
tagIndex: this.tagIndex,
|
|
2620
2650
|
snapshotSerializer: this.snapshotSerializer,
|
|
2621
2651
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2652
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2653
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2622
2654
|
qualifyKey: this.qualifyKey.bind(this),
|
|
2623
2655
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2624
2656
|
validateCacheKey,
|
|
@@ -2643,6 +2675,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2643
2675
|
layerWriter;
|
|
2644
2676
|
snapshots;
|
|
2645
2677
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2678
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2646
2679
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2647
2680
|
maintenance = new CacheStackMaintenance();
|
|
2648
2681
|
ttlResolver;
|
|
@@ -2887,7 +2920,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2887
2920
|
}
|
|
2888
2921
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2889
2922
|
const layer = this.layers[layerIndex];
|
|
2890
|
-
if (!layer) continue;
|
|
2923
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2891
2924
|
const keys = [...pending];
|
|
2892
2925
|
if (keys.length === 0) {
|
|
2893
2926
|
break;
|
|
@@ -2904,6 +2937,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2904
2937
|
await layer.delete(key);
|
|
2905
2938
|
continue;
|
|
2906
2939
|
}
|
|
2940
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2941
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2942
|
+
}
|
|
2907
2943
|
await this.tagIndex.touch(key);
|
|
2908
2944
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2909
2945
|
resultsByKey.set(key, resolved.value);
|
|
@@ -3159,7 +3195,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3159
3195
|
await this.unsubscribeInvalidation?.();
|
|
3160
3196
|
await this.flushWriteBehindQueue();
|
|
3161
3197
|
await this.maintenance.waitForGenerationCleanup();
|
|
3162
|
-
|
|
3198
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3199
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3200
|
+
}
|
|
3201
|
+
await Promise.allSettled(
|
|
3202
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3203
|
+
let timer;
|
|
3204
|
+
return Promise.race([
|
|
3205
|
+
promise,
|
|
3206
|
+
new Promise((resolve) => {
|
|
3207
|
+
timer = setTimeout(resolve, 5e3);
|
|
3208
|
+
timer.unref?.();
|
|
3209
|
+
})
|
|
3210
|
+
]).finally(() => {
|
|
3211
|
+
if (timer) clearTimeout(timer);
|
|
3212
|
+
});
|
|
3213
|
+
})
|
|
3214
|
+
);
|
|
3215
|
+
this.backgroundRefreshes.clear();
|
|
3216
|
+
this.backgroundRefreshAbort.clear();
|
|
3163
3217
|
this.maintenance.disposeWriteBehindTimer();
|
|
3164
3218
|
this.fetchRateLimiter.dispose();
|
|
3165
3219
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -3426,15 +3480,19 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3426
3480
|
}
|
|
3427
3481
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3428
3482
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3483
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3429
3484
|
const refresh = (async () => {
|
|
3430
3485
|
this.metricsCollector.increment("refreshes");
|
|
3431
3486
|
try {
|
|
3487
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3432
3488
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3433
3489
|
} catch (error) {
|
|
3490
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3434
3491
|
this.metricsCollector.increment("refreshErrors");
|
|
3435
3492
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3436
3493
|
} finally {
|
|
3437
3494
|
this.backgroundRefreshes.delete(key);
|
|
3495
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3438
3496
|
}
|
|
3439
3497
|
})();
|
|
3440
3498
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3537,7 +3595,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3537
3595
|
timer.unref?.();
|
|
3538
3596
|
})
|
|
3539
3597
|
]);
|
|
3540
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3598
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3541
3599
|
if (result.kind === "error") {
|
|
3542
3600
|
throw result.error;
|
|
3543
3601
|
}
|
|
@@ -3555,7 +3613,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3555
3613
|
}
|
|
3556
3614
|
async observeOperation(name, attributes, execute) {
|
|
3557
3615
|
const id = this.nextOperationId;
|
|
3558
|
-
this.nextOperationId
|
|
3616
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3559
3617
|
this.emit("operation-start", { id, name, attributes });
|
|
3560
3618
|
try {
|
|
3561
3619
|
const result = await execute();
|
|
@@ -443,6 +443,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
443
443
|
private readonly layerWriter;
|
|
444
444
|
private readonly snapshots;
|
|
445
445
|
private readonly backgroundRefreshes;
|
|
446
|
+
private readonly backgroundRefreshAbort;
|
|
446
447
|
private readonly layerDegradedUntil;
|
|
447
448
|
private readonly maintenance;
|
|
448
449
|
private readonly ttlResolver;
|
|
@@ -443,6 +443,7 @@ declare class CacheStack extends EventEmitter {
|
|
|
443
443
|
private readonly layerWriter;
|
|
444
444
|
private readonly snapshots;
|
|
445
445
|
private readonly backgroundRefreshes;
|
|
446
|
+
private readonly backgroundRefreshAbort;
|
|
446
447
|
private readonly layerDegradedUntil;
|
|
447
448
|
private readonly maintenance;
|
|
448
449
|
private readonly ttlResolver;
|