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.cjs
CHANGED
|
@@ -62,26 +62,148 @@ var import_node_events = require("events");
|
|
|
62
62
|
|
|
63
63
|
// src/CacheNamespace.ts
|
|
64
64
|
var import_async_mutex = require("async-mutex");
|
|
65
|
+
|
|
66
|
+
// src/internal/CacheNamespaceMetrics.ts
|
|
67
|
+
function createEmptyNamespaceMetrics(resetAt = Date.now()) {
|
|
68
|
+
return {
|
|
69
|
+
hits: 0,
|
|
70
|
+
misses: 0,
|
|
71
|
+
fetches: 0,
|
|
72
|
+
sets: 0,
|
|
73
|
+
deletes: 0,
|
|
74
|
+
backfills: 0,
|
|
75
|
+
invalidations: 0,
|
|
76
|
+
staleHits: 0,
|
|
77
|
+
refreshes: 0,
|
|
78
|
+
refreshErrors: 0,
|
|
79
|
+
writeFailures: 0,
|
|
80
|
+
singleFlightWaits: 0,
|
|
81
|
+
negativeCacheHits: 0,
|
|
82
|
+
circuitBreakerTrips: 0,
|
|
83
|
+
degradedOperations: 0,
|
|
84
|
+
hitsByLayer: {},
|
|
85
|
+
missesByLayer: {},
|
|
86
|
+
latencyByLayer: {},
|
|
87
|
+
resetAt
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function cloneNamespaceMetrics(metrics) {
|
|
91
|
+
return {
|
|
92
|
+
...metrics,
|
|
93
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
94
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
95
|
+
latencyByLayer: Object.fromEntries(
|
|
96
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
97
|
+
)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function diffNamespaceMetrics(before, after) {
|
|
101
|
+
const latencyByLayer = Object.fromEntries(
|
|
102
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
103
|
+
layer,
|
|
104
|
+
{
|
|
105
|
+
avgMs: value.avgMs,
|
|
106
|
+
maxMs: value.maxMs,
|
|
107
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
108
|
+
}
|
|
109
|
+
])
|
|
110
|
+
);
|
|
111
|
+
return {
|
|
112
|
+
hits: after.hits - before.hits,
|
|
113
|
+
misses: after.misses - before.misses,
|
|
114
|
+
fetches: after.fetches - before.fetches,
|
|
115
|
+
sets: after.sets - before.sets,
|
|
116
|
+
deletes: after.deletes - before.deletes,
|
|
117
|
+
backfills: after.backfills - before.backfills,
|
|
118
|
+
invalidations: after.invalidations - before.invalidations,
|
|
119
|
+
staleHits: after.staleHits - before.staleHits,
|
|
120
|
+
refreshes: after.refreshes - before.refreshes,
|
|
121
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
122
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
123
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
124
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
125
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
126
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
127
|
+
hitsByLayer: diffMetricMap(before.hitsByLayer, after.hitsByLayer),
|
|
128
|
+
missesByLayer: diffMetricMap(before.missesByLayer, after.missesByLayer),
|
|
129
|
+
latencyByLayer,
|
|
130
|
+
resetAt: after.resetAt
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
function addNamespaceMetrics(base, delta) {
|
|
134
|
+
return {
|
|
135
|
+
hits: base.hits + delta.hits,
|
|
136
|
+
misses: base.misses + delta.misses,
|
|
137
|
+
fetches: base.fetches + delta.fetches,
|
|
138
|
+
sets: base.sets + delta.sets,
|
|
139
|
+
deletes: base.deletes + delta.deletes,
|
|
140
|
+
backfills: base.backfills + delta.backfills,
|
|
141
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
142
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
143
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
144
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
145
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
146
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
147
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
148
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
149
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
150
|
+
hitsByLayer: addMetricMap(base.hitsByLayer, delta.hitsByLayer),
|
|
151
|
+
missesByLayer: addMetricMap(base.missesByLayer, delta.missesByLayer),
|
|
152
|
+
latencyByLayer: cloneNamespaceMetrics(delta).latencyByLayer,
|
|
153
|
+
resetAt: base.resetAt
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function computeNamespaceHitRate(metrics) {
|
|
157
|
+
const total = metrics.hits + metrics.misses;
|
|
158
|
+
const overall = total === 0 ? 0 : metrics.hits / total;
|
|
159
|
+
const byLayer = {};
|
|
160
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(metrics.hitsByLayer), ...Object.keys(metrics.missesByLayer)]);
|
|
161
|
+
for (const layer of layers) {
|
|
162
|
+
const hits = metrics.hitsByLayer[layer] ?? 0;
|
|
163
|
+
const misses = metrics.missesByLayer[layer] ?? 0;
|
|
164
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
165
|
+
}
|
|
166
|
+
return { overall, byLayer };
|
|
167
|
+
}
|
|
168
|
+
function diffMetricMap(before, after) {
|
|
169
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
170
|
+
const result = {};
|
|
171
|
+
for (const key of keys) {
|
|
172
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
function addMetricMap(base, delta) {
|
|
177
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
178
|
+
const result = {};
|
|
179
|
+
for (const key of keys) {
|
|
180
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/CacheNamespace.ts
|
|
65
186
|
var CacheNamespace = class _CacheNamespace {
|
|
66
187
|
constructor(cache, prefix) {
|
|
67
188
|
this.cache = cache;
|
|
68
189
|
this.prefix = prefix;
|
|
190
|
+
validateNamespaceKey(prefix);
|
|
69
191
|
}
|
|
70
192
|
cache;
|
|
71
193
|
prefix;
|
|
72
194
|
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
73
|
-
metrics =
|
|
195
|
+
metrics = createEmptyNamespaceMetrics();
|
|
74
196
|
async get(key, fetcher, options) {
|
|
75
|
-
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
197
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
76
198
|
}
|
|
77
199
|
async getOrSet(key, fetcher, options) {
|
|
78
|
-
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
200
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
79
201
|
}
|
|
80
202
|
/**
|
|
81
203
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
82
204
|
*/
|
|
83
205
|
async getOrThrow(key, fetcher, options) {
|
|
84
|
-
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
206
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, this.qualifyGetOptions(options)));
|
|
85
207
|
}
|
|
86
208
|
async has(key) {
|
|
87
209
|
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
@@ -90,7 +212,7 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
90
212
|
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
91
213
|
}
|
|
92
214
|
async set(key, value, options) {
|
|
93
|
-
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
215
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, this.qualifyWriteOptions(options)));
|
|
94
216
|
}
|
|
95
217
|
async delete(key) {
|
|
96
218
|
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
@@ -106,7 +228,8 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
106
228
|
() => this.cache.mget(
|
|
107
229
|
entries.map((entry) => ({
|
|
108
230
|
...entry,
|
|
109
|
-
key: this.qualify(entry.key)
|
|
231
|
+
key: this.qualify(entry.key),
|
|
232
|
+
options: this.qualifyGetOptions(entry.options)
|
|
110
233
|
}))
|
|
111
234
|
)
|
|
112
235
|
);
|
|
@@ -116,16 +239,22 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
116
239
|
() => this.cache.mset(
|
|
117
240
|
entries.map((entry) => ({
|
|
118
241
|
...entry,
|
|
119
|
-
key: this.qualify(entry.key)
|
|
242
|
+
key: this.qualify(entry.key),
|
|
243
|
+
options: this.qualifyWriteOptions(entry.options)
|
|
120
244
|
}))
|
|
121
245
|
)
|
|
122
246
|
);
|
|
123
247
|
}
|
|
124
248
|
async invalidateByTag(tag) {
|
|
125
|
-
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
249
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(this.qualifyTag(tag)));
|
|
126
250
|
}
|
|
127
251
|
async invalidateByTags(tags, mode = "any") {
|
|
128
|
-
await this.trackMetrics(
|
|
252
|
+
await this.trackMetrics(
|
|
253
|
+
() => this.cache.invalidateByTags(
|
|
254
|
+
tags.map((tag) => this.qualifyTag(tag)),
|
|
255
|
+
mode
|
|
256
|
+
)
|
|
257
|
+
);
|
|
129
258
|
}
|
|
130
259
|
async invalidateByPattern(pattern) {
|
|
131
260
|
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
@@ -137,34 +266,33 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
137
266
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
138
267
|
*/
|
|
139
268
|
async inspect(key) {
|
|
140
|
-
|
|
269
|
+
const result = await this.cache.inspect(this.qualify(key));
|
|
270
|
+
if (result === null) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
...result,
|
|
275
|
+
tags: result.tags.filter((tag) => tag.startsWith(`${this.prefix}:`)).map((tag) => tag.slice(this.prefix.length + 1))
|
|
276
|
+
};
|
|
141
277
|
}
|
|
142
278
|
wrap(keyPrefix, fetcher, options) {
|
|
143
|
-
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
279
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, this.qualifyWrapOptions(options));
|
|
144
280
|
}
|
|
145
281
|
warm(entries, options) {
|
|
146
282
|
return this.cache.warm(
|
|
147
283
|
entries.map((entry) => ({
|
|
148
284
|
...entry,
|
|
149
|
-
key: this.qualify(entry.key)
|
|
285
|
+
key: this.qualify(entry.key),
|
|
286
|
+
options: this.qualifyGetOptions(entry.options)
|
|
150
287
|
})),
|
|
151
288
|
options
|
|
152
289
|
);
|
|
153
290
|
}
|
|
154
291
|
getMetrics() {
|
|
155
|
-
return
|
|
292
|
+
return cloneNamespaceMetrics(this.metrics);
|
|
156
293
|
}
|
|
157
294
|
getHitRate() {
|
|
158
|
-
|
|
159
|
-
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
160
|
-
const byLayer = {};
|
|
161
|
-
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
162
|
-
for (const layer of layers) {
|
|
163
|
-
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
164
|
-
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
165
|
-
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
166
|
-
}
|
|
167
|
-
return { overall, byLayer };
|
|
295
|
+
return computeNamespaceHitRate(this.metrics);
|
|
168
296
|
}
|
|
169
297
|
/**
|
|
170
298
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -182,12 +310,30 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
182
310
|
qualify(key) {
|
|
183
311
|
return `${this.prefix}:${key}`;
|
|
184
312
|
}
|
|
313
|
+
qualifyTag(tag) {
|
|
314
|
+
return `${this.prefix}:${tag}`;
|
|
315
|
+
}
|
|
316
|
+
qualifyGetOptions(options) {
|
|
317
|
+
return this.qualifyWriteOptions(options);
|
|
318
|
+
}
|
|
319
|
+
qualifyWrapOptions(options) {
|
|
320
|
+
return this.qualifyWriteOptions(options);
|
|
321
|
+
}
|
|
322
|
+
qualifyWriteOptions(options) {
|
|
323
|
+
if (!options?.tags || options.tags.length === 0) {
|
|
324
|
+
return options;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
...options,
|
|
328
|
+
tags: options.tags.map((tag) => this.qualifyTag(tag))
|
|
329
|
+
};
|
|
330
|
+
}
|
|
185
331
|
async trackMetrics(operation) {
|
|
186
332
|
return this.getMetricsMutex().runExclusive(async () => {
|
|
187
333
|
const before = this.cache.getMetrics();
|
|
188
334
|
const result = await operation();
|
|
189
335
|
const after = this.cache.getMetrics();
|
|
190
|
-
this.metrics =
|
|
336
|
+
this.metrics = addNamespaceMetrics(this.metrics, diffNamespaceMetrics(before, after));
|
|
191
337
|
return result;
|
|
192
338
|
});
|
|
193
339
|
}
|
|
@@ -201,223 +347,773 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
201
347
|
return mutex;
|
|
202
348
|
}
|
|
203
349
|
};
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
singleFlightWaits: 0,
|
|
218
|
-
negativeCacheHits: 0,
|
|
219
|
-
circuitBreakerTrips: 0,
|
|
220
|
-
degradedOperations: 0,
|
|
221
|
-
hitsByLayer: {},
|
|
222
|
-
missesByLayer: {},
|
|
223
|
-
latencyByLayer: {},
|
|
224
|
-
resetAt: Date.now()
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
function cloneMetrics(metrics) {
|
|
228
|
-
return {
|
|
229
|
-
...metrics,
|
|
230
|
-
hitsByLayer: { ...metrics.hitsByLayer },
|
|
231
|
-
missesByLayer: { ...metrics.missesByLayer },
|
|
232
|
-
latencyByLayer: Object.fromEntries(
|
|
233
|
-
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
234
|
-
)
|
|
235
|
-
};
|
|
350
|
+
function validateNamespaceKey(key) {
|
|
351
|
+
if (key.length === 0) {
|
|
352
|
+
throw new Error("Namespace prefix must not be empty.");
|
|
353
|
+
}
|
|
354
|
+
if (key.length > 256) {
|
|
355
|
+
throw new Error("Namespace prefix must be at most 256 characters.");
|
|
356
|
+
}
|
|
357
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
358
|
+
throw new Error("Namespace prefix contains unsupported control characters.");
|
|
359
|
+
}
|
|
360
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
361
|
+
throw new Error("Namespace prefix contains unsupported surrogate code points.");
|
|
362
|
+
}
|
|
236
363
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
364
|
+
|
|
365
|
+
// src/invalidation/PatternMatcher.ts
|
|
366
|
+
var PatternMatcher = class _PatternMatcher {
|
|
367
|
+
/**
|
|
368
|
+
* Tests whether a glob-style pattern matches a value.
|
|
369
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
370
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
371
|
+
* quadratic memory usage on long patterns/keys.
|
|
372
|
+
*/
|
|
373
|
+
static matches(pattern, value) {
|
|
374
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
378
|
+
*/
|
|
379
|
+
static matchLinear(pattern, value) {
|
|
380
|
+
let patternIndex = 0;
|
|
381
|
+
let valueIndex = 0;
|
|
382
|
+
let starIndex = -1;
|
|
383
|
+
let backtrackValueIndex = 0;
|
|
384
|
+
while (valueIndex < value.length) {
|
|
385
|
+
const patternChar = pattern[patternIndex];
|
|
386
|
+
const valueChar = value[valueIndex];
|
|
387
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
388
|
+
starIndex = patternIndex;
|
|
389
|
+
patternIndex += 1;
|
|
390
|
+
backtrackValueIndex = valueIndex;
|
|
391
|
+
continue;
|
|
245
392
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
264
|
-
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
265
|
-
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
266
|
-
latencyByLayer,
|
|
267
|
-
resetAt: after.resetAt
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
function addMetrics(base, delta) {
|
|
271
|
-
return {
|
|
272
|
-
hits: base.hits + delta.hits,
|
|
273
|
-
misses: base.misses + delta.misses,
|
|
274
|
-
fetches: base.fetches + delta.fetches,
|
|
275
|
-
sets: base.sets + delta.sets,
|
|
276
|
-
deletes: base.deletes + delta.deletes,
|
|
277
|
-
backfills: base.backfills + delta.backfills,
|
|
278
|
-
invalidations: base.invalidations + delta.invalidations,
|
|
279
|
-
staleHits: base.staleHits + delta.staleHits,
|
|
280
|
-
refreshes: base.refreshes + delta.refreshes,
|
|
281
|
-
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
282
|
-
writeFailures: base.writeFailures + delta.writeFailures,
|
|
283
|
-
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
284
|
-
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
285
|
-
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
286
|
-
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
287
|
-
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
288
|
-
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
289
|
-
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
290
|
-
resetAt: base.resetAt
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
function diffMap(before, after) {
|
|
294
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
295
|
-
const result = {};
|
|
296
|
-
for (const key of keys) {
|
|
297
|
-
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
393
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
394
|
+
patternIndex += 1;
|
|
395
|
+
valueIndex += 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (starIndex !== -1) {
|
|
399
|
+
patternIndex = starIndex + 1;
|
|
400
|
+
backtrackValueIndex += 1;
|
|
401
|
+
valueIndex = backtrackValueIndex;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
407
|
+
patternIndex += 1;
|
|
408
|
+
}
|
|
409
|
+
return patternIndex === pattern.length;
|
|
298
410
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/internal/CacheKeyDiscovery.ts
|
|
414
|
+
var CacheKeyDiscovery = class {
|
|
415
|
+
constructor(options) {
|
|
416
|
+
this.options = options;
|
|
417
|
+
}
|
|
418
|
+
options;
|
|
419
|
+
async collectKeysWithPrefix(prefix, maxMatches = false) {
|
|
420
|
+
const { tagIndex } = this.options;
|
|
421
|
+
const matches = /* @__PURE__ */ new Set();
|
|
422
|
+
if (tagIndex.forEachKeyForPrefix) {
|
|
423
|
+
await tagIndex.forEachKeyForPrefix(prefix, async (key) => {
|
|
424
|
+
matches.add(key);
|
|
425
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
426
|
+
});
|
|
427
|
+
} else {
|
|
428
|
+
const initialMatches = tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`);
|
|
429
|
+
for (const key of initialMatches) {
|
|
430
|
+
matches.add(key);
|
|
431
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
await Promise.all(
|
|
435
|
+
this.options.layers.map(async (layer) => {
|
|
436
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
try {
|
|
440
|
+
if (layer.forEachKey) {
|
|
441
|
+
await layer.forEachKey(async (key) => {
|
|
442
|
+
if (key.startsWith(prefix)) {
|
|
443
|
+
matches.add(key);
|
|
444
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const keys = await layer.keys?.();
|
|
450
|
+
for (const key of keys ?? []) {
|
|
451
|
+
if (key.startsWith(prefix)) {
|
|
452
|
+
matches.add(key);
|
|
453
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
);
|
|
461
|
+
return [...matches];
|
|
462
|
+
}
|
|
463
|
+
async collectKeysMatchingPattern(pattern, maxMatches = false) {
|
|
464
|
+
const matches = /* @__PURE__ */ new Set();
|
|
465
|
+
if (this.options.tagIndex.forEachKeyMatchingPattern) {
|
|
466
|
+
await this.options.tagIndex.forEachKeyMatchingPattern(pattern, async (key) => {
|
|
467
|
+
matches.add(key);
|
|
468
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
469
|
+
});
|
|
470
|
+
} else {
|
|
471
|
+
for (const key of await this.options.tagIndex.matchPattern(pattern)) {
|
|
472
|
+
matches.add(key);
|
|
473
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
await Promise.all(
|
|
477
|
+
this.options.layers.map(async (layer) => {
|
|
478
|
+
if (!layer.keys && !layer.forEachKey || this.options.shouldSkipLayer(layer)) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
if (layer.forEachKey) {
|
|
483
|
+
await layer.forEachKey(async (key) => {
|
|
484
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
485
|
+
matches.add(key);
|
|
486
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const keys = await layer.keys?.();
|
|
492
|
+
for (const key of keys ?? []) {
|
|
493
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
494
|
+
matches.add(key);
|
|
495
|
+
this.assertWithinMatchLimit(matches, maxMatches);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
} catch (error) {
|
|
499
|
+
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
500
|
+
}
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
return [...matches];
|
|
504
|
+
}
|
|
505
|
+
assertWithinMatchLimit(matches, maxMatches) {
|
|
506
|
+
if (maxMatches !== false && matches.size > maxMatches) {
|
|
507
|
+
throw new Error(`Invalidation matched too many keys (${matches.size} > ${maxMatches}).`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// src/internal/CacheKeySerialization.ts
|
|
513
|
+
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
514
|
+
function normalizeForSerialization(value) {
|
|
515
|
+
if (Array.isArray(value)) {
|
|
516
|
+
return value.map((entry) => normalizeForSerialization(entry));
|
|
517
|
+
}
|
|
518
|
+
if (value && typeof value === "object") {
|
|
519
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
520
|
+
if (DANGEROUS_OBJECT_KEYS.has(key)) {
|
|
521
|
+
return normalized;
|
|
522
|
+
}
|
|
523
|
+
normalized[key] = normalizeForSerialization(value[key]);
|
|
524
|
+
return normalized;
|
|
525
|
+
}, {});
|
|
526
|
+
}
|
|
527
|
+
return value;
|
|
528
|
+
}
|
|
529
|
+
function serializeKeyPart(value) {
|
|
530
|
+
if (typeof value === "string") {
|
|
531
|
+
return `s:${value}`;
|
|
532
|
+
}
|
|
533
|
+
if (typeof value === "number") {
|
|
534
|
+
return `n:${value}`;
|
|
535
|
+
}
|
|
536
|
+
if (typeof value === "boolean") {
|
|
537
|
+
return `b:${value}`;
|
|
538
|
+
}
|
|
539
|
+
return `j:${JSON.stringify(normalizeForSerialization(value))}`;
|
|
540
|
+
}
|
|
541
|
+
function serializeOptions(options) {
|
|
542
|
+
return JSON.stringify(normalizeForSerialization(options) ?? null);
|
|
543
|
+
}
|
|
544
|
+
function createInstanceId() {
|
|
545
|
+
if (globalThis.crypto?.randomUUID) {
|
|
546
|
+
return globalThis.crypto.randomUUID();
|
|
547
|
+
}
|
|
548
|
+
const bytes = new Uint8Array(16);
|
|
549
|
+
if (globalThis.crypto?.getRandomValues) {
|
|
550
|
+
globalThis.crypto.getRandomValues(bytes);
|
|
551
|
+
} else {
|
|
552
|
+
for (let i = 0; i < bytes.length; i += 1) {
|
|
553
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return `layercache-${Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// src/internal/CacheSnapshotFile.ts
|
|
560
|
+
function isWithinSnapshotBase(realBaseDir, candidatePath, pathSeparator, path) {
|
|
561
|
+
const relative = path.relative(realBaseDir, candidatePath);
|
|
562
|
+
return !(relative === ".." || relative.startsWith(`..${pathSeparator}`) || path.isAbsolute(relative));
|
|
563
|
+
}
|
|
564
|
+
async function findExistingAncestor(directory, fs2, path) {
|
|
565
|
+
let current = directory;
|
|
566
|
+
while (true) {
|
|
567
|
+
try {
|
|
568
|
+
await fs2.lstat(current);
|
|
569
|
+
return current;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error.code !== "ENOENT") {
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const parent = path.dirname(current);
|
|
576
|
+
if (parent === current) {
|
|
577
|
+
return current;
|
|
578
|
+
}
|
|
579
|
+
current = parent;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
async function validateSnapshotFilePath(filePath, mode, snapshotBaseDir, cwd = process.cwd()) {
|
|
583
|
+
if (filePath.length === 0) {
|
|
584
|
+
throw new Error("filePath must not be empty.");
|
|
585
|
+
}
|
|
586
|
+
if (filePath.includes("\0")) {
|
|
587
|
+
throw new Error("filePath must not contain null bytes.");
|
|
588
|
+
}
|
|
589
|
+
const { promises: fs2 } = await import("fs");
|
|
590
|
+
const path = await import("path");
|
|
591
|
+
const resolved = path.resolve(filePath);
|
|
592
|
+
const baseDir = snapshotBaseDir === false ? false : path.resolve(snapshotBaseDir ?? cwd);
|
|
593
|
+
if (baseDir === false) {
|
|
594
|
+
return resolved;
|
|
595
|
+
}
|
|
596
|
+
await fs2.mkdir(baseDir, { recursive: true });
|
|
597
|
+
const realBaseDir = await fs2.realpath(baseDir);
|
|
598
|
+
if (!isWithinSnapshotBase(realBaseDir, resolved, path.sep, path)) {
|
|
599
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
600
|
+
}
|
|
601
|
+
if (mode === "read") {
|
|
602
|
+
const realTarget = await fs2.realpath(resolved);
|
|
603
|
+
if (!isWithinSnapshotBase(realBaseDir, realTarget, path.sep, path)) {
|
|
604
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
605
|
+
}
|
|
606
|
+
return realTarget;
|
|
607
|
+
}
|
|
608
|
+
const parentDir = path.dirname(resolved);
|
|
609
|
+
const existingAncestor = await findExistingAncestor(parentDir, fs2, path);
|
|
610
|
+
const realExistingAncestor = await fs2.realpath(existingAncestor);
|
|
611
|
+
if (!isWithinSnapshotBase(realBaseDir, realExistingAncestor, path.sep, path)) {
|
|
612
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
613
|
+
}
|
|
614
|
+
await fs2.mkdir(parentDir, { recursive: true });
|
|
615
|
+
const realParentDir = await fs2.realpath(parentDir);
|
|
616
|
+
if (!isWithinSnapshotBase(realBaseDir, realParentDir, path.sep, path)) {
|
|
617
|
+
throw new Error(`filePath is outside the allowed snapshot directory: ${realBaseDir}`);
|
|
618
|
+
}
|
|
619
|
+
const targetPath = path.join(realParentDir, path.basename(resolved));
|
|
620
|
+
try {
|
|
621
|
+
const existing = await fs2.lstat(targetPath);
|
|
622
|
+
if (existing.isSymbolicLink()) {
|
|
623
|
+
throw new Error("filePath must not point to a symbolic link.");
|
|
624
|
+
}
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (error.code !== "ENOENT") {
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return targetPath;
|
|
631
|
+
}
|
|
632
|
+
async function readUtf8HandleWithLimit(handle, byteLimit) {
|
|
633
|
+
if (byteLimit === false) {
|
|
634
|
+
return handle.readFile({ encoding: "utf8" });
|
|
635
|
+
}
|
|
636
|
+
const chunks = [];
|
|
637
|
+
let totalBytes = 0;
|
|
638
|
+
let position = 0;
|
|
639
|
+
while (true) {
|
|
640
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, byteLimit - totalBytes + 1));
|
|
641
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
642
|
+
if (bytesRead === 0) {
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
totalBytes += bytesRead;
|
|
646
|
+
if (totalBytes > byteLimit) {
|
|
647
|
+
throw new Error(`Snapshot file exceeds snapshotMaxBytes limit (${totalBytes} bytes > ${byteLimit} bytes).`);
|
|
648
|
+
}
|
|
649
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
650
|
+
position += bytesRead;
|
|
651
|
+
}
|
|
652
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/internal/CacheStackGeneration.ts
|
|
656
|
+
var DEFAULT_GENERATION_CLEANUP_BATCH_SIZE = 500;
|
|
657
|
+
function generationPrefix(generation) {
|
|
658
|
+
return generation === void 0 ? "" : `v${generation}:`;
|
|
659
|
+
}
|
|
660
|
+
function qualifyGenerationKey(key, generation) {
|
|
661
|
+
const prefix = generationPrefix(generation);
|
|
662
|
+
return prefix ? `${prefix}${key}` : key;
|
|
663
|
+
}
|
|
664
|
+
function qualifyGenerationPattern(pattern, generation) {
|
|
665
|
+
return qualifyGenerationKey(pattern, generation);
|
|
666
|
+
}
|
|
667
|
+
function stripGenerationPrefix(key, generation) {
|
|
668
|
+
const prefix = generationPrefix(generation);
|
|
669
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
670
|
+
return key;
|
|
671
|
+
}
|
|
672
|
+
return key.slice(prefix.length);
|
|
673
|
+
}
|
|
674
|
+
function resolveGenerationCleanupTarget({
|
|
675
|
+
previousGeneration,
|
|
676
|
+
nextGeneration,
|
|
677
|
+
generationCleanup
|
|
678
|
+
}) {
|
|
679
|
+
if (!generationCleanup || previousGeneration === void 0 || previousGeneration === nextGeneration) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
return previousGeneration;
|
|
683
|
+
}
|
|
684
|
+
function resolveGenerationCleanupBatchSize(generationCleanup) {
|
|
685
|
+
if (typeof generationCleanup !== "object" || generationCleanup === null) {
|
|
686
|
+
return DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
687
|
+
}
|
|
688
|
+
return generationCleanup.batchSize ?? DEFAULT_GENERATION_CLEANUP_BATCH_SIZE;
|
|
689
|
+
}
|
|
690
|
+
function planGenerationCleanupBatches(keys, generationCleanup) {
|
|
691
|
+
if (keys.length === 0) {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
const batchSize = resolveGenerationCleanupBatchSize(generationCleanup);
|
|
695
|
+
const batches = [];
|
|
696
|
+
for (let index = 0; index < keys.length; index += batchSize) {
|
|
697
|
+
batches.push(keys.slice(index, index + batchSize));
|
|
698
|
+
}
|
|
699
|
+
return batches;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/internal/CacheStackMaintenance.ts
|
|
703
|
+
var CacheStackMaintenance = class {
|
|
704
|
+
keyEpochs = /* @__PURE__ */ new Map();
|
|
705
|
+
writeBehindQueue = [];
|
|
706
|
+
writeBehindTimer;
|
|
707
|
+
writeBehindFlushPromise;
|
|
708
|
+
generationCleanupPromise;
|
|
709
|
+
clearEpoch = 0;
|
|
710
|
+
initializeWriteBehindTimer(writeStrategy, options, flush) {
|
|
711
|
+
if (writeStrategy !== "write-behind") {
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
715
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this.disposeWriteBehindTimer();
|
|
719
|
+
this.writeBehindTimer = setInterval(() => {
|
|
720
|
+
void flush();
|
|
721
|
+
}, flushIntervalMs);
|
|
722
|
+
this.writeBehindTimer.unref?.();
|
|
723
|
+
}
|
|
724
|
+
disposeWriteBehindTimer() {
|
|
725
|
+
if (!this.writeBehindTimer) {
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
clearInterval(this.writeBehindTimer);
|
|
729
|
+
this.writeBehindTimer = void 0;
|
|
730
|
+
}
|
|
731
|
+
beginClearEpoch() {
|
|
732
|
+
this.clearEpoch += 1;
|
|
733
|
+
this.keyEpochs.clear();
|
|
734
|
+
this.writeBehindQueue.length = 0;
|
|
735
|
+
}
|
|
736
|
+
currentClearEpoch() {
|
|
737
|
+
return this.clearEpoch;
|
|
738
|
+
}
|
|
739
|
+
currentKeyEpoch(key) {
|
|
740
|
+
return this.keyEpochs.get(key) ?? 0;
|
|
741
|
+
}
|
|
742
|
+
bumpKeyEpochs(keys) {
|
|
743
|
+
for (const key of keys) {
|
|
744
|
+
this.keyEpochs.set(key, this.currentKeyEpoch(key) + 1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch) {
|
|
748
|
+
if (expectedClearEpoch !== void 0 && expectedClearEpoch !== this.clearEpoch) {
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
if (expectedKeyEpoch !== void 0 && expectedKeyEpoch !== this.currentKeyEpoch(key)) {
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
async enqueueWriteBehind(operation, options, flushBatch) {
|
|
757
|
+
this.writeBehindQueue.push(operation);
|
|
758
|
+
const batchSize = options?.batchSize ?? 100;
|
|
759
|
+
const maxQueueSize = options?.maxQueueSize ?? batchSize * 10;
|
|
760
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
761
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
765
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
async flushWriteBehindQueue(options, flushBatch) {
|
|
769
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
770
|
+
await this.writeBehindFlushPromise;
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const batchSize = options?.batchSize ?? 100;
|
|
774
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
775
|
+
this.writeBehindFlushPromise = flushBatch(batch);
|
|
776
|
+
try {
|
|
777
|
+
await this.writeBehindFlushPromise;
|
|
778
|
+
} finally {
|
|
779
|
+
this.writeBehindFlushPromise = void 0;
|
|
780
|
+
}
|
|
781
|
+
if (this.writeBehindQueue.length > 0) {
|
|
782
|
+
await this.flushWriteBehindQueue(options, flushBatch);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
scheduleGenerationCleanup(generation, task, onError) {
|
|
786
|
+
const scheduledTask = (this.generationCleanupPromise ?? Promise.resolve()).then(() => task(generation)).catch((error) => {
|
|
787
|
+
onError(generation, error);
|
|
788
|
+
});
|
|
789
|
+
this.generationCleanupPromise = scheduledTask.finally(() => {
|
|
790
|
+
if (this.generationCleanupPromise === scheduledTask) {
|
|
791
|
+
this.generationCleanupPromise = void 0;
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
async waitForGenerationCleanup() {
|
|
796
|
+
await this.generationCleanupPromise;
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/internal/StoredValue.ts
|
|
801
|
+
function isStoredValueEnvelope(value) {
|
|
802
|
+
if (typeof value !== "object" || value === null) {
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
const v = value;
|
|
806
|
+
if (v.__layercache !== 1) {
|
|
807
|
+
return false;
|
|
808
|
+
}
|
|
809
|
+
if (v.kind !== "value" && v.kind !== "empty") {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
if (v.freshUntil !== null && (!Number.isFinite(v.freshUntil) || typeof v.freshUntil !== "number")) {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
if (v.staleUntil !== null && (!Number.isFinite(v.staleUntil) || typeof v.staleUntil !== "number")) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
if (v.errorUntil !== null && (!Number.isFinite(v.errorUntil) || typeof v.errorUntil !== "number")) {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
822
|
+
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
if (typeof v.staleUntil === "number" && v.staleUntil > maxTimestamp) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
if (typeof v.errorUntil === "number" && v.errorUntil > maxTimestamp) {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
if (v.freshUntil === null && (v.staleUntil !== null || v.errorUntil !== null)) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
if (typeof v.freshUntil === "number" && typeof v.staleUntil === "number" && v.staleUntil < v.freshUntil) {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
if (typeof v.freshUntil === "number" && typeof v.errorUntil === "number" && v.errorUntil < v.freshUntil) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
const maxTtlSeconds = 10 * 365 * 24 * 60 * 60;
|
|
841
|
+
if (!isValidEnvelopeTtlSeconds(v.freshTtlSeconds, maxTtlSeconds)) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
if (!isValidEnvelopeTtlSeconds(v.staleWhileRevalidateSeconds, maxTtlSeconds)) {
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
if (!isValidEnvelopeTtlSeconds(v.staleIfErrorSeconds, maxTtlSeconds)) {
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
if (v.freshTtlSeconds == null && (v.staleWhileRevalidateSeconds != null || v.staleIfErrorSeconds != null)) {
|
|
851
|
+
return false;
|
|
852
|
+
}
|
|
853
|
+
return true;
|
|
854
|
+
}
|
|
855
|
+
function createStoredValueEnvelope(options) {
|
|
856
|
+
const now = options.now ?? Date.now();
|
|
857
|
+
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
858
|
+
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
859
|
+
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
860
|
+
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
861
|
+
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
862
|
+
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
863
|
+
return {
|
|
864
|
+
__layercache: 1,
|
|
865
|
+
kind: options.kind,
|
|
866
|
+
value: options.value,
|
|
867
|
+
freshUntil,
|
|
868
|
+
staleUntil,
|
|
869
|
+
errorUntil,
|
|
870
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
871
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
872
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
function resolveStoredValue(stored, now = Date.now()) {
|
|
876
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
877
|
+
return { state: "fresh", value: stored, stored };
|
|
878
|
+
}
|
|
879
|
+
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
880
|
+
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
881
|
+
}
|
|
882
|
+
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
883
|
+
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
884
|
+
}
|
|
885
|
+
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
886
|
+
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
887
|
+
}
|
|
888
|
+
return { state: "expired", value: null, stored, envelope: stored };
|
|
889
|
+
}
|
|
890
|
+
function unwrapStoredValue(stored) {
|
|
891
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
892
|
+
return stored;
|
|
893
|
+
}
|
|
894
|
+
if (stored.kind === "empty") {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
return stored.value ?? null;
|
|
898
|
+
}
|
|
899
|
+
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
900
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
901
|
+
return void 0;
|
|
902
|
+
}
|
|
903
|
+
const expiry = maxExpiry(stored);
|
|
904
|
+
if (expiry === null) {
|
|
905
|
+
return void 0;
|
|
906
|
+
}
|
|
907
|
+
const remainingMs = expiry - now;
|
|
908
|
+
if (remainingMs <= 0) {
|
|
909
|
+
return 1;
|
|
910
|
+
}
|
|
911
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
912
|
+
}
|
|
913
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
914
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
915
|
+
return void 0;
|
|
916
|
+
}
|
|
917
|
+
const remainingMs = stored.freshUntil - now;
|
|
918
|
+
if (remainingMs <= 0) {
|
|
919
|
+
return 0;
|
|
920
|
+
}
|
|
921
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
922
|
+
}
|
|
923
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
924
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
925
|
+
return stored;
|
|
926
|
+
}
|
|
927
|
+
return createStoredValueEnvelope({
|
|
928
|
+
kind: stored.kind,
|
|
929
|
+
value: stored.value,
|
|
930
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
931
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
932
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
933
|
+
now
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function maxExpiry(stored) {
|
|
937
|
+
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
938
|
+
(value) => value !== null
|
|
939
|
+
);
|
|
940
|
+
if (values.length === 0) {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
return Math.max(...values);
|
|
944
|
+
}
|
|
945
|
+
function normalizePositiveSeconds(value) {
|
|
946
|
+
if (!value || value <= 0) {
|
|
947
|
+
return void 0;
|
|
948
|
+
}
|
|
949
|
+
return value;
|
|
950
|
+
}
|
|
951
|
+
function isValidEnvelopeTtlSeconds(value, maxTtlSeconds) {
|
|
952
|
+
if (value == null) {
|
|
953
|
+
return true;
|
|
954
|
+
}
|
|
955
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0 && value <= maxTtlSeconds;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// src/internal/CacheStackRuntimePolicy.ts
|
|
959
|
+
function shouldSkipLayer(degradedUntil, now = Date.now()) {
|
|
960
|
+
return degradedUntil !== void 0 && degradedUntil > now;
|
|
961
|
+
}
|
|
962
|
+
function shouldStartBackgroundRefresh({
|
|
963
|
+
isDisconnecting,
|
|
964
|
+
hasRefreshInFlight
|
|
965
|
+
}) {
|
|
966
|
+
return !isDisconnecting && !hasRefreshInFlight;
|
|
967
|
+
}
|
|
968
|
+
function resolveRecoverableLayerFailure(gracefulDegradation, now = Date.now()) {
|
|
969
|
+
if (!gracefulDegradation) {
|
|
970
|
+
return { degrade: false };
|
|
971
|
+
}
|
|
972
|
+
const retryAfterMs = typeof gracefulDegradation === "object" ? gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
973
|
+
return {
|
|
974
|
+
degrade: true,
|
|
975
|
+
degradedUntil: now + retryAfterMs
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
function planFreshReadPolicies({
|
|
979
|
+
stored,
|
|
980
|
+
hasFetcher,
|
|
981
|
+
slidingTtl,
|
|
982
|
+
refreshAheadSeconds
|
|
983
|
+
}) {
|
|
984
|
+
const refreshedStored = slidingTtl && isStoredValueEnvelope(stored) ? refreshStoredEnvelope(stored) : void 0;
|
|
985
|
+
const refreshedStoredTtl = refreshedStored ? remainingStoredTtlSeconds(refreshedStored) ?? void 0 : void 0;
|
|
986
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(stored) ?? 0;
|
|
987
|
+
return {
|
|
988
|
+
refreshedStored,
|
|
989
|
+
refreshedStoredTtl,
|
|
990
|
+
shouldScheduleBackgroundRefresh: hasFetcher && refreshAheadSeconds > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAheadSeconds
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/internal/CacheStackValidation.ts
|
|
995
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
996
|
+
var MAX_PATTERN_LENGTH = 1024;
|
|
997
|
+
var MAX_TAGS_PER_OPERATION = 128;
|
|
998
|
+
function validatePositiveNumber(name, value) {
|
|
999
|
+
if (value === void 0) {
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1003
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function validateNonNegativeNumber(name, value) {
|
|
1007
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1008
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function validateLayerNumberOption(name, value) {
|
|
1012
|
+
if (value === void 0) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (typeof value === "number") {
|
|
1016
|
+
validateNonNegativeNumber(name, value);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1020
|
+
if (layerValue === void 0) {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function validateRateLimitOptions(name, options) {
|
|
1027
|
+
if (!options) {
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1031
|
+
validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1032
|
+
validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1033
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1034
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1035
|
+
}
|
|
1036
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1037
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
306
1038
|
}
|
|
307
|
-
return result;
|
|
308
1039
|
}
|
|
309
|
-
function
|
|
1040
|
+
function validateCacheKey(key) {
|
|
310
1041
|
if (key.length === 0) {
|
|
311
|
-
throw new Error("
|
|
1042
|
+
throw new Error("Cache key must not be empty.");
|
|
312
1043
|
}
|
|
313
|
-
if (key.length >
|
|
314
|
-
throw new Error(
|
|
1044
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1045
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
315
1046
|
}
|
|
316
1047
|
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
317
|
-
throw new Error("
|
|
1048
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1049
|
+
}
|
|
1050
|
+
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
1051
|
+
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
318
1052
|
}
|
|
1053
|
+
return key;
|
|
319
1054
|
}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Tests whether a glob-style pattern matches a value.
|
|
325
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
326
|
-
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
327
|
-
* quadratic memory usage on long patterns/keys.
|
|
328
|
-
*/
|
|
329
|
-
static matches(pattern, value) {
|
|
330
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
1055
|
+
function validateTag(tag) {
|
|
1056
|
+
if (tag.length === 0) {
|
|
1057
|
+
throw new Error("Cache tag must not be empty.");
|
|
331
1058
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
*/
|
|
335
|
-
static matchLinear(pattern, value) {
|
|
336
|
-
let patternIndex = 0;
|
|
337
|
-
let valueIndex = 0;
|
|
338
|
-
let starIndex = -1;
|
|
339
|
-
let backtrackValueIndex = 0;
|
|
340
|
-
while (valueIndex < value.length) {
|
|
341
|
-
const patternChar = pattern[patternIndex];
|
|
342
|
-
const valueChar = value[valueIndex];
|
|
343
|
-
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
344
|
-
starIndex = patternIndex;
|
|
345
|
-
patternIndex += 1;
|
|
346
|
-
backtrackValueIndex = valueIndex;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (patternChar === "?" || patternChar === valueChar) {
|
|
350
|
-
patternIndex += 1;
|
|
351
|
-
valueIndex += 1;
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (starIndex !== -1) {
|
|
355
|
-
patternIndex = starIndex + 1;
|
|
356
|
-
backtrackValueIndex += 1;
|
|
357
|
-
valueIndex = backtrackValueIndex;
|
|
358
|
-
continue;
|
|
359
|
-
}
|
|
360
|
-
return false;
|
|
361
|
-
}
|
|
362
|
-
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
363
|
-
patternIndex += 1;
|
|
364
|
-
}
|
|
365
|
-
return patternIndex === pattern.length;
|
|
1059
|
+
if (tag.length > MAX_CACHE_KEY_LENGTH) {
|
|
1060
|
+
throw new Error(`Cache tag length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
366
1061
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
// src/internal/CacheKeyDiscovery.ts
|
|
370
|
-
var CacheKeyDiscovery = class {
|
|
371
|
-
constructor(options) {
|
|
372
|
-
this.options = options;
|
|
1062
|
+
if (/[\u0000-\u001F\u007F]/.test(tag)) {
|
|
1063
|
+
throw new Error("Cache tag contains unsupported control characters.");
|
|
373
1064
|
}
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const { tagIndex } = this.options;
|
|
377
|
-
const matches = new Set(
|
|
378
|
-
tagIndex.keysForPrefix ? await tagIndex.keysForPrefix(prefix) : await tagIndex.matchPattern(`${prefix}*`)
|
|
379
|
-
);
|
|
380
|
-
await Promise.all(
|
|
381
|
-
this.options.layers.map(async (layer) => {
|
|
382
|
-
if (!layer.keys || this.options.shouldSkipLayer(layer)) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
try {
|
|
386
|
-
const keys = await layer.keys();
|
|
387
|
-
for (const key of keys) {
|
|
388
|
-
if (key.startsWith(prefix)) {
|
|
389
|
-
matches.add(key);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
} catch (error) {
|
|
393
|
-
await this.options.handleLayerFailure(layer, "invalidate-prefix-scan", error);
|
|
394
|
-
}
|
|
395
|
-
})
|
|
396
|
-
);
|
|
397
|
-
return [...matches];
|
|
1065
|
+
if (/[\uD800-\uDFFF]/.test(tag)) {
|
|
1066
|
+
throw new Error("Cache tag contains unsupported surrogate code points.");
|
|
398
1067
|
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
return;
|
|
405
|
-
}
|
|
406
|
-
try {
|
|
407
|
-
const keys = await layer.keys();
|
|
408
|
-
for (const key of keys) {
|
|
409
|
-
if (PatternMatcher.matches(pattern, key)) {
|
|
410
|
-
matches.add(key);
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
} catch (error) {
|
|
414
|
-
await this.options.handleLayerFailure(layer, "invalidate-pattern-scan", error);
|
|
415
|
-
}
|
|
416
|
-
})
|
|
417
|
-
);
|
|
418
|
-
return [...matches];
|
|
1068
|
+
return tag;
|
|
1069
|
+
}
|
|
1070
|
+
function validateTags(tags) {
|
|
1071
|
+
if (!tags) {
|
|
1072
|
+
return;
|
|
419
1073
|
}
|
|
420
|
-
|
|
1074
|
+
if (tags.length > MAX_TAGS_PER_OPERATION) {
|
|
1075
|
+
throw new Error(`options.tags must contain at most ${MAX_TAGS_PER_OPERATION} tags.`);
|
|
1076
|
+
}
|
|
1077
|
+
for (const tag of tags) {
|
|
1078
|
+
validateTag(tag);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
function validatePattern(pattern) {
|
|
1082
|
+
if (pattern.length === 0) {
|
|
1083
|
+
throw new Error("Pattern must not be empty.");
|
|
1084
|
+
}
|
|
1085
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
1086
|
+
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
1087
|
+
}
|
|
1088
|
+
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
1089
|
+
throw new Error("Pattern contains unsupported control characters.");
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
function validateTtlPolicy(name, policy) {
|
|
1093
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
if ("alignTo" in policy) {
|
|
1097
|
+
validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
throw new Error(`${name} is invalid.`);
|
|
1101
|
+
}
|
|
1102
|
+
function validateAdaptiveTtlOptions(options) {
|
|
1103
|
+
if (!options || options === true) {
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1107
|
+
validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1108
|
+
validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1109
|
+
}
|
|
1110
|
+
function validateCircuitBreakerOptions(options) {
|
|
1111
|
+
if (!options) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1115
|
+
validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1116
|
+
}
|
|
421
1117
|
|
|
422
1118
|
// src/internal/CircuitBreakerManager.ts
|
|
423
1119
|
var CircuitBreakerManager = class {
|
|
@@ -448,7 +1144,6 @@ var CircuitBreakerManager = class {
|
|
|
448
1144
|
if (!options) {
|
|
449
1145
|
return;
|
|
450
1146
|
}
|
|
451
|
-
this.pruneIfNeeded();
|
|
452
1147
|
const failureThreshold = options.failureThreshold ?? 3;
|
|
453
1148
|
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
454
1149
|
const state = this.breakers.get(key) ?? { failures: 0, openUntil: null, createdAt: Date.now() };
|
|
@@ -457,6 +1152,7 @@ var CircuitBreakerManager = class {
|
|
|
457
1152
|
state.openUntil = Date.now() + cooldownMs;
|
|
458
1153
|
}
|
|
459
1154
|
this.breakers.set(key, state);
|
|
1155
|
+
this.pruneIfNeeded();
|
|
460
1156
|
}
|
|
461
1157
|
recordSuccess(key) {
|
|
462
1158
|
this.breakers.delete(key);
|
|
@@ -741,188 +1437,64 @@ var MetricsCollector = class {
|
|
|
741
1437
|
;
|
|
742
1438
|
this.data[field] += amount;
|
|
743
1439
|
}
|
|
744
|
-
incrementLayer(map, layerName) {
|
|
745
|
-
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
746
|
-
}
|
|
747
|
-
/**
|
|
748
|
-
* Records a read latency sample for the given layer.
|
|
749
|
-
* Maintains a rolling average and max using Welford's online algorithm.
|
|
750
|
-
*/
|
|
751
|
-
recordLatency(layerName, durationMs) {
|
|
752
|
-
const existing = this.data.latencyByLayer[layerName];
|
|
753
|
-
if (!existing) {
|
|
754
|
-
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
existing.count += 1;
|
|
758
|
-
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
759
|
-
if (durationMs > existing.maxMs) {
|
|
760
|
-
existing.maxMs = durationMs;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
reset() {
|
|
764
|
-
this.data = this.empty();
|
|
765
|
-
}
|
|
766
|
-
hitRate() {
|
|
767
|
-
const total = this.data.hits + this.data.misses;
|
|
768
|
-
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
769
|
-
const byLayer = {};
|
|
770
|
-
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
771
|
-
for (const layer of allLayers) {
|
|
772
|
-
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
773
|
-
const m = this.data.missesByLayer[layer] ?? 0;
|
|
774
|
-
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
775
|
-
}
|
|
776
|
-
return { overall, byLayer };
|
|
777
|
-
}
|
|
778
|
-
empty() {
|
|
779
|
-
return {
|
|
780
|
-
hits: 0,
|
|
781
|
-
misses: 0,
|
|
782
|
-
fetches: 0,
|
|
783
|
-
sets: 0,
|
|
784
|
-
deletes: 0,
|
|
785
|
-
backfills: 0,
|
|
786
|
-
invalidations: 0,
|
|
787
|
-
staleHits: 0,
|
|
788
|
-
refreshes: 0,
|
|
789
|
-
refreshErrors: 0,
|
|
790
|
-
writeFailures: 0,
|
|
791
|
-
singleFlightWaits: 0,
|
|
792
|
-
negativeCacheHits: 0,
|
|
793
|
-
circuitBreakerTrips: 0,
|
|
794
|
-
degradedOperations: 0,
|
|
795
|
-
hitsByLayer: {},
|
|
796
|
-
missesByLayer: {},
|
|
797
|
-
latencyByLayer: {},
|
|
798
|
-
resetAt: Date.now()
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
|
|
803
|
-
// src/internal/StoredValue.ts
|
|
804
|
-
function isStoredValueEnvelope(value) {
|
|
805
|
-
if (typeof value !== "object" || value === null) {
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
|
-
const v = value;
|
|
809
|
-
if (v.__layercache !== 1) {
|
|
810
|
-
return false;
|
|
811
|
-
}
|
|
812
|
-
if (v.kind !== "value" && v.kind !== "empty") {
|
|
813
|
-
return false;
|
|
814
|
-
}
|
|
815
|
-
if (v.freshUntil !== null && typeof v.freshUntil !== "number") {
|
|
816
|
-
return false;
|
|
817
|
-
}
|
|
818
|
-
if (v.staleUntil !== null && typeof v.staleUntil !== "number") {
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
if (v.errorUntil !== null && typeof v.errorUntil !== "number") {
|
|
822
|
-
return false;
|
|
823
|
-
}
|
|
824
|
-
const maxTimestamp = Date.now() + 10 * 365 * 24 * 60 * 60 * 1e3;
|
|
825
|
-
if (typeof v.freshUntil === "number" && v.freshUntil > maxTimestamp) {
|
|
826
|
-
return false;
|
|
827
|
-
}
|
|
828
|
-
return true;
|
|
829
|
-
}
|
|
830
|
-
function createStoredValueEnvelope(options) {
|
|
831
|
-
const now = options.now ?? Date.now();
|
|
832
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
833
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
834
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
835
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
836
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
837
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
838
|
-
return {
|
|
839
|
-
__layercache: 1,
|
|
840
|
-
kind: options.kind,
|
|
841
|
-
value: options.value,
|
|
842
|
-
freshUntil,
|
|
843
|
-
staleUntil,
|
|
844
|
-
errorUntil,
|
|
845
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
846
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
847
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
848
|
-
};
|
|
849
|
-
}
|
|
850
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
851
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
852
|
-
return { state: "fresh", value: stored, stored };
|
|
853
|
-
}
|
|
854
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
855
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
856
|
-
}
|
|
857
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
858
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
859
|
-
}
|
|
860
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
861
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
862
|
-
}
|
|
863
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
864
|
-
}
|
|
865
|
-
function unwrapStoredValue(stored) {
|
|
866
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
867
|
-
return stored;
|
|
868
|
-
}
|
|
869
|
-
if (stored.kind === "empty") {
|
|
870
|
-
return null;
|
|
871
|
-
}
|
|
872
|
-
return stored.value ?? null;
|
|
873
|
-
}
|
|
874
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
875
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
876
|
-
return void 0;
|
|
877
|
-
}
|
|
878
|
-
const expiry = maxExpiry(stored);
|
|
879
|
-
if (expiry === null) {
|
|
880
|
-
return void 0;
|
|
881
|
-
}
|
|
882
|
-
const remainingMs = expiry - now;
|
|
883
|
-
if (remainingMs <= 0) {
|
|
884
|
-
return 1;
|
|
885
|
-
}
|
|
886
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
887
|
-
}
|
|
888
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
889
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
890
|
-
return void 0;
|
|
1440
|
+
incrementLayer(map, layerName) {
|
|
1441
|
+
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
891
1442
|
}
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1443
|
+
/**
|
|
1444
|
+
* Records a read latency sample for the given layer.
|
|
1445
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
1446
|
+
*/
|
|
1447
|
+
recordLatency(layerName, durationMs) {
|
|
1448
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
1449
|
+
if (!existing) {
|
|
1450
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
existing.count += 1;
|
|
1454
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
1455
|
+
if (durationMs > existing.maxMs) {
|
|
1456
|
+
existing.maxMs = durationMs;
|
|
1457
|
+
}
|
|
895
1458
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
899
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
900
|
-
return stored;
|
|
1459
|
+
reset() {
|
|
1460
|
+
this.data = this.empty();
|
|
901
1461
|
}
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
(value) => value !== null
|
|
914
|
-
);
|
|
915
|
-
if (values.length === 0) {
|
|
916
|
-
return null;
|
|
1462
|
+
hitRate() {
|
|
1463
|
+
const total = this.data.hits + this.data.misses;
|
|
1464
|
+
const overall = total === 0 ? 0 : this.data.hits / total;
|
|
1465
|
+
const byLayer = {};
|
|
1466
|
+
const allLayers = /* @__PURE__ */ new Set([...Object.keys(this.data.hitsByLayer), ...Object.keys(this.data.missesByLayer)]);
|
|
1467
|
+
for (const layer of allLayers) {
|
|
1468
|
+
const h = this.data.hitsByLayer[layer] ?? 0;
|
|
1469
|
+
const m = this.data.missesByLayer[layer] ?? 0;
|
|
1470
|
+
byLayer[layer] = h + m === 0 ? 0 : h / (h + m);
|
|
1471
|
+
}
|
|
1472
|
+
return { overall, byLayer };
|
|
917
1473
|
}
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
1474
|
+
empty() {
|
|
1475
|
+
return {
|
|
1476
|
+
hits: 0,
|
|
1477
|
+
misses: 0,
|
|
1478
|
+
fetches: 0,
|
|
1479
|
+
sets: 0,
|
|
1480
|
+
deletes: 0,
|
|
1481
|
+
backfills: 0,
|
|
1482
|
+
invalidations: 0,
|
|
1483
|
+
staleHits: 0,
|
|
1484
|
+
refreshes: 0,
|
|
1485
|
+
refreshErrors: 0,
|
|
1486
|
+
writeFailures: 0,
|
|
1487
|
+
singleFlightWaits: 0,
|
|
1488
|
+
negativeCacheHits: 0,
|
|
1489
|
+
circuitBreakerTrips: 0,
|
|
1490
|
+
degradedOperations: 0,
|
|
1491
|
+
hitsByLayer: {},
|
|
1492
|
+
missesByLayer: {},
|
|
1493
|
+
latencyByLayer: {},
|
|
1494
|
+
resetAt: Date.now()
|
|
1495
|
+
};
|
|
923
1496
|
}
|
|
924
|
-
|
|
925
|
-
}
|
|
1497
|
+
};
|
|
926
1498
|
|
|
927
1499
|
// src/internal/TtlResolver.ts
|
|
928
1500
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
@@ -1077,6 +1649,11 @@ var TagIndex = class {
|
|
|
1077
1649
|
async keysForTag(tag) {
|
|
1078
1650
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
1079
1651
|
}
|
|
1652
|
+
async forEachKeyForTag(tag, visitor) {
|
|
1653
|
+
for (const key of this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()) {
|
|
1654
|
+
await visitor(key);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1080
1657
|
async keysForPrefix(prefix) {
|
|
1081
1658
|
const node = this.findNode(prefix);
|
|
1082
1659
|
if (!node) {
|
|
@@ -1086,6 +1663,13 @@ var TagIndex = class {
|
|
|
1086
1663
|
this.collectFromNode(node, prefix, matches);
|
|
1087
1664
|
return matches;
|
|
1088
1665
|
}
|
|
1666
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
1667
|
+
const node = this.findNode(prefix);
|
|
1668
|
+
if (!node) {
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
await this.visitFromNode(node, prefix, visitor);
|
|
1672
|
+
}
|
|
1089
1673
|
async tagsForKey(key) {
|
|
1090
1674
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
1091
1675
|
}
|
|
@@ -1094,6 +1678,12 @@ var TagIndex = class {
|
|
|
1094
1678
|
this.collectPatternMatches(this.root, "", pattern, 0, matches, /* @__PURE__ */ new Set(), 0);
|
|
1095
1679
|
return [...matches];
|
|
1096
1680
|
}
|
|
1681
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
1682
|
+
const matches = await this.matchPattern(pattern);
|
|
1683
|
+
for (const key of matches) {
|
|
1684
|
+
await visitor(key);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1097
1687
|
async clear() {
|
|
1098
1688
|
this.tagToKeys.clear();
|
|
1099
1689
|
this.keyToTags.clear();
|
|
@@ -1143,6 +1733,14 @@ var TagIndex = class {
|
|
|
1143
1733
|
this.collectFromNode(child, `${prefix}${character}`, matches);
|
|
1144
1734
|
}
|
|
1145
1735
|
}
|
|
1736
|
+
async visitFromNode(node, prefix, visitor) {
|
|
1737
|
+
if (node.terminal) {
|
|
1738
|
+
await visitor(prefix);
|
|
1739
|
+
}
|
|
1740
|
+
for (const [character, child] of node.children) {
|
|
1741
|
+
await this.visitFromNode(child, `${prefix}${character}`, visitor);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1146
1744
|
collectPatternMatches(node, prefix, pattern, patternIndex, matches, visited, depth) {
|
|
1147
1745
|
if (depth > MAX_PATTERN_RECURSION_DEPTH) {
|
|
1148
1746
|
return;
|
|
@@ -1260,22 +1858,27 @@ var TagIndex = class {
|
|
|
1260
1858
|
|
|
1261
1859
|
// src/serialization/JsonSerializer.ts
|
|
1262
1860
|
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1861
|
+
var MAX_SANITIZE_NODES = 1e4;
|
|
1263
1862
|
var JsonSerializer = class {
|
|
1264
1863
|
serialize(value) {
|
|
1265
1864
|
return JSON.stringify(value);
|
|
1266
1865
|
}
|
|
1267
1866
|
deserialize(payload) {
|
|
1268
1867
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1269
|
-
return sanitizeJsonValue(JSON.parse(normalized), 0);
|
|
1868
|
+
return sanitizeJsonValue(JSON.parse(normalized), 0, { count: 0 });
|
|
1270
1869
|
}
|
|
1271
1870
|
};
|
|
1272
1871
|
var MAX_SANITIZE_DEPTH = 200;
|
|
1273
|
-
function sanitizeJsonValue(value, depth) {
|
|
1872
|
+
function sanitizeJsonValue(value, depth, state) {
|
|
1873
|
+
state.count += 1;
|
|
1874
|
+
if (state.count > MAX_SANITIZE_NODES) {
|
|
1875
|
+
throw new Error(`JSON payload exceeds max node count of ${MAX_SANITIZE_NODES}.`);
|
|
1876
|
+
}
|
|
1274
1877
|
if (depth > MAX_SANITIZE_DEPTH) {
|
|
1275
|
-
|
|
1878
|
+
throw new Error(`JSON payload exceeds max depth of ${MAX_SANITIZE_DEPTH}.`);
|
|
1276
1879
|
}
|
|
1277
1880
|
if (Array.isArray(value)) {
|
|
1278
|
-
return value.map((entry) => sanitizeJsonValue(entry, depth + 1));
|
|
1881
|
+
return value.map((entry) => sanitizeJsonValue(entry, depth + 1, state));
|
|
1279
1882
|
}
|
|
1280
1883
|
if (!isPlainObject(value)) {
|
|
1281
1884
|
return value;
|
|
@@ -1285,7 +1888,7 @@ function sanitizeJsonValue(value, depth) {
|
|
|
1285
1888
|
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
1286
1889
|
continue;
|
|
1287
1890
|
}
|
|
1288
|
-
sanitized[key] = sanitizeJsonValue(entry, depth + 1);
|
|
1891
|
+
sanitized[key] = sanitizeJsonValue(entry, depth + 1, state);
|
|
1289
1892
|
}
|
|
1290
1893
|
return sanitized;
|
|
1291
1894
|
}
|
|
@@ -1335,10 +1938,11 @@ var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
|
1335
1938
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
1336
1939
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
1337
1940
|
var DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS = 3e4;
|
|
1338
|
-
var
|
|
1339
|
-
var
|
|
1941
|
+
var DEFAULT_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
|
|
1942
|
+
var DEFAULT_SNAPSHOT_MAX_ENTRIES = 1e4;
|
|
1943
|
+
var DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE = 50;
|
|
1944
|
+
var DEFAULT_INVALIDATION_MAX_KEYS = 1e4;
|
|
1340
1945
|
var DEFAULT_MAX_PROFILE_ENTRIES = 1e5;
|
|
1341
|
-
var DANGEROUS_OBJECT_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1342
1946
|
var DebugLogger = class {
|
|
1343
1947
|
enabled;
|
|
1344
1948
|
constructor(enabled) {
|
|
@@ -1425,13 +2029,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1425
2029
|
snapshotSerializer = new JsonSerializer();
|
|
1426
2030
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
1427
2031
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
2032
|
+
maintenance = new CacheStackMaintenance();
|
|
1428
2033
|
ttlResolver;
|
|
1429
2034
|
circuitBreakerManager;
|
|
1430
2035
|
currentGeneration;
|
|
1431
|
-
writeBehindQueue = [];
|
|
1432
|
-
writeBehindTimer;
|
|
1433
|
-
writeBehindFlushPromise;
|
|
1434
|
-
generationCleanupPromise;
|
|
1435
2036
|
isDisconnecting = false;
|
|
1436
2037
|
disconnectPromise;
|
|
1437
2038
|
/**
|
|
@@ -1441,7 +2042,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1441
2042
|
* and no `fetcher` is provided.
|
|
1442
2043
|
*/
|
|
1443
2044
|
async get(key, fetcher, options) {
|
|
1444
|
-
const normalizedKey = this.qualifyKey(
|
|
2045
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1445
2046
|
this.validateWriteOptions(options);
|
|
1446
2047
|
await this.awaitStartup("get");
|
|
1447
2048
|
return this.getPrepared(normalizedKey, fetcher, options);
|
|
@@ -1511,7 +2112,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1511
2112
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1512
2113
|
*/
|
|
1513
2114
|
async has(key) {
|
|
1514
|
-
const normalizedKey = this.qualifyKey(
|
|
2115
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1515
2116
|
await this.awaitStartup("has");
|
|
1516
2117
|
for (const layer of this.layers) {
|
|
1517
2118
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1544,7 +2145,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1544
2145
|
* that has it, or null if the key is not found / has no TTL.
|
|
1545
2146
|
*/
|
|
1546
2147
|
async ttl(key) {
|
|
1547
|
-
const normalizedKey = this.qualifyKey(
|
|
2148
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1548
2149
|
await this.awaitStartup("ttl");
|
|
1549
2150
|
for (const layer of this.layers) {
|
|
1550
2151
|
if (this.shouldSkipLayer(layer)) {
|
|
@@ -1566,7 +2167,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1566
2167
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1567
2168
|
*/
|
|
1568
2169
|
async set(key, value, options) {
|
|
1569
|
-
const normalizedKey = this.qualifyKey(
|
|
2170
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1570
2171
|
this.validateWriteOptions(options);
|
|
1571
2172
|
await this.awaitStartup("set");
|
|
1572
2173
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
@@ -1575,7 +2176,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1575
2176
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1576
2177
|
*/
|
|
1577
2178
|
async delete(key) {
|
|
1578
|
-
const normalizedKey = this.qualifyKey(
|
|
2179
|
+
const normalizedKey = this.qualifyKey(validateCacheKey(key));
|
|
1579
2180
|
await this.awaitStartup("delete");
|
|
1580
2181
|
await this.deleteKeys([normalizedKey]);
|
|
1581
2182
|
await this.publishInvalidation({
|
|
@@ -1587,6 +2188,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1587
2188
|
}
|
|
1588
2189
|
async clear() {
|
|
1589
2190
|
await this.awaitStartup("clear");
|
|
2191
|
+
this.maintenance.beginClearEpoch();
|
|
1590
2192
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1591
2193
|
await this.tagIndex.clear();
|
|
1592
2194
|
this.ttlResolver.clearProfiles();
|
|
@@ -1603,7 +2205,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1603
2205
|
return;
|
|
1604
2206
|
}
|
|
1605
2207
|
await this.awaitStartup("mdelete");
|
|
1606
|
-
const normalizedKeys = keys.map((k) =>
|
|
2208
|
+
const normalizedKeys = keys.map((k) => validateCacheKey(k));
|
|
1607
2209
|
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1608
2210
|
await this.deleteKeys(cacheKeys);
|
|
1609
2211
|
await this.publishInvalidation({
|
|
@@ -1620,7 +2222,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1620
2222
|
}
|
|
1621
2223
|
const normalizedEntries = entries.map((entry) => ({
|
|
1622
2224
|
...entry,
|
|
1623
|
-
key: this.qualifyKey(
|
|
2225
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1624
2226
|
}));
|
|
1625
2227
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1626
2228
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1629,7 +2231,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1629
2231
|
const pendingReads = /* @__PURE__ */ new Map();
|
|
1630
2232
|
return Promise.all(
|
|
1631
2233
|
normalizedEntries.map((entry) => {
|
|
1632
|
-
const optionsSignature =
|
|
2234
|
+
const optionsSignature = serializeOptions(entry.options);
|
|
1633
2235
|
const existing = pendingReads.get(entry.key);
|
|
1634
2236
|
if (!existing) {
|
|
1635
2237
|
const promise = this.getPrepared(entry.key, entry.fetch, entry.options);
|
|
@@ -1698,7 +2300,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1698
2300
|
this.assertActive("mset");
|
|
1699
2301
|
const normalizedEntries = entries.map((entry) => ({
|
|
1700
2302
|
...entry,
|
|
1701
|
-
key: this.qualifyKey(
|
|
2303
|
+
key: this.qualifyKey(validateCacheKey(entry.key))
|
|
1702
2304
|
}));
|
|
1703
2305
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1704
2306
|
await this.awaitStartup("mset");
|
|
@@ -1741,7 +2343,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1741
2343
|
*/
|
|
1742
2344
|
wrap(prefix, fetcher, options = {}) {
|
|
1743
2345
|
return (...args) => {
|
|
1744
|
-
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) =>
|
|
2346
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => serializeKeyPart(argument)).join(":");
|
|
1745
2347
|
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
1746
2348
|
return this.get(key, () => fetcher(...args), options);
|
|
1747
2349
|
};
|
|
@@ -1751,11 +2353,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1751
2353
|
* `prefix:`. Useful for multi-tenant or module-level isolation.
|
|
1752
2354
|
*/
|
|
1753
2355
|
namespace(prefix) {
|
|
2356
|
+
validateNamespaceKey(prefix);
|
|
1754
2357
|
return new CacheNamespace(this, prefix);
|
|
1755
2358
|
}
|
|
1756
2359
|
async invalidateByTag(tag) {
|
|
2360
|
+
validateTag(tag);
|
|
1757
2361
|
await this.awaitStartup("invalidateByTag");
|
|
1758
|
-
const keys = await this.
|
|
2362
|
+
const keys = await this.collectKeysForTag(tag);
|
|
1759
2363
|
await this.deleteKeys(keys);
|
|
1760
2364
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1761
2365
|
}
|
|
@@ -1763,23 +2367,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1763
2367
|
if (tags.length === 0) {
|
|
1764
2368
|
return;
|
|
1765
2369
|
}
|
|
2370
|
+
validateTags(tags);
|
|
1766
2371
|
await this.awaitStartup("invalidateByTags");
|
|
1767
|
-
const keysByTag = await Promise.all(tags.map((tag) => this.
|
|
2372
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.collectKeysForTag(tag)));
|
|
1768
2373
|
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
2374
|
+
this.assertWithinInvalidationKeyLimit(keys.length);
|
|
1769
2375
|
await this.deleteKeys(keys);
|
|
1770
2376
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1771
2377
|
}
|
|
1772
2378
|
async invalidateByPattern(pattern) {
|
|
1773
|
-
|
|
2379
|
+
validatePattern(pattern);
|
|
1774
2380
|
await this.awaitStartup("invalidateByPattern");
|
|
1775
|
-
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2381
|
+
const keys = await this.keyDiscovery.collectKeysMatchingPattern(
|
|
2382
|
+
this.qualifyPattern(pattern),
|
|
2383
|
+
this.invalidationMaxKeys()
|
|
2384
|
+
);
|
|
1776
2385
|
await this.deleteKeys(keys);
|
|
1777
2386
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1778
2387
|
}
|
|
1779
2388
|
async invalidateByPrefix(prefix) {
|
|
1780
2389
|
await this.awaitStartup("invalidateByPrefix");
|
|
1781
|
-
const qualifiedPrefix = this.qualifyKey(
|
|
1782
|
-
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix);
|
|
2390
|
+
const qualifiedPrefix = this.qualifyKey(validateCacheKey(prefix));
|
|
2391
|
+
const keys = await this.keyDiscovery.collectKeysWithPrefix(qualifiedPrefix, this.invalidationMaxKeys());
|
|
1783
2392
|
await this.deleteKeys(keys);
|
|
1784
2393
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1785
2394
|
}
|
|
@@ -1837,9 +2446,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1837
2446
|
bumpGeneration(nextGeneration) {
|
|
1838
2447
|
const current = this.currentGeneration ?? 0;
|
|
1839
2448
|
const previousGeneration = this.currentGeneration;
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
2449
|
+
const updatedGeneration = nextGeneration ?? current + 1;
|
|
2450
|
+
const generationToCleanup = resolveGenerationCleanupTarget({
|
|
2451
|
+
previousGeneration,
|
|
2452
|
+
nextGeneration: updatedGeneration,
|
|
2453
|
+
generationCleanup: this.options.generationCleanup
|
|
2454
|
+
});
|
|
2455
|
+
this.currentGeneration = updatedGeneration;
|
|
2456
|
+
if (generationToCleanup !== null) {
|
|
2457
|
+
this.scheduleGenerationCleanup(generationToCleanup);
|
|
1843
2458
|
}
|
|
1844
2459
|
return this.currentGeneration;
|
|
1845
2460
|
}
|
|
@@ -1849,7 +2464,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1849
2464
|
* Returns `null` if the key does not exist in any layer.
|
|
1850
2465
|
*/
|
|
1851
2466
|
async inspect(key) {
|
|
1852
|
-
const userKey =
|
|
2467
|
+
const userKey = validateCacheKey(key);
|
|
1853
2468
|
const normalizedKey = this.qualifyKey(userKey);
|
|
1854
2469
|
await this.awaitStartup("inspect");
|
|
1855
2470
|
const foundInLayers = [];
|
|
@@ -1886,50 +2501,79 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1886
2501
|
}
|
|
1887
2502
|
async exportState() {
|
|
1888
2503
|
await this.awaitStartup("exportState");
|
|
1889
|
-
const
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
const keys = await layer.keys();
|
|
1895
|
-
for (const key of keys) {
|
|
1896
|
-
const exportedKey = this.stripQualifiedKey(key);
|
|
1897
|
-
if (exported.has(exportedKey)) {
|
|
1898
|
-
continue;
|
|
1899
|
-
}
|
|
1900
|
-
const stored = await this.readLayerEntry(layer, key);
|
|
1901
|
-
if (stored === null) {
|
|
1902
|
-
continue;
|
|
1903
|
-
}
|
|
1904
|
-
exported.set(exportedKey, {
|
|
1905
|
-
key: exportedKey,
|
|
1906
|
-
value: stored,
|
|
1907
|
-
ttl: remainingStoredTtlSeconds(stored)
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
return [...exported.values()];
|
|
2504
|
+
const entries = [];
|
|
2505
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2506
|
+
entries.push(entry);
|
|
2507
|
+
});
|
|
2508
|
+
return entries;
|
|
1912
2509
|
}
|
|
1913
2510
|
async importState(entries) {
|
|
1914
2511
|
await this.awaitStartup("importState");
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2512
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
2513
|
+
key: this.qualifyKey(validateCacheKey(entry.key)),
|
|
2514
|
+
value: entry.value,
|
|
2515
|
+
ttl: entry.ttl
|
|
2516
|
+
}));
|
|
2517
|
+
for (let index = 0; index < normalizedEntries.length; index += DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE) {
|
|
2518
|
+
const batch = normalizedEntries.slice(index, index + DEFAULT_SNAPSHOT_IMPORT_BATCH_SIZE);
|
|
2519
|
+
await Promise.all(
|
|
2520
|
+
batch.map(async (entry) => {
|
|
2521
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2522
|
+
await this.tagIndex.touch(entry.key);
|
|
2523
|
+
})
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
1922
2526
|
}
|
|
1923
2527
|
async persistToFile(filePath) {
|
|
1924
2528
|
this.assertActive("persistToFile");
|
|
1925
|
-
const snapshot = await this.exportState();
|
|
1926
2529
|
const { promises: fs2 } = await import("fs");
|
|
1927
|
-
|
|
2530
|
+
const path = await import("path");
|
|
2531
|
+
const targetPath = await validateSnapshotFilePath(filePath, "write", this.options.snapshotBaseDir);
|
|
2532
|
+
const tempPath = path.join(
|
|
2533
|
+
path.dirname(targetPath),
|
|
2534
|
+
`.layercache-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.tmp`
|
|
2535
|
+
);
|
|
2536
|
+
let handle;
|
|
2537
|
+
try {
|
|
2538
|
+
handle = await fs2.open(tempPath, "wx");
|
|
2539
|
+
const openedHandle = handle;
|
|
2540
|
+
await openedHandle.writeFile("[", "utf8");
|
|
2541
|
+
let wroteAny = false;
|
|
2542
|
+
await this.visitExportEntries(this.snapshotMaxEntries(), async (entry) => {
|
|
2543
|
+
await openedHandle.writeFile(wroteAny ? ",\n" : "\n", "utf8");
|
|
2544
|
+
await openedHandle.writeFile(JSON.stringify(entry, null, 2), "utf8");
|
|
2545
|
+
wroteAny = true;
|
|
2546
|
+
});
|
|
2547
|
+
await openedHandle.writeFile(wroteAny ? "\n]" : "]", "utf8");
|
|
2548
|
+
await openedHandle.close();
|
|
2549
|
+
handle = void 0;
|
|
2550
|
+
await fs2.rename(tempPath, targetPath);
|
|
2551
|
+
} catch (error) {
|
|
2552
|
+
await handle?.close().catch(() => void 0);
|
|
2553
|
+
await fs2.unlink(tempPath).catch(() => void 0);
|
|
2554
|
+
throw error;
|
|
2555
|
+
}
|
|
1928
2556
|
}
|
|
1929
2557
|
async restoreFromFile(filePath) {
|
|
1930
2558
|
this.assertActive("restoreFromFile");
|
|
1931
|
-
const { promises: fs2 } = await import("fs");
|
|
1932
|
-
const
|
|
2559
|
+
const { promises: fs2, constants } = await import("fs");
|
|
2560
|
+
const validatedPath = await validateSnapshotFilePath(filePath, "read", this.options.snapshotBaseDir);
|
|
2561
|
+
const handle = await fs2.open(validatedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
2562
|
+
const snapshotMaxBytes = this.snapshotMaxBytes();
|
|
2563
|
+
let raw;
|
|
2564
|
+
try {
|
|
2565
|
+
if (snapshotMaxBytes !== false) {
|
|
2566
|
+
const stat = await handle.stat();
|
|
2567
|
+
if (stat.size > snapshotMaxBytes) {
|
|
2568
|
+
throw new Error(
|
|
2569
|
+
`Snapshot file exceeds snapshotMaxBytes limit (${stat.size} bytes > ${snapshotMaxBytes} bytes).`
|
|
2570
|
+
);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
raw = await readUtf8HandleWithLimit(handle, snapshotMaxBytes);
|
|
2574
|
+
} finally {
|
|
2575
|
+
await handle.close();
|
|
2576
|
+
}
|
|
1933
2577
|
let parsed;
|
|
1934
2578
|
try {
|
|
1935
2579
|
parsed = JSON.parse(raw);
|
|
@@ -1954,12 +2598,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1954
2598
|
await this.startup;
|
|
1955
2599
|
await this.unsubscribeInvalidation?.();
|
|
1956
2600
|
await this.flushWriteBehindQueue();
|
|
1957
|
-
await this.
|
|
2601
|
+
await this.maintenance.waitForGenerationCleanup();
|
|
1958
2602
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1959
|
-
|
|
1960
|
-
clearInterval(this.writeBehindTimer);
|
|
1961
|
-
this.writeBehindTimer = void 0;
|
|
1962
|
-
}
|
|
2603
|
+
this.maintenance.disposeWriteBehindTimer();
|
|
1963
2604
|
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1964
2605
|
})();
|
|
1965
2606
|
}
|
|
@@ -1973,14 +2614,14 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1973
2614
|
await this.handleInvalidationMessage(message);
|
|
1974
2615
|
});
|
|
1975
2616
|
}
|
|
1976
|
-
async fetchWithGuards(key, fetcher, options) {
|
|
2617
|
+
async fetchWithGuards(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
1977
2618
|
const fetchTask = async () => {
|
|
1978
2619
|
const secondHit = await this.readFromLayers(key, options, "fresh-only");
|
|
1979
2620
|
if (secondHit.found) {
|
|
1980
2621
|
this.metricsCollector.increment("hits");
|
|
1981
2622
|
return secondHit.value;
|
|
1982
2623
|
}
|
|
1983
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2624
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
1984
2625
|
};
|
|
1985
2626
|
const singleFlightTask = async () => {
|
|
1986
2627
|
if (!this.options.singleFlightCoordinator) {
|
|
@@ -1990,7 +2631,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1990
2631
|
key,
|
|
1991
2632
|
this.resolveSingleFlightOptions(),
|
|
1992
2633
|
fetchTask,
|
|
1993
|
-
() => this.waitForFreshValue(key, fetcher, options)
|
|
2634
|
+
() => this.waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch)
|
|
1994
2635
|
);
|
|
1995
2636
|
};
|
|
1996
2637
|
if (this.options.stampedePrevention === false) {
|
|
@@ -1998,7 +2639,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1998
2639
|
}
|
|
1999
2640
|
return this.stampedeGuard.execute(key, singleFlightTask);
|
|
2000
2641
|
}
|
|
2001
|
-
async waitForFreshValue(key, fetcher, options) {
|
|
2642
|
+
async waitForFreshValue(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2002
2643
|
const timeoutMs = this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS;
|
|
2003
2644
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
2004
2645
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -2012,9 +2653,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2012
2653
|
}
|
|
2013
2654
|
await this.sleep(pollIntervalMs);
|
|
2014
2655
|
}
|
|
2015
|
-
return this.fetchAndPopulate(key, fetcher, options);
|
|
2656
|
+
return this.fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch);
|
|
2016
2657
|
}
|
|
2017
|
-
async fetchAndPopulate(key, fetcher, options) {
|
|
2658
|
+
async fetchAndPopulate(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2018
2659
|
this.circuitBreakerManager.assertClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
2019
2660
|
this.metricsCollector.increment("fetches");
|
|
2020
2661
|
const fetchStart = Date.now();
|
|
@@ -2035,6 +2676,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2035
2676
|
if (!this.shouldNegativeCache(options)) {
|
|
2036
2677
|
return null;
|
|
2037
2678
|
}
|
|
2679
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2680
|
+
this.logger.debug?.("skip-negative-store-after-invalidation", {
|
|
2681
|
+
key,
|
|
2682
|
+
expectedClearEpoch,
|
|
2683
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2684
|
+
expectedKeyEpoch,
|
|
2685
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2686
|
+
});
|
|
2687
|
+
return null;
|
|
2688
|
+
}
|
|
2038
2689
|
await this.storeEntry(key, "empty", null, options);
|
|
2039
2690
|
return null;
|
|
2040
2691
|
}
|
|
@@ -2047,11 +2698,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2047
2698
|
this.logger.warn?.("shouldCache-error", { key, error: this.formatError(error) });
|
|
2048
2699
|
}
|
|
2049
2700
|
}
|
|
2701
|
+
if (this.maintenance.isWriteOutdated(key, expectedClearEpoch, expectedKeyEpoch)) {
|
|
2702
|
+
this.logger.debug?.("skip-store-after-invalidation", {
|
|
2703
|
+
key,
|
|
2704
|
+
expectedClearEpoch,
|
|
2705
|
+
clearEpoch: this.maintenance.currentClearEpoch(),
|
|
2706
|
+
expectedKeyEpoch,
|
|
2707
|
+
keyEpoch: this.maintenance.currentKeyEpoch(key)
|
|
2708
|
+
});
|
|
2709
|
+
return fetched;
|
|
2710
|
+
}
|
|
2050
2711
|
await this.storeEntry(key, "value", fetched, options);
|
|
2051
2712
|
return fetched;
|
|
2052
2713
|
}
|
|
2053
2714
|
async storeEntry(key, kind, value, options) {
|
|
2715
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2716
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2054
2717
|
await this.writeAcrossLayers(key, kind, value, options);
|
|
2718
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2055
2721
|
if (options?.tags) {
|
|
2056
2722
|
await this.tagIndex.track(key, options.tags);
|
|
2057
2723
|
} else {
|
|
@@ -2066,6 +2732,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2066
2732
|
}
|
|
2067
2733
|
async writeBatch(entries) {
|
|
2068
2734
|
const now = Date.now();
|
|
2735
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2736
|
+
const entryEpochs = new Map(entries.map((entry) => [entry.key, this.maintenance.currentKeyEpoch(entry.key)]));
|
|
2069
2737
|
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
2070
2738
|
const immediateOperations = [];
|
|
2071
2739
|
const deferredOperations = [];
|
|
@@ -2082,12 +2750,21 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2082
2750
|
}
|
|
2083
2751
|
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
2084
2752
|
const operation = async () => {
|
|
2753
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
const activeEntries = layerEntries.filter(
|
|
2757
|
+
(entry) => (entryEpochs.get(entry.key) ?? 0) === this.maintenance.currentKeyEpoch(entry.key)
|
|
2758
|
+
);
|
|
2759
|
+
if (activeEntries.length === 0) {
|
|
2760
|
+
return;
|
|
2761
|
+
}
|
|
2085
2762
|
try {
|
|
2086
2763
|
if (layer.setMany) {
|
|
2087
|
-
await layer.setMany(
|
|
2764
|
+
await layer.setMany(activeEntries);
|
|
2088
2765
|
return;
|
|
2089
2766
|
}
|
|
2090
|
-
await Promise.all(
|
|
2767
|
+
await Promise.all(activeEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
2091
2768
|
} catch (error) {
|
|
2092
2769
|
await this.handleLayerFailure(layer, "write", error);
|
|
2093
2770
|
}
|
|
@@ -2100,7 +2777,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2100
2777
|
}
|
|
2101
2778
|
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
2102
2779
|
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
2780
|
+
if (clearEpoch !== this.maintenance.currentClearEpoch()) {
|
|
2781
|
+
return;
|
|
2782
|
+
}
|
|
2103
2783
|
for (const entry of entries) {
|
|
2784
|
+
if (this.maintenance.isWriteOutdated(entry.key, clearEpoch, entryEpochs.get(entry.key))) {
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2104
2787
|
if (entry.options?.tags) {
|
|
2105
2788
|
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
2106
2789
|
} else {
|
|
@@ -2202,10 +2885,15 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2202
2885
|
}
|
|
2203
2886
|
async writeAcrossLayers(key, kind, value, options) {
|
|
2204
2887
|
const now = Date.now();
|
|
2888
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2889
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2205
2890
|
const immediateOperations = [];
|
|
2206
2891
|
const deferredOperations = [];
|
|
2207
2892
|
for (const layer of this.layers) {
|
|
2208
2893
|
const operation = async () => {
|
|
2894
|
+
if (this.maintenance.isWriteOutdated(key, clearEpoch, keyEpoch)) {
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2209
2897
|
if (this.shouldSkipLayer(layer)) {
|
|
2210
2898
|
return;
|
|
2211
2899
|
}
|
|
@@ -2266,13 +2954,18 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2266
2954
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
2267
2955
|
}
|
|
2268
2956
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
2269
|
-
if (
|
|
2957
|
+
if (!shouldStartBackgroundRefresh({
|
|
2958
|
+
isDisconnecting: this.isDisconnecting,
|
|
2959
|
+
hasRefreshInFlight: this.backgroundRefreshes.has(key)
|
|
2960
|
+
})) {
|
|
2270
2961
|
return;
|
|
2271
2962
|
}
|
|
2963
|
+
const clearEpoch = this.maintenance.currentClearEpoch();
|
|
2964
|
+
const keyEpoch = this.maintenance.currentKeyEpoch(key);
|
|
2272
2965
|
const refresh = (async () => {
|
|
2273
2966
|
this.metricsCollector.increment("refreshes");
|
|
2274
2967
|
try {
|
|
2275
|
-
await this.runBackgroundRefresh(key, fetcher, options);
|
|
2968
|
+
await this.runBackgroundRefresh(key, fetcher, options, clearEpoch, keyEpoch);
|
|
2276
2969
|
} catch (error) {
|
|
2277
2970
|
this.metricsCollector.increment("refreshErrors");
|
|
2278
2971
|
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
@@ -2282,14 +2975,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2282
2975
|
})();
|
|
2283
2976
|
this.backgroundRefreshes.set(key, refresh);
|
|
2284
2977
|
}
|
|
2285
|
-
async runBackgroundRefresh(key, fetcher, options) {
|
|
2978
|
+
async runBackgroundRefresh(key, fetcher, options, expectedClearEpoch, expectedKeyEpoch) {
|
|
2286
2979
|
const timeoutMs = this.options.backgroundRefreshTimeoutMs ?? DEFAULT_BACKGROUND_REFRESH_TIMEOUT_MS;
|
|
2287
2980
|
await this.fetchWithGuards(
|
|
2288
2981
|
key,
|
|
2289
2982
|
() => this.withTimeout(fetcher(), timeoutMs, () => {
|
|
2290
2983
|
return new Error(`Background refresh timed out after ${timeoutMs}ms for key "${key}".`);
|
|
2291
2984
|
}),
|
|
2292
|
-
options
|
|
2985
|
+
options,
|
|
2986
|
+
expectedClearEpoch,
|
|
2987
|
+
expectedKeyEpoch
|
|
2293
2988
|
);
|
|
2294
2989
|
}
|
|
2295
2990
|
resolveSingleFlightOptions() {
|
|
@@ -2304,6 +2999,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2304
2999
|
if (keys.length === 0) {
|
|
2305
3000
|
return;
|
|
2306
3001
|
}
|
|
3002
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2307
3003
|
await this.deleteKeysFromLayers(this.layers, keys);
|
|
2308
3004
|
for (const key of keys) {
|
|
2309
3005
|
await this.tagIndex.remove(key);
|
|
@@ -2326,21 +3022,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2326
3022
|
return;
|
|
2327
3023
|
}
|
|
2328
3024
|
const localLayers = this.layers.filter((layer) => layer.isLocal);
|
|
2329
|
-
if (localLayers.length === 0) {
|
|
2330
|
-
return;
|
|
2331
|
-
}
|
|
2332
3025
|
if (message.scope === "clear") {
|
|
3026
|
+
this.maintenance.beginClearEpoch();
|
|
2333
3027
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
2334
3028
|
await this.tagIndex.clear();
|
|
2335
3029
|
this.ttlResolver.clearProfiles();
|
|
3030
|
+
this.circuitBreakerManager.clear();
|
|
2336
3031
|
return;
|
|
2337
3032
|
}
|
|
2338
3033
|
const keys = message.keys ?? [];
|
|
3034
|
+
this.maintenance.bumpKeyEpochs(keys);
|
|
2339
3035
|
await this.deleteKeysFromLayers(localLayers, keys);
|
|
2340
3036
|
if (message.operation !== "write") {
|
|
2341
3037
|
for (const key of keys) {
|
|
2342
3038
|
await this.tagIndex.remove(key);
|
|
2343
3039
|
this.ttlResolver.deleteProfile(key);
|
|
3040
|
+
this.circuitBreakerManager.delete(key);
|
|
2344
3041
|
}
|
|
2345
3042
|
}
|
|
2346
3043
|
}
|
|
@@ -2392,35 +3089,22 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2392
3089
|
shouldBroadcastL1Invalidation() {
|
|
2393
3090
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? false;
|
|
2394
3091
|
}
|
|
2395
|
-
shouldCleanupGenerations() {
|
|
2396
|
-
return Boolean(this.options.generationCleanup);
|
|
2397
|
-
}
|
|
2398
|
-
generationCleanupBatchSize() {
|
|
2399
|
-
const configured = typeof this.options.generationCleanup === "object" ? this.options.generationCleanup.batchSize : void 0;
|
|
2400
|
-
return configured ?? 500;
|
|
2401
|
-
}
|
|
2402
3092
|
scheduleGenerationCleanup(generation) {
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
this.generationCleanupPromise = void 0;
|
|
3093
|
+
this.maintenance.scheduleGenerationCleanup(
|
|
3094
|
+
generation,
|
|
3095
|
+
async (generationToClean) => this.cleanupGeneration(generationToClean),
|
|
3096
|
+
(failedGeneration, error) => {
|
|
3097
|
+
this.logger.warn?.("generation-cleanup-error", {
|
|
3098
|
+
generation: failedGeneration,
|
|
3099
|
+
error: this.formatError(error)
|
|
3100
|
+
});
|
|
2412
3101
|
}
|
|
2413
|
-
|
|
3102
|
+
);
|
|
2414
3103
|
}
|
|
2415
3104
|
async cleanupGeneration(generation) {
|
|
2416
3105
|
const prefix = `v${generation}:`;
|
|
2417
3106
|
const keys = await this.keyDiscovery.collectKeysWithPrefix(prefix);
|
|
2418
|
-
|
|
2419
|
-
return;
|
|
2420
|
-
}
|
|
2421
|
-
const batchSize = this.generationCleanupBatchSize();
|
|
2422
|
-
for (let index = 0; index < keys.length; index += batchSize) {
|
|
2423
|
-
const batch = keys.slice(index, index + batchSize);
|
|
3107
|
+
for (const batch of planGenerationCleanupBatches(keys, this.options.generationCleanup)) {
|
|
2424
3108
|
await this.deleteKeys(batch);
|
|
2425
3109
|
await this.publishInvalidation({
|
|
2426
3110
|
scope: "keys",
|
|
@@ -2431,58 +3115,34 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2431
3115
|
}
|
|
2432
3116
|
}
|
|
2433
3117
|
initializeWriteBehind(options) {
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
return;
|
|
2440
|
-
}
|
|
2441
|
-
this.writeBehindTimer = setInterval(() => {
|
|
2442
|
-
void this.flushWriteBehindQueue();
|
|
2443
|
-
}, flushIntervalMs);
|
|
2444
|
-
this.writeBehindTimer.unref?.();
|
|
3118
|
+
this.maintenance.initializeWriteBehindTimer(
|
|
3119
|
+
this.options.writeStrategy,
|
|
3120
|
+
options,
|
|
3121
|
+
this.flushWriteBehindQueue.bind(this)
|
|
3122
|
+
);
|
|
2445
3123
|
}
|
|
2446
3124
|
shouldWriteBehind(layer) {
|
|
2447
3125
|
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2448
3126
|
}
|
|
2449
3127
|
async enqueueWriteBehind(operation) {
|
|
2450
|
-
this.
|
|
2451
|
-
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2452
|
-
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2453
|
-
if (this.writeBehindQueue.length >= batchSize) {
|
|
2454
|
-
await this.flushWriteBehindQueue();
|
|
2455
|
-
return;
|
|
2456
|
-
}
|
|
2457
|
-
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2458
|
-
await this.flushWriteBehindQueue();
|
|
2459
|
-
}
|
|
3128
|
+
await this.maintenance.enqueueWriteBehind(operation, this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
2460
3129
|
}
|
|
2461
3130
|
async flushWriteBehindQueue() {
|
|
2462
|
-
|
|
2463
|
-
|
|
3131
|
+
await this.maintenance.flushWriteBehindQueue(this.options.writeBehind, this.runWriteBehindBatch.bind(this));
|
|
3132
|
+
}
|
|
3133
|
+
async runWriteBehindBatch(batch) {
|
|
3134
|
+
const results = await Promise.allSettled(batch.map((operation) => operation()));
|
|
3135
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
3136
|
+
if (failures.length === 0) {
|
|
2464
3137
|
return;
|
|
2465
3138
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
this.logger.error?.("write-behind-flush-failure", {
|
|
2474
|
-
failed: failures.length,
|
|
2475
|
-
total: batch.length,
|
|
2476
|
-
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
2477
|
-
});
|
|
2478
|
-
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2479
|
-
}
|
|
2480
|
-
})();
|
|
2481
|
-
await this.writeBehindFlushPromise;
|
|
2482
|
-
this.writeBehindFlushPromise = void 0;
|
|
2483
|
-
if (this.writeBehindQueue.length > 0) {
|
|
2484
|
-
await this.flushWriteBehindQueue();
|
|
2485
|
-
}
|
|
3139
|
+
this.metricsCollector.increment("writeFailures", failures.length);
|
|
3140
|
+
this.logger.error?.("write-behind-flush-failure", {
|
|
3141
|
+
failed: failures.length,
|
|
3142
|
+
total: batch.length,
|
|
3143
|
+
errors: failures.map((failure) => this.formatError(failure.reason))
|
|
3144
|
+
});
|
|
3145
|
+
this.emitError("write-behind", { failed: failures.length, total: batch.length });
|
|
2486
3146
|
}
|
|
2487
3147
|
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2488
3148
|
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
@@ -2512,32 +3172,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2512
3172
|
return [];
|
|
2513
3173
|
}
|
|
2514
3174
|
const [firstGroup, ...rest] = groups;
|
|
2515
|
-
if (!firstGroup) {
|
|
2516
|
-
return [];
|
|
2517
|
-
}
|
|
2518
3175
|
const restSets = rest.map((group) => new Set(group));
|
|
2519
3176
|
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2520
3177
|
}
|
|
2521
3178
|
qualifyKey(key) {
|
|
2522
|
-
|
|
2523
|
-
return prefix ? `${prefix}${key}` : key;
|
|
3179
|
+
return qualifyGenerationKey(key, this.currentGeneration);
|
|
2524
3180
|
}
|
|
2525
3181
|
qualifyPattern(pattern) {
|
|
2526
|
-
|
|
2527
|
-
return prefix ? `${prefix}${pattern}` : pattern;
|
|
3182
|
+
return qualifyGenerationPattern(pattern, this.currentGeneration);
|
|
2528
3183
|
}
|
|
2529
3184
|
stripQualifiedKey(key) {
|
|
2530
|
-
|
|
2531
|
-
if (!prefix || !key.startsWith(prefix)) {
|
|
2532
|
-
return key;
|
|
2533
|
-
}
|
|
2534
|
-
return key.slice(prefix.length);
|
|
2535
|
-
}
|
|
2536
|
-
generationPrefix() {
|
|
2537
|
-
if (this.currentGeneration === void 0) {
|
|
2538
|
-
return "";
|
|
2539
|
-
}
|
|
2540
|
-
return `v${this.currentGeneration}:`;
|
|
3185
|
+
return stripGenerationPrefix(key, this.currentGeneration);
|
|
2541
3186
|
}
|
|
2542
3187
|
async deleteKeysFromLayers(layers, keys) {
|
|
2543
3188
|
await Promise.all(
|
|
@@ -2572,118 +3217,50 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2572
3217
|
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
2573
3218
|
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
2574
3219
|
}
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
3220
|
+
validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
3221
|
+
validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
3222
|
+
validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
3223
|
+
validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
3224
|
+
validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
3225
|
+
validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
3226
|
+
validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
3227
|
+
validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
3228
|
+
validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
3229
|
+
validatePositiveNumber("backgroundRefreshTimeoutMs", this.options.backgroundRefreshTimeoutMs);
|
|
3230
|
+
if (this.options.snapshotMaxBytes !== false) {
|
|
3231
|
+
validatePositiveNumber("snapshotMaxBytes", this.options.snapshotMaxBytes);
|
|
3232
|
+
}
|
|
3233
|
+
if (this.options.snapshotMaxEntries !== false) {
|
|
3234
|
+
validatePositiveNumber("snapshotMaxEntries", this.options.snapshotMaxEntries);
|
|
3235
|
+
}
|
|
3236
|
+
if (this.options.invalidationMaxKeys !== false) {
|
|
3237
|
+
validatePositiveNumber("invalidationMaxKeys", this.options.invalidationMaxKeys);
|
|
3238
|
+
}
|
|
3239
|
+
validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
3240
|
+
validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
3241
|
+
validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2588
3242
|
if (typeof this.options.generationCleanup === "object") {
|
|
2589
|
-
|
|
3243
|
+
validatePositiveNumber("generationCleanup.batchSize", this.options.generationCleanup.batchSize);
|
|
2590
3244
|
}
|
|
2591
3245
|
if (this.options.generation !== void 0) {
|
|
2592
|
-
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
validateWriteOptions(options) {
|
|
2596
|
-
if (!options) {
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
|
-
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
2600
|
-
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
2601
|
-
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
2602
|
-
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
2603
|
-
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
2604
|
-
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2605
|
-
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
2606
|
-
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
2607
|
-
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2608
|
-
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
2609
|
-
}
|
|
2610
|
-
validateLayerNumberOption(name, value) {
|
|
2611
|
-
if (value === void 0) {
|
|
2612
|
-
return;
|
|
2613
|
-
}
|
|
2614
|
-
if (typeof value === "number") {
|
|
2615
|
-
this.validateNonNegativeNumber(name, value);
|
|
2616
|
-
return;
|
|
2617
|
-
}
|
|
2618
|
-
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
2619
|
-
if (layerValue === void 0) {
|
|
2620
|
-
continue;
|
|
2621
|
-
}
|
|
2622
|
-
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
2623
|
-
}
|
|
2624
|
-
}
|
|
2625
|
-
validatePositiveNumber(name, value) {
|
|
2626
|
-
if (value === void 0) {
|
|
2627
|
-
return;
|
|
2628
|
-
}
|
|
2629
|
-
if (!Number.isFinite(value) || value <= 0) {
|
|
2630
|
-
throw new Error(`${name} must be a positive finite number.`);
|
|
2631
|
-
}
|
|
2632
|
-
}
|
|
2633
|
-
validateRateLimitOptions(name, options) {
|
|
2634
|
-
if (!options) {
|
|
2635
|
-
return;
|
|
2636
|
-
}
|
|
2637
|
-
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2638
|
-
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2639
|
-
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2640
|
-
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2641
|
-
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2642
|
-
}
|
|
2643
|
-
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2644
|
-
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2645
|
-
}
|
|
2646
|
-
}
|
|
2647
|
-
validateNonNegativeNumber(name, value) {
|
|
2648
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
2649
|
-
throw new Error(`${name} must be a non-negative finite number.`);
|
|
2650
|
-
}
|
|
2651
|
-
}
|
|
2652
|
-
validateCacheKey(key) {
|
|
2653
|
-
if (key.length === 0) {
|
|
2654
|
-
throw new Error("Cache key must not be empty.");
|
|
2655
|
-
}
|
|
2656
|
-
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
2657
|
-
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
2658
|
-
}
|
|
2659
|
-
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
2660
|
-
throw new Error("Cache key contains unsupported control characters.");
|
|
2661
|
-
}
|
|
2662
|
-
if (/[\uD800-\uDFFF]/.test(key)) {
|
|
2663
|
-
throw new Error("Cache key contains unsupported surrogate code points.");
|
|
2664
|
-
}
|
|
2665
|
-
return key;
|
|
2666
|
-
}
|
|
2667
|
-
validatePattern(pattern) {
|
|
2668
|
-
if (pattern.length === 0) {
|
|
2669
|
-
throw new Error("Pattern must not be empty.");
|
|
2670
|
-
}
|
|
2671
|
-
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2672
|
-
throw new Error(`Pattern length must be at most ${MAX_PATTERN_LENGTH} characters.`);
|
|
2673
|
-
}
|
|
2674
|
-
if (/[\u0000-\u001F\u007F]/.test(pattern)) {
|
|
2675
|
-
throw new Error("Pattern contains unsupported control characters.");
|
|
3246
|
+
validateNonNegativeNumber("generation", this.options.generation);
|
|
2676
3247
|
}
|
|
2677
3248
|
}
|
|
2678
|
-
|
|
2679
|
-
if (!
|
|
2680
|
-
return;
|
|
2681
|
-
}
|
|
2682
|
-
if ("alignTo" in policy) {
|
|
2683
|
-
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
3249
|
+
validateWriteOptions(options) {
|
|
3250
|
+
if (!options) {
|
|
2684
3251
|
return;
|
|
2685
3252
|
}
|
|
2686
|
-
|
|
3253
|
+
validateLayerNumberOption("options.ttl", options.ttl);
|
|
3254
|
+
validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
3255
|
+
validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
3256
|
+
validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
3257
|
+
validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
3258
|
+
validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
3259
|
+
validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
3260
|
+
validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
3261
|
+
validateCircuitBreakerOptions(options.circuitBreaker);
|
|
3262
|
+
validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
3263
|
+
validateTags(options.tags);
|
|
2687
3264
|
}
|
|
2688
3265
|
assertActive(operation) {
|
|
2689
3266
|
if (this.isDisconnecting) {
|
|
@@ -2695,56 +3272,39 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2695
3272
|
await this.startup;
|
|
2696
3273
|
this.assertActive(operation);
|
|
2697
3274
|
}
|
|
2698
|
-
serializeOptions(options) {
|
|
2699
|
-
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
2700
|
-
}
|
|
2701
|
-
validateAdaptiveTtlOptions(options) {
|
|
2702
|
-
if (!options || options === true) {
|
|
2703
|
-
return;
|
|
2704
|
-
}
|
|
2705
|
-
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
2706
|
-
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
2707
|
-
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
2708
|
-
}
|
|
2709
|
-
validateCircuitBreakerOptions(options) {
|
|
2710
|
-
if (!options) {
|
|
2711
|
-
return;
|
|
2712
|
-
}
|
|
2713
|
-
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
2714
|
-
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
2715
|
-
}
|
|
2716
3275
|
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
3276
|
+
const plan = planFreshReadPolicies({
|
|
3277
|
+
stored: hit.stored,
|
|
3278
|
+
hasFetcher: Boolean(fetcher),
|
|
3279
|
+
slidingTtl: options?.slidingTtl ?? false,
|
|
3280
|
+
refreshAheadSeconds: this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0
|
|
3281
|
+
});
|
|
3282
|
+
if (plan.refreshedStored) {
|
|
2722
3283
|
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
2723
3284
|
const layer = this.layers[index];
|
|
2724
3285
|
if (!layer || this.shouldSkipLayer(layer)) {
|
|
2725
3286
|
continue;
|
|
2726
3287
|
}
|
|
2727
3288
|
try {
|
|
2728
|
-
await layer.set(key,
|
|
3289
|
+
await layer.set(key, plan.refreshedStored, plan.refreshedStoredTtl);
|
|
2729
3290
|
} catch (error) {
|
|
2730
3291
|
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
2731
3292
|
}
|
|
2732
3293
|
}
|
|
2733
3294
|
}
|
|
2734
|
-
if (fetcher &&
|
|
3295
|
+
if (fetcher && plan.shouldScheduleBackgroundRefresh) {
|
|
2735
3296
|
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
2736
3297
|
}
|
|
2737
3298
|
}
|
|
2738
3299
|
shouldSkipLayer(layer) {
|
|
2739
|
-
|
|
2740
|
-
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
3300
|
+
return shouldSkipLayer(this.layerDegradedUntil.get(layer.name));
|
|
2741
3301
|
}
|
|
2742
3302
|
async handleLayerFailure(layer, operation, error) {
|
|
2743
|
-
|
|
3303
|
+
const recovery = resolveRecoverableLayerFailure(this.options.gracefulDegradation);
|
|
3304
|
+
if (!recovery.degrade) {
|
|
2744
3305
|
throw error;
|
|
2745
3306
|
}
|
|
2746
|
-
|
|
2747
|
-
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
3307
|
+
this.layerDegradedUntil.set(layer.name, recovery.degradedUntil);
|
|
2748
3308
|
this.metricsCollector.increment("degradedOperations");
|
|
2749
3309
|
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
2750
3310
|
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
@@ -2780,18 +3340,6 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2780
3340
|
this.emit("error", { operation, ...context });
|
|
2781
3341
|
}
|
|
2782
3342
|
}
|
|
2783
|
-
serializeKeyPart(value) {
|
|
2784
|
-
if (typeof value === "string") {
|
|
2785
|
-
return `s:${value}`;
|
|
2786
|
-
}
|
|
2787
|
-
if (typeof value === "number") {
|
|
2788
|
-
return `n:${value}`;
|
|
2789
|
-
}
|
|
2790
|
-
if (typeof value === "boolean") {
|
|
2791
|
-
return `b:${value}`;
|
|
2792
|
-
}
|
|
2793
|
-
return `j:${JSON.stringify(this.normalizeForSerialization(value))}`;
|
|
2794
|
-
}
|
|
2795
3343
|
isCacheSnapshotEntries(value) {
|
|
2796
3344
|
return Array.isArray(value) && value.every((entry) => {
|
|
2797
3345
|
if (!entry || typeof entry !== "object") {
|
|
@@ -2804,54 +3352,72 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
2804
3352
|
sanitizeSnapshotValue(value) {
|
|
2805
3353
|
return this.snapshotSerializer.deserialize(this.snapshotSerializer.serialize(value));
|
|
2806
3354
|
}
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
3355
|
+
snapshotMaxBytes() {
|
|
3356
|
+
return this.options.snapshotMaxBytes === false ? false : this.options.snapshotMaxBytes ?? DEFAULT_SNAPSHOT_MAX_BYTES;
|
|
3357
|
+
}
|
|
3358
|
+
snapshotMaxEntries() {
|
|
3359
|
+
return this.options.snapshotMaxEntries === false ? false : this.options.snapshotMaxEntries ?? DEFAULT_SNAPSHOT_MAX_ENTRIES;
|
|
3360
|
+
}
|
|
3361
|
+
invalidationMaxKeys() {
|
|
3362
|
+
return this.options.invalidationMaxKeys === false ? false : this.options.invalidationMaxKeys ?? DEFAULT_INVALIDATION_MAX_KEYS;
|
|
3363
|
+
}
|
|
3364
|
+
async collectKeysForTag(tag) {
|
|
3365
|
+
const keys = /* @__PURE__ */ new Set();
|
|
3366
|
+
if (this.tagIndex.forEachKeyForTag) {
|
|
3367
|
+
await this.tagIndex.forEachKeyForTag(tag, async (key) => {
|
|
3368
|
+
keys.add(key);
|
|
3369
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
3370
|
+
});
|
|
3371
|
+
return [...keys];
|
|
2813
3372
|
}
|
|
2814
|
-
const
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
if (baseDir !== false) {
|
|
2818
|
-
const relative = path.relative(baseDir, resolved);
|
|
2819
|
-
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
2820
|
-
throw new Error(`filePath is outside the allowed snapshot directory: ${baseDir}`);
|
|
2821
|
-
}
|
|
3373
|
+
for (const key of await this.tagIndex.keysForTag(tag)) {
|
|
3374
|
+
keys.add(key);
|
|
3375
|
+
this.assertWithinInvalidationKeyLimit(keys.size);
|
|
2822
3376
|
}
|
|
2823
|
-
return
|
|
3377
|
+
return [...keys];
|
|
2824
3378
|
}
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
3379
|
+
assertWithinInvalidationKeyLimit(size) {
|
|
3380
|
+
const maxKeys = this.invalidationMaxKeys();
|
|
3381
|
+
if (maxKeys !== false && size > maxKeys) {
|
|
3382
|
+
throw new Error(`Invalidation matched too many keys (${size} > ${maxKeys}).`);
|
|
2828
3383
|
}
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
3384
|
+
}
|
|
3385
|
+
async visitExportEntries(maxEntries, visitor) {
|
|
3386
|
+
const exported = /* @__PURE__ */ new Set();
|
|
3387
|
+
for (const layer of this.layers) {
|
|
3388
|
+
if (!layer.keys && !layer.forEachKey) {
|
|
3389
|
+
continue;
|
|
3390
|
+
}
|
|
3391
|
+
const visitKey = async (key) => {
|
|
3392
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
3393
|
+
if (exported.has(exportedKey)) {
|
|
3394
|
+
return;
|
|
2833
3395
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
3396
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
3397
|
+
if (stored === null) {
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
exported.add(exportedKey);
|
|
3401
|
+
if (maxEntries !== false && exported.size > maxEntries) {
|
|
3402
|
+
throw new Error(`Snapshot export exceeds snapshotMaxEntries limit (${exported.size} > ${maxEntries}).`);
|
|
3403
|
+
}
|
|
3404
|
+
await visitor({
|
|
3405
|
+
key: exportedKey,
|
|
3406
|
+
value: stored,
|
|
3407
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
3408
|
+
});
|
|
3409
|
+
};
|
|
3410
|
+
if (layer.forEachKey) {
|
|
3411
|
+
await layer.forEachKey(visitKey);
|
|
3412
|
+
continue;
|
|
3413
|
+
}
|
|
3414
|
+
const keys = await layer.keys?.();
|
|
3415
|
+
for (const key of keys ?? []) {
|
|
3416
|
+
await visitKey(key);
|
|
3417
|
+
}
|
|
2837
3418
|
}
|
|
2838
|
-
return value;
|
|
2839
3419
|
}
|
|
2840
3420
|
};
|
|
2841
|
-
function createInstanceId() {
|
|
2842
|
-
if (globalThis.crypto?.randomUUID) {
|
|
2843
|
-
return globalThis.crypto.randomUUID();
|
|
2844
|
-
}
|
|
2845
|
-
const bytes = new Uint8Array(16);
|
|
2846
|
-
if (globalThis.crypto?.getRandomValues) {
|
|
2847
|
-
globalThis.crypto.getRandomValues(bytes);
|
|
2848
|
-
} else {
|
|
2849
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
2850
|
-
bytes[i] = Math.floor(Math.random() * 256);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
return `layercache-${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
2854
|
-
}
|
|
2855
3421
|
|
|
2856
3422
|
// src/invalidation/RedisInvalidationBus.ts
|
|
2857
3423
|
var RedisInvalidationBus = class {
|
|
@@ -2930,15 +3496,24 @@ var RedisInvalidationBus = class {
|
|
|
2930
3496
|
}
|
|
2931
3497
|
};
|
|
2932
3498
|
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2933
|
-
|
|
3499
|
+
var MAX_SANITIZE_DEPTH2 = 64;
|
|
3500
|
+
var MAX_SANITIZE_NODES2 = 1e4;
|
|
3501
|
+
function sanitizeJsonValue2(value, depth = 0, state = { count: 0 }) {
|
|
3502
|
+
state.count += 1;
|
|
3503
|
+
if (state.count > MAX_SANITIZE_NODES2) {
|
|
3504
|
+
throw new Error(`Invalidation payload exceeds max node count of ${MAX_SANITIZE_NODES2}.`);
|
|
3505
|
+
}
|
|
3506
|
+
if (depth > MAX_SANITIZE_DEPTH2) {
|
|
3507
|
+
throw new Error(`Invalidation payload exceeds max depth of ${MAX_SANITIZE_DEPTH2}.`);
|
|
3508
|
+
}
|
|
2934
3509
|
if (Array.isArray(value)) {
|
|
2935
|
-
return value.map(sanitizeJsonValue2);
|
|
3510
|
+
return value.map((entry) => sanitizeJsonValue2(entry, depth + 1, state));
|
|
2936
3511
|
}
|
|
2937
3512
|
if (value && typeof value === "object") {
|
|
2938
3513
|
const result = /* @__PURE__ */ Object.create(null);
|
|
2939
3514
|
for (const key of Object.keys(value)) {
|
|
2940
3515
|
if (!DANGEROUS_KEYS.has(key)) {
|
|
2941
|
-
result[key] = sanitizeJsonValue2(value[key]);
|
|
3516
|
+
result[key] = sanitizeJsonValue2(value[key], depth + 1, state);
|
|
2942
3517
|
}
|
|
2943
3518
|
}
|
|
2944
3519
|
return result;
|
|
@@ -2992,6 +3567,17 @@ var RedisTagIndex = class {
|
|
|
2992
3567
|
async keysForTag(tag) {
|
|
2993
3568
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
2994
3569
|
}
|
|
3570
|
+
async forEachKeyForTag(tag, visitor) {
|
|
3571
|
+
let cursor = "0";
|
|
3572
|
+
const tagKey = this.tagKeysKey(tag);
|
|
3573
|
+
do {
|
|
3574
|
+
const [nextCursor, keys] = await this.client.sscan(tagKey, cursor, "COUNT", this.scanCount);
|
|
3575
|
+
cursor = nextCursor;
|
|
3576
|
+
for (const key of keys) {
|
|
3577
|
+
await visitor(key);
|
|
3578
|
+
}
|
|
3579
|
+
} while (cursor !== "0");
|
|
3580
|
+
}
|
|
2995
3581
|
async keysForPrefix(prefix) {
|
|
2996
3582
|
const matches = [];
|
|
2997
3583
|
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
@@ -3004,6 +3590,20 @@ var RedisTagIndex = class {
|
|
|
3004
3590
|
}
|
|
3005
3591
|
return matches;
|
|
3006
3592
|
}
|
|
3593
|
+
async forEachKeyForPrefix(prefix, visitor) {
|
|
3594
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
3595
|
+
let cursor = "0";
|
|
3596
|
+
do {
|
|
3597
|
+
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
3598
|
+
cursor = nextCursor;
|
|
3599
|
+
for (const key of keys) {
|
|
3600
|
+
if (key.startsWith(prefix)) {
|
|
3601
|
+
await visitor(key);
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
} while (cursor !== "0");
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3007
3607
|
async tagsForKey(key) {
|
|
3008
3608
|
return this.client.smembers(this.keyTagsKey(key));
|
|
3009
3609
|
}
|
|
@@ -3026,6 +3626,27 @@ var RedisTagIndex = class {
|
|
|
3026
3626
|
}
|
|
3027
3627
|
return matches;
|
|
3028
3628
|
}
|
|
3629
|
+
async forEachKeyMatchingPattern(pattern, visitor) {
|
|
3630
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
3631
|
+
let cursor = "0";
|
|
3632
|
+
do {
|
|
3633
|
+
const [nextCursor, keys] = await this.client.sscan(
|
|
3634
|
+
knownKeysKey,
|
|
3635
|
+
cursor,
|
|
3636
|
+
"MATCH",
|
|
3637
|
+
pattern,
|
|
3638
|
+
"COUNT",
|
|
3639
|
+
this.scanCount
|
|
3640
|
+
);
|
|
3641
|
+
cursor = nextCursor;
|
|
3642
|
+
for (const key of keys) {
|
|
3643
|
+
if (PatternMatcher.matches(pattern, key)) {
|
|
3644
|
+
await visitor(key);
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
} while (cursor !== "0");
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3029
3650
|
async clear() {
|
|
3030
3651
|
const indexKeys = await this.scanIndexKeys();
|
|
3031
3652
|
if (indexKeys.length === 0) {
|
|
@@ -3081,12 +3702,18 @@ function simpleHash(value) {
|
|
|
3081
3702
|
}
|
|
3082
3703
|
|
|
3083
3704
|
// src/http/createCacheStatsHandler.ts
|
|
3084
|
-
function createCacheStatsHandler(cache) {
|
|
3085
|
-
return async (
|
|
3086
|
-
response.statusCode = 200;
|
|
3705
|
+
function createCacheStatsHandler(cache, options = {}) {
|
|
3706
|
+
return async (request, response) => {
|
|
3087
3707
|
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
3088
3708
|
response.setHeader?.("cache-control", "no-store");
|
|
3089
3709
|
response.setHeader?.("x-content-type-options", "nosniff");
|
|
3710
|
+
const isAuthorized = options.allowPublicAccess === true || (options.authorize ? await options.authorize(request) : false);
|
|
3711
|
+
if (!isAuthorized) {
|
|
3712
|
+
response.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3713
|
+
response.end(JSON.stringify({ error: "Forbidden" }));
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
response.statusCode = 200;
|
|
3090
3717
|
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
3091
3718
|
};
|
|
3092
3719
|
}
|
|
@@ -3121,7 +3748,26 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
3121
3748
|
return async (fastify) => {
|
|
3122
3749
|
fastify.decorate("cache", cache);
|
|
3123
3750
|
if (options.exposeStatsRoute === true && fastify.get) {
|
|
3124
|
-
fastify.get(options.statsPath ?? "/cache/stats", async () =>
|
|
3751
|
+
fastify.get(options.statsPath ?? "/cache/stats", async (request, reply) => {
|
|
3752
|
+
const isAuthorized = options.allowPublicStatsRoute === true || (options.authorizeStatsRoute ? await options.authorizeStatsRoute(request) : false);
|
|
3753
|
+
reply.header?.("cache-control", "no-store");
|
|
3754
|
+
reply.header?.("x-content-type-options", "nosniff");
|
|
3755
|
+
if (!isAuthorized) {
|
|
3756
|
+
reply.statusCode = options.unauthorizedStatusCode ?? 403;
|
|
3757
|
+
const body2 = { error: "Forbidden" };
|
|
3758
|
+
if (reply.send) {
|
|
3759
|
+
reply.send(body2);
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
return body2;
|
|
3763
|
+
}
|
|
3764
|
+
const body = cache.getStats();
|
|
3765
|
+
if (reply.send) {
|
|
3766
|
+
reply.send(body);
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
return body;
|
|
3770
|
+
});
|
|
3125
3771
|
}
|
|
3126
3772
|
};
|
|
3127
3773
|
}
|
|
@@ -3136,6 +3782,10 @@ function createExpressCacheMiddleware(cache, options = {}) {
|
|
|
3136
3782
|
next();
|
|
3137
3783
|
return;
|
|
3138
3784
|
}
|
|
3785
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3786
|
+
next();
|
|
3787
|
+
return;
|
|
3788
|
+
}
|
|
3139
3789
|
const rawUrl = req.originalUrl ?? req.url ?? "/";
|
|
3140
3790
|
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${normalizeUrl(rawUrl)}`;
|
|
3141
3791
|
const cached = await cache.get(key, void 0, options);
|
|
@@ -3180,6 +3830,11 @@ function normalizeUrl(url) {
|
|
|
3180
3830
|
|
|
3181
3831
|
// src/integrations/graphql.ts
|
|
3182
3832
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
3833
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3834
|
+
throw new Error(
|
|
3835
|
+
"cacheGraphqlResolver requires a keyResolver or allowImplicitContextCaching=true because resolver output may depend on request context."
|
|
3836
|
+
);
|
|
3837
|
+
}
|
|
3183
3838
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
3184
3839
|
...options,
|
|
3185
3840
|
keyResolver: options.keyResolver
|
|
@@ -3196,14 +3851,17 @@ function createHonoCacheMiddleware(cache, options = {}) {
|
|
|
3196
3851
|
await next();
|
|
3197
3852
|
return;
|
|
3198
3853
|
}
|
|
3854
|
+
if (!options.keyResolver && options.allowPrivateCaching !== true) {
|
|
3855
|
+
await next();
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3199
3858
|
const rawPath = context.req.path ?? context.req.url ?? "/";
|
|
3200
3859
|
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${normalizeUrl2(rawPath)}`;
|
|
3201
3860
|
const cached = await cache.get(key, void 0, options);
|
|
3202
3861
|
if (cached !== null) {
|
|
3203
3862
|
context.header?.("x-cache", "HIT");
|
|
3204
3863
|
context.header?.("content-type", "application/json; charset=utf-8");
|
|
3205
|
-
context.json(cached);
|
|
3206
|
-
return;
|
|
3864
|
+
return context.json(cached);
|
|
3207
3865
|
}
|
|
3208
3866
|
const originalJson = context.json.bind(context);
|
|
3209
3867
|
context.json = (body, status) => {
|
|
@@ -3293,6 +3951,11 @@ function instrument(name, tracer, method, attributes) {
|
|
|
3293
3951
|
|
|
3294
3952
|
// src/integrations/trpc.ts
|
|
3295
3953
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
3954
|
+
if (!options.keyResolver && options.allowImplicitContextCaching !== true) {
|
|
3955
|
+
throw new Error(
|
|
3956
|
+
"createTrpcCacheMiddleware requires a keyResolver or allowImplicitContextCaching=true because procedure output may depend on request context."
|
|
3957
|
+
);
|
|
3958
|
+
}
|
|
3296
3959
|
return async (context) => {
|
|
3297
3960
|
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
3298
3961
|
let didFetch = false;
|
|
@@ -3432,6 +4095,12 @@ var MemoryLayer = class {
|
|
|
3432
4095
|
this.pruneExpired();
|
|
3433
4096
|
return [...this.entries.keys()];
|
|
3434
4097
|
}
|
|
4098
|
+
async forEachKey(visitor) {
|
|
4099
|
+
this.pruneExpired();
|
|
4100
|
+
for (const key of this.entries.keys()) {
|
|
4101
|
+
await visitor(key);
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
3435
4104
|
exportState() {
|
|
3436
4105
|
this.pruneExpired();
|
|
3437
4106
|
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
@@ -3499,13 +4168,12 @@ var MemoryLayer = class {
|
|
|
3499
4168
|
};
|
|
3500
4169
|
|
|
3501
4170
|
// src/layers/RedisLayer.ts
|
|
4171
|
+
var import_node_stream = require("stream");
|
|
3502
4172
|
var import_node_util = require("util");
|
|
3503
4173
|
var import_node_zlib = require("zlib");
|
|
3504
4174
|
var BATCH_DELETE_SIZE = 500;
|
|
3505
4175
|
var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
|
|
3506
|
-
var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
|
|
3507
4176
|
var brotliCompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliCompress);
|
|
3508
|
-
var brotliDecompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliDecompress);
|
|
3509
4177
|
var RedisLayer = class {
|
|
3510
4178
|
name;
|
|
3511
4179
|
defaultTtl;
|
|
@@ -3613,8 +4281,18 @@ var RedisLayer = class {
|
|
|
3613
4281
|
return remaining;
|
|
3614
4282
|
}
|
|
3615
4283
|
async size() {
|
|
3616
|
-
|
|
3617
|
-
|
|
4284
|
+
if (!this.prefix) {
|
|
4285
|
+
return this.client.dbsize();
|
|
4286
|
+
}
|
|
4287
|
+
const pattern = `${this.prefix}*`;
|
|
4288
|
+
let cursor = "0";
|
|
4289
|
+
let count = 0;
|
|
4290
|
+
do {
|
|
4291
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
4292
|
+
cursor = nextCursor;
|
|
4293
|
+
count += keys.length;
|
|
4294
|
+
} while (cursor !== "0");
|
|
4295
|
+
return count;
|
|
3618
4296
|
}
|
|
3619
4297
|
async ping() {
|
|
3620
4298
|
try {
|
|
@@ -3660,6 +4338,17 @@ var RedisLayer = class {
|
|
|
3660
4338
|
}
|
|
3661
4339
|
return keys.map((key) => key.slice(this.prefix.length));
|
|
3662
4340
|
}
|
|
4341
|
+
async forEachKey(visitor) {
|
|
4342
|
+
const pattern = `${this.prefix}*`;
|
|
4343
|
+
let cursor = "0";
|
|
4344
|
+
do {
|
|
4345
|
+
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
4346
|
+
cursor = nextCursor;
|
|
4347
|
+
for (const key of keys) {
|
|
4348
|
+
await visitor(this.prefix ? key.slice(this.prefix.length) : key);
|
|
4349
|
+
}
|
|
4350
|
+
} while (cursor !== "0");
|
|
4351
|
+
}
|
|
3663
4352
|
async scanKeys(pattern) {
|
|
3664
4353
|
const matches = [];
|
|
3665
4354
|
let cursor = "0";
|
|
@@ -3674,7 +4363,13 @@ var RedisLayer = class {
|
|
|
3674
4363
|
return `${this.prefix}${key}`;
|
|
3675
4364
|
}
|
|
3676
4365
|
async deserializeOrDelete(key, payload) {
|
|
3677
|
-
|
|
4366
|
+
let decodedPayload;
|
|
4367
|
+
try {
|
|
4368
|
+
decodedPayload = await this.decodePayload(payload);
|
|
4369
|
+
} catch {
|
|
4370
|
+
await this.deleteCorruptedKey(key);
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
3678
4373
|
for (const serializer of this.serializers) {
|
|
3679
4374
|
try {
|
|
3680
4375
|
const value = serializer.deserialize(decodedPayload);
|
|
@@ -3685,12 +4380,15 @@ var RedisLayer = class {
|
|
|
3685
4380
|
} catch {
|
|
3686
4381
|
}
|
|
3687
4382
|
}
|
|
4383
|
+
await this.deleteCorruptedKey(key);
|
|
4384
|
+
return null;
|
|
4385
|
+
}
|
|
4386
|
+
async deleteCorruptedKey(key) {
|
|
3688
4387
|
try {
|
|
3689
4388
|
await this.client.del(this.withPrefix(key));
|
|
3690
4389
|
} catch (deleteError) {
|
|
3691
4390
|
console.warn(`[layercache] RedisLayer: failed to delete corrupted key "${key}"`, deleteError);
|
|
3692
4391
|
}
|
|
3693
|
-
return null;
|
|
3694
4392
|
}
|
|
3695
4393
|
async rewriteWithPrimarySerializer(key, value) {
|
|
3696
4394
|
const serialized = this.primarySerializer().serialize(value);
|
|
@@ -3737,31 +4435,72 @@ var RedisLayer = class {
|
|
|
3737
4435
|
return payload;
|
|
3738
4436
|
}
|
|
3739
4437
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
3740
|
-
|
|
3741
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3742
|
-
throw new Error(
|
|
3743
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3744
|
-
);
|
|
3745
|
-
}
|
|
3746
|
-
return decompressed;
|
|
4438
|
+
return this.decompressWithLimit((0, import_node_zlib.createGunzip)(), payload.subarray(10));
|
|
3747
4439
|
}
|
|
3748
4440
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
3749
|
-
|
|
3750
|
-
if (decompressed.byteLength > this.decompressionMaxBytes) {
|
|
3751
|
-
throw new Error(
|
|
3752
|
-
`Decompressed payload (${decompressed.byteLength} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
3753
|
-
);
|
|
3754
|
-
}
|
|
3755
|
-
return decompressed;
|
|
4441
|
+
return this.decompressWithLimit((0, import_node_zlib.createBrotliDecompress)(), payload.subarray(12));
|
|
3756
4442
|
}
|
|
3757
4443
|
return payload;
|
|
3758
4444
|
}
|
|
4445
|
+
async decompressWithLimit(decompressor, payload) {
|
|
4446
|
+
return new Promise((resolve2, reject) => {
|
|
4447
|
+
const source = import_node_stream.Readable.from(payload);
|
|
4448
|
+
const chunks = [];
|
|
4449
|
+
let totalBytes = 0;
|
|
4450
|
+
let settled = false;
|
|
4451
|
+
const cleanup = () => {
|
|
4452
|
+
decompressor.removeAllListeners();
|
|
4453
|
+
};
|
|
4454
|
+
const fail = (error) => {
|
|
4455
|
+
if (settled) {
|
|
4456
|
+
return;
|
|
4457
|
+
}
|
|
4458
|
+
settled = true;
|
|
4459
|
+
cleanup();
|
|
4460
|
+
source.unpipe(decompressor);
|
|
4461
|
+
source.destroy();
|
|
4462
|
+
decompressor.destroy();
|
|
4463
|
+
reject(error);
|
|
4464
|
+
};
|
|
4465
|
+
decompressor.on("data", (chunk) => {
|
|
4466
|
+
const normalized = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4467
|
+
totalBytes += normalized.byteLength;
|
|
4468
|
+
if (totalBytes > this.decompressionMaxBytes) {
|
|
4469
|
+
fail(
|
|
4470
|
+
new Error(
|
|
4471
|
+
`Decompressed payload (${totalBytes} bytes) exceeds decompressionMaxBytes limit (${this.decompressionMaxBytes} bytes).`
|
|
4472
|
+
)
|
|
4473
|
+
);
|
|
4474
|
+
return;
|
|
4475
|
+
}
|
|
4476
|
+
chunks.push(normalized);
|
|
4477
|
+
});
|
|
4478
|
+
decompressor.once("error", (error) => {
|
|
4479
|
+
if (settled) {
|
|
4480
|
+
return;
|
|
4481
|
+
}
|
|
4482
|
+
settled = true;
|
|
4483
|
+
cleanup();
|
|
4484
|
+
reject(error);
|
|
4485
|
+
});
|
|
4486
|
+
decompressor.once("end", () => {
|
|
4487
|
+
if (settled) {
|
|
4488
|
+
return;
|
|
4489
|
+
}
|
|
4490
|
+
settled = true;
|
|
4491
|
+
cleanup();
|
|
4492
|
+
resolve2(Buffer.concat(chunks));
|
|
4493
|
+
});
|
|
4494
|
+
source.pipe(decompressor);
|
|
4495
|
+
});
|
|
4496
|
+
}
|
|
3759
4497
|
};
|
|
3760
4498
|
|
|
3761
4499
|
// src/layers/DiskLayer.ts
|
|
3762
4500
|
var import_node_crypto = require("crypto");
|
|
3763
4501
|
var import_node_fs = require("fs");
|
|
3764
4502
|
var import_node_path = require("path");
|
|
4503
|
+
var FILE_SCAN_CONCURRENCY = 32;
|
|
3765
4504
|
var DiskLayer = class {
|
|
3766
4505
|
name;
|
|
3767
4506
|
defaultTtl;
|
|
@@ -3769,6 +4508,7 @@ var DiskLayer = class {
|
|
|
3769
4508
|
directory;
|
|
3770
4509
|
serializer;
|
|
3771
4510
|
maxFiles;
|
|
4511
|
+
maxEntryBytes;
|
|
3772
4512
|
writeQueue = Promise.resolve();
|
|
3773
4513
|
constructor(options) {
|
|
3774
4514
|
this.directory = this.resolveDirectory(options.directory);
|
|
@@ -3776,16 +4516,15 @@ var DiskLayer = class {
|
|
|
3776
4516
|
this.name = options.name ?? "disk";
|
|
3777
4517
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
3778
4518
|
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
4519
|
+
this.maxEntryBytes = this.normalizeMaxEntryBytes(options.maxEntryBytes);
|
|
3779
4520
|
}
|
|
3780
4521
|
async get(key) {
|
|
3781
4522
|
return unwrapStoredValue(await this.getEntry(key));
|
|
3782
4523
|
}
|
|
3783
4524
|
async getEntry(key) {
|
|
3784
4525
|
const filePath = this.keyToPath(key);
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3788
|
-
} catch {
|
|
4526
|
+
const raw = await this.readEntryFile(filePath);
|
|
4527
|
+
if (raw === null) {
|
|
3789
4528
|
return null;
|
|
3790
4529
|
}
|
|
3791
4530
|
let entry;
|
|
@@ -3836,10 +4575,8 @@ var DiskLayer = class {
|
|
|
3836
4575
|
}
|
|
3837
4576
|
async ttl(key) {
|
|
3838
4577
|
const filePath = this.keyToPath(key);
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3842
|
-
} catch {
|
|
4578
|
+
const raw = await this.readEntryFile(filePath);
|
|
4579
|
+
if (raw === null) {
|
|
3843
4580
|
return null;
|
|
3844
4581
|
}
|
|
3845
4582
|
let entry;
|
|
@@ -3863,7 +4600,7 @@ var DiskLayer = class {
|
|
|
3863
4600
|
}
|
|
3864
4601
|
async deleteMany(keys) {
|
|
3865
4602
|
await this.enqueueWrite(async () => {
|
|
3866
|
-
await
|
|
4603
|
+
await this.deletePathsWithConcurrency(keys.map((key) => this.keyToPath(key)));
|
|
3867
4604
|
});
|
|
3868
4605
|
}
|
|
3869
4606
|
async clear() {
|
|
@@ -3874,8 +4611,8 @@ var DiskLayer = class {
|
|
|
3874
4611
|
} catch {
|
|
3875
4612
|
return;
|
|
3876
4613
|
}
|
|
3877
|
-
await
|
|
3878
|
-
entries.filter((name) => name.endsWith(".lc")).map((name) =>
|
|
4614
|
+
await this.deletePathsWithConcurrency(
|
|
4615
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => (0, import_node_path.join)(this.directory, name))
|
|
3879
4616
|
);
|
|
3880
4617
|
});
|
|
3881
4618
|
}
|
|
@@ -3884,42 +4621,23 @@ var DiskLayer = class {
|
|
|
3884
4621
|
* Expired entries are skipped and cleaned up during the scan.
|
|
3885
4622
|
*/
|
|
3886
4623
|
async keys() {
|
|
3887
|
-
let entries;
|
|
3888
|
-
try {
|
|
3889
|
-
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3890
|
-
} catch {
|
|
3891
|
-
return [];
|
|
3892
|
-
}
|
|
3893
|
-
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3894
4624
|
const keys = [];
|
|
3895
|
-
await
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
let raw;
|
|
3899
|
-
try {
|
|
3900
|
-
raw = await import_node_fs.promises.readFile(filePath);
|
|
3901
|
-
} catch {
|
|
3902
|
-
return;
|
|
3903
|
-
}
|
|
3904
|
-
let entry;
|
|
3905
|
-
try {
|
|
3906
|
-
entry = this.deserializeEntry(raw);
|
|
3907
|
-
} catch {
|
|
3908
|
-
await this.safeDelete(filePath);
|
|
3909
|
-
return;
|
|
3910
|
-
}
|
|
3911
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3912
|
-
await this.safeDelete(filePath);
|
|
3913
|
-
return;
|
|
3914
|
-
}
|
|
3915
|
-
keys.push(entry.key);
|
|
3916
|
-
})
|
|
3917
|
-
);
|
|
4625
|
+
await this.scanEntries(async (entry) => {
|
|
4626
|
+
keys.push(entry.key);
|
|
4627
|
+
});
|
|
3918
4628
|
return keys;
|
|
3919
4629
|
}
|
|
4630
|
+
async forEachKey(visitor) {
|
|
4631
|
+
await this.scanEntries(async (entry) => {
|
|
4632
|
+
await visitor(entry.key);
|
|
4633
|
+
});
|
|
4634
|
+
}
|
|
3920
4635
|
async size() {
|
|
3921
|
-
|
|
3922
|
-
|
|
4636
|
+
let count = 0;
|
|
4637
|
+
await this.scanEntries(async () => {
|
|
4638
|
+
count += 1;
|
|
4639
|
+
});
|
|
4640
|
+
return count;
|
|
3923
4641
|
}
|
|
3924
4642
|
async ping() {
|
|
3925
4643
|
try {
|
|
@@ -3953,6 +4671,113 @@ var DiskLayer = class {
|
|
|
3953
4671
|
}
|
|
3954
4672
|
return maxFiles;
|
|
3955
4673
|
}
|
|
4674
|
+
normalizeMaxEntryBytes(maxEntryBytes) {
|
|
4675
|
+
if (maxEntryBytes === false) {
|
|
4676
|
+
return false;
|
|
4677
|
+
}
|
|
4678
|
+
const normalized = maxEntryBytes ?? 16 * 1024 * 1024;
|
|
4679
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
4680
|
+
throw new Error("DiskLayer.maxEntryBytes must be a positive number or false.");
|
|
4681
|
+
}
|
|
4682
|
+
return normalized;
|
|
4683
|
+
}
|
|
4684
|
+
async readEntryFile(filePath) {
|
|
4685
|
+
let handle;
|
|
4686
|
+
try {
|
|
4687
|
+
handle = await import_node_fs.promises.open(filePath, "r");
|
|
4688
|
+
return await this.readHandleWithLimit(handle);
|
|
4689
|
+
} catch {
|
|
4690
|
+
await this.safeDelete(filePath);
|
|
4691
|
+
return null;
|
|
4692
|
+
} finally {
|
|
4693
|
+
await handle?.close().catch(() => void 0);
|
|
4694
|
+
}
|
|
4695
|
+
}
|
|
4696
|
+
async readHandleWithLimit(handle) {
|
|
4697
|
+
if (this.maxEntryBytes === false) {
|
|
4698
|
+
return handle.readFile();
|
|
4699
|
+
}
|
|
4700
|
+
const stat = await handle.stat();
|
|
4701
|
+
if (stat.size > this.maxEntryBytes) {
|
|
4702
|
+
throw new Error(`DiskLayer entry exceeds maxEntryBytes limit (${stat.size} bytes > ${this.maxEntryBytes} bytes).`);
|
|
4703
|
+
}
|
|
4704
|
+
const chunks = [];
|
|
4705
|
+
let totalBytes = 0;
|
|
4706
|
+
let position = 0;
|
|
4707
|
+
while (true) {
|
|
4708
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, this.maxEntryBytes - totalBytes + 1));
|
|
4709
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, position);
|
|
4710
|
+
if (bytesRead === 0) {
|
|
4711
|
+
break;
|
|
4712
|
+
}
|
|
4713
|
+
totalBytes += bytesRead;
|
|
4714
|
+
if (totalBytes > this.maxEntryBytes) {
|
|
4715
|
+
throw new Error(
|
|
4716
|
+
`DiskLayer entry exceeds maxEntryBytes limit (${totalBytes} bytes > ${this.maxEntryBytes} bytes).`
|
|
4717
|
+
);
|
|
4718
|
+
}
|
|
4719
|
+
chunks.push(buffer.subarray(0, bytesRead));
|
|
4720
|
+
position += bytesRead;
|
|
4721
|
+
}
|
|
4722
|
+
return Buffer.concat(chunks);
|
|
4723
|
+
}
|
|
4724
|
+
async scanEntries(visitor) {
|
|
4725
|
+
let entries;
|
|
4726
|
+
try {
|
|
4727
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
4728
|
+
} catch {
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
4732
|
+
let nextIndex = 0;
|
|
4733
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, lcFiles.length);
|
|
4734
|
+
await Promise.all(
|
|
4735
|
+
Array.from({ length: workerCount }, async () => {
|
|
4736
|
+
while (true) {
|
|
4737
|
+
const currentIndex = nextIndex;
|
|
4738
|
+
nextIndex += 1;
|
|
4739
|
+
const name = lcFiles[currentIndex];
|
|
4740
|
+
if (name === void 0) {
|
|
4741
|
+
return;
|
|
4742
|
+
}
|
|
4743
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
4744
|
+
const raw = await this.readEntryFile(filePath);
|
|
4745
|
+
if (raw === null) {
|
|
4746
|
+
continue;
|
|
4747
|
+
}
|
|
4748
|
+
let entry;
|
|
4749
|
+
try {
|
|
4750
|
+
entry = this.deserializeEntry(raw);
|
|
4751
|
+
} catch {
|
|
4752
|
+
await this.safeDelete(filePath);
|
|
4753
|
+
continue;
|
|
4754
|
+
}
|
|
4755
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
4756
|
+
await this.safeDelete(filePath);
|
|
4757
|
+
continue;
|
|
4758
|
+
}
|
|
4759
|
+
await visitor(entry);
|
|
4760
|
+
}
|
|
4761
|
+
})
|
|
4762
|
+
);
|
|
4763
|
+
}
|
|
4764
|
+
async deletePathsWithConcurrency(paths) {
|
|
4765
|
+
let nextIndex = 0;
|
|
4766
|
+
const workerCount = Math.min(FILE_SCAN_CONCURRENCY, paths.length);
|
|
4767
|
+
await Promise.all(
|
|
4768
|
+
Array.from({ length: workerCount }, async () => {
|
|
4769
|
+
while (true) {
|
|
4770
|
+
const currentIndex = nextIndex;
|
|
4771
|
+
nextIndex += 1;
|
|
4772
|
+
const filePath = paths[currentIndex];
|
|
4773
|
+
if (filePath === void 0) {
|
|
4774
|
+
return;
|
|
4775
|
+
}
|
|
4776
|
+
await this.safeDelete(filePath);
|
|
4777
|
+
}
|
|
4778
|
+
})
|
|
4779
|
+
);
|
|
4780
|
+
}
|
|
3956
4781
|
deserializeEntry(raw) {
|
|
3957
4782
|
const entry = this.serializer.deserialize(raw);
|
|
3958
4783
|
if (!isDiskEntry(entry)) {
|
|
@@ -4089,18 +4914,27 @@ var MemcachedLayer = class {
|
|
|
4089
4914
|
// src/serialization/MsgpackSerializer.ts
|
|
4090
4915
|
var import_msgpack = require("@msgpack/msgpack");
|
|
4091
4916
|
var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
4917
|
+
var MAX_SANITIZE_DEPTH3 = 64;
|
|
4918
|
+
var MAX_SANITIZE_NODES3 = 1e4;
|
|
4092
4919
|
var MsgpackSerializer = class {
|
|
4093
4920
|
serialize(value) {
|
|
4094
4921
|
return Buffer.from((0, import_msgpack.encode)(value));
|
|
4095
4922
|
}
|
|
4096
4923
|
deserialize(payload) {
|
|
4097
|
-
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
4098
|
-
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized));
|
|
4924
|
+
const normalized = Buffer.isBuffer(payload) ? payload : Buffer.from(payload, "latin1");
|
|
4925
|
+
return sanitizeMsgpackValue((0, import_msgpack.decode)(normalized), 0, { count: 0 });
|
|
4099
4926
|
}
|
|
4100
4927
|
};
|
|
4101
|
-
function sanitizeMsgpackValue(value) {
|
|
4928
|
+
function sanitizeMsgpackValue(value, depth, state) {
|
|
4929
|
+
state.count += 1;
|
|
4930
|
+
if (state.count > MAX_SANITIZE_NODES3) {
|
|
4931
|
+
throw new Error(`MessagePack payload exceeds max node count of ${MAX_SANITIZE_NODES3}.`);
|
|
4932
|
+
}
|
|
4933
|
+
if (depth > MAX_SANITIZE_DEPTH3) {
|
|
4934
|
+
throw new Error(`MessagePack payload exceeds max depth of ${MAX_SANITIZE_DEPTH3}.`);
|
|
4935
|
+
}
|
|
4102
4936
|
if (Array.isArray(value)) {
|
|
4103
|
-
return value.map((entry) => sanitizeMsgpackValue(entry));
|
|
4937
|
+
return value.map((entry) => sanitizeMsgpackValue(entry, depth + 1, state));
|
|
4104
4938
|
}
|
|
4105
4939
|
if (!isPlainObject2(value)) {
|
|
4106
4940
|
return value;
|
|
@@ -4110,7 +4944,7 @@ function sanitizeMsgpackValue(value) {
|
|
|
4110
4944
|
if (DANGEROUS_KEYS2.has(key)) {
|
|
4111
4945
|
continue;
|
|
4112
4946
|
}
|
|
4113
|
-
sanitized[key] = sanitizeMsgpackValue(entry);
|
|
4947
|
+
sanitized[key] = sanitizeMsgpackValue(entry, depth + 1, state);
|
|
4114
4948
|
}
|
|
4115
4949
|
return sanitized;
|
|
4116
4950
|
}
|