layercache 1.2.5 → 1.2.7
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/LICENSE +190 -21
- package/README.md +254 -912
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-JC26W3KK.js → chunk-GJBKCFE6.js} +38 -3
- package/dist/cli.cjs +83 -3
- package/dist/cli.js +2 -2
- package/dist/{edge-P07GCO2Y.d.ts → edge-BMmPVqaD.d.cts} +28 -21
- package/dist/{edge-P07GCO2Y.d.cts → edge-BMmPVqaD.d.ts} +28 -21
- package/dist/edge.cjs +74 -5
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1671 -837
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +1327 -608
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +1296 -732
- package/packages/nestjs/dist/index.d.cts +19 -20
- package/packages/nestjs/dist/index.d.ts +19 -20
- package/packages/nestjs/dist/index.js +1296 -732
|
@@ -259,27 +259,147 @@ var Mutex = class {
|
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
+
// ../../src/internal/CacheNamespaceMetrics.ts
|
|
263
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
264
|
+
return {
|
|
265
|
+
hits: 0,
|
|
266
|
+
misses: 0,
|
|
267
|
+
fetches: 0,
|
|
268
|
+
sets: 0,
|
|
269
|
+
deletes: 0,
|
|
270
|
+
backfills: 0,
|
|
271
|
+
invalidations: 0,
|
|
272
|
+
staleHits: 0,
|
|
273
|
+
refreshes: 0,
|
|
274
|
+
refreshErrors: 0,
|
|
275
|
+
writeFailures: 0,
|
|
276
|
+
singleFlightWaits: 0,
|
|
277
|
+
negativeCacheHits: 0,
|
|
278
|
+
circuitBreakerTrips: 0,
|
|
279
|
+
degradedOperations: 0,
|
|
280
|
+
hitsByLayer: {},
|
|
281
|
+
missesByLayer: {},
|
|
282
|
+
latencyByLayer: {},
|
|
283
|
+
resetAt
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
function cloneNamespaceMetrics(metrics) {
|
|
287
|
+
return {
|
|
288
|
+
...metrics,
|
|
289
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
290
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
291
|
+
latencyByLayer: Object.fromEntries(
|
|
292
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
293
|
+
)
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function diffNamespaceMetrics(before, after) {
|
|
297
|
+
const latencyByLayer = Object.fromEntries(
|
|
298
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
299
|
+
layer,
|
|
300
|
+
{
|
|
301
|
+
avgMs: value.avgMs,
|
|
302
|
+
maxMs: value.maxMs,
|
|
303
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
304
|
+
}
|
|
305
|
+
])
|
|
306
|
+
);
|
|
307
|
+
return {
|
|
308
|
+
hits: after.hits - before.hits,
|
|
309
|
+
misses: after.misses - before.misses,
|
|
310
|
+
fetches: after.fetches - before.fetches,
|
|
311
|
+
sets: after.sets - before.sets,
|
|
312
|
+
deletes: after.deletes - before.deletes,
|
|
313
|
+
backfills: after.backfills - before.backfills,
|
|
314
|
+
invalidations: after.invalidations - before.invalidations,
|
|
315
|
+
staleHits: after.staleHits - before.staleHits,
|
|
316
|
+
refreshes: after.refreshes - before.refreshes,
|
|
317
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
318
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
319
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
320
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
321
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
322
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
323
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
324
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
325
|
+
latencyByLayer,
|
|
326
|
+
resetAt: after.resetAt
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
function addNamespaceMetrics(base, delta) {
|
|
330
|
+
return {
|
|
331
|
+
hits: base.hits + delta.hits,
|
|
332
|
+
misses: base.misses + delta.misses,
|
|
333
|
+
fetches: base.fetches + delta.fetches,
|
|
334
|
+
sets: base.sets + delta.sets,
|
|
335
|
+
deletes: base.deletes + delta.deletes,
|
|
336
|
+
backfills: base.backfills + delta.backfills,
|
|
337
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
338
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
339
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
340
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
341
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
342
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
343
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
344
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
345
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
346
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
347
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
348
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
349
|
+
resetAt: base.resetAt
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function computeNamespaceHitRate(metrics) {
|
|
353
|
+
const total = metrics.hits + metrics.misses;
|
|
354
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
355
|
+
const byLayer = {};
|
|
356
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
357
|
+
for (const layer of layers) {
|
|
358
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
359
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
360
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
361
|
+
}
|
|
362
|
+
return { overall, byLayer };
|
|
363
|
+
}
|
|
364
|
+
function diffMetricMap(before, after) {
|
|
365
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
366
|
+
const result = {};
|
|
367
|
+
for (const key of keys) {
|
|
368
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
369
|
+
}
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
function addMetricMap(base, delta) {
|
|
373
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
374
|
+
const result = {};
|
|
375
|
+
for (const key of keys) {
|
|
376
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
}
|
|
380
|
+
|
|
262
381
|
// ../../src/CacheNamespace.ts
|
|
263
382
|
var CacheNamespace = class _CacheNamespace {
|
|
264
383
|
constructor(cache, prefix) {
|
|
265
384
|
this.cache = cache;
|
|
266
385
|
this.prefix = prefix;
|
|
386
|
+
validateNamespaceKey(prefix);
|
|
267
387
|
}
|
|
268
388
|
cache;
|
|
269
389
|
prefix;
|
|
270
390
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
271
|
-
metrics =
|
|
391
|
+
metrics = createEmptyNamespaceMetrics();
|
|
272
392
|
async get(key, fetcher, options) {
|
|
273
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
393
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
274
394
|
}
|
|
275
395
|
async getOrSet(key, fetcher, options) {
|
|
276
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
396
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
277
397
|
}
|
|
278
398
|
/**
|
|
279
399
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
280
400
|
*/
|
|
281
401
|
async getOrThrow(key, fetcher, options) {
|
|
282
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
402
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
283
403
|
}
|
|
284
404
|
async has(key) {
|
|
285
405
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -288,7 +408,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
288
408
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
289
409
|
}
|
|
290
410
|
async set(key, value, options) {
|
|
291
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
411
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
292
412
|
}
|
|
293
413
|
async delete(key) {
|
|
294
414
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -304,7 +424,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
304
424
|
() => this.cache.mget(
|
|
305
425
|
entries.map((entry) => ({
|
|
306
426
|
...entry,
|
|
307
|
-
key: this.qualify(entry.key)
|
|
427
|
+
key: this.qualify(entry.key),
|
|
428
|
+
options: this.qualifyGetOptions(entry.options)
|
|
308
429
|
}))
|
|
309
430
|
)
|
|
310
431
|
);
|
|
@@ -314,16 +435,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
314
435
|
() => this.cache.mset(
|
|
315
436
|
entries.map((entry) => ({
|
|
316
437
|
...entry,
|
|
317
|
-
key: this.qualify(entry.key)
|
|
438
|
+
key: this.qualify(entry.key),
|
|
439
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
318
440
|
}))
|
|
319
441
|
)
|
|
320
442
|
);
|
|
321
443
|
}
|
|
322
444
|
async invalidateByTag(tag) {
|
|
323
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
445
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
324
446
|
}
|
|
325
447
|
async invalidateByTags(tags, mode = "any") {
|
|
326
|
-
await this.trackMetrics(
|
|
448
|
+
await this.trackMetrics(
|
|
449
|
+
() => this.cache.invalidateByTags(
|
|
450
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
451
|
+
mode
|
|
452
|
+
)
|
|
453
|
+
);
|
|
327
454
|
}
|
|
328
455
|
async invalidateByPattern(pattern) {
|
|
329
456
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -335,34 +462,33 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
335
462
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
336
463
|
*/
|
|
337
464
|
async inspect(key) {
|
|
338
|
-
|
|
465
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
466
|
+
if (result === null) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
return {
|
|
470
|
+
...result,
|
|
471
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
472
|
+
};
|
|
339
473
|
}
|
|
340
474
|
wrap(keyPrefix, fetcher, options) {
|
|
341
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
475
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
342
476
|
}
|
|
343
477
|
warm(entries, options) {
|
|
344
478
|
return this.cache.warm(
|
|
345
479
|
entries.map((entry) => ({
|
|
346
480
|
...entry,
|
|
347
|
-
key: this.qualify(entry.key)
|
|
481
|
+
key: this.qualify(entry.key),
|
|
482
|
+
options: this.qualifyGetOptions(entry.options)
|
|
348
483
|
})),
|
|
349
484
|
options
|
|
350
485
|
);
|
|
351
486
|
}
|
|
352
487
|
getMetrics() {
|
|
353
|
-
return
|
|
488
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
354
489
|
}
|
|
355
490
|
getHitRate() {
|
|
356
|
-
|
|
357
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
358
|
-
const byLayer = {};
|
|
359
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
360
|
-
for (const layer of layers) {
|
|
361
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
362
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
363
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
364
|
-
}
|
|
365
|
-
return { overall, byLayer };
|
|
491
|
+
return computeNamespaceHitRate(this.metrics);
|
|
366
492
|
}
|
|
367
493
|
/**
|
|
368
494
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -380,12 +506,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
380
506
|
qualify(key) {
|
|
381
507
|
return `${this.prefix}:${key}`;
|
|
382
508
|
}
|
|
509
|
+
qualifyTag(tag) {
|
|
510
|
+
return `${this.prefix}:${tag}`;
|
|
511
|
+
}
|
|
512
|
+
qualifyGetOptions(options) {
|
|
513
|
+
return this.qualifyWriteOptions(options);
|
|
514
|
+
}
|
|
515
|
+
qualifyWrapOptions(options) {
|
|
516
|
+
return this.qualifyWriteOptions(options);
|
|
517
|
+
}
|
|
518
|
+
qualifyWriteOptions(options) {
|
|
519
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
520
|
+
return options;
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
...options,
|
|
524
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
525
|
+
};
|
|
526
|
+
}
|
|
383
527
|
async trackMetrics(operation) {
|
|
384
528
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
385
529
|
const before = this.cache.getMetrics();
|
|
386
530
|
const result = await operation();
|
|
387
531
|
const after = this.cache.getMetrics();
|
|
388
|
-
this.metrics =
|
|
532
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
389
533
|
return result;
|
|
390
534
|
});
|
|
391
535
|
}
|
|
@@ -399,223 +543,773 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
399
543
|
return mutex;
|
|
400
544
|
}
|
|
401
545
|
};
|
|
402
|
-
function
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
singleFlightWaits: 0,
|
|
416
|
-
negativeCacheHits: 0,
|
|
417
|
-
circuitBreakerTrips: 0,
|
|
418
|
-
degradedOperations: 0,
|
|
419
|
-
hitsByLayer: {},
|
|
420
|
-
missesByLayer: {},
|
|
421
|
-
latencyByLayer: {},
|
|
422
|
-
resetAt: Date.now()
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
function cloneMetrics(metrics) {
|
|
426
|
-
return {
|
|
427
|
-
...metrics,
|
|
428
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
429
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
430
|
-
latencyByLayer: Object.fromEntries(
|
|
431
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
432
|
-
)
|
|
433
|
-
};
|
|
546
|
+
function validateNamespaceKey(key) {
|
|
547
|
+
if (key.length === 0) {
|
|
548
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
549
|
+
}
|
|
550
|
+
if (key.length > 256) {
|
|
551
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
552
|
+
}
|
|
553
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
554
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
555
|
+
}
|
|
556
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
557
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
558
|
+
}
|
|
434
559
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
560
|
+
|
|
561
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
562
|
+
var PatternMatcher = class _PatternMatcher {
|
|
563
|
+
/**
|
|
564
|
+
* Tests whether a glob-style pattern matches a value.
|
|
565
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
566
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
567
|
+
* quadratic memory usage on long patterns/keys.
|
|
568
|
+
*/
|
|
569
|
+
static matches(pattern, value) {
|
|
570
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
574
|
+
*/
|
|
575
|
+
static matchLinear(pattern, value) {
|
|
576
|
+
let patternIndex = 0;
|
|
577
|
+
let valueIndex = 0;
|
|
578
|
+
let starIndex = -1;
|
|
579
|
+
let backtrackValueIndex = 0;
|
|
580
|
+
while (valueIndex < value.length) {
|
|
581
|
+
const patternChar = pattern[patternIndex];
|
|
582
|
+
const valueChar = value[valueIndex];
|
|
583
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
584
|
+
starIndex = patternIndex;
|
|
585
|
+
patternIndex += 1;
|
|
586
|
+
backtrackValueIndex = valueIndex;
|
|
587
|
+
continue;
|
|
443
588
|
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
462
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
463
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
464
|
-
latencyByLayer,
|
|
465
|
-
resetAt: after.resetAt
|
|
466
|
-
};
|
|
467
|
-
}
|
|
468
|
-
function addMetrics(base, delta) {
|
|
469
|
-
return {
|
|
470
|
-
hits: base.hits + delta.hits,
|
|
471
|
-
misses: base.misses + delta.misses,
|
|
472
|
-
fetches: base.fetches + delta.fetches,
|
|
473
|
-
sets: base.sets + delta.sets,
|
|
474
|
-
deletes: base.deletes + delta.deletes,
|
|
475
|
-
backfills: base.backfills + delta.backfills,
|
|
476
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
477
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
478
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
479
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
480
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
481
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
482
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
483
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
484
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
485
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
486
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
487
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
488
|
-
resetAt: base.resetAt
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
function diffMap(before, after) {
|
|
492
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
493
|
-
const result = {};
|
|
494
|
-
for (const key of keys) {
|
|
495
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
589
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
590
|
+
patternIndex += 1;
|
|
591
|
+
valueIndex += 1;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (starIndex !== -1) {
|
|
595
|
+
patternIndex = starIndex + 1;
|
|
596
|
+
backtrackValueIndex += 1;
|
|
597
|
+
valueIndex = backtrackValueIndex;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
return false;
|
|
601
|
+
}
|
|
602
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
603
|
+
patternIndex += 1;
|
|
604
|
+
}
|
|
605
|
+
return patternIndex === pattern.length;
|
|
496
606
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
610
|
+
var CacheKeyDiscovery = class {
|
|
611
|
+
constructor(options) {
|
|
612
|
+
this.options = options;
|
|
613
|
+
}
|
|
614
|
+
options;
|
|
615
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
616
|
+
const { tagIndex } = this.options;
|
|
617
|
+
const matches = /* @__PURE__ */ new Set();
|
|
618
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
619
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
620
|
+
matches.add(key);
|
|
621
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
622
|
+
});
|
|
623
|
+
} else {
|
|
624
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
625
|
+
for (const key of initialMatches) {
|
|
626
|
+
matches.add(key);
|
|
627
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
await Promise.all(
|
|
631
|
+
this.options.layers.map(async (layer) => {
|
|
632
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
try {
|
|
636
|
+
if (layer.forEachKey) {
|
|
637
|
+
await layer.forEachKey(async (key) => {
|
|
638
|
+
if (key.startsWith(prefix)) {
|
|
639
|
+
matches.add(key);
|
|
640
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
const keys = await layer.keys?.();
|
|
646
|
+
for (const key of keys ?? []) {
|
|
647
|
+
if (key.startsWith(prefix)) {
|
|
648
|
+
matches.add(key);
|
|
649
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
} catch (error) {
|
|
653
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
);
|
|
657
|
+
return [...matches];
|
|
658
|
+
}
|
|
659
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
660
|
+
const matches = /* @__PURE__ */ new Set();
|
|
661
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
662
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
663
|
+
matches.add(key);
|
|
664
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
668
|
+
matches.add(key);
|
|
669
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
await Promise.all(
|
|
673
|
+
this.options.layers.map(async (layer) => {
|
|
674
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
try {
|
|
678
|
+
if (layer.forEachKey) {
|
|
679
|
+
await layer.forEachKey(async (key) => {
|
|
680
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
681
|
+
matches.add(key);
|
|
682
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const keys = await layer.keys?.();
|
|
688
|
+
for (const key of keys ?? []) {
|
|
689
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
690
|
+
matches.add(key);
|
|
691
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (error) {
|
|
695
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
);
|
|
699
|
+
return [...matches];
|
|
700
|
+
}
|
|
701
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
702
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
703
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
// ../../src/internal/CacheKeySerialization.ts
|
|
709
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
710
|
+
function normalizeForSerialization(value) {
|
|
711
|
+
if (Array.isArray(value)) {
|
|
712
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
713
|
+
}
|
|
714
|
+
if (value && typeof value === "object") {
|
|
715
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
716
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
717
|
+
return normalized;
|
|
718
|
+
}
|
|
719
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
720
|
+
return normalized;
|
|
721
|
+
}, {});
|
|
722
|
+
}
|
|
723
|
+
return value;
|
|
724
|
+
}
|
|
725
|
+
function serializeKeyPart(value) {
|
|
726
|
+
if (typeof value === "string") {
|
|
727
|
+
return `s:${value}`;
|
|
728
|
+
}
|
|
729
|
+
if (typeof value === "number") {
|
|
730
|
+
return `n:${value}`;
|
|
731
|
+
}
|
|
732
|
+
if (typeof value === "boolean") {
|
|
733
|
+
return `b:${value}`;
|
|
734
|
+
}
|
|
735
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
736
|
+
}
|
|
737
|
+
function serializeOptions(options) {
|
|
738
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
739
|
+
}
|
|
740
|
+
function createInstanceId() {
|
|
741
|
+
if (globalThis.crypto?.randomUUID) {
|
|
742
|
+
return globalThis.crypto.randomUUID();
|
|
743
|
+
}
|
|
744
|
+
const bytes = new Uint8Array(16);
|
|
745
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
746
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
747
|
+
} else {
|
|
748
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
749
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
756
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
757
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
758
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
759
|
+
}
|
|
760
|
+
async function findExistingAncestor(directory, fs, path) {
|
|
761
|
+
let current = directory;
|
|
762
|
+
while (true) {
|
|
763
|
+
try {
|
|
764
|
+
await fs.lstat(current);
|
|
765
|
+
return current;
|
|
766
|
+
} catch (error) {
|
|
767
|
+
if (error.code !== "ENOENT") {
|
|
768
|
+
throw error;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const parent = path.dirname(current);
|
|
772
|
+
if (parent === current) {
|
|
773
|
+
return current;
|
|
774
|
+
}
|
|
775
|
+
current = parent;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
779
|
+
if (filePath.length === 0) {
|
|
780
|
+
throw new Error("filePath must not be empty.");
|
|
781
|
+
}
|
|
782
|
+
if (filePath.includes("\0")) {
|
|
783
|
+
throw new Error("filePath must not contain null bytes.");
|
|
784
|
+
}
|
|
785
|
+
const { promises: fs } = await import("fs");
|
|
786
|
+
const path = await import("path");
|
|
787
|
+
const resolved = path.resolve(filePath);
|
|
788
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
789
|
+
if (baseDir === false) {
|
|
790
|
+
return resolved;
|
|
791
|
+
}
|
|
792
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
793
|
+
const realBaseDir = await fs.realpath(baseDir);
|
|
794
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
795
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
796
|
+
}
|
|
797
|
+
if (mode === "read") {
|
|
798
|
+
const realTarget = await fs.realpath(resolved);
|
|
799
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
800
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
801
|
+
}
|
|
802
|
+
return realTarget;
|
|
803
|
+
}
|
|
804
|
+
const parentDir = path.dirname(resolved);
|
|
805
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
806
|
+
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
807
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
808
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
809
|
+
}
|
|
810
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
811
|
+
const realParentDir = await fs.realpath(parentDir);
|
|
812
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
813
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
814
|
+
}
|
|
815
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
816
|
+
try {
|
|
817
|
+
const existing = await fs.lstat(targetPath);
|
|
818
|
+
if (existing.isSymbolicLink()) {
|
|
819
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
820
|
+
}
|
|
821
|
+
} catch (error) {
|
|
822
|
+
if (error.code !== "ENOENT") {
|
|
823
|
+
throw error;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
return targetPath;
|
|
827
|
+
}
|
|
828
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
829
|
+
if (byteLimit === false) {
|
|
830
|
+
return handle.readFile({ encoding: "utf8" });
|
|
831
|
+
}
|
|
832
|
+
const chunks = [];
|
|
833
|
+
let totalBytes = 0;
|
|
834
|
+
let position = 0;
|
|
835
|
+
while (true) {
|
|
836
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
837
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
838
|
+
if (bytesRead === 0) {
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
totalBytes += bytesRead;
|
|
842
|
+
if (totalBytes > byteLimit) {
|
|
843
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
844
|
+
}
|
|
845
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
846
|
+
position += bytesRead;
|
|
847
|
+
}
|
|
848
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// ../../src/internal/CacheStackGeneration.ts
|
|
852
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
853
|
+
function generationPrefix(generation) {
|
|
854
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
855
|
+
}
|
|
856
|
+
function qualifyGenerationKey(key, generation) {
|
|
857
|
+
const prefix = generationPrefix(generation);
|
|
858
|
+
return prefix ? `${prefix}${key}` : key;
|
|
859
|
+
}
|
|
860
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
861
|
+
return qualifyGenerationKey(pattern, generation);
|
|
862
|
+
}
|
|
863
|
+
function stripGenerationPrefix(key, generation) {
|
|
864
|
+
const prefix = generationPrefix(generation);
|
|
865
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
866
|
+
return key;
|
|
867
|
+
}
|
|
868
|
+
return key.slice(prefix.length);
|
|
869
|
+
}
|
|
870
|
+
function resolveGenerationCleanupTarget({
|
|
871
|
+
previousGeneration,
|
|
872
|
+
nextGeneration,
|
|
873
|
+
generationCleanup
|
|
874
|
+
}) {
|
|
875
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
return previousGeneration;
|
|
879
|
+
}
|
|
880
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
881
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
882
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
883
|
+
}
|
|
884
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
885
|
+
}
|
|
886
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
887
|
+
if (keys.length === 0) {
|
|
888
|
+
return [];
|
|
889
|
+
}
|
|
890
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
891
|
+
const batches = [];
|
|
892
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
893
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
894
|
+
}
|
|
895
|
+
return batches;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
899
|
+
var CacheStackMaintenance = class {
|
|
900
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
901
|
+
writeBehindQueue = [];
|
|
902
|
+
writeBehindTimer;
|
|
903
|
+
writeBehindFlushPromise;
|
|
904
|
+
generationCleanupPromise;
|
|
905
|
+
clearEpoch = 0;
|
|
906
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
907
|
+
if (writeStrategy !== "write-behind") {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
911
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
this.disposeWriteBehindTimer();
|
|
915
|
+
this.writeBehindTimer = setInterval(() => {
|
|
916
|
+
void flush();
|
|
917
|
+
}, flushIntervalMs);
|
|
918
|
+
this.writeBehindTimer.unref?.();
|
|
919
|
+
}
|
|
920
|
+
disposeWriteBehindTimer() {
|
|
921
|
+
if (!this.writeBehindTimer) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
clearInterval(this.writeBehindTimer);
|
|
925
|
+
this.writeBehindTimer = void 0;
|
|
926
|
+
}
|
|
927
|
+
beginClearEpoch() {
|
|
928
|
+
this.clearEpoch += 1;
|
|
929
|
+
this.keyEpochs.clear();
|
|
930
|
+
this.writeBehindQueue.length = 0;
|
|
931
|
+
}
|
|
932
|
+
currentClearEpoch() {
|
|
933
|
+
return this.clearEpoch;
|
|
934
|
+
}
|
|
935
|
+
currentKeyEpoch(key) {
|
|
936
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
937
|
+
}
|
|
938
|
+
bumpKeyEpochs(keys) {
|
|
939
|
+
for (const key of keys) {
|
|
940
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
944
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
945
|
+
return true;
|
|
946
|
+
}
|
|
947
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
948
|
+
return true;
|
|
949
|
+
}
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
953
|
+
this.writeBehindQueue.push(operation);
|
|
954
|
+
const batchSize = options?.batchSize ?? 100;
|
|
955
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
956
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
957
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
961
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
965
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
966
|
+
await this.writeBehindFlushPromise;
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const batchSize = options?.batchSize ?? 100;
|
|
970
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
971
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
972
|
+
try {
|
|
973
|
+
await this.writeBehindFlushPromise;
|
|
974
|
+
} finally {
|
|
975
|
+
this.writeBehindFlushPromise = void 0;
|
|
976
|
+
}
|
|
977
|
+
if (this.writeBehindQueue.length > 0) {
|
|
978
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
982
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
983
|
+
onError(generation, error);
|
|
984
|
+
});
|
|
985
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
986
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
987
|
+
this.generationCleanupPromise = void 0;
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
async waitForGenerationCleanup() {
|
|
992
|
+
await this.generationCleanupPromise;
|
|
993
|
+
}
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
// ../../src/internal/StoredValue.ts
|
|
997
|
+
function isStoredValueEnvelope(value) {
|
|
998
|
+
if (typeof value !== "object" || value === null) {
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
const v = value;
|
|
1002
|
+
if (v.__layercache !== 1) {
|
|
1003
|
+
return false;
|
|
1004
|
+
}
|
|
1005
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
1009
|
+
return false;
|
|
1010
|
+
}
|
|
1011
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
1012
|
+
return false;
|
|
1013
|
+
}
|
|
1014
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
1015
|
+
return false;
|
|
1016
|
+
}
|
|
1017
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1018
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1019
|
+
return false;
|
|
1020
|
+
}
|
|
1021
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1025
|
+
return false;
|
|
1026
|
+
}
|
|
1027
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1028
|
+
return false;
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1037
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1041
|
+
return false;
|
|
1042
|
+
}
|
|
1043
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1047
|
+
return false;
|
|
1048
|
+
}
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
function createStoredValueEnvelope(options) {
|
|
1052
|
+
const now = options.now ?? Date.now();
|
|
1053
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1054
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1055
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1056
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1057
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1058
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1059
|
+
return {
|
|
1060
|
+
__layercache: 1,
|
|
1061
|
+
kind: options.kind,
|
|
1062
|
+
value: options.value,
|
|
1063
|
+
freshUntil,
|
|
1064
|
+
staleUntil,
|
|
1065
|
+
errorUntil,
|
|
1066
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1067
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1068
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
1072
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1073
|
+
return { state: "fresh", value: stored, stored };
|
|
1074
|
+
}
|
|
1075
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1076
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1077
|
+
}
|
|
1078
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1079
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1080
|
+
}
|
|
1081
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1082
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1083
|
+
}
|
|
1084
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
1085
|
+
}
|
|
1086
|
+
function unwrapStoredValue(stored) {
|
|
1087
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1088
|
+
return stored;
|
|
1089
|
+
}
|
|
1090
|
+
if (stored.kind === "empty") {
|
|
1091
|
+
return null;
|
|
1092
|
+
}
|
|
1093
|
+
return stored.value ?? null;
|
|
1094
|
+
}
|
|
1095
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1096
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1097
|
+
return void 0;
|
|
1098
|
+
}
|
|
1099
|
+
const expiry = maxExpiry(stored);
|
|
1100
|
+
if (expiry === null) {
|
|
1101
|
+
return void 0;
|
|
1102
|
+
}
|
|
1103
|
+
const remainingMs = expiry - now;
|
|
1104
|
+
if (remainingMs <= 0) {
|
|
1105
|
+
return 1;
|
|
1106
|
+
}
|
|
1107
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1108
|
+
}
|
|
1109
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1110
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1111
|
+
return void 0;
|
|
1112
|
+
}
|
|
1113
|
+
const remainingMs = stored.freshUntil - now;
|
|
1114
|
+
if (remainingMs <= 0) {
|
|
1115
|
+
return 0;
|
|
1116
|
+
}
|
|
1117
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1118
|
+
}
|
|
1119
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1120
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1121
|
+
return stored;
|
|
1122
|
+
}
|
|
1123
|
+
return createStoredValueEnvelope({
|
|
1124
|
+
kind: stored.kind,
|
|
1125
|
+
value: stored.value,
|
|
1126
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1127
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1128
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1129
|
+
now
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function maxExpiry(stored) {
|
|
1133
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1134
|
+
(value) => value !== null
|
|
1135
|
+
);
|
|
1136
|
+
if (values.length === 0) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
return Math.max(...values);
|
|
1140
|
+
}
|
|
1141
|
+
function normalizePositiveSeconds(value) {
|
|
1142
|
+
if (!value || value <= 0) {
|
|
1143
|
+
return void 0;
|
|
1144
|
+
}
|
|
1145
|
+
return value;
|
|
1146
|
+
}
|
|
1147
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1148
|
+
if (value == null) {
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1155
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1156
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1157
|
+
}
|
|
1158
|
+
function shouldStartBackgroundRefresh({
|
|
1159
|
+
isDisconnecting,
|
|
1160
|
+
hasRefreshInFlight
|
|
1161
|
+
}) {
|
|
1162
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1163
|
+
}
|
|
1164
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1165
|
+
if (!gracefulDegradation) {
|
|
1166
|
+
return { degrade: false };
|
|
1167
|
+
}
|
|
1168
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1169
|
+
return {
|
|
1170
|
+
degrade: true,
|
|
1171
|
+
degradedUntil: now + retryAfterMs
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
function planFreshReadPolicies({
|
|
1175
|
+
stored,
|
|
1176
|
+
hasFetcher,
|
|
1177
|
+
slidingTtl,
|
|
1178
|
+
refreshAheadSeconds
|
|
1179
|
+
}) {
|
|
1180
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1181
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1182
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1183
|
+
return {
|
|
1184
|
+
refreshedStored,
|
|
1185
|
+
refreshedStoredTtl,
|
|
1186
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// ../../src/internal/CacheStackValidation.ts
|
|
1191
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1192
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
1193
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
1194
|
+
function validatePositiveNumber(name, value) {
|
|
1195
|
+
if (value === void 0) {
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1199
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
function validateNonNegativeNumber(name, value) {
|
|
1203
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1204
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
function validateLayerNumberOption(name, value) {
|
|
1208
|
+
if (value === void 0) {
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (typeof value === "number") {
|
|
1212
|
+
validateNonNegativeNumber(name, value);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1216
|
+
if (layerValue === void 0) {
|
|
1217
|
+
continue;
|
|
1218
|
+
}
|
|
1219
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function validateRateLimitOptions(name, options) {
|
|
1223
|
+
if (!options) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1227
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1228
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1229
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1230
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1231
|
+
}
|
|
1232
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1233
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
504
1234
|
}
|
|
505
|
-
return result;
|
|
506
1235
|
}
|
|
507
|
-
function
|
|
1236
|
+
function validateCacheKey(key) {
|
|
508
1237
|
if (key.length === 0) {
|
|
509
|
-
throw new Error("
|
|
1238
|
+
throw new Error("Cache key must not be empty.");
|
|
510
1239
|
}
|
|
511
|
-
if (key.length >
|
|
512
|
-
throw new Error(
|
|
1240
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1241
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
513
1242
|
}
|
|
514
1243
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
515
|
-
throw new Error("
|
|
1244
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1245
|
+
}
|
|
1246
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
1247
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
516
1248
|
}
|
|
1249
|
+
return key;
|
|
517
1250
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
/**
|
|
522
|
-
* Tests whether a glob-style pattern matches a value.
|
|
523
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
524
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
525
|
-
* quadratic memory usage on long patterns/keys.
|
|
526
|
-
*/
|
|
527
|
-
static matches(pattern, value) {
|
|
528
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1251
|
+
function validateTag(tag) {
|
|
1252
|
+
if (tag.length === 0) {
|
|
1253
|
+
throw new Error("Cache tag must not be empty.");
|
|
529
1254
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
*/
|
|
533
|
-
static matchLinear(pattern, value) {
|
|
534
|
-
let patternIndex = 0;
|
|
535
|
-
let valueIndex = 0;
|
|
536
|
-
let starIndex = -1;
|
|
537
|
-
let backtrackValueIndex = 0;
|
|
538
|
-
while (valueIndex < value.length) {
|
|
539
|
-
const patternChar = pattern[patternIndex];
|
|
540
|
-
const valueChar = value[valueIndex];
|
|
541
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
542
|
-
starIndex = patternIndex;
|
|
543
|
-
patternIndex += 1;
|
|
544
|
-
backtrackValueIndex = valueIndex;
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
548
|
-
patternIndex += 1;
|
|
549
|
-
valueIndex += 1;
|
|
550
|
-
continue;
|
|
551
|
-
}
|
|
552
|
-
if (starIndex !== -1) {
|
|
553
|
-
patternIndex = starIndex + 1;
|
|
554
|
-
backtrackValueIndex += 1;
|
|
555
|
-
valueIndex = backtrackValueIndex;
|
|
556
|
-
continue;
|
|
557
|
-
}
|
|
558
|
-
return false;
|
|
559
|
-
}
|
|
560
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
561
|
-
patternIndex += 1;
|
|
562
|
-
}
|
|
563
|
-
return patternIndex === pattern.length;
|
|
1255
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
1256
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
564
1257
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
// ../../src/internal/CacheKeyDiscovery.ts
|
|
568
|
-
var CacheKeyDiscovery = class {
|
|
569
|
-
constructor(options) {
|
|
570
|
-
this.options = options;
|
|
1258
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
1259
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
571
1260
|
}
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const { tagIndex } = this.options;
|
|
575
|
-
const matches = new Set(
|
|
576
|
-
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
577
|
-
);
|
|
578
|
-
await Promise.all(
|
|
579
|
-
this.options.layers.map(async (layer) => {
|
|
580
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
try {
|
|
584
|
-
const keys = await layer.keys();
|
|
585
|
-
for (const key of keys) {
|
|
586
|
-
if (key.startsWith(prefix)) {
|
|
587
|
-
matches.add(key);
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
} catch (error) {
|
|
591
|
-
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
592
|
-
}
|
|
593
|
-
})
|
|
594
|
-
);
|
|
595
|
-
return [...matches];
|
|
1261
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
1262
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
596
1263
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
try {
|
|
605
|
-
const keys = await layer.keys();
|
|
606
|
-
for (const key of keys) {
|
|
607
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
608
|
-
matches.add(key);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
} catch (error) {
|
|
612
|
-
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
613
|
-
}
|
|
614
|
-
})
|
|
615
|
-
);
|
|
616
|
-
return [...matches];
|
|
1264
|
+
return tag;
|
|
1265
|
+
}
|
|
1266
|
+
function validateTags(tags) {
|
|
1267
|
+
if (!tags) {
|
|
1268
|
+
return;
|
|
617
1269
|
}
|
|
618
|
-
|
|
1270
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
1271
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
1272
|
+
}
|
|
1273
|
+
for (const tag of tags) {
|
|
1274
|
+
validateTag(tag);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function validatePattern(pattern) {
|
|
1278
|
+
if (pattern.length === 0) {
|
|
1279
|
+
throw new Error("Pattern must not be empty.");
|
|
1280
|
+
}
|
|
1281
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1282
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
1283
|
+
}
|
|
1284
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
1285
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function validateTtlPolicy(name, policy) {
|
|
1289
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
if ("alignTo" in policy) {
|
|
1293
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
throw new Error(`${name} is invalid.`);
|
|
1297
|
+
}
|
|
1298
|
+
function validateAdaptiveTtlOptions(options) {
|
|
1299
|
+
if (!options || options === true) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1303
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1304
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1305
|
+
}
|
|
1306
|
+
function validateCircuitBreakerOptions(options) {
|
|
1307
|
+
if (!options) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1311
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1312
|
+
}
|
|
619
1313
|
|
|
620
1314
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
621
1315
|
var CircuitBreakerManager = class {
|
|
@@ -646,7 +1340,6 @@ var CircuitBreakerManager = class {
|
|
|
646
1340
|
if (!options) {
|
|
647
1341
|
return;
|
|
648
1342
|
}
|
|
649
|
-
this.pruneIfNeeded();
|
|
650
1343
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
651
1344
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
652
1345
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -655,6 +1348,7 @@ var CircuitBreakerManager = class {
|
|
|
655
1348
|
state.openUntil = Date.now() + cooldownMs;
|
|
656
1349
|
}
|
|
657
1350
|
this.breakers.set(key, state);
|
|
1351
|
+
this.pruneIfNeeded();
|
|
658
1352
|
}
|
|
659
1353
|
recordSuccess(key) {
|
|
660
1354
|
this.breakers.delete(key);
|
|
@@ -969,158 +1663,34 @@ var MetricsCollector = class {
|
|
|
969
1663
|
for (const layer of allLayers) {
|
|
970
1664
|
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
971
1665
|
const m = this.data.missesByLayer[layer] ?? 0;
|
|
972
|
-
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
973
|
-
}
|
|
974
|
-
return { overall, byLayer };
|
|
975
|
-
}
|
|
976
|
-
empty() {
|
|
977
|
-
return {
|
|
978
|
-
hits: 0,
|
|
979
|
-
misses: 0,
|
|
980
|
-
fetches: 0,
|
|
981
|
-
sets: 0,
|
|
982
|
-
deletes: 0,
|
|
983
|
-
backfills: 0,
|
|
984
|
-
invalidations: 0,
|
|
985
|
-
staleHits: 0,
|
|
986
|
-
refreshes: 0,
|
|
987
|
-
refreshErrors: 0,
|
|
988
|
-
writeFailures: 0,
|
|
989
|
-
singleFlightWaits: 0,
|
|
990
|
-
negativeCacheHits: 0,
|
|
991
|
-
circuitBreakerTrips: 0,
|
|
992
|
-
degradedOperations: 0,
|
|
993
|
-
hitsByLayer: {},
|
|
994
|
-
missesByLayer: {},
|
|
995
|
-
latencyByLayer: {},
|
|
996
|
-
resetAt: Date.now()
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
};
|
|
1000
|
-
|
|
1001
|
-
// ../../src/internal/StoredValue.ts
|
|
1002
|
-
function isStoredValueEnvelope(value) {
|
|
1003
|
-
if (typeof value !== "object" || value === null) {
|
|
1004
|
-
return false;
|
|
1005
|
-
}
|
|
1006
|
-
const v = value;
|
|
1007
|
-
if (v.__layercache !== 1) {
|
|
1008
|
-
return false;
|
|
1009
|
-
}
|
|
1010
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
1011
|
-
return false;
|
|
1012
|
-
}
|
|
1013
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1014
|
-
return false;
|
|
1015
|
-
}
|
|
1016
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1017
|
-
return false;
|
|
1018
|
-
}
|
|
1019
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1020
|
-
return false;
|
|
1021
|
-
}
|
|
1022
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
1023
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
1024
|
-
return false;
|
|
1025
|
-
}
|
|
1026
|
-
return true;
|
|
1027
|
-
}
|
|
1028
|
-
function createStoredValueEnvelope(options) {
|
|
1029
|
-
const now = options.now ?? Date.now();
|
|
1030
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1031
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1032
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1033
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1034
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1035
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1036
|
-
return {
|
|
1037
|
-
__layercache: 1,
|
|
1038
|
-
kind: options.kind,
|
|
1039
|
-
value: options.value,
|
|
1040
|
-
freshUntil,
|
|
1041
|
-
staleUntil,
|
|
1042
|
-
errorUntil,
|
|
1043
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1044
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1045
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
1049
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1050
|
-
return { state: "fresh", value: stored, stored };
|
|
1051
|
-
}
|
|
1052
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1053
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1054
|
-
}
|
|
1055
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1056
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1057
|
-
}
|
|
1058
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1059
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1060
|
-
}
|
|
1061
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
1062
|
-
}
|
|
1063
|
-
function unwrapStoredValue(stored) {
|
|
1064
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1065
|
-
return stored;
|
|
1066
|
-
}
|
|
1067
|
-
if (stored.kind === "empty") {
|
|
1068
|
-
return null;
|
|
1069
|
-
}
|
|
1070
|
-
return stored.value ?? null;
|
|
1071
|
-
}
|
|
1072
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1073
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1074
|
-
return void 0;
|
|
1075
|
-
}
|
|
1076
|
-
const expiry = maxExpiry(stored);
|
|
1077
|
-
if (expiry === null) {
|
|
1078
|
-
return void 0;
|
|
1079
|
-
}
|
|
1080
|
-
const remainingMs = expiry - now;
|
|
1081
|
-
if (remainingMs <= 0) {
|
|
1082
|
-
return 1;
|
|
1083
|
-
}
|
|
1084
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1085
|
-
}
|
|
1086
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1087
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1088
|
-
return void 0;
|
|
1089
|
-
}
|
|
1090
|
-
const remainingMs = stored.freshUntil - now;
|
|
1091
|
-
if (remainingMs <= 0) {
|
|
1092
|
-
return 0;
|
|
1093
|
-
}
|
|
1094
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1095
|
-
}
|
|
1096
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1097
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1098
|
-
return stored;
|
|
1099
|
-
}
|
|
1100
|
-
return createStoredValueEnvelope({
|
|
1101
|
-
kind: stored.kind,
|
|
1102
|
-
value: stored.value,
|
|
1103
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1104
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1105
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1106
|
-
now
|
|
1107
|
-
});
|
|
1108
|
-
}
|
|
1109
|
-
function maxExpiry(stored) {
|
|
1110
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1111
|
-
(value) => value !== null
|
|
1112
|
-
);
|
|
1113
|
-
if (values.length === 0) {
|
|
1114
|
-
return null;
|
|
1115
|
-
}
|
|
1116
|
-
return Math.max(...values);
|
|
1117
|
-
}
|
|
1118
|
-
function normalizePositiveSeconds(value) {
|
|
1119
|
-
if (!value || value <= 0) {
|
|
1120
|
-
return void 0;
|
|
1666
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
1667
|
+
}
|
|
1668
|
+
return { overall, byLayer };
|
|
1121
1669
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1670
|
+
empty() {
|
|
1671
|
+
return {
|
|
1672
|
+
hits: 0,
|
|
1673
|
+
misses: 0,
|
|
1674
|
+
fetches: 0,
|
|
1675
|
+
sets: 0,
|
|
1676
|
+
deletes: 0,
|
|
1677
|
+
backfills: 0,
|
|
1678
|
+
invalidations: 0,
|
|
1679
|
+
staleHits: 0,
|
|
1680
|
+
refreshes: 0,
|
|
1681
|
+
refreshErrors: 0,
|
|
1682
|
+
writeFailures: 0,
|
|
1683
|
+
singleFlightWaits: 0,
|
|
1684
|
+
negativeCacheHits: 0,
|
|
1685
|
+
circuitBreakerTrips: 0,
|
|
1686
|
+
degradedOperations: 0,
|
|
1687
|
+
hitsByLayer: {},
|
|
1688
|
+
missesByLayer: {},
|
|
1689
|
+
latencyByLayer: {},
|
|
1690
|
+
resetAt: Date.now()
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
};
|
|
1124
1694
|
|
|
1125
1695
|
// ../../src/internal/TtlResolver.ts
|
|
1126
1696
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1275,6 +1845,11 @@ var TagIndex = class {
|
|
|
1275
1845
|
async keysForTag(tag) {
|
|
1276
1846
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1277
1847
|
}
|
|
1848
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1849
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1850
|
+
await visitor(key);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1278
1853
|
async keysForPrefix(prefix) {
|
|
1279
1854
|
const node = this.findNode(prefix);
|
|
1280
1855
|
if (!node) {
|
|
@@ -1284,6 +1859,13 @@ var TagIndex = class {
|
|
|
1284
1859
|
this.collectFromNode(node, prefix, matches);
|
|
1285
1860
|
return matches;
|
|
1286
1861
|
}
|
|
1862
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1863
|
+
const node = this.findNode(prefix);
|
|
1864
|
+
if (!node) {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1868
|
+
}
|
|
1287
1869
|
async tagsForKey(key) {
|
|
1288
1870
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1289
1871
|
}
|
|
@@ -1292,6 +1874,12 @@ var TagIndex = class {
|
|
|
1292
1874
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1293
1875
|
return [...matches];
|
|
1294
1876
|
}
|
|
1877
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1878
|
+
const matches = await this.matchPattern(pattern);
|
|
1879
|
+
for (const key of matches) {
|
|
1880
|
+
await visitor(key);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1295
1883
|
async clear() {
|
|
1296
1884
|
this.tagToKeys.clear();
|
|
1297
1885
|
this.keyToTags.clear();
|
|
@@ -1341,6 +1929,14 @@ var TagIndex = class {
|
|
|
1341
1929
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1342
1930
|
}
|
|
1343
1931
|
}
|
|
1932
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1933
|
+
if (node.terminal) {
|
|
1934
|
+
await visitor(prefix);
|
|
1935
|
+
}
|
|
1936
|
+
for (const [character, child] of node.children) {
|
|
1937
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1344
1940
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1345
1941
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1346
1942
|
return;
|
|
@@ -1458,22 +2054,27 @@ var TagIndex = class {
|
|
|
1458
2054
|
|
|
1459
2055
|
// ../../src/serialization/JsonSerializer.ts
|
|
1460
2056
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2057
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1461
2058
|
var JsonSerializer = class {
|
|
1462
2059
|
serialize(value) {
|
|
1463
2060
|
return JSON.stringify(value);
|
|
1464
2061
|
}
|
|
1465
2062
|
deserialize(payload) {
|
|
1466
2063
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1467
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
2064
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1468
2065
|
}
|
|
1469
2066
|
};
|
|
1470
2067
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1471
|
-
function sanitizeJsonValue(value, depth) {
|
|
2068
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
2069
|
+
state.count += 1;
|
|
2070
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
2071
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
2072
|
+
}
|
|
1472
2073
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1473
|
-
|
|
2074
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1474
2075
|
}
|
|
1475
2076
|
if (Array.isArray(value)) {
|
|
1476
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
2077
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1477
2078
|
}
|
|
1478
2079
|
if (!isPlainObject(value)) {
|
|
1479
2080
|
return value;
|
|
@@ -1483,7 +2084,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1483
2084
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1484
2085
|
continue;
|
|
1485
2086
|
}
|
|
1486
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
2087
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1487
2088
|
}
|
|
1488
2089
|
return sanitized;
|
|
1489
2090
|
}
|
|
@@ -1532,10 +2133,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1532
2133
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1533
2134
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1534
2135
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1535
|
-
var
|
|
1536
|
-
var
|
|
2136
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2137
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
2138
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
2139
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1537
2140
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1538
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1539
2141
|
var DebugLogger = class {
|
|
1540
2142
|
enabled;
|
|
1541
2143
|
constructor(enabled) {
|
|
@@ -1622,13 +2224,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1622
2224
|
snapshotSerializer = new JsonSerializer();
|
|
1623
2225
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1624
2226
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2227
|
+
maintenance = new CacheStackMaintenance();
|
|
1625
2228
|
ttlResolver;
|
|
1626
2229
|
circuitBreakerManager;
|
|
1627
2230
|
currentGeneration;
|
|
1628
|
-
writeBehindQueue = [];
|
|
1629
|
-
writeBehindTimer;
|
|
1630
|
-
writeBehindFlushPromise;
|
|
1631
|
-
generationCleanupPromise;
|
|
1632
2231
|
isDisconnecting = false;
|
|
1633
2232
|
disconnectPromise;
|
|
1634
2233
|
/**
|
|
@@ -1638,7 +2237,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1638
2237
|
* and no `fetcher` is provided.
|
|
1639
2238
|
*/
|
|
1640
2239
|
async get(key, fetcher, options) {
|
|
1641
|
-
const normalizedKey = this.qualifyKey(
|
|
2240
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1642
2241
|
this.validateWriteOptions(options);
|
|
1643
2242
|
await this.awaitStartup("get");
|
|
1644
2243
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1708,7 +2307,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1708
2307
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1709
2308
|
*/
|
|
1710
2309
|
async has(key) {
|
|
1711
|
-
const normalizedKey = this.qualifyKey(
|
|
2310
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1712
2311
|
await this.awaitStartup("has");
|
|
1713
2312
|
for (const layer of this.layers) {
|
|
1714
2313
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1741,7 +2340,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1741
2340
|
* that has it, or null if the key is not found / has no TTL.
|
|
1742
2341
|
*/
|
|
1743
2342
|
async ttl(key) {
|
|
1744
|
-
const normalizedKey = this.qualifyKey(
|
|
2343
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1745
2344
|
await this.awaitStartup("ttl");
|
|
1746
2345
|
for (const layer of this.layers) {
|
|
1747
2346
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1763,7 +2362,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1763
2362
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1764
2363
|
*/
|
|
1765
2364
|
async set(key, value, options) {
|
|
1766
|
-
const normalizedKey = this.qualifyKey(
|
|
2365
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1767
2366
|
this.validateWriteOptions(options);
|
|
1768
2367
|
await this.awaitStartup("set");
|
|
1769
2368
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1772,7 +2371,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1772
2371
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1773
2372
|
*/
|
|
1774
2373
|
async delete(key) {
|
|
1775
|
-
const normalizedKey = this.qualifyKey(
|
|
2374
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1776
2375
|
await this.awaitStartup("delete");
|
|
1777
2376
|
await this.deleteKeys([normalizedKey]);
|
|
1778
2377
|
await this.publishInvalidation({
|
|
@@ -1784,6 +2383,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1784
2383
|
}
|
|
1785
2384
|
async clear() {
|
|
1786
2385
|
await this.awaitStartup("clear");
|
|
2386
|
+
this.maintenance.beginClearEpoch();
|
|
1787
2387
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1788
2388
|
await this.tagIndex.clear();
|
|
1789
2389
|
this.ttlResolver.clearProfiles();
|
|
@@ -1800,7 +2400,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1800
2400
|
return;
|
|
1801
2401
|
}
|
|
1802
2402
|
await this.awaitStartup("mdelete");
|
|
1803
|
-
const normalizedKeys = keys.map((k) =>
|
|
2403
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1804
2404
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1805
2405
|
await this.deleteKeys(cacheKeys);
|
|
1806
2406
|
await this.publishInvalidation({
|
|
@@ -1817,7 +2417,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1817
2417
|
}
|
|
1818
2418
|
const normalizedEntries = entries.map((entry) => ({
|
|
1819
2419
|
...entry,
|
|
1820
|
-
key: this.qualifyKey(
|
|
2420
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1821
2421
|
}));
|
|
1822
2422
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1823
2423
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1826,7 +2426,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1826
2426
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1827
2427
|
return Promise.all(
|
|
1828
2428
|
normalizedEntries.map((entry) => {
|
|
1829
|
-
const optionsSignature =
|
|
2429
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1830
2430
|
const existing = pendingReads.get(entry.key);
|
|
1831
2431
|
if (!existing) {
|
|
1832
2432
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1895,7 +2495,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1895
2495
|
this.assertActive("mset");
|
|
1896
2496
|
const normalizedEntries = entries.map((entry) => ({
|
|
1897
2497
|
...entry,
|
|
1898
|
-
key: this.qualifyKey(
|
|
2498
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1899
2499
|
}));
|
|
1900
2500
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1901
2501
|
await this.awaitStartup("mset");
|
|
@@ -1938,7 +2538,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1938
2538
|
*/
|
|
1939
2539
|
wrap(prefix, fetcher, options = {}) {
|
|
1940
2540
|
return (...args) => {
|
|
1941
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2541
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1942
2542
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1943
2543
|
return this.get(key, () => fetcher(...args), options);
|
|
1944
2544
|
};
|
|
@@ -1948,11 +2548,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1948
2548
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1949
2549
|
*/
|
|
1950
2550
|
namespace(prefix) {
|
|
2551
|
+
validateNamespaceKey(prefix);
|
|
1951
2552
|
return new CacheNamespace(this, prefix);
|
|
1952
2553
|
}
|
|
1953
2554
|
async invalidateByTag(tag) {
|
|
2555
|
+
validateTag(tag);
|
|
1954
2556
|
await this.awaitStartup("invalidateByTag");
|
|
1955
|
-
const keys = await this.
|
|
2557
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1956
2558
|
await this.deleteKeys(keys);
|
|
1957
2559
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1958
2560
|
}
|
|
@@ -1960,23 +2562,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1960
2562
|
if (tags.length === 0) {
|
|
1961
2563
|
return;
|
|
1962
2564
|
}
|
|
2565
|
+
validateTags(tags);
|
|
1963
2566
|
await this.awaitStartup("invalidateByTags");
|
|
1964
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2567
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1965
2568
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2569
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1966
2570
|
await this.deleteKeys(keys);
|
|
1967
2571
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1968
2572
|
}
|
|
1969
2573
|
async invalidateByPattern(pattern) {
|
|
1970
|
-
|
|
2574
|
+
validatePattern(pattern);
|
|
1971
2575
|
await this.awaitStartup("invalidateByPattern");
|
|
1972
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2576
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2577
|
+
this.qualifyPattern(pattern),
|
|
2578
|
+
this.invalidationMaxKeys()
|
|
2579
|
+
);
|
|
1973
2580
|
await this.deleteKeys(keys);
|
|
1974
2581
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1975
2582
|
}
|
|
1976
2583
|
async invalidateByPrefix(prefix) {
|
|
1977
2584
|
await this.awaitStartup("invalidateByPrefix");
|
|
1978
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1979
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2585
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2586
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1980
2587
|
await this.deleteKeys(keys);
|
|
1981
2588
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1982
2589
|
}
|
|
@@ -2034,9 +2641,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2034
2641
|
bumpGeneration(nextGeneration) {
|
|
2035
2642
|
const current = this.currentGeneration ?? 0;
|
|
2036
2643
|
const previousGeneration = this.currentGeneration;
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2644
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
2645
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
2646
|
+
previousGeneration,
|
|
2647
|
+
nextGeneration: updatedGeneration,
|
|
2648
|
+
generationCleanup: this.options.generationCleanup
|
|
2649
|
+
});
|
|
2650
|
+
this.currentGeneration = updatedGeneration;
|
|
2651
|
+
if (generationToCleanup !== null) {
|
|
2652
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
2040
2653
|
}
|
|
2041
2654
|
return this.currentGeneration;
|
|
2042
2655
|
}
|
|
@@ -2046,7 +2659,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2046
2659
|
* Returns `null` if the key does not exist in any layer.
|
|
2047
2660
|
*/
|
|
2048
2661
|
async inspect(key) {
|
|
2049
|
-
const userKey =
|
|
2662
|
+
const userKey = validateCacheKey(key);
|
|
2050
2663
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2051
2664
|
await this.awaitStartup("inspect");
|
|
2052
2665
|
const foundInLayers = [];
|
|
@@ -2083,50 +2696,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2083
2696
|
}
|
|
2084
2697
|
async exportState() {
|
|
2085
2698
|
await this.awaitStartup("exportState");
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
const keys = await layer.keys();
|
|
2092
|
-
for (const key of keys) {
|
|
2093
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2094
|
-
if (exported.has(exportedKey)) {
|
|
2095
|
-
continue;
|
|
2096
|
-
}
|
|
2097
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2098
|
-
if (stored === null) {
|
|
2099
|
-
continue;
|
|
2100
|
-
}
|
|
2101
|
-
exported.set(exportedKey, {
|
|
2102
|
-
key: exportedKey,
|
|
2103
|
-
value: stored,
|
|
2104
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2105
|
-
});
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
return [...exported.values()];
|
|
2699
|
+
const entries = [];
|
|
2700
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2701
|
+
entries.push(entry);
|
|
2702
|
+
});
|
|
2703
|
+
return entries;
|
|
2109
2704
|
}
|
|
2110
2705
|
async importState(entries) {
|
|
2111
2706
|
await this.awaitStartup("importState");
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2707
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2708
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2709
|
+
value: entry.value,
|
|
2710
|
+
ttl: entry.ttl
|
|
2711
|
+
}));
|
|
2712
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2713
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2714
|
+
await Promise.all(
|
|
2715
|
+
batch.map(async (entry) => {
|
|
2716
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2717
|
+
await this.tagIndex.touch(entry.key);
|
|
2718
|
+
})
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2119
2721
|
}
|
|
2120
2722
|
async persistToFile(filePath) {
|
|
2121
2723
|
this.assertActive("persistToFile");
|
|
2122
|
-
const snapshot = await this.exportState();
|
|
2123
2724
|
const { promises: fs } = await import("fs");
|
|
2124
|
-
|
|
2725
|
+
const path = await import("path");
|
|
2726
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2727
|
+
const tempPath = path.join(
|
|
2728
|
+
path.dirname(targetPath),
|
|
2729
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2730
|
+
);
|
|
2731
|
+
let handle;
|
|
2732
|
+
try {
|
|
2733
|
+
handle = await fs.open(tempPath, "wx");
|
|
2734
|
+
const openedHandle = handle;
|
|
2735
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2736
|
+
let wroteAny = false;
|
|
2737
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2738
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2739
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2740
|
+
wroteAny = true;
|
|
2741
|
+
});
|
|
2742
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2743
|
+
await openedHandle.close();
|
|
2744
|
+
handle = void 0;
|
|
2745
|
+
await fs.rename(tempPath, targetPath);
|
|
2746
|
+
} catch (error) {
|
|
2747
|
+
await handle?.close().catch(() => void 0);
|
|
2748
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
2749
|
+
throw error;
|
|
2750
|
+
}
|
|
2125
2751
|
}
|
|
2126
2752
|
async restoreFromFile(filePath) {
|
|
2127
2753
|
this.assertActive("restoreFromFile");
|
|
2128
|
-
const { promises: fs } = await import("fs");
|
|
2129
|
-
const
|
|
2754
|
+
const { promises: fs, constants } = await import("fs");
|
|
2755
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2756
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2757
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2758
|
+
let raw;
|
|
2759
|
+
try {
|
|
2760
|
+
if (snapshotMaxBytes !== false) {
|
|
2761
|
+
const stat = await handle.stat();
|
|
2762
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2763
|
+
throw new Error(
|
|
2764
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2765
|
+
);
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2769
|
+
} finally {
|
|
2770
|
+
await handle.close();
|
|
2771
|
+
}
|
|
2130
2772
|
let parsed;
|
|
2131
2773
|
try {
|
|
2132
2774
|
parsed = JSON.parse(raw);
|
|
@@ -2151,12 +2793,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2151
2793
|
await this.startup;
|
|
2152
2794
|
await this.unsubscribeInvalidation?.();
|
|
2153
2795
|
await this.flushWriteBehindQueue();
|
|
2154
|
-
await this.
|
|
2796
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
2155
2797
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2156
|
-
|
|
2157
|
-
clearInterval(this.writeBehindTimer);
|
|
2158
|
-
this.writeBehindTimer = void 0;
|
|
2159
|
-
}
|
|
2798
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
2160
2799
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2161
2800
|
})();
|
|
2162
2801
|
}
|
|
@@ -2170,14 +2809,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2170
2809
|
await this.handleInvalidationMessage(message);
|
|
2171
2810
|
});
|
|
2172
2811
|
}
|
|
2173
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2812
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2174
2813
|
const fetchTask = async () => {
|
|
2175
2814
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2176
2815
|
if (secondHit.found) {
|
|
2177
2816
|
this.metricsCollector.increment("hits");
|
|
2178
2817
|
return secondHit.value;
|
|
2179
2818
|
}
|
|
2180
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2819
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2181
2820
|
};
|
|
2182
2821
|
const singleFlightTask = async () => {
|
|
2183
2822
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2187,7 +2826,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2187
2826
|
key,
|
|
2188
2827
|
this.resolveSingleFlightOptions(),
|
|
2189
2828
|
fetchTask,
|
|
2190
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2829
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2191
2830
|
);
|
|
2192
2831
|
};
|
|
2193
2832
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2195,7 +2834,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2195
2834
|
}
|
|
2196
2835
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2197
2836
|
}
|
|
2198
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2837
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2199
2838
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2200
2839
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2201
2840
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2209,9 +2848,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2209
2848
|
}
|
|
2210
2849
|
await this.sleep(pollIntervalMs);
|
|
2211
2850
|
}
|
|
2212
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2851
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2213
2852
|
}
|
|
2214
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2853
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2215
2854
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2216
2855
|
this.metricsCollector.increment("fetches");
|
|
2217
2856
|
const fetchStart = Date.now();
|
|
@@ -2232,6 +2871,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2232
2871
|
if (!this.shouldNegativeCache(options)) {
|
|
2233
2872
|
return null;
|
|
2234
2873
|
}
|
|
2874
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2875
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2876
|
+
key,
|
|
2877
|
+
expectedClearEpoch,
|
|
2878
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2879
|
+
expectedKeyEpoch,
|
|
2880
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2881
|
+
});
|
|
2882
|
+
return null;
|
|
2883
|
+
}
|
|
2235
2884
|
await this.storeEntry(key, "empty", null, options);
|
|
2236
2885
|
return null;
|
|
2237
2886
|
}
|
|
@@ -2244,11 +2893,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2244
2893
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2245
2894
|
}
|
|
2246
2895
|
}
|
|
2896
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2897
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2898
|
+
key,
|
|
2899
|
+
expectedClearEpoch,
|
|
2900
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2901
|
+
expectedKeyEpoch,
|
|
2902
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2903
|
+
});
|
|
2904
|
+
return fetched;
|
|
2905
|
+
}
|
|
2247
2906
|
await this.storeEntry(key, "value", fetched, options);
|
|
2248
2907
|
return fetched;
|
|
2249
2908
|
}
|
|
2250
2909
|
async storeEntry(key, kind, value, options) {
|
|
2910
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2911
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2251
2912
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2913
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2252
2916
|
if (options?.tags) {
|
|
2253
2917
|
await this.tagIndex.track(key, options.tags);
|
|
2254
2918
|
} else {
|
|
@@ -2263,6 +2927,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2263
2927
|
}
|
|
2264
2928
|
async writeBatch(entries) {
|
|
2265
2929
|
const now = Date.now();
|
|
2930
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2931
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2266
2932
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2267
2933
|
const immediateOperations = [];
|
|
2268
2934
|
const deferredOperations = [];
|
|
@@ -2279,12 +2945,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2279
2945
|
}
|
|
2280
2946
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2281
2947
|
const operation = async () => {
|
|
2948
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2949
|
+
return;
|
|
2950
|
+
}
|
|
2951
|
+
const activeEntries = layerEntries.filter(
|
|
2952
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2953
|
+
);
|
|
2954
|
+
if (activeEntries.length === 0) {
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2282
2957
|
try {
|
|
2283
2958
|
if (layer.setMany) {
|
|
2284
|
-
await layer.setMany(
|
|
2959
|
+
await layer.setMany(activeEntries);
|
|
2285
2960
|
return;
|
|
2286
2961
|
}
|
|
2287
|
-
await Promise.all(
|
|
2962
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2288
2963
|
} catch (error) {
|
|
2289
2964
|
await this.handleLayerFailure(layer, "write", error);
|
|
2290
2965
|
}
|
|
@@ -2297,7 +2972,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2297
2972
|
}
|
|
2298
2973
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2299
2974
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2975
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2300
2978
|
for (const entry of entries) {
|
|
2979
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2980
|
+
continue;
|
|
2981
|
+
}
|
|
2301
2982
|
if (entry.options?.tags) {
|
|
2302
2983
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2303
2984
|
} else {
|
|
@@ -2399,10 +3080,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2399
3080
|
}
|
|
2400
3081
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2401
3082
|
const now = Date.now();
|
|
3083
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3084
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2402
3085
|
const immediateOperations = [];
|
|
2403
3086
|
const deferredOperations = [];
|
|
2404
3087
|
for (const layer of this.layers) {
|
|
2405
3088
|
const operation = async () => {
|
|
3089
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
2406
3092
|
if (this.shouldSkipLayer(layer)) {
|
|
2407
3093
|
return;
|
|
2408
3094
|
}
|
|
@@ -2463,13 +3149,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2463
3149
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2464
3150
|
}
|
|
2465
3151
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2466
|
-
if (
|
|
3152
|
+
if (!shouldStartBackgroundRefresh({
|
|
3153
|
+
isDisconnecting: this.isDisconnecting,
|
|
3154
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
3155
|
+
})) {
|
|
2467
3156
|
return;
|
|
2468
3157
|
}
|
|
3158
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3159
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2469
3160
|
const refresh = (async () => {
|
|
2470
3161
|
this.metricsCollector.increment("refreshes");
|
|
2471
3162
|
try {
|
|
2472
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
3163
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2473
3164
|
} catch (error) {
|
|
2474
3165
|
this.metricsCollector.increment("refreshErrors");
|
|
2475
3166
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2479,14 +3170,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2479
3170
|
})();
|
|
2480
3171
|
this.backgroundRefreshes.set(key, refresh);
|
|
2481
3172
|
}
|
|
2482
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
3173
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2483
3174
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2484
3175
|
await this.fetchWithGuards(
|
|
2485
3176
|
key,
|
|
2486
3177
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2487
3178
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2488
3179
|
}),
|
|
2489
|
-
options
|
|
3180
|
+
options,
|
|
3181
|
+
expectedClearEpoch,
|
|
3182
|
+
expectedKeyEpoch
|
|
2490
3183
|
);
|
|
2491
3184
|
}
|
|
2492
3185
|
resolveSingleFlightOptions() {
|
|
@@ -2501,6 +3194,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2501
3194
|
if (keys.length === 0) {
|
|
2502
3195
|
return;
|
|
2503
3196
|
}
|
|
3197
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2504
3198
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2505
3199
|
for (const key of keys) {
|
|
2506
3200
|
await this.tagIndex.remove(key);
|
|
@@ -2523,21 +3217,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2523
3217
|
return;
|
|
2524
3218
|
}
|
|
2525
3219
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2526
|
-
if (localLayers.length === 0) {
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
3220
|
if (message.scope === "clear") {
|
|
3221
|
+
this.maintenance.beginClearEpoch();
|
|
2530
3222
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2531
3223
|
await this.tagIndex.clear();
|
|
2532
3224
|
this.ttlResolver.clearProfiles();
|
|
3225
|
+
this.circuitBreakerManager.clear();
|
|
2533
3226
|
return;
|
|
2534
3227
|
}
|
|
2535
3228
|
const keys = message.keys ?? [];
|
|
3229
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2536
3230
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2537
3231
|
if (message.operation !== "write") {
|
|
2538
3232
|
for (const key of keys) {
|
|
2539
3233
|
await this.tagIndex.remove(key);
|
|
2540
3234
|
this.ttlResolver.deleteProfile(key);
|
|
3235
|
+
this.circuitBreakerManager.delete(key);
|
|
2541
3236
|
}
|
|
2542
3237
|
}
|
|
2543
3238
|
}
|
|
@@ -2589,35 +3284,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2589
3284
|
shouldBroadcastL1Invalidation() {
|
|
2590
3285
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2591
3286
|
}
|
|
2592
|
-
shouldCleanupGenerations() {
|
|
2593
|
-
return Boolean(this.options.generationCleanup);
|
|
2594
|
-
}
|
|
2595
|
-
generationCleanupBatchSize() {
|
|
2596
|
-
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2597
|
-
return configured ?? 500;
|
|
2598
|
-
}
|
|
2599
3287
|
scheduleGenerationCleanup(generation) {
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
this.generationCleanupPromise = void 0;
|
|
3288
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3289
|
+
generation,
|
|
3290
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3291
|
+
(failedGeneration, error) => {
|
|
3292
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3293
|
+
generation: failedGeneration,
|
|
3294
|
+
error: this.formatError(error)
|
|
3295
|
+
});
|
|
2609
3296
|
}
|
|
2610
|
-
|
|
3297
|
+
);
|
|
2611
3298
|
}
|
|
2612
3299
|
async cleanupGeneration(generation) {
|
|
2613
3300
|
const prefix = `v${generation}:`;
|
|
2614
3301
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2615
|
-
|
|
2616
|
-
return;
|
|
2617
|
-
}
|
|
2618
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
2619
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2620
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3302
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
2621
3303
|
await this.deleteKeys(batch);
|
|
2622
3304
|
await this.publishInvalidation({
|
|
2623
3305
|
scope: "keys",
|
|
@@ -2628,58 +3310,34 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2628
3310
|
}
|
|
2629
3311
|
}
|
|
2630
3312
|
initializeWriteBehind(options) {
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
return;
|
|
2637
|
-
}
|
|
2638
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2639
|
-
void this.flushWriteBehindQueue();
|
|
2640
|
-
}, flushIntervalMs);
|
|
2641
|
-
this.writeBehindTimer.unref?.();
|
|
3313
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3314
|
+
this.options.writeStrategy,
|
|
3315
|
+
options,
|
|
3316
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3317
|
+
);
|
|
2642
3318
|
}
|
|
2643
3319
|
shouldWriteBehind(layer) {
|
|
2644
3320
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2645
3321
|
}
|
|
2646
3322
|
async enqueueWriteBehind(operation) {
|
|
2647
|
-
this.
|
|
2648
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2649
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2650
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2651
|
-
await this.flushWriteBehindQueue();
|
|
2652
|
-
return;
|
|
2653
|
-
}
|
|
2654
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2655
|
-
await this.flushWriteBehindQueue();
|
|
2656
|
-
}
|
|
3323
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2657
3324
|
}
|
|
2658
3325
|
async flushWriteBehindQueue() {
|
|
2659
|
-
|
|
2660
|
-
|
|
3326
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3327
|
+
}
|
|
3328
|
+
async runWriteBehindBatch(batch) {
|
|
3329
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3330
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3331
|
+
if (failures.length === 0) {
|
|
2661
3332
|
return;
|
|
2662
3333
|
}
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
2671
|
-
failed: failures.length,
|
|
2672
|
-
total: batch.length,
|
|
2673
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2674
|
-
});
|
|
2675
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2676
|
-
}
|
|
2677
|
-
})();
|
|
2678
|
-
await this.writeBehindFlushPromise;
|
|
2679
|
-
this.writeBehindFlushPromise = void 0;
|
|
2680
|
-
if (this.writeBehindQueue.length > 0) {
|
|
2681
|
-
await this.flushWriteBehindQueue();
|
|
2682
|
-
}
|
|
3334
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3335
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3336
|
+
failed: failures.length,
|
|
3337
|
+
total: batch.length,
|
|
3338
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3339
|
+
});
|
|
3340
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2683
3341
|
}
|
|
2684
3342
|
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2685
3343
|
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
@@ -2709,32 +3367,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2709
3367
|
return [];
|
|
2710
3368
|
}
|
|
2711
3369
|
const [firstGroup, ...rest] = groups;
|
|
2712
|
-
if (!firstGroup) {
|
|
2713
|
-
return [];
|
|
2714
|
-
}
|
|
2715
3370
|
const restSets = rest.map((group) => new Set(group));
|
|
2716
3371
|
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2717
3372
|
}
|
|
2718
3373
|
qualifyKey(key) {
|
|
2719
|
-
|
|
2720
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3374
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2721
3375
|
}
|
|
2722
3376
|
qualifyPattern(pattern) {
|
|
2723
|
-
|
|
2724
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3377
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
2725
3378
|
}
|
|
2726
3379
|
stripQualifiedKey(key) {
|
|
2727
|
-
|
|
2728
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
2729
|
-
return key;
|
|
2730
|
-
}
|
|
2731
|
-
return key.slice(prefix.length);
|
|
2732
|
-
}
|
|
2733
|
-
generationPrefix() {
|
|
2734
|
-
if (this.currentGeneration === void 0) {
|
|
2735
|
-
return "";
|
|
2736
|
-
}
|
|
2737
|
-
return `v${this.currentGeneration}:`;
|
|
3380
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2738
3381
|
}
|
|
2739
3382
|
async deleteKeysFromLayers(layers, keys) {
|
|
2740
3383
|
await Promise.all(
|
|
@@ -2769,118 +3412,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2769
3412
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2770
3413
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2771
3414
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
3415
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3416
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3417
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3418
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3419
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3420
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3421
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3422
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3423
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3424
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3425
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3426
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3427
|
+
}
|
|
3428
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3429
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3430
|
+
}
|
|
3431
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3432
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3433
|
+
}
|
|
3434
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3435
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3436
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2785
3437
|
if (typeof this.options.generationCleanup === "object") {
|
|
2786
|
-
|
|
3438
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2787
3439
|
}
|
|
2788
3440
|
if (this.options.generation !== void 0) {
|
|
2789
|
-
|
|
3441
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2790
3442
|
}
|
|
2791
3443
|
}
|
|
2792
3444
|
validateWriteOptions(options) {
|
|
2793
3445
|
if (!options) {
|
|
2794
3446
|
return;
|
|
2795
3447
|
}
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
validateLayerNumberOption(name, value) {
|
|
2808
|
-
if (value === void 0) {
|
|
2809
|
-
return;
|
|
2810
|
-
}
|
|
2811
|
-
if (typeof value === "number") {
|
|
2812
|
-
this.validateNonNegativeNumber(name, value);
|
|
2813
|
-
return;
|
|
2814
|
-
}
|
|
2815
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2816
|
-
if (layerValue === void 0) {
|
|
2817
|
-
continue;
|
|
2818
|
-
}
|
|
2819
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2820
|
-
}
|
|
2821
|
-
}
|
|
2822
|
-
validatePositiveNumber(name, value) {
|
|
2823
|
-
if (value === void 0) {
|
|
2824
|
-
return;
|
|
2825
|
-
}
|
|
2826
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2827
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2828
|
-
}
|
|
2829
|
-
}
|
|
2830
|
-
validateRateLimitOptions(name, options) {
|
|
2831
|
-
if (!options) {
|
|
2832
|
-
return;
|
|
2833
|
-
}
|
|
2834
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2835
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2836
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2837
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2838
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2839
|
-
}
|
|
2840
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2841
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2842
|
-
}
|
|
2843
|
-
}
|
|
2844
|
-
validateNonNegativeNumber(name, value) {
|
|
2845
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2846
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
validateCacheKey(key) {
|
|
2850
|
-
if (key.length === 0) {
|
|
2851
|
-
throw new Error("Cache key must not be empty.");
|
|
2852
|
-
}
|
|
2853
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2854
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2855
|
-
}
|
|
2856
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2857
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2858
|
-
}
|
|
2859
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2860
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2861
|
-
}
|
|
2862
|
-
return key;
|
|
2863
|
-
}
|
|
2864
|
-
validatePattern(pattern) {
|
|
2865
|
-
if (pattern.length === 0) {
|
|
2866
|
-
throw new Error("Pattern must not be empty.");
|
|
2867
|
-
}
|
|
2868
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2869
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2870
|
-
}
|
|
2871
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2872
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
validateTtlPolicy(name, policy) {
|
|
2876
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2877
|
-
return;
|
|
2878
|
-
}
|
|
2879
|
-
if ("alignTo" in policy) {
|
|
2880
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2881
|
-
return;
|
|
2882
|
-
}
|
|
2883
|
-
throw new Error(`${name} is invalid.`);
|
|
3448
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3449
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3450
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3451
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3452
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3453
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3454
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3455
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3456
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3457
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3458
|
+
validateTags(options.tags);
|
|
2884
3459
|
}
|
|
2885
3460
|
assertActive(operation) {
|
|
2886
3461
|
if (this.isDisconnecting) {
|
|
@@ -2892,56 +3467,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2892
3467
|
await this.startup;
|
|
2893
3468
|
this.assertActive(operation);
|
|
2894
3469
|
}
|
|
2895
|
-
serializeOptions(options) {
|
|
2896
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2897
|
-
}
|
|
2898
|
-
validateAdaptiveTtlOptions(options) {
|
|
2899
|
-
if (!options || options === true) {
|
|
2900
|
-
return;
|
|
2901
|
-
}
|
|
2902
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2903
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2904
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2905
|
-
}
|
|
2906
|
-
validateCircuitBreakerOptions(options) {
|
|
2907
|
-
if (!options) {
|
|
2908
|
-
return;
|
|
2909
|
-
}
|
|
2910
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2911
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2912
|
-
}
|
|
2913
3470
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2914
|
-
const
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
3471
|
+
const plan = planFreshReadPolicies({
|
|
3472
|
+
stored: hit.stored,
|
|
3473
|
+
hasFetcher: Boolean(fetcher),
|
|
3474
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3475
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3476
|
+
});
|
|
3477
|
+
if (plan.refreshedStored) {
|
|
2919
3478
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
2920
3479
|
const layer = this.layers[index];
|
|
2921
3480
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2922
3481
|
continue;
|
|
2923
3482
|
}
|
|
2924
3483
|
try {
|
|
2925
|
-
await layer.set(key,
|
|
3484
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
2926
3485
|
} catch (error) {
|
|
2927
3486
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
2928
3487
|
}
|
|
2929
3488
|
}
|
|
2930
3489
|
}
|
|
2931
|
-
if (fetcher &&
|
|
3490
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
2932
3491
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
2933
3492
|
}
|
|
2934
3493
|
}
|
|
2935
3494
|
shouldSkipLayer(layer) {
|
|
2936
|
-
|
|
2937
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3495
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
2938
3496
|
}
|
|
2939
3497
|
async handleLayerFailure(layer, operation, error) {
|
|
2940
|
-
|
|
3498
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3499
|
+
if (!recovery.degrade) {
|
|
2941
3500
|
throw error;
|
|
2942
3501
|
}
|
|
2943
|
-
|
|
2944
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3502
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
2945
3503
|
this.metricsCollector.increment("degradedOperations");
|
|
2946
3504
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2947
3505
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -2977,18 +3535,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2977
3535
|
this.emit("error", { operation, ...context });
|
|
2978
3536
|
}
|
|
2979
3537
|
}
|
|
2980
|
-
serializeKeyPart(value) {
|
|
2981
|
-
if (typeof value === "string") {
|
|
2982
|
-
return `s:${value}`;
|
|
2983
|
-
}
|
|
2984
|
-
if (typeof value === "number") {
|
|
2985
|
-
return `n:${value}`;
|
|
2986
|
-
}
|
|
2987
|
-
if (typeof value === "boolean") {
|
|
2988
|
-
return `b:${value}`;
|
|
2989
|
-
}
|
|
2990
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2991
|
-
}
|
|
2992
3538
|
isCacheSnapshotEntries(value) {
|
|
2993
3539
|
return Array.isArray(value) && value.every((entry) => {
|
|
2994
3540
|
if (!entry || typeof entry !== "object") {
|
|
@@ -3001,54 +3547,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
3001
3547
|
sanitizeSnapshotValue(value) {
|
|
3002
3548
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
3003
3549
|
}
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3550
|
+
snapshotMaxBytes() {
|
|
3551
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3552
|
+
}
|
|
3553
|
+
snapshotMaxEntries() {
|
|
3554
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3555
|
+
}
|
|
3556
|
+
invalidationMaxKeys() {
|
|
3557
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3558
|
+
}
|
|
3559
|
+
async collectKeysForTag(tag) {
|
|
3560
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3561
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3562
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3563
|
+
keys.add(key);
|
|
3564
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3565
|
+
});
|
|
3566
|
+
return [...keys];
|
|
3010
3567
|
}
|
|
3011
|
-
const
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
if (baseDir !== false) {
|
|
3015
|
-
const relative = path.relative(baseDir, resolved);
|
|
3016
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
3017
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
3018
|
-
}
|
|
3568
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3569
|
+
keys.add(key);
|
|
3570
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3019
3571
|
}
|
|
3020
|
-
return
|
|
3572
|
+
return [...keys];
|
|
3021
3573
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3574
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3575
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3576
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3577
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
3025
3578
|
}
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3579
|
+
}
|
|
3580
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3581
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3582
|
+
for (const layer of this.layers) {
|
|
3583
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3584
|
+
continue;
|
|
3585
|
+
}
|
|
3586
|
+
const visitKey = async (key) => {
|
|
3587
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3588
|
+
if (exported.has(exportedKey)) {
|
|
3589
|
+
return;
|
|
3030
3590
|
}
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3591
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3592
|
+
if (stored === null) {
|
|
3593
|
+
return;
|
|
3594
|
+
}
|
|
3595
|
+
exported.add(exportedKey);
|
|
3596
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3597
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3598
|
+
}
|
|
3599
|
+
await visitor({
|
|
3600
|
+
key: exportedKey,
|
|
3601
|
+
value: stored,
|
|
3602
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3603
|
+
});
|
|
3604
|
+
};
|
|
3605
|
+
if (layer.forEachKey) {
|
|
3606
|
+
await layer.forEachKey(visitKey);
|
|
3607
|
+
continue;
|
|
3608
|
+
}
|
|
3609
|
+
const keys = await layer.keys?.();
|
|
3610
|
+
for (const key of keys ?? []) {
|
|
3611
|
+
await visitKey(key);
|
|
3612
|
+
}
|
|
3034
3613
|
}
|
|
3035
|
-
return value;
|
|
3036
3614
|
}
|
|
3037
3615
|
};
|
|
3038
|
-
function createInstanceId() {
|
|
3039
|
-
if (globalThis.crypto?.randomUUID) {
|
|
3040
|
-
return globalThis.crypto.randomUUID();
|
|
3041
|
-
}
|
|
3042
|
-
const bytes = new Uint8Array(16);
|
|
3043
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
3044
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
3045
|
-
} else {
|
|
3046
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
3047
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
3048
|
-
}
|
|
3049
|
-
}
|
|
3050
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
3051
|
-
}
|
|
3052
3616
|
|
|
3053
3617
|
// src/module.ts
|
|
3054
3618
|
var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
|