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.
@@ -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(this.options.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
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()}-${Math.random().toString(36).slice(2)}.tmp`
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
- return this.options.snapshotSerializer.deserialize(this.options.snapshotSerializer.serialize(value));
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.drain();
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
- await Promise.allSettled([...this.backgroundRefreshes.values()]);
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 += 1;
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();