layercache 1.2.5 → 1.2.6
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-DLstcDMn.d.cts} +32 -14
- package/dist/{edge-P07GCO2Y.d.cts → edge-DLstcDMn.d.ts} +32 -14
- 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 +1070 -352
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +950 -347
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +722 -272
- package/packages/nestjs/dist/index.d.cts +23 -13
- package/packages/nestjs/dist/index.d.ts +23 -13
- package/packages/nestjs/dist/index.js +722 -272
package/dist/index.cjs
CHANGED
|
@@ -66,22 +66,23 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
66
66
|
constructor(cache, prefix) {
|
|
67
67
|
this.cache = cache;
|
|
68
68
|
this.prefix = prefix;
|
|
69
|
+
validateNamespaceKey(prefix);
|
|
69
70
|
}
|
|
70
71
|
cache;
|
|
71
72
|
prefix;
|
|
72
73
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
73
74
|
metrics = emptyMetrics();
|
|
74
75
|
async get(key, fetcher, options) {
|
|
75
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
76
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
76
77
|
}
|
|
77
78
|
async getOrSet(key, fetcher, options) {
|
|
78
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
79
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
79
80
|
}
|
|
80
81
|
/**
|
|
81
82
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
82
83
|
*/
|
|
83
84
|
async getOrThrow(key, fetcher, options) {
|
|
84
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
85
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
85
86
|
}
|
|
86
87
|
async has(key) {
|
|
87
88
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -90,7 +91,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
90
91
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
91
92
|
}
|
|
92
93
|
async set(key, value, options) {
|
|
93
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
94
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
94
95
|
}
|
|
95
96
|
async delete(key) {
|
|
96
97
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -106,7 +107,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
106
107
|
() => this.cache.mget(
|
|
107
108
|
entries.map((entry) => ({
|
|
108
109
|
...entry,
|
|
109
|
-
key: this.qualify(entry.key)
|
|
110
|
+
key: this.qualify(entry.key),
|
|
111
|
+
options: this.qualifyGetOptions(entry.options)
|
|
110
112
|
}))
|
|
111
113
|
)
|
|
112
114
|
);
|
|
@@ -116,16 +118,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
116
118
|
() => this.cache.mset(
|
|
117
119
|
entries.map((entry) => ({
|
|
118
120
|
...entry,
|
|
119
|
-
key: this.qualify(entry.key)
|
|
121
|
+
key: this.qualify(entry.key),
|
|
122
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
120
123
|
}))
|
|
121
124
|
)
|
|
122
125
|
);
|
|
123
126
|
}
|
|
124
127
|
async invalidateByTag(tag) {
|
|
125
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
128
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
126
129
|
}
|
|
127
130
|
async invalidateByTags(tags, mode = "any") {
|
|
128
|
-
await this.trackMetrics(
|
|
131
|
+
await this.trackMetrics(
|
|
132
|
+
() => this.cache.invalidateByTags(
|
|
133
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
134
|
+
mode
|
|
135
|
+
)
|
|
136
|
+
);
|
|
129
137
|
}
|
|
130
138
|
async invalidateByPattern(pattern) {
|
|
131
139
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -137,16 +145,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
137
145
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
138
146
|
*/
|
|
139
147
|
async inspect(key) {
|
|
140
|
-
|
|
148
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
149
|
+
if (result === null) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
...result,
|
|
154
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
155
|
+
};
|
|
141
156
|
}
|
|
142
157
|
wrap(keyPrefix, fetcher, options) {
|
|
143
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
158
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
144
159
|
}
|
|
145
160
|
warm(entries, options) {
|
|
146
161
|
return this.cache.warm(
|
|
147
162
|
entries.map((entry) => ({
|
|
148
163
|
...entry,
|
|
149
|
-
key: this.qualify(entry.key)
|
|
164
|
+
key: this.qualify(entry.key),
|
|
165
|
+
options: this.qualifyGetOptions(entry.options)
|
|
150
166
|
})),
|
|
151
167
|
options
|
|
152
168
|
);
|
|
@@ -182,6 +198,24 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
182
198
|
qualify(key) {
|
|
183
199
|
return `${this.prefix}:${key}`;
|
|
184
200
|
}
|
|
201
|
+
qualifyTag(tag) {
|
|
202
|
+
return `${this.prefix}:${tag}`;
|
|
203
|
+
}
|
|
204
|
+
qualifyGetOptions(options) {
|
|
205
|
+
return this.qualifyWriteOptions(options);
|
|
206
|
+
}
|
|
207
|
+
qualifyWrapOptions(options) {
|
|
208
|
+
return this.qualifyWriteOptions(options);
|
|
209
|
+
}
|
|
210
|
+
qualifyWriteOptions(options) {
|
|
211
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
212
|
+
return options;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
...options,
|
|
216
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
217
|
+
};
|
|
218
|
+
}
|
|
185
219
|
async trackMetrics(operation) {
|
|
186
220
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
187
221
|
const before = this.cache.getMetrics();
|
|
@@ -316,6 +350,9 @@ function validateNamespaceKey(key) {
|
|
|
316
350
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
317
351
|
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
318
352
|
}
|
|
353
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
354
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
355
|
+
}
|
|
319
356
|
}
|
|
320
357
|
|
|
321
358
|
// src/invalidation/PatternMatcher.ts
|
|
@@ -372,21 +409,41 @@ var CacheKeyDiscovery = class {
|
|
|
372
409
|
this.options = options;
|
|
373
410
|
}
|
|
374
411
|
options;
|
|
375
|
-
async collectKeysWithPrefix(prefix) {
|
|
412
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
376
413
|
const { tagIndex } = this.options;
|
|
377
|
-
const matches = new Set(
|
|
378
|
-
|
|
379
|
-
|
|
414
|
+
const matches = /* @__PURE__ */ new Set();
|
|
415
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
416
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
417
|
+
matches.add(key);
|
|
418
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
422
|
+
for (const key of initialMatches) {
|
|
423
|
+
matches.add(key);
|
|
424
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
380
427
|
await Promise.all(
|
|
381
428
|
this.options.layers.map(async (layer) => {
|
|
382
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
429
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
383
430
|
return;
|
|
384
431
|
}
|
|
385
432
|
try {
|
|
386
|
-
|
|
387
|
-
|
|
433
|
+
if (layer.forEachKey) {
|
|
434
|
+
await layer.forEachKey(async (key) => {
|
|
435
|
+
if (key.startsWith(prefix)) {
|
|
436
|
+
matches.add(key);
|
|
437
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const keys = await layer.keys?.();
|
|
443
|
+
for (const key of keys ?? []) {
|
|
388
444
|
if (key.startsWith(prefix)) {
|
|
389
445
|
matches.add(key);
|
|
446
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
390
447
|
}
|
|
391
448
|
}
|
|
392
449
|
} catch (error) {
|
|
@@ -396,18 +453,39 @@ var CacheKeyDiscovery = class {
|
|
|
396
453
|
);
|
|
397
454
|
return [...matches];
|
|
398
455
|
}
|
|
399
|
-
async collectKeysMatchingPattern(pattern) {
|
|
400
|
-
const matches = new Set(
|
|
456
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
457
|
+
const matches = /* @__PURE__ */ new Set();
|
|
458
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
459
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
460
|
+
matches.add(key);
|
|
461
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
465
|
+
matches.add(key);
|
|
466
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
401
469
|
await Promise.all(
|
|
402
470
|
this.options.layers.map(async (layer) => {
|
|
403
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
471
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
404
472
|
return;
|
|
405
473
|
}
|
|
406
474
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
475
|
+
if (layer.forEachKey) {
|
|
476
|
+
await layer.forEachKey(async (key) => {
|
|
477
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
478
|
+
matches.add(key);
|
|
479
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
const keys = await layer.keys?.();
|
|
485
|
+
for (const key of keys ?? []) {
|
|
409
486
|
if (PatternMatcher.matches(pattern, key)) {
|
|
410
487
|
matches.add(key);
|
|
488
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
411
489
|
}
|
|
412
490
|
}
|
|
413
491
|
} catch (error) {
|
|
@@ -417,8 +495,280 @@ var CacheKeyDiscovery = class {
|
|
|
417
495
|
);
|
|
418
496
|
return [...matches];
|
|
419
497
|
}
|
|
498
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
499
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
500
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
420
503
|
};
|
|
421
504
|
|
|
505
|
+
// src/internal/CacheKeySerialization.ts
|
|
506
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
507
|
+
function normalizeForSerialization(value) {
|
|
508
|
+
if (Array.isArray(value)) {
|
|
509
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
510
|
+
}
|
|
511
|
+
if (value && typeof value === "object") {
|
|
512
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
513
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
514
|
+
return normalized;
|
|
515
|
+
}
|
|
516
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
517
|
+
return normalized;
|
|
518
|
+
}, {});
|
|
519
|
+
}
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
function serializeKeyPart(value) {
|
|
523
|
+
if (typeof value === "string") {
|
|
524
|
+
return `s:${value}`;
|
|
525
|
+
}
|
|
526
|
+
if (typeof value === "number") {
|
|
527
|
+
return `n:${value}`;
|
|
528
|
+
}
|
|
529
|
+
if (typeof value === "boolean") {
|
|
530
|
+
return `b:${value}`;
|
|
531
|
+
}
|
|
532
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
533
|
+
}
|
|
534
|
+
function serializeOptions(options) {
|
|
535
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
536
|
+
}
|
|
537
|
+
function createInstanceId() {
|
|
538
|
+
if (globalThis.crypto?.randomUUID) {
|
|
539
|
+
return globalThis.crypto.randomUUID();
|
|
540
|
+
}
|
|
541
|
+
const bytes = new Uint8Array(16);
|
|
542
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
543
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
544
|
+
} else {
|
|
545
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
546
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/internal/CacheSnapshotFile.ts
|
|
553
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
554
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
555
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
556
|
+
}
|
|
557
|
+
async function findExistingAncestor(directory, fs2, path) {
|
|
558
|
+
let current = directory;
|
|
559
|
+
while (true) {
|
|
560
|
+
try {
|
|
561
|
+
await fs2.lstat(current);
|
|
562
|
+
return current;
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (error.code !== "ENOENT") {
|
|
565
|
+
throw error;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const parent = path.dirname(current);
|
|
569
|
+
if (parent === current) {
|
|
570
|
+
return current;
|
|
571
|
+
}
|
|
572
|
+
current = parent;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
576
|
+
if (filePath.length === 0) {
|
|
577
|
+
throw new Error("filePath must not be empty.");
|
|
578
|
+
}
|
|
579
|
+
if (filePath.includes("\0")) {
|
|
580
|
+
throw new Error("filePath must not contain null bytes.");
|
|
581
|
+
}
|
|
582
|
+
const { promises: fs2 } = await import("fs");
|
|
583
|
+
const path = await import("path");
|
|
584
|
+
const resolved = path.resolve(filePath);
|
|
585
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
586
|
+
if (baseDir === false) {
|
|
587
|
+
return resolved;
|
|
588
|
+
}
|
|
589
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
590
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
591
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
592
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
593
|
+
}
|
|
594
|
+
if (mode === "read") {
|
|
595
|
+
const realTarget = await fs2.realpath(resolved);
|
|
596
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
597
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
598
|
+
}
|
|
599
|
+
return realTarget;
|
|
600
|
+
}
|
|
601
|
+
const parentDir = path.dirname(resolved);
|
|
602
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
|
|
603
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
604
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
605
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
606
|
+
}
|
|
607
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
608
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
609
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
610
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
611
|
+
}
|
|
612
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
613
|
+
try {
|
|
614
|
+
const existing = await fs2.lstat(targetPath);
|
|
615
|
+
if (existing.isSymbolicLink()) {
|
|
616
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
617
|
+
}
|
|
618
|
+
} catch (error) {
|
|
619
|
+
if (error.code !== "ENOENT") {
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return targetPath;
|
|
624
|
+
}
|
|
625
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
626
|
+
if (byteLimit === false) {
|
|
627
|
+
return handle.readFile({ encoding: "utf8" });
|
|
628
|
+
}
|
|
629
|
+
const chunks = [];
|
|
630
|
+
let totalBytes = 0;
|
|
631
|
+
let position = 0;
|
|
632
|
+
while (true) {
|
|
633
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
634
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
635
|
+
if (bytesRead === 0) {
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
totalBytes += bytesRead;
|
|
639
|
+
if (totalBytes > byteLimit) {
|
|
640
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
641
|
+
}
|
|
642
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
643
|
+
position += bytesRead;
|
|
644
|
+
}
|
|
645
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// src/internal/CacheStackValidation.ts
|
|
649
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
650
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
651
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
652
|
+
function validatePositiveNumber(name, value) {
|
|
653
|
+
if (value === void 0) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
657
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
function validateNonNegativeNumber(name, value) {
|
|
661
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
662
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
function validateLayerNumberOption(name, value) {
|
|
666
|
+
if (value === void 0) {
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
if (typeof value === "number") {
|
|
670
|
+
validateNonNegativeNumber(name, value);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
674
|
+
if (layerValue === void 0) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function validateRateLimitOptions(name, options) {
|
|
681
|
+
if (!options) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
685
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
686
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
687
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
688
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
689
|
+
}
|
|
690
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
691
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
function validateCacheKey(key) {
|
|
695
|
+
if (key.length === 0) {
|
|
696
|
+
throw new Error("Cache key must not be empty.");
|
|
697
|
+
}
|
|
698
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
699
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
700
|
+
}
|
|
701
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
702
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
703
|
+
}
|
|
704
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
705
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
706
|
+
}
|
|
707
|
+
return key;
|
|
708
|
+
}
|
|
709
|
+
function validateTag(tag) {
|
|
710
|
+
if (tag.length === 0) {
|
|
711
|
+
throw new Error("Cache tag must not be empty.");
|
|
712
|
+
}
|
|
713
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
714
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
715
|
+
}
|
|
716
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
717
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
718
|
+
}
|
|
719
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
720
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
721
|
+
}
|
|
722
|
+
return tag;
|
|
723
|
+
}
|
|
724
|
+
function validateTags(tags) {
|
|
725
|
+
if (!tags) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
729
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
730
|
+
}
|
|
731
|
+
for (const tag of tags) {
|
|
732
|
+
validateTag(tag);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
function validatePattern(pattern) {
|
|
736
|
+
if (pattern.length === 0) {
|
|
737
|
+
throw new Error("Pattern must not be empty.");
|
|
738
|
+
}
|
|
739
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
740
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
741
|
+
}
|
|
742
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
743
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
function validateTtlPolicy(name, policy) {
|
|
747
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if ("alignTo" in policy) {
|
|
751
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
throw new Error(`${name} is invalid.`);
|
|
755
|
+
}
|
|
756
|
+
function validateAdaptiveTtlOptions(options) {
|
|
757
|
+
if (!options || options === true) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
761
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
762
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
763
|
+
}
|
|
764
|
+
function validateCircuitBreakerOptions(options) {
|
|
765
|
+
if (!options) {
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
769
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
770
|
+
}
|
|
771
|
+
|
|
422
772
|
// src/internal/CircuitBreakerManager.ts
|
|
423
773
|
var CircuitBreakerManager = class {
|
|
424
774
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -812,19 +1162,47 @@ function isStoredValueEnvelope(value) {
|
|
|
812
1162
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
813
1163
|
return false;
|
|
814
1164
|
}
|
|
815
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1165
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
816
1166
|
return false;
|
|
817
1167
|
}
|
|
818
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1168
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
819
1169
|
return false;
|
|
820
1170
|
}
|
|
821
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1171
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
822
1172
|
return false;
|
|
823
1173
|
}
|
|
824
1174
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
825
1175
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
826
1176
|
return false;
|
|
827
1177
|
}
|
|
1178
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1181
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
1194
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
1195
|
+
return false;
|
|
1196
|
+
}
|
|
1197
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
1201
|
+
return false;
|
|
1202
|
+
}
|
|
1203
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
1204
|
+
return false;
|
|
1205
|
+
}
|
|
828
1206
|
return true;
|
|
829
1207
|
}
|
|
830
1208
|
function createStoredValueEnvelope(options) {
|
|
@@ -923,6 +1301,12 @@ function normalizePositiveSeconds(value) {
|
|
|
923
1301
|
}
|
|
924
1302
|
return value;
|
|
925
1303
|
}
|
|
1304
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
1305
|
+
if (value == null) {
|
|
1306
|
+
return true;
|
|
1307
|
+
}
|
|
1308
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
1309
|
+
}
|
|
926
1310
|
|
|
927
1311
|
// src/internal/TtlResolver.ts
|
|
928
1312
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1077,6 +1461,11 @@ var TagIndex = class {
|
|
|
1077
1461
|
async keysForTag(tag) {
|
|
1078
1462
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1079
1463
|
}
|
|
1464
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1465
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1466
|
+
await visitor(key);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1080
1469
|
async keysForPrefix(prefix) {
|
|
1081
1470
|
const node = this.findNode(prefix);
|
|
1082
1471
|
if (!node) {
|
|
@@ -1086,6 +1475,13 @@ var TagIndex = class {
|
|
|
1086
1475
|
this.collectFromNode(node, prefix, matches);
|
|
1087
1476
|
return matches;
|
|
1088
1477
|
}
|
|
1478
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1479
|
+
const node = this.findNode(prefix);
|
|
1480
|
+
if (!node) {
|
|
1481
|
+
return;
|
|
1482
|
+
}
|
|
1483
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1484
|
+
}
|
|
1089
1485
|
async tagsForKey(key) {
|
|
1090
1486
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1091
1487
|
}
|
|
@@ -1094,6 +1490,12 @@ var TagIndex = class {
|
|
|
1094
1490
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1095
1491
|
return [...matches];
|
|
1096
1492
|
}
|
|
1493
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1494
|
+
const matches = await this.matchPattern(pattern);
|
|
1495
|
+
for (const key of matches) {
|
|
1496
|
+
await visitor(key);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1097
1499
|
async clear() {
|
|
1098
1500
|
this.tagToKeys.clear();
|
|
1099
1501
|
this.keyToTags.clear();
|
|
@@ -1143,6 +1545,14 @@ var TagIndex = class {
|
|
|
1143
1545
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1144
1546
|
}
|
|
1145
1547
|
}
|
|
1548
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1549
|
+
if (node.terminal) {
|
|
1550
|
+
await visitor(prefix);
|
|
1551
|
+
}
|
|
1552
|
+
for (const [character, child] of node.children) {
|
|
1553
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1146
1556
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1147
1557
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1148
1558
|
return;
|
|
@@ -1260,22 +1670,27 @@ var TagIndex = class {
|
|
|
1260
1670
|
|
|
1261
1671
|
// src/serialization/JsonSerializer.ts
|
|
1262
1672
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1673
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1263
1674
|
var JsonSerializer = class {
|
|
1264
1675
|
serialize(value) {
|
|
1265
1676
|
return JSON.stringify(value);
|
|
1266
1677
|
}
|
|
1267
1678
|
deserialize(payload) {
|
|
1268
1679
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1269
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1680
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1270
1681
|
}
|
|
1271
1682
|
};
|
|
1272
1683
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1273
|
-
function sanitizeJsonValue(value, depth) {
|
|
1684
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
1685
|
+
state.count += 1;
|
|
1686
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
1687
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1688
|
+
}
|
|
1274
1689
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1275
|
-
|
|
1690
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1276
1691
|
}
|
|
1277
1692
|
if (Array.isArray(value)) {
|
|
1278
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1693
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1279
1694
|
}
|
|
1280
1695
|
if (!isPlainObject(value)) {
|
|
1281
1696
|
return value;
|
|
@@ -1285,7 +1700,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1285
1700
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1286
1701
|
continue;
|
|
1287
1702
|
}
|
|
1288
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1703
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1289
1704
|
}
|
|
1290
1705
|
return sanitized;
|
|
1291
1706
|
}
|
|
@@ -1335,10 +1750,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1335
1750
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1336
1751
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1337
1752
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1338
|
-
var
|
|
1339
|
-
var
|
|
1753
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1754
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1755
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1756
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1340
1757
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1341
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1342
1758
|
var DebugLogger = class {
|
|
1343
1759
|
enabled;
|
|
1344
1760
|
constructor(enabled) {
|
|
@@ -1425,6 +1841,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1425
1841
|
snapshotSerializer = new JsonSerializer();
|
|
1426
1842
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1427
1843
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1844
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1428
1845
|
ttlResolver;
|
|
1429
1846
|
circuitBreakerManager;
|
|
1430
1847
|
currentGeneration;
|
|
@@ -1432,6 +1849,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1432
1849
|
writeBehindTimer;
|
|
1433
1850
|
writeBehindFlushPromise;
|
|
1434
1851
|
generationCleanupPromise;
|
|
1852
|
+
clearEpoch = 0;
|
|
1435
1853
|
isDisconnecting = false;
|
|
1436
1854
|
disconnectPromise;
|
|
1437
1855
|
/**
|
|
@@ -1441,7 +1859,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1441
1859
|
* and no `fetcher` is provided.
|
|
1442
1860
|
*/
|
|
1443
1861
|
async get(key, fetcher, options) {
|
|
1444
|
-
const normalizedKey = this.qualifyKey(
|
|
1862
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1445
1863
|
this.validateWriteOptions(options);
|
|
1446
1864
|
await this.awaitStartup("get");
|
|
1447
1865
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1511,7 +1929,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1511
1929
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1512
1930
|
*/
|
|
1513
1931
|
async has(key) {
|
|
1514
|
-
const normalizedKey = this.qualifyKey(
|
|
1932
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1515
1933
|
await this.awaitStartup("has");
|
|
1516
1934
|
for (const layer of this.layers) {
|
|
1517
1935
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1544,7 +1962,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1544
1962
|
* that has it, or null if the key is not found / has no TTL.
|
|
1545
1963
|
*/
|
|
1546
1964
|
async ttl(key) {
|
|
1547
|
-
const normalizedKey = this.qualifyKey(
|
|
1965
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1548
1966
|
await this.awaitStartup("ttl");
|
|
1549
1967
|
for (const layer of this.layers) {
|
|
1550
1968
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1566,7 +1984,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1566
1984
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1567
1985
|
*/
|
|
1568
1986
|
async set(key, value, options) {
|
|
1569
|
-
const normalizedKey = this.qualifyKey(
|
|
1987
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1570
1988
|
this.validateWriteOptions(options);
|
|
1571
1989
|
await this.awaitStartup("set");
|
|
1572
1990
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1575,7 +1993,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1575
1993
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1576
1994
|
*/
|
|
1577
1995
|
async delete(key) {
|
|
1578
|
-
const normalizedKey = this.qualifyKey(
|
|
1996
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1579
1997
|
await this.awaitStartup("delete");
|
|
1580
1998
|
await this.deleteKeys([normalizedKey]);
|
|
1581
1999
|
await this.publishInvalidation({
|
|
@@ -1587,6 +2005,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1587
2005
|
}
|
|
1588
2006
|
async clear() {
|
|
1589
2007
|
await this.awaitStartup("clear");
|
|
2008
|
+
this.beginClearEpoch();
|
|
1590
2009
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1591
2010
|
await this.tagIndex.clear();
|
|
1592
2011
|
this.ttlResolver.clearProfiles();
|
|
@@ -1603,7 +2022,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1603
2022
|
return;
|
|
1604
2023
|
}
|
|
1605
2024
|
await this.awaitStartup("mdelete");
|
|
1606
|
-
const normalizedKeys = keys.map((k) =>
|
|
2025
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1607
2026
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1608
2027
|
await this.deleteKeys(cacheKeys);
|
|
1609
2028
|
await this.publishInvalidation({
|
|
@@ -1620,7 +2039,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1620
2039
|
}
|
|
1621
2040
|
const normalizedEntries = entries.map((entry) => ({
|
|
1622
2041
|
...entry,
|
|
1623
|
-
key: this.qualifyKey(
|
|
2042
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1624
2043
|
}));
|
|
1625
2044
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1626
2045
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1629,7 +2048,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1629
2048
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1630
2049
|
return Promise.all(
|
|
1631
2050
|
normalizedEntries.map((entry) => {
|
|
1632
|
-
const optionsSignature =
|
|
2051
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1633
2052
|
const existing = pendingReads.get(entry.key);
|
|
1634
2053
|
if (!existing) {
|
|
1635
2054
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1698,7 +2117,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1698
2117
|
this.assertActive("mset");
|
|
1699
2118
|
const normalizedEntries = entries.map((entry) => ({
|
|
1700
2119
|
...entry,
|
|
1701
|
-
key: this.qualifyKey(
|
|
2120
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1702
2121
|
}));
|
|
1703
2122
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1704
2123
|
await this.awaitStartup("mset");
|
|
@@ -1741,7 +2160,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1741
2160
|
*/
|
|
1742
2161
|
wrap(prefix, fetcher, options = {}) {
|
|
1743
2162
|
return (...args) => {
|
|
1744
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2163
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1745
2164
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1746
2165
|
return this.get(key, () => fetcher(...args), options);
|
|
1747
2166
|
};
|
|
@@ -1751,11 +2170,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1751
2170
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1752
2171
|
*/
|
|
1753
2172
|
namespace(prefix) {
|
|
2173
|
+
validateNamespaceKey(prefix);
|
|
1754
2174
|
return new CacheNamespace(this, prefix);
|
|
1755
2175
|
}
|
|
1756
2176
|
async invalidateByTag(tag) {
|
|
2177
|
+
validateTag(tag);
|
|
1757
2178
|
await this.awaitStartup("invalidateByTag");
|
|
1758
|
-
const keys = await this.
|
|
2179
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1759
2180
|
await this.deleteKeys(keys);
|
|
1760
2181
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1761
2182
|
}
|
|
@@ -1763,23 +2184,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1763
2184
|
if (tags.length === 0) {
|
|
1764
2185
|
return;
|
|
1765
2186
|
}
|
|
2187
|
+
validateTags(tags);
|
|
1766
2188
|
await this.awaitStartup("invalidateByTags");
|
|
1767
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2189
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1768
2190
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2191
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1769
2192
|
await this.deleteKeys(keys);
|
|
1770
2193
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1771
2194
|
}
|
|
1772
2195
|
async invalidateByPattern(pattern) {
|
|
1773
|
-
|
|
2196
|
+
validatePattern(pattern);
|
|
1774
2197
|
await this.awaitStartup("invalidateByPattern");
|
|
1775
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2198
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2199
|
+
this.qualifyPattern(pattern),
|
|
2200
|
+
this.invalidationMaxKeys()
|
|
2201
|
+
);
|
|
1776
2202
|
await this.deleteKeys(keys);
|
|
1777
2203
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1778
2204
|
}
|
|
1779
2205
|
async invalidateByPrefix(prefix) {
|
|
1780
2206
|
await this.awaitStartup("invalidateByPrefix");
|
|
1781
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1782
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2207
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2208
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1783
2209
|
await this.deleteKeys(keys);
|
|
1784
2210
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1785
2211
|
}
|
|
@@ -1849,7 +2275,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1849
2275
|
* Returns `null` if the key does not exist in any layer.
|
|
1850
2276
|
*/
|
|
1851
2277
|
async inspect(key) {
|
|
1852
|
-
const userKey =
|
|
2278
|
+
const userKey = validateCacheKey(key);
|
|
1853
2279
|
const normalizedKey = this.qualifyKey(userKey);
|
|
1854
2280
|
await this.awaitStartup("inspect");
|
|
1855
2281
|
const foundInLayers = [];
|
|
@@ -1886,50 +2312,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1886
2312
|
}
|
|
1887
2313
|
async exportState() {
|
|
1888
2314
|
await this.awaitStartup("exportState");
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const keys = await layer.keys();
|
|
1895
|
-
for (const key of keys) {
|
|
1896
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
1897
|
-
if (exported.has(exportedKey)) {
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
1901
|
-
if (stored === null) {
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
exported.set(exportedKey, {
|
|
1905
|
-
key: exportedKey,
|
|
1906
|
-
value: stored,
|
|
1907
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
return [...exported.values()];
|
|
2315
|
+
const entries = [];
|
|
2316
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2317
|
+
entries.push(entry);
|
|
2318
|
+
});
|
|
2319
|
+
return entries;
|
|
1912
2320
|
}
|
|
1913
2321
|
async importState(entries) {
|
|
1914
2322
|
await this.awaitStartup("importState");
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2323
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2324
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2325
|
+
value: entry.value,
|
|
2326
|
+
ttl: entry.ttl
|
|
2327
|
+
}));
|
|
2328
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2329
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2330
|
+
await Promise.all(
|
|
2331
|
+
batch.map(async (entry) => {
|
|
2332
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2333
|
+
await this.tagIndex.touch(entry.key);
|
|
2334
|
+
})
|
|
2335
|
+
);
|
|
2336
|
+
}
|
|
1922
2337
|
}
|
|
1923
2338
|
async persistToFile(filePath) {
|
|
1924
2339
|
this.assertActive("persistToFile");
|
|
1925
|
-
const snapshot = await this.exportState();
|
|
1926
2340
|
const { promises: fs2 } = await import("fs");
|
|
1927
|
-
|
|
2341
|
+
const path = await import("path");
|
|
2342
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2343
|
+
const tempPath = path.join(
|
|
2344
|
+
path.dirname(targetPath),
|
|
2345
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2346
|
+
);
|
|
2347
|
+
let handle;
|
|
2348
|
+
try {
|
|
2349
|
+
handle = await fs2.open(tempPath, "wx");
|
|
2350
|
+
const openedHandle = handle;
|
|
2351
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2352
|
+
let wroteAny = false;
|
|
2353
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2354
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2355
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2356
|
+
wroteAny = true;
|
|
2357
|
+
});
|
|
2358
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2359
|
+
await openedHandle.close();
|
|
2360
|
+
handle = void 0;
|
|
2361
|
+
await fs2.rename(tempPath, targetPath);
|
|
2362
|
+
} catch (error) {
|
|
2363
|
+
await handle?.close().catch(() => void 0);
|
|
2364
|
+
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2365
|
+
throw error;
|
|
2366
|
+
}
|
|
1928
2367
|
}
|
|
1929
2368
|
async restoreFromFile(filePath) {
|
|
1930
2369
|
this.assertActive("restoreFromFile");
|
|
1931
|
-
const { promises: fs2 } = await import("fs");
|
|
1932
|
-
const
|
|
2370
|
+
const { promises: fs2, constants } = await import("fs");
|
|
2371
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2372
|
+
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2373
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2374
|
+
let raw;
|
|
2375
|
+
try {
|
|
2376
|
+
if (snapshotMaxBytes !== false) {
|
|
2377
|
+
const stat = await handle.stat();
|
|
2378
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2379
|
+
throw new Error(
|
|
2380
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2381
|
+
);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2385
|
+
} finally {
|
|
2386
|
+
await handle.close();
|
|
2387
|
+
}
|
|
1933
2388
|
let parsed;
|
|
1934
2389
|
try {
|
|
1935
2390
|
parsed = JSON.parse(raw);
|
|
@@ -1973,14 +2428,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1973
2428
|
await this.handleInvalidationMessage(message);
|
|
1974
2429
|
});
|
|
1975
2430
|
}
|
|
1976
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2431
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1977
2432
|
const fetchTask = async () => {
|
|
1978
2433
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1979
2434
|
if (secondHit.found) {
|
|
1980
2435
|
this.metricsCollector.increment("hits");
|
|
1981
2436
|
return secondHit.value;
|
|
1982
2437
|
}
|
|
1983
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2438
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1984
2439
|
};
|
|
1985
2440
|
const singleFlightTask = async () => {
|
|
1986
2441
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -1990,7 +2445,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1990
2445
|
key,
|
|
1991
2446
|
this.resolveSingleFlightOptions(),
|
|
1992
2447
|
fetchTask,
|
|
1993
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2448
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1994
2449
|
);
|
|
1995
2450
|
};
|
|
1996
2451
|
if (this.options.stampedePrevention === false) {
|
|
@@ -1998,7 +2453,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1998
2453
|
}
|
|
1999
2454
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2000
2455
|
}
|
|
2001
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2456
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2002
2457
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2003
2458
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2004
2459
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2012,9 +2467,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2012
2467
|
}
|
|
2013
2468
|
await this.sleep(pollIntervalMs);
|
|
2014
2469
|
}
|
|
2015
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2470
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2016
2471
|
}
|
|
2017
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2472
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2018
2473
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2019
2474
|
this.metricsCollector.increment("fetches");
|
|
2020
2475
|
const fetchStart = Date.now();
|
|
@@ -2035,6 +2490,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2035
2490
|
if (!this.shouldNegativeCache(options)) {
|
|
2036
2491
|
return null;
|
|
2037
2492
|
}
|
|
2493
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2494
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2495
|
+
key,
|
|
2496
|
+
expectedClearEpoch,
|
|
2497
|
+
clearEpoch: this.clearEpoch,
|
|
2498
|
+
expectedKeyEpoch,
|
|
2499
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2500
|
+
});
|
|
2501
|
+
return null;
|
|
2502
|
+
}
|
|
2038
2503
|
await this.storeEntry(key, "empty", null, options);
|
|
2039
2504
|
return null;
|
|
2040
2505
|
}
|
|
@@ -2047,11 +2512,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2047
2512
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2048
2513
|
}
|
|
2049
2514
|
}
|
|
2515
|
+
if (this.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2516
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2517
|
+
key,
|
|
2518
|
+
expectedClearEpoch,
|
|
2519
|
+
clearEpoch: this.clearEpoch,
|
|
2520
|
+
expectedKeyEpoch,
|
|
2521
|
+
keyEpoch: this.currentKeyEpoch(key)
|
|
2522
|
+
});
|
|
2523
|
+
return fetched;
|
|
2524
|
+
}
|
|
2050
2525
|
await this.storeEntry(key, "value", fetched, options);
|
|
2051
2526
|
return fetched;
|
|
2052
2527
|
}
|
|
2053
2528
|
async storeEntry(key, kind, value, options) {
|
|
2529
|
+
const clearEpoch = this.clearEpoch;
|
|
2530
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2054
2531
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2532
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2055
2535
|
if (options?.tags) {
|
|
2056
2536
|
await this.tagIndex.track(key, options.tags);
|
|
2057
2537
|
} else {
|
|
@@ -2066,6 +2546,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2066
2546
|
}
|
|
2067
2547
|
async writeBatch(entries) {
|
|
2068
2548
|
const now = Date.now();
|
|
2549
|
+
const clearEpoch = this.clearEpoch;
|
|
2550
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2069
2551
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2070
2552
|
const immediateOperations = [];
|
|
2071
2553
|
const deferredOperations = [];
|
|
@@ -2082,12 +2564,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2082
2564
|
}
|
|
2083
2565
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2084
2566
|
const operation = async () => {
|
|
2567
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2568
|
+
return;
|
|
2569
|
+
}
|
|
2570
|
+
const activeEntries = layerEntries.filter(
|
|
2571
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.currentKeyEpoch(entry.key)
|
|
2572
|
+
);
|
|
2573
|
+
if (activeEntries.length === 0) {
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2085
2576
|
try {
|
|
2086
2577
|
if (layer.setMany) {
|
|
2087
|
-
await layer.setMany(
|
|
2578
|
+
await layer.setMany(activeEntries);
|
|
2088
2579
|
return;
|
|
2089
2580
|
}
|
|
2090
|
-
await Promise.all(
|
|
2581
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2091
2582
|
} catch (error) {
|
|
2092
2583
|
await this.handleLayerFailure(layer, "write", error);
|
|
2093
2584
|
}
|
|
@@ -2100,7 +2591,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2100
2591
|
}
|
|
2101
2592
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2102
2593
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2594
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2103
2597
|
for (const entry of entries) {
|
|
2598
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2104
2601
|
if (entry.options?.tags) {
|
|
2105
2602
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2106
2603
|
} else {
|
|
@@ -2202,10 +2699,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2202
2699
|
}
|
|
2203
2700
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2204
2701
|
const now = Date.now();
|
|
2702
|
+
const clearEpoch = this.clearEpoch;
|
|
2703
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2205
2704
|
const immediateOperations = [];
|
|
2206
2705
|
const deferredOperations = [];
|
|
2207
2706
|
for (const layer of this.layers) {
|
|
2208
2707
|
const operation = async () => {
|
|
2708
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2209
2711
|
if (this.shouldSkipLayer(layer)) {
|
|
2210
2712
|
return;
|
|
2211
2713
|
}
|
|
@@ -2269,10 +2771,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2269
2771
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2270
2772
|
return;
|
|
2271
2773
|
}
|
|
2774
|
+
const clearEpoch = this.clearEpoch;
|
|
2775
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2272
2776
|
const refresh = (async () => {
|
|
2273
2777
|
this.metricsCollector.increment("refreshes");
|
|
2274
2778
|
try {
|
|
2275
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2779
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2276
2780
|
} catch (error) {
|
|
2277
2781
|
this.metricsCollector.increment("refreshErrors");
|
|
2278
2782
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2282,14 +2786,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2282
2786
|
})();
|
|
2283
2787
|
this.backgroundRefreshes.set(key, refresh);
|
|
2284
2788
|
}
|
|
2285
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2789
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2286
2790
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2287
2791
|
await this.fetchWithGuards(
|
|
2288
2792
|
key,
|
|
2289
2793
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2290
2794
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2291
2795
|
}),
|
|
2292
|
-
options
|
|
2796
|
+
options,
|
|
2797
|
+
expectedClearEpoch,
|
|
2798
|
+
expectedKeyEpoch
|
|
2293
2799
|
);
|
|
2294
2800
|
}
|
|
2295
2801
|
resolveSingleFlightOptions() {
|
|
@@ -2304,6 +2810,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2304
2810
|
if (keys.length === 0) {
|
|
2305
2811
|
return;
|
|
2306
2812
|
}
|
|
2813
|
+
this.bumpKeyEpochs(keys);
|
|
2307
2814
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2308
2815
|
for (const key of keys) {
|
|
2309
2816
|
await this.tagIndex.remove(key);
|
|
@@ -2326,21 +2833,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2326
2833
|
return;
|
|
2327
2834
|
}
|
|
2328
2835
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2329
|
-
if (localLayers.length === 0) {
|
|
2330
|
-
return;
|
|
2331
|
-
}
|
|
2332
2836
|
if (message.scope === "clear") {
|
|
2837
|
+
this.beginClearEpoch();
|
|
2333
2838
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2334
2839
|
await this.tagIndex.clear();
|
|
2335
2840
|
this.ttlResolver.clearProfiles();
|
|
2841
|
+
this.circuitBreakerManager.clear();
|
|
2336
2842
|
return;
|
|
2337
2843
|
}
|
|
2338
2844
|
const keys = message.keys ?? [];
|
|
2845
|
+
this.bumpKeyEpochs(keys);
|
|
2339
2846
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2340
2847
|
if (message.operation !== "write") {
|
|
2341
2848
|
for (const key of keys) {
|
|
2342
2849
|
await this.tagIndex.remove(key);
|
|
2343
2850
|
this.ttlResolver.deleteProfile(key);
|
|
2851
|
+
this.circuitBreakerManager.delete(key);
|
|
2344
2852
|
}
|
|
2345
2853
|
}
|
|
2346
2854
|
}
|
|
@@ -2446,6 +2954,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2446
2954
|
shouldWriteBehind(layer) {
|
|
2447
2955
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2448
2956
|
}
|
|
2957
|
+
beginClearEpoch() {
|
|
2958
|
+
this.clearEpoch += 1;
|
|
2959
|
+
this.keyEpochs.clear();
|
|
2960
|
+
this.writeBehindQueue.length = 0;
|
|
2961
|
+
}
|
|
2962
|
+
currentKeyEpoch(key) {
|
|
2963
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
2964
|
+
}
|
|
2965
|
+
bumpKeyEpochs(keys) {
|
|
2966
|
+
for (const key of keys) {
|
|
2967
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
2971
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
2972
|
+
return true;
|
|
2973
|
+
}
|
|
2974
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
2975
|
+
return true;
|
|
2976
|
+
}
|
|
2977
|
+
return false;
|
|
2978
|
+
}
|
|
2449
2979
|
async enqueueWriteBehind(operation) {
|
|
2450
2980
|
this.writeBehindQueue.push(operation);
|
|
2451
2981
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2572,122 +3102,54 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2572
3102
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2573
3103
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2574
3104
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3105
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3106
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3107
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3108
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3109
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3110
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3111
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3112
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3113
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3114
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3115
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3116
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3117
|
+
}
|
|
3118
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3119
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3120
|
+
}
|
|
3121
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3122
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3123
|
+
}
|
|
3124
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3125
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3126
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2588
3127
|
if (typeof this.options.generationCleanup === "object") {
|
|
2589
|
-
|
|
3128
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2590
3129
|
}
|
|
2591
3130
|
if (this.options.generation !== void 0) {
|
|
2592
|
-
|
|
3131
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2593
3132
|
}
|
|
2594
3133
|
}
|
|
2595
3134
|
validateWriteOptions(options) {
|
|
2596
3135
|
if (!options) {
|
|
2597
3136
|
return;
|
|
2598
3137
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
if (typeof value === "number") {
|
|
2615
|
-
this.validateNonNegativeNumber(name, value);
|
|
2616
|
-
return;
|
|
2617
|
-
}
|
|
2618
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2619
|
-
if (layerValue === void 0) {
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
validatePositiveNumber(name, value) {
|
|
2626
|
-
if (value === void 0) {
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2630
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
validateRateLimitOptions(name, options) {
|
|
2634
|
-
if (!options) {
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2638
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2639
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2640
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2641
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2642
|
-
}
|
|
2643
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2644
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
validateNonNegativeNumber(name, value) {
|
|
2648
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2649
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
validateCacheKey(key) {
|
|
2653
|
-
if (key.length === 0) {
|
|
2654
|
-
throw new Error("Cache key must not be empty.");
|
|
2655
|
-
}
|
|
2656
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2657
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2658
|
-
}
|
|
2659
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2660
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2661
|
-
}
|
|
2662
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2663
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2664
|
-
}
|
|
2665
|
-
return key;
|
|
2666
|
-
}
|
|
2667
|
-
validatePattern(pattern) {
|
|
2668
|
-
if (pattern.length === 0) {
|
|
2669
|
-
throw new Error("Pattern must not be empty.");
|
|
2670
|
-
}
|
|
2671
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2672
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2673
|
-
}
|
|
2674
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2675
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
2676
|
-
}
|
|
2677
|
-
}
|
|
2678
|
-
validateTtlPolicy(name, policy) {
|
|
2679
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
if ("alignTo" in policy) {
|
|
2683
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2684
|
-
return;
|
|
2685
|
-
}
|
|
2686
|
-
throw new Error(`${name} is invalid.`);
|
|
2687
|
-
}
|
|
2688
|
-
assertActive(operation) {
|
|
2689
|
-
if (this.isDisconnecting) {
|
|
2690
|
-
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
3138
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3139
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3140
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3141
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3142
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3143
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3144
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3145
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3146
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3147
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3148
|
+
validateTags(options.tags);
|
|
3149
|
+
}
|
|
3150
|
+
assertActive(operation) {
|
|
3151
|
+
if (this.isDisconnecting) {
|
|
3152
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2691
3153
|
}
|
|
2692
3154
|
}
|
|
2693
3155
|
async awaitStartup(operation) {
|
|
@@ -2695,24 +3157,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2695
3157
|
await this.startup;
|
|
2696
3158
|
this.assertActive(operation);
|
|
2697
3159
|
}
|
|
2698
|
-
serializeOptions(options) {
|
|
2699
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2700
|
-
}
|
|
2701
|
-
validateAdaptiveTtlOptions(options) {
|
|
2702
|
-
if (!options || options === true) {
|
|
2703
|
-
return;
|
|
2704
|
-
}
|
|
2705
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2706
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2707
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2708
|
-
}
|
|
2709
|
-
validateCircuitBreakerOptions(options) {
|
|
2710
|
-
if (!options) {
|
|
2711
|
-
return;
|
|
2712
|
-
}
|
|
2713
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2714
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2715
|
-
}
|
|
2716
3160
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2717
3161
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2718
3162
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2780,18 +3224,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2780
3224
|
this.emit("error", { operation, ...context });
|
|
2781
3225
|
}
|
|
2782
3226
|
}
|
|
2783
|
-
serializeKeyPart(value) {
|
|
2784
|
-
if (typeof value === "string") {
|
|
2785
|
-
return `s:${value}`;
|
|
2786
|
-
}
|
|
2787
|
-
if (typeof value === "number") {
|
|
2788
|
-
return `n:${value}`;
|
|
2789
|
-
}
|
|
2790
|
-
if (typeof value === "boolean") {
|
|
2791
|
-
return `b:${value}`;
|
|
2792
|
-
}
|
|
2793
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2794
|
-
}
|
|
2795
3227
|
isCacheSnapshotEntries(value) {
|
|
2796
3228
|
return Array.isArray(value) && value.every((entry) => {
|
|
2797
3229
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2804,54 +3236,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2804
3236
|
sanitizeSnapshotValue(value) {
|
|
2805
3237
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2806
3238
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3239
|
+
snapshotMaxBytes() {
|
|
3240
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3241
|
+
}
|
|
3242
|
+
snapshotMaxEntries() {
|
|
3243
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3244
|
+
}
|
|
3245
|
+
invalidationMaxKeys() {
|
|
3246
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3247
|
+
}
|
|
3248
|
+
async collectKeysForTag(tag) {
|
|
3249
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3250
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3251
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3252
|
+
keys.add(key);
|
|
3253
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3254
|
+
});
|
|
3255
|
+
return [...keys];
|
|
2813
3256
|
}
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
if (baseDir !== false) {
|
|
2818
|
-
const relative = path.relative(baseDir, resolved);
|
|
2819
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2820
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2821
|
-
}
|
|
3257
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3258
|
+
keys.add(key);
|
|
3259
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2822
3260
|
}
|
|
2823
|
-
return
|
|
3261
|
+
return [...keys];
|
|
2824
3262
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3263
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3264
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3265
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3266
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2828
3267
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3268
|
+
}
|
|
3269
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3270
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3271
|
+
for (const layer of this.layers) {
|
|
3272
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3273
|
+
continue;
|
|
3274
|
+
}
|
|
3275
|
+
const visitKey = async (key) => {
|
|
3276
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3277
|
+
if (exported.has(exportedKey)) {
|
|
3278
|
+
return;
|
|
2833
3279
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3280
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3281
|
+
if (stored === null) {
|
|
3282
|
+
return;
|
|
3283
|
+
}
|
|
3284
|
+
exported.add(exportedKey);
|
|
3285
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3286
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3287
|
+
}
|
|
3288
|
+
await visitor({
|
|
3289
|
+
key: exportedKey,
|
|
3290
|
+
value: stored,
|
|
3291
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3292
|
+
});
|
|
3293
|
+
};
|
|
3294
|
+
if (layer.forEachKey) {
|
|
3295
|
+
await layer.forEachKey(visitKey);
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
const keys = await layer.keys?.();
|
|
3299
|
+
for (const key of keys ?? []) {
|
|
3300
|
+
await visitKey(key);
|
|
3301
|
+
}
|
|
2837
3302
|
}
|
|
2838
|
-
return value;
|
|
2839
3303
|
}
|
|
2840
3304
|
};
|
|
2841
|
-
function createInstanceId() {
|
|
2842
|
-
if (globalThis.crypto?.randomUUID) {
|
|
2843
|
-
return globalThis.crypto.randomUUID();
|
|
2844
|
-
}
|
|
2845
|
-
const bytes = new Uint8Array(16);
|
|
2846
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
2847
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
2848
|
-
} else {
|
|
2849
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2850
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2854
|
-
}
|
|
2855
3305
|
|
|
2856
3306
|
// src/invalidation/RedisInvalidationBus.ts
|
|
2857
3307
|
var RedisInvalidationBus = class {
|
|
@@ -2930,15 +3380,24 @@ var RedisInvalidationBus = class {
|
|
|
2930
3380
|
}
|
|
2931
3381
|
};
|
|
2932
3382
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2933
|
-
|
|
3383
|
+
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3384
|
+
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3385
|
+
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3386
|
+
state.count += 1;
|
|
3387
|
+
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3388
|
+
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3389
|
+
}
|
|
3390
|
+
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3391
|
+
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3392
|
+
}
|
|
2934
3393
|
if (Array.isArray(value)) {
|
|
2935
|
-
return value.map(sanitizeJsonValue2);
|
|
3394
|
+
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
2936
3395
|
}
|
|
2937
3396
|
if (value && typeof value === "object") {
|
|
2938
3397
|
const result = /* @__PURE__ */ Object.create(null);
|
|
2939
3398
|
for (const key of Object.keys(value)) {
|
|
2940
3399
|
if (!DANGEROUS_KEYS.has(key)) {
|
|
2941
|
-
result[key] = sanitizeJsonValue2(value[key]);
|
|
3400
|
+
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
2942
3401
|
}
|
|
2943
3402
|
}
|
|
2944
3403
|
return result;
|
|
@@ -2992,6 +3451,17 @@ var RedisTagIndex = class {
|
|
|
2992
3451
|
async keysForTag(tag) {
|
|
2993
3452
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
2994
3453
|
}
|
|
3454
|
+
async forEachKeyForTag(tag, visitor) {
|
|
3455
|
+
let cursor = "0";
|
|
3456
|
+
const tagKey = this.tagKeysKey(tag);
|
|
3457
|
+
do {
|
|
3458
|
+
const [nextCursor, keys] = await this.client.sscan(tagKey, cursor, "COUNT", this.scanCount);
|
|
3459
|
+
cursor = nextCursor;
|
|
3460
|
+
for (const key of keys) {
|
|
3461
|
+
await visitor(key);
|
|
3462
|
+
}
|
|
3463
|
+
} while (cursor !== "0");
|
|
3464
|
+
}
|
|
2995
3465
|
async keysForPrefix(prefix) {
|
|
2996
3466
|
const matches = [];
|
|
2997
3467
|
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
@@ -3004,6 +3474,20 @@ var RedisTagIndex = class {
|
|
|
3004
3474
|
}
|
|
3005
3475
|
return matches;
|
|
3006
3476
|
}
|
|
3477
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
3478
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
3479
|
+
let cursor = "0";
|
|
3480
|
+
do {
|
|
3481
|
+
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
3482
|
+
cursor = nextCursor;
|
|
3483
|
+
for (const key of keys) {
|
|
3484
|
+
if (key.startsWith(prefix)) {
|
|
3485
|
+
await visitor(key);
|
|
3486
|
+
}
|
|
3487
|
+
}
|
|
3488
|
+
} while (cursor !== "0");
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3007
3491
|
async tagsForKey(key) {
|
|
3008
3492
|
return this.client.smembers(this.keyTagsKey(key));
|
|
3009
3493
|
}
|
|
@@ -3026,6 +3510,27 @@ var RedisTagIndex = class {
|
|
|
3026
3510
|
}
|
|
3027
3511
|
return matches;
|
|
3028
3512
|
}
|
|
3513
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
3514
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
3515
|
+
let cursor = "0";
|
|
3516
|
+
do {
|
|
3517
|
+
const [nextCursor, keys] = await this.client.sscan(
|
|
3518
|
+
knownKeysKey,
|
|
3519
|
+
cursor,
|
|
3520
|
+
"MATCH",
|
|
3521
|
+
pattern,
|
|
3522
|
+
"COUNT",
|
|
3523
|
+
this.scanCount
|
|
3524
|
+
);
|
|
3525
|
+
cursor = nextCursor;
|
|
3526
|
+
for (const key of keys) {
|
|
3527
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
3528
|
+
await visitor(key);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
} while (cursor !== "0");
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3029
3534
|
async clear() {
|
|
3030
3535
|
const indexKeys = await this.scanIndexKeys();
|
|
3031
3536
|
if (indexKeys.length === 0) {
|
|
@@ -3081,12 +3586,18 @@ function simpleHash(value) {
|
|
|
3081
3586
|
}
|
|
3082
3587
|
|
|
3083
3588
|
// src/http/createCacheStatsHandler.ts
|
|
3084
|
-
function createCacheStatsHandler(cache) {
|
|
3085
|
-
return async (
|
|
3086
|
-
response.statusCode = 200;
|
|
3589
|
+
function createCacheStatsHandler(cache, options = {}) {
|
|
3590
|
+
return async (request, response) => {
|
|
3087
3591
|
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
3088
3592
|
response.setHeader?.("cache-control", "no-store");
|
|
3089
3593
|
response.setHeader?.("x-content-type-options", "nosniff");
|
|
3594
|
+
const isAuthorized = options.allowPublicAccess === true || (options.authorize ? await options.authorize(request) : false);
|
|
3595
|
+
if (!isAuthorized) {
|
|
3596
|
+
response.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3597
|
+
response.end(JSON.stringify({ error: "Forbidden" }));
|
|
3598
|
+
return;
|
|
3599
|
+
}
|
|
3600
|
+
response.statusCode = 200;
|
|
3090
3601
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
3091
3602
|
};
|
|
3092
3603
|
}
|
|
@@ -3121,7 +3632,26 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
3121
3632
|
return async (fastify) => {
|
|
3122
3633
|
fastify.decorate("cache", cache);
|
|
3123
3634
|
if (options.exposeStatsRoute === true && fastify.get) {
|
|
3124
|
-
fastify.get(options.statsPath ?? "/cache/stats", async () =>
|
|
3635
|
+
fastify.get(options.statsPath ?? "/cache/stats", async (request, reply) => {
|
|
3636
|
+
const isAuthorized = options.allowPublicStatsRoute === true || (options.authorizeStatsRoute ? await options.authorizeStatsRoute(request) : false);
|
|
3637
|
+
reply.header?.("cache-control", "no-store");
|
|
3638
|
+
reply.header?.("x-content-type-options", "nosniff");
|
|
3639
|
+
if (!isAuthorized) {
|
|
3640
|
+
reply.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3641
|
+
const body2 = { error: "Forbidden" };
|
|
3642
|
+
if (reply.send) {
|
|
3643
|
+
reply.send(body2);
|
|
3644
|
+
return;
|
|
3645
|
+
}
|
|
3646
|
+
return body2;
|
|
3647
|
+
}
|
|
3648
|
+
const body = cache.getStats();
|
|
3649
|
+
if (reply.send) {
|
|
3650
|
+
reply.send(body);
|
|
3651
|
+
return;
|
|
3652
|
+
}
|
|
3653
|
+
return body;
|
|
3654
|
+
});
|
|
3125
3655
|
}
|
|
3126
3656
|
};
|
|
3127
3657
|
}
|
|
@@ -3136,6 +3666,10 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3136
3666
|
next();
|
|
3137
3667
|
return;
|
|
3138
3668
|
}
|
|
3669
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3670
|
+
next();
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3139
3673
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3140
3674
|
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
|
|
3141
3675
|
const cached = await cache.get(key, void 0, options);
|
|
@@ -3180,6 +3714,11 @@ function normalizeUrl(url) {
|
|
|
3180
3714
|
|
|
3181
3715
|
// src/integrations/graphql.ts
|
|
3182
3716
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
3717
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3718
|
+
throw new Error(
|
|
3719
|
+
"cacheGraphqlResolver requires a keyResolver or allowImplicitContextCaching=true because resolver output may depend on request context."
|
|
3720
|
+
);
|
|
3721
|
+
}
|
|
3183
3722
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
3184
3723
|
...options,
|
|
3185
3724
|
keyResolver: options.keyResolver
|
|
@@ -3196,14 +3735,17 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
3196
3735
|
await next();
|
|
3197
3736
|
return;
|
|
3198
3737
|
}
|
|
3738
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3739
|
+
await next();
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3199
3742
|
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
3200
3743
|
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
|
|
3201
3744
|
const cached = await cache.get(key, void 0, options);
|
|
3202
3745
|
if (cached !== null) {
|
|
3203
3746
|
context.header?.("x-cache", "HIT");
|
|
3204
3747
|
context.header?.("content-type", "application/json; charset=utf-8");
|
|
3205
|
-
context.json(cached);
|
|
3206
|
-
return;
|
|
3748
|
+
return context.json(cached);
|
|
3207
3749
|
}
|
|
3208
3750
|
const originalJson = context.json.bind(context);
|
|
3209
3751
|
context.json = (body, status) => {
|
|
@@ -3293,6 +3835,11 @@ function instrument(name, tracer, method, attributes) {
|
|
|
3293
3835
|
|
|
3294
3836
|
// src/integrations/trpc.ts
|
|
3295
3837
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
3838
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3839
|
+
throw new Error(
|
|
3840
|
+
"createTrpcCacheMiddleware requires a keyResolver or allowImplicitContextCaching=true because procedure output may depend on request context."
|
|
3841
|
+
);
|
|
3842
|
+
}
|
|
3296
3843
|
return async (context) => {
|
|
3297
3844
|
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
3298
3845
|
let didFetch = false;
|
|
@@ -3432,6 +3979,12 @@ var MemoryLayer = class {
|
|
|
3432
3979
|
this.pruneExpired();
|
|
3433
3980
|
return [...this.entries.keys()];
|
|
3434
3981
|
}
|
|
3982
|
+
async forEachKey(visitor) {
|
|
3983
|
+
this.pruneExpired();
|
|
3984
|
+
for (const key of this.entries.keys()) {
|
|
3985
|
+
await visitor(key);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3435
3988
|
exportState() {
|
|
3436
3989
|
this.pruneExpired();
|
|
3437
3990
|
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
@@ -3499,13 +4052,12 @@ var MemoryLayer = class {
|
|
|
3499
4052
|
};
|
|
3500
4053
|
|
|
3501
4054
|
// src/layers/RedisLayer.ts
|
|
4055
|
+
var import_node_stream = require("stream");
|
|
3502
4056
|
var import_node_util = require("util");
|
|
3503
4057
|
var import_node_zlib = require("zlib");
|
|
3504
4058
|
var BATCH_DELETE_SIZE = 500;
|
|
3505
4059
|
var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
|
|
3506
|
-
var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
|
|
3507
4060
|
var brotliCompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliCompress);
|
|
3508
|
-
var brotliDecompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliDecompress);
|
|
3509
4061
|
var RedisLayer = class {
|
|
3510
4062
|
name;
|
|
3511
4063
|
defaultTtl;
|
|
@@ -3613,8 +4165,18 @@ var RedisLayer = class {
|
|
|
3613
4165
|
return remaining;
|
|
3614
4166
|
}
|
|
3615
4167
|
async size() {
|
|
3616
|
-
|
|
3617
|
-
|
|
4168
|
+
if (!this.prefix) {
|
|
4169
|
+
return this.client.dbsize();
|
|
4170
|
+
}
|
|
4171
|
+
const pattern = `${this.prefix}*`;
|
|
4172
|
+
let cursor = "0";
|
|
4173
|
+
let count = 0;
|
|
4174
|
+
do {
|
|
4175
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
4176
|
+
cursor = nextCursor;
|
|
4177
|
+
count += keys.length;
|
|
4178
|
+
} while (cursor !== "0");
|
|
4179
|
+
return count;
|
|
3618
4180
|
}
|
|
3619
4181
|
async ping() {
|
|
3620
4182
|
try {
|
|
@@ -3660,6 +4222,17 @@ var RedisLayer = class {
|
|
|
3660
4222
|
}
|
|
3661
4223
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
3662
4224
|
}
|
|
4225
|
+
async forEachKey(visitor) {
|
|
4226
|
+
const pattern = `${this.prefix}*`;
|
|
4227
|
+
let cursor = "0";
|
|
4228
|
+
do {
|
|
4229
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
4230
|
+
cursor = nextCursor;
|
|
4231
|
+
for (const key of keys) {
|
|
4232
|
+
await visitor(this.prefix ? key.slice(this.prefix.length) : key);
|
|
4233
|
+
}
|
|
4234
|
+
} while (cursor !== "0");
|
|
4235
|
+
}
|
|
3663
4236
|
async scanKeys(pattern) {
|
|
3664
4237
|
const matches = [];
|
|
3665
4238
|
let cursor = "0";
|
|
@@ -3674,7 +4247,13 @@ var RedisLayer = class {
|
|
|
3674
4247
|
return `${this.prefix}${key}`;
|
|
3675
4248
|
}
|
|
3676
4249
|
async deserializeOrDelete(key, payload) {
|
|
3677
|
-
|
|
4250
|
+
let decodedPayload;
|
|
4251
|
+
try {
|
|
4252
|
+
decodedPayload = await this.decodePayload(payload);
|
|
4253
|
+
} catch {
|
|
4254
|
+
await this.deleteCorruptedKey(key);
|
|
4255
|
+
return null;
|
|
4256
|
+
}
|
|
3678
4257
|
for (const serializer of this.serializers) {
|
|
3679
4258
|
try {
|
|
3680
4259
|
const value = serializer.deserialize(decodedPayload);
|
|
@@ -3685,12 +4264,15 @@ var RedisLayer = class {
|
|
|
3685
4264
|
} catch {
|
|
3686
4265
|
}
|
|
3687
4266
|
}
|
|
4267
|
+
await this.deleteCorruptedKey(key);
|
|
4268
|
+
return null;
|
|
4269
|
+
}
|
|
4270
|
+
async deleteCorruptedKey(key) {
|
|
3688
4271
|
try {
|
|
3689
4272
|
await this.client.del(this.withPrefix(key));
|
|
3690
4273
|
} catch (deleteError) {
|
|
3691
4274
|
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
3692
4275
|
}
|
|
3693
|
-
return null;
|
|
3694
4276
|
}
|
|
3695
4277
|
async rewriteWithPrimarySerializer(key, value) {
|
|
3696
4278
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -3737,31 +4319,72 @@ var RedisLayer = class {
|
|
|
3737
4319
|
return payload;
|
|
3738
4320
|
}
|
|
3739
4321
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
3740
|
-
|
|
3741
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3742
|
-
throw new Error(
|
|
3743
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3744
|
-
);
|
|
3745
|
-
}
|
|
3746
|
-
return decompressed;
|
|
4322
|
+
return this.decompressWithLimit((0, import_node_zlib.createGunzip)(), payload.subarray(10));
|
|
3747
4323
|
}
|
|
3748
4324
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
3749
|
-
|
|
3750
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3751
|
-
throw new Error(
|
|
3752
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3753
|
-
);
|
|
3754
|
-
}
|
|
3755
|
-
return decompressed;
|
|
4325
|
+
return this.decompressWithLimit((0, import_node_zlib.createBrotliDecompress)(), payload.subarray(12));
|
|
3756
4326
|
}
|
|
3757
4327
|
return payload;
|
|
3758
4328
|
}
|
|
4329
|
+
async decompressWithLimit(decompressor, payload) {
|
|
4330
|
+
return new Promise((resolve2, reject) => {
|
|
4331
|
+
const source = import_node_stream.Readable.from(payload);
|
|
4332
|
+
const chunks = [];
|
|
4333
|
+
let totalBytes = 0;
|
|
4334
|
+
let settled = false;
|
|
4335
|
+
const cleanup = () => {
|
|
4336
|
+
decompressor.removeAllListeners();
|
|
4337
|
+
};
|
|
4338
|
+
const fail = (error) => {
|
|
4339
|
+
if (settled) {
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
settled = true;
|
|
4343
|
+
cleanup();
|
|
4344
|
+
source.unpipe(decompressor);
|
|
4345
|
+
source.destroy();
|
|
4346
|
+
decompressor.destroy();
|
|
4347
|
+
reject(error);
|
|
4348
|
+
};
|
|
4349
|
+
decompressor.on("data", (chunk) => {
|
|
4350
|
+
const normalized = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4351
|
+
totalBytes += normalized.byteLength;
|
|
4352
|
+
if (totalBytes > this.decompressionMaxBytes) {
|
|
4353
|
+
fail(
|
|
4354
|
+
new Error(
|
|
4355
|
+
`Decompressed payload (${totalBytes} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
4356
|
+
)
|
|
4357
|
+
);
|
|
4358
|
+
return;
|
|
4359
|
+
}
|
|
4360
|
+
chunks.push(normalized);
|
|
4361
|
+
});
|
|
4362
|
+
decompressor.once("error", (error) => {
|
|
4363
|
+
if (settled) {
|
|
4364
|
+
return;
|
|
4365
|
+
}
|
|
4366
|
+
settled = true;
|
|
4367
|
+
cleanup();
|
|
4368
|
+
reject(error);
|
|
4369
|
+
});
|
|
4370
|
+
decompressor.once("end", () => {
|
|
4371
|
+
if (settled) {
|
|
4372
|
+
return;
|
|
4373
|
+
}
|
|
4374
|
+
settled = true;
|
|
4375
|
+
cleanup();
|
|
4376
|
+
resolve2(Buffer.concat(chunks));
|
|
4377
|
+
});
|
|
4378
|
+
source.pipe(decompressor);
|
|
4379
|
+
});
|
|
4380
|
+
}
|
|
3759
4381
|
};
|
|
3760
4382
|
|
|
3761
4383
|
// src/layers/DiskLayer.ts
|
|
3762
4384
|
var import_node_crypto = require("crypto");
|
|
3763
4385
|
var import_node_fs = require("fs");
|
|
3764
4386
|
var import_node_path = require("path");
|
|
4387
|
+
var FILE_SCAN_CONCURRENCY = 32;
|
|
3765
4388
|
var DiskLayer = class {
|
|
3766
4389
|
name;
|
|
3767
4390
|
defaultTtl;
|
|
@@ -3769,6 +4392,7 @@ var DiskLayer = class {
|
|
|
3769
4392
|
directory;
|
|
3770
4393
|
serializer;
|
|
3771
4394
|
maxFiles;
|
|
4395
|
+
maxEntryBytes;
|
|
3772
4396
|
writeQueue = Promise.resolve();
|
|
3773
4397
|
constructor(options) {
|
|
3774
4398
|
this.directory = this.resolveDirectory(options.directory);
|
|
@@ -3776,16 +4400,15 @@ var DiskLayer = class {
|
|
|
3776
4400
|
this.name = options.name ?? "disk";
|
|
3777
4401
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
3778
4402
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
4403
|
+
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
3779
4404
|
}
|
|
3780
4405
|
async get(key) {
|
|
3781
4406
|
return unwrapStoredValue(await this.getEntry(key));
|
|
3782
4407
|
}
|
|
3783
4408
|
async getEntry(key) {
|
|
3784
4409
|
const filePath = this.keyToPath(key);
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3788
|
-
} catch {
|
|
4410
|
+
const raw = await this.readEntryFile(filePath);
|
|
4411
|
+
if (raw === null) {
|
|
3789
4412
|
return null;
|
|
3790
4413
|
}
|
|
3791
4414
|
let entry;
|
|
@@ -3836,10 +4459,8 @@ var DiskLayer = class {
|
|
|
3836
4459
|
}
|
|
3837
4460
|
async ttl(key) {
|
|
3838
4461
|
const filePath = this.keyToPath(key);
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3842
|
-
} catch {
|
|
4462
|
+
const raw = await this.readEntryFile(filePath);
|
|
4463
|
+
if (raw === null) {
|
|
3843
4464
|
return null;
|
|
3844
4465
|
}
|
|
3845
4466
|
let entry;
|
|
@@ -3863,7 +4484,7 @@ var DiskLayer = class {
|
|
|
3863
4484
|
}
|
|
3864
4485
|
async deleteMany(keys) {
|
|
3865
4486
|
await this.enqueueWrite(async () => {
|
|
3866
|
-
await
|
|
4487
|
+
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
3867
4488
|
});
|
|
3868
4489
|
}
|
|
3869
4490
|
async clear() {
|
|
@@ -3874,8 +4495,8 @@ var DiskLayer = class {
|
|
|
3874
4495
|
} catch {
|
|
3875
4496
|
return;
|
|
3876
4497
|
}
|
|
3877
|
-
await
|
|
3878
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) =>
|
|
4498
|
+
await this.deletePathsWithConcurrency(
|
|
4499
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
|
|
3879
4500
|
);
|
|
3880
4501
|
});
|
|
3881
4502
|
}
|
|
@@ -3884,42 +4505,23 @@ var DiskLayer = class {
|
|
|
3884
4505
|
* Expired entries are skipped and cleaned up during the scan.
|
|
3885
4506
|
*/
|
|
3886
4507
|
async keys() {
|
|
3887
|
-
let entries;
|
|
3888
|
-
try {
|
|
3889
|
-
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3890
|
-
} catch {
|
|
3891
|
-
return [];
|
|
3892
|
-
}
|
|
3893
|
-
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3894
4508
|
const keys = [];
|
|
3895
|
-
await
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
let raw;
|
|
3899
|
-
try {
|
|
3900
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3901
|
-
} catch {
|
|
3902
|
-
return;
|
|
3903
|
-
}
|
|
3904
|
-
let entry;
|
|
3905
|
-
try {
|
|
3906
|
-
entry = this.deserializeEntry(raw);
|
|
3907
|
-
} catch {
|
|
3908
|
-
await this.safeDelete(filePath);
|
|
3909
|
-
return;
|
|
3910
|
-
}
|
|
3911
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3912
|
-
await this.safeDelete(filePath);
|
|
3913
|
-
return;
|
|
3914
|
-
}
|
|
3915
|
-
keys.push(entry.key);
|
|
3916
|
-
})
|
|
3917
|
-
);
|
|
4509
|
+
await this.scanEntries(async (entry) => {
|
|
4510
|
+
keys.push(entry.key);
|
|
4511
|
+
});
|
|
3918
4512
|
return keys;
|
|
3919
4513
|
}
|
|
4514
|
+
async forEachKey(visitor) {
|
|
4515
|
+
await this.scanEntries(async (entry) => {
|
|
4516
|
+
await visitor(entry.key);
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
3920
4519
|
async size() {
|
|
3921
|
-
|
|
3922
|
-
|
|
4520
|
+
let count = 0;
|
|
4521
|
+
await this.scanEntries(async () => {
|
|
4522
|
+
count += 1;
|
|
4523
|
+
});
|
|
4524
|
+
return count;
|
|
3923
4525
|
}
|
|
3924
4526
|
async ping() {
|
|
3925
4527
|
try {
|
|
@@ -3953,6 +4555,113 @@ var DiskLayer = class {
|
|
|
3953
4555
|
}
|
|
3954
4556
|
return maxFiles;
|
|
3955
4557
|
}
|
|
4558
|
+
normalizeMaxEntryBytes(maxEntryBytes) {
|
|
4559
|
+
if (maxEntryBytes === false) {
|
|
4560
|
+
return false;
|
|
4561
|
+
}
|
|
4562
|
+
const normalized = maxEntryBytes ?? 16 * 1024 * 1024;
|
|
4563
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
4564
|
+
throw new Error("DiskLayer.maxEntryBytes must be a positive number or false.");
|
|
4565
|
+
}
|
|
4566
|
+
return normalized;
|
|
4567
|
+
}
|
|
4568
|
+
async readEntryFile(filePath) {
|
|
4569
|
+
let handle;
|
|
4570
|
+
try {
|
|
4571
|
+
handle = await import_node_fs.promises.open(filePath, "r");
|
|
4572
|
+
return await this.readHandleWithLimit(handle);
|
|
4573
|
+
} catch {
|
|
4574
|
+
await this.safeDelete(filePath);
|
|
4575
|
+
return null;
|
|
4576
|
+
} finally {
|
|
4577
|
+
await handle?.close().catch(() => void 0);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
async readHandleWithLimit(handle) {
|
|
4581
|
+
if (this.maxEntryBytes === false) {
|
|
4582
|
+
return handle.readFile();
|
|
4583
|
+
}
|
|
4584
|
+
const stat = await handle.stat();
|
|
4585
|
+
if (stat.size > this.maxEntryBytes) {
|
|
4586
|
+
throw new Error(`DiskLayer entry exceeds maxEntryBytes limit (${stat.size} bytes > ${this.maxEntryBytes} bytes).`);
|
|
4587
|
+
}
|
|
4588
|
+
const chunks = [];
|
|
4589
|
+
let totalBytes = 0;
|
|
4590
|
+
let position = 0;
|
|
4591
|
+
while (true) {
|
|
4592
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, this.maxEntryBytes - totalBytes + 1));
|
|
4593
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
4594
|
+
if (bytesRead === 0) {
|
|
4595
|
+
break;
|
|
4596
|
+
}
|
|
4597
|
+
totalBytes += bytesRead;
|
|
4598
|
+
if (totalBytes > this.maxEntryBytes) {
|
|
4599
|
+
throw new Error(
|
|
4600
|
+
`DiskLayer entry exceeds maxEntryBytes limit (${totalBytes} bytes > ${this.maxEntryBytes} bytes).`
|
|
4601
|
+
);
|
|
4602
|
+
}
|
|
4603
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
4604
|
+
position += bytesRead;
|
|
4605
|
+
}
|
|
4606
|
+
return Buffer.concat(chunks);
|
|
4607
|
+
}
|
|
4608
|
+
async scanEntries(visitor) {
|
|
4609
|
+
let entries;
|
|
4610
|
+
try {
|
|
4611
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
4612
|
+
} catch {
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
4616
|
+
let nextIndex = 0;
|
|
4617
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, lcFiles.length);
|
|
4618
|
+
await Promise.all(
|
|
4619
|
+
Array.from({ length: workerCount }, async () => {
|
|
4620
|
+
while (true) {
|
|
4621
|
+
const currentIndex = nextIndex;
|
|
4622
|
+
nextIndex += 1;
|
|
4623
|
+
const name = lcFiles[currentIndex];
|
|
4624
|
+
if (name === void 0) {
|
|
4625
|
+
return;
|
|
4626
|
+
}
|
|
4627
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
4628
|
+
const raw = await this.readEntryFile(filePath);
|
|
4629
|
+
if (raw === null) {
|
|
4630
|
+
continue;
|
|
4631
|
+
}
|
|
4632
|
+
let entry;
|
|
4633
|
+
try {
|
|
4634
|
+
entry = this.deserializeEntry(raw);
|
|
4635
|
+
} catch {
|
|
4636
|
+
await this.safeDelete(filePath);
|
|
4637
|
+
continue;
|
|
4638
|
+
}
|
|
4639
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
4640
|
+
await this.safeDelete(filePath);
|
|
4641
|
+
continue;
|
|
4642
|
+
}
|
|
4643
|
+
await visitor(entry);
|
|
4644
|
+
}
|
|
4645
|
+
})
|
|
4646
|
+
);
|
|
4647
|
+
}
|
|
4648
|
+
async deletePathsWithConcurrency(paths) {
|
|
4649
|
+
let nextIndex = 0;
|
|
4650
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, paths.length);
|
|
4651
|
+
await Promise.all(
|
|
4652
|
+
Array.from({ length: workerCount }, async () => {
|
|
4653
|
+
while (true) {
|
|
4654
|
+
const currentIndex = nextIndex;
|
|
4655
|
+
nextIndex += 1;
|
|
4656
|
+
const filePath = paths[currentIndex];
|
|
4657
|
+
if (filePath === void 0) {
|
|
4658
|
+
return;
|
|
4659
|
+
}
|
|
4660
|
+
await this.safeDelete(filePath);
|
|
4661
|
+
}
|
|
4662
|
+
})
|
|
4663
|
+
);
|
|
4664
|
+
}
|
|
3956
4665
|
deserializeEntry(raw) {
|
|
3957
4666
|
const entry = this.serializer.deserialize(raw);
|
|
3958
4667
|
if (!isDiskEntry(entry)) {
|
|
@@ -4089,18 +4798,27 @@ var MemcachedLayer = class {
|
|
|
4089
4798
|
// src/serialization/MsgpackSerializer.ts
|
|
4090
4799
|
var import_msgpack = require("@msgpack/msgpack");
|
|
4091
4800
|
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4801
|
+
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4802
|
+
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4092
4803
|
var MsgpackSerializer = class {
|
|
4093
4804
|
serialize(value) {
|
|
4094
4805
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4095
4806
|
}
|
|
4096
4807
|
deserialize(payload) {
|
|
4097
4808
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
4098
|
-
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized));
|
|
4809
|
+
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
|
|
4099
4810
|
}
|
|
4100
4811
|
};
|
|
4101
|
-
function sanitizeMsgpackValue(value) {
|
|
4812
|
+
function sanitizeMsgpackValue(value, depth, state) {
|
|
4813
|
+
state.count += 1;
|
|
4814
|
+
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4815
|
+
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4816
|
+
}
|
|
4817
|
+
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4818
|
+
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4819
|
+
}
|
|
4102
4820
|
if (Array.isArray(value)) {
|
|
4103
|
-
return value.map((entry) => sanitizeMsgpackValue(entry));
|
|
4821
|
+
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4104
4822
|
}
|
|
4105
4823
|
if (!isPlainObject2(value)) {
|
|
4106
4824
|
return value;
|
|
@@ -4110,7 +4828,7 @@ function sanitizeMsgpackValue(value) {
|
|
|
4110
4828
|
if (DANGEROUS_KEYS2.has(key)) {
|
|
4111
4829
|
continue;
|
|
4112
4830
|
}
|
|
4113
|
-
sanitized[key] = sanitizeMsgpackValue(entry);
|
|
4831
|
+
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4114
4832
|
}
|
|
4115
4833
|
return sanitized;
|
|
4116
4834
|
}
|