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
|
@@ -223,27 +223,147 @@ var Mutex = class {
|
|
|
223
223
|
}
|
|
224
224
|
};
|
|
225
225
|
|
|
226
|
+
// ../../src/internal/CacheNamespaceMetrics.ts
|
|
227
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
228
|
+
return {
|
|
229
|
+
hits: 0,
|
|
230
|
+
misses: 0,
|
|
231
|
+
fetches: 0,
|
|
232
|
+
sets: 0,
|
|
233
|
+
deletes: 0,
|
|
234
|
+
backfills: 0,
|
|
235
|
+
invalidations: 0,
|
|
236
|
+
staleHits: 0,
|
|
237
|
+
refreshes: 0,
|
|
238
|
+
refreshErrors: 0,
|
|
239
|
+
writeFailures: 0,
|
|
240
|
+
singleFlightWaits: 0,
|
|
241
|
+
negativeCacheHits: 0,
|
|
242
|
+
circuitBreakerTrips: 0,
|
|
243
|
+
degradedOperations: 0,
|
|
244
|
+
hitsByLayer: {},
|
|
245
|
+
missesByLayer: {},
|
|
246
|
+
latencyByLayer: {},
|
|
247
|
+
resetAt
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function cloneNamespaceMetrics(metrics) {
|
|
251
|
+
return {
|
|
252
|
+
...metrics,
|
|
253
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
254
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
255
|
+
latencyByLayer: Object.fromEntries(
|
|
256
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
257
|
+
)
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function diffNamespaceMetrics(before, after) {
|
|
261
|
+
const latencyByLayer = Object.fromEntries(
|
|
262
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
263
|
+
layer,
|
|
264
|
+
{
|
|
265
|
+
avgMs: value.avgMs,
|
|
266
|
+
maxMs: value.maxMs,
|
|
267
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
268
|
+
}
|
|
269
|
+
])
|
|
270
|
+
);
|
|
271
|
+
return {
|
|
272
|
+
hits: after.hits - before.hits,
|
|
273
|
+
misses: after.misses - before.misses,
|
|
274
|
+
fetches: after.fetches - before.fetches,
|
|
275
|
+
sets: after.sets - before.sets,
|
|
276
|
+
deletes: after.deletes - before.deletes,
|
|
277
|
+
backfills: after.backfills - before.backfills,
|
|
278
|
+
invalidations: after.invalidations - before.invalidations,
|
|
279
|
+
staleHits: after.staleHits - before.staleHits,
|
|
280
|
+
refreshes: after.refreshes - before.refreshes,
|
|
281
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
282
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
283
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
284
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
285
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
286
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
287
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
288
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
289
|
+
latencyByLayer,
|
|
290
|
+
resetAt: after.resetAt
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function addNamespaceMetrics(base, delta) {
|
|
294
|
+
return {
|
|
295
|
+
hits: base.hits + delta.hits,
|
|
296
|
+
misses: base.misses + delta.misses,
|
|
297
|
+
fetches: base.fetches + delta.fetches,
|
|
298
|
+
sets: base.sets + delta.sets,
|
|
299
|
+
deletes: base.deletes + delta.deletes,
|
|
300
|
+
backfills: base.backfills + delta.backfills,
|
|
301
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
302
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
303
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
304
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
305
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
306
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
307
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
308
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
309
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
310
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
311
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
312
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
313
|
+
resetAt: base.resetAt
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
function computeNamespaceHitRate(metrics) {
|
|
317
|
+
const total = metrics.hits + metrics.misses;
|
|
318
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
319
|
+
const byLayer = {};
|
|
320
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
321
|
+
for (const layer of layers) {
|
|
322
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
323
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
324
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
325
|
+
}
|
|
326
|
+
return { overall, byLayer };
|
|
327
|
+
}
|
|
328
|
+
function diffMetricMap(before, after) {
|
|
329
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
330
|
+
const result = {};
|
|
331
|
+
for (const key of keys) {
|
|
332
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
function addMetricMap(base, delta) {
|
|
337
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
338
|
+
const result = {};
|
|
339
|
+
for (const key of keys) {
|
|
340
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
341
|
+
}
|
|
342
|
+
return result;
|
|
343
|
+
}
|
|
344
|
+
|
|
226
345
|
// ../../src/CacheNamespace.ts
|
|
227
346
|
var CacheNamespace = class _CacheNamespace {
|
|
228
347
|
constructor(cache, prefix) {
|
|
229
348
|
this.cache = cache;
|
|
230
349
|
this.prefix = prefix;
|
|
350
|
+
validateNamespaceKey(prefix);
|
|
231
351
|
}
|
|
232
352
|
cache;
|
|
233
353
|
prefix;
|
|
234
354
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
235
|
-
metrics =
|
|
355
|
+
metrics = createEmptyNamespaceMetrics();
|
|
236
356
|
async get(key, fetcher, options) {
|
|
237
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
357
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
238
358
|
}
|
|
239
359
|
async getOrSet(key, fetcher, options) {
|
|
240
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
360
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
241
361
|
}
|
|
242
362
|
/**
|
|
243
363
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
244
364
|
*/
|
|
245
365
|
async getOrThrow(key, fetcher, options) {
|
|
246
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
366
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
247
367
|
}
|
|
248
368
|
async has(key) {
|
|
249
369
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -252,7 +372,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
252
372
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
253
373
|
}
|
|
254
374
|
async set(key, value, options) {
|
|
255
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
375
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
256
376
|
}
|
|
257
377
|
async delete(key) {
|
|
258
378
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -268,7 +388,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
268
388
|
() => this.cache.mget(
|
|
269
389
|
entries.map((entry) => ({
|
|
270
390
|
...entry,
|
|
271
|
-
key: this.qualify(entry.key)
|
|
391
|
+
key: this.qualify(entry.key),
|
|
392
|
+
options: this.qualifyGetOptions(entry.options)
|
|
272
393
|
}))
|
|
273
394
|
)
|
|
274
395
|
);
|
|
@@ -278,16 +399,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
278
399
|
() => this.cache.mset(
|
|
279
400
|
entries.map((entry) => ({
|
|
280
401
|
...entry,
|
|
281
|
-
key: this.qualify(entry.key)
|
|
402
|
+
key: this.qualify(entry.key),
|
|
403
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
282
404
|
}))
|
|
283
405
|
)
|
|
284
406
|
);
|
|
285
407
|
}
|
|
286
408
|
async invalidateByTag(tag) {
|
|
287
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
409
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
288
410
|
}
|
|
289
411
|
async invalidateByTags(tags, mode = "any") {
|
|
290
|
-
await this.trackMetrics(
|
|
412
|
+
await this.trackMetrics(
|
|
413
|
+
() => this.cache.invalidateByTags(
|
|
414
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
415
|
+
mode
|
|
416
|
+
)
|
|
417
|
+
);
|
|
291
418
|
}
|
|
292
419
|
async invalidateByPattern(pattern) {
|
|
293
420
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -299,34 +426,33 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
299
426
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
300
427
|
*/
|
|
301
428
|
async inspect(key) {
|
|
302
|
-
|
|
429
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
430
|
+
if (result === null) {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
...result,
|
|
435
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
436
|
+
};
|
|
303
437
|
}
|
|
304
438
|
wrap(keyPrefix, fetcher, options) {
|
|
305
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
439
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
306
440
|
}
|
|
307
441
|
warm(entries, options) {
|
|
308
442
|
return this.cache.warm(
|
|
309
443
|
entries.map((entry) => ({
|
|
310
444
|
...entry,
|
|
311
|
-
key: this.qualify(entry.key)
|
|
445
|
+
key: this.qualify(entry.key),
|
|
446
|
+
options: this.qualifyGetOptions(entry.options)
|
|
312
447
|
})),
|
|
313
448
|
options
|
|
314
449
|
);
|
|
315
450
|
}
|
|
316
451
|
getMetrics() {
|
|
317
|
-
return
|
|
452
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
318
453
|
}
|
|
319
454
|
getHitRate() {
|
|
320
|
-
|
|
321
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
322
|
-
const byLayer = {};
|
|
323
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
324
|
-
for (const layer of layers) {
|
|
325
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
326
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
327
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
328
|
-
}
|
|
329
|
-
return { overall, byLayer };
|
|
455
|
+
return computeNamespaceHitRate(this.metrics);
|
|
330
456
|
}
|
|
331
457
|
/**
|
|
332
458
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -344,12 +470,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
344
470
|
qualify(key) {
|
|
345
471
|
return `${this.prefix}:${key}`;
|
|
346
472
|
}
|
|
473
|
+
qualifyTag(tag) {
|
|
474
|
+
return `${this.prefix}:${tag}`;
|
|
475
|
+
}
|
|
476
|
+
qualifyGetOptions(options) {
|
|
477
|
+
return this.qualifyWriteOptions(options);
|
|
478
|
+
}
|
|
479
|
+
qualifyWrapOptions(options) {
|
|
480
|
+
return this.qualifyWriteOptions(options);
|
|
481
|
+
}
|
|
482
|
+
qualifyWriteOptions(options) {
|
|
483
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
484
|
+
return options;
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
...options,
|
|
488
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
489
|
+
};
|
|
490
|
+
}
|
|
347
491
|
async trackMetrics(operation) {
|
|
348
492
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
349
493
|
const before = this.cache.getMetrics();
|
|
350
494
|
const result = await operation();
|
|
351
495
|
const after = this.cache.getMetrics();
|
|
352
|
-
this.metrics =
|
|
496
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
353
497
|
return result;
|
|
354
498
|
});
|
|
355
499
|
}
|
|
@@ -363,223 +507,773 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
363
507
|
return mutex;
|
|
364
508
|
}
|
|
365
509
|
};
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
singleFlightWaits: 0,
|
|
380
|
-
negativeCacheHits: 0,
|
|
381
|
-
circuitBreakerTrips: 0,
|
|
382
|
-
degradedOperations: 0,
|
|
383
|
-
hitsByLayer: {},
|
|
384
|
-
missesByLayer: {},
|
|
385
|
-
latencyByLayer: {},
|
|
386
|
-
resetAt: Date.now()
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
function cloneMetrics(metrics) {
|
|
390
|
-
return {
|
|
391
|
-
...metrics,
|
|
392
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
393
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
394
|
-
latencyByLayer: Object.fromEntries(
|
|
395
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
396
|
-
)
|
|
397
|
-
};
|
|
510
|
+
function validateNamespaceKey(key) {
|
|
511
|
+
if (key.length === 0) {
|
|
512
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
513
|
+
}
|
|
514
|
+
if (key.length > 256) {
|
|
515
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
516
|
+
}
|
|
517
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
518
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
519
|
+
}
|
|
520
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
521
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
522
|
+
}
|
|
398
523
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
524
|
+
|
|
525
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
526
|
+
var PatternMatcher = class _PatternMatcher {
|
|
527
|
+
/**
|
|
528
|
+
* Tests whether a glob-style pattern matches a value.
|
|
529
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
530
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
531
|
+
* quadratic memory usage on long patterns/keys.
|
|
532
|
+
*/
|
|
533
|
+
static matches(pattern, value) {
|
|
534
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
538
|
+
*/
|
|
539
|
+
static matchLinear(pattern, value) {
|
|
540
|
+
let patternIndex = 0;
|
|
541
|
+
let valueIndex = 0;
|
|
542
|
+
let starIndex = -1;
|
|
543
|
+
let backtrackValueIndex = 0;
|
|
544
|
+
while (valueIndex < value.length) {
|
|
545
|
+
const patternChar = pattern[patternIndex];
|
|
546
|
+
const valueChar = value[valueIndex];
|
|
547
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
548
|
+
starIndex = patternIndex;
|
|
549
|
+
patternIndex += 1;
|
|
550
|
+
backtrackValueIndex = valueIndex;
|
|
551
|
+
continue;
|
|
407
552
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
426
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
427
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
428
|
-
latencyByLayer,
|
|
429
|
-
resetAt: after.resetAt
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
function addMetrics(base, delta) {
|
|
433
|
-
return {
|
|
434
|
-
hits: base.hits + delta.hits,
|
|
435
|
-
misses: base.misses + delta.misses,
|
|
436
|
-
fetches: base.fetches + delta.fetches,
|
|
437
|
-
sets: base.sets + delta.sets,
|
|
438
|
-
deletes: base.deletes + delta.deletes,
|
|
439
|
-
backfills: base.backfills + delta.backfills,
|
|
440
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
441
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
442
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
443
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
444
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
445
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
446
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
447
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
448
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
449
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
450
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
451
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
452
|
-
resetAt: base.resetAt
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
function diffMap(before, after) {
|
|
456
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
457
|
-
const result = {};
|
|
458
|
-
for (const key of keys) {
|
|
459
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
553
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
554
|
+
patternIndex += 1;
|
|
555
|
+
valueIndex += 1;
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
if (starIndex !== -1) {
|
|
559
|
+
patternIndex = starIndex + 1;
|
|
560
|
+
backtrackValueIndex += 1;
|
|
561
|
+
valueIndex = backtrackValueIndex;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
567
|
+
patternIndex += 1;
|
|
568
|
+
}
|
|
569
|
+
return patternIndex === pattern.length;
|
|
460
570
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// ../../src/internal/CacheKeyDiscovery.ts
|
|
574
|
+
var CacheKeyDiscovery = class {
|
|
575
|
+
constructor(options) {
|
|
576
|
+
this.options = options;
|
|
577
|
+
}
|
|
578
|
+
options;
|
|
579
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
580
|
+
const { tagIndex } = this.options;
|
|
581
|
+
const matches = /* @__PURE__ */ new Set();
|
|
582
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
583
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
584
|
+
matches.add(key);
|
|
585
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
586
|
+
});
|
|
587
|
+
} else {
|
|
588
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
589
|
+
for (const key of initialMatches) {
|
|
590
|
+
matches.add(key);
|
|
591
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
await Promise.all(
|
|
595
|
+
this.options.layers.map(async (layer) => {
|
|
596
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
if (layer.forEachKey) {
|
|
601
|
+
await layer.forEachKey(async (key) => {
|
|
602
|
+
if (key.startsWith(prefix)) {
|
|
603
|
+
matches.add(key);
|
|
604
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
const keys = await layer.keys?.();
|
|
610
|
+
for (const key of keys ?? []) {
|
|
611
|
+
if (key.startsWith(prefix)) {
|
|
612
|
+
matches.add(key);
|
|
613
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
} catch (error) {
|
|
617
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
618
|
+
}
|
|
619
|
+
})
|
|
620
|
+
);
|
|
621
|
+
return [...matches];
|
|
622
|
+
}
|
|
623
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
624
|
+
const matches = /* @__PURE__ */ new Set();
|
|
625
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
626
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
627
|
+
matches.add(key);
|
|
628
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
629
|
+
});
|
|
630
|
+
} else {
|
|
631
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
632
|
+
matches.add(key);
|
|
633
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
await Promise.all(
|
|
637
|
+
this.options.layers.map(async (layer) => {
|
|
638
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
if (layer.forEachKey) {
|
|
643
|
+
await layer.forEachKey(async (key) => {
|
|
644
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
645
|
+
matches.add(key);
|
|
646
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
const keys = await layer.keys?.();
|
|
652
|
+
for (const key of keys ?? []) {
|
|
653
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
654
|
+
matches.add(key);
|
|
655
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch (error) {
|
|
659
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
660
|
+
}
|
|
661
|
+
})
|
|
662
|
+
);
|
|
663
|
+
return [...matches];
|
|
664
|
+
}
|
|
665
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
666
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
667
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// ../../src/internal/CacheKeySerialization.ts
|
|
673
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
674
|
+
function normalizeForSerialization(value) {
|
|
675
|
+
if (Array.isArray(value)) {
|
|
676
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
677
|
+
}
|
|
678
|
+
if (value && typeof value === "object") {
|
|
679
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
680
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
681
|
+
return normalized;
|
|
682
|
+
}
|
|
683
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
684
|
+
return normalized;
|
|
685
|
+
}, {});
|
|
686
|
+
}
|
|
687
|
+
return value;
|
|
688
|
+
}
|
|
689
|
+
function serializeKeyPart(value) {
|
|
690
|
+
if (typeof value === "string") {
|
|
691
|
+
return `s:${value}`;
|
|
692
|
+
}
|
|
693
|
+
if (typeof value === "number") {
|
|
694
|
+
return `n:${value}`;
|
|
695
|
+
}
|
|
696
|
+
if (typeof value === "boolean") {
|
|
697
|
+
return `b:${value}`;
|
|
698
|
+
}
|
|
699
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
700
|
+
}
|
|
701
|
+
function serializeOptions(options) {
|
|
702
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
703
|
+
}
|
|
704
|
+
function createInstanceId() {
|
|
705
|
+
if (globalThis.crypto?.randomUUID) {
|
|
706
|
+
return globalThis.crypto.randomUUID();
|
|
707
|
+
}
|
|
708
|
+
const bytes = new Uint8Array(16);
|
|
709
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
710
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
711
|
+
} else {
|
|
712
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
713
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ../../src/internal/CacheSnapshotFile.ts
|
|
720
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
721
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
722
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
723
|
+
}
|
|
724
|
+
async function findExistingAncestor(directory, fs, path) {
|
|
725
|
+
let current = directory;
|
|
726
|
+
while (true) {
|
|
727
|
+
try {
|
|
728
|
+
await fs.lstat(current);
|
|
729
|
+
return current;
|
|
730
|
+
} catch (error) {
|
|
731
|
+
if (error.code !== "ENOENT") {
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const parent = path.dirname(current);
|
|
736
|
+
if (parent === current) {
|
|
737
|
+
return current;
|
|
738
|
+
}
|
|
739
|
+
current = parent;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
743
|
+
if (filePath.length === 0) {
|
|
744
|
+
throw new Error("filePath must not be empty.");
|
|
745
|
+
}
|
|
746
|
+
if (filePath.includes("\0")) {
|
|
747
|
+
throw new Error("filePath must not contain null bytes.");
|
|
748
|
+
}
|
|
749
|
+
const { promises: fs } = await import("fs");
|
|
750
|
+
const path = await import("path");
|
|
751
|
+
const resolved = path.resolve(filePath);
|
|
752
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
753
|
+
if (baseDir === false) {
|
|
754
|
+
return resolved;
|
|
755
|
+
}
|
|
756
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
757
|
+
const realBaseDir = await fs.realpath(baseDir);
|
|
758
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
759
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
760
|
+
}
|
|
761
|
+
if (mode === "read") {
|
|
762
|
+
const realTarget = await fs.realpath(resolved);
|
|
763
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
764
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
765
|
+
}
|
|
766
|
+
return realTarget;
|
|
767
|
+
}
|
|
768
|
+
const parentDir = path.dirname(resolved);
|
|
769
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs, path);
|
|
770
|
+
const realExistingAncestor = await fs.realpath(existingAncestor);
|
|
771
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
772
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
773
|
+
}
|
|
774
|
+
await fs.mkdir(parentDir, { recursive: true });
|
|
775
|
+
const realParentDir = await fs.realpath(parentDir);
|
|
776
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
777
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
778
|
+
}
|
|
779
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
780
|
+
try {
|
|
781
|
+
const existing = await fs.lstat(targetPath);
|
|
782
|
+
if (existing.isSymbolicLink()) {
|
|
783
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
784
|
+
}
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error.code !== "ENOENT") {
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return targetPath;
|
|
791
|
+
}
|
|
792
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
793
|
+
if (byteLimit === false) {
|
|
794
|
+
return handle.readFile({ encoding: "utf8" });
|
|
795
|
+
}
|
|
796
|
+
const chunks = [];
|
|
797
|
+
let totalBytes = 0;
|
|
798
|
+
let position = 0;
|
|
799
|
+
while (true) {
|
|
800
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
801
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
802
|
+
if (bytesRead === 0) {
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
totalBytes += bytesRead;
|
|
806
|
+
if (totalBytes > byteLimit) {
|
|
807
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
808
|
+
}
|
|
809
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
810
|
+
position += bytesRead;
|
|
811
|
+
}
|
|
812
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// ../../src/internal/CacheStackGeneration.ts
|
|
816
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
817
|
+
function generationPrefix(generation) {
|
|
818
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
819
|
+
}
|
|
820
|
+
function qualifyGenerationKey(key, generation) {
|
|
821
|
+
const prefix = generationPrefix(generation);
|
|
822
|
+
return prefix ? `${prefix}${key}` : key;
|
|
823
|
+
}
|
|
824
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
825
|
+
return qualifyGenerationKey(pattern, generation);
|
|
826
|
+
}
|
|
827
|
+
function stripGenerationPrefix(key, generation) {
|
|
828
|
+
const prefix = generationPrefix(generation);
|
|
829
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
830
|
+
return key;
|
|
831
|
+
}
|
|
832
|
+
return key.slice(prefix.length);
|
|
833
|
+
}
|
|
834
|
+
function resolveGenerationCleanupTarget({
|
|
835
|
+
previousGeneration,
|
|
836
|
+
nextGeneration,
|
|
837
|
+
generationCleanup
|
|
838
|
+
}) {
|
|
839
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
return previousGeneration;
|
|
843
|
+
}
|
|
844
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
845
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
846
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
847
|
+
}
|
|
848
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
849
|
+
}
|
|
850
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
851
|
+
if (keys.length === 0) {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
855
|
+
const batches = [];
|
|
856
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
857
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
858
|
+
}
|
|
859
|
+
return batches;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// ../../src/internal/CacheStackMaintenance.ts
|
|
863
|
+
var CacheStackMaintenance = class {
|
|
864
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
865
|
+
writeBehindQueue = [];
|
|
866
|
+
writeBehindTimer;
|
|
867
|
+
writeBehindFlushPromise;
|
|
868
|
+
generationCleanupPromise;
|
|
869
|
+
clearEpoch = 0;
|
|
870
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
871
|
+
if (writeStrategy !== "write-behind") {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
875
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
this.disposeWriteBehindTimer();
|
|
879
|
+
this.writeBehindTimer = setInterval(() => {
|
|
880
|
+
void flush();
|
|
881
|
+
}, flushIntervalMs);
|
|
882
|
+
this.writeBehindTimer.unref?.();
|
|
883
|
+
}
|
|
884
|
+
disposeWriteBehindTimer() {
|
|
885
|
+
if (!this.writeBehindTimer) {
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
clearInterval(this.writeBehindTimer);
|
|
889
|
+
this.writeBehindTimer = void 0;
|
|
890
|
+
}
|
|
891
|
+
beginClearEpoch() {
|
|
892
|
+
this.clearEpoch += 1;
|
|
893
|
+
this.keyEpochs.clear();
|
|
894
|
+
this.writeBehindQueue.length = 0;
|
|
895
|
+
}
|
|
896
|
+
currentClearEpoch() {
|
|
897
|
+
return this.clearEpoch;
|
|
898
|
+
}
|
|
899
|
+
currentKeyEpoch(key) {
|
|
900
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
901
|
+
}
|
|
902
|
+
bumpKeyEpochs(keys) {
|
|
903
|
+
for (const key of keys) {
|
|
904
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
908
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
return false;
|
|
915
|
+
}
|
|
916
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
917
|
+
this.writeBehindQueue.push(operation);
|
|
918
|
+
const batchSize = options?.batchSize ?? 100;
|
|
919
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
920
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
921
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
925
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
929
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
930
|
+
await this.writeBehindFlushPromise;
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const batchSize = options?.batchSize ?? 100;
|
|
934
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
935
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
936
|
+
try {
|
|
937
|
+
await this.writeBehindFlushPromise;
|
|
938
|
+
} finally {
|
|
939
|
+
this.writeBehindFlushPromise = void 0;
|
|
940
|
+
}
|
|
941
|
+
if (this.writeBehindQueue.length > 0) {
|
|
942
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
946
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
947
|
+
onError(generation, error);
|
|
948
|
+
});
|
|
949
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
950
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
951
|
+
this.generationCleanupPromise = void 0;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
async waitForGenerationCleanup() {
|
|
956
|
+
await this.generationCleanupPromise;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// ../../src/internal/StoredValue.ts
|
|
961
|
+
function isStoredValueEnvelope(value) {
|
|
962
|
+
if (typeof value !== "object" || value === null) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
const v = value;
|
|
966
|
+
if (v.__layercache !== 1) {
|
|
967
|
+
return false;
|
|
968
|
+
}
|
|
969
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
973
|
+
return false;
|
|
974
|
+
}
|
|
975
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
976
|
+
return false;
|
|
977
|
+
}
|
|
978
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
979
|
+
return false;
|
|
980
|
+
}
|
|
981
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
982
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
983
|
+
return false;
|
|
984
|
+
}
|
|
985
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
989
|
+
return false;
|
|
990
|
+
}
|
|
991
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
998
|
+
return false;
|
|
999
|
+
}
|
|
1000
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1001
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1005
|
+
return false;
|
|
1006
|
+
}
|
|
1007
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1008
|
+
return false;
|
|
1009
|
+
}
|
|
1010
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1011
|
+
return false;
|
|
1012
|
+
}
|
|
1013
|
+
return true;
|
|
1014
|
+
}
|
|
1015
|
+
function createStoredValueEnvelope(options) {
|
|
1016
|
+
const now = options.now ?? Date.now();
|
|
1017
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
1018
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
1019
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
1020
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
1021
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
1022
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1023
|
+
return {
|
|
1024
|
+
__layercache: 1,
|
|
1025
|
+
kind: options.kind,
|
|
1026
|
+
value: options.value,
|
|
1027
|
+
freshUntil,
|
|
1028
|
+
staleUntil,
|
|
1029
|
+
errorUntil,
|
|
1030
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1031
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1032
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
1036
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1037
|
+
return { state: "fresh", value: stored, stored };
|
|
1038
|
+
}
|
|
1039
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1040
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1041
|
+
}
|
|
1042
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1043
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1044
|
+
}
|
|
1045
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1046
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1047
|
+
}
|
|
1048
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
1049
|
+
}
|
|
1050
|
+
function unwrapStoredValue(stored) {
|
|
1051
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1052
|
+
return stored;
|
|
1053
|
+
}
|
|
1054
|
+
if (stored.kind === "empty") {
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
return stored.value ?? null;
|
|
1058
|
+
}
|
|
1059
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1060
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1061
|
+
return void 0;
|
|
1062
|
+
}
|
|
1063
|
+
const expiry = maxExpiry(stored);
|
|
1064
|
+
if (expiry === null) {
|
|
1065
|
+
return void 0;
|
|
1066
|
+
}
|
|
1067
|
+
const remainingMs = expiry - now;
|
|
1068
|
+
if (remainingMs <= 0) {
|
|
1069
|
+
return 1;
|
|
1070
|
+
}
|
|
1071
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1072
|
+
}
|
|
1073
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1074
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1075
|
+
return void 0;
|
|
1076
|
+
}
|
|
1077
|
+
const remainingMs = stored.freshUntil - now;
|
|
1078
|
+
if (remainingMs <= 0) {
|
|
1079
|
+
return 0;
|
|
1080
|
+
}
|
|
1081
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1082
|
+
}
|
|
1083
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1084
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
1085
|
+
return stored;
|
|
1086
|
+
}
|
|
1087
|
+
return createStoredValueEnvelope({
|
|
1088
|
+
kind: stored.kind,
|
|
1089
|
+
value: stored.value,
|
|
1090
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1091
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1092
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1093
|
+
now
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
function maxExpiry(stored) {
|
|
1097
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1098
|
+
(value) => value !== null
|
|
1099
|
+
);
|
|
1100
|
+
if (values.length === 0) {
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
return Math.max(...values);
|
|
1104
|
+
}
|
|
1105
|
+
function normalizePositiveSeconds(value) {
|
|
1106
|
+
if (!value || value <= 0) {
|
|
1107
|
+
return void 0;
|
|
1108
|
+
}
|
|
1109
|
+
return value;
|
|
1110
|
+
}
|
|
1111
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1112
|
+
if (value == null) {
|
|
1113
|
+
return true;
|
|
1114
|
+
}
|
|
1115
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// ../../src/internal/CacheStackRuntimePolicy.ts
|
|
1119
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
1120
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
1121
|
+
}
|
|
1122
|
+
function shouldStartBackgroundRefresh({
|
|
1123
|
+
isDisconnecting,
|
|
1124
|
+
hasRefreshInFlight
|
|
1125
|
+
}) {
|
|
1126
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
1127
|
+
}
|
|
1128
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
1129
|
+
if (!gracefulDegradation) {
|
|
1130
|
+
return { degrade: false };
|
|
1131
|
+
}
|
|
1132
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1133
|
+
return {
|
|
1134
|
+
degrade: true,
|
|
1135
|
+
degradedUntil: now + retryAfterMs
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function planFreshReadPolicies({
|
|
1139
|
+
stored,
|
|
1140
|
+
hasFetcher,
|
|
1141
|
+
slidingTtl,
|
|
1142
|
+
refreshAheadSeconds
|
|
1143
|
+
}) {
|
|
1144
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
1145
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
1146
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
1147
|
+
return {
|
|
1148
|
+
refreshedStored,
|
|
1149
|
+
refreshedStoredTtl,
|
|
1150
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ../../src/internal/CacheStackValidation.ts
|
|
1155
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
1156
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
1157
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
1158
|
+
function validatePositiveNumber(name, value) {
|
|
1159
|
+
if (value === void 0) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1163
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function validateNonNegativeNumber(name, value) {
|
|
1167
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1168
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
function validateLayerNumberOption(name, value) {
|
|
1172
|
+
if (value === void 0) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
if (typeof value === "number") {
|
|
1176
|
+
validateNonNegativeNumber(name, value);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1180
|
+
if (layerValue === void 0) {
|
|
1181
|
+
continue;
|
|
1182
|
+
}
|
|
1183
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
function validateRateLimitOptions(name, options) {
|
|
1187
|
+
if (!options) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1191
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1192
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1193
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1194
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1195
|
+
}
|
|
1196
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1197
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
468
1198
|
}
|
|
469
|
-
return result;
|
|
470
1199
|
}
|
|
471
|
-
function
|
|
1200
|
+
function validateCacheKey(key) {
|
|
472
1201
|
if (key.length === 0) {
|
|
473
|
-
throw new Error("
|
|
1202
|
+
throw new Error("Cache key must not be empty.");
|
|
474
1203
|
}
|
|
475
|
-
if (key.length >
|
|
476
|
-
throw new Error(
|
|
1204
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1205
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
477
1206
|
}
|
|
478
1207
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
479
|
-
throw new Error("
|
|
1208
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1209
|
+
}
|
|
1210
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
1211
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
480
1212
|
}
|
|
1213
|
+
return key;
|
|
481
1214
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Tests whether a glob-style pattern matches a value.
|
|
487
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
488
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
489
|
-
* quadratic memory usage on long patterns/keys.
|
|
490
|
-
*/
|
|
491
|
-
static matches(pattern, value) {
|
|
492
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1215
|
+
function validateTag(tag) {
|
|
1216
|
+
if (tag.length === 0) {
|
|
1217
|
+
throw new Error("Cache tag must not be empty.");
|
|
493
1218
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
*/
|
|
497
|
-
static matchLinear(pattern, value) {
|
|
498
|
-
let patternIndex = 0;
|
|
499
|
-
let valueIndex = 0;
|
|
500
|
-
let starIndex = -1;
|
|
501
|
-
let backtrackValueIndex = 0;
|
|
502
|
-
while (valueIndex < value.length) {
|
|
503
|
-
const patternChar = pattern[patternIndex];
|
|
504
|
-
const valueChar = value[valueIndex];
|
|
505
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
506
|
-
starIndex = patternIndex;
|
|
507
|
-
patternIndex += 1;
|
|
508
|
-
backtrackValueIndex = valueIndex;
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
512
|
-
patternIndex += 1;
|
|
513
|
-
valueIndex += 1;
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
if (starIndex !== -1) {
|
|
517
|
-
patternIndex = starIndex + 1;
|
|
518
|
-
backtrackValueIndex += 1;
|
|
519
|
-
valueIndex = backtrackValueIndex;
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
525
|
-
patternIndex += 1;
|
|
526
|
-
}
|
|
527
|
-
return patternIndex === pattern.length;
|
|
1219
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
1220
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
528
1221
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
// ../../src/internal/CacheKeyDiscovery.ts
|
|
532
|
-
var CacheKeyDiscovery = class {
|
|
533
|
-
constructor(options) {
|
|
534
|
-
this.options = options;
|
|
1222
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
1223
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
535
1224
|
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const { tagIndex } = this.options;
|
|
539
|
-
const matches = new Set(
|
|
540
|
-
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
541
|
-
);
|
|
542
|
-
await Promise.all(
|
|
543
|
-
this.options.layers.map(async (layer) => {
|
|
544
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
try {
|
|
548
|
-
const keys = await layer.keys();
|
|
549
|
-
for (const key of keys) {
|
|
550
|
-
if (key.startsWith(prefix)) {
|
|
551
|
-
matches.add(key);
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
} catch (error) {
|
|
555
|
-
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
556
|
-
}
|
|
557
|
-
})
|
|
558
|
-
);
|
|
559
|
-
return [...matches];
|
|
1225
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
1226
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
560
1227
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
return;
|
|
567
|
-
}
|
|
568
|
-
try {
|
|
569
|
-
const keys = await layer.keys();
|
|
570
|
-
for (const key of keys) {
|
|
571
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
572
|
-
matches.add(key);
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
} catch (error) {
|
|
576
|
-
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
577
|
-
}
|
|
578
|
-
})
|
|
579
|
-
);
|
|
580
|
-
return [...matches];
|
|
1228
|
+
return tag;
|
|
1229
|
+
}
|
|
1230
|
+
function validateTags(tags) {
|
|
1231
|
+
if (!tags) {
|
|
1232
|
+
return;
|
|
581
1233
|
}
|
|
582
|
-
|
|
1234
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
1235
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
1236
|
+
}
|
|
1237
|
+
for (const tag of tags) {
|
|
1238
|
+
validateTag(tag);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function validatePattern(pattern) {
|
|
1242
|
+
if (pattern.length === 0) {
|
|
1243
|
+
throw new Error("Pattern must not be empty.");
|
|
1244
|
+
}
|
|
1245
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1246
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
1247
|
+
}
|
|
1248
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
1249
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function validateTtlPolicy(name, policy) {
|
|
1253
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
if ("alignTo" in policy) {
|
|
1257
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
throw new Error(`${name} is invalid.`);
|
|
1261
|
+
}
|
|
1262
|
+
function validateAdaptiveTtlOptions(options) {
|
|
1263
|
+
if (!options || options === true) {
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1267
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1268
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1269
|
+
}
|
|
1270
|
+
function validateCircuitBreakerOptions(options) {
|
|
1271
|
+
if (!options) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1275
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1276
|
+
}
|
|
583
1277
|
|
|
584
1278
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
585
1279
|
var CircuitBreakerManager = class {
|
|
@@ -610,7 +1304,6 @@ var CircuitBreakerManager = class {
|
|
|
610
1304
|
if (!options) {
|
|
611
1305
|
return;
|
|
612
1306
|
}
|
|
613
|
-
this.pruneIfNeeded();
|
|
614
1307
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
615
1308
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
616
1309
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -619,6 +1312,7 @@ var CircuitBreakerManager = class {
|
|
|
619
1312
|
state.openUntil = Date.now() + cooldownMs;
|
|
620
1313
|
}
|
|
621
1314
|
this.breakers.set(key, state);
|
|
1315
|
+
this.pruneIfNeeded();
|
|
622
1316
|
}
|
|
623
1317
|
recordSuccess(key) {
|
|
624
1318
|
this.breakers.delete(key);
|
|
@@ -933,158 +1627,34 @@ var MetricsCollector = class {
|
|
|
933
1627
|
for (const layer of allLayers) {
|
|
934
1628
|
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
935
1629
|
const m = this.data.missesByLayer[layer] ?? 0;
|
|
936
|
-
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
937
|
-
}
|
|
938
|
-
return { overall, byLayer };
|
|
939
|
-
}
|
|
940
|
-
empty() {
|
|
941
|
-
return {
|
|
942
|
-
hits: 0,
|
|
943
|
-
misses: 0,
|
|
944
|
-
fetches: 0,
|
|
945
|
-
sets: 0,
|
|
946
|
-
deletes: 0,
|
|
947
|
-
backfills: 0,
|
|
948
|
-
invalidations: 0,
|
|
949
|
-
staleHits: 0,
|
|
950
|
-
refreshes: 0,
|
|
951
|
-
refreshErrors: 0,
|
|
952
|
-
writeFailures: 0,
|
|
953
|
-
singleFlightWaits: 0,
|
|
954
|
-
negativeCacheHits: 0,
|
|
955
|
-
circuitBreakerTrips: 0,
|
|
956
|
-
degradedOperations: 0,
|
|
957
|
-
hitsByLayer: {},
|
|
958
|
-
missesByLayer: {},
|
|
959
|
-
latencyByLayer: {},
|
|
960
|
-
resetAt: Date.now()
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
};
|
|
964
|
-
|
|
965
|
-
// ../../src/internal/StoredValue.ts
|
|
966
|
-
function isStoredValueEnvelope(value) {
|
|
967
|
-
if (typeof value !== "object" || value === null) {
|
|
968
|
-
return false;
|
|
969
|
-
}
|
|
970
|
-
const v = value;
|
|
971
|
-
if (v.__layercache !== 1) {
|
|
972
|
-
return false;
|
|
973
|
-
}
|
|
974
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
975
|
-
return false;
|
|
976
|
-
}
|
|
977
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
981
|
-
return false;
|
|
982
|
-
}
|
|
983
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
987
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
988
|
-
return false;
|
|
989
|
-
}
|
|
990
|
-
return true;
|
|
991
|
-
}
|
|
992
|
-
function createStoredValueEnvelope(options) {
|
|
993
|
-
const now = options.now ?? Date.now();
|
|
994
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
995
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
996
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
997
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
998
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
999
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
1000
|
-
return {
|
|
1001
|
-
__layercache: 1,
|
|
1002
|
-
kind: options.kind,
|
|
1003
|
-
value: options.value,
|
|
1004
|
-
freshUntil,
|
|
1005
|
-
staleUntil,
|
|
1006
|
-
errorUntil,
|
|
1007
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
1008
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
1009
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
1010
|
-
};
|
|
1011
|
-
}
|
|
1012
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
1013
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1014
|
-
return { state: "fresh", value: stored, stored };
|
|
1015
|
-
}
|
|
1016
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
1017
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1018
|
-
}
|
|
1019
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
1020
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1021
|
-
}
|
|
1022
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
1023
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
1024
|
-
}
|
|
1025
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
1026
|
-
}
|
|
1027
|
-
function unwrapStoredValue(stored) {
|
|
1028
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1029
|
-
return stored;
|
|
1030
|
-
}
|
|
1031
|
-
if (stored.kind === "empty") {
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
return stored.value ?? null;
|
|
1035
|
-
}
|
|
1036
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
1037
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1038
|
-
return void 0;
|
|
1039
|
-
}
|
|
1040
|
-
const expiry = maxExpiry(stored);
|
|
1041
|
-
if (expiry === null) {
|
|
1042
|
-
return void 0;
|
|
1043
|
-
}
|
|
1044
|
-
const remainingMs = expiry - now;
|
|
1045
|
-
if (remainingMs <= 0) {
|
|
1046
|
-
return 1;
|
|
1047
|
-
}
|
|
1048
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1049
|
-
}
|
|
1050
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
1051
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
1052
|
-
return void 0;
|
|
1053
|
-
}
|
|
1054
|
-
const remainingMs = stored.freshUntil - now;
|
|
1055
|
-
if (remainingMs <= 0) {
|
|
1056
|
-
return 0;
|
|
1057
|
-
}
|
|
1058
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
1059
|
-
}
|
|
1060
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
1061
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
1062
|
-
return stored;
|
|
1063
|
-
}
|
|
1064
|
-
return createStoredValueEnvelope({
|
|
1065
|
-
kind: stored.kind,
|
|
1066
|
-
value: stored.value,
|
|
1067
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
1068
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
1069
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
1070
|
-
now
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
function maxExpiry(stored) {
|
|
1074
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
1075
|
-
(value) => value !== null
|
|
1076
|
-
);
|
|
1077
|
-
if (values.length === 0) {
|
|
1078
|
-
return null;
|
|
1079
|
-
}
|
|
1080
|
-
return Math.max(...values);
|
|
1081
|
-
}
|
|
1082
|
-
function normalizePositiveSeconds(value) {
|
|
1083
|
-
if (!value || value <= 0) {
|
|
1084
|
-
return void 0;
|
|
1630
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
1631
|
+
}
|
|
1632
|
+
return { overall, byLayer };
|
|
1085
1633
|
}
|
|
1086
|
-
|
|
1087
|
-
|
|
1634
|
+
empty() {
|
|
1635
|
+
return {
|
|
1636
|
+
hits: 0,
|
|
1637
|
+
misses: 0,
|
|
1638
|
+
fetches: 0,
|
|
1639
|
+
sets: 0,
|
|
1640
|
+
deletes: 0,
|
|
1641
|
+
backfills: 0,
|
|
1642
|
+
invalidations: 0,
|
|
1643
|
+
staleHits: 0,
|
|
1644
|
+
refreshes: 0,
|
|
1645
|
+
refreshErrors: 0,
|
|
1646
|
+
writeFailures: 0,
|
|
1647
|
+
singleFlightWaits: 0,
|
|
1648
|
+
negativeCacheHits: 0,
|
|
1649
|
+
circuitBreakerTrips: 0,
|
|
1650
|
+
degradedOperations: 0,
|
|
1651
|
+
hitsByLayer: {},
|
|
1652
|
+
missesByLayer: {},
|
|
1653
|
+
latencyByLayer: {},
|
|
1654
|
+
resetAt: Date.now()
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
};
|
|
1088
1658
|
|
|
1089
1659
|
// ../../src/internal/TtlResolver.ts
|
|
1090
1660
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1239,6 +1809,11 @@ var TagIndex = class {
|
|
|
1239
1809
|
async keysForTag(tag) {
|
|
1240
1810
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1241
1811
|
}
|
|
1812
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1813
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1814
|
+
await visitor(key);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1242
1817
|
async keysForPrefix(prefix) {
|
|
1243
1818
|
const node = this.findNode(prefix);
|
|
1244
1819
|
if (!node) {
|
|
@@ -1248,6 +1823,13 @@ var TagIndex = class {
|
|
|
1248
1823
|
this.collectFromNode(node, prefix, matches);
|
|
1249
1824
|
return matches;
|
|
1250
1825
|
}
|
|
1826
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1827
|
+
const node = this.findNode(prefix);
|
|
1828
|
+
if (!node) {
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1832
|
+
}
|
|
1251
1833
|
async tagsForKey(key) {
|
|
1252
1834
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1253
1835
|
}
|
|
@@ -1256,6 +1838,12 @@ var TagIndex = class {
|
|
|
1256
1838
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1257
1839
|
return [...matches];
|
|
1258
1840
|
}
|
|
1841
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1842
|
+
const matches = await this.matchPattern(pattern);
|
|
1843
|
+
for (const key of matches) {
|
|
1844
|
+
await visitor(key);
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1259
1847
|
async clear() {
|
|
1260
1848
|
this.tagToKeys.clear();
|
|
1261
1849
|
this.keyToTags.clear();
|
|
@@ -1305,6 +1893,14 @@ var TagIndex = class {
|
|
|
1305
1893
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1306
1894
|
}
|
|
1307
1895
|
}
|
|
1896
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1897
|
+
if (node.terminal) {
|
|
1898
|
+
await visitor(prefix);
|
|
1899
|
+
}
|
|
1900
|
+
for (const [character, child] of node.children) {
|
|
1901
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1308
1904
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1309
1905
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1310
1906
|
return;
|
|
@@ -1422,22 +2018,27 @@ var TagIndex = class {
|
|
|
1422
2018
|
|
|
1423
2019
|
// ../../src/serialization/JsonSerializer.ts
|
|
1424
2020
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2021
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1425
2022
|
var JsonSerializer = class {
|
|
1426
2023
|
serialize(value) {
|
|
1427
2024
|
return JSON.stringify(value);
|
|
1428
2025
|
}
|
|
1429
2026
|
deserialize(payload) {
|
|
1430
2027
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1431
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
2028
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1432
2029
|
}
|
|
1433
2030
|
};
|
|
1434
2031
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1435
|
-
function sanitizeJsonValue(value, depth) {
|
|
2032
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
2033
|
+
state.count += 1;
|
|
2034
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
2035
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
2036
|
+
}
|
|
1436
2037
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1437
|
-
|
|
2038
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1438
2039
|
}
|
|
1439
2040
|
if (Array.isArray(value)) {
|
|
1440
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
2041
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1441
2042
|
}
|
|
1442
2043
|
if (!isPlainObject(value)) {
|
|
1443
2044
|
return value;
|
|
@@ -1447,7 +2048,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1447
2048
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1448
2049
|
continue;
|
|
1449
2050
|
}
|
|
1450
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
2051
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1451
2052
|
}
|
|
1452
2053
|
return sanitized;
|
|
1453
2054
|
}
|
|
@@ -1496,10 +2097,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1496
2097
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1497
2098
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1498
2099
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1499
|
-
var
|
|
1500
|
-
var
|
|
2100
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
2101
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
2102
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
2103
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1501
2104
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1502
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1503
2105
|
var DebugLogger = class {
|
|
1504
2106
|
enabled;
|
|
1505
2107
|
constructor(enabled) {
|
|
@@ -1586,13 +2188,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1586
2188
|
snapshotSerializer = new JsonSerializer();
|
|
1587
2189
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1588
2190
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2191
|
+
maintenance = new CacheStackMaintenance();
|
|
1589
2192
|
ttlResolver;
|
|
1590
2193
|
circuitBreakerManager;
|
|
1591
2194
|
currentGeneration;
|
|
1592
|
-
writeBehindQueue = [];
|
|
1593
|
-
writeBehindTimer;
|
|
1594
|
-
writeBehindFlushPromise;
|
|
1595
|
-
generationCleanupPromise;
|
|
1596
2195
|
isDisconnecting = false;
|
|
1597
2196
|
disconnectPromise;
|
|
1598
2197
|
/**
|
|
@@ -1602,7 +2201,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1602
2201
|
* and no `fetcher` is provided.
|
|
1603
2202
|
*/
|
|
1604
2203
|
async get(key, fetcher, options) {
|
|
1605
|
-
const normalizedKey = this.qualifyKey(
|
|
2204
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1606
2205
|
this.validateWriteOptions(options);
|
|
1607
2206
|
await this.awaitStartup("get");
|
|
1608
2207
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1672,7 +2271,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1672
2271
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1673
2272
|
*/
|
|
1674
2273
|
async has(key) {
|
|
1675
|
-
const normalizedKey = this.qualifyKey(
|
|
2274
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1676
2275
|
await this.awaitStartup("has");
|
|
1677
2276
|
for (const layer of this.layers) {
|
|
1678
2277
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1705,7 +2304,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1705
2304
|
* that has it, or null if the key is not found / has no TTL.
|
|
1706
2305
|
*/
|
|
1707
2306
|
async ttl(key) {
|
|
1708
|
-
const normalizedKey = this.qualifyKey(
|
|
2307
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1709
2308
|
await this.awaitStartup("ttl");
|
|
1710
2309
|
for (const layer of this.layers) {
|
|
1711
2310
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1727,7 +2326,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1727
2326
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1728
2327
|
*/
|
|
1729
2328
|
async set(key, value, options) {
|
|
1730
|
-
const normalizedKey = this.qualifyKey(
|
|
2329
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1731
2330
|
this.validateWriteOptions(options);
|
|
1732
2331
|
await this.awaitStartup("set");
|
|
1733
2332
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1736,7 +2335,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1736
2335
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1737
2336
|
*/
|
|
1738
2337
|
async delete(key) {
|
|
1739
|
-
const normalizedKey = this.qualifyKey(
|
|
2338
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1740
2339
|
await this.awaitStartup("delete");
|
|
1741
2340
|
await this.deleteKeys([normalizedKey]);
|
|
1742
2341
|
await this.publishInvalidation({
|
|
@@ -1748,6 +2347,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1748
2347
|
}
|
|
1749
2348
|
async clear() {
|
|
1750
2349
|
await this.awaitStartup("clear");
|
|
2350
|
+
this.maintenance.beginClearEpoch();
|
|
1751
2351
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1752
2352
|
await this.tagIndex.clear();
|
|
1753
2353
|
this.ttlResolver.clearProfiles();
|
|
@@ -1764,7 +2364,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1764
2364
|
return;
|
|
1765
2365
|
}
|
|
1766
2366
|
await this.awaitStartup("mdelete");
|
|
1767
|
-
const normalizedKeys = keys.map((k) =>
|
|
2367
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1768
2368
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1769
2369
|
await this.deleteKeys(cacheKeys);
|
|
1770
2370
|
await this.publishInvalidation({
|
|
@@ -1781,7 +2381,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1781
2381
|
}
|
|
1782
2382
|
const normalizedEntries = entries.map((entry) => ({
|
|
1783
2383
|
...entry,
|
|
1784
|
-
key: this.qualifyKey(
|
|
2384
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1785
2385
|
}));
|
|
1786
2386
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1787
2387
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1790,7 +2390,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1790
2390
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1791
2391
|
return Promise.all(
|
|
1792
2392
|
normalizedEntries.map((entry) => {
|
|
1793
|
-
const optionsSignature =
|
|
2393
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1794
2394
|
const existing = pendingReads.get(entry.key);
|
|
1795
2395
|
if (!existing) {
|
|
1796
2396
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1859,7 +2459,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1859
2459
|
this.assertActive("mset");
|
|
1860
2460
|
const normalizedEntries = entries.map((entry) => ({
|
|
1861
2461
|
...entry,
|
|
1862
|
-
key: this.qualifyKey(
|
|
2462
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1863
2463
|
}));
|
|
1864
2464
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1865
2465
|
await this.awaitStartup("mset");
|
|
@@ -1902,7 +2502,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1902
2502
|
*/
|
|
1903
2503
|
wrap(prefix, fetcher, options = {}) {
|
|
1904
2504
|
return (...args) => {
|
|
1905
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2505
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1906
2506
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1907
2507
|
return this.get(key, () => fetcher(...args), options);
|
|
1908
2508
|
};
|
|
@@ -1912,11 +2512,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1912
2512
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1913
2513
|
*/
|
|
1914
2514
|
namespace(prefix) {
|
|
2515
|
+
validateNamespaceKey(prefix);
|
|
1915
2516
|
return new CacheNamespace(this, prefix);
|
|
1916
2517
|
}
|
|
1917
2518
|
async invalidateByTag(tag) {
|
|
2519
|
+
validateTag(tag);
|
|
1918
2520
|
await this.awaitStartup("invalidateByTag");
|
|
1919
|
-
const keys = await this.
|
|
2521
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1920
2522
|
await this.deleteKeys(keys);
|
|
1921
2523
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1922
2524
|
}
|
|
@@ -1924,23 +2526,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1924
2526
|
if (tags.length === 0) {
|
|
1925
2527
|
return;
|
|
1926
2528
|
}
|
|
2529
|
+
validateTags(tags);
|
|
1927
2530
|
await this.awaitStartup("invalidateByTags");
|
|
1928
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2531
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1929
2532
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2533
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1930
2534
|
await this.deleteKeys(keys);
|
|
1931
2535
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1932
2536
|
}
|
|
1933
2537
|
async invalidateByPattern(pattern) {
|
|
1934
|
-
|
|
2538
|
+
validatePattern(pattern);
|
|
1935
2539
|
await this.awaitStartup("invalidateByPattern");
|
|
1936
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2540
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2541
|
+
this.qualifyPattern(pattern),
|
|
2542
|
+
this.invalidationMaxKeys()
|
|
2543
|
+
);
|
|
1937
2544
|
await this.deleteKeys(keys);
|
|
1938
2545
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1939
2546
|
}
|
|
1940
2547
|
async invalidateByPrefix(prefix) {
|
|
1941
2548
|
await this.awaitStartup("invalidateByPrefix");
|
|
1942
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1943
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2549
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2550
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1944
2551
|
await this.deleteKeys(keys);
|
|
1945
2552
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1946
2553
|
}
|
|
@@ -1998,9 +2605,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1998
2605
|
bumpGeneration(nextGeneration) {
|
|
1999
2606
|
const current = this.currentGeneration ?? 0;
|
|
2000
2607
|
const previousGeneration = this.currentGeneration;
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2608
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
2609
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
2610
|
+
previousGeneration,
|
|
2611
|
+
nextGeneration: updatedGeneration,
|
|
2612
|
+
generationCleanup: this.options.generationCleanup
|
|
2613
|
+
});
|
|
2614
|
+
this.currentGeneration = updatedGeneration;
|
|
2615
|
+
if (generationToCleanup !== null) {
|
|
2616
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
2004
2617
|
}
|
|
2005
2618
|
return this.currentGeneration;
|
|
2006
2619
|
}
|
|
@@ -2010,7 +2623,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2010
2623
|
* Returns `null` if the key does not exist in any layer.
|
|
2011
2624
|
*/
|
|
2012
2625
|
async inspect(key) {
|
|
2013
|
-
const userKey =
|
|
2626
|
+
const userKey = validateCacheKey(key);
|
|
2014
2627
|
const normalizedKey = this.qualifyKey(userKey);
|
|
2015
2628
|
await this.awaitStartup("inspect");
|
|
2016
2629
|
const foundInLayers = [];
|
|
@@ -2047,50 +2660,79 @@ var CacheStack = class extends EventEmitter {
|
|
|
2047
2660
|
}
|
|
2048
2661
|
async exportState() {
|
|
2049
2662
|
await this.awaitStartup("exportState");
|
|
2050
|
-
const
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
const keys = await layer.keys();
|
|
2056
|
-
for (const key of keys) {
|
|
2057
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
2058
|
-
if (exported.has(exportedKey)) {
|
|
2059
|
-
continue;
|
|
2060
|
-
}
|
|
2061
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
2062
|
-
if (stored === null) {
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
exported.set(exportedKey, {
|
|
2066
|
-
key: exportedKey,
|
|
2067
|
-
value: stored,
|
|
2068
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
2069
|
-
});
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
return [...exported.values()];
|
|
2663
|
+
const entries = [];
|
|
2664
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2665
|
+
entries.push(entry);
|
|
2666
|
+
});
|
|
2667
|
+
return entries;
|
|
2073
2668
|
}
|
|
2074
2669
|
async importState(entries) {
|
|
2075
2670
|
await this.awaitStartup("importState");
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2671
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2672
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2673
|
+
value: entry.value,
|
|
2674
|
+
ttl: entry.ttl
|
|
2675
|
+
}));
|
|
2676
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2677
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2678
|
+
await Promise.all(
|
|
2679
|
+
batch.map(async (entry) => {
|
|
2680
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2681
|
+
await this.tagIndex.touch(entry.key);
|
|
2682
|
+
})
|
|
2683
|
+
);
|
|
2684
|
+
}
|
|
2083
2685
|
}
|
|
2084
2686
|
async persistToFile(filePath) {
|
|
2085
2687
|
this.assertActive("persistToFile");
|
|
2086
|
-
const snapshot = await this.exportState();
|
|
2087
2688
|
const { promises: fs } = await import("fs");
|
|
2088
|
-
|
|
2689
|
+
const path = await import("path");
|
|
2690
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2691
|
+
const tempPath = path.join(
|
|
2692
|
+
path.dirname(targetPath),
|
|
2693
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2694
|
+
);
|
|
2695
|
+
let handle;
|
|
2696
|
+
try {
|
|
2697
|
+
handle = await fs.open(tempPath, "wx");
|
|
2698
|
+
const openedHandle = handle;
|
|
2699
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2700
|
+
let wroteAny = false;
|
|
2701
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2702
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2703
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2704
|
+
wroteAny = true;
|
|
2705
|
+
});
|
|
2706
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2707
|
+
await openedHandle.close();
|
|
2708
|
+
handle = void 0;
|
|
2709
|
+
await fs.rename(tempPath, targetPath);
|
|
2710
|
+
} catch (error) {
|
|
2711
|
+
await handle?.close().catch(() => void 0);
|
|
2712
|
+
await fs.unlink(tempPath).catch(() => void 0);
|
|
2713
|
+
throw error;
|
|
2714
|
+
}
|
|
2089
2715
|
}
|
|
2090
2716
|
async restoreFromFile(filePath) {
|
|
2091
2717
|
this.assertActive("restoreFromFile");
|
|
2092
|
-
const { promises: fs } = await import("fs");
|
|
2093
|
-
const
|
|
2718
|
+
const { promises: fs, constants } = await import("fs");
|
|
2719
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2720
|
+
const handle = await fs.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2721
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2722
|
+
let raw;
|
|
2723
|
+
try {
|
|
2724
|
+
if (snapshotMaxBytes !== false) {
|
|
2725
|
+
const stat = await handle.stat();
|
|
2726
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2727
|
+
throw new Error(
|
|
2728
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2729
|
+
);
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2733
|
+
} finally {
|
|
2734
|
+
await handle.close();
|
|
2735
|
+
}
|
|
2094
2736
|
let parsed;
|
|
2095
2737
|
try {
|
|
2096
2738
|
parsed = JSON.parse(raw);
|
|
@@ -2115,12 +2757,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2115
2757
|
await this.startup;
|
|
2116
2758
|
await this.unsubscribeInvalidation?.();
|
|
2117
2759
|
await this.flushWriteBehindQueue();
|
|
2118
|
-
await this.
|
|
2760
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
2119
2761
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
2120
|
-
|
|
2121
|
-
clearInterval(this.writeBehindTimer);
|
|
2122
|
-
this.writeBehindTimer = void 0;
|
|
2123
|
-
}
|
|
2762
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
2124
2763
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
2125
2764
|
})();
|
|
2126
2765
|
}
|
|
@@ -2134,14 +2773,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
2134
2773
|
await this.handleInvalidationMessage(message);
|
|
2135
2774
|
});
|
|
2136
2775
|
}
|
|
2137
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2776
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2138
2777
|
const fetchTask = async () => {
|
|
2139
2778
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
2140
2779
|
if (secondHit.found) {
|
|
2141
2780
|
this.metricsCollector.increment("hits");
|
|
2142
2781
|
return secondHit.value;
|
|
2143
2782
|
}
|
|
2144
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2783
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2145
2784
|
};
|
|
2146
2785
|
const singleFlightTask = async () => {
|
|
2147
2786
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -2151,7 +2790,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2151
2790
|
key,
|
|
2152
2791
|
this.resolveSingleFlightOptions(),
|
|
2153
2792
|
fetchTask,
|
|
2154
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2793
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
2155
2794
|
);
|
|
2156
2795
|
};
|
|
2157
2796
|
if (this.options.stampedePrevention === false) {
|
|
@@ -2159,7 +2798,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2159
2798
|
}
|
|
2160
2799
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2161
2800
|
}
|
|
2162
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2801
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2163
2802
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2164
2803
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2165
2804
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2173,9 +2812,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
2173
2812
|
}
|
|
2174
2813
|
await this.sleep(pollIntervalMs);
|
|
2175
2814
|
}
|
|
2176
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2815
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2177
2816
|
}
|
|
2178
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2817
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2179
2818
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2180
2819
|
this.metricsCollector.increment("fetches");
|
|
2181
2820
|
const fetchStart = Date.now();
|
|
@@ -2196,6 +2835,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2196
2835
|
if (!this.shouldNegativeCache(options)) {
|
|
2197
2836
|
return null;
|
|
2198
2837
|
}
|
|
2838
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2839
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2840
|
+
key,
|
|
2841
|
+
expectedClearEpoch,
|
|
2842
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2843
|
+
expectedKeyEpoch,
|
|
2844
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2845
|
+
});
|
|
2846
|
+
return null;
|
|
2847
|
+
}
|
|
2199
2848
|
await this.storeEntry(key, "empty", null, options);
|
|
2200
2849
|
return null;
|
|
2201
2850
|
}
|
|
@@ -2208,11 +2857,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
2208
2857
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2209
2858
|
}
|
|
2210
2859
|
}
|
|
2860
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2861
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2862
|
+
key,
|
|
2863
|
+
expectedClearEpoch,
|
|
2864
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2865
|
+
expectedKeyEpoch,
|
|
2866
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2867
|
+
});
|
|
2868
|
+
return fetched;
|
|
2869
|
+
}
|
|
2211
2870
|
await this.storeEntry(key, "value", fetched, options);
|
|
2212
2871
|
return fetched;
|
|
2213
2872
|
}
|
|
2214
2873
|
async storeEntry(key, kind, value, options) {
|
|
2874
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2875
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2215
2876
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2877
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2878
|
+
return;
|
|
2879
|
+
}
|
|
2216
2880
|
if (options?.tags) {
|
|
2217
2881
|
await this.tagIndex.track(key, options.tags);
|
|
2218
2882
|
} else {
|
|
@@ -2227,6 +2891,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
2227
2891
|
}
|
|
2228
2892
|
async writeBatch(entries) {
|
|
2229
2893
|
const now = Date.now();
|
|
2894
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2895
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2230
2896
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2231
2897
|
const immediateOperations = [];
|
|
2232
2898
|
const deferredOperations = [];
|
|
@@ -2243,12 +2909,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
2243
2909
|
}
|
|
2244
2910
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2245
2911
|
const operation = async () => {
|
|
2912
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
const activeEntries = layerEntries.filter(
|
|
2916
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2917
|
+
);
|
|
2918
|
+
if (activeEntries.length === 0) {
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2246
2921
|
try {
|
|
2247
2922
|
if (layer.setMany) {
|
|
2248
|
-
await layer.setMany(
|
|
2923
|
+
await layer.setMany(activeEntries);
|
|
2249
2924
|
return;
|
|
2250
2925
|
}
|
|
2251
|
-
await Promise.all(
|
|
2926
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2252
2927
|
} catch (error) {
|
|
2253
2928
|
await this.handleLayerFailure(layer, "write", error);
|
|
2254
2929
|
}
|
|
@@ -2261,7 +2936,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
2261
2936
|
}
|
|
2262
2937
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2263
2938
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2939
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2940
|
+
return;
|
|
2941
|
+
}
|
|
2264
2942
|
for (const entry of entries) {
|
|
2943
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2265
2946
|
if (entry.options?.tags) {
|
|
2266
2947
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2267
2948
|
} else {
|
|
@@ -2363,10 +3044,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
2363
3044
|
}
|
|
2364
3045
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2365
3046
|
const now = Date.now();
|
|
3047
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3048
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2366
3049
|
const immediateOperations = [];
|
|
2367
3050
|
const deferredOperations = [];
|
|
2368
3051
|
for (const layer of this.layers) {
|
|
2369
3052
|
const operation = async () => {
|
|
3053
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
3054
|
+
return;
|
|
3055
|
+
}
|
|
2370
3056
|
if (this.shouldSkipLayer(layer)) {
|
|
2371
3057
|
return;
|
|
2372
3058
|
}
|
|
@@ -2427,13 +3113,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
2427
3113
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2428
3114
|
}
|
|
2429
3115
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2430
|
-
if (
|
|
3116
|
+
if (!shouldStartBackgroundRefresh({
|
|
3117
|
+
isDisconnecting: this.isDisconnecting,
|
|
3118
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
3119
|
+
})) {
|
|
2431
3120
|
return;
|
|
2432
3121
|
}
|
|
3122
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
3123
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2433
3124
|
const refresh = (async () => {
|
|
2434
3125
|
this.metricsCollector.increment("refreshes");
|
|
2435
3126
|
try {
|
|
2436
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
3127
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2437
3128
|
} catch (error) {
|
|
2438
3129
|
this.metricsCollector.increment("refreshErrors");
|
|
2439
3130
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2443,14 +3134,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
2443
3134
|
})();
|
|
2444
3135
|
this.backgroundRefreshes.set(key, refresh);
|
|
2445
3136
|
}
|
|
2446
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
3137
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2447
3138
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2448
3139
|
await this.fetchWithGuards(
|
|
2449
3140
|
key,
|
|
2450
3141
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2451
3142
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2452
3143
|
}),
|
|
2453
|
-
options
|
|
3144
|
+
options,
|
|
3145
|
+
expectedClearEpoch,
|
|
3146
|
+
expectedKeyEpoch
|
|
2454
3147
|
);
|
|
2455
3148
|
}
|
|
2456
3149
|
resolveSingleFlightOptions() {
|
|
@@ -2465,6 +3158,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
2465
3158
|
if (keys.length === 0) {
|
|
2466
3159
|
return;
|
|
2467
3160
|
}
|
|
3161
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2468
3162
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2469
3163
|
for (const key of keys) {
|
|
2470
3164
|
await this.tagIndex.remove(key);
|
|
@@ -2487,21 +3181,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
2487
3181
|
return;
|
|
2488
3182
|
}
|
|
2489
3183
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2490
|
-
if (localLayers.length === 0) {
|
|
2491
|
-
return;
|
|
2492
|
-
}
|
|
2493
3184
|
if (message.scope === "clear") {
|
|
3185
|
+
this.maintenance.beginClearEpoch();
|
|
2494
3186
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2495
3187
|
await this.tagIndex.clear();
|
|
2496
3188
|
this.ttlResolver.clearProfiles();
|
|
3189
|
+
this.circuitBreakerManager.clear();
|
|
2497
3190
|
return;
|
|
2498
3191
|
}
|
|
2499
3192
|
const keys = message.keys ?? [];
|
|
3193
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2500
3194
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2501
3195
|
if (message.operation !== "write") {
|
|
2502
3196
|
for (const key of keys) {
|
|
2503
3197
|
await this.tagIndex.remove(key);
|
|
2504
3198
|
this.ttlResolver.deleteProfile(key);
|
|
3199
|
+
this.circuitBreakerManager.delete(key);
|
|
2505
3200
|
}
|
|
2506
3201
|
}
|
|
2507
3202
|
}
|
|
@@ -2553,35 +3248,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
2553
3248
|
shouldBroadcastL1Invalidation() {
|
|
2554
3249
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2555
3250
|
}
|
|
2556
|
-
shouldCleanupGenerations() {
|
|
2557
|
-
return Boolean(this.options.generationCleanup);
|
|
2558
|
-
}
|
|
2559
|
-
generationCleanupBatchSize() {
|
|
2560
|
-
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2561
|
-
return configured ?? 500;
|
|
2562
|
-
}
|
|
2563
3251
|
scheduleGenerationCleanup(generation) {
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
this.generationCleanupPromise = void 0;
|
|
3252
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3253
|
+
generation,
|
|
3254
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3255
|
+
(failedGeneration, error) => {
|
|
3256
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3257
|
+
generation: failedGeneration,
|
|
3258
|
+
error: this.formatError(error)
|
|
3259
|
+
});
|
|
2573
3260
|
}
|
|
2574
|
-
|
|
3261
|
+
);
|
|
2575
3262
|
}
|
|
2576
3263
|
async cleanupGeneration(generation) {
|
|
2577
3264
|
const prefix = `v${generation}:`;
|
|
2578
3265
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2579
|
-
|
|
2580
|
-
return;
|
|
2581
|
-
}
|
|
2582
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
2583
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2584
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3266
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
2585
3267
|
await this.deleteKeys(batch);
|
|
2586
3268
|
await this.publishInvalidation({
|
|
2587
3269
|
scope: "keys",
|
|
@@ -2592,58 +3274,34 @@ var CacheStack = class extends EventEmitter {
|
|
|
2592
3274
|
}
|
|
2593
3275
|
}
|
|
2594
3276
|
initializeWriteBehind(options) {
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
return;
|
|
2601
|
-
}
|
|
2602
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2603
|
-
void this.flushWriteBehindQueue();
|
|
2604
|
-
}, flushIntervalMs);
|
|
2605
|
-
this.writeBehindTimer.unref?.();
|
|
3277
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3278
|
+
this.options.writeStrategy,
|
|
3279
|
+
options,
|
|
3280
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3281
|
+
);
|
|
2606
3282
|
}
|
|
2607
3283
|
shouldWriteBehind(layer) {
|
|
2608
3284
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2609
3285
|
}
|
|
2610
3286
|
async enqueueWriteBehind(operation) {
|
|
2611
|
-
this.
|
|
2612
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2613
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2614
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2615
|
-
await this.flushWriteBehindQueue();
|
|
2616
|
-
return;
|
|
2617
|
-
}
|
|
2618
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2619
|
-
await this.flushWriteBehindQueue();
|
|
2620
|
-
}
|
|
3287
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2621
3288
|
}
|
|
2622
3289
|
async flushWriteBehindQueue() {
|
|
2623
|
-
|
|
2624
|
-
|
|
3290
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3291
|
+
}
|
|
3292
|
+
async runWriteBehindBatch(batch) {
|
|
3293
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3294
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3295
|
+
if (failures.length === 0) {
|
|
2625
3296
|
return;
|
|
2626
3297
|
}
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
2635
|
-
failed: failures.length,
|
|
2636
|
-
total: batch.length,
|
|
2637
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2638
|
-
});
|
|
2639
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2640
|
-
}
|
|
2641
|
-
})();
|
|
2642
|
-
await this.writeBehindFlushPromise;
|
|
2643
|
-
this.writeBehindFlushPromise = void 0;
|
|
2644
|
-
if (this.writeBehindQueue.length > 0) {
|
|
2645
|
-
await this.flushWriteBehindQueue();
|
|
2646
|
-
}
|
|
3298
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3299
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3300
|
+
failed: failures.length,
|
|
3301
|
+
total: batch.length,
|
|
3302
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3303
|
+
});
|
|
3304
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2647
3305
|
}
|
|
2648
3306
|
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2649
3307
|
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
@@ -2673,32 +3331,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2673
3331
|
return [];
|
|
2674
3332
|
}
|
|
2675
3333
|
const [firstGroup, ...rest] = groups;
|
|
2676
|
-
if (!firstGroup) {
|
|
2677
|
-
return [];
|
|
2678
|
-
}
|
|
2679
3334
|
const restSets = rest.map((group) => new Set(group));
|
|
2680
3335
|
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2681
3336
|
}
|
|
2682
3337
|
qualifyKey(key) {
|
|
2683
|
-
|
|
2684
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3338
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2685
3339
|
}
|
|
2686
3340
|
qualifyPattern(pattern) {
|
|
2687
|
-
|
|
2688
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3341
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
2689
3342
|
}
|
|
2690
3343
|
stripQualifiedKey(key) {
|
|
2691
|
-
|
|
2692
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
2693
|
-
return key;
|
|
2694
|
-
}
|
|
2695
|
-
return key.slice(prefix.length);
|
|
2696
|
-
}
|
|
2697
|
-
generationPrefix() {
|
|
2698
|
-
if (this.currentGeneration === void 0) {
|
|
2699
|
-
return "";
|
|
2700
|
-
}
|
|
2701
|
-
return `v${this.currentGeneration}:`;
|
|
3344
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2702
3345
|
}
|
|
2703
3346
|
async deleteKeysFromLayers(layers, keys) {
|
|
2704
3347
|
await Promise.all(
|
|
@@ -2733,118 +3376,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2733
3376
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2734
3377
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2735
3378
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
3379
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3380
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3381
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3382
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3383
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3384
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3385
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3386
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3387
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3388
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3389
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3390
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3391
|
+
}
|
|
3392
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3393
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3394
|
+
}
|
|
3395
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3396
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3397
|
+
}
|
|
3398
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3399
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3400
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2749
3401
|
if (typeof this.options.generationCleanup === "object") {
|
|
2750
|
-
|
|
3402
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2751
3403
|
}
|
|
2752
3404
|
if (this.options.generation !== void 0) {
|
|
2753
|
-
|
|
3405
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2754
3406
|
}
|
|
2755
3407
|
}
|
|
2756
3408
|
validateWriteOptions(options) {
|
|
2757
3409
|
if (!options) {
|
|
2758
3410
|
return;
|
|
2759
3411
|
}
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
validateLayerNumberOption(name, value) {
|
|
2772
|
-
if (value === void 0) {
|
|
2773
|
-
return;
|
|
2774
|
-
}
|
|
2775
|
-
if (typeof value === "number") {
|
|
2776
|
-
this.validateNonNegativeNumber(name, value);
|
|
2777
|
-
return;
|
|
2778
|
-
}
|
|
2779
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2780
|
-
if (layerValue === void 0) {
|
|
2781
|
-
continue;
|
|
2782
|
-
}
|
|
2783
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
validatePositiveNumber(name, value) {
|
|
2787
|
-
if (value === void 0) {
|
|
2788
|
-
return;
|
|
2789
|
-
}
|
|
2790
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2791
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
validateRateLimitOptions(name, options) {
|
|
2795
|
-
if (!options) {
|
|
2796
|
-
return;
|
|
2797
|
-
}
|
|
2798
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2799
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2800
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2801
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2802
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2803
|
-
}
|
|
2804
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2805
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
validateNonNegativeNumber(name, value) {
|
|
2809
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2810
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2811
|
-
}
|
|
2812
|
-
}
|
|
2813
|
-
validateCacheKey(key) {
|
|
2814
|
-
if (key.length === 0) {
|
|
2815
|
-
throw new Error("Cache key must not be empty.");
|
|
2816
|
-
}
|
|
2817
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2818
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2819
|
-
}
|
|
2820
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2821
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2822
|
-
}
|
|
2823
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2824
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2825
|
-
}
|
|
2826
|
-
return key;
|
|
2827
|
-
}
|
|
2828
|
-
validatePattern(pattern) {
|
|
2829
|
-
if (pattern.length === 0) {
|
|
2830
|
-
throw new Error("Pattern must not be empty.");
|
|
2831
|
-
}
|
|
2832
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2833
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2834
|
-
}
|
|
2835
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2836
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
2837
|
-
}
|
|
2838
|
-
}
|
|
2839
|
-
validateTtlPolicy(name, policy) {
|
|
2840
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2841
|
-
return;
|
|
2842
|
-
}
|
|
2843
|
-
if ("alignTo" in policy) {
|
|
2844
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2845
|
-
return;
|
|
2846
|
-
}
|
|
2847
|
-
throw new Error(`${name} is invalid.`);
|
|
3412
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3413
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3414
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3415
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3416
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3417
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3418
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3419
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3420
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3421
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3422
|
+
validateTags(options.tags);
|
|
2848
3423
|
}
|
|
2849
3424
|
assertActive(operation) {
|
|
2850
3425
|
if (this.isDisconnecting) {
|
|
@@ -2856,56 +3431,39 @@ var CacheStack = class extends EventEmitter {
|
|
|
2856
3431
|
await this.startup;
|
|
2857
3432
|
this.assertActive(operation);
|
|
2858
3433
|
}
|
|
2859
|
-
serializeOptions(options) {
|
|
2860
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2861
|
-
}
|
|
2862
|
-
validateAdaptiveTtlOptions(options) {
|
|
2863
|
-
if (!options || options === true) {
|
|
2864
|
-
return;
|
|
2865
|
-
}
|
|
2866
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2867
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2868
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2869
|
-
}
|
|
2870
|
-
validateCircuitBreakerOptions(options) {
|
|
2871
|
-
if (!options) {
|
|
2872
|
-
return;
|
|
2873
|
-
}
|
|
2874
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2875
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2876
|
-
}
|
|
2877
3434
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2878
|
-
const
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3435
|
+
const plan = planFreshReadPolicies({
|
|
3436
|
+
stored: hit.stored,
|
|
3437
|
+
hasFetcher: Boolean(fetcher),
|
|
3438
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3439
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3440
|
+
});
|
|
3441
|
+
if (plan.refreshedStored) {
|
|
2883
3442
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
2884
3443
|
const layer = this.layers[index];
|
|
2885
3444
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2886
3445
|
continue;
|
|
2887
3446
|
}
|
|
2888
3447
|
try {
|
|
2889
|
-
await layer.set(key,
|
|
3448
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
2890
3449
|
} catch (error) {
|
|
2891
3450
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
2892
3451
|
}
|
|
2893
3452
|
}
|
|
2894
3453
|
}
|
|
2895
|
-
if (fetcher &&
|
|
3454
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
2896
3455
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
2897
3456
|
}
|
|
2898
3457
|
}
|
|
2899
3458
|
shouldSkipLayer(layer) {
|
|
2900
|
-
|
|
2901
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3459
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
2902
3460
|
}
|
|
2903
3461
|
async handleLayerFailure(layer, operation, error) {
|
|
2904
|
-
|
|
3462
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3463
|
+
if (!recovery.degrade) {
|
|
2905
3464
|
throw error;
|
|
2906
3465
|
}
|
|
2907
|
-
|
|
2908
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3466
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
2909
3467
|
this.metricsCollector.increment("degradedOperations");
|
|
2910
3468
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2911
3469
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -2941,18 +3499,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2941
3499
|
this.emit("error", { operation, ...context });
|
|
2942
3500
|
}
|
|
2943
3501
|
}
|
|
2944
|
-
serializeKeyPart(value) {
|
|
2945
|
-
if (typeof value === "string") {
|
|
2946
|
-
return `s:${value}`;
|
|
2947
|
-
}
|
|
2948
|
-
if (typeof value === "number") {
|
|
2949
|
-
return `n:${value}`;
|
|
2950
|
-
}
|
|
2951
|
-
if (typeof value === "boolean") {
|
|
2952
|
-
return `b:${value}`;
|
|
2953
|
-
}
|
|
2954
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2955
|
-
}
|
|
2956
3502
|
isCacheSnapshotEntries(value) {
|
|
2957
3503
|
return Array.isArray(value) && value.every((entry) => {
|
|
2958
3504
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2965,54 +3511,72 @@ var CacheStack = class extends EventEmitter {
|
|
|
2965
3511
|
sanitizeSnapshotValue(value) {
|
|
2966
3512
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2967
3513
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
3514
|
+
snapshotMaxBytes() {
|
|
3515
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3516
|
+
}
|
|
3517
|
+
snapshotMaxEntries() {
|
|
3518
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3519
|
+
}
|
|
3520
|
+
invalidationMaxKeys() {
|
|
3521
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3522
|
+
}
|
|
3523
|
+
async collectKeysForTag(tag) {
|
|
3524
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3525
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3526
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3527
|
+
keys.add(key);
|
|
3528
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3529
|
+
});
|
|
3530
|
+
return [...keys];
|
|
2974
3531
|
}
|
|
2975
|
-
const
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
if (baseDir !== false) {
|
|
2979
|
-
const relative = path.relative(baseDir, resolved);
|
|
2980
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2981
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2982
|
-
}
|
|
3532
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3533
|
+
keys.add(key);
|
|
3534
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2983
3535
|
}
|
|
2984
|
-
return
|
|
3536
|
+
return [...keys];
|
|
2985
3537
|
}
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
3538
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3539
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3540
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3541
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2989
3542
|
}
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
3543
|
+
}
|
|
3544
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3545
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3546
|
+
for (const layer of this.layers) {
|
|
3547
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3548
|
+
continue;
|
|
3549
|
+
}
|
|
3550
|
+
const visitKey = async (key) => {
|
|
3551
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3552
|
+
if (exported.has(exportedKey)) {
|
|
3553
|
+
return;
|
|
2994
3554
|
}
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
3555
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3556
|
+
if (stored === null) {
|
|
3557
|
+
return;
|
|
3558
|
+
}
|
|
3559
|
+
exported.add(exportedKey);
|
|
3560
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3561
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3562
|
+
}
|
|
3563
|
+
await visitor({
|
|
3564
|
+
key: exportedKey,
|
|
3565
|
+
value: stored,
|
|
3566
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3567
|
+
});
|
|
3568
|
+
};
|
|
3569
|
+
if (layer.forEachKey) {
|
|
3570
|
+
await layer.forEachKey(visitKey);
|
|
3571
|
+
continue;
|
|
3572
|
+
}
|
|
3573
|
+
const keys = await layer.keys?.();
|
|
3574
|
+
for (const key of keys ?? []) {
|
|
3575
|
+
await visitKey(key);
|
|
3576
|
+
}
|
|
2998
3577
|
}
|
|
2999
|
-
return value;
|
|
3000
3578
|
}
|
|
3001
3579
|
};
|
|
3002
|
-
function createInstanceId() {
|
|
3003
|
-
if (globalThis.crypto?.randomUUID) {
|
|
3004
|
-
return globalThis.crypto.randomUUID();
|
|
3005
|
-
}
|
|
3006
|
-
const bytes = new Uint8Array(16);
|
|
3007
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
3008
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
3009
|
-
} else {
|
|
3010
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
3011
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
3012
|
-
}
|
|
3013
|
-
}
|
|
3014
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
3015
|
-
}
|
|
3016
3580
|
|
|
3017
3581
|
// src/module.ts
|
|
3018
3582
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|