layercache 1.1.0 → 1.2.1
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 +165 -7
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-QUB5VZFZ.js → chunk-GF47Y3XR.js} +16 -38
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +133 -23
- package/dist/cli.js +66 -4
- package/dist/edge-C1sBhTfv.d.cts +667 -0
- package/dist/edge-C1sBhTfv.d.ts +667 -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 +1259 -192
- package/dist/index.d.cts +132 -480
- package/dist/index.d.ts +132 -480
- package/dist/index.js +1115 -474
- package/package.json +7 -2
- package/packages/nestjs/dist/index.cjs +1025 -327
- package/packages/nestjs/dist/index.d.cts +167 -1
- package/packages/nestjs/dist/index.d.ts +167 -1
- package/packages/nestjs/dist/index.js +1013 -325
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,11 +17,20 @@ 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
|
|
21
31
|
var index_exports = {};
|
|
22
32
|
__export(index_exports, {
|
|
33
|
+
CacheMissError: () => CacheMissError,
|
|
23
34
|
CacheNamespace: () => CacheNamespace,
|
|
24
35
|
CacheStack: () => CacheStack,
|
|
25
36
|
DiskLayer: () => DiskLayer,
|
|
@@ -37,70 +48,96 @@ __export(index_exports, {
|
|
|
37
48
|
cacheGraphqlResolver: () => cacheGraphqlResolver,
|
|
38
49
|
createCacheStatsHandler: () => createCacheStatsHandler,
|
|
39
50
|
createCachedMethodDecorator: () => createCachedMethodDecorator,
|
|
51
|
+
createExpressCacheMiddleware: () => createExpressCacheMiddleware,
|
|
40
52
|
createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
|
|
53
|
+
createHonoCacheMiddleware: () => createHonoCacheMiddleware,
|
|
54
|
+
createOpenTelemetryPlugin: () => createOpenTelemetryPlugin,
|
|
41
55
|
createPrometheusMetricsExporter: () => createPrometheusMetricsExporter,
|
|
42
56
|
createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
|
|
43
57
|
});
|
|
44
58
|
module.exports = __toCommonJS(index_exports);
|
|
45
59
|
|
|
46
60
|
// src/CacheStack.ts
|
|
47
|
-
var import_node_crypto = require("crypto");
|
|
48
61
|
var import_node_events = require("events");
|
|
49
|
-
var import_node_fs = require("fs");
|
|
50
62
|
|
|
51
63
|
// src/CacheNamespace.ts
|
|
52
|
-
var
|
|
64
|
+
var import_async_mutex = require("async-mutex");
|
|
65
|
+
var CacheNamespace = class _CacheNamespace {
|
|
53
66
|
constructor(cache, prefix) {
|
|
54
67
|
this.cache = cache;
|
|
55
68
|
this.prefix = prefix;
|
|
56
69
|
}
|
|
57
70
|
cache;
|
|
58
71
|
prefix;
|
|
72
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
metrics = emptyMetrics();
|
|
59
74
|
async get(key, fetcher, options) {
|
|
60
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
75
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
61
76
|
}
|
|
62
77
|
async getOrSet(key, fetcher, options) {
|
|
63
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
78
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
82
|
+
*/
|
|
83
|
+
async getOrThrow(key, fetcher, options) {
|
|
84
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
64
85
|
}
|
|
65
86
|
async has(key) {
|
|
66
|
-
return this.cache.has(this.qualify(key));
|
|
87
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
67
88
|
}
|
|
68
89
|
async ttl(key) {
|
|
69
|
-
return this.cache.ttl(this.qualify(key));
|
|
90
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
70
91
|
}
|
|
71
92
|
async set(key, value, options) {
|
|
72
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
93
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
73
94
|
}
|
|
74
95
|
async delete(key) {
|
|
75
|
-
await this.cache.delete(this.qualify(key));
|
|
96
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
76
97
|
}
|
|
77
98
|
async mdelete(keys) {
|
|
78
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
99
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
79
100
|
}
|
|
80
101
|
async clear() {
|
|
81
|
-
await this.cache.
|
|
102
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
82
103
|
}
|
|
83
104
|
async mget(entries) {
|
|
84
|
-
return this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
105
|
+
return this.trackMetrics(
|
|
106
|
+
() => this.cache.mget(
|
|
107
|
+
entries.map((entry) => ({
|
|
108
|
+
...entry,
|
|
109
|
+
key: this.qualify(entry.key)
|
|
110
|
+
}))
|
|
111
|
+
)
|
|
89
112
|
);
|
|
90
113
|
}
|
|
91
114
|
async mset(entries) {
|
|
92
|
-
await this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
await this.trackMetrics(
|
|
116
|
+
() => this.cache.mset(
|
|
117
|
+
entries.map((entry) => ({
|
|
118
|
+
...entry,
|
|
119
|
+
key: this.qualify(entry.key)
|
|
120
|
+
}))
|
|
121
|
+
)
|
|
97
122
|
);
|
|
98
123
|
}
|
|
99
124
|
async invalidateByTag(tag) {
|
|
100
|
-
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));
|
|
101
129
|
}
|
|
102
130
|
async invalidateByPattern(pattern) {
|
|
103
|
-
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)));
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
138
|
+
*/
|
|
139
|
+
async inspect(key) {
|
|
140
|
+
return this.cache.inspect(this.qualify(key));
|
|
104
141
|
}
|
|
105
142
|
wrap(keyPrefix, fetcher, options) {
|
|
106
143
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
@@ -115,15 +152,159 @@ var CacheNamespace = class {
|
|
|
115
152
|
);
|
|
116
153
|
}
|
|
117
154
|
getMetrics() {
|
|
118
|
-
return this.
|
|
155
|
+
return cloneMetrics(this.metrics);
|
|
119
156
|
}
|
|
120
157
|
getHitRate() {
|
|
121
|
-
|
|
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 };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
171
|
+
*
|
|
172
|
+
* ```ts
|
|
173
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
174
|
+
* const posts = tenant.namespace('posts')
|
|
175
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
176
|
+
* ```
|
|
177
|
+
*/
|
|
178
|
+
namespace(childPrefix) {
|
|
179
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
122
180
|
}
|
|
123
181
|
qualify(key) {
|
|
124
182
|
return `${this.prefix}:${key}`;
|
|
125
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
|
+
}
|
|
126
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
|
+
}
|
|
127
308
|
|
|
128
309
|
// src/internal/CircuitBreakerManager.ts
|
|
129
310
|
var CircuitBreakerManager = class {
|
|
@@ -217,11 +398,105 @@ var CircuitBreakerManager = class {
|
|
|
217
398
|
}
|
|
218
399
|
};
|
|
219
400
|
|
|
401
|
+
// src/internal/FetchRateLimiter.ts
|
|
402
|
+
var FetchRateLimiter = class {
|
|
403
|
+
active = 0;
|
|
404
|
+
queue = [];
|
|
405
|
+
startedAt = [];
|
|
406
|
+
drainTimer;
|
|
407
|
+
async schedule(options, task) {
|
|
408
|
+
if (!options) {
|
|
409
|
+
return task();
|
|
410
|
+
}
|
|
411
|
+
const normalized = this.normalize(options);
|
|
412
|
+
if (!normalized) {
|
|
413
|
+
return task();
|
|
414
|
+
}
|
|
415
|
+
return new Promise((resolve, reject) => {
|
|
416
|
+
this.queue.push({ options: normalized, task, resolve, reject });
|
|
417
|
+
this.drain();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
normalize(options) {
|
|
421
|
+
const maxConcurrent = options.maxConcurrent;
|
|
422
|
+
const intervalMs = options.intervalMs;
|
|
423
|
+
const maxPerInterval = options.maxPerInterval;
|
|
424
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
425
|
+
return void 0;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
maxConcurrent,
|
|
429
|
+
intervalMs,
|
|
430
|
+
maxPerInterval
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
drain() {
|
|
434
|
+
if (this.drainTimer) {
|
|
435
|
+
clearTimeout(this.drainTimer);
|
|
436
|
+
this.drainTimer = void 0;
|
|
437
|
+
}
|
|
438
|
+
while (this.queue.length > 0) {
|
|
439
|
+
const next = this.queue[0];
|
|
440
|
+
if (!next) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const waitMs = this.waitTime(next.options);
|
|
444
|
+
if (waitMs > 0) {
|
|
445
|
+
this.drainTimer = setTimeout(() => {
|
|
446
|
+
this.drainTimer = void 0;
|
|
447
|
+
this.drain();
|
|
448
|
+
}, waitMs);
|
|
449
|
+
this.drainTimer.unref?.();
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.queue.shift();
|
|
453
|
+
this.active += 1;
|
|
454
|
+
this.startedAt.push(Date.now());
|
|
455
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
456
|
+
this.active -= 1;
|
|
457
|
+
this.drain();
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
waitTime(options) {
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
if (options.maxConcurrent && this.active >= options.maxConcurrent) {
|
|
464
|
+
return 1;
|
|
465
|
+
}
|
|
466
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
this.prune(now, options.intervalMs);
|
|
470
|
+
if (this.startedAt.length < options.maxPerInterval) {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
const oldest = this.startedAt[0];
|
|
474
|
+
if (!oldest) {
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
478
|
+
}
|
|
479
|
+
prune(now, intervalMs) {
|
|
480
|
+
while (this.startedAt.length > 0) {
|
|
481
|
+
const startedAt = this.startedAt[0];
|
|
482
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
483
|
+
break;
|
|
484
|
+
}
|
|
485
|
+
this.startedAt.shift();
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
220
490
|
// src/internal/MetricsCollector.ts
|
|
221
491
|
var MetricsCollector = class {
|
|
222
492
|
data = this.empty();
|
|
223
493
|
get snapshot() {
|
|
224
|
-
return {
|
|
494
|
+
return {
|
|
495
|
+
...this.data,
|
|
496
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
497
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
498
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
499
|
+
};
|
|
225
500
|
}
|
|
226
501
|
increment(field, amount = 1) {
|
|
227
502
|
;
|
|
@@ -230,6 +505,22 @@ var MetricsCollector = class {
|
|
|
230
505
|
incrementLayer(map, layerName) {
|
|
231
506
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
232
507
|
}
|
|
508
|
+
/**
|
|
509
|
+
* Records a read latency sample for the given layer.
|
|
510
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
511
|
+
*/
|
|
512
|
+
recordLatency(layerName, durationMs) {
|
|
513
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
514
|
+
if (!existing) {
|
|
515
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
existing.count += 1;
|
|
519
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
520
|
+
if (durationMs > existing.maxMs) {
|
|
521
|
+
existing.maxMs = durationMs;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
233
524
|
reset() {
|
|
234
525
|
this.data = this.empty();
|
|
235
526
|
}
|
|
@@ -264,6 +555,7 @@ var MetricsCollector = class {
|
|
|
264
555
|
degradedOperations: 0,
|
|
265
556
|
hitsByLayer: {},
|
|
266
557
|
missesByLayer: {},
|
|
558
|
+
latencyByLayer: {},
|
|
267
559
|
resetAt: Date.now()
|
|
268
560
|
};
|
|
269
561
|
}
|
|
@@ -391,13 +683,14 @@ var TtlResolver = class {
|
|
|
391
683
|
clearProfiles() {
|
|
392
684
|
this.accessProfiles.clear();
|
|
393
685
|
}
|
|
394
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
686
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
687
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
395
688
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
396
689
|
layerName,
|
|
397
690
|
options?.negativeTtl,
|
|
398
691
|
globalNegativeTtl,
|
|
399
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
400
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
692
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
693
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
401
694
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
402
695
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
403
696
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -436,6 +729,29 @@ var TtlResolver = class {
|
|
|
436
729
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
437
730
|
return Math.max(1, Math.round(ttl + delta));
|
|
438
731
|
}
|
|
732
|
+
resolvePolicyTtl(key, value, policy) {
|
|
733
|
+
if (!policy) {
|
|
734
|
+
return void 0;
|
|
735
|
+
}
|
|
736
|
+
if (typeof policy === "function") {
|
|
737
|
+
return policy({ key, value });
|
|
738
|
+
}
|
|
739
|
+
const now = /* @__PURE__ */ new Date();
|
|
740
|
+
if (policy === "until-midnight") {
|
|
741
|
+
const nextMidnight = new Date(now);
|
|
742
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
743
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
744
|
+
}
|
|
745
|
+
if (policy === "next-hour") {
|
|
746
|
+
const nextHour = new Date(now);
|
|
747
|
+
nextHour.setMinutes(60, 0, 0);
|
|
748
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
749
|
+
}
|
|
750
|
+
const alignToSeconds = policy.alignTo;
|
|
751
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
752
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
753
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
754
|
+
}
|
|
439
755
|
readLayerNumber(layerName, value) {
|
|
440
756
|
if (typeof value === "number") {
|
|
441
757
|
return value;
|
|
@@ -463,36 +779,46 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
463
779
|
/**
|
|
464
780
|
* Tests whether a glob-style pattern matches a value.
|
|
465
781
|
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
466
|
-
* Uses a
|
|
782
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
783
|
+
* quadratic memory usage on long patterns/keys.
|
|
467
784
|
*/
|
|
468
785
|
static matches(pattern, value) {
|
|
469
786
|
return _PatternMatcher.matchLinear(pattern, value);
|
|
470
787
|
}
|
|
471
788
|
/**
|
|
472
|
-
* Linear-time glob matching
|
|
473
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
789
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
474
790
|
*/
|
|
475
791
|
static matchLinear(pattern, value) {
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
792
|
+
let patternIndex = 0;
|
|
793
|
+
let valueIndex = 0;
|
|
794
|
+
let starIndex = -1;
|
|
795
|
+
let backtrackValueIndex = 0;
|
|
796
|
+
while (valueIndex < value.length) {
|
|
797
|
+
const patternChar = pattern[patternIndex];
|
|
798
|
+
const valueChar = value[valueIndex];
|
|
799
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
800
|
+
starIndex = patternIndex;
|
|
801
|
+
patternIndex += 1;
|
|
802
|
+
backtrackValueIndex = valueIndex;
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
805
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
806
|
+
patternIndex += 1;
|
|
807
|
+
valueIndex += 1;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (starIndex !== -1) {
|
|
811
|
+
patternIndex = starIndex + 1;
|
|
812
|
+
backtrackValueIndex += 1;
|
|
813
|
+
valueIndex = backtrackValueIndex;
|
|
814
|
+
continue;
|
|
493
815
|
}
|
|
816
|
+
return false;
|
|
494
817
|
}
|
|
495
|
-
|
|
818
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
819
|
+
patternIndex += 1;
|
|
820
|
+
}
|
|
821
|
+
return patternIndex === pattern.length;
|
|
496
822
|
}
|
|
497
823
|
};
|
|
498
824
|
|
|
@@ -501,11 +827,17 @@ var TagIndex = class {
|
|
|
501
827
|
tagToKeys = /* @__PURE__ */ new Map();
|
|
502
828
|
keyToTags = /* @__PURE__ */ new Map();
|
|
503
829
|
knownKeys = /* @__PURE__ */ new Set();
|
|
830
|
+
maxKnownKeys;
|
|
831
|
+
constructor(options = {}) {
|
|
832
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
833
|
+
}
|
|
504
834
|
async touch(key) {
|
|
505
835
|
this.knownKeys.add(key);
|
|
836
|
+
this.pruneKnownKeysIfNeeded();
|
|
506
837
|
}
|
|
507
838
|
async track(key, tags) {
|
|
508
839
|
this.knownKeys.add(key);
|
|
840
|
+
this.pruneKnownKeysIfNeeded();
|
|
509
841
|
if (tags.length === 0) {
|
|
510
842
|
return;
|
|
511
843
|
}
|
|
@@ -524,6 +856,40 @@ var TagIndex = class {
|
|
|
524
856
|
}
|
|
525
857
|
}
|
|
526
858
|
async remove(key) {
|
|
859
|
+
this.removeKey(key);
|
|
860
|
+
}
|
|
861
|
+
async keysForTag(tag) {
|
|
862
|
+
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
863
|
+
}
|
|
864
|
+
async keysForPrefix(prefix) {
|
|
865
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
866
|
+
}
|
|
867
|
+
async tagsForKey(key) {
|
|
868
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
869
|
+
}
|
|
870
|
+
async matchPattern(pattern) {
|
|
871
|
+
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
872
|
+
}
|
|
873
|
+
async clear() {
|
|
874
|
+
this.tagToKeys.clear();
|
|
875
|
+
this.keyToTags.clear();
|
|
876
|
+
this.knownKeys.clear();
|
|
877
|
+
}
|
|
878
|
+
pruneKnownKeysIfNeeded() {
|
|
879
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
883
|
+
let removed = 0;
|
|
884
|
+
for (const key of this.knownKeys) {
|
|
885
|
+
if (removed >= toRemove) {
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
this.removeKey(key);
|
|
889
|
+
removed += 1;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
removeKey(key) {
|
|
527
893
|
this.knownKeys.delete(key);
|
|
528
894
|
const tags = this.keyToTags.get(key);
|
|
529
895
|
if (!tags) {
|
|
@@ -541,21 +907,10 @@ var TagIndex = class {
|
|
|
541
907
|
}
|
|
542
908
|
this.keyToTags.delete(key);
|
|
543
909
|
}
|
|
544
|
-
async keysForTag(tag) {
|
|
545
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
546
|
-
}
|
|
547
|
-
async matchPattern(pattern) {
|
|
548
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
549
|
-
}
|
|
550
|
-
async clear() {
|
|
551
|
-
this.tagToKeys.clear();
|
|
552
|
-
this.keyToTags.clear();
|
|
553
|
-
this.knownKeys.clear();
|
|
554
|
-
}
|
|
555
910
|
};
|
|
556
911
|
|
|
557
912
|
// src/stampede/StampedeGuard.ts
|
|
558
|
-
var
|
|
913
|
+
var import_async_mutex2 = require("async-mutex");
|
|
559
914
|
var StampedeGuard = class {
|
|
560
915
|
mutexes = /* @__PURE__ */ new Map();
|
|
561
916
|
async execute(key, task) {
|
|
@@ -572,7 +927,7 @@ var StampedeGuard = class {
|
|
|
572
927
|
getMutexEntry(key) {
|
|
573
928
|
let entry = this.mutexes.get(key);
|
|
574
929
|
if (!entry) {
|
|
575
|
-
entry = { mutex: new
|
|
930
|
+
entry = { mutex: new import_async_mutex2.Mutex(), references: 0 };
|
|
576
931
|
this.mutexes.set(key, entry);
|
|
577
932
|
}
|
|
578
933
|
entry.references += 1;
|
|
@@ -580,6 +935,16 @@ var StampedeGuard = class {
|
|
|
580
935
|
}
|
|
581
936
|
};
|
|
582
937
|
|
|
938
|
+
// src/types.ts
|
|
939
|
+
var CacheMissError = class extends Error {
|
|
940
|
+
key;
|
|
941
|
+
constructor(key) {
|
|
942
|
+
super(`Cache miss for key "${key}".`);
|
|
943
|
+
this.name = "CacheMissError";
|
|
944
|
+
this.key = key;
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
583
948
|
// src/CacheStack.ts
|
|
584
949
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
585
950
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
@@ -623,6 +988,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
623
988
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
624
989
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
625
990
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
991
|
+
this.currentGeneration = options.generation;
|
|
626
992
|
if (options.publishSetInvalidation !== void 0) {
|
|
627
993
|
console.warn(
|
|
628
994
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -631,21 +997,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
631
997
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
632
998
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
633
999
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1000
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
634
1001
|
this.startup = this.initialize();
|
|
635
1002
|
}
|
|
636
1003
|
layers;
|
|
637
1004
|
options;
|
|
638
1005
|
stampedeGuard = new StampedeGuard();
|
|
639
1006
|
metricsCollector = new MetricsCollector();
|
|
640
|
-
instanceId = (
|
|
1007
|
+
instanceId = createInstanceId();
|
|
641
1008
|
startup;
|
|
642
1009
|
unsubscribeInvalidation;
|
|
643
1010
|
logger;
|
|
644
1011
|
tagIndex;
|
|
1012
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
645
1013
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
646
1014
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
647
1015
|
ttlResolver;
|
|
648
1016
|
circuitBreakerManager;
|
|
1017
|
+
currentGeneration;
|
|
1018
|
+
writeBehindQueue = [];
|
|
1019
|
+
writeBehindTimer;
|
|
1020
|
+
writeBehindFlushPromise;
|
|
649
1021
|
isDisconnecting = false;
|
|
650
1022
|
disconnectPromise;
|
|
651
1023
|
/**
|
|
@@ -655,9 +1027,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
655
1027
|
* and no `fetcher` is provided.
|
|
656
1028
|
*/
|
|
657
1029
|
async get(key, fetcher, options) {
|
|
658
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1030
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
659
1031
|
this.validateWriteOptions(options);
|
|
660
|
-
await this.
|
|
1032
|
+
await this.awaitStartup("get");
|
|
661
1033
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
662
1034
|
if (hit.found) {
|
|
663
1035
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -706,12 +1078,24 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
706
1078
|
async getOrSet(key, fetcher, options) {
|
|
707
1079
|
return this.get(key, fetcher, options);
|
|
708
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
1083
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
1084
|
+
* return non-null.
|
|
1085
|
+
*/
|
|
1086
|
+
async getOrThrow(key, fetcher, options) {
|
|
1087
|
+
const value = await this.get(key, fetcher, options);
|
|
1088
|
+
if (value === null) {
|
|
1089
|
+
throw new CacheMissError(key);
|
|
1090
|
+
}
|
|
1091
|
+
return value;
|
|
1092
|
+
}
|
|
709
1093
|
/**
|
|
710
1094
|
* Returns true if the given key exists and is not expired in any layer.
|
|
711
1095
|
*/
|
|
712
1096
|
async has(key) {
|
|
713
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
714
|
-
await this.
|
|
1097
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1098
|
+
await this.awaitStartup("has");
|
|
715
1099
|
for (const layer of this.layers) {
|
|
716
1100
|
if (this.shouldSkipLayer(layer)) {
|
|
717
1101
|
continue;
|
|
@@ -741,8 +1125,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
741
1125
|
* that has it, or null if the key is not found / has no TTL.
|
|
742
1126
|
*/
|
|
743
1127
|
async ttl(key) {
|
|
744
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
745
|
-
await this.
|
|
1128
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1129
|
+
await this.awaitStartup("ttl");
|
|
746
1130
|
for (const layer of this.layers) {
|
|
747
1131
|
if (this.shouldSkipLayer(layer)) {
|
|
748
1132
|
continue;
|
|
@@ -763,17 +1147,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
763
1147
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
764
1148
|
*/
|
|
765
1149
|
async set(key, value, options) {
|
|
766
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1150
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
767
1151
|
this.validateWriteOptions(options);
|
|
768
|
-
await this.
|
|
1152
|
+
await this.awaitStartup("set");
|
|
769
1153
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
770
1154
|
}
|
|
771
1155
|
/**
|
|
772
1156
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
773
1157
|
*/
|
|
774
1158
|
async delete(key) {
|
|
775
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
776
|
-
await this.
|
|
1159
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1160
|
+
await this.awaitStartup("delete");
|
|
777
1161
|
await this.deleteKeys([normalizedKey]);
|
|
778
1162
|
await this.publishInvalidation({
|
|
779
1163
|
scope: "key",
|
|
@@ -783,7 +1167,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
783
1167
|
});
|
|
784
1168
|
}
|
|
785
1169
|
async clear() {
|
|
786
|
-
await this.
|
|
1170
|
+
await this.awaitStartup("clear");
|
|
787
1171
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
788
1172
|
await this.tagIndex.clear();
|
|
789
1173
|
this.ttlResolver.clearProfiles();
|
|
@@ -799,23 +1183,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
799
1183
|
if (keys.length === 0) {
|
|
800
1184
|
return;
|
|
801
1185
|
}
|
|
802
|
-
await this.
|
|
1186
|
+
await this.awaitStartup("mdelete");
|
|
803
1187
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
804
|
-
|
|
1188
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1189
|
+
await this.deleteKeys(cacheKeys);
|
|
805
1190
|
await this.publishInvalidation({
|
|
806
1191
|
scope: "keys",
|
|
807
|
-
keys:
|
|
1192
|
+
keys: cacheKeys,
|
|
808
1193
|
sourceId: this.instanceId,
|
|
809
1194
|
operation: "delete"
|
|
810
1195
|
});
|
|
811
1196
|
}
|
|
812
1197
|
async mget(entries) {
|
|
1198
|
+
this.assertActive("mget");
|
|
813
1199
|
if (entries.length === 0) {
|
|
814
1200
|
return [];
|
|
815
1201
|
}
|
|
816
1202
|
const normalizedEntries = entries.map((entry) => ({
|
|
817
1203
|
...entry,
|
|
818
|
-
key: this.validateCacheKey(entry.key)
|
|
1204
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
819
1205
|
}));
|
|
820
1206
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
821
1207
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -841,7 +1227,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
841
1227
|
})
|
|
842
1228
|
);
|
|
843
1229
|
}
|
|
844
|
-
await this.
|
|
1230
|
+
await this.awaitStartup("mget");
|
|
845
1231
|
const pending = /* @__PURE__ */ new Set();
|
|
846
1232
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
847
1233
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -889,14 +1275,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
889
1275
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
890
1276
|
}
|
|
891
1277
|
async mset(entries) {
|
|
1278
|
+
this.assertActive("mset");
|
|
892
1279
|
const normalizedEntries = entries.map((entry) => ({
|
|
893
1280
|
...entry,
|
|
894
|
-
key: this.validateCacheKey(entry.key)
|
|
1281
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
895
1282
|
}));
|
|
896
1283
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
897
|
-
await
|
|
1284
|
+
await this.awaitStartup("mset");
|
|
1285
|
+
await this.writeBatch(normalizedEntries);
|
|
898
1286
|
}
|
|
899
1287
|
async warm(entries, options = {}) {
|
|
1288
|
+
this.assertActive("warm");
|
|
900
1289
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
901
1290
|
const total = entries.length;
|
|
902
1291
|
let completed = 0;
|
|
@@ -945,14 +1334,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
945
1334
|
return new CacheNamespace(this, prefix);
|
|
946
1335
|
}
|
|
947
1336
|
async invalidateByTag(tag) {
|
|
948
|
-
await this.
|
|
1337
|
+
await this.awaitStartup("invalidateByTag");
|
|
949
1338
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
950
1339
|
await this.deleteKeys(keys);
|
|
951
1340
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
952
1341
|
}
|
|
1342
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1343
|
+
if (tags.length === 0) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
await this.awaitStartup("invalidateByTags");
|
|
1347
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1348
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1349
|
+
await this.deleteKeys(keys);
|
|
1350
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1351
|
+
}
|
|
953
1352
|
async invalidateByPattern(pattern) {
|
|
954
|
-
await this.
|
|
955
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1353
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1354
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1355
|
+
await this.deleteKeys(keys);
|
|
1356
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1357
|
+
}
|
|
1358
|
+
async invalidateByPrefix(prefix) {
|
|
1359
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1360
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1361
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
956
1362
|
await this.deleteKeys(keys);
|
|
957
1363
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
958
1364
|
}
|
|
@@ -979,8 +1385,77 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
979
1385
|
getHitRate() {
|
|
980
1386
|
return this.metricsCollector.hitRate();
|
|
981
1387
|
}
|
|
982
|
-
async
|
|
1388
|
+
async healthCheck() {
|
|
983
1389
|
await this.startup;
|
|
1390
|
+
return Promise.all(
|
|
1391
|
+
this.layers.map(async (layer) => {
|
|
1392
|
+
const startedAt = performance.now();
|
|
1393
|
+
try {
|
|
1394
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1395
|
+
return {
|
|
1396
|
+
layer: layer.name,
|
|
1397
|
+
healthy,
|
|
1398
|
+
latencyMs: performance.now() - startedAt
|
|
1399
|
+
};
|
|
1400
|
+
} catch (error) {
|
|
1401
|
+
return {
|
|
1402
|
+
layer: layer.name,
|
|
1403
|
+
healthy: false,
|
|
1404
|
+
latencyMs: performance.now() - startedAt,
|
|
1405
|
+
error: this.formatError(error)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
})
|
|
1409
|
+
);
|
|
1410
|
+
}
|
|
1411
|
+
bumpGeneration(nextGeneration) {
|
|
1412
|
+
const current = this.currentGeneration ?? 0;
|
|
1413
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1414
|
+
return this.currentGeneration;
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1418
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1419
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1420
|
+
*/
|
|
1421
|
+
async inspect(key) {
|
|
1422
|
+
const userKey = this.validateCacheKey(key);
|
|
1423
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1424
|
+
await this.awaitStartup("inspect");
|
|
1425
|
+
const foundInLayers = [];
|
|
1426
|
+
let freshTtlSeconds = null;
|
|
1427
|
+
let staleTtlSeconds = null;
|
|
1428
|
+
let errorTtlSeconds = null;
|
|
1429
|
+
let isStale = false;
|
|
1430
|
+
for (const layer of this.layers) {
|
|
1431
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1435
|
+
if (stored === null) {
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
const resolved = resolveStoredValue(stored);
|
|
1439
|
+
if (resolved.state === "expired") {
|
|
1440
|
+
continue;
|
|
1441
|
+
}
|
|
1442
|
+
foundInLayers.push(layer.name);
|
|
1443
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1444
|
+
const now = Date.now();
|
|
1445
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1446
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1447
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1448
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (foundInLayers.length === 0) {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1454
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1455
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1456
|
+
}
|
|
1457
|
+
async exportState() {
|
|
1458
|
+
await this.awaitStartup("exportState");
|
|
984
1459
|
const exported = /* @__PURE__ */ new Map();
|
|
985
1460
|
for (const layer of this.layers) {
|
|
986
1461
|
if (!layer.keys) {
|
|
@@ -988,15 +1463,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
988
1463
|
}
|
|
989
1464
|
const keys = await layer.keys();
|
|
990
1465
|
for (const key of keys) {
|
|
991
|
-
|
|
1466
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1467
|
+
if (exported.has(exportedKey)) {
|
|
992
1468
|
continue;
|
|
993
1469
|
}
|
|
994
1470
|
const stored = await this.readLayerEntry(layer, key);
|
|
995
1471
|
if (stored === null) {
|
|
996
1472
|
continue;
|
|
997
1473
|
}
|
|
998
|
-
exported.set(
|
|
999
|
-
key,
|
|
1474
|
+
exported.set(exportedKey, {
|
|
1475
|
+
key: exportedKey,
|
|
1000
1476
|
value: stored,
|
|
1001
1477
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1002
1478
|
});
|
|
@@ -1005,20 +1481,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1005
1481
|
return [...exported.values()];
|
|
1006
1482
|
}
|
|
1007
1483
|
async importState(entries) {
|
|
1008
|
-
await this.
|
|
1484
|
+
await this.awaitStartup("importState");
|
|
1009
1485
|
await Promise.all(
|
|
1010
1486
|
entries.map(async (entry) => {
|
|
1011
|
-
|
|
1012
|
-
await this.
|
|
1487
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1488
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1489
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1013
1490
|
})
|
|
1014
1491
|
);
|
|
1015
1492
|
}
|
|
1016
1493
|
async persistToFile(filePath) {
|
|
1494
|
+
this.assertActive("persistToFile");
|
|
1017
1495
|
const snapshot = await this.exportState();
|
|
1018
|
-
|
|
1496
|
+
const { promises: fs2 } = await import("fs");
|
|
1497
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1019
1498
|
}
|
|
1020
1499
|
async restoreFromFile(filePath) {
|
|
1021
|
-
|
|
1500
|
+
this.assertActive("restoreFromFile");
|
|
1501
|
+
const { promises: fs2 } = await import("fs");
|
|
1502
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
1022
1503
|
let parsed;
|
|
1023
1504
|
try {
|
|
1024
1505
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1041,7 +1522,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1041
1522
|
this.disconnectPromise = (async () => {
|
|
1042
1523
|
await this.startup;
|
|
1043
1524
|
await this.unsubscribeInvalidation?.();
|
|
1525
|
+
await this.flushWriteBehindQueue();
|
|
1044
1526
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1527
|
+
if (this.writeBehindTimer) {
|
|
1528
|
+
clearInterval(this.writeBehindTimer);
|
|
1529
|
+
this.writeBehindTimer = void 0;
|
|
1530
|
+
}
|
|
1531
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1045
1532
|
})();
|
|
1046
1533
|
}
|
|
1047
1534
|
await this.disconnectPromise;
|
|
@@ -1101,7 +1588,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1101
1588
|
const fetchStart = Date.now();
|
|
1102
1589
|
let fetched;
|
|
1103
1590
|
try {
|
|
1104
|
-
fetched = await
|
|
1591
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1592
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1593
|
+
fetcher
|
|
1594
|
+
);
|
|
1105
1595
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1106
1596
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1107
1597
|
} catch (error) {
|
|
@@ -1115,6 +1605,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1115
1605
|
await this.storeEntry(key, "empty", null, options);
|
|
1116
1606
|
return null;
|
|
1117
1607
|
}
|
|
1608
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1609
|
+
return fetched;
|
|
1610
|
+
}
|
|
1118
1611
|
await this.storeEntry(key, "value", fetched, options);
|
|
1119
1612
|
return fetched;
|
|
1120
1613
|
}
|
|
@@ -1132,12 +1625,70 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1132
1625
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1133
1626
|
}
|
|
1134
1627
|
}
|
|
1628
|
+
async writeBatch(entries) {
|
|
1629
|
+
const now = Date.now();
|
|
1630
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1631
|
+
const immediateOperations = [];
|
|
1632
|
+
const deferredOperations = [];
|
|
1633
|
+
for (const entry of entries) {
|
|
1634
|
+
for (const layer of this.layers) {
|
|
1635
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1636
|
+
continue;
|
|
1637
|
+
}
|
|
1638
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1639
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1640
|
+
bucket.push(layerEntry);
|
|
1641
|
+
entriesByLayer.set(layer, bucket);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1645
|
+
const operation = async () => {
|
|
1646
|
+
try {
|
|
1647
|
+
if (layer.setMany) {
|
|
1648
|
+
await layer.setMany(layerEntries);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1657
|
+
deferredOperations.push(operation);
|
|
1658
|
+
} else {
|
|
1659
|
+
immediateOperations.push(operation);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1663
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1664
|
+
for (const entry of entries) {
|
|
1665
|
+
if (entry.options?.tags) {
|
|
1666
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1667
|
+
} else {
|
|
1668
|
+
await this.tagIndex.touch(entry.key);
|
|
1669
|
+
}
|
|
1670
|
+
this.metricsCollector.increment("sets");
|
|
1671
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1672
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1673
|
+
}
|
|
1674
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1675
|
+
await this.publishInvalidation({
|
|
1676
|
+
scope: "keys",
|
|
1677
|
+
keys: entries.map((entry) => entry.key),
|
|
1678
|
+
sourceId: this.instanceId,
|
|
1679
|
+
operation: "write"
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1135
1683
|
async readFromLayers(key, options, mode) {
|
|
1136
1684
|
let sawRetainableValue = false;
|
|
1137
1685
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
1138
1686
|
const layer = this.layers[index];
|
|
1139
1687
|
if (!layer) continue;
|
|
1688
|
+
const readStart = performance.now();
|
|
1140
1689
|
const stored = await this.readLayerEntry(layer, key);
|
|
1690
|
+
const readDuration = performance.now() - readStart;
|
|
1691
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1141
1692
|
if (stored === null) {
|
|
1142
1693
|
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1143
1694
|
continue;
|
|
@@ -1212,33 +1763,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1212
1763
|
}
|
|
1213
1764
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1214
1765
|
const now = Date.now();
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
options
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
});
|
|
1234
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1235
|
-
try {
|
|
1236
|
-
await layer.set(key, payload, ttl);
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1766
|
+
const immediateOperations = [];
|
|
1767
|
+
const deferredOperations = [];
|
|
1768
|
+
for (const layer of this.layers) {
|
|
1769
|
+
const operation = async () => {
|
|
1770
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1774
|
+
try {
|
|
1775
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1776
|
+
} catch (error) {
|
|
1777
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1781
|
+
deferredOperations.push(operation);
|
|
1782
|
+
} else {
|
|
1783
|
+
immediateOperations.push(operation);
|
|
1239
1784
|
}
|
|
1240
|
-
}
|
|
1241
|
-
await this.executeLayerOperations(
|
|
1785
|
+
}
|
|
1786
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1787
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1242
1788
|
}
|
|
1243
1789
|
async executeLayerOperations(operations, context) {
|
|
1244
1790
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1262,8 +1808,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1262
1808
|
);
|
|
1263
1809
|
}
|
|
1264
1810
|
}
|
|
1265
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1266
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1811
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1812
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1813
|
+
key,
|
|
1814
|
+
layerName,
|
|
1815
|
+
kind,
|
|
1816
|
+
options,
|
|
1817
|
+
fallbackTtl,
|
|
1818
|
+
this.options.negativeTtl,
|
|
1819
|
+
void 0,
|
|
1820
|
+
value
|
|
1821
|
+
);
|
|
1267
1822
|
}
|
|
1268
1823
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1269
1824
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1339,6 +1894,12 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1339
1894
|
}
|
|
1340
1895
|
}
|
|
1341
1896
|
}
|
|
1897
|
+
async getTagsForKey(key) {
|
|
1898
|
+
if (this.tagIndex.tagsForKey) {
|
|
1899
|
+
return this.tagIndex.tagsForKey(key);
|
|
1900
|
+
}
|
|
1901
|
+
return [];
|
|
1902
|
+
}
|
|
1342
1903
|
formatError(error) {
|
|
1343
1904
|
if (error instanceof Error) {
|
|
1344
1905
|
return error.message;
|
|
@@ -1351,6 +1912,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1351
1912
|
shouldBroadcastL1Invalidation() {
|
|
1352
1913
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1353
1914
|
}
|
|
1915
|
+
initializeWriteBehind(options) {
|
|
1916
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1920
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1924
|
+
void this.flushWriteBehindQueue();
|
|
1925
|
+
}, flushIntervalMs);
|
|
1926
|
+
this.writeBehindTimer.unref?.();
|
|
1927
|
+
}
|
|
1928
|
+
shouldWriteBehind(layer) {
|
|
1929
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
1930
|
+
}
|
|
1931
|
+
async enqueueWriteBehind(operation) {
|
|
1932
|
+
this.writeBehindQueue.push(operation);
|
|
1933
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1934
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
1935
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1936
|
+
await this.flushWriteBehindQueue();
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1940
|
+
await this.flushWriteBehindQueue();
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
async flushWriteBehindQueue() {
|
|
1944
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1945
|
+
await this.writeBehindFlushPromise;
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1949
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1950
|
+
this.writeBehindFlushPromise = (async () => {
|
|
1951
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
1952
|
+
})();
|
|
1953
|
+
await this.writeBehindFlushPromise;
|
|
1954
|
+
this.writeBehindFlushPromise = void 0;
|
|
1955
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1956
|
+
await this.flushWriteBehindQueue();
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
1960
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
1961
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
1962
|
+
layer.name,
|
|
1963
|
+
options?.staleWhileRevalidate,
|
|
1964
|
+
this.options.staleWhileRevalidate
|
|
1965
|
+
);
|
|
1966
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
1967
|
+
const payload = createStoredValueEnvelope({
|
|
1968
|
+
kind,
|
|
1969
|
+
value,
|
|
1970
|
+
freshTtlSeconds: freshTtl,
|
|
1971
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1972
|
+
staleIfErrorSeconds: staleIfError,
|
|
1973
|
+
now
|
|
1974
|
+
});
|
|
1975
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1976
|
+
return {
|
|
1977
|
+
key,
|
|
1978
|
+
value: payload,
|
|
1979
|
+
ttl
|
|
1980
|
+
};
|
|
1981
|
+
}
|
|
1982
|
+
intersectKeys(groups) {
|
|
1983
|
+
if (groups.length === 0) {
|
|
1984
|
+
return [];
|
|
1985
|
+
}
|
|
1986
|
+
const [firstGroup, ...rest] = groups;
|
|
1987
|
+
if (!firstGroup) {
|
|
1988
|
+
return [];
|
|
1989
|
+
}
|
|
1990
|
+
const restSets = rest.map((group) => new Set(group));
|
|
1991
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
1992
|
+
}
|
|
1993
|
+
qualifyKey(key) {
|
|
1994
|
+
const prefix = this.generationPrefix();
|
|
1995
|
+
return prefix ? `${prefix}${key}` : key;
|
|
1996
|
+
}
|
|
1997
|
+
qualifyPattern(pattern) {
|
|
1998
|
+
const prefix = this.generationPrefix();
|
|
1999
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2000
|
+
}
|
|
2001
|
+
stripQualifiedKey(key) {
|
|
2002
|
+
const prefix = this.generationPrefix();
|
|
2003
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2004
|
+
return key;
|
|
2005
|
+
}
|
|
2006
|
+
return key.slice(prefix.length);
|
|
2007
|
+
}
|
|
2008
|
+
generationPrefix() {
|
|
2009
|
+
if (this.currentGeneration === void 0) {
|
|
2010
|
+
return "";
|
|
2011
|
+
}
|
|
2012
|
+
return `v${this.currentGeneration}:`;
|
|
2013
|
+
}
|
|
1354
2014
|
async deleteKeysFromLayers(layers, keys) {
|
|
1355
2015
|
await Promise.all(
|
|
1356
2016
|
layers.map(async (layer) => {
|
|
@@ -1394,6 +2054,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1394
2054
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1395
2055
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1396
2056
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2057
|
+
if (this.options.generation !== void 0) {
|
|
2058
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2059
|
+
}
|
|
1397
2060
|
}
|
|
1398
2061
|
validateWriteOptions(options) {
|
|
1399
2062
|
if (!options) {
|
|
@@ -1405,6 +2068,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1405
2068
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1406
2069
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1407
2070
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2071
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1408
2072
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1409
2073
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1410
2074
|
}
|
|
@@ -1448,6 +2112,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1448
2112
|
}
|
|
1449
2113
|
return key;
|
|
1450
2114
|
}
|
|
2115
|
+
validateTtlPolicy(name, policy) {
|
|
2116
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if ("alignTo" in policy) {
|
|
2120
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
throw new Error(`${name} is invalid.`);
|
|
2124
|
+
}
|
|
2125
|
+
assertActive(operation) {
|
|
2126
|
+
if (this.isDisconnecting) {
|
|
2127
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async awaitStartup(operation) {
|
|
2131
|
+
this.assertActive(operation);
|
|
2132
|
+
await this.startup;
|
|
2133
|
+
this.assertActive(operation);
|
|
2134
|
+
}
|
|
1451
2135
|
serializeOptions(options) {
|
|
1452
2136
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1453
2137
|
}
|
|
@@ -1553,41 +2237,47 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1553
2237
|
return value;
|
|
1554
2238
|
}
|
|
1555
2239
|
};
|
|
2240
|
+
function createInstanceId() {
|
|
2241
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2242
|
+
}
|
|
1556
2243
|
|
|
1557
2244
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1558
2245
|
var RedisInvalidationBus = class {
|
|
1559
2246
|
channel;
|
|
1560
2247
|
publisher;
|
|
1561
2248
|
subscriber;
|
|
1562
|
-
|
|
2249
|
+
logger;
|
|
2250
|
+
handlers = /* @__PURE__ */ new Set();
|
|
2251
|
+
sharedListener;
|
|
1563
2252
|
constructor(options) {
|
|
1564
2253
|
this.publisher = options.publisher;
|
|
1565
2254
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1566
2255
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
2256
|
+
this.logger = options.logger;
|
|
1567
2257
|
}
|
|
1568
2258
|
async subscribe(handler) {
|
|
1569
|
-
if (this.
|
|
1570
|
-
|
|
2259
|
+
if (this.handlers.size === 0) {
|
|
2260
|
+
const listener = (_channel, payload) => {
|
|
2261
|
+
void this.dispatchToHandlers(payload);
|
|
2262
|
+
};
|
|
2263
|
+
this.sharedListener = listener;
|
|
2264
|
+
this.subscriber.on("message", listener);
|
|
2265
|
+
await this.subscriber.subscribe(this.channel);
|
|
1571
2266
|
}
|
|
1572
|
-
|
|
1573
|
-
void this.handleMessage(payload, handler);
|
|
1574
|
-
};
|
|
1575
|
-
this.activeListener = listener;
|
|
1576
|
-
this.subscriber.on("message", listener);
|
|
1577
|
-
await this.subscriber.subscribe(this.channel);
|
|
2267
|
+
this.handlers.add(handler);
|
|
1578
2268
|
return async () => {
|
|
1579
|
-
|
|
1580
|
-
|
|
2269
|
+
this.handlers.delete(handler);
|
|
2270
|
+
if (this.handlers.size === 0 && this.sharedListener) {
|
|
2271
|
+
this.subscriber.off("message", this.sharedListener);
|
|
2272
|
+
this.sharedListener = void 0;
|
|
2273
|
+
await this.subscriber.unsubscribe(this.channel);
|
|
1581
2274
|
}
|
|
1582
|
-
this.activeListener = void 0;
|
|
1583
|
-
this.subscriber.off("message", listener);
|
|
1584
|
-
await this.subscriber.unsubscribe(this.channel);
|
|
1585
2275
|
};
|
|
1586
2276
|
}
|
|
1587
2277
|
async publish(message) {
|
|
1588
2278
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
1589
2279
|
}
|
|
1590
|
-
async
|
|
2280
|
+
async dispatchToHandlers(payload) {
|
|
1591
2281
|
let message;
|
|
1592
2282
|
try {
|
|
1593
2283
|
const parsed = JSON.parse(payload);
|
|
@@ -1599,11 +2289,15 @@ var RedisInvalidationBus = class {
|
|
|
1599
2289
|
this.reportError("invalid invalidation payload", error);
|
|
1600
2290
|
return;
|
|
1601
2291
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2292
|
+
await Promise.all(
|
|
2293
|
+
[...this.handlers].map(async (handler) => {
|
|
2294
|
+
try {
|
|
2295
|
+
await handler(message);
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
this.reportError("invalidation handler failed", error);
|
|
2298
|
+
}
|
|
2299
|
+
})
|
|
2300
|
+
);
|
|
1607
2301
|
}
|
|
1608
2302
|
isInvalidationMessage(value) {
|
|
1609
2303
|
if (!value || typeof value !== "object") {
|
|
@@ -1616,6 +2310,10 @@ var RedisInvalidationBus = class {
|
|
|
1616
2310
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1617
2311
|
}
|
|
1618
2312
|
reportError(message, error) {
|
|
2313
|
+
if (this.logger?.error) {
|
|
2314
|
+
this.logger.error(message, { error });
|
|
2315
|
+
return;
|
|
2316
|
+
}
|
|
1619
2317
|
console.error(`[layercache] ${message}`, error);
|
|
1620
2318
|
}
|
|
1621
2319
|
};
|
|
@@ -1664,6 +2362,19 @@ var RedisTagIndex = class {
|
|
|
1664
2362
|
async keysForTag(tag) {
|
|
1665
2363
|
return this.client.smembers(this.tagKeysKey(tag));
|
|
1666
2364
|
}
|
|
2365
|
+
async keysForPrefix(prefix) {
|
|
2366
|
+
const matches = [];
|
|
2367
|
+
let cursor = "0";
|
|
2368
|
+
do {
|
|
2369
|
+
const [nextCursor, keys] = await this.client.sscan(this.knownKeysKey(), cursor, "COUNT", this.scanCount);
|
|
2370
|
+
cursor = nextCursor;
|
|
2371
|
+
matches.push(...keys.filter((key) => key.startsWith(prefix)));
|
|
2372
|
+
} while (cursor !== "0");
|
|
2373
|
+
return matches;
|
|
2374
|
+
}
|
|
2375
|
+
async tagsForKey(key) {
|
|
2376
|
+
return this.client.smembers(this.keyTagsKey(key));
|
|
2377
|
+
}
|
|
1667
2378
|
async matchPattern(pattern) {
|
|
1668
2379
|
const matches = [];
|
|
1669
2380
|
let cursor = "0";
|
|
@@ -1754,6 +2465,43 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1754
2465
|
};
|
|
1755
2466
|
}
|
|
1756
2467
|
|
|
2468
|
+
// src/integrations/express.ts
|
|
2469
|
+
function createExpressCacheMiddleware(cache, options = {}) {
|
|
2470
|
+
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
2471
|
+
return async (req, res, next) => {
|
|
2472
|
+
try {
|
|
2473
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2474
|
+
if (!allowedMethods.has(method)) {
|
|
2475
|
+
next();
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
2479
|
+
const cached = await cache.get(key, void 0, options);
|
|
2480
|
+
if (cached !== null) {
|
|
2481
|
+
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2482
|
+
res.setHeader?.("x-cache", "HIT");
|
|
2483
|
+
if (res.json) {
|
|
2484
|
+
res.json(cached);
|
|
2485
|
+
} else {
|
|
2486
|
+
res.end?.(JSON.stringify(cached));
|
|
2487
|
+
}
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
const originalJson = res.json?.bind(res);
|
|
2491
|
+
if (originalJson) {
|
|
2492
|
+
res.json = (body) => {
|
|
2493
|
+
res.setHeader?.("x-cache", "MISS");
|
|
2494
|
+
void cache.set(key, body, options);
|
|
2495
|
+
return originalJson(body);
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
next();
|
|
2499
|
+
} catch (error) {
|
|
2500
|
+
next(error);
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
|
|
1757
2505
|
// src/integrations/graphql.ts
|
|
1758
2506
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1759
2507
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
@@ -1763,6 +2511,95 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1763
2511
|
return (...args) => wrapped(...args);
|
|
1764
2512
|
}
|
|
1765
2513
|
|
|
2514
|
+
// src/integrations/hono.ts
|
|
2515
|
+
function createHonoCacheMiddleware(cache, options = {}) {
|
|
2516
|
+
const allowedMethods = new Set((options.methods ?? ["GET"]).map((method) => method.toUpperCase()));
|
|
2517
|
+
return async (context, next) => {
|
|
2518
|
+
const method = (context.req.method ?? "GET").toUpperCase();
|
|
2519
|
+
if (!allowedMethods.has(method)) {
|
|
2520
|
+
await next();
|
|
2521
|
+
return;
|
|
2522
|
+
}
|
|
2523
|
+
const key = options.keyResolver ? options.keyResolver(context.req) : `${method}:${context.req.path ?? context.req.url ?? "/"}`;
|
|
2524
|
+
const cached = await cache.get(key, void 0, options);
|
|
2525
|
+
if (cached !== null) {
|
|
2526
|
+
context.header?.("x-cache", "HIT");
|
|
2527
|
+
context.header?.("content-type", "application/json; charset=utf-8");
|
|
2528
|
+
context.json(cached);
|
|
2529
|
+
return;
|
|
2530
|
+
}
|
|
2531
|
+
const originalJson = context.json.bind(context);
|
|
2532
|
+
context.json = (body, status) => {
|
|
2533
|
+
context.header?.("x-cache", "MISS");
|
|
2534
|
+
void cache.set(key, body, options);
|
|
2535
|
+
return originalJson(body, status);
|
|
2536
|
+
};
|
|
2537
|
+
await next();
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
// src/integrations/opentelemetry.ts
|
|
2542
|
+
function createOpenTelemetryPlugin(cache, tracer) {
|
|
2543
|
+
const originals = {
|
|
2544
|
+
get: cache.get.bind(cache),
|
|
2545
|
+
set: cache.set.bind(cache),
|
|
2546
|
+
delete: cache.delete.bind(cache),
|
|
2547
|
+
mget: cache.mget.bind(cache),
|
|
2548
|
+
mset: cache.mset.bind(cache),
|
|
2549
|
+
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
2550
|
+
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
2551
|
+
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
2552
|
+
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
2553
|
+
};
|
|
2554
|
+
cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
|
|
2555
|
+
"layercache.key": String(args[0] ?? "")
|
|
2556
|
+
}));
|
|
2557
|
+
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
2558
|
+
"layercache.key": String(args[0] ?? "")
|
|
2559
|
+
}));
|
|
2560
|
+
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
2561
|
+
"layercache.key": String(args[0] ?? "")
|
|
2562
|
+
}));
|
|
2563
|
+
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
2564
|
+
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
2565
|
+
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
2566
|
+
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
2567
|
+
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
2568
|
+
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
2569
|
+
return {
|
|
2570
|
+
uninstall() {
|
|
2571
|
+
cache.get = originals.get;
|
|
2572
|
+
cache.set = originals.set;
|
|
2573
|
+
cache.delete = originals.delete;
|
|
2574
|
+
cache.mget = originals.mget;
|
|
2575
|
+
cache.mset = originals.mset;
|
|
2576
|
+
cache.invalidateByTag = originals.invalidateByTag;
|
|
2577
|
+
cache.invalidateByTags = originals.invalidateByTags;
|
|
2578
|
+
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
2579
|
+
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
}
|
|
2583
|
+
function instrument(name, tracer, method, attributes) {
|
|
2584
|
+
return (async (...args) => {
|
|
2585
|
+
const span = tracer.startSpan(name, { attributes: attributes?.(args) });
|
|
2586
|
+
try {
|
|
2587
|
+
const result = await method(...args);
|
|
2588
|
+
span.setAttribute?.("layercache.success", true);
|
|
2589
|
+
if (result === null) {
|
|
2590
|
+
span.setAttribute?.("layercache.result", "null");
|
|
2591
|
+
}
|
|
2592
|
+
return result;
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
span.setAttribute?.("layercache.success", false);
|
|
2595
|
+
span.recordException?.(error);
|
|
2596
|
+
throw error;
|
|
2597
|
+
} finally {
|
|
2598
|
+
span.end();
|
|
2599
|
+
}
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
|
|
1766
2603
|
// src/integrations/trpc.ts
|
|
1767
2604
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1768
2605
|
return async (context) => {
|
|
@@ -1795,12 +2632,21 @@ var MemoryLayer = class {
|
|
|
1795
2632
|
isLocal = true;
|
|
1796
2633
|
maxSize;
|
|
1797
2634
|
evictionPolicy;
|
|
2635
|
+
onEvict;
|
|
1798
2636
|
entries = /* @__PURE__ */ new Map();
|
|
2637
|
+
cleanupTimer;
|
|
1799
2638
|
constructor(options = {}) {
|
|
1800
2639
|
this.name = options.name ?? "memory";
|
|
1801
2640
|
this.defaultTtl = options.ttl;
|
|
1802
2641
|
this.maxSize = options.maxSize ?? 1e3;
|
|
1803
2642
|
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
2643
|
+
this.onEvict = options.onEvict;
|
|
2644
|
+
if (options.cleanupIntervalMs && options.cleanupIntervalMs > 0) {
|
|
2645
|
+
this.cleanupTimer = setInterval(() => {
|
|
2646
|
+
this.pruneExpired();
|
|
2647
|
+
}, options.cleanupIntervalMs);
|
|
2648
|
+
this.cleanupTimer.unref?.();
|
|
2649
|
+
}
|
|
1804
2650
|
}
|
|
1805
2651
|
async get(key) {
|
|
1806
2652
|
const value = await this.getEntry(key);
|
|
@@ -1817,10 +2663,10 @@ var MemoryLayer = class {
|
|
|
1817
2663
|
}
|
|
1818
2664
|
if (this.evictionPolicy === "lru") {
|
|
1819
2665
|
this.entries.delete(key);
|
|
1820
|
-
entry.
|
|
2666
|
+
entry.accessCount += 1;
|
|
1821
2667
|
this.entries.set(key, entry);
|
|
1822
|
-
} else {
|
|
1823
|
-
entry.
|
|
2668
|
+
} else if (this.evictionPolicy === "lfu") {
|
|
2669
|
+
entry.accessCount += 1;
|
|
1824
2670
|
}
|
|
1825
2671
|
return entry.value;
|
|
1826
2672
|
}
|
|
@@ -1831,12 +2677,17 @@ var MemoryLayer = class {
|
|
|
1831
2677
|
}
|
|
1832
2678
|
return values;
|
|
1833
2679
|
}
|
|
2680
|
+
async setMany(entries) {
|
|
2681
|
+
for (const entry of entries) {
|
|
2682
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
1834
2685
|
async set(key, value, ttl = this.defaultTtl) {
|
|
1835
2686
|
this.entries.delete(key);
|
|
1836
2687
|
this.entries.set(key, {
|
|
1837
2688
|
value,
|
|
1838
2689
|
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1839
|
-
|
|
2690
|
+
accessCount: 0,
|
|
1840
2691
|
insertedAt: Date.now()
|
|
1841
2692
|
});
|
|
1842
2693
|
while (this.entries.size > this.maxSize) {
|
|
@@ -1883,6 +2734,15 @@ var MemoryLayer = class {
|
|
|
1883
2734
|
async clear() {
|
|
1884
2735
|
this.entries.clear();
|
|
1885
2736
|
}
|
|
2737
|
+
async ping() {
|
|
2738
|
+
return true;
|
|
2739
|
+
}
|
|
2740
|
+
async dispose() {
|
|
2741
|
+
if (this.cleanupTimer) {
|
|
2742
|
+
clearInterval(this.cleanupTimer);
|
|
2743
|
+
this.cleanupTimer = void 0;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
1886
2746
|
async keys() {
|
|
1887
2747
|
this.pruneExpired();
|
|
1888
2748
|
return [...this.entries.keys()];
|
|
@@ -1903,7 +2763,7 @@ var MemoryLayer = class {
|
|
|
1903
2763
|
this.entries.set(entry.key, {
|
|
1904
2764
|
value: entry.value,
|
|
1905
2765
|
expiresAt: entry.expiresAt,
|
|
1906
|
-
|
|
2766
|
+
accessCount: 0,
|
|
1907
2767
|
insertedAt: Date.now()
|
|
1908
2768
|
});
|
|
1909
2769
|
}
|
|
@@ -1915,22 +2775,30 @@ var MemoryLayer = class {
|
|
|
1915
2775
|
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1916
2776
|
const oldestKey = this.entries.keys().next().value;
|
|
1917
2777
|
if (oldestKey !== void 0) {
|
|
2778
|
+
const entry = this.entries.get(oldestKey);
|
|
1918
2779
|
this.entries.delete(oldestKey);
|
|
2780
|
+
if (entry) {
|
|
2781
|
+
this.onEvict?.(oldestKey, unwrapStoredValue(entry.value));
|
|
2782
|
+
}
|
|
1919
2783
|
}
|
|
1920
2784
|
return;
|
|
1921
2785
|
}
|
|
1922
2786
|
let victimKey;
|
|
1923
|
-
let
|
|
2787
|
+
let minCount = Number.POSITIVE_INFINITY;
|
|
1924
2788
|
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1925
2789
|
for (const [key, entry] of this.entries.entries()) {
|
|
1926
|
-
if (entry.
|
|
1927
|
-
|
|
2790
|
+
if (entry.accessCount < minCount || entry.accessCount === minCount && entry.insertedAt < minInsertedAt) {
|
|
2791
|
+
minCount = entry.accessCount;
|
|
1928
2792
|
minInsertedAt = entry.insertedAt;
|
|
1929
2793
|
victimKey = key;
|
|
1930
2794
|
}
|
|
1931
2795
|
}
|
|
1932
2796
|
if (victimKey !== void 0) {
|
|
2797
|
+
const victim = this.entries.get(victimKey);
|
|
1933
2798
|
this.entries.delete(victimKey);
|
|
2799
|
+
if (victim) {
|
|
2800
|
+
this.onEvict?.(victimKey, unwrapStoredValue(victim.value));
|
|
2801
|
+
}
|
|
1934
2802
|
}
|
|
1935
2803
|
}
|
|
1936
2804
|
pruneExpired() {
|
|
@@ -1946,6 +2814,7 @@ var MemoryLayer = class {
|
|
|
1946
2814
|
};
|
|
1947
2815
|
|
|
1948
2816
|
// src/layers/RedisLayer.ts
|
|
2817
|
+
var import_node_util = require("util");
|
|
1949
2818
|
var import_node_zlib = require("zlib");
|
|
1950
2819
|
|
|
1951
2820
|
// src/serialization/JsonSerializer.ts
|
|
@@ -1961,27 +2830,33 @@ var JsonSerializer = class {
|
|
|
1961
2830
|
|
|
1962
2831
|
// src/layers/RedisLayer.ts
|
|
1963
2832
|
var BATCH_DELETE_SIZE = 500;
|
|
2833
|
+
var gzipAsync = (0, import_node_util.promisify)(import_node_zlib.gzip);
|
|
2834
|
+
var gunzipAsync = (0, import_node_util.promisify)(import_node_zlib.gunzip);
|
|
2835
|
+
var brotliCompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliCompress);
|
|
2836
|
+
var brotliDecompressAsync = (0, import_node_util.promisify)(import_node_zlib.brotliDecompress);
|
|
1964
2837
|
var RedisLayer = class {
|
|
1965
2838
|
name;
|
|
1966
2839
|
defaultTtl;
|
|
1967
2840
|
isLocal = false;
|
|
1968
2841
|
client;
|
|
1969
|
-
|
|
2842
|
+
serializers;
|
|
1970
2843
|
prefix;
|
|
1971
2844
|
allowUnprefixedClear;
|
|
1972
2845
|
scanCount;
|
|
1973
2846
|
compression;
|
|
1974
2847
|
compressionThreshold;
|
|
2848
|
+
disconnectOnDispose;
|
|
1975
2849
|
constructor(options) {
|
|
1976
2850
|
this.client = options.client;
|
|
1977
2851
|
this.defaultTtl = options.ttl;
|
|
1978
2852
|
this.name = options.name ?? "redis";
|
|
1979
|
-
this.
|
|
2853
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
1980
2854
|
this.prefix = options.prefix ?? "";
|
|
1981
2855
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
1982
2856
|
this.scanCount = options.scanCount ?? 100;
|
|
1983
2857
|
this.compression = options.compression;
|
|
1984
2858
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2859
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
1985
2860
|
}
|
|
1986
2861
|
async get(key) {
|
|
1987
2862
|
const payload = await this.getEntry(key);
|
|
@@ -2016,8 +2891,26 @@ var RedisLayer = class {
|
|
|
2016
2891
|
})
|
|
2017
2892
|
);
|
|
2018
2893
|
}
|
|
2894
|
+
async setMany(entries) {
|
|
2895
|
+
if (entries.length === 0) {
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
const pipeline = this.client.pipeline();
|
|
2899
|
+
for (const entry of entries) {
|
|
2900
|
+
const serialized = this.primarySerializer().serialize(entry.value);
|
|
2901
|
+
const payload = await this.encodePayload(serialized);
|
|
2902
|
+
const normalizedKey = this.withPrefix(entry.key);
|
|
2903
|
+
if (entry.ttl && entry.ttl > 0) {
|
|
2904
|
+
pipeline.set(normalizedKey, payload, "EX", entry.ttl);
|
|
2905
|
+
} else {
|
|
2906
|
+
pipeline.set(normalizedKey, payload);
|
|
2907
|
+
}
|
|
2908
|
+
}
|
|
2909
|
+
await pipeline.exec();
|
|
2910
|
+
}
|
|
2019
2911
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2020
|
-
const
|
|
2912
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2913
|
+
const payload = await this.encodePayload(serialized);
|
|
2021
2914
|
const normalizedKey = this.withPrefix(key);
|
|
2022
2915
|
if (ttl && ttl > 0) {
|
|
2023
2916
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -2049,6 +2942,18 @@ var RedisLayer = class {
|
|
|
2049
2942
|
const keys = await this.keys();
|
|
2050
2943
|
return keys.length;
|
|
2051
2944
|
}
|
|
2945
|
+
async ping() {
|
|
2946
|
+
try {
|
|
2947
|
+
return await this.client.ping() === "PONG";
|
|
2948
|
+
} catch {
|
|
2949
|
+
return false;
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
async dispose() {
|
|
2953
|
+
if (this.disconnectOnDispose) {
|
|
2954
|
+
this.client.disconnect();
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2052
2957
|
/**
|
|
2053
2958
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2054
2959
|
* loading millions of keys into memory at once.
|
|
@@ -2095,17 +3000,48 @@ var RedisLayer = class {
|
|
|
2095
3000
|
return `${this.prefix}${key}`;
|
|
2096
3001
|
}
|
|
2097
3002
|
async deserializeOrDelete(key, payload) {
|
|
3003
|
+
const decodedPayload = await this.decodePayload(payload);
|
|
3004
|
+
for (const serializer of this.serializers) {
|
|
3005
|
+
try {
|
|
3006
|
+
const value = serializer.deserialize(decodedPayload);
|
|
3007
|
+
if (serializer !== this.primarySerializer()) {
|
|
3008
|
+
await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
|
|
3009
|
+
}
|
|
3010
|
+
return value;
|
|
3011
|
+
} catch {
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
2098
3014
|
try {
|
|
2099
|
-
return this.serializer.deserialize(this.decodePayload(payload));
|
|
2100
|
-
} catch {
|
|
2101
3015
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2102
|
-
|
|
3016
|
+
} catch {
|
|
2103
3017
|
}
|
|
3018
|
+
return null;
|
|
3019
|
+
}
|
|
3020
|
+
async rewriteWithPrimarySerializer(key, value) {
|
|
3021
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
3022
|
+
const payload = await this.encodePayload(serialized);
|
|
3023
|
+
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
3024
|
+
if (ttl > 0) {
|
|
3025
|
+
await this.client.set(this.withPrefix(key), payload, "EX", ttl);
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
await this.client.set(this.withPrefix(key), payload);
|
|
3029
|
+
}
|
|
3030
|
+
primarySerializer() {
|
|
3031
|
+
const serializer = this.serializers[0];
|
|
3032
|
+
if (!serializer) {
|
|
3033
|
+
throw new Error("RedisLayer requires at least one serializer.");
|
|
3034
|
+
}
|
|
3035
|
+
return serializer;
|
|
2104
3036
|
}
|
|
2105
3037
|
isSerializablePayload(payload) {
|
|
2106
3038
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
2107
3039
|
}
|
|
2108
|
-
|
|
3040
|
+
/**
|
|
3041
|
+
* Compresses the payload asynchronously if compression is enabled and the
|
|
3042
|
+
* payload exceeds the threshold. This avoids blocking the event loop.
|
|
3043
|
+
*/
|
|
3044
|
+
async encodePayload(payload) {
|
|
2109
3045
|
if (!this.compression) {
|
|
2110
3046
|
return payload;
|
|
2111
3047
|
}
|
|
@@ -2114,26 +3050,29 @@ var RedisLayer = class {
|
|
|
2114
3050
|
return payload;
|
|
2115
3051
|
}
|
|
2116
3052
|
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
2117
|
-
const compressed = this.compression === "gzip" ?
|
|
3053
|
+
const compressed = this.compression === "gzip" ? await gzipAsync(source) : await brotliCompressAsync(source);
|
|
2118
3054
|
return Buffer.concat([header, compressed]);
|
|
2119
3055
|
}
|
|
2120
|
-
|
|
3056
|
+
/**
|
|
3057
|
+
* Decompresses the payload asynchronously if a compression header is present.
|
|
3058
|
+
*/
|
|
3059
|
+
async decodePayload(payload) {
|
|
2121
3060
|
if (!Buffer.isBuffer(payload)) {
|
|
2122
3061
|
return payload;
|
|
2123
3062
|
}
|
|
2124
3063
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
2125
|
-
return (
|
|
3064
|
+
return gunzipAsync(payload.subarray(10));
|
|
2126
3065
|
}
|
|
2127
3066
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
2128
|
-
return (
|
|
3067
|
+
return brotliDecompressAsync(payload.subarray(12));
|
|
2129
3068
|
}
|
|
2130
3069
|
return payload;
|
|
2131
3070
|
}
|
|
2132
3071
|
};
|
|
2133
3072
|
|
|
2134
3073
|
// src/layers/DiskLayer.ts
|
|
2135
|
-
var
|
|
2136
|
-
var
|
|
3074
|
+
var import_node_crypto = require("crypto");
|
|
3075
|
+
var import_node_fs = require("fs");
|
|
2137
3076
|
var import_node_path = require("path");
|
|
2138
3077
|
var DiskLayer = class {
|
|
2139
3078
|
name;
|
|
@@ -2141,11 +3080,14 @@ var DiskLayer = class {
|
|
|
2141
3080
|
isLocal = true;
|
|
2142
3081
|
directory;
|
|
2143
3082
|
serializer;
|
|
3083
|
+
maxFiles;
|
|
3084
|
+
writeQueue = Promise.resolve();
|
|
2144
3085
|
constructor(options) {
|
|
2145
3086
|
this.directory = options.directory;
|
|
2146
3087
|
this.defaultTtl = options.ttl;
|
|
2147
3088
|
this.name = options.name ?? "disk";
|
|
2148
3089
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
3090
|
+
this.maxFiles = options.maxFiles;
|
|
2149
3091
|
}
|
|
2150
3092
|
async get(key) {
|
|
2151
3093
|
return unwrapStoredValue(await this.getEntry(key));
|
|
@@ -2154,7 +3096,7 @@ var DiskLayer = class {
|
|
|
2154
3096
|
const filePath = this.keyToPath(key);
|
|
2155
3097
|
let raw;
|
|
2156
3098
|
try {
|
|
2157
|
-
raw = await
|
|
3099
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2158
3100
|
} catch {
|
|
2159
3101
|
return null;
|
|
2160
3102
|
}
|
|
@@ -2172,13 +3114,30 @@ var DiskLayer = class {
|
|
|
2172
3114
|
return entry.value;
|
|
2173
3115
|
}
|
|
2174
3116
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2175
|
-
await
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
3117
|
+
await this.enqueueWrite(async () => {
|
|
3118
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3119
|
+
const entry = {
|
|
3120
|
+
key,
|
|
3121
|
+
value,
|
|
3122
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
3123
|
+
};
|
|
3124
|
+
const payload = this.serializer.serialize(entry);
|
|
3125
|
+
const targetPath = this.keyToPath(key);
|
|
3126
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
3127
|
+
await import_node_fs.promises.writeFile(tempPath, payload);
|
|
3128
|
+
await import_node_fs.promises.rename(tempPath, targetPath);
|
|
3129
|
+
if (this.maxFiles !== void 0) {
|
|
3130
|
+
await this.enforceMaxFiles();
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
async getMany(keys) {
|
|
3135
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3136
|
+
}
|
|
3137
|
+
async setMany(entries) {
|
|
3138
|
+
for (const entry of entries) {
|
|
3139
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
3140
|
+
}
|
|
2182
3141
|
}
|
|
2183
3142
|
async has(key) {
|
|
2184
3143
|
const value = await this.getEntry(key);
|
|
@@ -2188,7 +3147,7 @@ var DiskLayer = class {
|
|
|
2188
3147
|
const filePath = this.keyToPath(key);
|
|
2189
3148
|
let raw;
|
|
2190
3149
|
try {
|
|
2191
|
-
raw = await
|
|
3150
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
2192
3151
|
} catch {
|
|
2193
3152
|
return null;
|
|
2194
3153
|
}
|
|
@@ -2208,44 +3167,124 @@ var DiskLayer = class {
|
|
|
2208
3167
|
return remaining;
|
|
2209
3168
|
}
|
|
2210
3169
|
async delete(key) {
|
|
2211
|
-
await this.safeDelete(this.keyToPath(key));
|
|
3170
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2212
3171
|
}
|
|
2213
3172
|
async deleteMany(keys) {
|
|
2214
|
-
await
|
|
3173
|
+
await this.enqueueWrite(async () => {
|
|
3174
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
3175
|
+
});
|
|
2215
3176
|
}
|
|
2216
3177
|
async clear() {
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
3178
|
+
await this.enqueueWrite(async () => {
|
|
3179
|
+
let entries;
|
|
3180
|
+
try {
|
|
3181
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
3182
|
+
} catch {
|
|
3183
|
+
return;
|
|
3184
|
+
}
|
|
3185
|
+
await Promise.all(
|
|
3186
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete((0, import_node_path.join)(this.directory, name)))
|
|
3187
|
+
);
|
|
3188
|
+
});
|
|
2226
3189
|
}
|
|
3190
|
+
/**
|
|
3191
|
+
* Returns the original cache key strings stored on disk.
|
|
3192
|
+
* Expired entries are skipped and cleaned up during the scan.
|
|
3193
|
+
*/
|
|
2227
3194
|
async keys() {
|
|
2228
3195
|
let entries;
|
|
2229
3196
|
try {
|
|
2230
|
-
entries = await
|
|
3197
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2231
3198
|
} catch {
|
|
2232
3199
|
return [];
|
|
2233
3200
|
}
|
|
2234
|
-
|
|
3201
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3202
|
+
const keys = [];
|
|
3203
|
+
await Promise.all(
|
|
3204
|
+
lcFiles.map(async (name) => {
|
|
3205
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
3206
|
+
let raw;
|
|
3207
|
+
try {
|
|
3208
|
+
raw = await import_node_fs.promises.readFile(filePath);
|
|
3209
|
+
} catch {
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
let entry;
|
|
3213
|
+
try {
|
|
3214
|
+
entry = this.serializer.deserialize(raw);
|
|
3215
|
+
} catch {
|
|
3216
|
+
await this.safeDelete(filePath);
|
|
3217
|
+
return;
|
|
3218
|
+
}
|
|
3219
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
3220
|
+
await this.safeDelete(filePath);
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
keys.push(entry.key);
|
|
3224
|
+
})
|
|
3225
|
+
);
|
|
3226
|
+
return keys;
|
|
2235
3227
|
}
|
|
2236
3228
|
async size() {
|
|
2237
3229
|
const keys = await this.keys();
|
|
2238
3230
|
return keys.length;
|
|
2239
3231
|
}
|
|
3232
|
+
async ping() {
|
|
3233
|
+
try {
|
|
3234
|
+
await import_node_fs.promises.mkdir(this.directory, { recursive: true });
|
|
3235
|
+
return true;
|
|
3236
|
+
} catch {
|
|
3237
|
+
return false;
|
|
3238
|
+
}
|
|
3239
|
+
}
|
|
3240
|
+
async dispose() {
|
|
3241
|
+
}
|
|
2240
3242
|
keyToPath(key) {
|
|
2241
|
-
const hash = (0,
|
|
3243
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(key).digest("hex");
|
|
2242
3244
|
return (0, import_node_path.join)(this.directory, `${hash}.lc`);
|
|
2243
3245
|
}
|
|
2244
3246
|
async safeDelete(filePath) {
|
|
2245
3247
|
try {
|
|
2246
|
-
await
|
|
3248
|
+
await import_node_fs.promises.unlink(filePath);
|
|
3249
|
+
} catch {
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
enqueueWrite(operation) {
|
|
3253
|
+
const next = this.writeQueue.then(operation, operation);
|
|
3254
|
+
this.writeQueue = next.catch(() => void 0);
|
|
3255
|
+
return next;
|
|
3256
|
+
}
|
|
3257
|
+
/**
|
|
3258
|
+
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
3259
|
+
*/
|
|
3260
|
+
async enforceMaxFiles() {
|
|
3261
|
+
if (this.maxFiles === void 0) {
|
|
3262
|
+
return;
|
|
3263
|
+
}
|
|
3264
|
+
let entries;
|
|
3265
|
+
try {
|
|
3266
|
+
entries = await import_node_fs.promises.readdir(this.directory);
|
|
2247
3267
|
} catch {
|
|
3268
|
+
return;
|
|
3269
|
+
}
|
|
3270
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
3271
|
+
if (lcFiles.length <= this.maxFiles) {
|
|
3272
|
+
return;
|
|
2248
3273
|
}
|
|
3274
|
+
const withStats = await Promise.all(
|
|
3275
|
+
lcFiles.map(async (name) => {
|
|
3276
|
+
const filePath = (0, import_node_path.join)(this.directory, name);
|
|
3277
|
+
try {
|
|
3278
|
+
const stat = await import_node_fs.promises.stat(filePath);
|
|
3279
|
+
return { filePath, mtimeMs: stat.mtimeMs };
|
|
3280
|
+
} catch {
|
|
3281
|
+
return { filePath, mtimeMs: 0 };
|
|
3282
|
+
}
|
|
3283
|
+
})
|
|
3284
|
+
);
|
|
3285
|
+
withStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
3286
|
+
const toEvict = withStats.slice(0, lcFiles.length - this.maxFiles);
|
|
3287
|
+
await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
|
|
2249
3288
|
}
|
|
2250
3289
|
};
|
|
2251
3290
|
|
|
@@ -2256,29 +3295,41 @@ var MemcachedLayer = class {
|
|
|
2256
3295
|
isLocal = false;
|
|
2257
3296
|
client;
|
|
2258
3297
|
keyPrefix;
|
|
3298
|
+
serializer;
|
|
2259
3299
|
constructor(options) {
|
|
2260
3300
|
this.client = options.client;
|
|
2261
3301
|
this.defaultTtl = options.ttl;
|
|
2262
3302
|
this.name = options.name ?? "memcached";
|
|
2263
3303
|
this.keyPrefix = options.keyPrefix ?? "";
|
|
3304
|
+
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2264
3305
|
}
|
|
2265
3306
|
async get(key) {
|
|
3307
|
+
return unwrapStoredValue(await this.getEntry(key));
|
|
3308
|
+
}
|
|
3309
|
+
async getEntry(key) {
|
|
2266
3310
|
const result = await this.client.get(this.withPrefix(key));
|
|
2267
3311
|
if (!result || result.value === null) {
|
|
2268
3312
|
return null;
|
|
2269
3313
|
}
|
|
2270
3314
|
try {
|
|
2271
|
-
return
|
|
3315
|
+
return this.serializer.deserialize(result.value);
|
|
2272
3316
|
} catch {
|
|
2273
3317
|
return null;
|
|
2274
3318
|
}
|
|
2275
3319
|
}
|
|
3320
|
+
async getMany(keys) {
|
|
3321
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
3322
|
+
}
|
|
2276
3323
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2277
|
-
const payload =
|
|
3324
|
+
const payload = this.serializer.serialize(value);
|
|
2278
3325
|
await this.client.set(this.withPrefix(key), payload, {
|
|
2279
3326
|
expires: ttl && ttl > 0 ? ttl : void 0
|
|
2280
3327
|
});
|
|
2281
3328
|
}
|
|
3329
|
+
async has(key) {
|
|
3330
|
+
const result = await this.client.get(this.withPrefix(key));
|
|
3331
|
+
return result !== null && result.value !== null;
|
|
3332
|
+
}
|
|
2282
3333
|
async delete(key) {
|
|
2283
3334
|
await this.client.delete(this.withPrefix(key));
|
|
2284
3335
|
}
|
|
@@ -2308,7 +3359,7 @@ var MsgpackSerializer = class {
|
|
|
2308
3359
|
};
|
|
2309
3360
|
|
|
2310
3361
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2311
|
-
var
|
|
3362
|
+
var import_node_crypto2 = require("crypto");
|
|
2312
3363
|
var RELEASE_SCRIPT = `
|
|
2313
3364
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2314
3365
|
return redis.call("del", KEYS[1])
|
|
@@ -2324,7 +3375,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2324
3375
|
}
|
|
2325
3376
|
async execute(key, options, worker, waiter) {
|
|
2326
3377
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2327
|
-
const token = (0,
|
|
3378
|
+
const token = (0, import_node_crypto2.randomUUID)();
|
|
2328
3379
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2329
3380
|
if (acquired === "OK") {
|
|
2330
3381
|
try {
|
|
@@ -2372,6 +3423,12 @@ function createPrometheusMetricsExporter(stacks) {
|
|
|
2372
3423
|
lines.push("# TYPE layercache_hits_by_layer_total counter");
|
|
2373
3424
|
lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
|
|
2374
3425
|
lines.push("# TYPE layercache_misses_by_layer_total counter");
|
|
3426
|
+
lines.push("# HELP layercache_layer_latency_avg_ms Average read latency per layer in milliseconds");
|
|
3427
|
+
lines.push("# TYPE layercache_layer_latency_avg_ms gauge");
|
|
3428
|
+
lines.push("# HELP layercache_layer_latency_max_ms Maximum read latency per layer in milliseconds");
|
|
3429
|
+
lines.push("# TYPE layercache_layer_latency_max_ms gauge");
|
|
3430
|
+
lines.push("# HELP layercache_layer_latency_count Number of read latency samples per layer");
|
|
3431
|
+
lines.push("# TYPE layercache_layer_latency_count counter");
|
|
2375
3432
|
for (const { stack, name } of entries) {
|
|
2376
3433
|
const m = stack.getMetrics();
|
|
2377
3434
|
const hr = stack.getHitRate();
|
|
@@ -2395,6 +3452,12 @@ function createPrometheusMetricsExporter(stacks) {
|
|
|
2395
3452
|
for (const [layerName, count] of Object.entries(m.missesByLayer)) {
|
|
2396
3453
|
lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2397
3454
|
}
|
|
3455
|
+
for (const [layerName, latency] of Object.entries(m.latencyByLayer)) {
|
|
3456
|
+
const layerLabel = `${label},layer="${sanitizeLabel(layerName)}"`;
|
|
3457
|
+
lines.push(`layercache_layer_latency_avg_ms{${layerLabel}} ${latency.avgMs.toFixed(4)}`);
|
|
3458
|
+
lines.push(`layercache_layer_latency_max_ms{${layerLabel}} ${latency.maxMs.toFixed(4)}`);
|
|
3459
|
+
lines.push(`layercache_layer_latency_count{${layerLabel}} ${latency.count}`);
|
|
3460
|
+
}
|
|
2398
3461
|
}
|
|
2399
3462
|
lines.push("");
|
|
2400
3463
|
return lines.join("\n");
|
|
@@ -2405,6 +3468,7 @@ function sanitizeLabel(value) {
|
|
|
2405
3468
|
}
|
|
2406
3469
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2407
3470
|
0 && (module.exports = {
|
|
3471
|
+
CacheMissError,
|
|
2408
3472
|
CacheNamespace,
|
|
2409
3473
|
CacheStack,
|
|
2410
3474
|
DiskLayer,
|
|
@@ -2422,7 +3486,10 @@ function sanitizeLabel(value) {
|
|
|
2422
3486
|
cacheGraphqlResolver,
|
|
2423
3487
|
createCacheStatsHandler,
|
|
2424
3488
|
createCachedMethodDecorator,
|
|
3489
|
+
createExpressCacheMiddleware,
|
|
2425
3490
|
createFastifyLayercachePlugin,
|
|
3491
|
+
createHonoCacheMiddleware,
|
|
3492
|
+
createOpenTelemetryPlugin,
|
|
2426
3493
|
createPrometheusMetricsExporter,
|
|
2427
3494
|
createTrpcCacheMiddleware
|
|
2428
3495
|
});
|