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
|
@@ -688,7 +688,7 @@ function normalizeForSerialization(value) {
|
|
|
688
688
|
}
|
|
689
689
|
function serializeKeyPart(value) {
|
|
690
690
|
if (typeof value === "string") {
|
|
691
|
-
return `s:${value}`;
|
|
691
|
+
return `s:${value.replace(/%/g, "%25").replace(/:/g, "%3A")}`;
|
|
692
692
|
}
|
|
693
693
|
if (typeof value === "number") {
|
|
694
694
|
return `n:${value}`;
|
|
@@ -1077,6 +1077,7 @@ var CacheStackLayerWriter = class {
|
|
|
1077
1077
|
}
|
|
1078
1078
|
const results = await Promise.allSettled(operations.map((operation) => operation()));
|
|
1079
1079
|
const failures = results.filter((result) => result.status === "rejected");
|
|
1080
|
+
const degraded = results.filter((result) => result.status === "fulfilled");
|
|
1080
1081
|
if (failures.length === 0) {
|
|
1081
1082
|
return;
|
|
1082
1083
|
}
|
|
@@ -1255,6 +1256,7 @@ function planFreshReadPolicies({
|
|
|
1255
1256
|
}
|
|
1256
1257
|
|
|
1257
1258
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1259
|
+
import { randomBytes } from "crypto";
|
|
1258
1260
|
import { constants, promises as fs } from "fs";
|
|
1259
1261
|
import path from "path";
|
|
1260
1262
|
|
|
@@ -1354,6 +1356,42 @@ async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
|
1354
1356
|
return Buffer.concat(chunks).toString("utf8");
|
|
1355
1357
|
}
|
|
1356
1358
|
|
|
1359
|
+
// ../../src/internal/StructuredDataSanitizer.ts
|
|
1360
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1361
|
+
function sanitizeStructuredData(value, options) {
|
|
1362
|
+
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
1363
|
+
}
|
|
1364
|
+
function sanitizeValue(value, depth, state, options) {
|
|
1365
|
+
state.count += 1;
|
|
1366
|
+
if (state.count > options.maxNodes) {
|
|
1367
|
+
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
1368
|
+
}
|
|
1369
|
+
if (depth > options.maxDepth) {
|
|
1370
|
+
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
1371
|
+
}
|
|
1372
|
+
if (Array.isArray(value)) {
|
|
1373
|
+
const sanitized2 = [];
|
|
1374
|
+
for (const entry of value) {
|
|
1375
|
+
sanitized2.push(sanitizeValue(entry, depth + 1, state, options));
|
|
1376
|
+
}
|
|
1377
|
+
return sanitized2;
|
|
1378
|
+
}
|
|
1379
|
+
if (!isPlainObject(value)) {
|
|
1380
|
+
return value;
|
|
1381
|
+
}
|
|
1382
|
+
const sanitized = options.createObject?.() ?? {};
|
|
1383
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1384
|
+
if (DANGEROUS_KEYS.has(key)) {
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
1388
|
+
}
|
|
1389
|
+
return sanitized;
|
|
1390
|
+
}
|
|
1391
|
+
function isPlainObject(value) {
|
|
1392
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1357
1395
|
// ../../src/internal/CacheStackSnapshotManager.ts
|
|
1358
1396
|
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1359
1397
|
var CacheStackSnapshotManager = class {
|
|
@@ -1378,7 +1416,16 @@ var CacheStackSnapshotManager = class {
|
|
|
1378
1416
|
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
1379
1417
|
await Promise.all(
|
|
1380
1418
|
batch.map(async (entry) => {
|
|
1381
|
-
await Promise.all(
|
|
1419
|
+
await Promise.all(
|
|
1420
|
+
this.options.layers.map(async (layer) => {
|
|
1421
|
+
if (this.options.shouldSkipLayer(layer)) return;
|
|
1422
|
+
try {
|
|
1423
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1424
|
+
} catch (error) {
|
|
1425
|
+
await this.options.handleLayerFailure(layer, "write", error);
|
|
1426
|
+
}
|
|
1427
|
+
})
|
|
1428
|
+
);
|
|
1382
1429
|
await this.options.tagIndex.touch(entry.key);
|
|
1383
1430
|
})
|
|
1384
1431
|
);
|
|
@@ -1388,7 +1435,7 @@ var CacheStackSnapshotManager = class {
|
|
|
1388
1435
|
const targetPath = await validateSnapshotFilePath(filePath, "write", snapshotBaseDir);
|
|
1389
1436
|
const tempPath = path.join(
|
|
1390
1437
|
path.dirname(targetPath),
|
|
1391
|
-
`.layercache-${process.pid}-${Date.now()}-${
|
|
1438
|
+
`.layercache-${process.pid}-${Date.now()}-${randomBytes(8).toString("hex")}.tmp`
|
|
1392
1439
|
);
|
|
1393
1440
|
let handle;
|
|
1394
1441
|
try {
|
|
@@ -1488,7 +1535,13 @@ var CacheStackSnapshotManager = class {
|
|
|
1488
1535
|
});
|
|
1489
1536
|
}
|
|
1490
1537
|
sanitizeSnapshotValue(value) {
|
|
1491
|
-
|
|
1538
|
+
const roundTripped = this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
|
|
1539
|
+
return sanitizeStructuredData(roundTripped, {
|
|
1540
|
+
label: "Snapshot value",
|
|
1541
|
+
maxDepth: 64,
|
|
1542
|
+
maxNodes: 1e4,
|
|
1543
|
+
createObject: () => /* @__PURE__ */ Object.create(null)
|
|
1544
|
+
});
|
|
1492
1545
|
}
|
|
1493
1546
|
};
|
|
1494
1547
|
|
|
@@ -1868,7 +1921,13 @@ var FetchRateLimiter = class {
|
|
|
1868
1921
|
this.pendingBuckets.add(next.bucketKey);
|
|
1869
1922
|
}
|
|
1870
1923
|
this.cleanupBucket(next.bucketKey, bucket, next.options.intervalMs);
|
|
1871
|
-
this.
|
|
1924
|
+
if (!this.drainTimer) {
|
|
1925
|
+
this.drainTimer = setTimeout(() => {
|
|
1926
|
+
this.drainTimer = void 0;
|
|
1927
|
+
this.drain();
|
|
1928
|
+
}, 0);
|
|
1929
|
+
this.drainTimer.unref?.();
|
|
1930
|
+
}
|
|
1872
1931
|
});
|
|
1873
1932
|
}
|
|
1874
1933
|
}
|
|
@@ -1910,6 +1969,9 @@ var FetchRateLimiter = class {
|
|
|
1910
1969
|
}
|
|
1911
1970
|
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1912
1971
|
this.evictIdleBuckets();
|
|
1972
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1973
|
+
throw new Error(`FetchRateLimiter bucket limit (${MAX_BUCKETS}) exceeded.`);
|
|
1974
|
+
}
|
|
1913
1975
|
}
|
|
1914
1976
|
const bucket = { active: 0, startedAt: [] };
|
|
1915
1977
|
this.buckets.set(bucketKey, bucket);
|
|
@@ -2388,38 +2450,6 @@ var TagIndex = class {
|
|
|
2388
2450
|
}
|
|
2389
2451
|
};
|
|
2390
2452
|
|
|
2391
|
-
// ../../src/internal/StructuredDataSanitizer.ts
|
|
2392
|
-
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2393
|
-
function sanitizeStructuredData(value, options) {
|
|
2394
|
-
return sanitizeValue(value, 0, { count: 0 }, options);
|
|
2395
|
-
}
|
|
2396
|
-
function sanitizeValue(value, depth, state, options) {
|
|
2397
|
-
state.count += 1;
|
|
2398
|
-
if (state.count > options.maxNodes) {
|
|
2399
|
-
throw new Error(`${options.label} exceeds max node count of ${options.maxNodes}.`);
|
|
2400
|
-
}
|
|
2401
|
-
if (depth > options.maxDepth) {
|
|
2402
|
-
throw new Error(`${options.label} exceeds max depth of ${options.maxDepth}.`);
|
|
2403
|
-
}
|
|
2404
|
-
if (Array.isArray(value)) {
|
|
2405
|
-
return value.map((entry) => sanitizeValue(entry, depth + 1, state, options));
|
|
2406
|
-
}
|
|
2407
|
-
if (!isPlainObject(value)) {
|
|
2408
|
-
return value;
|
|
2409
|
-
}
|
|
2410
|
-
const sanitized = options.createObject?.() ?? {};
|
|
2411
|
-
for (const [key, entry] of Object.entries(value)) {
|
|
2412
|
-
if (DANGEROUS_KEYS.has(key)) {
|
|
2413
|
-
continue;
|
|
2414
|
-
}
|
|
2415
|
-
sanitized[key] = sanitizeValue(entry, depth + 1, state, options);
|
|
2416
|
-
}
|
|
2417
|
-
return sanitized;
|
|
2418
|
-
}
|
|
2419
|
-
function isPlainObject(value) {
|
|
2420
|
-
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2421
|
-
}
|
|
2422
|
-
|
|
2423
2453
|
// ../../src/serialization/JsonSerializer.ts
|
|
2424
2454
|
var JsonSerializer = class {
|
|
2425
2455
|
serialize(value) {
|
|
@@ -2583,6 +2613,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2583
2613
|
tagIndex: this.tagIndex,
|
|
2584
2614
|
snapshotSerializer: this.snapshotSerializer,
|
|
2585
2615
|
readLayerEntry: this.readLayerEntry.bind(this),
|
|
2616
|
+
shouldSkipLayer: (layer) => this.shouldSkipLayer(layer),
|
|
2617
|
+
handleLayerFailure: async (layer, operation, error) => this.handleLayerFailure(layer, operation, error),
|
|
2586
2618
|
qualifyKey: this.qualifyKey.bind(this),
|
|
2587
2619
|
stripQualifiedKey: this.stripQualifiedKey.bind(this),
|
|
2588
2620
|
validateCacheKey,
|
|
@@ -2607,6 +2639,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2607
2639
|
layerWriter;
|
|
2608
2640
|
snapshots;
|
|
2609
2641
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
2642
|
+
backgroundRefreshAbort = /* @__PURE__ */ new Map();
|
|
2610
2643
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2611
2644
|
maintenance = new CacheStackMaintenance();
|
|
2612
2645
|
ttlResolver;
|
|
@@ -2851,7 +2884,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2851
2884
|
}
|
|
2852
2885
|
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
2853
2886
|
const layer = this.layers[layerIndex];
|
|
2854
|
-
if (!layer) continue;
|
|
2887
|
+
if (!layer || this.shouldSkipLayer(layer)) continue;
|
|
2855
2888
|
const keys = [...pending];
|
|
2856
2889
|
if (keys.length === 0) {
|
|
2857
2890
|
break;
|
|
@@ -2868,6 +2901,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2868
2901
|
await layer.delete(key);
|
|
2869
2902
|
continue;
|
|
2870
2903
|
}
|
|
2904
|
+
if (resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error") {
|
|
2905
|
+
this.metricsCollector.increment("staleHits", indexesByKey.get(key)?.length ?? 1);
|
|
2906
|
+
}
|
|
2871
2907
|
await this.tagIndex.touch(key);
|
|
2872
2908
|
await this.backfill(key, stored, layerIndex - 1);
|
|
2873
2909
|
resultsByKey.set(key, resolved.value);
|
|
@@ -3123,7 +3159,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
3123
3159
|
await this.unsubscribeInvalidation?.();
|
|
3124
3160
|
await this.flushWriteBehindQueue();
|
|
3125
3161
|
await this.maintenance.waitForGenerationCleanup();
|
|
3126
|
-
|
|
3162
|
+
for (const key of this.backgroundRefreshAbort.keys()) {
|
|
3163
|
+
this.backgroundRefreshAbort.set(key, true);
|
|
3164
|
+
}
|
|
3165
|
+
await Promise.allSettled(
|
|
3166
|
+
[...this.backgroundRefreshes.values()].map((promise) => {
|
|
3167
|
+
let timer;
|
|
3168
|
+
return Promise.race([
|
|
3169
|
+
promise,
|
|
3170
|
+
new Promise((resolve) => {
|
|
3171
|
+
timer = setTimeout(resolve, 5e3);
|
|
3172
|
+
timer.unref?.();
|
|
3173
|
+
})
|
|
3174
|
+
]).finally(() => {
|
|
3175
|
+
if (timer) clearTimeout(timer);
|
|
3176
|
+
});
|
|
3177
|
+
})
|
|
3178
|
+
);
|
|
3179
|
+
this.backgroundRefreshes.clear();
|
|
3180
|
+
this.backgroundRefreshAbort.clear();
|
|
3127
3181
|
this.maintenance.disposeWriteBehindTimer();
|
|
3128
3182
|
this.fetchRateLimiter.dispose();
|
|
3129
3183
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
@@ -3390,15 +3444,19 @@ var CacheStack = class extends EventEmitter {
|
|
|
3390
3444
|
}
|
|
3391
3445
|
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3392
3446
|
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
3447
|
+
this.backgroundRefreshAbort.set(key, false);
|
|
3393
3448
|
const refresh = (async () => {
|
|
3394
3449
|
this.metricsCollector.increment("refreshes");
|
|
3395
3450
|
try {
|
|
3451
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3396
3452
|
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
3397
3453
|
} catch (error) {
|
|
3454
|
+
if (this.backgroundRefreshAbort.get(key)) return;
|
|
3398
3455
|
this.metricsCollector.increment("refreshErrors");
|
|
3399
3456
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
3400
3457
|
} finally {
|
|
3401
3458
|
this.backgroundRefreshes.delete(key);
|
|
3459
|
+
this.backgroundRefreshAbort.delete(key);
|
|
3402
3460
|
}
|
|
3403
3461
|
})();
|
|
3404
3462
|
this.backgroundRefreshes.set(key, refresh);
|
|
@@ -3501,7 +3559,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3501
3559
|
timer.unref?.();
|
|
3502
3560
|
})
|
|
3503
3561
|
]);
|
|
3504
|
-
if (result && typeof result === "object" && "kind" in result) {
|
|
3562
|
+
if (result !== null && result !== void 0 && typeof result === "object" && "kind" in result) {
|
|
3505
3563
|
if (result.kind === "error") {
|
|
3506
3564
|
throw result.error;
|
|
3507
3565
|
}
|
|
@@ -3519,7 +3577,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
3519
3577
|
}
|
|
3520
3578
|
async observeOperation(name, attributes, execute) {
|
|
3521
3579
|
const id = this.nextOperationId;
|
|
3522
|
-
this.nextOperationId
|
|
3580
|
+
this.nextOperationId = (this.nextOperationId + 1) % Number.MAX_SAFE_INTEGER;
|
|
3523
3581
|
this.emit("operation-start", { id, name, attributes });
|
|
3524
3582
|
try {
|
|
3525
3583
|
const result = await execute();
|