layercache 1.2.4 → 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 -906
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-KOYGHLVP.js → chunk-GJBKCFE6.js} +65 -10
- package/dist/cli.cjs +85 -19
- package/dist/cli.js +4 -18
- package/dist/{edge-Dw97n89L.d.ts → edge-DLstcDMn.d.cts} +32 -13
- package/dist/{edge-Dw97n89L.d.cts → edge-DLstcDMn.d.ts} +32 -13
- package/dist/edge.cjs +101 -12
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1160 -350
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +1017 -342
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +793 -270
- package/packages/nestjs/dist/index.d.cts +23 -12
- package/packages/nestjs/dist/index.d.ts +23 -12
- package/packages/nestjs/dist/index.js +793 -270
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
|
);
|
|
@@ -176,11 +192,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
176
192
|
* ```
|
|
177
193
|
*/
|
|
178
194
|
namespace(childPrefix) {
|
|
195
|
+
validateNamespaceKey(childPrefix);
|
|
179
196
|
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
180
197
|
}
|
|
181
198
|
qualify(key) {
|
|
182
199
|
return `${this.prefix}:${key}`;
|
|
183
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
|
+
}
|
|
184
219
|
async trackMetrics(operation) {
|
|
185
220
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
186
221
|
const before = this.cache.getMetrics();
|
|
@@ -305,6 +340,20 @@ function addMap(base, delta) {
|
|
|
305
340
|
}
|
|
306
341
|
return result;
|
|
307
342
|
}
|
|
343
|
+
function validateNamespaceKey(key) {
|
|
344
|
+
if (key.length === 0) {
|
|
345
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
346
|
+
}
|
|
347
|
+
if (key.length > 256) {
|
|
348
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
349
|
+
}
|
|
350
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
351
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
352
|
+
}
|
|
353
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
354
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
308
357
|
|
|
309
358
|
// src/invalidation/PatternMatcher.ts
|
|
310
359
|
var PatternMatcher = class _PatternMatcher {
|
|
@@ -360,21 +409,41 @@ var CacheKeyDiscovery = class {
|
|
|
360
409
|
this.options = options;
|
|
361
410
|
}
|
|
362
411
|
options;
|
|
363
|
-
async collectKeysWithPrefix(prefix) {
|
|
412
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
364
413
|
const { tagIndex } = this.options;
|
|
365
|
-
const matches = new Set(
|
|
366
|
-
|
|
367
|
-
|
|
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
|
+
}
|
|
368
427
|
await Promise.all(
|
|
369
428
|
this.options.layers.map(async (layer) => {
|
|
370
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
429
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
371
430
|
return;
|
|
372
431
|
}
|
|
373
432
|
try {
|
|
374
|
-
|
|
375
|
-
|
|
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 ?? []) {
|
|
376
444
|
if (key.startsWith(prefix)) {
|
|
377
445
|
matches.add(key);
|
|
446
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
378
447
|
}
|
|
379
448
|
}
|
|
380
449
|
} catch (error) {
|
|
@@ -384,18 +453,39 @@ var CacheKeyDiscovery = class {
|
|
|
384
453
|
);
|
|
385
454
|
return [...matches];
|
|
386
455
|
}
|
|
387
|
-
async collectKeysMatchingPattern(pattern) {
|
|
388
|
-
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
|
+
}
|
|
389
469
|
await Promise.all(
|
|
390
470
|
this.options.layers.map(async (layer) => {
|
|
391
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
471
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
392
472
|
return;
|
|
393
473
|
}
|
|
394
474
|
try {
|
|
395
|
-
|
|
396
|
-
|
|
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 ?? []) {
|
|
397
486
|
if (PatternMatcher.matches(pattern, key)) {
|
|
398
487
|
matches.add(key);
|
|
488
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
399
489
|
}
|
|
400
490
|
}
|
|
401
491
|
} catch (error) {
|
|
@@ -405,8 +495,280 @@ var CacheKeyDiscovery = class {
|
|
|
405
495
|
);
|
|
406
496
|
return [...matches];
|
|
407
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
|
+
}
|
|
408
503
|
};
|
|
409
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
|
+
|
|
410
772
|
// src/internal/CircuitBreakerManager.ts
|
|
411
773
|
var CircuitBreakerManager = class {
|
|
412
774
|
breakers = /* @__PURE__ */ new Map();
|
|
@@ -425,9 +787,7 @@ var CircuitBreakerManager = class {
|
|
|
425
787
|
}
|
|
426
788
|
const now = Date.now();
|
|
427
789
|
if (state.openUntil <= now) {
|
|
428
|
-
|
|
429
|
-
state.failures = 0;
|
|
430
|
-
this.breakers.set(key, state);
|
|
790
|
+
this.breakers.delete(key);
|
|
431
791
|
return;
|
|
432
792
|
}
|
|
433
793
|
const remainingMs = state.openUntil - now;
|
|
@@ -438,15 +798,15 @@ var CircuitBreakerManager = class {
|
|
|
438
798
|
if (!options) {
|
|
439
799
|
return;
|
|
440
800
|
}
|
|
801
|
+
this.pruneIfNeeded();
|
|
441
802
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
442
803
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
443
|
-
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null };
|
|
804
|
+
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
444
805
|
state.failures += 1;
|
|
445
806
|
if (state.failures >= failureThreshold) {
|
|
446
807
|
state.openUntil = Date.now() + cooldownMs;
|
|
447
808
|
}
|
|
448
809
|
this.breakers.set(key, state);
|
|
449
|
-
this.pruneIfNeeded();
|
|
450
810
|
}
|
|
451
811
|
recordSuccess(key) {
|
|
452
812
|
this.breakers.delete(key);
|
|
@@ -457,8 +817,7 @@ var CircuitBreakerManager = class {
|
|
|
457
817
|
return false;
|
|
458
818
|
}
|
|
459
819
|
if (state.openUntil <= Date.now()) {
|
|
460
|
-
|
|
461
|
-
state.failures = 0;
|
|
820
|
+
this.breakers.delete(key);
|
|
462
821
|
return false;
|
|
463
822
|
}
|
|
464
823
|
return true;
|
|
@@ -482,15 +841,20 @@ var CircuitBreakerManager = class {
|
|
|
482
841
|
if (this.breakers.size <= this.maxEntries) {
|
|
483
842
|
return;
|
|
484
843
|
}
|
|
844
|
+
const now = Date.now();
|
|
485
845
|
for (const [key, state] of this.breakers.entries()) {
|
|
486
846
|
if (this.breakers.size <= this.maxEntries) {
|
|
487
|
-
|
|
847
|
+
return;
|
|
488
848
|
}
|
|
489
|
-
if (!state.openUntil || state.openUntil <=
|
|
849
|
+
if (!state.openUntil || state.openUntil <= now) {
|
|
490
850
|
this.breakers.delete(key);
|
|
491
851
|
}
|
|
492
852
|
}
|
|
493
|
-
|
|
853
|
+
if (this.breakers.size <= this.maxEntries) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
const sorted = [...this.breakers.entries()].sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
857
|
+
for (const [key] of sorted) {
|
|
494
858
|
if (this.breakers.size <= this.maxEntries) {
|
|
495
859
|
break;
|
|
496
860
|
}
|
|
@@ -500,6 +864,7 @@ var CircuitBreakerManager = class {
|
|
|
500
864
|
};
|
|
501
865
|
|
|
502
866
|
// src/internal/FetchRateLimiter.ts
|
|
867
|
+
var MAX_BUCKETS = 1e4;
|
|
503
868
|
var FetchRateLimiter = class {
|
|
504
869
|
buckets = /* @__PURE__ */ new Map();
|
|
505
870
|
queuesByBucket = /* @__PURE__ */ new Map();
|
|
@@ -665,10 +1030,25 @@ var FetchRateLimiter = class {
|
|
|
665
1030
|
if (existing) {
|
|
666
1031
|
return existing;
|
|
667
1032
|
}
|
|
1033
|
+
if (this.buckets.size >= MAX_BUCKETS) {
|
|
1034
|
+
this.evictIdleBuckets();
|
|
1035
|
+
}
|
|
668
1036
|
const bucket = { active: 0, startedAt: [] };
|
|
669
1037
|
this.buckets.set(bucketKey, bucket);
|
|
670
1038
|
return bucket;
|
|
671
1039
|
}
|
|
1040
|
+
evictIdleBuckets() {
|
|
1041
|
+
for (const [key, bucket] of this.buckets.entries()) {
|
|
1042
|
+
if (this.buckets.size <= MAX_BUCKETS * 0.9) {
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
if (bucket.active === 0 && bucket.startedAt.length === 0 && !this.queuesByBucket.has(key)) {
|
|
1046
|
+
this.buckets.delete(key);
|
|
1047
|
+
this.queuesByBucket.delete(key);
|
|
1048
|
+
this.pendingBuckets.delete(key);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
672
1052
|
cleanupBucket(bucketKey, bucket, intervalMs) {
|
|
673
1053
|
const queued = this.queuesByBucket.get(bucketKey)?.length ?? 0;
|
|
674
1054
|
if (queued === 0 && bucket.active === 0 && bucket.startedAt.length === 0) {
|
|
@@ -782,19 +1162,47 @@ function isStoredValueEnvelope(value) {
|
|
|
782
1162
|
if (v.kind !== "value" && v.kind !== "empty") {
|
|
783
1163
|
return false;
|
|
784
1164
|
}
|
|
785
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
1165
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
786
1166
|
return false;
|
|
787
1167
|
}
|
|
788
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
1168
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
789
1169
|
return false;
|
|
790
1170
|
}
|
|
791
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
1171
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
792
1172
|
return false;
|
|
793
1173
|
}
|
|
794
1174
|
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
795
1175
|
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
796
1176
|
return false;
|
|
797
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
|
+
}
|
|
798
1206
|
return true;
|
|
799
1207
|
}
|
|
800
1208
|
function createStoredValueEnvelope(options) {
|
|
@@ -893,6 +1301,12 @@ function normalizePositiveSeconds(value) {
|
|
|
893
1301
|
}
|
|
894
1302
|
return value;
|
|
895
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
|
+
}
|
|
896
1310
|
|
|
897
1311
|
// src/internal/TtlResolver.ts
|
|
898
1312
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -995,18 +1409,18 @@ var TtlResolver = class {
|
|
|
995
1409
|
return;
|
|
996
1410
|
}
|
|
997
1411
|
const toRemove = Math.ceil(this.maxProfileEntries * 0.1);
|
|
998
|
-
|
|
999
|
-
for (
|
|
1000
|
-
|
|
1001
|
-
|
|
1412
|
+
const sorted = [...this.accessProfiles.entries()].sort((a, b) => a[1].lastAccessAt - b[1].lastAccessAt);
|
|
1413
|
+
for (let i = 0; i < toRemove && i < sorted.length; i++) {
|
|
1414
|
+
const entry = sorted[i];
|
|
1415
|
+
if (entry) {
|
|
1416
|
+
this.accessProfiles.delete(entry[0]);
|
|
1002
1417
|
}
|
|
1003
|
-
this.accessProfiles.delete(key);
|
|
1004
|
-
removed += 1;
|
|
1005
1418
|
}
|
|
1006
1419
|
}
|
|
1007
1420
|
};
|
|
1008
1421
|
|
|
1009
1422
|
// src/invalidation/TagIndex.ts
|
|
1423
|
+
var MAX_PATTERN_RECURSION_DEPTH = 500;
|
|
1010
1424
|
var TagIndex = class {
|
|
1011
1425
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
1012
1426
|
keyToTags = /* @__PURE__ */ new Map();
|
|
@@ -1047,6 +1461,11 @@ var TagIndex = class {
|
|
|
1047
1461
|
async keysForTag(tag) {
|
|
1048
1462
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1049
1463
|
}
|
|
1464
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1465
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1466
|
+
await visitor(key);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1050
1469
|
async keysForPrefix(prefix) {
|
|
1051
1470
|
const node = this.findNode(prefix);
|
|
1052
1471
|
if (!node) {
|
|
@@ -1056,14 +1475,27 @@ var TagIndex = class {
|
|
|
1056
1475
|
this.collectFromNode(node, prefix, matches);
|
|
1057
1476
|
return matches;
|
|
1058
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
|
+
}
|
|
1059
1485
|
async tagsForKey(key) {
|
|
1060
1486
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1061
1487
|
}
|
|
1062
1488
|
async matchPattern(pattern) {
|
|
1063
1489
|
const matches = /* @__PURE__ */ new Set();
|
|
1064
|
-
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set());
|
|
1490
|
+
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1065
1491
|
return [...matches];
|
|
1066
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
|
+
}
|
|
1067
1499
|
async clear() {
|
|
1068
1500
|
this.tagToKeys.clear();
|
|
1069
1501
|
this.keyToTags.clear();
|
|
@@ -1113,7 +1545,18 @@ var TagIndex = class {
|
|
|
1113
1545
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1114
1546
|
}
|
|
1115
1547
|
}
|
|
1116
|
-
|
|
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
|
+
}
|
|
1556
|
+
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1557
|
+
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1117
1560
|
const stateKey = `${node.id}:${patternIndex}`;
|
|
1118
1561
|
if (visited.has(stateKey)) {
|
|
1119
1562
|
return;
|
|
@@ -1130,21 +1573,37 @@ var TagIndex = class {
|
|
|
1130
1573
|
return;
|
|
1131
1574
|
}
|
|
1132
1575
|
if (patternChar === "*") {
|
|
1133
|
-
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited);
|
|
1576
|
+
this.collectPatternMatches(node, prefix, pattern, patternIndex + 1, matches, visited, depth + 1);
|
|
1134
1577
|
for (const [character, child2] of node.children) {
|
|
1135
|
-
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited);
|
|
1578
|
+
this.collectPatternMatches(child2, `${prefix}${character}`, pattern, patternIndex, matches, visited, depth + 1);
|
|
1136
1579
|
}
|
|
1137
1580
|
return;
|
|
1138
1581
|
}
|
|
1139
1582
|
if (patternChar === "?") {
|
|
1140
1583
|
for (const [character, child2] of node.children) {
|
|
1141
|
-
this.collectPatternMatches(
|
|
1584
|
+
this.collectPatternMatches(
|
|
1585
|
+
child2,
|
|
1586
|
+
`${prefix}${character}`,
|
|
1587
|
+
pattern,
|
|
1588
|
+
patternIndex + 1,
|
|
1589
|
+
matches,
|
|
1590
|
+
visited,
|
|
1591
|
+
depth + 1
|
|
1592
|
+
);
|
|
1142
1593
|
}
|
|
1143
1594
|
return;
|
|
1144
1595
|
}
|
|
1145
1596
|
const child = node.children.get(patternChar);
|
|
1146
1597
|
if (child) {
|
|
1147
|
-
this.collectPatternMatches(
|
|
1598
|
+
this.collectPatternMatches(
|
|
1599
|
+
child,
|
|
1600
|
+
`${prefix}${patternChar}`,
|
|
1601
|
+
pattern,
|
|
1602
|
+
patternIndex + 1,
|
|
1603
|
+
matches,
|
|
1604
|
+
visited,
|
|
1605
|
+
depth + 1
|
|
1606
|
+
);
|
|
1148
1607
|
}
|
|
1149
1608
|
}
|
|
1150
1609
|
pruneKnownKeysIfNeeded() {
|
|
@@ -1211,22 +1670,27 @@ var TagIndex = class {
|
|
|
1211
1670
|
|
|
1212
1671
|
// src/serialization/JsonSerializer.ts
|
|
1213
1672
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1673
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1214
1674
|
var JsonSerializer = class {
|
|
1215
1675
|
serialize(value) {
|
|
1216
1676
|
return JSON.stringify(value);
|
|
1217
1677
|
}
|
|
1218
1678
|
deserialize(payload) {
|
|
1219
1679
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1220
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1680
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1221
1681
|
}
|
|
1222
1682
|
};
|
|
1223
1683
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1224
|
-
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
|
+
}
|
|
1225
1689
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1226
|
-
|
|
1690
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1227
1691
|
}
|
|
1228
1692
|
if (Array.isArray(value)) {
|
|
1229
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1693
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1230
1694
|
}
|
|
1231
1695
|
if (!isPlainObject(value)) {
|
|
1232
1696
|
return value;
|
|
@@ -1236,7 +1700,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1236
1700
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1237
1701
|
continue;
|
|
1238
1702
|
}
|
|
1239
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1703
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1240
1704
|
}
|
|
1241
1705
|
return sanitized;
|
|
1242
1706
|
}
|
|
@@ -1286,9 +1750,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1286
1750
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1287
1751
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1288
1752
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1289
|
-
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;
|
|
1290
1757
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1291
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1292
1758
|
var DebugLogger = class {
|
|
1293
1759
|
enabled;
|
|
1294
1760
|
constructor(enabled) {
|
|
@@ -1375,6 +1841,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1375
1841
|
snapshotSerializer = new JsonSerializer();
|
|
1376
1842
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1377
1843
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1844
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
1378
1845
|
ttlResolver;
|
|
1379
1846
|
circuitBreakerManager;
|
|
1380
1847
|
currentGeneration;
|
|
@@ -1382,6 +1849,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1382
1849
|
writeBehindTimer;
|
|
1383
1850
|
writeBehindFlushPromise;
|
|
1384
1851
|
generationCleanupPromise;
|
|
1852
|
+
clearEpoch = 0;
|
|
1385
1853
|
isDisconnecting = false;
|
|
1386
1854
|
disconnectPromise;
|
|
1387
1855
|
/**
|
|
@@ -1391,7 +1859,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1391
1859
|
* and no `fetcher` is provided.
|
|
1392
1860
|
*/
|
|
1393
1861
|
async get(key, fetcher, options) {
|
|
1394
|
-
const normalizedKey = this.qualifyKey(
|
|
1862
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1395
1863
|
this.validateWriteOptions(options);
|
|
1396
1864
|
await this.awaitStartup("get");
|
|
1397
1865
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1461,7 +1929,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1461
1929
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1462
1930
|
*/
|
|
1463
1931
|
async has(key) {
|
|
1464
|
-
const normalizedKey = this.qualifyKey(
|
|
1932
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1465
1933
|
await this.awaitStartup("has");
|
|
1466
1934
|
for (const layer of this.layers) {
|
|
1467
1935
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1494,7 +1962,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1494
1962
|
* that has it, or null if the key is not found / has no TTL.
|
|
1495
1963
|
*/
|
|
1496
1964
|
async ttl(key) {
|
|
1497
|
-
const normalizedKey = this.qualifyKey(
|
|
1965
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1498
1966
|
await this.awaitStartup("ttl");
|
|
1499
1967
|
for (const layer of this.layers) {
|
|
1500
1968
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1516,7 +1984,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1516
1984
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1517
1985
|
*/
|
|
1518
1986
|
async set(key, value, options) {
|
|
1519
|
-
const normalizedKey = this.qualifyKey(
|
|
1987
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1520
1988
|
this.validateWriteOptions(options);
|
|
1521
1989
|
await this.awaitStartup("set");
|
|
1522
1990
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1525,7 +1993,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1525
1993
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1526
1994
|
*/
|
|
1527
1995
|
async delete(key) {
|
|
1528
|
-
const normalizedKey = this.qualifyKey(
|
|
1996
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1529
1997
|
await this.awaitStartup("delete");
|
|
1530
1998
|
await this.deleteKeys([normalizedKey]);
|
|
1531
1999
|
await this.publishInvalidation({
|
|
@@ -1537,6 +2005,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1537
2005
|
}
|
|
1538
2006
|
async clear() {
|
|
1539
2007
|
await this.awaitStartup("clear");
|
|
2008
|
+
this.beginClearEpoch();
|
|
1540
2009
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1541
2010
|
await this.tagIndex.clear();
|
|
1542
2011
|
this.ttlResolver.clearProfiles();
|
|
@@ -1553,7 +2022,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1553
2022
|
return;
|
|
1554
2023
|
}
|
|
1555
2024
|
await this.awaitStartup("mdelete");
|
|
1556
|
-
const normalizedKeys = keys.map((k) =>
|
|
2025
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1557
2026
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1558
2027
|
await this.deleteKeys(cacheKeys);
|
|
1559
2028
|
await this.publishInvalidation({
|
|
@@ -1570,7 +2039,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1570
2039
|
}
|
|
1571
2040
|
const normalizedEntries = entries.map((entry) => ({
|
|
1572
2041
|
...entry,
|
|
1573
|
-
key: this.qualifyKey(
|
|
2042
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1574
2043
|
}));
|
|
1575
2044
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1576
2045
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1579,7 +2048,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1579
2048
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1580
2049
|
return Promise.all(
|
|
1581
2050
|
normalizedEntries.map((entry) => {
|
|
1582
|
-
const optionsSignature =
|
|
2051
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1583
2052
|
const existing = pendingReads.get(entry.key);
|
|
1584
2053
|
if (!existing) {
|
|
1585
2054
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1648,7 +2117,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1648
2117
|
this.assertActive("mset");
|
|
1649
2118
|
const normalizedEntries = entries.map((entry) => ({
|
|
1650
2119
|
...entry,
|
|
1651
|
-
key: this.qualifyKey(
|
|
2120
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1652
2121
|
}));
|
|
1653
2122
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1654
2123
|
await this.awaitStartup("mset");
|
|
@@ -1691,7 +2160,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1691
2160
|
*/
|
|
1692
2161
|
wrap(prefix, fetcher, options = {}) {
|
|
1693
2162
|
return (...args) => {
|
|
1694
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2163
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1695
2164
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1696
2165
|
return this.get(key, () => fetcher(...args), options);
|
|
1697
2166
|
};
|
|
@@ -1701,11 +2170,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1701
2170
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1702
2171
|
*/
|
|
1703
2172
|
namespace(prefix) {
|
|
2173
|
+
validateNamespaceKey(prefix);
|
|
1704
2174
|
return new CacheNamespace(this, prefix);
|
|
1705
2175
|
}
|
|
1706
2176
|
async invalidateByTag(tag) {
|
|
2177
|
+
validateTag(tag);
|
|
1707
2178
|
await this.awaitStartup("invalidateByTag");
|
|
1708
|
-
const keys = await this.
|
|
2179
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1709
2180
|
await this.deleteKeys(keys);
|
|
1710
2181
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1711
2182
|
}
|
|
@@ -1713,22 +2184,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1713
2184
|
if (tags.length === 0) {
|
|
1714
2185
|
return;
|
|
1715
2186
|
}
|
|
2187
|
+
validateTags(tags);
|
|
1716
2188
|
await this.awaitStartup("invalidateByTags");
|
|
1717
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2189
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1718
2190
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2191
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1719
2192
|
await this.deleteKeys(keys);
|
|
1720
2193
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1721
2194
|
}
|
|
1722
2195
|
async invalidateByPattern(pattern) {
|
|
2196
|
+
validatePattern(pattern);
|
|
1723
2197
|
await this.awaitStartup("invalidateByPattern");
|
|
1724
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2198
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2199
|
+
this.qualifyPattern(pattern),
|
|
2200
|
+
this.invalidationMaxKeys()
|
|
2201
|
+
);
|
|
1725
2202
|
await this.deleteKeys(keys);
|
|
1726
2203
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1727
2204
|
}
|
|
1728
2205
|
async invalidateByPrefix(prefix) {
|
|
1729
2206
|
await this.awaitStartup("invalidateByPrefix");
|
|
1730
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1731
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2207
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2208
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1732
2209
|
await this.deleteKeys(keys);
|
|
1733
2210
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1734
2211
|
}
|
|
@@ -1798,7 +2275,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1798
2275
|
* Returns `null` if the key does not exist in any layer.
|
|
1799
2276
|
*/
|
|
1800
2277
|
async inspect(key) {
|
|
1801
|
-
const userKey =
|
|
2278
|
+
const userKey = validateCacheKey(key);
|
|
1802
2279
|
const normalizedKey = this.qualifyKey(userKey);
|
|
1803
2280
|
await this.awaitStartup("inspect");
|
|
1804
2281
|
const foundInLayers = [];
|
|
@@ -1835,50 +2312,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1835
2312
|
}
|
|
1836
2313
|
async exportState() {
|
|
1837
2314
|
await this.awaitStartup("exportState");
|
|
1838
|
-
const
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
const keys = await layer.keys();
|
|
1844
|
-
for (const key of keys) {
|
|
1845
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
1846
|
-
if (exported.has(exportedKey)) {
|
|
1847
|
-
continue;
|
|
1848
|
-
}
|
|
1849
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
1850
|
-
if (stored === null) {
|
|
1851
|
-
continue;
|
|
1852
|
-
}
|
|
1853
|
-
exported.set(exportedKey, {
|
|
1854
|
-
key: exportedKey,
|
|
1855
|
-
value: stored,
|
|
1856
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
1857
|
-
});
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
return [...exported.values()];
|
|
2315
|
+
const entries = [];
|
|
2316
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2317
|
+
entries.push(entry);
|
|
2318
|
+
});
|
|
2319
|
+
return entries;
|
|
1861
2320
|
}
|
|
1862
2321
|
async importState(entries) {
|
|
1863
2322
|
await this.awaitStartup("importState");
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
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
|
+
}
|
|
1871
2337
|
}
|
|
1872
2338
|
async persistToFile(filePath) {
|
|
1873
2339
|
this.assertActive("persistToFile");
|
|
1874
|
-
const snapshot = await this.exportState();
|
|
1875
2340
|
const { promises: fs2 } = await import("fs");
|
|
1876
|
-
|
|
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
|
+
}
|
|
1877
2367
|
}
|
|
1878
2368
|
async restoreFromFile(filePath) {
|
|
1879
2369
|
this.assertActive("restoreFromFile");
|
|
1880
|
-
const { promises: fs2 } = await import("fs");
|
|
1881
|
-
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
|
+
}
|
|
1882
2388
|
let parsed;
|
|
1883
2389
|
try {
|
|
1884
2390
|
parsed = JSON.parse(raw);
|
|
@@ -1922,14 +2428,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1922
2428
|
await this.handleInvalidationMessage(message);
|
|
1923
2429
|
});
|
|
1924
2430
|
}
|
|
1925
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2431
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1926
2432
|
const fetchTask = async () => {
|
|
1927
2433
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1928
2434
|
if (secondHit.found) {
|
|
1929
2435
|
this.metricsCollector.increment("hits");
|
|
1930
2436
|
return secondHit.value;
|
|
1931
2437
|
}
|
|
1932
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2438
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1933
2439
|
};
|
|
1934
2440
|
const singleFlightTask = async () => {
|
|
1935
2441
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -1939,7 +2445,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1939
2445
|
key,
|
|
1940
2446
|
this.resolveSingleFlightOptions(),
|
|
1941
2447
|
fetchTask,
|
|
1942
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2448
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1943
2449
|
);
|
|
1944
2450
|
};
|
|
1945
2451
|
if (this.options.stampedePrevention === false) {
|
|
@@ -1947,7 +2453,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1947
2453
|
}
|
|
1948
2454
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
1949
2455
|
}
|
|
1950
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2456
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1951
2457
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1952
2458
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1953
2459
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1961,9 +2467,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1961
2467
|
}
|
|
1962
2468
|
await this.sleep(pollIntervalMs);
|
|
1963
2469
|
}
|
|
1964
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2470
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1965
2471
|
}
|
|
1966
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2472
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1967
2473
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1968
2474
|
this.metricsCollector.increment("fetches");
|
|
1969
2475
|
const fetchStart = Date.now();
|
|
@@ -1984,6 +2490,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1984
2490
|
if (!this.shouldNegativeCache(options)) {
|
|
1985
2491
|
return null;
|
|
1986
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
|
+
}
|
|
1987
2503
|
await this.storeEntry(key, "empty", null, options);
|
|
1988
2504
|
return null;
|
|
1989
2505
|
}
|
|
@@ -1996,11 +2512,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1996
2512
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
1997
2513
|
}
|
|
1998
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
|
+
}
|
|
1999
2525
|
await this.storeEntry(key, "value", fetched, options);
|
|
2000
2526
|
return fetched;
|
|
2001
2527
|
}
|
|
2002
2528
|
async storeEntry(key, kind, value, options) {
|
|
2529
|
+
const clearEpoch = this.clearEpoch;
|
|
2530
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2003
2531
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2532
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2533
|
+
return;
|
|
2534
|
+
}
|
|
2004
2535
|
if (options?.tags) {
|
|
2005
2536
|
await this.tagIndex.track(key, options.tags);
|
|
2006
2537
|
} else {
|
|
@@ -2015,6 +2546,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2015
2546
|
}
|
|
2016
2547
|
async writeBatch(entries) {
|
|
2017
2548
|
const now = Date.now();
|
|
2549
|
+
const clearEpoch = this.clearEpoch;
|
|
2550
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.currentKeyEpoch(entry.key)]));
|
|
2018
2551
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2019
2552
|
const immediateOperations = [];
|
|
2020
2553
|
const deferredOperations = [];
|
|
@@ -2031,12 +2564,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2031
2564
|
}
|
|
2032
2565
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2033
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
|
+
}
|
|
2034
2576
|
try {
|
|
2035
2577
|
if (layer.setMany) {
|
|
2036
|
-
await layer.setMany(
|
|
2578
|
+
await layer.setMany(activeEntries);
|
|
2037
2579
|
return;
|
|
2038
2580
|
}
|
|
2039
|
-
await Promise.all(
|
|
2581
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2040
2582
|
} catch (error) {
|
|
2041
2583
|
await this.handleLayerFailure(layer, "write", error);
|
|
2042
2584
|
}
|
|
@@ -2049,7 +2591,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2049
2591
|
}
|
|
2050
2592
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2051
2593
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2594
|
+
if (clearEpoch !== this.clearEpoch) {
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2052
2597
|
for (const entry of entries) {
|
|
2598
|
+
if (this.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2053
2601
|
if (entry.options?.tags) {
|
|
2054
2602
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2055
2603
|
} else {
|
|
@@ -2151,10 +2699,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2151
2699
|
}
|
|
2152
2700
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2153
2701
|
const now = Date.now();
|
|
2702
|
+
const clearEpoch = this.clearEpoch;
|
|
2703
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2154
2704
|
const immediateOperations = [];
|
|
2155
2705
|
const deferredOperations = [];
|
|
2156
2706
|
for (const layer of this.layers) {
|
|
2157
2707
|
const operation = async () => {
|
|
2708
|
+
if (this.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2158
2711
|
if (this.shouldSkipLayer(layer)) {
|
|
2159
2712
|
return;
|
|
2160
2713
|
}
|
|
@@ -2218,10 +2771,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2218
2771
|
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
2219
2772
|
return;
|
|
2220
2773
|
}
|
|
2774
|
+
const clearEpoch = this.clearEpoch;
|
|
2775
|
+
const keyEpoch = this.currentKeyEpoch(key);
|
|
2221
2776
|
const refresh = (async () => {
|
|
2222
2777
|
this.metricsCollector.increment("refreshes");
|
|
2223
2778
|
try {
|
|
2224
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2779
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2225
2780
|
} catch (error) {
|
|
2226
2781
|
this.metricsCollector.increment("refreshErrors");
|
|
2227
2782
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2231,14 +2786,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2231
2786
|
})();
|
|
2232
2787
|
this.backgroundRefreshes.set(key, refresh);
|
|
2233
2788
|
}
|
|
2234
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2789
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2235
2790
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2236
2791
|
await this.fetchWithGuards(
|
|
2237
2792
|
key,
|
|
2238
2793
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2239
2794
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2240
2795
|
}),
|
|
2241
|
-
options
|
|
2796
|
+
options,
|
|
2797
|
+
expectedClearEpoch,
|
|
2798
|
+
expectedKeyEpoch
|
|
2242
2799
|
);
|
|
2243
2800
|
}
|
|
2244
2801
|
resolveSingleFlightOptions() {
|
|
@@ -2253,6 +2810,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2253
2810
|
if (keys.length === 0) {
|
|
2254
2811
|
return;
|
|
2255
2812
|
}
|
|
2813
|
+
this.bumpKeyEpochs(keys);
|
|
2256
2814
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2257
2815
|
for (const key of keys) {
|
|
2258
2816
|
await this.tagIndex.remove(key);
|
|
@@ -2275,21 +2833,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2275
2833
|
return;
|
|
2276
2834
|
}
|
|
2277
2835
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2278
|
-
if (localLayers.length === 0) {
|
|
2279
|
-
return;
|
|
2280
|
-
}
|
|
2281
2836
|
if (message.scope === "clear") {
|
|
2837
|
+
this.beginClearEpoch();
|
|
2282
2838
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2283
2839
|
await this.tagIndex.clear();
|
|
2284
2840
|
this.ttlResolver.clearProfiles();
|
|
2841
|
+
this.circuitBreakerManager.clear();
|
|
2285
2842
|
return;
|
|
2286
2843
|
}
|
|
2287
2844
|
const keys = message.keys ?? [];
|
|
2845
|
+
this.bumpKeyEpochs(keys);
|
|
2288
2846
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2289
2847
|
if (message.operation !== "write") {
|
|
2290
2848
|
for (const key of keys) {
|
|
2291
2849
|
await this.tagIndex.remove(key);
|
|
2292
2850
|
this.ttlResolver.deleteProfile(key);
|
|
2851
|
+
this.circuitBreakerManager.delete(key);
|
|
2293
2852
|
}
|
|
2294
2853
|
}
|
|
2295
2854
|
}
|
|
@@ -2395,6 +2954,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2395
2954
|
shouldWriteBehind(layer) {
|
|
2396
2955
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2397
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
|
+
}
|
|
2398
2979
|
async enqueueWriteBehind(operation) {
|
|
2399
2980
|
this.writeBehindQueue.push(operation);
|
|
2400
2981
|
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
@@ -2521,107 +3102,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2521
3102
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2522
3103
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2523
3104
|
}
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
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);
|
|
2537
3127
|
if (typeof this.options.generationCleanup === "object") {
|
|
2538
|
-
|
|
3128
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2539
3129
|
}
|
|
2540
3130
|
if (this.options.generation !== void 0) {
|
|
2541
|
-
|
|
3131
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2542
3132
|
}
|
|
2543
3133
|
}
|
|
2544
3134
|
validateWriteOptions(options) {
|
|
2545
3135
|
if (!options) {
|
|
2546
3136
|
return;
|
|
2547
3137
|
}
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
validateLayerNumberOption(name, value) {
|
|
2560
|
-
if (value === void 0) {
|
|
2561
|
-
return;
|
|
2562
|
-
}
|
|
2563
|
-
if (typeof value === "number") {
|
|
2564
|
-
this.validateNonNegativeNumber(name, value);
|
|
2565
|
-
return;
|
|
2566
|
-
}
|
|
2567
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2568
|
-
if (layerValue === void 0) {
|
|
2569
|
-
continue;
|
|
2570
|
-
}
|
|
2571
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2572
|
-
}
|
|
2573
|
-
}
|
|
2574
|
-
validatePositiveNumber(name, value) {
|
|
2575
|
-
if (value === void 0) {
|
|
2576
|
-
return;
|
|
2577
|
-
}
|
|
2578
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2579
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
validateRateLimitOptions(name, options) {
|
|
2583
|
-
if (!options) {
|
|
2584
|
-
return;
|
|
2585
|
-
}
|
|
2586
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2587
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2588
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2589
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2590
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2591
|
-
}
|
|
2592
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2593
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2594
|
-
}
|
|
2595
|
-
}
|
|
2596
|
-
validateNonNegativeNumber(name, value) {
|
|
2597
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2598
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2599
|
-
}
|
|
2600
|
-
}
|
|
2601
|
-
validateCacheKey(key) {
|
|
2602
|
-
if (key.length === 0) {
|
|
2603
|
-
throw new Error("Cache key must not be empty.");
|
|
2604
|
-
}
|
|
2605
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2606
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2607
|
-
}
|
|
2608
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2609
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2610
|
-
}
|
|
2611
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2612
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2613
|
-
}
|
|
2614
|
-
return key;
|
|
2615
|
-
}
|
|
2616
|
-
validateTtlPolicy(name, policy) {
|
|
2617
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2618
|
-
return;
|
|
2619
|
-
}
|
|
2620
|
-
if ("alignTo" in policy) {
|
|
2621
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2622
|
-
return;
|
|
2623
|
-
}
|
|
2624
|
-
throw new Error(`${name} is invalid.`);
|
|
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);
|
|
2625
3149
|
}
|
|
2626
3150
|
assertActive(operation) {
|
|
2627
3151
|
if (this.isDisconnecting) {
|
|
@@ -2633,24 +3157,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2633
3157
|
await this.startup;
|
|
2634
3158
|
this.assertActive(operation);
|
|
2635
3159
|
}
|
|
2636
|
-
serializeOptions(options) {
|
|
2637
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2638
|
-
}
|
|
2639
|
-
validateAdaptiveTtlOptions(options) {
|
|
2640
|
-
if (!options || options === true) {
|
|
2641
|
-
return;
|
|
2642
|
-
}
|
|
2643
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2644
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2645
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2646
|
-
}
|
|
2647
|
-
validateCircuitBreakerOptions(options) {
|
|
2648
|
-
if (!options) {
|
|
2649
|
-
return;
|
|
2650
|
-
}
|
|
2651
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2652
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2653
|
-
}
|
|
2654
3160
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2655
3161
|
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
2656
3162
|
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
@@ -2718,18 +3224,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2718
3224
|
this.emit("error", { operation, ...context });
|
|
2719
3225
|
}
|
|
2720
3226
|
}
|
|
2721
|
-
serializeKeyPart(value) {
|
|
2722
|
-
if (typeof value === "string") {
|
|
2723
|
-
return `s:${value}`;
|
|
2724
|
-
}
|
|
2725
|
-
if (typeof value === "number") {
|
|
2726
|
-
return `n:${value}`;
|
|
2727
|
-
}
|
|
2728
|
-
if (typeof value === "boolean") {
|
|
2729
|
-
return `b:${value}`;
|
|
2730
|
-
}
|
|
2731
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2732
|
-
}
|
|
2733
3227
|
isCacheSnapshotEntries(value) {
|
|
2734
3228
|
return Array.isArray(value) && value.every((entry) => {
|
|
2735
3229
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2742,43 +3236,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2742
3236
|
sanitizeSnapshotValue(value) {
|
|
2743
3237
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2744
3238
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
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];
|
|
2751
3256
|
}
|
|
2752
|
-
const
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
if (baseDir !== false) {
|
|
2756
|
-
const relative = path.relative(baseDir, resolved);
|
|
2757
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2758
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2759
|
-
}
|
|
3257
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3258
|
+
keys.add(key);
|
|
3259
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2760
3260
|
}
|
|
2761
|
-
return
|
|
3261
|
+
return [...keys];
|
|
2762
3262
|
}
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
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}).`);
|
|
2766
3267
|
}
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
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;
|
|
2771
3279
|
}
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
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
|
+
}
|
|
2775
3302
|
}
|
|
2776
|
-
return value;
|
|
2777
3303
|
}
|
|
2778
3304
|
};
|
|
2779
|
-
function createInstanceId() {
|
|
2780
|
-
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2781
|
-
}
|
|
2782
3305
|
|
|
2783
3306
|
// src/invalidation/RedisInvalidationBus.ts
|
|
2784
3307
|
var RedisInvalidationBus = class {
|
|
@@ -2819,7 +3342,7 @@ var RedisInvalidationBus = class {
|
|
|
2819
3342
|
async dispatchToHandlers(payload) {
|
|
2820
3343
|
let message;
|
|
2821
3344
|
try {
|
|
2822
|
-
const parsed = JSON.parse(payload);
|
|
3345
|
+
const parsed = sanitizeJsonValue2(JSON.parse(payload));
|
|
2823
3346
|
if (!this.isInvalidationMessage(parsed)) {
|
|
2824
3347
|
throw new Error("Invalid invalidation payload shape.");
|
|
2825
3348
|
}
|
|
@@ -2856,6 +3379,31 @@ var RedisInvalidationBus = class {
|
|
|
2856
3379
|
console.error(`[layercache] ${message}`, error);
|
|
2857
3380
|
}
|
|
2858
3381
|
};
|
|
3382
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
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
|
+
}
|
|
3393
|
+
if (Array.isArray(value)) {
|
|
3394
|
+
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
3395
|
+
}
|
|
3396
|
+
if (value && typeof value === "object") {
|
|
3397
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
3398
|
+
for (const key of Object.keys(value)) {
|
|
3399
|
+
if (!DANGEROUS_KEYS.has(key)) {
|
|
3400
|
+
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
return result;
|
|
3404
|
+
}
|
|
3405
|
+
return value;
|
|
3406
|
+
}
|
|
2859
3407
|
|
|
2860
3408
|
// src/invalidation/RedisTagIndex.ts
|
|
2861
3409
|
var RedisTagIndex = class {
|
|
@@ -2903,6 +3451,17 @@ var RedisTagIndex = class {
|
|
|
2903
3451
|
async keysForTag(tag) {
|
|
2904
3452
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
2905
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
|
+
}
|
|
2906
3465
|
async keysForPrefix(prefix) {
|
|
2907
3466
|
const matches = [];
|
|
2908
3467
|
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
@@ -2915,6 +3474,20 @@ var RedisTagIndex = class {
|
|
|
2915
3474
|
}
|
|
2916
3475
|
return matches;
|
|
2917
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
|
+
}
|
|
2918
3491
|
async tagsForKey(key) {
|
|
2919
3492
|
return this.client.smembers(this.keyTagsKey(key));
|
|
2920
3493
|
}
|
|
@@ -2937,6 +3510,27 @@ var RedisTagIndex = class {
|
|
|
2937
3510
|
}
|
|
2938
3511
|
return matches;
|
|
2939
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
|
+
}
|
|
2940
3534
|
async clear() {
|
|
2941
3535
|
const indexKeys = await this.scanIndexKeys();
|
|
2942
3536
|
if (indexKeys.length === 0) {
|
|
@@ -2992,10 +3586,18 @@ function simpleHash(value) {
|
|
|
2992
3586
|
}
|
|
2993
3587
|
|
|
2994
3588
|
// src/http/createCacheStatsHandler.ts
|
|
2995
|
-
function createCacheStatsHandler(cache) {
|
|
2996
|
-
return async (
|
|
2997
|
-
response.statusCode = 200;
|
|
3589
|
+
function createCacheStatsHandler(cache, options = {}) {
|
|
3590
|
+
return async (request, response) => {
|
|
2998
3591
|
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
3592
|
+
response.setHeader?.("cache-control", "no-store");
|
|
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;
|
|
2999
3601
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
3000
3602
|
};
|
|
3001
3603
|
}
|
|
@@ -3030,7 +3632,26 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
3030
3632
|
return async (fastify) => {
|
|
3031
3633
|
fastify.decorate("cache", cache);
|
|
3032
3634
|
if (options.exposeStatsRoute === true && fastify.get) {
|
|
3033
|
-
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
|
+
});
|
|
3034
3655
|
}
|
|
3035
3656
|
};
|
|
3036
3657
|
}
|
|
@@ -3045,6 +3666,10 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3045
3666
|
next();
|
|
3046
3667
|
return;
|
|
3047
3668
|
}
|
|
3669
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3670
|
+
next();
|
|
3671
|
+
return;
|
|
3672
|
+
}
|
|
3048
3673
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3049
3674
|
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
|
|
3050
3675
|
const cached = await cache.get(key, void 0, options);
|
|
@@ -3081,7 +3706,7 @@ function normalizeUrl(url) {
|
|
|
3081
3706
|
try {
|
|
3082
3707
|
const parsed = new URL(url, "http://localhost");
|
|
3083
3708
|
parsed.searchParams.sort();
|
|
3084
|
-
return
|
|
3709
|
+
return parsed.pathname + parsed.search;
|
|
3085
3710
|
} catch {
|
|
3086
3711
|
return url;
|
|
3087
3712
|
}
|
|
@@ -3089,6 +3714,11 @@ function normalizeUrl(url) {
|
|
|
3089
3714
|
|
|
3090
3715
|
// src/integrations/graphql.ts
|
|
3091
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
|
+
}
|
|
3092
3722
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
3093
3723
|
...options,
|
|
3094
3724
|
keyResolver: options.keyResolver
|
|
@@ -3105,14 +3735,17 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
3105
3735
|
await next();
|
|
3106
3736
|
return;
|
|
3107
3737
|
}
|
|
3738
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3739
|
+
await next();
|
|
3740
|
+
return;
|
|
3741
|
+
}
|
|
3108
3742
|
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
3109
3743
|
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
|
|
3110
3744
|
const cached = await cache.get(key, void 0, options);
|
|
3111
3745
|
if (cached !== null) {
|
|
3112
3746
|
context.header?.("x-cache", "HIT");
|
|
3113
3747
|
context.header?.("content-type", "application/json; charset=utf-8");
|
|
3114
|
-
context.json(cached);
|
|
3115
|
-
return;
|
|
3748
|
+
return context.json(cached);
|
|
3116
3749
|
}
|
|
3117
3750
|
const originalJson = context.json.bind(context);
|
|
3118
3751
|
context.json = (body, status) => {
|
|
@@ -3132,7 +3765,7 @@ function normalizeUrl2(url) {
|
|
|
3132
3765
|
try {
|
|
3133
3766
|
const parsed = new URL(url, "http://localhost");
|
|
3134
3767
|
parsed.searchParams.sort();
|
|
3135
|
-
return
|
|
3768
|
+
return parsed.pathname + parsed.search;
|
|
3136
3769
|
} catch {
|
|
3137
3770
|
return url;
|
|
3138
3771
|
}
|
|
@@ -3202,6 +3835,11 @@ function instrument(name, tracer, method, attributes) {
|
|
|
3202
3835
|
|
|
3203
3836
|
// src/integrations/trpc.ts
|
|
3204
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
|
+
}
|
|
3205
3843
|
return async (context) => {
|
|
3206
3844
|
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
3207
3845
|
let didFetch = false;
|
|
@@ -3341,6 +3979,12 @@ var MemoryLayer = class {
|
|
|
3341
3979
|
this.pruneExpired();
|
|
3342
3980
|
return [...this.entries.keys()];
|
|
3343
3981
|
}
|
|
3982
|
+
async forEachKey(visitor) {
|
|
3983
|
+
this.pruneExpired();
|
|
3984
|
+
for (const key of this.entries.keys()) {
|
|
3985
|
+
await visitor(key);
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3344
3988
|
exportState() {
|
|
3345
3989
|
this.pruneExpired();
|
|
3346
3990
|
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
@@ -3408,13 +4052,12 @@ var MemoryLayer = class {
|
|
|
3408
4052
|
};
|
|
3409
4053
|
|
|
3410
4054
|
// src/layers/RedisLayer.ts
|
|
4055
|
+
var import_node_stream = require("stream");
|
|
3411
4056
|
var import_node_util = require("util");
|
|
3412
4057
|
var import_node_zlib = require("zlib");
|
|
3413
4058
|
var BATCH_DELETE_SIZE = 500;
|
|
3414
4059
|
var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
|
|
3415
|
-
var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
|
|
3416
4060
|
var brotliCompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliCompress);
|
|
3417
|
-
var brotliDecompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliDecompress);
|
|
3418
4061
|
var RedisLayer = class {
|
|
3419
4062
|
name;
|
|
3420
4063
|
defaultTtl;
|
|
@@ -3522,8 +4165,18 @@ var RedisLayer = class {
|
|
|
3522
4165
|
return remaining;
|
|
3523
4166
|
}
|
|
3524
4167
|
async size() {
|
|
3525
|
-
|
|
3526
|
-
|
|
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;
|
|
3527
4180
|
}
|
|
3528
4181
|
async ping() {
|
|
3529
4182
|
try {
|
|
@@ -3569,6 +4222,17 @@ var RedisLayer = class {
|
|
|
3569
4222
|
}
|
|
3570
4223
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
3571
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
|
+
}
|
|
3572
4236
|
async scanKeys(pattern) {
|
|
3573
4237
|
const matches = [];
|
|
3574
4238
|
let cursor = "0";
|
|
@@ -3583,7 +4247,13 @@ var RedisLayer = class {
|
|
|
3583
4247
|
return `${this.prefix}${key}`;
|
|
3584
4248
|
}
|
|
3585
4249
|
async deserializeOrDelete(key, payload) {
|
|
3586
|
-
|
|
4250
|
+
let decodedPayload;
|
|
4251
|
+
try {
|
|
4252
|
+
decodedPayload = await this.decodePayload(payload);
|
|
4253
|
+
} catch {
|
|
4254
|
+
await this.deleteCorruptedKey(key);
|
|
4255
|
+
return null;
|
|
4256
|
+
}
|
|
3587
4257
|
for (const serializer of this.serializers) {
|
|
3588
4258
|
try {
|
|
3589
4259
|
const value = serializer.deserialize(decodedPayload);
|
|
@@ -3594,11 +4264,15 @@ var RedisLayer = class {
|
|
|
3594
4264
|
} catch {
|
|
3595
4265
|
}
|
|
3596
4266
|
}
|
|
4267
|
+
await this.deleteCorruptedKey(key);
|
|
4268
|
+
return null;
|
|
4269
|
+
}
|
|
4270
|
+
async deleteCorruptedKey(key) {
|
|
3597
4271
|
try {
|
|
3598
|
-
await this.client.del(this.withPrefix(key))
|
|
3599
|
-
} catch {
|
|
4272
|
+
await this.client.del(this.withPrefix(key));
|
|
4273
|
+
} catch (deleteError) {
|
|
4274
|
+
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
3600
4275
|
}
|
|
3601
|
-
return null;
|
|
3602
4276
|
}
|
|
3603
4277
|
async rewriteWithPrimarySerializer(key, value) {
|
|
3604
4278
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -3645,31 +4319,72 @@ var RedisLayer = class {
|
|
|
3645
4319
|
return payload;
|
|
3646
4320
|
}
|
|
3647
4321
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
3648
|
-
|
|
3649
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3650
|
-
throw new Error(
|
|
3651
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3652
|
-
);
|
|
3653
|
-
}
|
|
3654
|
-
return decompressed;
|
|
4322
|
+
return this.decompressWithLimit((0, import_node_zlib.createGunzip)(), payload.subarray(10));
|
|
3655
4323
|
}
|
|
3656
4324
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
3657
|
-
|
|
3658
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3659
|
-
throw new Error(
|
|
3660
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3661
|
-
);
|
|
3662
|
-
}
|
|
3663
|
-
return decompressed;
|
|
4325
|
+
return this.decompressWithLimit((0, import_node_zlib.createBrotliDecompress)(), payload.subarray(12));
|
|
3664
4326
|
}
|
|
3665
4327
|
return payload;
|
|
3666
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
|
+
}
|
|
3667
4381
|
};
|
|
3668
4382
|
|
|
3669
4383
|
// src/layers/DiskLayer.ts
|
|
3670
4384
|
var import_node_crypto = require("crypto");
|
|
3671
4385
|
var import_node_fs = require("fs");
|
|
3672
4386
|
var import_node_path = require("path");
|
|
4387
|
+
var FILE_SCAN_CONCURRENCY = 32;
|
|
3673
4388
|
var DiskLayer = class {
|
|
3674
4389
|
name;
|
|
3675
4390
|
defaultTtl;
|
|
@@ -3677,6 +4392,7 @@ var DiskLayer = class {
|
|
|
3677
4392
|
directory;
|
|
3678
4393
|
serializer;
|
|
3679
4394
|
maxFiles;
|
|
4395
|
+
maxEntryBytes;
|
|
3680
4396
|
writeQueue = Promise.resolve();
|
|
3681
4397
|
constructor(options) {
|
|
3682
4398
|
this.directory = this.resolveDirectory(options.directory);
|
|
@@ -3684,16 +4400,15 @@ var DiskLayer = class {
|
|
|
3684
4400
|
this.name = options.name ?? "disk";
|
|
3685
4401
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
3686
4402
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
4403
|
+
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
3687
4404
|
}
|
|
3688
4405
|
async get(key) {
|
|
3689
4406
|
return unwrapStoredValue(await this.getEntry(key));
|
|
3690
4407
|
}
|
|
3691
4408
|
async getEntry(key) {
|
|
3692
4409
|
const filePath = this.keyToPath(key);
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3696
|
-
} catch {
|
|
4410
|
+
const raw = await this.readEntryFile(filePath);
|
|
4411
|
+
if (raw === null) {
|
|
3697
4412
|
return null;
|
|
3698
4413
|
}
|
|
3699
4414
|
let entry;
|
|
@@ -3744,10 +4459,8 @@ var DiskLayer = class {
|
|
|
3744
4459
|
}
|
|
3745
4460
|
async ttl(key) {
|
|
3746
4461
|
const filePath = this.keyToPath(key);
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3750
|
-
} catch {
|
|
4462
|
+
const raw = await this.readEntryFile(filePath);
|
|
4463
|
+
if (raw === null) {
|
|
3751
4464
|
return null;
|
|
3752
4465
|
}
|
|
3753
4466
|
let entry;
|
|
@@ -3771,7 +4484,7 @@ var DiskLayer = class {
|
|
|
3771
4484
|
}
|
|
3772
4485
|
async deleteMany(keys) {
|
|
3773
4486
|
await this.enqueueWrite(async () => {
|
|
3774
|
-
await
|
|
4487
|
+
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
3775
4488
|
});
|
|
3776
4489
|
}
|
|
3777
4490
|
async clear() {
|
|
@@ -3782,8 +4495,8 @@ var DiskLayer = class {
|
|
|
3782
4495
|
} catch {
|
|
3783
4496
|
return;
|
|
3784
4497
|
}
|
|
3785
|
-
await
|
|
3786
|
-
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))
|
|
3787
4500
|
);
|
|
3788
4501
|
});
|
|
3789
4502
|
}
|
|
@@ -3792,42 +4505,23 @@ var DiskLayer = class {
|
|
|
3792
4505
|
* Expired entries are skipped and cleaned up during the scan.
|
|
3793
4506
|
*/
|
|
3794
4507
|
async keys() {
|
|
3795
|
-
let entries;
|
|
3796
|
-
try {
|
|
3797
|
-
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3798
|
-
} catch {
|
|
3799
|
-
return [];
|
|
3800
|
-
}
|
|
3801
|
-
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3802
4508
|
const keys = [];
|
|
3803
|
-
await
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
let raw;
|
|
3807
|
-
try {
|
|
3808
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3809
|
-
} catch {
|
|
3810
|
-
return;
|
|
3811
|
-
}
|
|
3812
|
-
let entry;
|
|
3813
|
-
try {
|
|
3814
|
-
entry = this.deserializeEntry(raw);
|
|
3815
|
-
} catch {
|
|
3816
|
-
await this.safeDelete(filePath);
|
|
3817
|
-
return;
|
|
3818
|
-
}
|
|
3819
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3820
|
-
await this.safeDelete(filePath);
|
|
3821
|
-
return;
|
|
3822
|
-
}
|
|
3823
|
-
keys.push(entry.key);
|
|
3824
|
-
})
|
|
3825
|
-
);
|
|
4509
|
+
await this.scanEntries(async (entry) => {
|
|
4510
|
+
keys.push(entry.key);
|
|
4511
|
+
});
|
|
3826
4512
|
return keys;
|
|
3827
4513
|
}
|
|
4514
|
+
async forEachKey(visitor) {
|
|
4515
|
+
await this.scanEntries(async (entry) => {
|
|
4516
|
+
await visitor(entry.key);
|
|
4517
|
+
});
|
|
4518
|
+
}
|
|
3828
4519
|
async size() {
|
|
3829
|
-
|
|
3830
|
-
|
|
4520
|
+
let count = 0;
|
|
4521
|
+
await this.scanEntries(async () => {
|
|
4522
|
+
count += 1;
|
|
4523
|
+
});
|
|
4524
|
+
return count;
|
|
3831
4525
|
}
|
|
3832
4526
|
async ping() {
|
|
3833
4527
|
try {
|
|
@@ -3861,6 +4555,113 @@ var DiskLayer = class {
|
|
|
3861
4555
|
}
|
|
3862
4556
|
return maxFiles;
|
|
3863
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
|
+
}
|
|
3864
4665
|
deserializeEntry(raw) {
|
|
3865
4666
|
const entry = this.serializer.deserialize(raw);
|
|
3866
4667
|
if (!isDiskEntry(entry)) {
|
|
@@ -3996,29 +4797,38 @@ var MemcachedLayer = class {
|
|
|
3996
4797
|
|
|
3997
4798
|
// src/serialization/MsgpackSerializer.ts
|
|
3998
4799
|
var import_msgpack = require("@msgpack/msgpack");
|
|
3999
|
-
var
|
|
4800
|
+
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4801
|
+
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4802
|
+
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4000
4803
|
var MsgpackSerializer = class {
|
|
4001
4804
|
serialize(value) {
|
|
4002
4805
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4003
4806
|
}
|
|
4004
4807
|
deserialize(payload) {
|
|
4005
4808
|
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
4006
|
-
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized));
|
|
4809
|
+
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
|
|
4007
4810
|
}
|
|
4008
4811
|
};
|
|
4009
|
-
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
|
+
}
|
|
4010
4820
|
if (Array.isArray(value)) {
|
|
4011
|
-
return value.map((entry) => sanitizeMsgpackValue(entry));
|
|
4821
|
+
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4012
4822
|
}
|
|
4013
4823
|
if (!isPlainObject2(value)) {
|
|
4014
4824
|
return value;
|
|
4015
4825
|
}
|
|
4016
4826
|
const sanitized = {};
|
|
4017
4827
|
for (const [key, entry] of Object.entries(value)) {
|
|
4018
|
-
if (
|
|
4828
|
+
if (DANGEROUS_KEYS2.has(key)) {
|
|
4019
4829
|
continue;
|
|
4020
4830
|
}
|
|
4021
|
-
sanitized[key] = sanitizeMsgpackValue(entry);
|
|
4831
|
+
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4022
4832
|
}
|
|
4023
4833
|
return sanitized;
|
|
4024
4834
|
}
|