layercache 1.2.5 → 1.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -21
- package/README.md +254 -912
- package/dist/{chunk-7V7XAB74.js → chunk-4PPBOOXT.js} +37 -3
- package/dist/{chunk-QHWG7QS5.js → chunk-BQLL6IM5.js} +47 -1
- package/dist/{chunk-JC26W3KK.js → chunk-GJBKCFE6.js} +38 -3
- package/dist/cli.cjs +83 -3
- package/dist/cli.js +2 -2
- package/dist/{edge-P07GCO2Y.d.ts → edge-BMmPVqaD.d.cts} +28 -21
- package/dist/{edge-P07GCO2Y.d.cts → edge-BMmPVqaD.d.ts} +28 -21
- package/dist/edge.cjs +74 -5
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +1671 -837
- package/dist/index.d.cts +42 -4
- package/dist/index.d.ts +42 -4
- package/dist/index.js +1327 -608
- package/package.json +29 -3
- package/packages/nestjs/dist/index.cjs +1296 -732
- package/packages/nestjs/dist/index.d.cts +19 -20
- package/packages/nestjs/dist/index.d.ts +19 -20
- package/packages/nestjs/dist/index.js +1296 -732
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
RedisTagIndex
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BQLL6IM5.js";
|
|
4
4
|
import {
|
|
5
5
|
MemoryLayer,
|
|
6
6
|
TagIndex,
|
|
7
7
|
createHonoCacheMiddleware
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-GJBKCFE6.js";
|
|
9
9
|
import {
|
|
10
10
|
PatternMatcher,
|
|
11
11
|
createStoredValueEnvelope,
|
|
@@ -15,33 +15,155 @@ import {
|
|
|
15
15
|
remainingStoredTtlSeconds,
|
|
16
16
|
resolveStoredValue,
|
|
17
17
|
unwrapStoredValue
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-4PPBOOXT.js";
|
|
19
19
|
|
|
20
20
|
// src/CacheStack.ts
|
|
21
21
|
import { EventEmitter } from "events";
|
|
22
22
|
|
|
23
23
|
// src/CacheNamespace.ts
|
|
24
24
|
import { Mutex } from "async-mutex";
|
|
25
|
+
|
|
26
|
+
// src/internal/CacheNamespaceMetrics.ts
|
|
27
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
28
|
+
return {
|
|
29
|
+
hits: 0,
|
|
30
|
+
misses: 0,
|
|
31
|
+
fetches: 0,
|
|
32
|
+
sets: 0,
|
|
33
|
+
deletes: 0,
|
|
34
|
+
backfills: 0,
|
|
35
|
+
invalidations: 0,
|
|
36
|
+
staleHits: 0,
|
|
37
|
+
refreshes: 0,
|
|
38
|
+
refreshErrors: 0,
|
|
39
|
+
writeFailures: 0,
|
|
40
|
+
singleFlightWaits: 0,
|
|
41
|
+
negativeCacheHits: 0,
|
|
42
|
+
circuitBreakerTrips: 0,
|
|
43
|
+
degradedOperations: 0,
|
|
44
|
+
hitsByLayer: {},
|
|
45
|
+
missesByLayer: {},
|
|
46
|
+
latencyByLayer: {},
|
|
47
|
+
resetAt
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function cloneNamespaceMetrics(metrics) {
|
|
51
|
+
return {
|
|
52
|
+
...metrics,
|
|
53
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
54
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
55
|
+
latencyByLayer: Object.fromEntries(
|
|
56
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
57
|
+
)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function diffNamespaceMetrics(before, after) {
|
|
61
|
+
const latencyByLayer = Object.fromEntries(
|
|
62
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
63
|
+
layer,
|
|
64
|
+
{
|
|
65
|
+
avgMs: value.avgMs,
|
|
66
|
+
maxMs: value.maxMs,
|
|
67
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
68
|
+
}
|
|
69
|
+
])
|
|
70
|
+
);
|
|
71
|
+
return {
|
|
72
|
+
hits: after.hits - before.hits,
|
|
73
|
+
misses: after.misses - before.misses,
|
|
74
|
+
fetches: after.fetches - before.fetches,
|
|
75
|
+
sets: after.sets - before.sets,
|
|
76
|
+
deletes: after.deletes - before.deletes,
|
|
77
|
+
backfills: after.backfills - before.backfills,
|
|
78
|
+
invalidations: after.invalidations - before.invalidations,
|
|
79
|
+
staleHits: after.staleHits - before.staleHits,
|
|
80
|
+
refreshes: after.refreshes - before.refreshes,
|
|
81
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
82
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
83
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
84
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
85
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
86
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
87
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
88
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
89
|
+
latencyByLayer,
|
|
90
|
+
resetAt: after.resetAt
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function addNamespaceMetrics(base, delta) {
|
|
94
|
+
return {
|
|
95
|
+
hits: base.hits + delta.hits,
|
|
96
|
+
misses: base.misses + delta.misses,
|
|
97
|
+
fetches: base.fetches + delta.fetches,
|
|
98
|
+
sets: base.sets + delta.sets,
|
|
99
|
+
deletes: base.deletes + delta.deletes,
|
|
100
|
+
backfills: base.backfills + delta.backfills,
|
|
101
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
102
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
103
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
104
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
105
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
106
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
107
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
108
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
109
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
110
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
111
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
112
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
113
|
+
resetAt: base.resetAt
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
function computeNamespaceHitRate(metrics) {
|
|
117
|
+
const total = metrics.hits + metrics.misses;
|
|
118
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
119
|
+
const byLayer = {};
|
|
120
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
121
|
+
for (const layer of layers) {
|
|
122
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
123
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
124
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
125
|
+
}
|
|
126
|
+
return { overall, byLayer };
|
|
127
|
+
}
|
|
128
|
+
function diffMetricMap(before, after) {
|
|
129
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
130
|
+
const result = {};
|
|
131
|
+
for (const key of keys) {
|
|
132
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
function addMetricMap(base, delta) {
|
|
137
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
138
|
+
const result = {};
|
|
139
|
+
for (const key of keys) {
|
|
140
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
141
|
+
}
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/CacheNamespace.ts
|
|
25
146
|
var CacheNamespace = class _CacheNamespace {
|
|
26
147
|
constructor(cache, prefix) {
|
|
27
148
|
this.cache = cache;
|
|
28
149
|
this.prefix = prefix;
|
|
150
|
+
validateNamespaceKey(prefix);
|
|
29
151
|
}
|
|
30
152
|
cache;
|
|
31
153
|
prefix;
|
|
32
154
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
33
|
-
metrics =
|
|
155
|
+
metrics = createEmptyNamespaceMetrics();
|
|
34
156
|
async get(key, fetcher, options) {
|
|
35
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
157
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
36
158
|
}
|
|
37
159
|
async getOrSet(key, fetcher, options) {
|
|
38
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
160
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
39
161
|
}
|
|
40
162
|
/**
|
|
41
163
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
42
164
|
*/
|
|
43
165
|
async getOrThrow(key, fetcher, options) {
|
|
44
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
166
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
45
167
|
}
|
|
46
168
|
async has(key) {
|
|
47
169
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -50,7 +172,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
50
172
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
51
173
|
}
|
|
52
174
|
async set(key, value, options) {
|
|
53
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
175
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
54
176
|
}
|
|
55
177
|
async delete(key) {
|
|
56
178
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -66,7 +188,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
66
188
|
() => this.cache.mget(
|
|
67
189
|
entries.map((entry) => ({
|
|
68
190
|
...entry,
|
|
69
|
-
key: this.qualify(entry.key)
|
|
191
|
+
key: this.qualify(entry.key),
|
|
192
|
+
options: this.qualifyGetOptions(entry.options)
|
|
70
193
|
}))
|
|
71
194
|
)
|
|
72
195
|
);
|
|
@@ -76,16 +199,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
76
199
|
() => this.cache.mset(
|
|
77
200
|
entries.map((entry) => ({
|
|
78
201
|
...entry,
|
|
79
|
-
key: this.qualify(entry.key)
|
|
202
|
+
key: this.qualify(entry.key),
|
|
203
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
80
204
|
}))
|
|
81
205
|
)
|
|
82
206
|
);
|
|
83
207
|
}
|
|
84
208
|
async invalidateByTag(tag) {
|
|
85
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
209
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
86
210
|
}
|
|
87
211
|
async invalidateByTags(tags, mode = "any") {
|
|
88
|
-
await this.trackMetrics(
|
|
212
|
+
await this.trackMetrics(
|
|
213
|
+
() => this.cache.invalidateByTags(
|
|
214
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
215
|
+
mode
|
|
216
|
+
)
|
|
217
|
+
);
|
|
89
218
|
}
|
|
90
219
|
async invalidateByPattern(pattern) {
|
|
91
220
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -97,34 +226,33 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
97
226
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
98
227
|
*/
|
|
99
228
|
async inspect(key) {
|
|
100
|
-
|
|
229
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
230
|
+
if (result === null) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
...result,
|
|
235
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
236
|
+
};
|
|
101
237
|
}
|
|
102
238
|
wrap(keyPrefix, fetcher, options) {
|
|
103
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
239
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
104
240
|
}
|
|
105
241
|
warm(entries, options) {
|
|
106
242
|
return this.cache.warm(
|
|
107
243
|
entries.map((entry) => ({
|
|
108
244
|
...entry,
|
|
109
|
-
key: this.qualify(entry.key)
|
|
245
|
+
key: this.qualify(entry.key),
|
|
246
|
+
options: this.qualifyGetOptions(entry.options)
|
|
110
247
|
})),
|
|
111
248
|
options
|
|
112
249
|
);
|
|
113
250
|
}
|
|
114
251
|
getMetrics() {
|
|
115
|
-
return
|
|
252
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
116
253
|
}
|
|
117
254
|
getHitRate() {
|
|
118
|
-
|
|
119
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
120
|
-
const byLayer = {};
|
|
121
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
122
|
-
for (const layer of layers) {
|
|
123
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
124
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
125
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
126
|
-
}
|
|
127
|
-
return { overall, byLayer };
|
|
255
|
+
return computeNamespaceHitRate(this.metrics);
|
|
128
256
|
}
|
|
129
257
|
/**
|
|
130
258
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -142,12 +270,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
142
270
|
qualify(key) {
|
|
143
271
|
return `${this.prefix}:${key}`;
|
|
144
272
|
}
|
|
273
|
+
qualifyTag(tag) {
|
|
274
|
+
return `${this.prefix}:${tag}`;
|
|
275
|
+
}
|
|
276
|
+
qualifyGetOptions(options) {
|
|
277
|
+
return this.qualifyWriteOptions(options);
|
|
278
|
+
}
|
|
279
|
+
qualifyWrapOptions(options) {
|
|
280
|
+
return this.qualifyWriteOptions(options);
|
|
281
|
+
}
|
|
282
|
+
qualifyWriteOptions(options) {
|
|
283
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
284
|
+
return options;
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
...options,
|
|
288
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
289
|
+
};
|
|
290
|
+
}
|
|
145
291
|
async trackMetrics(operation) {
|
|
146
292
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
147
293
|
const before = this.cache.getMetrics();
|
|
148
294
|
const result = await operation();
|
|
149
295
|
const after = this.cache.getMetrics();
|
|
150
|
-
this.metrics =
|
|
296
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
151
297
|
return result;
|
|
152
298
|
});
|
|
153
299
|
}
|
|
@@ -161,175 +307,567 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
161
307
|
return mutex;
|
|
162
308
|
}
|
|
163
309
|
};
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
310
|
+
function validateNamespaceKey(key) {
|
|
311
|
+
if (key.length === 0) {
|
|
312
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
313
|
+
}
|
|
314
|
+
if (key.length > 256) {
|
|
315
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
316
|
+
}
|
|
317
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
318
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
319
|
+
}
|
|
320
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
321
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
326
|
+
var CacheKeyDiscovery = class {
|
|
327
|
+
constructor(options) {
|
|
328
|
+
this.options = options;
|
|
329
|
+
}
|
|
330
|
+
options;
|
|
331
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
332
|
+
const { tagIndex } = this.options;
|
|
333
|
+
const matches = /* @__PURE__ */ new Set();
|
|
334
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
335
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
336
|
+
matches.add(key);
|
|
337
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
338
|
+
});
|
|
339
|
+
} else {
|
|
340
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
341
|
+
for (const key of initialMatches) {
|
|
342
|
+
matches.add(key);
|
|
343
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
await Promise.all(
|
|
347
|
+
this.options.layers.map(async (layer) => {
|
|
348
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
if (layer.forEachKey) {
|
|
353
|
+
await layer.forEachKey(async (key) => {
|
|
354
|
+
if (key.startsWith(prefix)) {
|
|
355
|
+
matches.add(key);
|
|
356
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const keys = await layer.keys?.();
|
|
362
|
+
for (const key of keys ?? []) {
|
|
363
|
+
if (key.startsWith(prefix)) {
|
|
364
|
+
matches.add(key);
|
|
365
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
} catch (error) {
|
|
369
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
return [...matches];
|
|
374
|
+
}
|
|
375
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
376
|
+
const matches = /* @__PURE__ */ new Set();
|
|
377
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
378
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
379
|
+
matches.add(key);
|
|
380
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
384
|
+
matches.add(key);
|
|
385
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
await Promise.all(
|
|
389
|
+
this.options.layers.map(async (layer) => {
|
|
390
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
if (layer.forEachKey) {
|
|
395
|
+
await layer.forEachKey(async (key) => {
|
|
396
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
397
|
+
matches.add(key);
|
|
398
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const keys = await layer.keys?.();
|
|
404
|
+
for (const key of keys ?? []) {
|
|
405
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
406
|
+
matches.add(key);
|
|
407
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
} catch (error) {
|
|
411
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
);
|
|
415
|
+
return [...matches];
|
|
416
|
+
}
|
|
417
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
418
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
419
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/internal/CacheKeySerialization.ts
|
|
425
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
426
|
+
function normalizeForSerialization(value) {
|
|
427
|
+
if (Array.isArray(value)) {
|
|
428
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
429
|
+
}
|
|
430
|
+
if (value && typeof value === "object") {
|
|
431
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
432
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
433
|
+
return normalized;
|
|
434
|
+
}
|
|
435
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
436
|
+
return normalized;
|
|
437
|
+
}, {});
|
|
438
|
+
}
|
|
439
|
+
return value;
|
|
440
|
+
}
|
|
441
|
+
function serializeKeyPart(value) {
|
|
442
|
+
if (typeof value === "string") {
|
|
443
|
+
return `s:${value}`;
|
|
444
|
+
}
|
|
445
|
+
if (typeof value === "number") {
|
|
446
|
+
return `n:${value}`;
|
|
447
|
+
}
|
|
448
|
+
if (typeof value === "boolean") {
|
|
449
|
+
return `b:${value}`;
|
|
450
|
+
}
|
|
451
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
452
|
+
}
|
|
453
|
+
function serializeOptions(options) {
|
|
454
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
455
|
+
}
|
|
456
|
+
function createInstanceId() {
|
|
457
|
+
if (globalThis.crypto?.randomUUID) {
|
|
458
|
+
return globalThis.crypto.randomUUID();
|
|
459
|
+
}
|
|
460
|
+
const bytes = new Uint8Array(16);
|
|
461
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
462
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
463
|
+
} else {
|
|
464
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
465
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/internal/CacheSnapshotFile.ts
|
|
472
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
473
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
474
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
475
|
+
}
|
|
476
|
+
async function findExistingAncestor(directory, fs2, path) {
|
|
477
|
+
let current = directory;
|
|
478
|
+
while (true) {
|
|
479
|
+
try {
|
|
480
|
+
await fs2.lstat(current);
|
|
481
|
+
return current;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
if (error.code !== "ENOENT") {
|
|
484
|
+
throw error;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
const parent = path.dirname(current);
|
|
488
|
+
if (parent === current) {
|
|
489
|
+
return current;
|
|
490
|
+
}
|
|
491
|
+
current = parent;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
495
|
+
if (filePath.length === 0) {
|
|
496
|
+
throw new Error("filePath must not be empty.");
|
|
497
|
+
}
|
|
498
|
+
if (filePath.includes("\0")) {
|
|
499
|
+
throw new Error("filePath must not contain null bytes.");
|
|
500
|
+
}
|
|
501
|
+
const { promises: fs2 } = await import("fs");
|
|
502
|
+
const path = await import("path");
|
|
503
|
+
const resolved = path.resolve(filePath);
|
|
504
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
505
|
+
if (baseDir === false) {
|
|
506
|
+
return resolved;
|
|
507
|
+
}
|
|
508
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
509
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
510
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
511
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
512
|
+
}
|
|
513
|
+
if (mode === "read") {
|
|
514
|
+
const realTarget = await fs2.realpath(resolved);
|
|
515
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
516
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
517
|
+
}
|
|
518
|
+
return realTarget;
|
|
519
|
+
}
|
|
520
|
+
const parentDir = path.dirname(resolved);
|
|
521
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
|
|
522
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
523
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
524
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
525
|
+
}
|
|
526
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
527
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
528
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
529
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
530
|
+
}
|
|
531
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
532
|
+
try {
|
|
533
|
+
const existing = await fs2.lstat(targetPath);
|
|
534
|
+
if (existing.isSymbolicLink()) {
|
|
535
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
536
|
+
}
|
|
537
|
+
} catch (error) {
|
|
538
|
+
if (error.code !== "ENOENT") {
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return targetPath;
|
|
543
|
+
}
|
|
544
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
545
|
+
if (byteLimit === false) {
|
|
546
|
+
return handle.readFile({ encoding: "utf8" });
|
|
547
|
+
}
|
|
548
|
+
const chunks = [];
|
|
549
|
+
let totalBytes = 0;
|
|
550
|
+
let position = 0;
|
|
551
|
+
while (true) {
|
|
552
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
553
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
554
|
+
if (bytesRead === 0) {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
totalBytes += bytesRead;
|
|
558
|
+
if (totalBytes > byteLimit) {
|
|
559
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
560
|
+
}
|
|
561
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
562
|
+
position += bytesRead;
|
|
563
|
+
}
|
|
564
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/internal/CacheStackGeneration.ts
|
|
568
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
569
|
+
function generationPrefix(generation) {
|
|
570
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
571
|
+
}
|
|
572
|
+
function qualifyGenerationKey(key, generation) {
|
|
573
|
+
const prefix = generationPrefix(generation);
|
|
574
|
+
return prefix ? `${prefix}${key}` : key;
|
|
575
|
+
}
|
|
576
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
577
|
+
return qualifyGenerationKey(pattern, generation);
|
|
578
|
+
}
|
|
579
|
+
function stripGenerationPrefix(key, generation) {
|
|
580
|
+
const prefix = generationPrefix(generation);
|
|
581
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
582
|
+
return key;
|
|
583
|
+
}
|
|
584
|
+
return key.slice(prefix.length);
|
|
585
|
+
}
|
|
586
|
+
function resolveGenerationCleanupTarget({
|
|
587
|
+
previousGeneration,
|
|
588
|
+
nextGeneration,
|
|
589
|
+
generationCleanup
|
|
590
|
+
}) {
|
|
591
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
return previousGeneration;
|
|
595
|
+
}
|
|
596
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
597
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
598
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
599
|
+
}
|
|
600
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
601
|
+
}
|
|
602
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
603
|
+
if (keys.length === 0) {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
607
|
+
const batches = [];
|
|
608
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
609
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
610
|
+
}
|
|
611
|
+
return batches;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/internal/CacheStackMaintenance.ts
|
|
615
|
+
var CacheStackMaintenance = class {
|
|
616
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
617
|
+
writeBehindQueue = [];
|
|
618
|
+
writeBehindTimer;
|
|
619
|
+
writeBehindFlushPromise;
|
|
620
|
+
generationCleanupPromise;
|
|
621
|
+
clearEpoch = 0;
|
|
622
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
623
|
+
if (writeStrategy !== "write-behind") {
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
627
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
this.disposeWriteBehindTimer();
|
|
631
|
+
this.writeBehindTimer = setInterval(() => {
|
|
632
|
+
void flush();
|
|
633
|
+
}, flushIntervalMs);
|
|
634
|
+
this.writeBehindTimer.unref?.();
|
|
635
|
+
}
|
|
636
|
+
disposeWriteBehindTimer() {
|
|
637
|
+
if (!this.writeBehindTimer) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
clearInterval(this.writeBehindTimer);
|
|
641
|
+
this.writeBehindTimer = void 0;
|
|
642
|
+
}
|
|
643
|
+
beginClearEpoch() {
|
|
644
|
+
this.clearEpoch += 1;
|
|
645
|
+
this.keyEpochs.clear();
|
|
646
|
+
this.writeBehindQueue.length = 0;
|
|
647
|
+
}
|
|
648
|
+
currentClearEpoch() {
|
|
649
|
+
return this.clearEpoch;
|
|
650
|
+
}
|
|
651
|
+
currentKeyEpoch(key) {
|
|
652
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
653
|
+
}
|
|
654
|
+
bumpKeyEpochs(keys) {
|
|
655
|
+
for (const key of keys) {
|
|
656
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
660
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
669
|
+
this.writeBehindQueue.push(operation);
|
|
670
|
+
const batchSize = options?.batchSize ?? 100;
|
|
671
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
672
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
673
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
677
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
681
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
682
|
+
await this.writeBehindFlushPromise;
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const batchSize = options?.batchSize ?? 100;
|
|
686
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
687
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
688
|
+
try {
|
|
689
|
+
await this.writeBehindFlushPromise;
|
|
690
|
+
} finally {
|
|
691
|
+
this.writeBehindFlushPromise = void 0;
|
|
692
|
+
}
|
|
693
|
+
if (this.writeBehindQueue.length > 0) {
|
|
694
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
698
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
699
|
+
onError(generation, error);
|
|
700
|
+
});
|
|
701
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
702
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
703
|
+
this.generationCleanupPromise = void 0;
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
async waitForGenerationCleanup() {
|
|
708
|
+
await this.generationCleanupPromise;
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
713
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
714
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
186
715
|
}
|
|
187
|
-
function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
latencyByLayer: Object.fromEntries(
|
|
193
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
194
|
-
)
|
|
195
|
-
};
|
|
716
|
+
function shouldStartBackgroundRefresh({
|
|
717
|
+
isDisconnecting,
|
|
718
|
+
hasRefreshInFlight
|
|
719
|
+
}) {
|
|
720
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
196
721
|
}
|
|
197
|
-
function
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
avgMs: value.avgMs,
|
|
203
|
-
maxMs: value.maxMs,
|
|
204
|
-
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
205
|
-
}
|
|
206
|
-
])
|
|
207
|
-
);
|
|
722
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
723
|
+
if (!gracefulDegradation) {
|
|
724
|
+
return { degrade: false };
|
|
725
|
+
}
|
|
726
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
208
727
|
return {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
fetches: after.fetches - before.fetches,
|
|
212
|
-
sets: after.sets - before.sets,
|
|
213
|
-
deletes: after.deletes - before.deletes,
|
|
214
|
-
backfills: after.backfills - before.backfills,
|
|
215
|
-
invalidations: after.invalidations - before.invalidations,
|
|
216
|
-
staleHits: after.staleHits - before.staleHits,
|
|
217
|
-
refreshes: after.refreshes - before.refreshes,
|
|
218
|
-
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
219
|
-
writeFailures: after.writeFailures - before.writeFailures,
|
|
220
|
-
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
221
|
-
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
222
|
-
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
223
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
224
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
225
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
226
|
-
latencyByLayer,
|
|
227
|
-
resetAt: after.resetAt
|
|
728
|
+
degrade: true,
|
|
729
|
+
degradedUntil: now + retryAfterMs
|
|
228
730
|
};
|
|
229
731
|
}
|
|
230
|
-
function
|
|
732
|
+
function planFreshReadPolicies({
|
|
733
|
+
stored,
|
|
734
|
+
hasFetcher,
|
|
735
|
+
slidingTtl,
|
|
736
|
+
refreshAheadSeconds
|
|
737
|
+
}) {
|
|
738
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
739
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
740
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
231
741
|
return {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
sets: base.sets + delta.sets,
|
|
236
|
-
deletes: base.deletes + delta.deletes,
|
|
237
|
-
backfills: base.backfills + delta.backfills,
|
|
238
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
239
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
240
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
241
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
242
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
243
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
244
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
245
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
246
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
247
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
248
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
249
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
250
|
-
resetAt: base.resetAt
|
|
742
|
+
refreshedStored,
|
|
743
|
+
refreshedStoredTtl,
|
|
744
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
251
745
|
};
|
|
252
746
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
747
|
+
|
|
748
|
+
// src/internal/CacheStackValidation.ts
|
|
749
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
750
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
751
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
752
|
+
function validatePositiveNumber(name, value) {
|
|
753
|
+
if (value === void 0) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
757
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
258
758
|
}
|
|
259
|
-
return result;
|
|
260
759
|
}
|
|
261
|
-
function
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
for (const key of keys) {
|
|
265
|
-
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
760
|
+
function validateNonNegativeNumber(name, value) {
|
|
761
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
762
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
266
763
|
}
|
|
267
|
-
return result;
|
|
268
764
|
}
|
|
269
|
-
function
|
|
765
|
+
function validateLayerNumberOption(name, value) {
|
|
766
|
+
if (value === void 0) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (typeof value === "number") {
|
|
770
|
+
validateNonNegativeNumber(name, value);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
774
|
+
if (layerValue === void 0) {
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
function validateRateLimitOptions(name, options) {
|
|
781
|
+
if (!options) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
785
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
786
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
787
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
788
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
789
|
+
}
|
|
790
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
791
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function validateCacheKey(key) {
|
|
270
795
|
if (key.length === 0) {
|
|
271
|
-
throw new Error("
|
|
796
|
+
throw new Error("Cache key must not be empty.");
|
|
272
797
|
}
|
|
273
|
-
if (key.length >
|
|
274
|
-
throw new Error(
|
|
798
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
799
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
275
800
|
}
|
|
276
801
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
277
|
-
throw new Error("
|
|
802
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
803
|
+
}
|
|
804
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
805
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
278
806
|
}
|
|
807
|
+
return key;
|
|
279
808
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
constructor(options) {
|
|
284
|
-
this.options = options;
|
|
809
|
+
function validateTag(tag) {
|
|
810
|
+
if (tag.length === 0) {
|
|
811
|
+
throw new Error("Cache tag must not be empty.");
|
|
285
812
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const { tagIndex } = this.options;
|
|
289
|
-
const matches = new Set(
|
|
290
|
-
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
291
|
-
);
|
|
292
|
-
await Promise.all(
|
|
293
|
-
this.options.layers.map(async (layer) => {
|
|
294
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
295
|
-
return;
|
|
296
|
-
}
|
|
297
|
-
try {
|
|
298
|
-
const keys = await layer.keys();
|
|
299
|
-
for (const key of keys) {
|
|
300
|
-
if (key.startsWith(prefix)) {
|
|
301
|
-
matches.add(key);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
} catch (error) {
|
|
305
|
-
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
306
|
-
}
|
|
307
|
-
})
|
|
308
|
-
);
|
|
309
|
-
return [...matches];
|
|
813
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
814
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
310
815
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
await Promise.all(
|
|
314
|
-
this.options.layers.map(async (layer) => {
|
|
315
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
|
-
const keys = await layer.keys();
|
|
320
|
-
for (const key of keys) {
|
|
321
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
322
|
-
matches.add(key);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
} catch (error) {
|
|
326
|
-
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
327
|
-
}
|
|
328
|
-
})
|
|
329
|
-
);
|
|
330
|
-
return [...matches];
|
|
816
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
817
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
331
818
|
}
|
|
332
|
-
|
|
819
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
820
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
821
|
+
}
|
|
822
|
+
return tag;
|
|
823
|
+
}
|
|
824
|
+
function validateTags(tags) {
|
|
825
|
+
if (!tags) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
829
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
830
|
+
}
|
|
831
|
+
for (const tag of tags) {
|
|
832
|
+
validateTag(tag);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
function validatePattern(pattern) {
|
|
836
|
+
if (pattern.length === 0) {
|
|
837
|
+
throw new Error("Pattern must not be empty.");
|
|
838
|
+
}
|
|
839
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
840
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
841
|
+
}
|
|
842
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
843
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function validateTtlPolicy(name, policy) {
|
|
847
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if ("alignTo" in policy) {
|
|
851
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
throw new Error(`${name} is invalid.`);
|
|
855
|
+
}
|
|
856
|
+
function validateAdaptiveTtlOptions(options) {
|
|
857
|
+
if (!options || options === true) {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
861
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
862
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
863
|
+
}
|
|
864
|
+
function validateCircuitBreakerOptions(options) {
|
|
865
|
+
if (!options) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
869
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
870
|
+
}
|
|
333
871
|
|
|
334
872
|
// src/internal/CircuitBreakerManager.ts
|
|
335
873
|
var CircuitBreakerManager = class {
|
|
@@ -360,7 +898,6 @@ var CircuitBreakerManager = class {
|
|
|
360
898
|
if (!options) {
|
|
361
899
|
return;
|
|
362
900
|
}
|
|
363
|
-
this.pruneIfNeeded();
|
|
364
901
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
365
902
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
366
903
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -369,6 +906,7 @@ var CircuitBreakerManager = class {
|
|
|
369
906
|
state.openUntil = Date.now() + cooldownMs;
|
|
370
907
|
}
|
|
371
908
|
this.breakers.set(key, state);
|
|
909
|
+
this.pruneIfNeeded();
|
|
372
910
|
}
|
|
373
911
|
recordSuccess(key) {
|
|
374
912
|
this.breakers.delete(key);
|
|
@@ -825,22 +1363,27 @@ var TtlResolver = class {
|
|
|
825
1363
|
|
|
826
1364
|
// src/serialization/JsonSerializer.ts
|
|
827
1365
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1366
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
828
1367
|
var JsonSerializer = class {
|
|
829
1368
|
serialize(value) {
|
|
830
1369
|
return JSON.stringify(value);
|
|
831
1370
|
}
|
|
832
1371
|
deserialize(payload) {
|
|
833
1372
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
834
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1373
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
835
1374
|
}
|
|
836
1375
|
};
|
|
837
1376
|
var MAX_SANITIZE_DEPTH = 200;
|
|
838
|
-
function sanitizeJsonValue(value, depth) {
|
|
1377
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
1378
|
+
state.count += 1;
|
|
1379
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
1380
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1381
|
+
}
|
|
839
1382
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
840
|
-
|
|
1383
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
841
1384
|
}
|
|
842
1385
|
if (Array.isArray(value)) {
|
|
843
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1386
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
844
1387
|
}
|
|
845
1388
|
if (!isPlainObject(value)) {
|
|
846
1389
|
return value;
|
|
@@ -850,7 +1393,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
850
1393
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
851
1394
|
continue;
|
|
852
1395
|
}
|
|
853
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1396
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
854
1397
|
}
|
|
855
1398
|
return sanitized;
|
|
856
1399
|
}
|
|
@@ -900,10 +1443,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
900
1443
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
901
1444
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
902
1445
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
903
|
-
var
|
|
904
|
-
var
|
|
1446
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1447
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1448
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1449
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
905
1450
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
906
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
907
1451
|
var DebugLogger = class {
|
|
908
1452
|
enabled;
|
|
909
1453
|
constructor(enabled) {
|
|
@@ -990,13 +1534,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
990
1534
|
snapshotSerializer = new JsonSerializer();
|
|
991
1535
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
992
1536
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
1537
|
+
maintenance = new CacheStackMaintenance();
|
|
993
1538
|
ttlResolver;
|
|
994
1539
|
circuitBreakerManager;
|
|
995
1540
|
currentGeneration;
|
|
996
|
-
writeBehindQueue = [];
|
|
997
|
-
writeBehindTimer;
|
|
998
|
-
writeBehindFlushPromise;
|
|
999
|
-
generationCleanupPromise;
|
|
1000
1541
|
isDisconnecting = false;
|
|
1001
1542
|
disconnectPromise;
|
|
1002
1543
|
/**
|
|
@@ -1006,7 +1547,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1006
1547
|
* and no `fetcher` is provided.
|
|
1007
1548
|
*/
|
|
1008
1549
|
async get(key, fetcher, options) {
|
|
1009
|
-
const normalizedKey = this.qualifyKey(
|
|
1550
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1010
1551
|
this.validateWriteOptions(options);
|
|
1011
1552
|
await this.awaitStartup("get");
|
|
1012
1553
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1076,7 +1617,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1076
1617
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1077
1618
|
*/
|
|
1078
1619
|
async has(key) {
|
|
1079
|
-
const normalizedKey = this.qualifyKey(
|
|
1620
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1080
1621
|
await this.awaitStartup("has");
|
|
1081
1622
|
for (const layer of this.layers) {
|
|
1082
1623
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1109,7 +1650,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1109
1650
|
* that has it, or null if the key is not found / has no TTL.
|
|
1110
1651
|
*/
|
|
1111
1652
|
async ttl(key) {
|
|
1112
|
-
const normalizedKey = this.qualifyKey(
|
|
1653
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1113
1654
|
await this.awaitStartup("ttl");
|
|
1114
1655
|
for (const layer of this.layers) {
|
|
1115
1656
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1131,7 +1672,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1131
1672
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1132
1673
|
*/
|
|
1133
1674
|
async set(key, value, options) {
|
|
1134
|
-
const normalizedKey = this.qualifyKey(
|
|
1675
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1135
1676
|
this.validateWriteOptions(options);
|
|
1136
1677
|
await this.awaitStartup("set");
|
|
1137
1678
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1140,7 +1681,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1140
1681
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1141
1682
|
*/
|
|
1142
1683
|
async delete(key) {
|
|
1143
|
-
const normalizedKey = this.qualifyKey(
|
|
1684
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1144
1685
|
await this.awaitStartup("delete");
|
|
1145
1686
|
await this.deleteKeys([normalizedKey]);
|
|
1146
1687
|
await this.publishInvalidation({
|
|
@@ -1152,6 +1693,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1152
1693
|
}
|
|
1153
1694
|
async clear() {
|
|
1154
1695
|
await this.awaitStartup("clear");
|
|
1696
|
+
this.maintenance.beginClearEpoch();
|
|
1155
1697
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1156
1698
|
await this.tagIndex.clear();
|
|
1157
1699
|
this.ttlResolver.clearProfiles();
|
|
@@ -1168,7 +1710,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1168
1710
|
return;
|
|
1169
1711
|
}
|
|
1170
1712
|
await this.awaitStartup("mdelete");
|
|
1171
|
-
const normalizedKeys = keys.map((k) =>
|
|
1713
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1172
1714
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1173
1715
|
await this.deleteKeys(cacheKeys);
|
|
1174
1716
|
await this.publishInvalidation({
|
|
@@ -1185,7 +1727,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1185
1727
|
}
|
|
1186
1728
|
const normalizedEntries = entries.map((entry) => ({
|
|
1187
1729
|
...entry,
|
|
1188
|
-
key: this.qualifyKey(
|
|
1730
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1189
1731
|
}));
|
|
1190
1732
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1191
1733
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1194,7 +1736,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1194
1736
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1195
1737
|
return Promise.all(
|
|
1196
1738
|
normalizedEntries.map((entry) => {
|
|
1197
|
-
const optionsSignature =
|
|
1739
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1198
1740
|
const existing = pendingReads.get(entry.key);
|
|
1199
1741
|
if (!existing) {
|
|
1200
1742
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1263,7 +1805,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1263
1805
|
this.assertActive("mset");
|
|
1264
1806
|
const normalizedEntries = entries.map((entry) => ({
|
|
1265
1807
|
...entry,
|
|
1266
|
-
key: this.qualifyKey(
|
|
1808
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1267
1809
|
}));
|
|
1268
1810
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1269
1811
|
await this.awaitStartup("mset");
|
|
@@ -1306,7 +1848,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1306
1848
|
*/
|
|
1307
1849
|
wrap(prefix, fetcher, options = {}) {
|
|
1308
1850
|
return (...args) => {
|
|
1309
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
1851
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1310
1852
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1311
1853
|
return this.get(key, () => fetcher(...args), options);
|
|
1312
1854
|
};
|
|
@@ -1316,11 +1858,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1316
1858
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1317
1859
|
*/
|
|
1318
1860
|
namespace(prefix) {
|
|
1861
|
+
validateNamespaceKey(prefix);
|
|
1319
1862
|
return new CacheNamespace(this, prefix);
|
|
1320
1863
|
}
|
|
1321
1864
|
async invalidateByTag(tag) {
|
|
1865
|
+
validateTag(tag);
|
|
1322
1866
|
await this.awaitStartup("invalidateByTag");
|
|
1323
|
-
const keys = await this.
|
|
1867
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1324
1868
|
await this.deleteKeys(keys);
|
|
1325
1869
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1326
1870
|
}
|
|
@@ -1328,23 +1872,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1328
1872
|
if (tags.length === 0) {
|
|
1329
1873
|
return;
|
|
1330
1874
|
}
|
|
1875
|
+
validateTags(tags);
|
|
1331
1876
|
await this.awaitStartup("invalidateByTags");
|
|
1332
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
1877
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1333
1878
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1879
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1334
1880
|
await this.deleteKeys(keys);
|
|
1335
1881
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1336
1882
|
}
|
|
1337
1883
|
async invalidateByPattern(pattern) {
|
|
1338
|
-
|
|
1884
|
+
validatePattern(pattern);
|
|
1339
1885
|
await this.awaitStartup("invalidateByPattern");
|
|
1340
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
1886
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
1887
|
+
this.qualifyPattern(pattern),
|
|
1888
|
+
this.invalidationMaxKeys()
|
|
1889
|
+
);
|
|
1341
1890
|
await this.deleteKeys(keys);
|
|
1342
1891
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1343
1892
|
}
|
|
1344
1893
|
async invalidateByPrefix(prefix) {
|
|
1345
1894
|
await this.awaitStartup("invalidateByPrefix");
|
|
1346
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1347
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
1895
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
1896
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1348
1897
|
await this.deleteKeys(keys);
|
|
1349
1898
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1350
1899
|
}
|
|
@@ -1402,9 +1951,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1402
1951
|
bumpGeneration(nextGeneration) {
|
|
1403
1952
|
const current = this.currentGeneration ?? 0;
|
|
1404
1953
|
const previousGeneration = this.currentGeneration;
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1954
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
1955
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
1956
|
+
previousGeneration,
|
|
1957
|
+
nextGeneration: updatedGeneration,
|
|
1958
|
+
generationCleanup: this.options.generationCleanup
|
|
1959
|
+
});
|
|
1960
|
+
this.currentGeneration = updatedGeneration;
|
|
1961
|
+
if (generationToCleanup !== null) {
|
|
1962
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
1408
1963
|
}
|
|
1409
1964
|
return this.currentGeneration;
|
|
1410
1965
|
}
|
|
@@ -1414,7 +1969,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1414
1969
|
* Returns `null` if the key does not exist in any layer.
|
|
1415
1970
|
*/
|
|
1416
1971
|
async inspect(key) {
|
|
1417
|
-
const userKey =
|
|
1972
|
+
const userKey = validateCacheKey(key);
|
|
1418
1973
|
const normalizedKey = this.qualifyKey(userKey);
|
|
1419
1974
|
await this.awaitStartup("inspect");
|
|
1420
1975
|
const foundInLayers = [];
|
|
@@ -1451,50 +2006,79 @@ var CacheStack = class extends EventEmitter {
|
|
|
1451
2006
|
}
|
|
1452
2007
|
async exportState() {
|
|
1453
2008
|
await this.awaitStartup("exportState");
|
|
1454
|
-
const
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
const keys = await layer.keys();
|
|
1460
|
-
for (const key of keys) {
|
|
1461
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
1462
|
-
if (exported.has(exportedKey)) {
|
|
1463
|
-
continue;
|
|
1464
|
-
}
|
|
1465
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
1466
|
-
if (stored === null) {
|
|
1467
|
-
continue;
|
|
1468
|
-
}
|
|
1469
|
-
exported.set(exportedKey, {
|
|
1470
|
-
key: exportedKey,
|
|
1471
|
-
value: stored,
|
|
1472
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
return [...exported.values()];
|
|
2009
|
+
const entries = [];
|
|
2010
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2011
|
+
entries.push(entry);
|
|
2012
|
+
});
|
|
2013
|
+
return entries;
|
|
1477
2014
|
}
|
|
1478
2015
|
async importState(entries) {
|
|
1479
2016
|
await this.awaitStartup("importState");
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2017
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2018
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2019
|
+
value: entry.value,
|
|
2020
|
+
ttl: entry.ttl
|
|
2021
|
+
}));
|
|
2022
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2023
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2024
|
+
await Promise.all(
|
|
2025
|
+
batch.map(async (entry) => {
|
|
2026
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2027
|
+
await this.tagIndex.touch(entry.key);
|
|
2028
|
+
})
|
|
2029
|
+
);
|
|
2030
|
+
}
|
|
1487
2031
|
}
|
|
1488
2032
|
async persistToFile(filePath) {
|
|
1489
2033
|
this.assertActive("persistToFile");
|
|
1490
|
-
const snapshot = await this.exportState();
|
|
1491
2034
|
const { promises: fs2 } = await import("fs");
|
|
1492
|
-
|
|
2035
|
+
const path = await import("path");
|
|
2036
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2037
|
+
const tempPath = path.join(
|
|
2038
|
+
path.dirname(targetPath),
|
|
2039
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2040
|
+
);
|
|
2041
|
+
let handle;
|
|
2042
|
+
try {
|
|
2043
|
+
handle = await fs2.open(tempPath, "wx");
|
|
2044
|
+
const openedHandle = handle;
|
|
2045
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2046
|
+
let wroteAny = false;
|
|
2047
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2048
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2049
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2050
|
+
wroteAny = true;
|
|
2051
|
+
});
|
|
2052
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2053
|
+
await openedHandle.close();
|
|
2054
|
+
handle = void 0;
|
|
2055
|
+
await fs2.rename(tempPath, targetPath);
|
|
2056
|
+
} catch (error) {
|
|
2057
|
+
await handle?.close().catch(() => void 0);
|
|
2058
|
+
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2059
|
+
throw error;
|
|
2060
|
+
}
|
|
1493
2061
|
}
|
|
1494
2062
|
async restoreFromFile(filePath) {
|
|
1495
2063
|
this.assertActive("restoreFromFile");
|
|
1496
|
-
const { promises: fs2 } = await import("fs");
|
|
1497
|
-
const
|
|
2064
|
+
const { promises: fs2, constants } = await import("fs");
|
|
2065
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2066
|
+
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2067
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2068
|
+
let raw;
|
|
2069
|
+
try {
|
|
2070
|
+
if (snapshotMaxBytes !== false) {
|
|
2071
|
+
const stat = await handle.stat();
|
|
2072
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2079
|
+
} finally {
|
|
2080
|
+
await handle.close();
|
|
2081
|
+
}
|
|
1498
2082
|
let parsed;
|
|
1499
2083
|
try {
|
|
1500
2084
|
parsed = JSON.parse(raw);
|
|
@@ -1519,12 +2103,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1519
2103
|
await this.startup;
|
|
1520
2104
|
await this.unsubscribeInvalidation?.();
|
|
1521
2105
|
await this.flushWriteBehindQueue();
|
|
1522
|
-
await this.
|
|
2106
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
1523
2107
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1524
|
-
|
|
1525
|
-
clearInterval(this.writeBehindTimer);
|
|
1526
|
-
this.writeBehindTimer = void 0;
|
|
1527
|
-
}
|
|
2108
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
1528
2109
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1529
2110
|
})();
|
|
1530
2111
|
}
|
|
@@ -1538,14 +2119,14 @@ var CacheStack = class extends EventEmitter {
|
|
|
1538
2119
|
await this.handleInvalidationMessage(message);
|
|
1539
2120
|
});
|
|
1540
2121
|
}
|
|
1541
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2122
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1542
2123
|
const fetchTask = async () => {
|
|
1543
2124
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1544
2125
|
if (secondHit.found) {
|
|
1545
2126
|
this.metricsCollector.increment("hits");
|
|
1546
2127
|
return secondHit.value;
|
|
1547
2128
|
}
|
|
1548
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2129
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1549
2130
|
};
|
|
1550
2131
|
const singleFlightTask = async () => {
|
|
1551
2132
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -1555,7 +2136,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1555
2136
|
key,
|
|
1556
2137
|
this.resolveSingleFlightOptions(),
|
|
1557
2138
|
fetchTask,
|
|
1558
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2139
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1559
2140
|
);
|
|
1560
2141
|
};
|
|
1561
2142
|
if (this.options.stampedePrevention === false) {
|
|
@@ -1563,7 +2144,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1563
2144
|
}
|
|
1564
2145
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
1565
2146
|
}
|
|
1566
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2147
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1567
2148
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
1568
2149
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
1569
2150
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1577,9 +2158,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1577
2158
|
}
|
|
1578
2159
|
await this.sleep(pollIntervalMs);
|
|
1579
2160
|
}
|
|
1580
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2161
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1581
2162
|
}
|
|
1582
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2163
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1583
2164
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
1584
2165
|
this.metricsCollector.increment("fetches");
|
|
1585
2166
|
const fetchStart = Date.now();
|
|
@@ -1600,6 +2181,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1600
2181
|
if (!this.shouldNegativeCache(options)) {
|
|
1601
2182
|
return null;
|
|
1602
2183
|
}
|
|
2184
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2185
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2186
|
+
key,
|
|
2187
|
+
expectedClearEpoch,
|
|
2188
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2189
|
+
expectedKeyEpoch,
|
|
2190
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2191
|
+
});
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
1603
2194
|
await this.storeEntry(key, "empty", null, options);
|
|
1604
2195
|
return null;
|
|
1605
2196
|
}
|
|
@@ -1612,11 +2203,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1612
2203
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
1613
2204
|
}
|
|
1614
2205
|
}
|
|
2206
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2207
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2208
|
+
key,
|
|
2209
|
+
expectedClearEpoch,
|
|
2210
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2211
|
+
expectedKeyEpoch,
|
|
2212
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2213
|
+
});
|
|
2214
|
+
return fetched;
|
|
2215
|
+
}
|
|
1615
2216
|
await this.storeEntry(key, "value", fetched, options);
|
|
1616
2217
|
return fetched;
|
|
1617
2218
|
}
|
|
1618
2219
|
async storeEntry(key, kind, value, options) {
|
|
2220
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2221
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
1619
2222
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2223
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
1620
2226
|
if (options?.tags) {
|
|
1621
2227
|
await this.tagIndex.track(key, options.tags);
|
|
1622
2228
|
} else {
|
|
@@ -1631,6 +2237,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1631
2237
|
}
|
|
1632
2238
|
async writeBatch(entries) {
|
|
1633
2239
|
const now = Date.now();
|
|
2240
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2241
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
1634
2242
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1635
2243
|
const immediateOperations = [];
|
|
1636
2244
|
const deferredOperations = [];
|
|
@@ -1647,12 +2255,21 @@ var CacheStack = class extends EventEmitter {
|
|
|
1647
2255
|
}
|
|
1648
2256
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1649
2257
|
const operation = async () => {
|
|
2258
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
const activeEntries = layerEntries.filter(
|
|
2262
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2263
|
+
);
|
|
2264
|
+
if (activeEntries.length === 0) {
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
1650
2267
|
try {
|
|
1651
2268
|
if (layer.setMany) {
|
|
1652
|
-
await layer.setMany(
|
|
2269
|
+
await layer.setMany(activeEntries);
|
|
1653
2270
|
return;
|
|
1654
2271
|
}
|
|
1655
|
-
await Promise.all(
|
|
2272
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1656
2273
|
} catch (error) {
|
|
1657
2274
|
await this.handleLayerFailure(layer, "write", error);
|
|
1658
2275
|
}
|
|
@@ -1665,7 +2282,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1665
2282
|
}
|
|
1666
2283
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1667
2284
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2285
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
1668
2288
|
for (const entry of entries) {
|
|
2289
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2290
|
+
continue;
|
|
2291
|
+
}
|
|
1669
2292
|
if (entry.options?.tags) {
|
|
1670
2293
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1671
2294
|
} else {
|
|
@@ -1767,10 +2390,15 @@ var CacheStack = class extends EventEmitter {
|
|
|
1767
2390
|
}
|
|
1768
2391
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1769
2392
|
const now = Date.now();
|
|
2393
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2394
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
1770
2395
|
const immediateOperations = [];
|
|
1771
2396
|
const deferredOperations = [];
|
|
1772
2397
|
for (const layer of this.layers) {
|
|
1773
2398
|
const operation = async () => {
|
|
2399
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
1774
2402
|
if (this.shouldSkipLayer(layer)) {
|
|
1775
2403
|
return;
|
|
1776
2404
|
}
|
|
@@ -1831,13 +2459,18 @@ var CacheStack = class extends EventEmitter {
|
|
|
1831
2459
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
1832
2460
|
}
|
|
1833
2461
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
1834
|
-
if (
|
|
2462
|
+
if (!shouldStartBackgroundRefresh({
|
|
2463
|
+
isDisconnecting: this.isDisconnecting,
|
|
2464
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
2465
|
+
})) {
|
|
1835
2466
|
return;
|
|
1836
2467
|
}
|
|
2468
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2469
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
1837
2470
|
const refresh = (async () => {
|
|
1838
2471
|
this.metricsCollector.increment("refreshes");
|
|
1839
2472
|
try {
|
|
1840
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2473
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
1841
2474
|
} catch (error) {
|
|
1842
2475
|
this.metricsCollector.increment("refreshErrors");
|
|
1843
2476
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -1847,14 +2480,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1847
2480
|
})();
|
|
1848
2481
|
this.backgroundRefreshes.set(key, refresh);
|
|
1849
2482
|
}
|
|
1850
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2483
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1851
2484
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
1852
2485
|
await this.fetchWithGuards(
|
|
1853
2486
|
key,
|
|
1854
2487
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
1855
2488
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
1856
2489
|
}),
|
|
1857
|
-
options
|
|
2490
|
+
options,
|
|
2491
|
+
expectedClearEpoch,
|
|
2492
|
+
expectedKeyEpoch
|
|
1858
2493
|
);
|
|
1859
2494
|
}
|
|
1860
2495
|
resolveSingleFlightOptions() {
|
|
@@ -1869,6 +2504,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1869
2504
|
if (keys.length === 0) {
|
|
1870
2505
|
return;
|
|
1871
2506
|
}
|
|
2507
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
1872
2508
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
1873
2509
|
for (const key of keys) {
|
|
1874
2510
|
await this.tagIndex.remove(key);
|
|
@@ -1891,21 +2527,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
1891
2527
|
return;
|
|
1892
2528
|
}
|
|
1893
2529
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
1894
|
-
if (localLayers.length === 0) {
|
|
1895
|
-
return;
|
|
1896
|
-
}
|
|
1897
2530
|
if (message.scope === "clear") {
|
|
2531
|
+
this.maintenance.beginClearEpoch();
|
|
1898
2532
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
1899
2533
|
await this.tagIndex.clear();
|
|
1900
2534
|
this.ttlResolver.clearProfiles();
|
|
2535
|
+
this.circuitBreakerManager.clear();
|
|
1901
2536
|
return;
|
|
1902
2537
|
}
|
|
1903
2538
|
const keys = message.keys ?? [];
|
|
2539
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
1904
2540
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
1905
2541
|
if (message.operation !== "write") {
|
|
1906
2542
|
for (const key of keys) {
|
|
1907
2543
|
await this.tagIndex.remove(key);
|
|
1908
2544
|
this.ttlResolver.deleteProfile(key);
|
|
2545
|
+
this.circuitBreakerManager.delete(key);
|
|
1909
2546
|
}
|
|
1910
2547
|
}
|
|
1911
2548
|
}
|
|
@@ -1957,35 +2594,22 @@ var CacheStack = class extends EventEmitter {
|
|
|
1957
2594
|
shouldBroadcastL1Invalidation() {
|
|
1958
2595
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
1959
2596
|
}
|
|
1960
|
-
shouldCleanupGenerations() {
|
|
1961
|
-
return Boolean(this.options.generationCleanup);
|
|
1962
|
-
}
|
|
1963
|
-
generationCleanupBatchSize() {
|
|
1964
|
-
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
1965
|
-
return configured ?? 500;
|
|
1966
|
-
}
|
|
1967
2597
|
scheduleGenerationCleanup(generation) {
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
this.generationCleanupPromise = void 0;
|
|
2598
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
2599
|
+
generation,
|
|
2600
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
2601
|
+
(failedGeneration, error) => {
|
|
2602
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
2603
|
+
generation: failedGeneration,
|
|
2604
|
+
error: this.formatError(error)
|
|
2605
|
+
});
|
|
1977
2606
|
}
|
|
1978
|
-
|
|
2607
|
+
);
|
|
1979
2608
|
}
|
|
1980
2609
|
async cleanupGeneration(generation) {
|
|
1981
2610
|
const prefix = `v${generation}:`;
|
|
1982
2611
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
1983
|
-
|
|
1984
|
-
return;
|
|
1985
|
-
}
|
|
1986
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
1987
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
1988
|
-
const batch = keys.slice(index, index + batchSize);
|
|
2612
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
1989
2613
|
await this.deleteKeys(batch);
|
|
1990
2614
|
await this.publishInvalidation({
|
|
1991
2615
|
scope: "keys",
|
|
@@ -1996,58 +2620,34 @@ var CacheStack = class extends EventEmitter {
|
|
|
1996
2620
|
}
|
|
1997
2621
|
}
|
|
1998
2622
|
initializeWriteBehind(options) {
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
return;
|
|
2005
|
-
}
|
|
2006
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2007
|
-
void this.flushWriteBehindQueue();
|
|
2008
|
-
}, flushIntervalMs);
|
|
2009
|
-
this.writeBehindTimer.unref?.();
|
|
2623
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
2624
|
+
this.options.writeStrategy,
|
|
2625
|
+
options,
|
|
2626
|
+
this.flushWriteBehindQueue.bind(this)
|
|
2627
|
+
);
|
|
2010
2628
|
}
|
|
2011
2629
|
shouldWriteBehind(layer) {
|
|
2012
2630
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2013
2631
|
}
|
|
2014
2632
|
async enqueueWriteBehind(operation) {
|
|
2015
|
-
this.
|
|
2016
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2017
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2018
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2019
|
-
await this.flushWriteBehindQueue();
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2023
|
-
await this.flushWriteBehindQueue();
|
|
2024
|
-
}
|
|
2633
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2025
2634
|
}
|
|
2026
2635
|
async flushWriteBehindQueue() {
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
const
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2035
|
-
const failures = results.filter((result) => result.status === "rejected");
|
|
2036
|
-
if (failures.length > 0) {
|
|
2037
|
-
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2038
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
2039
|
-
failed: failures.length,
|
|
2040
|
-
total: batch.length,
|
|
2041
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2042
|
-
});
|
|
2043
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2044
|
-
}
|
|
2045
|
-
})();
|
|
2046
|
-
await this.writeBehindFlushPromise;
|
|
2047
|
-
this.writeBehindFlushPromise = void 0;
|
|
2048
|
-
if (this.writeBehindQueue.length > 0) {
|
|
2049
|
-
await this.flushWriteBehindQueue();
|
|
2636
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2637
|
+
}
|
|
2638
|
+
async runWriteBehindBatch(batch) {
|
|
2639
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
2640
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
2641
|
+
if (failures.length === 0) {
|
|
2642
|
+
return;
|
|
2050
2643
|
}
|
|
2644
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
2645
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
2646
|
+
failed: failures.length,
|
|
2647
|
+
total: batch.length,
|
|
2648
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2649
|
+
});
|
|
2650
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2051
2651
|
}
|
|
2052
2652
|
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2053
2653
|
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
@@ -2077,32 +2677,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
2077
2677
|
return [];
|
|
2078
2678
|
}
|
|
2079
2679
|
const [firstGroup, ...rest] = groups;
|
|
2080
|
-
if (!firstGroup) {
|
|
2081
|
-
return [];
|
|
2082
|
-
}
|
|
2083
2680
|
const restSets = rest.map((group) => new Set(group));
|
|
2084
2681
|
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2085
2682
|
}
|
|
2086
2683
|
qualifyKey(key) {
|
|
2087
|
-
|
|
2088
|
-
return prefix ? `${prefix}${key}` : key;
|
|
2684
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2089
2685
|
}
|
|
2090
2686
|
qualifyPattern(pattern) {
|
|
2091
|
-
|
|
2092
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2687
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
2093
2688
|
}
|
|
2094
2689
|
stripQualifiedKey(key) {
|
|
2095
|
-
|
|
2096
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
2097
|
-
return key;
|
|
2098
|
-
}
|
|
2099
|
-
return key.slice(prefix.length);
|
|
2100
|
-
}
|
|
2101
|
-
generationPrefix() {
|
|
2102
|
-
if (this.currentGeneration === void 0) {
|
|
2103
|
-
return "";
|
|
2104
|
-
}
|
|
2105
|
-
return `v${this.currentGeneration}:`;
|
|
2690
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2106
2691
|
}
|
|
2107
2692
|
async deleteKeysFromLayers(layers, keys) {
|
|
2108
2693
|
await Promise.all(
|
|
@@ -2137,118 +2722,50 @@ var CacheStack = class extends EventEmitter {
|
|
|
2137
2722
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2138
2723
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2139
2724
|
}
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2725
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
2726
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
2727
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
2728
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
2729
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
2730
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
2731
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
2732
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2733
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2734
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
2735
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
2736
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
2737
|
+
}
|
|
2738
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
2739
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
2740
|
+
}
|
|
2741
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
2742
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
2743
|
+
}
|
|
2744
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
2745
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
2746
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2153
2747
|
if (typeof this.options.generationCleanup === "object") {
|
|
2154
|
-
|
|
2748
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2155
2749
|
}
|
|
2156
2750
|
if (this.options.generation !== void 0) {
|
|
2157
|
-
|
|
2751
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2158
2752
|
}
|
|
2159
2753
|
}
|
|
2160
2754
|
validateWriteOptions(options) {
|
|
2161
2755
|
if (!options) {
|
|
2162
2756
|
return;
|
|
2163
2757
|
}
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
validateLayerNumberOption(name, value) {
|
|
2176
|
-
if (value === void 0) {
|
|
2177
|
-
return;
|
|
2178
|
-
}
|
|
2179
|
-
if (typeof value === "number") {
|
|
2180
|
-
this.validateNonNegativeNumber(name, value);
|
|
2181
|
-
return;
|
|
2182
|
-
}
|
|
2183
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2184
|
-
if (layerValue === void 0) {
|
|
2185
|
-
continue;
|
|
2186
|
-
}
|
|
2187
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
validatePositiveNumber(name, value) {
|
|
2191
|
-
if (value === void 0) {
|
|
2192
|
-
return;
|
|
2193
|
-
}
|
|
2194
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2195
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2196
|
-
}
|
|
2197
|
-
}
|
|
2198
|
-
validateRateLimitOptions(name, options) {
|
|
2199
|
-
if (!options) {
|
|
2200
|
-
return;
|
|
2201
|
-
}
|
|
2202
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2203
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2204
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2205
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2206
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2207
|
-
}
|
|
2208
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2209
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
validateNonNegativeNumber(name, value) {
|
|
2213
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2214
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
validateCacheKey(key) {
|
|
2218
|
-
if (key.length === 0) {
|
|
2219
|
-
throw new Error("Cache key must not be empty.");
|
|
2220
|
-
}
|
|
2221
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2222
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2223
|
-
}
|
|
2224
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2225
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2226
|
-
}
|
|
2227
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2228
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2229
|
-
}
|
|
2230
|
-
return key;
|
|
2231
|
-
}
|
|
2232
|
-
validatePattern(pattern) {
|
|
2233
|
-
if (pattern.length === 0) {
|
|
2234
|
-
throw new Error("Pattern must not be empty.");
|
|
2235
|
-
}
|
|
2236
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2237
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2238
|
-
}
|
|
2239
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2240
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
2241
|
-
}
|
|
2242
|
-
}
|
|
2243
|
-
validateTtlPolicy(name, policy) {
|
|
2244
|
-
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2245
|
-
return;
|
|
2246
|
-
}
|
|
2247
|
-
if ("alignTo" in policy) {
|
|
2248
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2249
|
-
return;
|
|
2250
|
-
}
|
|
2251
|
-
throw new Error(`${name} is invalid.`);
|
|
2758
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
2759
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
2760
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
2761
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
2762
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
2763
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2764
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
2765
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
2766
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2767
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
2768
|
+
validateTags(options.tags);
|
|
2252
2769
|
}
|
|
2253
2770
|
assertActive(operation) {
|
|
2254
2771
|
if (this.isDisconnecting) {
|
|
@@ -2260,56 +2777,39 @@ var CacheStack = class extends EventEmitter {
|
|
|
2260
2777
|
await this.startup;
|
|
2261
2778
|
this.assertActive(operation);
|
|
2262
2779
|
}
|
|
2263
|
-
serializeOptions(options) {
|
|
2264
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2265
|
-
}
|
|
2266
|
-
validateAdaptiveTtlOptions(options) {
|
|
2267
|
-
if (!options || options === true) {
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2271
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2272
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2273
|
-
}
|
|
2274
|
-
validateCircuitBreakerOptions(options) {
|
|
2275
|
-
if (!options) {
|
|
2276
|
-
return;
|
|
2277
|
-
}
|
|
2278
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2279
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2280
|
-
}
|
|
2281
2780
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2282
|
-
const
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2781
|
+
const plan = planFreshReadPolicies({
|
|
2782
|
+
stored: hit.stored,
|
|
2783
|
+
hasFetcher: Boolean(fetcher),
|
|
2784
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
2785
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
2786
|
+
});
|
|
2787
|
+
if (plan.refreshedStored) {
|
|
2287
2788
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
2288
2789
|
const layer = this.layers[index];
|
|
2289
2790
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2290
2791
|
continue;
|
|
2291
2792
|
}
|
|
2292
2793
|
try {
|
|
2293
|
-
await layer.set(key,
|
|
2794
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
2294
2795
|
} catch (error) {
|
|
2295
2796
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
2296
2797
|
}
|
|
2297
2798
|
}
|
|
2298
2799
|
}
|
|
2299
|
-
if (fetcher &&
|
|
2800
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
2300
2801
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
2301
2802
|
}
|
|
2302
2803
|
}
|
|
2303
2804
|
shouldSkipLayer(layer) {
|
|
2304
|
-
|
|
2305
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
2805
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
2306
2806
|
}
|
|
2307
2807
|
async handleLayerFailure(layer, operation, error) {
|
|
2308
|
-
|
|
2808
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
2809
|
+
if (!recovery.degrade) {
|
|
2309
2810
|
throw error;
|
|
2310
2811
|
}
|
|
2311
|
-
|
|
2312
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
2812
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
2313
2813
|
this.metricsCollector.increment("degradedOperations");
|
|
2314
2814
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2315
2815
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -2345,18 +2845,6 @@ var CacheStack = class extends EventEmitter {
|
|
|
2345
2845
|
this.emit("error", { operation, ...context });
|
|
2346
2846
|
}
|
|
2347
2847
|
}
|
|
2348
|
-
serializeKeyPart(value) {
|
|
2349
|
-
if (typeof value === "string") {
|
|
2350
|
-
return `s:${value}`;
|
|
2351
|
-
}
|
|
2352
|
-
if (typeof value === "number") {
|
|
2353
|
-
return `n:${value}`;
|
|
2354
|
-
}
|
|
2355
|
-
if (typeof value === "boolean") {
|
|
2356
|
-
return `b:${value}`;
|
|
2357
|
-
}
|
|
2358
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2359
|
-
}
|
|
2360
2848
|
isCacheSnapshotEntries(value) {
|
|
2361
2849
|
return Array.isArray(value) && value.every((entry) => {
|
|
2362
2850
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2369,54 +2857,72 @@ var CacheStack = class extends EventEmitter {
|
|
|
2369
2857
|
sanitizeSnapshotValue(value) {
|
|
2370
2858
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2371
2859
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2860
|
+
snapshotMaxBytes() {
|
|
2861
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
2862
|
+
}
|
|
2863
|
+
snapshotMaxEntries() {
|
|
2864
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
2865
|
+
}
|
|
2866
|
+
invalidationMaxKeys() {
|
|
2867
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
2868
|
+
}
|
|
2869
|
+
async collectKeysForTag(tag) {
|
|
2870
|
+
const keys = /* @__PURE__ */ new Set();
|
|
2871
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
2872
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
2873
|
+
keys.add(key);
|
|
2874
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2875
|
+
});
|
|
2876
|
+
return [...keys];
|
|
2378
2877
|
}
|
|
2379
|
-
const
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
if (baseDir !== false) {
|
|
2383
|
-
const relative = path.relative(baseDir, resolved);
|
|
2384
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2385
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2386
|
-
}
|
|
2878
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
2879
|
+
keys.add(key);
|
|
2880
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2387
2881
|
}
|
|
2388
|
-
return
|
|
2882
|
+
return [...keys];
|
|
2389
2883
|
}
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2884
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
2885
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
2886
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
2887
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2393
2888
|
}
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2889
|
+
}
|
|
2890
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
2891
|
+
const exported = /* @__PURE__ */ new Set();
|
|
2892
|
+
for (const layer of this.layers) {
|
|
2893
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
2894
|
+
continue;
|
|
2895
|
+
}
|
|
2896
|
+
const visitKey = async (key) => {
|
|
2897
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
2898
|
+
if (exported.has(exportedKey)) {
|
|
2899
|
+
return;
|
|
2398
2900
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2901
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
2902
|
+
if (stored === null) {
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
exported.add(exportedKey);
|
|
2906
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
2907
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
2908
|
+
}
|
|
2909
|
+
await visitor({
|
|
2910
|
+
key: exportedKey,
|
|
2911
|
+
value: stored,
|
|
2912
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
2913
|
+
});
|
|
2914
|
+
};
|
|
2915
|
+
if (layer.forEachKey) {
|
|
2916
|
+
await layer.forEachKey(visitKey);
|
|
2917
|
+
continue;
|
|
2918
|
+
}
|
|
2919
|
+
const keys = await layer.keys?.();
|
|
2920
|
+
for (const key of keys ?? []) {
|
|
2921
|
+
await visitKey(key);
|
|
2922
|
+
}
|
|
2402
2923
|
}
|
|
2403
|
-
return value;
|
|
2404
2924
|
}
|
|
2405
2925
|
};
|
|
2406
|
-
function createInstanceId() {
|
|
2407
|
-
if (globalThis.crypto?.randomUUID) {
|
|
2408
|
-
return globalThis.crypto.randomUUID();
|
|
2409
|
-
}
|
|
2410
|
-
const bytes = new Uint8Array(16);
|
|
2411
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
2412
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
2413
|
-
} else {
|
|
2414
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2415
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
2416
|
-
}
|
|
2417
|
-
}
|
|
2418
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2419
|
-
}
|
|
2420
2926
|
|
|
2421
2927
|
// src/invalidation/RedisInvalidationBus.ts
|
|
2422
2928
|
var RedisInvalidationBus = class {
|
|
@@ -2495,15 +3001,24 @@ var RedisInvalidationBus = class {
|
|
|
2495
3001
|
}
|
|
2496
3002
|
};
|
|
2497
3003
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2498
|
-
|
|
3004
|
+
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3005
|
+
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3006
|
+
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3007
|
+
state.count += 1;
|
|
3008
|
+
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3009
|
+
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3010
|
+
}
|
|
3011
|
+
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3012
|
+
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3013
|
+
}
|
|
2499
3014
|
if (Array.isArray(value)) {
|
|
2500
|
-
return value.map(sanitizeJsonValue2);
|
|
3015
|
+
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
2501
3016
|
}
|
|
2502
3017
|
if (value && typeof value === "object") {
|
|
2503
3018
|
const result = /* @__PURE__ */ Object.create(null);
|
|
2504
3019
|
for (const key of Object.keys(value)) {
|
|
2505
3020
|
if (!DANGEROUS_KEYS.has(key)) {
|
|
2506
|
-
result[key] = sanitizeJsonValue2(value[key]);
|
|
3021
|
+
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
2507
3022
|
}
|
|
2508
3023
|
}
|
|
2509
3024
|
return result;
|
|
@@ -2512,12 +3027,18 @@ function sanitizeJsonValue2(value) {
|
|
|
2512
3027
|
}
|
|
2513
3028
|
|
|
2514
3029
|
// src/http/createCacheStatsHandler.ts
|
|
2515
|
-
function createCacheStatsHandler(cache) {
|
|
2516
|
-
return async (
|
|
2517
|
-
response.statusCode = 200;
|
|
3030
|
+
function createCacheStatsHandler(cache, options = {}) {
|
|
3031
|
+
return async (request, response) => {
|
|
2518
3032
|
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2519
3033
|
response.setHeader?.("cache-control", "no-store");
|
|
2520
3034
|
response.setHeader?.("x-content-type-options", "nosniff");
|
|
3035
|
+
const isAuthorized = options.allowPublicAccess === true || (options.authorize ? await options.authorize(request) : false);
|
|
3036
|
+
if (!isAuthorized) {
|
|
3037
|
+
response.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3038
|
+
response.end(JSON.stringify({ error: "Forbidden" }));
|
|
3039
|
+
return;
|
|
3040
|
+
}
|
|
3041
|
+
response.statusCode = 200;
|
|
2521
3042
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
2522
3043
|
};
|
|
2523
3044
|
}
|
|
@@ -2552,7 +3073,26 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
2552
3073
|
return async (fastify) => {
|
|
2553
3074
|
fastify.decorate("cache", cache);
|
|
2554
3075
|
if (options.exposeStatsRoute === true && fastify.get) {
|
|
2555
|
-
fastify.get(options.statsPath ?? "/cache/stats", async () =>
|
|
3076
|
+
fastify.get(options.statsPath ?? "/cache/stats", async (request, reply) => {
|
|
3077
|
+
const isAuthorized = options.allowPublicStatsRoute === true || (options.authorizeStatsRoute ? await options.authorizeStatsRoute(request) : false);
|
|
3078
|
+
reply.header?.("cache-control", "no-store");
|
|
3079
|
+
reply.header?.("x-content-type-options", "nosniff");
|
|
3080
|
+
if (!isAuthorized) {
|
|
3081
|
+
reply.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3082
|
+
const body2 = { error: "Forbidden" };
|
|
3083
|
+
if (reply.send) {
|
|
3084
|
+
reply.send(body2);
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
return body2;
|
|
3088
|
+
}
|
|
3089
|
+
const body = cache.getStats();
|
|
3090
|
+
if (reply.send) {
|
|
3091
|
+
reply.send(body);
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
return body;
|
|
3095
|
+
});
|
|
2556
3096
|
}
|
|
2557
3097
|
};
|
|
2558
3098
|
}
|
|
@@ -2567,6 +3107,10 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
2567
3107
|
next();
|
|
2568
3108
|
return;
|
|
2569
3109
|
}
|
|
3110
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3111
|
+
next();
|
|
3112
|
+
return;
|
|
3113
|
+
}
|
|
2570
3114
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
2571
3115
|
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
|
|
2572
3116
|
const cached = await cache.get(key, void 0, options);
|
|
@@ -2611,6 +3155,11 @@ function normalizeUrl(url) {
|
|
|
2611
3155
|
|
|
2612
3156
|
// src/integrations/graphql.ts
|
|
2613
3157
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
3158
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3159
|
+
throw new Error(
|
|
3160
|
+
"cacheGraphqlResolver requires a keyResolver or allowImplicitContextCaching=true because resolver output may depend on request context."
|
|
3161
|
+
);
|
|
3162
|
+
}
|
|
2614
3163
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
2615
3164
|
...options,
|
|
2616
3165
|
keyResolver: options.keyResolver
|
|
@@ -2682,6 +3231,11 @@ function instrument(name, tracer, method, attributes) {
|
|
|
2682
3231
|
|
|
2683
3232
|
// src/integrations/trpc.ts
|
|
2684
3233
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
3234
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3235
|
+
throw new Error(
|
|
3236
|
+
"createTrpcCacheMiddleware requires a keyResolver or allowImplicitContextCaching=true because procedure output may depend on request context."
|
|
3237
|
+
);
|
|
3238
|
+
}
|
|
2685
3239
|
return async (context) => {
|
|
2686
3240
|
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
2687
3241
|
let didFetch = false;
|
|
@@ -2706,13 +3260,12 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
|
2706
3260
|
}
|
|
2707
3261
|
|
|
2708
3262
|
// src/layers/RedisLayer.ts
|
|
3263
|
+
import { Readable } from "stream";
|
|
2709
3264
|
import { promisify } from "util";
|
|
2710
|
-
import { brotliCompress,
|
|
3265
|
+
import { brotliCompress, createBrotliDecompress, createGunzip, gzip } from "zlib";
|
|
2711
3266
|
var BATCH_DELETE_SIZE = 500;
|
|
2712
3267
|
var gzipAsync = promisify(gzip);
|
|
2713
|
-
var gunzipAsync = promisify(gunzip);
|
|
2714
3268
|
var brotliCompressAsync = promisify(brotliCompress);
|
|
2715
|
-
var brotliDecompressAsync = promisify(brotliDecompress);
|
|
2716
3269
|
var RedisLayer = class {
|
|
2717
3270
|
name;
|
|
2718
3271
|
defaultTtl;
|
|
@@ -2820,8 +3373,18 @@ var RedisLayer = class {
|
|
|
2820
3373
|
return remaining;
|
|
2821
3374
|
}
|
|
2822
3375
|
async size() {
|
|
2823
|
-
|
|
2824
|
-
|
|
3376
|
+
if (!this.prefix) {
|
|
3377
|
+
return this.client.dbsize();
|
|
3378
|
+
}
|
|
3379
|
+
const pattern = `${this.prefix}*`;
|
|
3380
|
+
let cursor = "0";
|
|
3381
|
+
let count = 0;
|
|
3382
|
+
do {
|
|
3383
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
3384
|
+
cursor = nextCursor;
|
|
3385
|
+
count += keys.length;
|
|
3386
|
+
} while (cursor !== "0");
|
|
3387
|
+
return count;
|
|
2825
3388
|
}
|
|
2826
3389
|
async ping() {
|
|
2827
3390
|
try {
|
|
@@ -2867,6 +3430,17 @@ var RedisLayer = class {
|
|
|
2867
3430
|
}
|
|
2868
3431
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
2869
3432
|
}
|
|
3433
|
+
async forEachKey(visitor) {
|
|
3434
|
+
const pattern = `${this.prefix}*`;
|
|
3435
|
+
let cursor = "0";
|
|
3436
|
+
do {
|
|
3437
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
3438
|
+
cursor = nextCursor;
|
|
3439
|
+
for (const key of keys) {
|
|
3440
|
+
await visitor(this.prefix ? key.slice(this.prefix.length) : key);
|
|
3441
|
+
}
|
|
3442
|
+
} while (cursor !== "0");
|
|
3443
|
+
}
|
|
2870
3444
|
async scanKeys(pattern) {
|
|
2871
3445
|
const matches = [];
|
|
2872
3446
|
let cursor = "0";
|
|
@@ -2881,7 +3455,13 @@ var RedisLayer = class {
|
|
|
2881
3455
|
return `${this.prefix}${key}`;
|
|
2882
3456
|
}
|
|
2883
3457
|
async deserializeOrDelete(key, payload) {
|
|
2884
|
-
|
|
3458
|
+
let decodedPayload;
|
|
3459
|
+
try {
|
|
3460
|
+
decodedPayload = await this.decodePayload(payload);
|
|
3461
|
+
} catch {
|
|
3462
|
+
await this.deleteCorruptedKey(key);
|
|
3463
|
+
return null;
|
|
3464
|
+
}
|
|
2885
3465
|
for (const serializer of this.serializers) {
|
|
2886
3466
|
try {
|
|
2887
3467
|
const value = serializer.deserialize(decodedPayload);
|
|
@@ -2892,12 +3472,15 @@ var RedisLayer = class {
|
|
|
2892
3472
|
} catch {
|
|
2893
3473
|
}
|
|
2894
3474
|
}
|
|
3475
|
+
await this.deleteCorruptedKey(key);
|
|
3476
|
+
return null;
|
|
3477
|
+
}
|
|
3478
|
+
async deleteCorruptedKey(key) {
|
|
2895
3479
|
try {
|
|
2896
3480
|
await this.client.del(this.withPrefix(key));
|
|
2897
3481
|
} catch (deleteError) {
|
|
2898
3482
|
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
2899
3483
|
}
|
|
2900
|
-
return null;
|
|
2901
3484
|
}
|
|
2902
3485
|
async rewriteWithPrimarySerializer(key, value) {
|
|
2903
3486
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -2944,31 +3527,72 @@ var RedisLayer = class {
|
|
|
2944
3527
|
return payload;
|
|
2945
3528
|
}
|
|
2946
3529
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
2947
|
-
|
|
2948
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
2949
|
-
throw new Error(
|
|
2950
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
2951
|
-
);
|
|
2952
|
-
}
|
|
2953
|
-
return decompressed;
|
|
3530
|
+
return this.decompressWithLimit(createGunzip(), payload.subarray(10));
|
|
2954
3531
|
}
|
|
2955
3532
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
2956
|
-
|
|
2957
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
2958
|
-
throw new Error(
|
|
2959
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
2960
|
-
);
|
|
2961
|
-
}
|
|
2962
|
-
return decompressed;
|
|
3533
|
+
return this.decompressWithLimit(createBrotliDecompress(), payload.subarray(12));
|
|
2963
3534
|
}
|
|
2964
3535
|
return payload;
|
|
2965
3536
|
}
|
|
3537
|
+
async decompressWithLimit(decompressor, payload) {
|
|
3538
|
+
return new Promise((resolve2, reject) => {
|
|
3539
|
+
const source = Readable.from(payload);
|
|
3540
|
+
const chunks = [];
|
|
3541
|
+
let totalBytes = 0;
|
|
3542
|
+
let settled = false;
|
|
3543
|
+
const cleanup = () => {
|
|
3544
|
+
decompressor.removeAllListeners();
|
|
3545
|
+
};
|
|
3546
|
+
const fail = (error) => {
|
|
3547
|
+
if (settled) {
|
|
3548
|
+
return;
|
|
3549
|
+
}
|
|
3550
|
+
settled = true;
|
|
3551
|
+
cleanup();
|
|
3552
|
+
source.unpipe(decompressor);
|
|
3553
|
+
source.destroy();
|
|
3554
|
+
decompressor.destroy();
|
|
3555
|
+
reject(error);
|
|
3556
|
+
};
|
|
3557
|
+
decompressor.on("data", (chunk) => {
|
|
3558
|
+
const normalized = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
3559
|
+
totalBytes += normalized.byteLength;
|
|
3560
|
+
if (totalBytes > this.decompressionMaxBytes) {
|
|
3561
|
+
fail(
|
|
3562
|
+
new Error(
|
|
3563
|
+
`Decompressed payload (${totalBytes} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3564
|
+
)
|
|
3565
|
+
);
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
chunks.push(normalized);
|
|
3569
|
+
});
|
|
3570
|
+
decompressor.once("error", (error) => {
|
|
3571
|
+
if (settled) {
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
settled = true;
|
|
3575
|
+
cleanup();
|
|
3576
|
+
reject(error);
|
|
3577
|
+
});
|
|
3578
|
+
decompressor.once("end", () => {
|
|
3579
|
+
if (settled) {
|
|
3580
|
+
return;
|
|
3581
|
+
}
|
|
3582
|
+
settled = true;
|
|
3583
|
+
cleanup();
|
|
3584
|
+
resolve2(Buffer.concat(chunks));
|
|
3585
|
+
});
|
|
3586
|
+
source.pipe(decompressor);
|
|
3587
|
+
});
|
|
3588
|
+
}
|
|
2966
3589
|
};
|
|
2967
3590
|
|
|
2968
3591
|
// src/layers/DiskLayer.ts
|
|
2969
3592
|
import { createHash } from "crypto";
|
|
2970
3593
|
import { promises as fs } from "fs";
|
|
2971
3594
|
import { join, resolve } from "path";
|
|
3595
|
+
var FILE_SCAN_CONCURRENCY = 32;
|
|
2972
3596
|
var DiskLayer = class {
|
|
2973
3597
|
name;
|
|
2974
3598
|
defaultTtl;
|
|
@@ -2976,6 +3600,7 @@ var DiskLayer = class {
|
|
|
2976
3600
|
directory;
|
|
2977
3601
|
serializer;
|
|
2978
3602
|
maxFiles;
|
|
3603
|
+
maxEntryBytes;
|
|
2979
3604
|
writeQueue = Promise.resolve();
|
|
2980
3605
|
constructor(options) {
|
|
2981
3606
|
this.directory = this.resolveDirectory(options.directory);
|
|
@@ -2983,16 +3608,15 @@ var DiskLayer = class {
|
|
|
2983
3608
|
this.name = options.name ?? "disk";
|
|
2984
3609
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2985
3610
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
3611
|
+
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
2986
3612
|
}
|
|
2987
3613
|
async get(key) {
|
|
2988
3614
|
return unwrapStoredValue(await this.getEntry(key));
|
|
2989
3615
|
}
|
|
2990
3616
|
async getEntry(key) {
|
|
2991
3617
|
const filePath = this.keyToPath(key);
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
raw = await fs.readFile(filePath);
|
|
2995
|
-
} catch {
|
|
3618
|
+
const raw = await this.readEntryFile(filePath);
|
|
3619
|
+
if (raw === null) {
|
|
2996
3620
|
return null;
|
|
2997
3621
|
}
|
|
2998
3622
|
let entry;
|
|
@@ -3043,10 +3667,8 @@ var DiskLayer = class {
|
|
|
3043
3667
|
}
|
|
3044
3668
|
async ttl(key) {
|
|
3045
3669
|
const filePath = this.keyToPath(key);
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
raw = await fs.readFile(filePath);
|
|
3049
|
-
} catch {
|
|
3670
|
+
const raw = await this.readEntryFile(filePath);
|
|
3671
|
+
if (raw === null) {
|
|
3050
3672
|
return null;
|
|
3051
3673
|
}
|
|
3052
3674
|
let entry;
|
|
@@ -3070,7 +3692,7 @@ var DiskLayer = class {
|
|
|
3070
3692
|
}
|
|
3071
3693
|
async deleteMany(keys) {
|
|
3072
3694
|
await this.enqueueWrite(async () => {
|
|
3073
|
-
await
|
|
3695
|
+
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
3074
3696
|
});
|
|
3075
3697
|
}
|
|
3076
3698
|
async clear() {
|
|
@@ -3081,8 +3703,8 @@ var DiskLayer = class {
|
|
|
3081
3703
|
} catch {
|
|
3082
3704
|
return;
|
|
3083
3705
|
}
|
|
3084
|
-
await
|
|
3085
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) =>
|
|
3706
|
+
await this.deletePathsWithConcurrency(
|
|
3707
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => join(this.directory, name))
|
|
3086
3708
|
);
|
|
3087
3709
|
});
|
|
3088
3710
|
}
|
|
@@ -3091,42 +3713,23 @@ var DiskLayer = class {
|
|
|
3091
3713
|
* Expired entries are skipped and cleaned up during the scan.
|
|
3092
3714
|
*/
|
|
3093
3715
|
async keys() {
|
|
3094
|
-
let entries;
|
|
3095
|
-
try {
|
|
3096
|
-
entries = await fs.readdir(this.directory);
|
|
3097
|
-
} catch {
|
|
3098
|
-
return [];
|
|
3099
|
-
}
|
|
3100
|
-
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3101
3716
|
const keys = [];
|
|
3102
|
-
await
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
let raw;
|
|
3106
|
-
try {
|
|
3107
|
-
raw = await fs.readFile(filePath);
|
|
3108
|
-
} catch {
|
|
3109
|
-
return;
|
|
3110
|
-
}
|
|
3111
|
-
let entry;
|
|
3112
|
-
try {
|
|
3113
|
-
entry = this.deserializeEntry(raw);
|
|
3114
|
-
} catch {
|
|
3115
|
-
await this.safeDelete(filePath);
|
|
3116
|
-
return;
|
|
3117
|
-
}
|
|
3118
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3119
|
-
await this.safeDelete(filePath);
|
|
3120
|
-
return;
|
|
3121
|
-
}
|
|
3122
|
-
keys.push(entry.key);
|
|
3123
|
-
})
|
|
3124
|
-
);
|
|
3717
|
+
await this.scanEntries(async (entry) => {
|
|
3718
|
+
keys.push(entry.key);
|
|
3719
|
+
});
|
|
3125
3720
|
return keys;
|
|
3126
3721
|
}
|
|
3722
|
+
async forEachKey(visitor) {
|
|
3723
|
+
await this.scanEntries(async (entry) => {
|
|
3724
|
+
await visitor(entry.key);
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3127
3727
|
async size() {
|
|
3128
|
-
|
|
3129
|
-
|
|
3728
|
+
let count = 0;
|
|
3729
|
+
await this.scanEntries(async () => {
|
|
3730
|
+
count += 1;
|
|
3731
|
+
});
|
|
3732
|
+
return count;
|
|
3130
3733
|
}
|
|
3131
3734
|
async ping() {
|
|
3132
3735
|
try {
|
|
@@ -3160,6 +3763,113 @@ var DiskLayer = class {
|
|
|
3160
3763
|
}
|
|
3161
3764
|
return maxFiles;
|
|
3162
3765
|
}
|
|
3766
|
+
normalizeMaxEntryBytes(maxEntryBytes) {
|
|
3767
|
+
if (maxEntryBytes === false) {
|
|
3768
|
+
return false;
|
|
3769
|
+
}
|
|
3770
|
+
const normalized = maxEntryBytes ?? 16 * 1024 * 1024;
|
|
3771
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
3772
|
+
throw new Error("DiskLayer.maxEntryBytes must be a positive number or false.");
|
|
3773
|
+
}
|
|
3774
|
+
return normalized;
|
|
3775
|
+
}
|
|
3776
|
+
async readEntryFile(filePath) {
|
|
3777
|
+
let handle;
|
|
3778
|
+
try {
|
|
3779
|
+
handle = await fs.open(filePath, "r");
|
|
3780
|
+
return await this.readHandleWithLimit(handle);
|
|
3781
|
+
} catch {
|
|
3782
|
+
await this.safeDelete(filePath);
|
|
3783
|
+
return null;
|
|
3784
|
+
} finally {
|
|
3785
|
+
await handle?.close().catch(() => void 0);
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
async readHandleWithLimit(handle) {
|
|
3789
|
+
if (this.maxEntryBytes === false) {
|
|
3790
|
+
return handle.readFile();
|
|
3791
|
+
}
|
|
3792
|
+
const stat = await handle.stat();
|
|
3793
|
+
if (stat.size > this.maxEntryBytes) {
|
|
3794
|
+
throw new Error(`DiskLayer entry exceeds maxEntryBytes limit (${stat.size} bytes > ${this.maxEntryBytes} bytes).`);
|
|
3795
|
+
}
|
|
3796
|
+
const chunks = [];
|
|
3797
|
+
let totalBytes = 0;
|
|
3798
|
+
let position = 0;
|
|
3799
|
+
while (true) {
|
|
3800
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, this.maxEntryBytes - totalBytes + 1));
|
|
3801
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
3802
|
+
if (bytesRead === 0) {
|
|
3803
|
+
break;
|
|
3804
|
+
}
|
|
3805
|
+
totalBytes += bytesRead;
|
|
3806
|
+
if (totalBytes > this.maxEntryBytes) {
|
|
3807
|
+
throw new Error(
|
|
3808
|
+
`DiskLayer entry exceeds maxEntryBytes limit (${totalBytes} bytes > ${this.maxEntryBytes} bytes).`
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
3812
|
+
position += bytesRead;
|
|
3813
|
+
}
|
|
3814
|
+
return Buffer.concat(chunks);
|
|
3815
|
+
}
|
|
3816
|
+
async scanEntries(visitor) {
|
|
3817
|
+
let entries;
|
|
3818
|
+
try {
|
|
3819
|
+
entries = await fs.readdir(this.directory);
|
|
3820
|
+
} catch {
|
|
3821
|
+
return;
|
|
3822
|
+
}
|
|
3823
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3824
|
+
let nextIndex = 0;
|
|
3825
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, lcFiles.length);
|
|
3826
|
+
await Promise.all(
|
|
3827
|
+
Array.from({ length: workerCount }, async () => {
|
|
3828
|
+
while (true) {
|
|
3829
|
+
const currentIndex = nextIndex;
|
|
3830
|
+
nextIndex += 1;
|
|
3831
|
+
const name = lcFiles[currentIndex];
|
|
3832
|
+
if (name === void 0) {
|
|
3833
|
+
return;
|
|
3834
|
+
}
|
|
3835
|
+
const filePath = join(this.directory, name);
|
|
3836
|
+
const raw = await this.readEntryFile(filePath);
|
|
3837
|
+
if (raw === null) {
|
|
3838
|
+
continue;
|
|
3839
|
+
}
|
|
3840
|
+
let entry;
|
|
3841
|
+
try {
|
|
3842
|
+
entry = this.deserializeEntry(raw);
|
|
3843
|
+
} catch {
|
|
3844
|
+
await this.safeDelete(filePath);
|
|
3845
|
+
continue;
|
|
3846
|
+
}
|
|
3847
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3848
|
+
await this.safeDelete(filePath);
|
|
3849
|
+
continue;
|
|
3850
|
+
}
|
|
3851
|
+
await visitor(entry);
|
|
3852
|
+
}
|
|
3853
|
+
})
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3856
|
+
async deletePathsWithConcurrency(paths) {
|
|
3857
|
+
let nextIndex = 0;
|
|
3858
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, paths.length);
|
|
3859
|
+
await Promise.all(
|
|
3860
|
+
Array.from({ length: workerCount }, async () => {
|
|
3861
|
+
while (true) {
|
|
3862
|
+
const currentIndex = nextIndex;
|
|
3863
|
+
nextIndex += 1;
|
|
3864
|
+
const filePath = paths[currentIndex];
|
|
3865
|
+
if (filePath === void 0) {
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
await this.safeDelete(filePath);
|
|
3869
|
+
}
|
|
3870
|
+
})
|
|
3871
|
+
);
|
|
3872
|
+
}
|
|
3163
3873
|
deserializeEntry(raw) {
|
|
3164
3874
|
const entry = this.serializer.deserialize(raw);
|
|
3165
3875
|
if (!isDiskEntry(entry)) {
|
|
@@ -3296,18 +4006,27 @@ var MemcachedLayer = class {
|
|
|
3296
4006
|
// src/serialization/MsgpackSerializer.ts
|
|
3297
4007
|
import { decode, encode } from "@msgpack/msgpack";
|
|
3298
4008
|
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4009
|
+
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4010
|
+
var MAX_SANITIZE_NODES3 = 1e4;
|
|
3299
4011
|
var MsgpackSerializer = class {
|
|
3300
4012
|
serialize(value) {
|
|
3301
4013
|
return Buffer.from(encode(value));
|
|
3302
4014
|
}
|
|
3303
4015
|
deserialize(payload) {
|
|
3304
|
-
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
3305
|
-
return sanitizeMsgpackValue(decode(normalized));
|
|
4016
|
+
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4017
|
+
return sanitizeMsgpackValue(decode(normalized), 0, { count: 0 });
|
|
3306
4018
|
}
|
|
3307
4019
|
};
|
|
3308
|
-
function sanitizeMsgpackValue(value) {
|
|
4020
|
+
function sanitizeMsgpackValue(value, depth, state) {
|
|
4021
|
+
state.count += 1;
|
|
4022
|
+
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4023
|
+
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4024
|
+
}
|
|
4025
|
+
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4026
|
+
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4027
|
+
}
|
|
3309
4028
|
if (Array.isArray(value)) {
|
|
3310
|
-
return value.map((entry) => sanitizeMsgpackValue(entry));
|
|
4029
|
+
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
3311
4030
|
}
|
|
3312
4031
|
if (!isPlainObject2(value)) {
|
|
3313
4032
|
return value;
|
|
@@ -3317,7 +4036,7 @@ function sanitizeMsgpackValue(value) {
|
|
|
3317
4036
|
if (DANGEROUS_KEYS2.has(key)) {
|
|
3318
4037
|
continue;
|
|
3319
4038
|
}
|
|
3320
|
-
sanitized[key] = sanitizeMsgpackValue(entry);
|
|
4039
|
+
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
3321
4040
|
}
|
|
3322
4041
|
return sanitized;
|
|
3323
4042
|
}
|