layercache 1.2.0 → 1.2.2
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/README.md +110 -8
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-IXCMHVHP.js} +62 -56
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +170 -39
- package/dist/cli.js +57 -2
- package/dist/edge-DLpdQN0W.d.cts +672 -0
- package/dist/edge-DLpdQN0W.d.ts +672 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +1173 -221
- package/dist/index.d.cts +51 -568
- package/dist/index.d.ts +51 -568
- package/dist/index.js +1005 -505
- package/package.json +8 -3
- package/packages/nestjs/dist/index.cjs +980 -370
- package/packages/nestjs/dist/index.d.cts +80 -0
- package/packages/nestjs/dist/index.d.ts +80 -0
- package/packages/nestjs/dist/index.js +968 -368
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -40,17 +50,18 @@ __export(index_exports, {
|
|
|
40
50
|
createCachedMethodDecorator: () => createCachedMethodDecorator,
|
|
41
51
|
createExpressCacheMiddleware: () => createExpressCacheMiddleware,
|
|
42
52
|
createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
|
|
53
|
+
createHonoCacheMiddleware: () => createHonoCacheMiddleware,
|
|
54
|
+
createOpenTelemetryPlugin: () => createOpenTelemetryPlugin,
|
|
43
55
|
createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
|
|
44
56
|
createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
|
|
45
57
|
});
|
|
46
58
|
module.exports = __toCommonJS(index_exports);
|
|
47
59
|
|
|
48
60
|
// src/CacheStack.ts
|
|
49
|
-
var import_node_crypto = require("crypto");
|
|
50
61
|
var import_node_events = require("events");
|
|
51
|
-
var import_node_fs = require("fs");
|
|
52
62
|
|
|
53
63
|
// src/CacheNamespace.ts
|
|
64
|
+
var import_async_mutex = require("async-mutex");
|
|
54
65
|
var CacheNamespace = class _CacheNamespace {
|
|
55
66
|
constructor(cache, prefix) {
|
|
56
67
|
this.cache = cache;
|
|
@@ -58,57 +69,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
58
69
|
}
|
|
59
70
|
cache;
|
|
60
71
|
prefix;
|
|
72
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
metrics = emptyMetrics();
|
|
61
74
|
async get(key, fetcher, options) {
|
|
62
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
75
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
63
76
|
}
|
|
64
77
|
async getOrSet(key, fetcher, options) {
|
|
65
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
78
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
66
79
|
}
|
|
67
80
|
/**
|
|
68
81
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
69
82
|
*/
|
|
70
83
|
async getOrThrow(key, fetcher, options) {
|
|
71
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
84
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
72
85
|
}
|
|
73
86
|
async has(key) {
|
|
74
|
-
return this.cache.has(this.qualify(key));
|
|
87
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
75
88
|
}
|
|
76
89
|
async ttl(key) {
|
|
77
|
-
return this.cache.ttl(this.qualify(key));
|
|
90
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
78
91
|
}
|
|
79
92
|
async set(key, value, options) {
|
|
80
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
93
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
81
94
|
}
|
|
82
95
|
async delete(key) {
|
|
83
|
-
await this.cache.delete(this.qualify(key));
|
|
96
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
84
97
|
}
|
|
85
98
|
async mdelete(keys) {
|
|
86
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
99
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
87
100
|
}
|
|
88
101
|
async clear() {
|
|
89
|
-
await this.cache.
|
|
102
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
90
103
|
}
|
|
91
104
|
async mget(entries) {
|
|
92
|
-
return this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
105
|
+
return this.trackMetrics(
|
|
106
|
+
() => this.cache.mget(
|
|
107
|
+
entries.map((entry) => ({
|
|
108
|
+
...entry,
|
|
109
|
+
key: this.qualify(entry.key)
|
|
110
|
+
}))
|
|
111
|
+
)
|
|
97
112
|
);
|
|
98
113
|
}
|
|
99
114
|
async mset(entries) {
|
|
100
|
-
await this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
await this.trackMetrics(
|
|
116
|
+
() => this.cache.mset(
|
|
117
|
+
entries.map((entry) => ({
|
|
118
|
+
...entry,
|
|
119
|
+
key: this.qualify(entry.key)
|
|
120
|
+
}))
|
|
121
|
+
)
|
|
105
122
|
);
|
|
106
123
|
}
|
|
107
124
|
async invalidateByTag(tag) {
|
|
108
|
-
await this.cache.invalidateByTag(tag);
|
|
125
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
126
|
+
}
|
|
127
|
+
async invalidateByTags(tags, mode = "any") {
|
|
128
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
109
129
|
}
|
|
110
130
|
async invalidateByPattern(pattern) {
|
|
111
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
131
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
132
|
+
}
|
|
133
|
+
async invalidateByPrefix(prefix) {
|
|
134
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
112
135
|
}
|
|
113
136
|
/**
|
|
114
137
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -129,10 +152,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
129
152
|
);
|
|
130
153
|
}
|
|
131
154
|
getMetrics() {
|
|
132
|
-
return this.
|
|
155
|
+
return cloneMetrics(this.metrics);
|
|
133
156
|
}
|
|
134
157
|
getHitRate() {
|
|
135
|
-
|
|
158
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
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 };
|
|
136
168
|
}
|
|
137
169
|
/**
|
|
138
170
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -149,7 +181,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
149
181
|
qualify(key) {
|
|
150
182
|
return `${this.prefix}:${key}`;
|
|
151
183
|
}
|
|
184
|
+
async trackMetrics(operation) {
|
|
185
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
186
|
+
const before = this.cache.getMetrics();
|
|
187
|
+
const result = await operation();
|
|
188
|
+
const after = this.cache.getMetrics();
|
|
189
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
190
|
+
return result;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
getMetricsMutex() {
|
|
194
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return existing;
|
|
197
|
+
}
|
|
198
|
+
const mutex = new import_async_mutex.Mutex();
|
|
199
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
200
|
+
return mutex;
|
|
201
|
+
}
|
|
152
202
|
};
|
|
203
|
+
function emptyMetrics() {
|
|
204
|
+
return {
|
|
205
|
+
hits: 0,
|
|
206
|
+
misses: 0,
|
|
207
|
+
fetches: 0,
|
|
208
|
+
sets: 0,
|
|
209
|
+
deletes: 0,
|
|
210
|
+
backfills: 0,
|
|
211
|
+
invalidations: 0,
|
|
212
|
+
staleHits: 0,
|
|
213
|
+
refreshes: 0,
|
|
214
|
+
refreshErrors: 0,
|
|
215
|
+
writeFailures: 0,
|
|
216
|
+
singleFlightWaits: 0,
|
|
217
|
+
negativeCacheHits: 0,
|
|
218
|
+
circuitBreakerTrips: 0,
|
|
219
|
+
degradedOperations: 0,
|
|
220
|
+
hitsByLayer: {},
|
|
221
|
+
missesByLayer: {},
|
|
222
|
+
latencyByLayer: {},
|
|
223
|
+
resetAt: Date.now()
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function cloneMetrics(metrics) {
|
|
227
|
+
return {
|
|
228
|
+
...metrics,
|
|
229
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
230
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
231
|
+
latencyByLayer: Object.fromEntries(
|
|
232
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
233
|
+
)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function diffMetrics(before, after) {
|
|
237
|
+
const latencyByLayer = Object.fromEntries(
|
|
238
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
239
|
+
layer,
|
|
240
|
+
{
|
|
241
|
+
avgMs: value.avgMs,
|
|
242
|
+
maxMs: value.maxMs,
|
|
243
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
244
|
+
}
|
|
245
|
+
])
|
|
246
|
+
);
|
|
247
|
+
return {
|
|
248
|
+
hits: after.hits - before.hits,
|
|
249
|
+
misses: after.misses - before.misses,
|
|
250
|
+
fetches: after.fetches - before.fetches,
|
|
251
|
+
sets: after.sets - before.sets,
|
|
252
|
+
deletes: after.deletes - before.deletes,
|
|
253
|
+
backfills: after.backfills - before.backfills,
|
|
254
|
+
invalidations: after.invalidations - before.invalidations,
|
|
255
|
+
staleHits: after.staleHits - before.staleHits,
|
|
256
|
+
refreshes: after.refreshes - before.refreshes,
|
|
257
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
258
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
259
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
260
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
261
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
262
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
263
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
264
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
265
|
+
latencyByLayer,
|
|
266
|
+
resetAt: after.resetAt
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function addMetrics(base, delta) {
|
|
270
|
+
return {
|
|
271
|
+
hits: base.hits + delta.hits,
|
|
272
|
+
misses: base.misses + delta.misses,
|
|
273
|
+
fetches: base.fetches + delta.fetches,
|
|
274
|
+
sets: base.sets + delta.sets,
|
|
275
|
+
deletes: base.deletes + delta.deletes,
|
|
276
|
+
backfills: base.backfills + delta.backfills,
|
|
277
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
278
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
279
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
280
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
281
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
282
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
283
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
284
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
285
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
286
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
287
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
288
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
289
|
+
resetAt: base.resetAt
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function diffMap(before, after) {
|
|
293
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
294
|
+
const result = {};
|
|
295
|
+
for (const key of keys) {
|
|
296
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
297
|
+
}
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
function addMap(base, delta) {
|
|
301
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
302
|
+
const result = {};
|
|
303
|
+
for (const key of keys) {
|
|
304
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
305
|
+
}
|
|
306
|
+
return result;
|
|
307
|
+
}
|
|
153
308
|
|
|
154
309
|
// src/internal/CircuitBreakerManager.ts
|
|
155
310
|
var CircuitBreakerManager = class {
|
|
@@ -243,6 +398,148 @@ var CircuitBreakerManager = class {
|
|
|
243
398
|
}
|
|
244
399
|
};
|
|
245
400
|
|
|
401
|
+
// src/internal/FetchRateLimiter.ts
|
|
402
|
+
var FetchRateLimiter = class {
|
|
403
|
+
queue = [];
|
|
404
|
+
buckets = /* @__PURE__ */ new Map();
|
|
405
|
+
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
406
|
+
nextFetcherBucketId = 0;
|
|
407
|
+
drainTimer;
|
|
408
|
+
async schedule(options, context, task) {
|
|
409
|
+
if (!options) {
|
|
410
|
+
return task();
|
|
411
|
+
}
|
|
412
|
+
const normalized = this.normalize(options);
|
|
413
|
+
if (!normalized) {
|
|
414
|
+
return task();
|
|
415
|
+
}
|
|
416
|
+
return new Promise((resolve2, reject) => {
|
|
417
|
+
this.queue.push({
|
|
418
|
+
bucketKey: this.resolveBucketKey(normalized, context),
|
|
419
|
+
options: normalized,
|
|
420
|
+
task,
|
|
421
|
+
resolve: resolve2,
|
|
422
|
+
reject
|
|
423
|
+
});
|
|
424
|
+
this.drain();
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
normalize(options) {
|
|
428
|
+
const maxConcurrent = options.maxConcurrent;
|
|
429
|
+
const intervalMs = options.intervalMs;
|
|
430
|
+
const maxPerInterval = options.maxPerInterval;
|
|
431
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
maxConcurrent,
|
|
436
|
+
intervalMs,
|
|
437
|
+
maxPerInterval,
|
|
438
|
+
scope: options.scope ?? "global",
|
|
439
|
+
bucketKey: options.bucketKey
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
resolveBucketKey(options, context) {
|
|
443
|
+
if (options.bucketKey) {
|
|
444
|
+
return `custom:${options.bucketKey}`;
|
|
445
|
+
}
|
|
446
|
+
if (options.scope === "key") {
|
|
447
|
+
return `key:${context.key}`;
|
|
448
|
+
}
|
|
449
|
+
if (options.scope === "fetcher") {
|
|
450
|
+
const existing = this.fetcherBuckets.get(context.fetcher);
|
|
451
|
+
if (existing) {
|
|
452
|
+
return existing;
|
|
453
|
+
}
|
|
454
|
+
const bucket = `fetcher:${this.nextFetcherBucketId}`;
|
|
455
|
+
this.nextFetcherBucketId += 1;
|
|
456
|
+
this.fetcherBuckets.set(context.fetcher, bucket);
|
|
457
|
+
return bucket;
|
|
458
|
+
}
|
|
459
|
+
return "global";
|
|
460
|
+
}
|
|
461
|
+
drain() {
|
|
462
|
+
if (this.drainTimer) {
|
|
463
|
+
clearTimeout(this.drainTimer);
|
|
464
|
+
this.drainTimer = void 0;
|
|
465
|
+
}
|
|
466
|
+
while (this.queue.length > 0) {
|
|
467
|
+
let nextIndex = -1;
|
|
468
|
+
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
469
|
+
for (let index = 0; index < this.queue.length; index += 1) {
|
|
470
|
+
const next2 = this.queue[index];
|
|
471
|
+
if (!next2) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const waitMs = this.waitTime(next2.bucketKey, next2.options);
|
|
475
|
+
if (waitMs <= 0) {
|
|
476
|
+
nextIndex = index;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
480
|
+
}
|
|
481
|
+
if (nextIndex < 0) {
|
|
482
|
+
if (Number.isFinite(nextWaitMs)) {
|
|
483
|
+
this.drainTimer = setTimeout(() => {
|
|
484
|
+
this.drainTimer = void 0;
|
|
485
|
+
this.drain();
|
|
486
|
+
}, nextWaitMs);
|
|
487
|
+
this.drainTimer.unref?.();
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const next = this.queue.splice(nextIndex, 1)[0];
|
|
492
|
+
if (!next) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const bucket = this.bucketState(next.bucketKey);
|
|
496
|
+
bucket.active += 1;
|
|
497
|
+
bucket.startedAt.push(Date.now());
|
|
498
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
499
|
+
bucket.active -= 1;
|
|
500
|
+
this.drain();
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
waitTime(bucketKey, options) {
|
|
505
|
+
const bucket = this.bucketState(bucketKey);
|
|
506
|
+
const now = Date.now();
|
|
507
|
+
if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
this.prune(bucket, now, options.intervalMs);
|
|
514
|
+
if (bucket.startedAt.length < options.maxPerInterval) {
|
|
515
|
+
return 0;
|
|
516
|
+
}
|
|
517
|
+
const oldest = bucket.startedAt[0];
|
|
518
|
+
if (!oldest) {
|
|
519
|
+
return 0;
|
|
520
|
+
}
|
|
521
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
522
|
+
}
|
|
523
|
+
prune(bucket, now, intervalMs) {
|
|
524
|
+
while (bucket.startedAt.length > 0) {
|
|
525
|
+
const startedAt = bucket.startedAt[0];
|
|
526
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
bucket.startedAt.shift();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
bucketState(bucketKey) {
|
|
533
|
+
const existing = this.buckets.get(bucketKey);
|
|
534
|
+
if (existing) {
|
|
535
|
+
return existing;
|
|
536
|
+
}
|
|
537
|
+
const bucket = { active: 0, startedAt: [] };
|
|
538
|
+
this.buckets.set(bucketKey, bucket);
|
|
539
|
+
return bucket;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
246
543
|
// src/internal/MetricsCollector.ts
|
|
247
544
|
var MetricsCollector = class {
|
|
248
545
|
data = this.empty();
|
|
@@ -439,13 +736,14 @@ var TtlResolver = class {
|
|
|
439
736
|
clearProfiles() {
|
|
440
737
|
this.accessProfiles.clear();
|
|
441
738
|
}
|
|
442
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
739
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
740
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
443
741
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
444
742
|
layerName,
|
|
445
743
|
options?.negativeTtl,
|
|
446
744
|
globalNegativeTtl,
|
|
447
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
448
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
745
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
746
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
449
747
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
450
748
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
451
749
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -484,6 +782,29 @@ var TtlResolver = class {
|
|
|
484
782
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
485
783
|
return Math.max(1, Math.round(ttl + delta));
|
|
486
784
|
}
|
|
785
|
+
resolvePolicyTtl(key, value, policy) {
|
|
786
|
+
if (!policy) {
|
|
787
|
+
return void 0;
|
|
788
|
+
}
|
|
789
|
+
if (typeof policy === "function") {
|
|
790
|
+
return policy({ key, value });
|
|
791
|
+
}
|
|
792
|
+
const now = /* @__PURE__ */ new Date();
|
|
793
|
+
if (policy === "until-midnight") {
|
|
794
|
+
const nextMidnight = new Date(now);
|
|
795
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
796
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
797
|
+
}
|
|
798
|
+
if (policy === "next-hour") {
|
|
799
|
+
const nextHour = new Date(now);
|
|
800
|
+
nextHour.setMinutes(60, 0, 0);
|
|
801
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
802
|
+
}
|
|
803
|
+
const alignToSeconds = policy.alignTo;
|
|
804
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
805
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
806
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
807
|
+
}
|
|
487
808
|
readLayerNumber(layerName, value) {
|
|
488
809
|
if (typeof value === "number") {
|
|
489
810
|
return value;
|
|
@@ -511,36 +832,46 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
511
832
|
/**
|
|
512
833
|
* Tests whether a glob-style pattern matches a value.
|
|
513
834
|
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
514
|
-
* Uses a
|
|
835
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
836
|
+
* quadratic memory usage on long patterns/keys.
|
|
515
837
|
*/
|
|
516
838
|
static matches(pattern, value) {
|
|
517
839
|
return _PatternMatcher.matchLinear(pattern, value);
|
|
518
840
|
}
|
|
519
841
|
/**
|
|
520
|
-
* Linear-time glob matching
|
|
521
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
842
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
522
843
|
*/
|
|
523
844
|
static matchLinear(pattern, value) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
if (pc === "*") {
|
|
537
|
-
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
538
|
-
} else if (pc === "?" || pc === value[j - 1]) {
|
|
539
|
-
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
540
|
-
}
|
|
845
|
+
let patternIndex = 0;
|
|
846
|
+
let valueIndex = 0;
|
|
847
|
+
let starIndex = -1;
|
|
848
|
+
let backtrackValueIndex = 0;
|
|
849
|
+
while (valueIndex < value.length) {
|
|
850
|
+
const patternChar = pattern[patternIndex];
|
|
851
|
+
const valueChar = value[valueIndex];
|
|
852
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
853
|
+
starIndex = patternIndex;
|
|
854
|
+
patternIndex += 1;
|
|
855
|
+
backtrackValueIndex = valueIndex;
|
|
856
|
+
continue;
|
|
541
857
|
}
|
|
858
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
859
|
+
patternIndex += 1;
|
|
860
|
+
valueIndex += 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
if (starIndex !== -1) {
|
|
864
|
+
patternIndex = starIndex + 1;
|
|
865
|
+
backtrackValueIndex += 1;
|
|
866
|
+
valueIndex = backtrackValueIndex;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
return false;
|
|
542
870
|
}
|
|
543
|
-
|
|
871
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
872
|
+
patternIndex += 1;
|
|
873
|
+
}
|
|
874
|
+
return patternIndex === pattern.length;
|
|
544
875
|
}
|
|
545
876
|
};
|
|
546
877
|
|
|
@@ -578,26 +909,14 @@ var TagIndex = class {
|
|
|
578
909
|
}
|
|
579
910
|
}
|
|
580
911
|
async remove(key) {
|
|
581
|
-
this.
|
|
582
|
-
const tags = this.keyToTags.get(key);
|
|
583
|
-
if (!tags) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
for (const tag of tags) {
|
|
587
|
-
const keys = this.tagToKeys.get(tag);
|
|
588
|
-
if (!keys) {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
keys.delete(key);
|
|
592
|
-
if (keys.size === 0) {
|
|
593
|
-
this.tagToKeys.delete(tag);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
this.keyToTags.delete(key);
|
|
912
|
+
this.removeKey(key);
|
|
597
913
|
}
|
|
598
914
|
async keysForTag(tag) {
|
|
599
915
|
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
600
916
|
}
|
|
917
|
+
async keysForPrefix(prefix) {
|
|
918
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
919
|
+
}
|
|
601
920
|
async tagsForKey(key) {
|
|
602
921
|
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
603
922
|
}
|
|
@@ -619,15 +938,32 @@ var TagIndex = class {
|
|
|
619
938
|
if (removed >= toRemove) {
|
|
620
939
|
break;
|
|
621
940
|
}
|
|
622
|
-
this.
|
|
623
|
-
this.keyToTags.delete(key);
|
|
941
|
+
this.removeKey(key);
|
|
624
942
|
removed += 1;
|
|
625
943
|
}
|
|
626
944
|
}
|
|
945
|
+
removeKey(key) {
|
|
946
|
+
this.knownKeys.delete(key);
|
|
947
|
+
const tags = this.keyToTags.get(key);
|
|
948
|
+
if (!tags) {
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
for (const tag of tags) {
|
|
952
|
+
const keys = this.tagToKeys.get(tag);
|
|
953
|
+
if (!keys) {
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
keys.delete(key);
|
|
957
|
+
if (keys.size === 0) {
|
|
958
|
+
this.tagToKeys.delete(tag);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
this.keyToTags.delete(key);
|
|
962
|
+
}
|
|
627
963
|
};
|
|
628
964
|
|
|
629
965
|
// src/stampede/StampedeGuard.ts
|
|
630
|
-
var
|
|
966
|
+
var import_async_mutex2 = require("async-mutex");
|
|
631
967
|
var StampedeGuard = class {
|
|
632
968
|
mutexes = /* @__PURE__ */ new Map();
|
|
633
969
|
async execute(key, task) {
|
|
@@ -644,7 +980,7 @@ var StampedeGuard = class {
|
|
|
644
980
|
getMutexEntry(key) {
|
|
645
981
|
let entry = this.mutexes.get(key);
|
|
646
982
|
if (!entry) {
|
|
647
|
-
entry = { mutex: new
|
|
983
|
+
entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
|
|
648
984
|
this.mutexes.set(key, entry);
|
|
649
985
|
}
|
|
650
986
|
entry.references += 1;
|
|
@@ -705,6 +1041,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
705
1041
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
706
1042
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
707
1043
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
1044
|
+
this.currentGeneration = options.generation;
|
|
708
1045
|
if (options.publishSetInvalidation !== void 0) {
|
|
709
1046
|
console.warn(
|
|
710
1047
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -713,21 +1050,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
713
1050
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
714
1051
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
715
1052
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1053
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
716
1054
|
this.startup = this.initialize();
|
|
717
1055
|
}
|
|
718
1056
|
layers;
|
|
719
1057
|
options;
|
|
720
1058
|
stampedeGuard = new StampedeGuard();
|
|
721
1059
|
metricsCollector = new MetricsCollector();
|
|
722
|
-
instanceId = (
|
|
1060
|
+
instanceId = createInstanceId();
|
|
723
1061
|
startup;
|
|
724
1062
|
unsubscribeInvalidation;
|
|
725
1063
|
logger;
|
|
726
1064
|
tagIndex;
|
|
1065
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
727
1066
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
728
1067
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
729
1068
|
ttlResolver;
|
|
730
1069
|
circuitBreakerManager;
|
|
1070
|
+
currentGeneration;
|
|
1071
|
+
writeBehindQueue = [];
|
|
1072
|
+
writeBehindTimer;
|
|
1073
|
+
writeBehindFlushPromise;
|
|
731
1074
|
isDisconnecting = false;
|
|
732
1075
|
disconnectPromise;
|
|
733
1076
|
/**
|
|
@@ -737,9 +1080,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
737
1080
|
* and no `fetcher` is provided.
|
|
738
1081
|
*/
|
|
739
1082
|
async get(key, fetcher, options) {
|
|
740
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1083
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
741
1084
|
this.validateWriteOptions(options);
|
|
742
|
-
await this.
|
|
1085
|
+
await this.awaitStartup("get");
|
|
743
1086
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
744
1087
|
if (hit.found) {
|
|
745
1088
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -804,8 +1147,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
804
1147
|
* Returns true if the given key exists and is not expired in any layer.
|
|
805
1148
|
*/
|
|
806
1149
|
async has(key) {
|
|
807
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
808
|
-
await this.
|
|
1150
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1151
|
+
await this.awaitStartup("has");
|
|
809
1152
|
for (const layer of this.layers) {
|
|
810
1153
|
if (this.shouldSkipLayer(layer)) {
|
|
811
1154
|
continue;
|
|
@@ -835,8 +1178,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
835
1178
|
* that has it, or null if the key is not found / has no TTL.
|
|
836
1179
|
*/
|
|
837
1180
|
async ttl(key) {
|
|
838
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
839
|
-
await this.
|
|
1181
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1182
|
+
await this.awaitStartup("ttl");
|
|
840
1183
|
for (const layer of this.layers) {
|
|
841
1184
|
if (this.shouldSkipLayer(layer)) {
|
|
842
1185
|
continue;
|
|
@@ -857,17 +1200,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
857
1200
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
858
1201
|
*/
|
|
859
1202
|
async set(key, value, options) {
|
|
860
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1203
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
861
1204
|
this.validateWriteOptions(options);
|
|
862
|
-
await this.
|
|
1205
|
+
await this.awaitStartup("set");
|
|
863
1206
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
864
1207
|
}
|
|
865
1208
|
/**
|
|
866
1209
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
867
1210
|
*/
|
|
868
1211
|
async delete(key) {
|
|
869
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
870
|
-
await this.
|
|
1212
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1213
|
+
await this.awaitStartup("delete");
|
|
871
1214
|
await this.deleteKeys([normalizedKey]);
|
|
872
1215
|
await this.publishInvalidation({
|
|
873
1216
|
scope: "key",
|
|
@@ -877,7 +1220,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
877
1220
|
});
|
|
878
1221
|
}
|
|
879
1222
|
async clear() {
|
|
880
|
-
await this.
|
|
1223
|
+
await this.awaitStartup("clear");
|
|
881
1224
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
882
1225
|
await this.tagIndex.clear();
|
|
883
1226
|
this.ttlResolver.clearProfiles();
|
|
@@ -893,23 +1236,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
893
1236
|
if (keys.length === 0) {
|
|
894
1237
|
return;
|
|
895
1238
|
}
|
|
896
|
-
await this.
|
|
1239
|
+
await this.awaitStartup("mdelete");
|
|
897
1240
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
898
|
-
|
|
1241
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1242
|
+
await this.deleteKeys(cacheKeys);
|
|
899
1243
|
await this.publishInvalidation({
|
|
900
1244
|
scope: "keys",
|
|
901
|
-
keys:
|
|
1245
|
+
keys: cacheKeys,
|
|
902
1246
|
sourceId: this.instanceId,
|
|
903
1247
|
operation: "delete"
|
|
904
1248
|
});
|
|
905
1249
|
}
|
|
906
1250
|
async mget(entries) {
|
|
1251
|
+
this.assertActive("mget");
|
|
907
1252
|
if (entries.length === 0) {
|
|
908
1253
|
return [];
|
|
909
1254
|
}
|
|
910
1255
|
const normalizedEntries = entries.map((entry) => ({
|
|
911
1256
|
...entry,
|
|
912
|
-
key: this.validateCacheKey(entry.key)
|
|
1257
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
913
1258
|
}));
|
|
914
1259
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
915
1260
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -935,7 +1280,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
935
1280
|
})
|
|
936
1281
|
);
|
|
937
1282
|
}
|
|
938
|
-
await this.
|
|
1283
|
+
await this.awaitStartup("mget");
|
|
939
1284
|
const pending = /* @__PURE__ */ new Set();
|
|
940
1285
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
941
1286
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -983,14 +1328,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
983
1328
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
984
1329
|
}
|
|
985
1330
|
async mset(entries) {
|
|
1331
|
+
this.assertActive("mset");
|
|
986
1332
|
const normalizedEntries = entries.map((entry) => ({
|
|
987
1333
|
...entry,
|
|
988
|
-
key: this.validateCacheKey(entry.key)
|
|
1334
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
989
1335
|
}));
|
|
990
1336
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
991
|
-
await
|
|
1337
|
+
await this.awaitStartup("mset");
|
|
1338
|
+
await this.writeBatch(normalizedEntries);
|
|
992
1339
|
}
|
|
993
1340
|
async warm(entries, options = {}) {
|
|
1341
|
+
this.assertActive("warm");
|
|
994
1342
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
995
1343
|
const total = entries.length;
|
|
996
1344
|
let completed = 0;
|
|
@@ -1039,14 +1387,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1039
1387
|
return new CacheNamespace(this, prefix);
|
|
1040
1388
|
}
|
|
1041
1389
|
async invalidateByTag(tag) {
|
|
1042
|
-
await this.
|
|
1390
|
+
await this.awaitStartup("invalidateByTag");
|
|
1043
1391
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1044
1392
|
await this.deleteKeys(keys);
|
|
1045
1393
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1046
1394
|
}
|
|
1395
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1396
|
+
if (tags.length === 0) {
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
await this.awaitStartup("invalidateByTags");
|
|
1400
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1401
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1402
|
+
await this.deleteKeys(keys);
|
|
1403
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1404
|
+
}
|
|
1047
1405
|
async invalidateByPattern(pattern) {
|
|
1048
|
-
await this.
|
|
1049
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1406
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1407
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1408
|
+
await this.deleteKeys(keys);
|
|
1409
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1410
|
+
}
|
|
1411
|
+
async invalidateByPrefix(prefix) {
|
|
1412
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1413
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1414
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
1050
1415
|
await this.deleteKeys(keys);
|
|
1051
1416
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1052
1417
|
}
|
|
@@ -1073,14 +1438,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1073
1438
|
getHitRate() {
|
|
1074
1439
|
return this.metricsCollector.hitRate();
|
|
1075
1440
|
}
|
|
1441
|
+
async healthCheck() {
|
|
1442
|
+
await this.startup;
|
|
1443
|
+
return Promise.all(
|
|
1444
|
+
this.layers.map(async (layer) => {
|
|
1445
|
+
const startedAt = performance.now();
|
|
1446
|
+
try {
|
|
1447
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1448
|
+
return {
|
|
1449
|
+
layer: layer.name,
|
|
1450
|
+
healthy,
|
|
1451
|
+
latencyMs: performance.now() - startedAt
|
|
1452
|
+
};
|
|
1453
|
+
} catch (error) {
|
|
1454
|
+
return {
|
|
1455
|
+
layer: layer.name,
|
|
1456
|
+
healthy: false,
|
|
1457
|
+
latencyMs: performance.now() - startedAt,
|
|
1458
|
+
error: this.formatError(error)
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
})
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
bumpGeneration(nextGeneration) {
|
|
1465
|
+
const current = this.currentGeneration ?? 0;
|
|
1466
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1467
|
+
return this.currentGeneration;
|
|
1468
|
+
}
|
|
1076
1469
|
/**
|
|
1077
1470
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1078
1471
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1079
1472
|
* Returns `null` if the key does not exist in any layer.
|
|
1080
1473
|
*/
|
|
1081
1474
|
async inspect(key) {
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1475
|
+
const userKey = this.validateCacheKey(key);
|
|
1476
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1477
|
+
await this.awaitStartup("inspect");
|
|
1084
1478
|
const foundInLayers = [];
|
|
1085
1479
|
let freshTtlSeconds = null;
|
|
1086
1480
|
let staleTtlSeconds = null;
|
|
@@ -1111,10 +1505,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1111
1505
|
return null;
|
|
1112
1506
|
}
|
|
1113
1507
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1114
|
-
return { key:
|
|
1508
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1115
1509
|
}
|
|
1116
1510
|
async exportState() {
|
|
1117
|
-
await this.
|
|
1511
|
+
await this.awaitStartup("exportState");
|
|
1118
1512
|
const exported = /* @__PURE__ */ new Map();
|
|
1119
1513
|
for (const layer of this.layers) {
|
|
1120
1514
|
if (!layer.keys) {
|
|
@@ -1122,15 +1516,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1122
1516
|
}
|
|
1123
1517
|
const keys = await layer.keys();
|
|
1124
1518
|
for (const key of keys) {
|
|
1125
|
-
|
|
1519
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1520
|
+
if (exported.has(exportedKey)) {
|
|
1126
1521
|
continue;
|
|
1127
1522
|
}
|
|
1128
1523
|
const stored = await this.readLayerEntry(layer, key);
|
|
1129
1524
|
if (stored === null) {
|
|
1130
1525
|
continue;
|
|
1131
1526
|
}
|
|
1132
|
-
exported.set(
|
|
1133
|
-
key,
|
|
1527
|
+
exported.set(exportedKey, {
|
|
1528
|
+
key: exportedKey,
|
|
1134
1529
|
value: stored,
|
|
1135
1530
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1136
1531
|
});
|
|
@@ -1139,20 +1534,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1139
1534
|
return [...exported.values()];
|
|
1140
1535
|
}
|
|
1141
1536
|
async importState(entries) {
|
|
1142
|
-
await this.
|
|
1537
|
+
await this.awaitStartup("importState");
|
|
1143
1538
|
await Promise.all(
|
|
1144
1539
|
entries.map(async (entry) => {
|
|
1145
|
-
|
|
1146
|
-
await this.
|
|
1540
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1541
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1542
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1147
1543
|
})
|
|
1148
1544
|
);
|
|
1149
1545
|
}
|
|
1150
1546
|
async persistToFile(filePath) {
|
|
1547
|
+
this.assertActive("persistToFile");
|
|
1151
1548
|
const snapshot = await this.exportState();
|
|
1152
|
-
|
|
1549
|
+
const { promises: fs2 } = await import("fs");
|
|
1550
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1153
1551
|
}
|
|
1154
1552
|
async restoreFromFile(filePath) {
|
|
1155
|
-
|
|
1553
|
+
this.assertActive("restoreFromFile");
|
|
1554
|
+
const { promises: fs2 } = await import("fs");
|
|
1555
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
1156
1556
|
let parsed;
|
|
1157
1557
|
try {
|
|
1158
1558
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1175,7 +1575,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1175
1575
|
this.disconnectPromise = (async () => {
|
|
1176
1576
|
await this.startup;
|
|
1177
1577
|
await this.unsubscribeInvalidation?.();
|
|
1578
|
+
await this.flushWriteBehindQueue();
|
|
1178
1579
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1580
|
+
if (this.writeBehindTimer) {
|
|
1581
|
+
clearInterval(this.writeBehindTimer);
|
|
1582
|
+
this.writeBehindTimer = void 0;
|
|
1583
|
+
}
|
|
1584
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1179
1585
|
})();
|
|
1180
1586
|
}
|
|
1181
1587
|
await this.disconnectPromise;
|
|
@@ -1235,7 +1641,11 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1235
1641
|
const fetchStart = Date.now();
|
|
1236
1642
|
let fetched;
|
|
1237
1643
|
try {
|
|
1238
|
-
fetched = await
|
|
1644
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1645
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1646
|
+
{ key, fetcher },
|
|
1647
|
+
fetcher
|
|
1648
|
+
);
|
|
1239
1649
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1240
1650
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1241
1651
|
} catch (error) {
|
|
@@ -1269,6 +1679,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1269
1679
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1270
1680
|
}
|
|
1271
1681
|
}
|
|
1682
|
+
async writeBatch(entries) {
|
|
1683
|
+
const now = Date.now();
|
|
1684
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1685
|
+
const immediateOperations = [];
|
|
1686
|
+
const deferredOperations = [];
|
|
1687
|
+
for (const entry of entries) {
|
|
1688
|
+
for (const layer of this.layers) {
|
|
1689
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1693
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1694
|
+
bucket.push(layerEntry);
|
|
1695
|
+
entriesByLayer.set(layer, bucket);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1699
|
+
const operation = async () => {
|
|
1700
|
+
try {
|
|
1701
|
+
if (layer.setMany) {
|
|
1702
|
+
await layer.setMany(layerEntries);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1706
|
+
} catch (error) {
|
|
1707
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1708
|
+
}
|
|
1709
|
+
};
|
|
1710
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1711
|
+
deferredOperations.push(operation);
|
|
1712
|
+
} else {
|
|
1713
|
+
immediateOperations.push(operation);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1717
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1718
|
+
for (const entry of entries) {
|
|
1719
|
+
if (entry.options?.tags) {
|
|
1720
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1721
|
+
} else {
|
|
1722
|
+
await this.tagIndex.touch(entry.key);
|
|
1723
|
+
}
|
|
1724
|
+
this.metricsCollector.increment("sets");
|
|
1725
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1726
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1727
|
+
}
|
|
1728
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1729
|
+
await this.publishInvalidation({
|
|
1730
|
+
scope: "keys",
|
|
1731
|
+
keys: entries.map((entry) => entry.key),
|
|
1732
|
+
sourceId: this.instanceId,
|
|
1733
|
+
operation: "write"
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1272
1737
|
async readFromLayers(key, options, mode) {
|
|
1273
1738
|
let sawRetainableValue = false;
|
|
1274
1739
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1352,33 +1817,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1352
1817
|
}
|
|
1353
1818
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1354
1819
|
const now = Date.now();
|
|
1355
|
-
const
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
options
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
});
|
|
1374
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1375
|
-
try {
|
|
1376
|
-
await layer.set(key, payload, ttl);
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1820
|
+
const immediateOperations = [];
|
|
1821
|
+
const deferredOperations = [];
|
|
1822
|
+
for (const layer of this.layers) {
|
|
1823
|
+
const operation = async () => {
|
|
1824
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1828
|
+
try {
|
|
1829
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1835
|
+
deferredOperations.push(operation);
|
|
1836
|
+
} else {
|
|
1837
|
+
immediateOperations.push(operation);
|
|
1379
1838
|
}
|
|
1380
|
-
}
|
|
1381
|
-
await this.executeLayerOperations(
|
|
1839
|
+
}
|
|
1840
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1841
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1382
1842
|
}
|
|
1383
1843
|
async executeLayerOperations(operations, context) {
|
|
1384
1844
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1402,8 +1862,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1402
1862
|
);
|
|
1403
1863
|
}
|
|
1404
1864
|
}
|
|
1405
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1406
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1865
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1866
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1867
|
+
key,
|
|
1868
|
+
layerName,
|
|
1869
|
+
kind,
|
|
1870
|
+
options,
|
|
1871
|
+
fallbackTtl,
|
|
1872
|
+
this.options.negativeTtl,
|
|
1873
|
+
void 0,
|
|
1874
|
+
value
|
|
1875
|
+
);
|
|
1407
1876
|
}
|
|
1408
1877
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1409
1878
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1432,7 +1901,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1432
1901
|
return {
|
|
1433
1902
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1434
1903
|
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1435
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
1904
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
1905
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1436
1906
|
};
|
|
1437
1907
|
}
|
|
1438
1908
|
async deleteKeys(keys) {
|
|
@@ -1492,11 +1962,110 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1492
1962
|
return String(error);
|
|
1493
1963
|
}
|
|
1494
1964
|
sleep(ms) {
|
|
1495
|
-
return new Promise((
|
|
1965
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1496
1966
|
}
|
|
1497
1967
|
shouldBroadcastL1Invalidation() {
|
|
1498
1968
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1499
1969
|
}
|
|
1970
|
+
initializeWriteBehind(options) {
|
|
1971
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1975
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1979
|
+
void this.flushWriteBehindQueue();
|
|
1980
|
+
}, flushIntervalMs);
|
|
1981
|
+
this.writeBehindTimer.unref?.();
|
|
1982
|
+
}
|
|
1983
|
+
shouldWriteBehind(layer) {
|
|
1984
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
1985
|
+
}
|
|
1986
|
+
async enqueueWriteBehind(operation) {
|
|
1987
|
+
this.writeBehindQueue.push(operation);
|
|
1988
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1989
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
1990
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1991
|
+
await this.flushWriteBehindQueue();
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1995
|
+
await this.flushWriteBehindQueue();
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
async flushWriteBehindQueue() {
|
|
1999
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
2000
|
+
await this.writeBehindFlushPromise;
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2004
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2005
|
+
this.writeBehindFlushPromise = (async () => {
|
|
2006
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2007
|
+
})();
|
|
2008
|
+
await this.writeBehindFlushPromise;
|
|
2009
|
+
this.writeBehindFlushPromise = void 0;
|
|
2010
|
+
if (this.writeBehindQueue.length > 0) {
|
|
2011
|
+
await this.flushWriteBehindQueue();
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2015
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2016
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2017
|
+
layer.name,
|
|
2018
|
+
options?.staleWhileRevalidate,
|
|
2019
|
+
this.options.staleWhileRevalidate
|
|
2020
|
+
);
|
|
2021
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2022
|
+
const payload = createStoredValueEnvelope({
|
|
2023
|
+
kind,
|
|
2024
|
+
value,
|
|
2025
|
+
freshTtlSeconds: freshTtl,
|
|
2026
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2027
|
+
staleIfErrorSeconds: staleIfError,
|
|
2028
|
+
now
|
|
2029
|
+
});
|
|
2030
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
2031
|
+
return {
|
|
2032
|
+
key,
|
|
2033
|
+
value: payload,
|
|
2034
|
+
ttl
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
intersectKeys(groups) {
|
|
2038
|
+
if (groups.length === 0) {
|
|
2039
|
+
return [];
|
|
2040
|
+
}
|
|
2041
|
+
const [firstGroup, ...rest] = groups;
|
|
2042
|
+
if (!firstGroup) {
|
|
2043
|
+
return [];
|
|
2044
|
+
}
|
|
2045
|
+
const restSets = rest.map((group) => new Set(group));
|
|
2046
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2047
|
+
}
|
|
2048
|
+
qualifyKey(key) {
|
|
2049
|
+
const prefix = this.generationPrefix();
|
|
2050
|
+
return prefix ? `${prefix}${key}` : key;
|
|
2051
|
+
}
|
|
2052
|
+
qualifyPattern(pattern) {
|
|
2053
|
+
const prefix = this.generationPrefix();
|
|
2054
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2055
|
+
}
|
|
2056
|
+
stripQualifiedKey(key) {
|
|
2057
|
+
const prefix = this.generationPrefix();
|
|
2058
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2059
|
+
return key;
|
|
2060
|
+
}
|
|
2061
|
+
return key.slice(prefix.length);
|
|
2062
|
+
}
|
|
2063
|
+
generationPrefix() {
|
|
2064
|
+
if (this.currentGeneration === void 0) {
|
|
2065
|
+
return "";
|
|
2066
|
+
}
|
|
2067
|
+
return `v${this.currentGeneration}:`;
|
|
2068
|
+
}
|
|
1500
2069
|
async deleteKeysFromLayers(layers, keys) {
|
|
1501
2070
|
await Promise.all(
|
|
1502
2071
|
layers.map(async (layer) => {
|
|
@@ -1538,8 +2107,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1538
2107
|
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1539
2108
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1540
2109
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2110
|
+
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2111
|
+
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
1541
2112
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1542
2113
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2114
|
+
if (this.options.generation !== void 0) {
|
|
2115
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2116
|
+
}
|
|
1543
2117
|
}
|
|
1544
2118
|
validateWriteOptions(options) {
|
|
1545
2119
|
if (!options) {
|
|
@@ -1551,8 +2125,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1551
2125
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1552
2126
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1553
2127
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2128
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1554
2129
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1555
2130
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2131
|
+
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
1556
2132
|
}
|
|
1557
2133
|
validateLayerNumberOption(name, value) {
|
|
1558
2134
|
if (value === void 0) {
|
|
@@ -1577,6 +2153,20 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1577
2153
|
throw new Error(`${name} must be a positive finite number.`);
|
|
1578
2154
|
}
|
|
1579
2155
|
}
|
|
2156
|
+
validateRateLimitOptions(name, options) {
|
|
2157
|
+
if (!options) {
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2161
|
+
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2162
|
+
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2163
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2164
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2165
|
+
}
|
|
2166
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2167
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
1580
2170
|
validateNonNegativeNumber(name, value) {
|
|
1581
2171
|
if (!Number.isFinite(value) || value < 0) {
|
|
1582
2172
|
throw new Error(`${name} must be a non-negative finite number.`);
|
|
@@ -1594,6 +2184,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1594
2184
|
}
|
|
1595
2185
|
return key;
|
|
1596
2186
|
}
|
|
2187
|
+
validateTtlPolicy(name, policy) {
|
|
2188
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
if ("alignTo" in policy) {
|
|
2192
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
throw new Error(`${name} is invalid.`);
|
|
2196
|
+
}
|
|
2197
|
+
assertActive(operation) {
|
|
2198
|
+
if (this.isDisconnecting) {
|
|
2199
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
async awaitStartup(operation) {
|
|
2203
|
+
this.assertActive(operation);
|
|
2204
|
+
await this.startup;
|
|
2205
|
+
this.assertActive(operation);
|
|
2206
|
+
}
|
|
1597
2207
|
serializeOptions(options) {
|
|
1598
2208
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1599
2209
|
}
|
|
@@ -1699,18 +2309,23 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1699
2309
|
return value;
|
|
1700
2310
|
}
|
|
1701
2311
|
};
|
|
2312
|
+
function createInstanceId() {
|
|
2313
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2314
|
+
}
|
|
1702
2315
|
|
|
1703
2316
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1704
2317
|
var RedisInvalidationBus = class {
|
|
1705
2318
|
channel;
|
|
1706
2319
|
publisher;
|
|
1707
2320
|
subscriber;
|
|
2321
|
+
logger;
|
|
1708
2322
|
handlers = /* @__PURE__ */ new Set();
|
|
1709
2323
|
sharedListener;
|
|
1710
2324
|
constructor(options) {
|
|
1711
2325
|
this.publisher = options.publisher;
|
|
1712
2326
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1713
2327
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
2328
|
+
this.logger = options.logger;
|
|
1714
2329
|
}
|
|
1715
2330
|
async subscribe(handler) {
|
|
1716
2331
|
if (this.handlers.size === 0) {
|
|
@@ -1767,6 +2382,10 @@ var RedisInvalidationBus = class {
|
|
|
1767
2382
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1768
2383
|
}
|
|
1769
2384
|
reportError(message, error) {
|
|
2385
|
+
if (this.logger?.error) {
|
|
2386
|
+
this.logger.error(message, { error });
|
|
2387
|
+
return;
|
|
2388
|
+
}
|
|
1770
2389
|
console.error(`[layercache] ${message}`, error);
|
|
1771
2390
|
}
|
|
1772
2391
|
};
|
|
@@ -1776,19 +2395,21 @@ var RedisTagIndex = class {
|
|
|
1776
2395
|
client;
|
|
1777
2396
|
prefix;
|
|
1778
2397
|
scanCount;
|
|
2398
|
+
knownKeysShards;
|
|
1779
2399
|
constructor(options) {
|
|
1780
2400
|
this.client = options.client;
|
|
1781
2401
|
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
1782
2402
|
this.scanCount = options.scanCount ?? 100;
|
|
2403
|
+
this.knownKeysShards = normalizeKnownKeysShards(options.knownKeysShards);
|
|
1783
2404
|
}
|
|
1784
2405
|
async touch(key) {
|
|
1785
|
-
await this.client.sadd(this.
|
|
2406
|
+
await this.client.sadd(this.knownKeysKeyFor(key), key);
|
|
1786
2407
|
}
|
|
1787
2408
|
async track(key, tags) {
|
|
1788
2409
|
const keyTagsKey = this.keyTagsKey(key);
|
|
1789
2410
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
1790
2411
|
const pipeline = this.client.pipeline();
|
|
1791
|
-
pipeline.sadd(this.
|
|
2412
|
+
pipeline.sadd(this.knownKeysKeyFor(key), key);
|
|
1792
2413
|
for (const tag of existingTags) {
|
|
1793
2414
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
1794
2415
|
}
|
|
@@ -1805,7 +2426,7 @@ var RedisTagIndex = class {
|
|
|
1805
2426
|
const keyTagsKey = this.keyTagsKey(key);
|
|
1806
2427
|
const existingTags = await this.client.smembers(keyTagsKey);
|
|
1807
2428
|
const pipeline = this.client.pipeline();
|
|
1808
|
-
pipeline.srem(this.
|
|
2429
|
+
pipeline.srem(this.knownKeysKeyFor(key), key);
|
|
1809
2430
|
pipeline.del(keyTagsKey);
|
|
1810
2431
|
for (const tag of existingTags) {
|
|
1811
2432
|
pipeline.srem(this.tagKeysKey(tag), key);
|
|
@@ -1815,24 +2436,38 @@ var RedisTagIndex = class {
|
|
|
1815
2436
|
async keysForTag(tag) {
|
|
1816
2437
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
1817
2438
|
}
|
|
2439
|
+
async keysForPrefix(prefix) {
|
|
2440
|
+
const matches = [];
|
|
2441
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
2442
|
+
let cursor = "0";
|
|
2443
|
+
do {
|
|
2444
|
+
const [nextCursor, keys] = await this.client.sscan(knownKeysKey, cursor, "COUNT", this.scanCount);
|
|
2445
|
+
cursor = nextCursor;
|
|
2446
|
+
matches.push(...keys.filter((key) => key.startsWith(prefix)));
|
|
2447
|
+
} while (cursor !== "0");
|
|
2448
|
+
}
|
|
2449
|
+
return matches;
|
|
2450
|
+
}
|
|
1818
2451
|
async tagsForKey(key) {
|
|
1819
2452
|
return this.client.smembers(this.keyTagsKey(key));
|
|
1820
2453
|
}
|
|
1821
2454
|
async matchPattern(pattern) {
|
|
1822
2455
|
const matches = [];
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
this.
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
2456
|
+
for (const knownKeysKey of this.knownKeysKeys()) {
|
|
2457
|
+
let cursor = "0";
|
|
2458
|
+
do {
|
|
2459
|
+
const [nextCursor, keys] = await this.client.sscan(
|
|
2460
|
+
knownKeysKey,
|
|
2461
|
+
cursor,
|
|
2462
|
+
"MATCH",
|
|
2463
|
+
pattern,
|
|
2464
|
+
"COUNT",
|
|
2465
|
+
this.scanCount
|
|
2466
|
+
);
|
|
2467
|
+
cursor = nextCursor;
|
|
2468
|
+
matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
|
|
2469
|
+
} while (cursor !== "0");
|
|
2470
|
+
}
|
|
1836
2471
|
return matches;
|
|
1837
2472
|
}
|
|
1838
2473
|
async clear() {
|
|
@@ -1853,8 +2488,17 @@ var RedisTagIndex = class {
|
|
|
1853
2488
|
} while (cursor !== "0");
|
|
1854
2489
|
return matches;
|
|
1855
2490
|
}
|
|
1856
|
-
|
|
1857
|
-
|
|
2491
|
+
knownKeysKeyFor(key) {
|
|
2492
|
+
if (this.knownKeysShards === 1) {
|
|
2493
|
+
return `${this.prefix}:keys`;
|
|
2494
|
+
}
|
|
2495
|
+
return `${this.prefix}:keys:${simpleHash(key) % this.knownKeysShards}`;
|
|
2496
|
+
}
|
|
2497
|
+
knownKeysKeys() {
|
|
2498
|
+
if (this.knownKeysShards === 1) {
|
|
2499
|
+
return [`${this.prefix}:keys`];
|
|
2500
|
+
}
|
|
2501
|
+
return Array.from({ length: this.knownKeysShards }, (_, index) => `${this.prefix}:keys:${index}`);
|
|
1858
2502
|
}
|
|
1859
2503
|
keyTagsKey(key) {
|
|
1860
2504
|
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
@@ -1863,6 +2507,22 @@ var RedisTagIndex = class {
|
|
|
1863
2507
|
return `${this.prefix}:tag:${encodeURIComponent(tag)}`;
|
|
1864
2508
|
}
|
|
1865
2509
|
};
|
|
2510
|
+
function normalizeKnownKeysShards(value) {
|
|
2511
|
+
if (value === void 0) {
|
|
2512
|
+
return 1;
|
|
2513
|
+
}
|
|
2514
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
2515
|
+
throw new Error("RedisTagIndex.knownKeysShards must be a positive integer.");
|
|
2516
|
+
}
|
|
2517
|
+
return value;
|
|
2518
|
+
}
|
|
2519
|
+
function simpleHash(value) {
|
|
2520
|
+
let hash = 0;
|
|
2521
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2522
|
+
hash = hash * 31 + value.charCodeAt(index) >>> 0;
|
|
2523
|
+
}
|
|
2524
|
+
return hash;
|
|
2525
|
+
}
|
|
1866
2526
|
|
|
1867
2527
|
// src/http/createCacheStatsHandler.ts
|
|
1868
2528
|
function createCacheStatsHandler(cache) {
|
|
@@ -1912,32 +2572,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1912
2572
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
1913
2573
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
1914
2574
|
return async (req, res, next) => {
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
1921
|
-
const cached = await cache.get(key, void 0, options);
|
|
1922
|
-
if (cached !== null) {
|
|
1923
|
-
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1924
|
-
res.setHeader?.("x-cache", "HIT");
|
|
1925
|
-
if (res.json) {
|
|
1926
|
-
res.json(cached);
|
|
1927
|
-
} else {
|
|
1928
|
-
res.end?.(JSON.stringify(cached));
|
|
2575
|
+
try {
|
|
2576
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2577
|
+
if (!allowedMethods.has(method)) {
|
|
2578
|
+
next();
|
|
2579
|
+
return;
|
|
1929
2580
|
}
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
res.
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
2581
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
2582
|
+
const cached = await cache.get(key, void 0, options);
|
|
2583
|
+
if (cached !== null) {
|
|
2584
|
+
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2585
|
+
res.setHeader?.("x-cache", "HIT");
|
|
2586
|
+
if (res.json) {
|
|
2587
|
+
res.json(cached);
|
|
2588
|
+
} else {
|
|
2589
|
+
res.end?.(JSON.stringify(cached));
|
|
2590
|
+
}
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const originalJson = res.json?.bind(res);
|
|
2594
|
+
if (originalJson) {
|
|
2595
|
+
res.json = (body) => {
|
|
2596
|
+
res.setHeader?.("x-cache", "MISS");
|
|
2597
|
+
void cache.set(key, body, options);
|
|
2598
|
+
return originalJson(body);
|
|
2599
|
+
};
|
|
2600
|
+
}
|
|
2601
|
+
next();
|
|
2602
|
+
} catch (error) {
|
|
2603
|
+
next(error);
|
|
1939
2604
|
}
|
|
1940
|
-
next();
|
|
1941
2605
|
};
|
|
1942
2606
|
}
|
|
1943
2607
|
|
|
@@ -1950,6 +2614,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1950
2614
|
return (...args) => wrapped(...args);
|
|
1951
2615
|
}
|
|
1952
2616
|
|
|
2617
|
+
// src/integrations/hono.ts
|
|
2618
|
+
function createHonoCacheMiddleware(cache, options = {}) {
|
|
2619
|
+
const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
|
|
2620
|
+
return async (context, next) => {
|
|
2621
|
+
const method = (context.req.method ?? "GET").toUpperCase();
|
|
2622
|
+
if (!allowedMethods.has(method)) {
|
|
2623
|
+
await next();
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
|
|
2627
|
+
const cached = await cache.get(key, void 0, options);
|
|
2628
|
+
if (cached !== null) {
|
|
2629
|
+
context.header?.("x-cache", "HIT");
|
|
2630
|
+
context.header?.("content-type", "application/json; charset=utf-8");
|
|
2631
|
+
context.json(cached);
|
|
2632
|
+
return;
|
|
2633
|
+
}
|
|
2634
|
+
const originalJson = context.json.bind(context);
|
|
2635
|
+
context.json = (body, status) => {
|
|
2636
|
+
context.header?.("x-cache", "MISS");
|
|
2637
|
+
void cache.set(key, body, options);
|
|
2638
|
+
return originalJson(body, status);
|
|
2639
|
+
};
|
|
2640
|
+
await next();
|
|
2641
|
+
};
|
|
2642
|
+
}
|
|
2643
|
+
|
|
2644
|
+
// src/integrations/opentelemetry.ts
|
|
2645
|
+
function createOpenTelemetryPlugin(cache, tracer) {
|
|
2646
|
+
const originals = {
|
|
2647
|
+
get: cache.get.bind(cache),
|
|
2648
|
+
set: cache.set.bind(cache),
|
|
2649
|
+
delete: cache.delete.bind(cache),
|
|
2650
|
+
mget: cache.mget.bind(cache),
|
|
2651
|
+
mset: cache.mset.bind(cache),
|
|
2652
|
+
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
2653
|
+
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
2654
|
+
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
2655
|
+
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
2656
|
+
};
|
|
2657
|
+
cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
|
|
2658
|
+
"layercache.key": String(args[0] ?? "")
|
|
2659
|
+
}));
|
|
2660
|
+
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
2661
|
+
"layercache.key": String(args[0] ?? "")
|
|
2662
|
+
}));
|
|
2663
|
+
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
2664
|
+
"layercache.key": String(args[0] ?? "")
|
|
2665
|
+
}));
|
|
2666
|
+
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
2667
|
+
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
2668
|
+
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
2669
|
+
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
2670
|
+
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
2671
|
+
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
2672
|
+
return {
|
|
2673
|
+
uninstall() {
|
|
2674
|
+
cache.get = originals.get;
|
|
2675
|
+
cache.set = originals.set;
|
|
2676
|
+
cache.delete = originals.delete;
|
|
2677
|
+
cache.mget = originals.mget;
|
|
2678
|
+
cache.mset = originals.mset;
|
|
2679
|
+
cache.invalidateByTag = originals.invalidateByTag;
|
|
2680
|
+
cache.invalidateByTags = originals.invalidateByTags;
|
|
2681
|
+
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
2682
|
+
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
2683
|
+
}
|
|
2684
|
+
};
|
|
2685
|
+
}
|
|
2686
|
+
function instrument(name, tracer, method, attributes) {
|
|
2687
|
+
return (async (...args) => {
|
|
2688
|
+
const span = tracer.startSpan(name, { attributes: attributes?.(args) });
|
|
2689
|
+
try {
|
|
2690
|
+
const result = await method(...args);
|
|
2691
|
+
span.setAttribute?.("layercache.success", true);
|
|
2692
|
+
if (result === null) {
|
|
2693
|
+
span.setAttribute?.("layercache.result", "null");
|
|
2694
|
+
}
|
|
2695
|
+
return result;
|
|
2696
|
+
} catch (error) {
|
|
2697
|
+
span.setAttribute?.("layercache.success", false);
|
|
2698
|
+
span.recordException?.(error);
|
|
2699
|
+
throw error;
|
|
2700
|
+
} finally {
|
|
2701
|
+
span.end();
|
|
2702
|
+
}
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
|
|
1953
2706
|
// src/integrations/trpc.ts
|
|
1954
2707
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1955
2708
|
return async (context) => {
|
|
@@ -1982,12 +2735,21 @@ var MemoryLayer = class {
|
|
|
1982
2735
|
isLocal = true;
|
|
1983
2736
|
maxSize;
|
|
1984
2737
|
evictionPolicy;
|
|
2738
|
+
onEvict;
|
|
1985
2739
|
entries = /* @__PURE__ */ new Map();
|
|
2740
|
+
cleanupTimer;
|
|
1986
2741
|
constructor(options = {}) {
|
|
1987
2742
|
this.name = options.name ?? "memory";
|
|
1988
2743
|
this.defaultTtl = options.ttl;
|
|
1989
2744
|
this.maxSize = options.maxSize ?? 1e3;
|
|
1990
2745
|
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
2746
|
+
this.onEvict = options.onEvict;
|
|
2747
|
+
if (options.cleanupIntervalMs && options.cleanupIntervalMs > 0) {
|
|
2748
|
+
this.cleanupTimer = setInterval(() => {
|
|
2749
|
+
this.pruneExpired();
|
|
2750
|
+
}, options.cleanupIntervalMs);
|
|
2751
|
+
this.cleanupTimer.unref?.();
|
|
2752
|
+
}
|
|
1991
2753
|
}
|
|
1992
2754
|
async get(key) {
|
|
1993
2755
|
const value = await this.getEntry(key);
|
|
@@ -2018,6 +2780,11 @@ var MemoryLayer = class {
|
|
|
2018
2780
|
}
|
|
2019
2781
|
return values;
|
|
2020
2782
|
}
|
|
2783
|
+
async setMany(entries) {
|
|
2784
|
+
for (const entry of entries) {
|
|
2785
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2021
2788
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2022
2789
|
this.entries.delete(key);
|
|
2023
2790
|
this.entries.set(key, {
|
|
@@ -2070,6 +2837,15 @@ var MemoryLayer = class {
|
|
|
2070
2837
|
async clear() {
|
|
2071
2838
|
this.entries.clear();
|
|
2072
2839
|
}
|
|
2840
|
+
async ping() {
|
|
2841
|
+
return true;
|
|
2842
|
+
}
|
|
2843
|
+
async dispose() {
|
|
2844
|
+
if (this.cleanupTimer) {
|
|
2845
|
+
clearInterval(this.cleanupTimer);
|
|
2846
|
+
this.cleanupTimer = void 0;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2073
2849
|
async keys() {
|
|
2074
2850
|
this.pruneExpired();
|
|
2075
2851
|
return [...this.entries.keys()];
|
|
@@ -2102,7 +2878,11 @@ var MemoryLayer = class {
|
|
|
2102
2878
|
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
2103
2879
|
const oldestKey = this.entries.keys().next().value;
|
|
2104
2880
|
if (oldestKey !== void 0) {
|
|
2881
|
+
const entry = this.entries.get(oldestKey);
|
|
2105
2882
|
this.entries.delete(oldestKey);
|
|
2883
|
+
if (entry) {
|
|
2884
|
+
this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
|
|
2885
|
+
}
|
|
2106
2886
|
}
|
|
2107
2887
|
return;
|
|
2108
2888
|
}
|
|
@@ -2117,7 +2897,11 @@ var MemoryLayer = class {
|
|
|
2117
2897
|
}
|
|
2118
2898
|
}
|
|
2119
2899
|
if (victimKey !== void 0) {
|
|
2900
|
+
const victim = this.entries.get(victimKey);
|
|
2120
2901
|
this.entries.delete(victimKey);
|
|
2902
|
+
if (victim) {
|
|
2903
|
+
this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
|
|
2904
|
+
}
|
|
2121
2905
|
}
|
|
2122
2906
|
}
|
|
2123
2907
|
pruneExpired() {
|
|
@@ -2137,15 +2921,35 @@ var import_node_util = require("util");
|
|
|
2137
2921
|
var import_node_zlib = require("zlib");
|
|
2138
2922
|
|
|
2139
2923
|
// src/serialization/JsonSerializer.ts
|
|
2924
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
2140
2925
|
var JsonSerializer = class {
|
|
2141
2926
|
serialize(value) {
|
|
2142
2927
|
return JSON.stringify(value);
|
|
2143
2928
|
}
|
|
2144
2929
|
deserialize(payload) {
|
|
2145
2930
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
2146
|
-
return JSON.parse(normalized);
|
|
2931
|
+
return sanitizeJsonValue(JSON.parse(normalized));
|
|
2147
2932
|
}
|
|
2148
2933
|
};
|
|
2934
|
+
function sanitizeJsonValue(value) {
|
|
2935
|
+
if (Array.isArray(value)) {
|
|
2936
|
+
return value.map((entry) => sanitizeJsonValue(entry));
|
|
2937
|
+
}
|
|
2938
|
+
if (!isPlainObject(value)) {
|
|
2939
|
+
return value;
|
|
2940
|
+
}
|
|
2941
|
+
const sanitized = {};
|
|
2942
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2943
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
sanitized[key] = sanitizeJsonValue(entry);
|
|
2947
|
+
}
|
|
2948
|
+
return sanitized;
|
|
2949
|
+
}
|
|
2950
|
+
function isPlainObject(value) {
|
|
2951
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2952
|
+
}
|
|
2149
2953
|
|
|
2150
2954
|
// src/layers/RedisLayer.ts
|
|
2151
2955
|
var BATCH_DELETE_SIZE = 500;
|
|
@@ -2158,22 +2962,24 @@ var RedisLayer = class {
|
|
|
2158
2962
|
defaultTtl;
|
|
2159
2963
|
isLocal = false;
|
|
2160
2964
|
client;
|
|
2161
|
-
|
|
2965
|
+
serializers;
|
|
2162
2966
|
prefix;
|
|
2163
2967
|
allowUnprefixedClear;
|
|
2164
2968
|
scanCount;
|
|
2165
2969
|
compression;
|
|
2166
2970
|
compressionThreshold;
|
|
2971
|
+
disconnectOnDispose;
|
|
2167
2972
|
constructor(options) {
|
|
2168
2973
|
this.client = options.client;
|
|
2169
2974
|
this.defaultTtl = options.ttl;
|
|
2170
2975
|
this.name = options.name ?? "redis";
|
|
2171
|
-
this.
|
|
2976
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
2172
2977
|
this.prefix = options.prefix ?? "";
|
|
2173
2978
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
2174
2979
|
this.scanCount = options.scanCount ?? 100;
|
|
2175
2980
|
this.compression = options.compression;
|
|
2176
2981
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2982
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
2177
2983
|
}
|
|
2178
2984
|
async get(key) {
|
|
2179
2985
|
const payload = await this.getEntry(key);
|
|
@@ -2208,8 +3014,25 @@ var RedisLayer = class {
|
|
|
2208
3014
|
})
|
|
2209
3015
|
);
|
|
2210
3016
|
}
|
|
3017
|
+
async setMany(entries) {
|
|
3018
|
+
if (entries.length === 0) {
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
const pipeline = this.client.pipeline();
|
|
3022
|
+
for (const entry of entries) {
|
|
3023
|
+
const serialized = this.primarySerializer().serialize(entry.value);
|
|
3024
|
+
const payload = await this.encodePayload(serialized);
|
|
3025
|
+
const normalizedKey = this.withPrefix(entry.key);
|
|
3026
|
+
if (entry.ttl && entry.ttl > 0) {
|
|
3027
|
+
pipeline.set(normalizedKey, payload, "EX", entry.ttl);
|
|
3028
|
+
} else {
|
|
3029
|
+
pipeline.set(normalizedKey, payload);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
await pipeline.exec();
|
|
3033
|
+
}
|
|
2211
3034
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2212
|
-
const serialized = this.
|
|
3035
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2213
3036
|
const payload = await this.encodePayload(serialized);
|
|
2214
3037
|
const normalizedKey = this.withPrefix(key);
|
|
2215
3038
|
if (ttl && ttl > 0) {
|
|
@@ -2242,6 +3065,18 @@ var RedisLayer = class {
|
|
|
2242
3065
|
const keys = await this.keys();
|
|
2243
3066
|
return keys.length;
|
|
2244
3067
|
}
|
|
3068
|
+
async ping() {
|
|
3069
|
+
try {
|
|
3070
|
+
return await this.client.ping() === "PONG";
|
|
3071
|
+
} catch {
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
async dispose() {
|
|
3076
|
+
if (this.disconnectOnDispose) {
|
|
3077
|
+
this.client.disconnect();
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
2245
3080
|
/**
|
|
2246
3081
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2247
3082
|
* loading millions of keys into memory at once.
|
|
@@ -2288,12 +3123,39 @@ var RedisLayer = class {
|
|
|
2288
3123
|
return `${this.prefix}${key}`;
|
|
2289
3124
|
}
|
|
2290
3125
|
async deserializeOrDelete(key, payload) {
|
|
3126
|
+
const decodedPayload = await this.decodePayload(payload);
|
|
3127
|
+
for (const serializer of this.serializers) {
|
|
3128
|
+
try {
|
|
3129
|
+
const value = serializer.deserialize(decodedPayload);
|
|
3130
|
+
if (serializer !== this.primarySerializer()) {
|
|
3131
|
+
await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
|
|
3132
|
+
}
|
|
3133
|
+
return value;
|
|
3134
|
+
} catch {
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
2291
3137
|
try {
|
|
2292
|
-
return this.serializer.deserialize(await this.decodePayload(payload));
|
|
2293
|
-
} catch {
|
|
2294
3138
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2295
|
-
|
|
3139
|
+
} catch {
|
|
2296
3140
|
}
|
|
3141
|
+
return null;
|
|
3142
|
+
}
|
|
3143
|
+
async rewriteWithPrimarySerializer(key, value) {
|
|
3144
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
3145
|
+
const payload = await this.encodePayload(serialized);
|
|
3146
|
+
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
3147
|
+
if (ttl > 0) {
|
|
3148
|
+
await this.client.set(this.withPrefix(key), payload, "EX", ttl);
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
await this.client.set(this.withPrefix(key), payload);
|
|
3152
|
+
}
|
|
3153
|
+
primarySerializer() {
|
|
3154
|
+
const serializer = this.serializers[0];
|
|
3155
|
+
if (!serializer) {
|
|
3156
|
+
throw new Error("RedisLayer requires at least one serializer.");
|
|
3157
|
+
}
|
|
3158
|
+
return serializer;
|
|
2297
3159
|
}
|
|
2298
3160
|
isSerializablePayload(payload) {
|
|
2299
3161
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
@@ -2332,8 +3194,8 @@ var RedisLayer = class {
|
|
|
2332
3194
|
};
|
|
2333
3195
|
|
|
2334
3196
|
// src/layers/DiskLayer.ts
|
|
2335
|
-
var
|
|
2336
|
-
var
|
|
3197
|
+
var import_node_crypto = require("crypto");
|
|
3198
|
+
var import_node_fs = require("fs");
|
|
2337
3199
|
var import_node_path = require("path");
|
|
2338
3200
|
var DiskLayer = class {
|
|
2339
3201
|
name;
|
|
@@ -2342,12 +3204,13 @@ var DiskLayer = class {
|
|
|
2342
3204
|
directory;
|
|
2343
3205
|
serializer;
|
|
2344
3206
|
maxFiles;
|
|
3207
|
+
writeQueue = Promise.resolve();
|
|
2345
3208
|
constructor(options) {
|
|
2346
|
-
this.directory = options.directory;
|
|
3209
|
+
this.directory = this.resolveDirectory(options.directory);
|
|
2347
3210
|
this.defaultTtl = options.ttl;
|
|
2348
3211
|
this.name = options.name ?? "disk";
|
|
2349
3212
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2350
|
-
this.maxFiles = options.maxFiles;
|
|
3213
|
+
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
2351
3214
|
}
|
|
2352
3215
|
async get(key) {
|
|
2353
3216
|
return unwrapStoredValue(await this.getEntry(key));
|
|
@@ -2356,13 +3219,13 @@ var DiskLayer = class {
|
|
|
2356
3219
|
const filePath = this.keyToPath(key);
|
|
2357
3220
|
let raw;
|
|
2358
3221
|
try {
|
|
2359
|
-
raw = await
|
|
3222
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2360
3223
|
} catch {
|
|
2361
3224
|
return null;
|
|
2362
3225
|
}
|
|
2363
3226
|
let entry;
|
|
2364
3227
|
try {
|
|
2365
|
-
entry = this.
|
|
3228
|
+
entry = this.deserializeEntry(raw);
|
|
2366
3229
|
} catch {
|
|
2367
3230
|
await this.safeDelete(filePath);
|
|
2368
3231
|
return null;
|
|
@@ -2374,16 +3237,29 @@ var DiskLayer = class {
|
|
|
2374
3237
|
return entry.value;
|
|
2375
3238
|
}
|
|
2376
3239
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2377
|
-
await
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
3240
|
+
await this.enqueueWrite(async () => {
|
|
3241
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3242
|
+
const entry = {
|
|
3243
|
+
key,
|
|
3244
|
+
value,
|
|
3245
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
3246
|
+
};
|
|
3247
|
+
const payload = this.serializer.serialize(entry);
|
|
3248
|
+
const targetPath = this.keyToPath(key);
|
|
3249
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3250
|
+
await import_node_fs.promises.writeFile(tempPath, payload);
|
|
3251
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
3252
|
+
if (this.maxFiles !== void 0) {
|
|
3253
|
+
await this.enforceMaxFiles();
|
|
3254
|
+
}
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
async getMany(keys) {
|
|
3258
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3259
|
+
}
|
|
3260
|
+
async setMany(entries) {
|
|
3261
|
+
for (const entry of entries) {
|
|
3262
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2387
3263
|
}
|
|
2388
3264
|
}
|
|
2389
3265
|
async has(key) {
|
|
@@ -2394,14 +3270,15 @@ var DiskLayer = class {
|
|
|
2394
3270
|
const filePath = this.keyToPath(key);
|
|
2395
3271
|
let raw;
|
|
2396
3272
|
try {
|
|
2397
|
-
raw = await
|
|
3273
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2398
3274
|
} catch {
|
|
2399
3275
|
return null;
|
|
2400
3276
|
}
|
|
2401
3277
|
let entry;
|
|
2402
3278
|
try {
|
|
2403
|
-
entry = this.
|
|
3279
|
+
entry = this.deserializeEntry(raw);
|
|
2404
3280
|
} catch {
|
|
3281
|
+
await this.safeDelete(filePath);
|
|
2405
3282
|
return null;
|
|
2406
3283
|
}
|
|
2407
3284
|
if (entry.expiresAt === null) {
|
|
@@ -2414,21 +3291,25 @@ var DiskLayer = class {
|
|
|
2414
3291
|
return remaining;
|
|
2415
3292
|
}
|
|
2416
3293
|
async delete(key) {
|
|
2417
|
-
await this.safeDelete(this.keyToPath(key));
|
|
3294
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2418
3295
|
}
|
|
2419
3296
|
async deleteMany(keys) {
|
|
2420
|
-
await
|
|
3297
|
+
await this.enqueueWrite(async () => {
|
|
3298
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
3299
|
+
});
|
|
2421
3300
|
}
|
|
2422
3301
|
async clear() {
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
3302
|
+
await this.enqueueWrite(async () => {
|
|
3303
|
+
let entries;
|
|
3304
|
+
try {
|
|
3305
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3306
|
+
} catch {
|
|
3307
|
+
return;
|
|
3308
|
+
}
|
|
3309
|
+
await Promise.all(
|
|
3310
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
|
|
3311
|
+
);
|
|
3312
|
+
});
|
|
2432
3313
|
}
|
|
2433
3314
|
/**
|
|
2434
3315
|
* Returns the original cache key strings stored on disk.
|
|
@@ -2437,7 +3318,7 @@ var DiskLayer = class {
|
|
|
2437
3318
|
async keys() {
|
|
2438
3319
|
let entries;
|
|
2439
3320
|
try {
|
|
2440
|
-
entries = await
|
|
3321
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2441
3322
|
} catch {
|
|
2442
3323
|
return [];
|
|
2443
3324
|
}
|
|
@@ -2448,13 +3329,13 @@ var DiskLayer = class {
|
|
|
2448
3329
|
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
2449
3330
|
let raw;
|
|
2450
3331
|
try {
|
|
2451
|
-
raw = await
|
|
3332
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2452
3333
|
} catch {
|
|
2453
3334
|
return;
|
|
2454
3335
|
}
|
|
2455
3336
|
let entry;
|
|
2456
3337
|
try {
|
|
2457
|
-
entry = this.
|
|
3338
|
+
entry = this.deserializeEntry(raw);
|
|
2458
3339
|
} catch {
|
|
2459
3340
|
await this.safeDelete(filePath);
|
|
2460
3341
|
return;
|
|
@@ -2472,16 +3353,56 @@ var DiskLayer = class {
|
|
|
2472
3353
|
const keys = await this.keys();
|
|
2473
3354
|
return keys.length;
|
|
2474
3355
|
}
|
|
3356
|
+
async ping() {
|
|
3357
|
+
try {
|
|
3358
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3359
|
+
return true;
|
|
3360
|
+
} catch {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
async dispose() {
|
|
3365
|
+
}
|
|
2475
3366
|
keyToPath(key) {
|
|
2476
|
-
const hash = (0,
|
|
3367
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
|
|
2477
3368
|
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
2478
3369
|
}
|
|
3370
|
+
resolveDirectory(directory) {
|
|
3371
|
+
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
3372
|
+
throw new Error("DiskLayer.directory must be a non-empty path.");
|
|
3373
|
+
}
|
|
3374
|
+
if (directory.includes("\0")) {
|
|
3375
|
+
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
3376
|
+
}
|
|
3377
|
+
return (0, import_node_path.resolve)(directory);
|
|
3378
|
+
}
|
|
3379
|
+
normalizeMaxFiles(maxFiles) {
|
|
3380
|
+
if (maxFiles === void 0) {
|
|
3381
|
+
return void 0;
|
|
3382
|
+
}
|
|
3383
|
+
if (!Number.isInteger(maxFiles) || maxFiles <= 0) {
|
|
3384
|
+
throw new Error("DiskLayer.maxFiles must be a positive integer.");
|
|
3385
|
+
}
|
|
3386
|
+
return maxFiles;
|
|
3387
|
+
}
|
|
3388
|
+
deserializeEntry(raw) {
|
|
3389
|
+
const entry = this.serializer.deserialize(raw);
|
|
3390
|
+
if (!isDiskEntry(entry)) {
|
|
3391
|
+
throw new Error("Invalid disk cache entry.");
|
|
3392
|
+
}
|
|
3393
|
+
return entry;
|
|
3394
|
+
}
|
|
2479
3395
|
async safeDelete(filePath) {
|
|
2480
3396
|
try {
|
|
2481
|
-
await
|
|
3397
|
+
await import_node_fs.promises.unlink(filePath);
|
|
2482
3398
|
} catch {
|
|
2483
3399
|
}
|
|
2484
3400
|
}
|
|
3401
|
+
enqueueWrite(operation) {
|
|
3402
|
+
const next = this.writeQueue.then(operation, operation);
|
|
3403
|
+
this.writeQueue = next.catch(() => void 0);
|
|
3404
|
+
return next;
|
|
3405
|
+
}
|
|
2485
3406
|
/**
|
|
2486
3407
|
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
2487
3408
|
*/
|
|
@@ -2491,7 +3412,7 @@ var DiskLayer = class {
|
|
|
2491
3412
|
}
|
|
2492
3413
|
let entries;
|
|
2493
3414
|
try {
|
|
2494
|
-
entries = await
|
|
3415
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2495
3416
|
} catch {
|
|
2496
3417
|
return;
|
|
2497
3418
|
}
|
|
@@ -2503,7 +3424,7 @@ var DiskLayer = class {
|
|
|
2503
3424
|
lcFiles.map(async (name) => {
|
|
2504
3425
|
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
2505
3426
|
try {
|
|
2506
|
-
const stat = await
|
|
3427
|
+
const stat = await import_node_fs.promises.stat(filePath);
|
|
2507
3428
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
2508
3429
|
} catch {
|
|
2509
3430
|
return { filePath, mtimeMs: 0 };
|
|
@@ -2515,6 +3436,14 @@ var DiskLayer = class {
|
|
|
2515
3436
|
await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
|
|
2516
3437
|
}
|
|
2517
3438
|
};
|
|
3439
|
+
function isDiskEntry(value) {
|
|
3440
|
+
if (!value || typeof value !== "object") {
|
|
3441
|
+
return false;
|
|
3442
|
+
}
|
|
3443
|
+
const candidate = value;
|
|
3444
|
+
const validExpiry = candidate.expiresAt === null || typeof candidate.expiresAt === "number";
|
|
3445
|
+
return typeof candidate.key === "string" && validExpiry && "value" in candidate;
|
|
3446
|
+
}
|
|
2518
3447
|
|
|
2519
3448
|
// src/layers/MemcachedLayer.ts
|
|
2520
3449
|
var MemcachedLayer = class {
|
|
@@ -2587,13 +3516,19 @@ var MsgpackSerializer = class {
|
|
|
2587
3516
|
};
|
|
2588
3517
|
|
|
2589
3518
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2590
|
-
var
|
|
3519
|
+
var import_node_crypto2 = require("crypto");
|
|
2591
3520
|
var RELEASE_SCRIPT = `
|
|
2592
3521
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2593
3522
|
return redis.call("del", KEYS[1])
|
|
2594
3523
|
end
|
|
2595
3524
|
return 0
|
|
2596
3525
|
`;
|
|
3526
|
+
var RENEW_SCRIPT = `
|
|
3527
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
3528
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
3529
|
+
end
|
|
3530
|
+
return 0
|
|
3531
|
+
`;
|
|
2597
3532
|
var RedisSingleFlightCoordinator = class {
|
|
2598
3533
|
client;
|
|
2599
3534
|
prefix;
|
|
@@ -2603,17 +3538,32 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2603
3538
|
}
|
|
2604
3539
|
async execute(key, options, worker, waiter) {
|
|
2605
3540
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2606
|
-
const token = (0,
|
|
3541
|
+
const token = (0, import_node_crypto2.randomUUID)();
|
|
2607
3542
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2608
3543
|
if (acquired === "OK") {
|
|
3544
|
+
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|
|
2609
3545
|
try {
|
|
2610
3546
|
return await worker();
|
|
2611
3547
|
} finally {
|
|
3548
|
+
if (renewTimer) {
|
|
3549
|
+
clearInterval(renewTimer);
|
|
3550
|
+
}
|
|
2612
3551
|
await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
|
|
2613
3552
|
}
|
|
2614
3553
|
}
|
|
2615
3554
|
return waiter();
|
|
2616
3555
|
}
|
|
3556
|
+
startLeaseRenewal(lockKey, token, options) {
|
|
3557
|
+
const renewIntervalMs = options.renewIntervalMs ?? Math.max(100, Math.floor(options.leaseMs / 2));
|
|
3558
|
+
if (renewIntervalMs <= 0 || renewIntervalMs >= options.leaseMs) {
|
|
3559
|
+
return void 0;
|
|
3560
|
+
}
|
|
3561
|
+
const timer = setInterval(() => {
|
|
3562
|
+
void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
|
|
3563
|
+
}, renewIntervalMs);
|
|
3564
|
+
timer.unref?.();
|
|
3565
|
+
return timer;
|
|
3566
|
+
}
|
|
2617
3567
|
};
|
|
2618
3568
|
|
|
2619
3569
|
// src/metrics/PrometheusExporter.ts
|
|
@@ -2716,6 +3666,8 @@ function sanitizeLabel(value) {
|
|
|
2716
3666
|
createCachedMethodDecorator,
|
|
2717
3667
|
createExpressCacheMiddleware,
|
|
2718
3668
|
createFastifyLayercachePlugin,
|
|
3669
|
+
createHonoCacheMiddleware,
|
|
3670
|
+
createOpenTelemetryPlugin,
|
|
2719
3671
|
createPrometheusMetricsExporter,
|
|
2720
3672
|
createTrpcCacheMiddleware
|
|
2721
3673
|
});
|