layercache 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -8
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-IXCMHVHP.js} +62 -56
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +170 -39
- package/dist/cli.js +57 -2
- package/dist/edge-DLpdQN0W.d.cts +672 -0
- package/dist/edge-DLpdQN0W.d.ts +672 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +1173 -221
- package/dist/index.d.cts +51 -568
- package/dist/index.d.ts +51 -568
- package/dist/index.js +1005 -505
- package/package.json +8 -3
- package/packages/nestjs/dist/index.cjs +980 -370
- package/packages/nestjs/dist/index.d.cts +80 -0
- package/packages/nestjs/dist/index.d.ts +80 -0
- package/packages/nestjs/dist/index.js +968 -368
package/dist/index.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PatternMatcher,
|
|
3
2
|
RedisTagIndex
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-IXCMHVHP.js";
|
|
4
|
+
import {
|
|
5
|
+
MemoryLayer,
|
|
6
|
+
TagIndex,
|
|
7
|
+
createHonoCacheMiddleware
|
|
8
|
+
} from "./chunk-46UH7LNM.js";
|
|
9
|
+
import {
|
|
10
|
+
PatternMatcher,
|
|
11
|
+
createStoredValueEnvelope,
|
|
12
|
+
isStoredValueEnvelope,
|
|
13
|
+
refreshStoredEnvelope,
|
|
14
|
+
remainingFreshTtlSeconds,
|
|
15
|
+
remainingStoredTtlSeconds,
|
|
16
|
+
resolveStoredValue,
|
|
17
|
+
unwrapStoredValue
|
|
18
|
+
} from "./chunk-ZMDB5KOK.js";
|
|
5
19
|
|
|
6
20
|
// src/CacheStack.ts
|
|
7
|
-
import { randomUUID } from "crypto";
|
|
8
21
|
import { EventEmitter } from "events";
|
|
9
|
-
import { promises as fs } from "fs";
|
|
10
22
|
|
|
11
23
|
// src/CacheNamespace.ts
|
|
24
|
+
import { Mutex } from "async-mutex";
|
|
12
25
|
var CacheNamespace = class _CacheNamespace {
|
|
13
26
|
constructor(cache, prefix) {
|
|
14
27
|
this.cache = cache;
|
|
@@ -16,57 +29,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
16
29
|
}
|
|
17
30
|
cache;
|
|
18
31
|
prefix;
|
|
32
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
metrics = emptyMetrics();
|
|
19
34
|
async get(key, fetcher, options) {
|
|
20
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
35
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
21
36
|
}
|
|
22
37
|
async getOrSet(key, fetcher, options) {
|
|
23
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
38
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
26
41
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
27
42
|
*/
|
|
28
43
|
async getOrThrow(key, fetcher, options) {
|
|
29
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
44
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
30
45
|
}
|
|
31
46
|
async has(key) {
|
|
32
|
-
return this.cache.has(this.qualify(key));
|
|
47
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
33
48
|
}
|
|
34
49
|
async ttl(key) {
|
|
35
|
-
return this.cache.ttl(this.qualify(key));
|
|
50
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
36
51
|
}
|
|
37
52
|
async set(key, value, options) {
|
|
38
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
53
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
39
54
|
}
|
|
40
55
|
async delete(key) {
|
|
41
|
-
await this.cache.delete(this.qualify(key));
|
|
56
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
42
57
|
}
|
|
43
58
|
async mdelete(keys) {
|
|
44
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
59
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
45
60
|
}
|
|
46
61
|
async clear() {
|
|
47
|
-
await this.cache.
|
|
62
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
48
63
|
}
|
|
49
64
|
async mget(entries) {
|
|
50
|
-
return this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
return this.trackMetrics(
|
|
66
|
+
() => this.cache.mget(
|
|
67
|
+
entries.map((entry) => ({
|
|
68
|
+
...entry,
|
|
69
|
+
key: this.qualify(entry.key)
|
|
70
|
+
}))
|
|
71
|
+
)
|
|
55
72
|
);
|
|
56
73
|
}
|
|
57
74
|
async mset(entries) {
|
|
58
|
-
await this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
await this.trackMetrics(
|
|
76
|
+
() => this.cache.mset(
|
|
77
|
+
entries.map((entry) => ({
|
|
78
|
+
...entry,
|
|
79
|
+
key: this.qualify(entry.key)
|
|
80
|
+
}))
|
|
81
|
+
)
|
|
63
82
|
);
|
|
64
83
|
}
|
|
65
84
|
async invalidateByTag(tag) {
|
|
66
|
-
await this.cache.invalidateByTag(tag);
|
|
85
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
86
|
+
}
|
|
87
|
+
async invalidateByTags(tags, mode = "any") {
|
|
88
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
67
89
|
}
|
|
68
90
|
async invalidateByPattern(pattern) {
|
|
69
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
91
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
92
|
+
}
|
|
93
|
+
async invalidateByPrefix(prefix) {
|
|
94
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
70
95
|
}
|
|
71
96
|
/**
|
|
72
97
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -87,10 +112,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
87
112
|
);
|
|
88
113
|
}
|
|
89
114
|
getMetrics() {
|
|
90
|
-
return this.
|
|
115
|
+
return cloneMetrics(this.metrics);
|
|
91
116
|
}
|
|
92
117
|
getHitRate() {
|
|
93
|
-
|
|
118
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
119
|
+
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
120
|
+
const byLayer = {};
|
|
121
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
122
|
+
for (const layer of layers) {
|
|
123
|
+
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
124
|
+
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
125
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
126
|
+
}
|
|
127
|
+
return { overall, byLayer };
|
|
94
128
|
}
|
|
95
129
|
/**
|
|
96
130
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -107,7 +141,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
107
141
|
qualify(key) {
|
|
108
142
|
return `${this.prefix}:${key}`;
|
|
109
143
|
}
|
|
144
|
+
async trackMetrics(operation) {
|
|
145
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
146
|
+
const before = this.cache.getMetrics();
|
|
147
|
+
const result = await operation();
|
|
148
|
+
const after = this.cache.getMetrics();
|
|
149
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
150
|
+
return result;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
getMetricsMutex() {
|
|
154
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
155
|
+
if (existing) {
|
|
156
|
+
return existing;
|
|
157
|
+
}
|
|
158
|
+
const mutex = new Mutex();
|
|
159
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
160
|
+
return mutex;
|
|
161
|
+
}
|
|
110
162
|
};
|
|
163
|
+
function emptyMetrics() {
|
|
164
|
+
return {
|
|
165
|
+
hits: 0,
|
|
166
|
+
misses: 0,
|
|
167
|
+
fetches: 0,
|
|
168
|
+
sets: 0,
|
|
169
|
+
deletes: 0,
|
|
170
|
+
backfills: 0,
|
|
171
|
+
invalidations: 0,
|
|
172
|
+
staleHits: 0,
|
|
173
|
+
refreshes: 0,
|
|
174
|
+
refreshErrors: 0,
|
|
175
|
+
writeFailures: 0,
|
|
176
|
+
singleFlightWaits: 0,
|
|
177
|
+
negativeCacheHits: 0,
|
|
178
|
+
circuitBreakerTrips: 0,
|
|
179
|
+
degradedOperations: 0,
|
|
180
|
+
hitsByLayer: {},
|
|
181
|
+
missesByLayer: {},
|
|
182
|
+
latencyByLayer: {},
|
|
183
|
+
resetAt: Date.now()
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function cloneMetrics(metrics) {
|
|
187
|
+
return {
|
|
188
|
+
...metrics,
|
|
189
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
190
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
191
|
+
latencyByLayer: Object.fromEntries(
|
|
192
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
193
|
+
)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function diffMetrics(before, after) {
|
|
197
|
+
const latencyByLayer = Object.fromEntries(
|
|
198
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
199
|
+
layer,
|
|
200
|
+
{
|
|
201
|
+
avgMs: value.avgMs,
|
|
202
|
+
maxMs: value.maxMs,
|
|
203
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
hits: after.hits - before.hits,
|
|
209
|
+
misses: after.misses - before.misses,
|
|
210
|
+
fetches: after.fetches - before.fetches,
|
|
211
|
+
sets: after.sets - before.sets,
|
|
212
|
+
deletes: after.deletes - before.deletes,
|
|
213
|
+
backfills: after.backfills - before.backfills,
|
|
214
|
+
invalidations: after.invalidations - before.invalidations,
|
|
215
|
+
staleHits: after.staleHits - before.staleHits,
|
|
216
|
+
refreshes: after.refreshes - before.refreshes,
|
|
217
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
218
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
219
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
220
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
221
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
222
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
223
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
224
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
225
|
+
latencyByLayer,
|
|
226
|
+
resetAt: after.resetAt
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function addMetrics(base, delta) {
|
|
230
|
+
return {
|
|
231
|
+
hits: base.hits + delta.hits,
|
|
232
|
+
misses: base.misses + delta.misses,
|
|
233
|
+
fetches: base.fetches + delta.fetches,
|
|
234
|
+
sets: base.sets + delta.sets,
|
|
235
|
+
deletes: base.deletes + delta.deletes,
|
|
236
|
+
backfills: base.backfills + delta.backfills,
|
|
237
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
238
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
239
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
240
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
241
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
242
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
243
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
244
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
245
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
246
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
247
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
248
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
249
|
+
resetAt: base.resetAt
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function diffMap(before, after) {
|
|
253
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
254
|
+
const result = {};
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
function addMap(base, delta) {
|
|
261
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
262
|
+
const result = {};
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
111
268
|
|
|
112
269
|
// src/internal/CircuitBreakerManager.ts
|
|
113
270
|
var CircuitBreakerManager = class {
|
|
@@ -201,6 +358,148 @@ var CircuitBreakerManager = class {
|
|
|
201
358
|
}
|
|
202
359
|
};
|
|
203
360
|
|
|
361
|
+
// src/internal/FetchRateLimiter.ts
|
|
362
|
+
var FetchRateLimiter = class {
|
|
363
|
+
queue = [];
|
|
364
|
+
buckets = /* @__PURE__ */ new Map();
|
|
365
|
+
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
366
|
+
nextFetcherBucketId = 0;
|
|
367
|
+
drainTimer;
|
|
368
|
+
async schedule(options, context, task) {
|
|
369
|
+
if (!options) {
|
|
370
|
+
return task();
|
|
371
|
+
}
|
|
372
|
+
const normalized = this.normalize(options);
|
|
373
|
+
if (!normalized) {
|
|
374
|
+
return task();
|
|
375
|
+
}
|
|
376
|
+
return new Promise((resolve2, reject) => {
|
|
377
|
+
this.queue.push({
|
|
378
|
+
bucketKey: this.resolveBucketKey(normalized, context),
|
|
379
|
+
options: normalized,
|
|
380
|
+
task,
|
|
381
|
+
resolve: resolve2,
|
|
382
|
+
reject
|
|
383
|
+
});
|
|
384
|
+
this.drain();
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
normalize(options) {
|
|
388
|
+
const maxConcurrent = options.maxConcurrent;
|
|
389
|
+
const intervalMs = options.intervalMs;
|
|
390
|
+
const maxPerInterval = options.maxPerInterval;
|
|
391
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
maxConcurrent,
|
|
396
|
+
intervalMs,
|
|
397
|
+
maxPerInterval,
|
|
398
|
+
scope: options.scope ?? "global",
|
|
399
|
+
bucketKey: options.bucketKey
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
resolveBucketKey(options, context) {
|
|
403
|
+
if (options.bucketKey) {
|
|
404
|
+
return `custom:${options.bucketKey}`;
|
|
405
|
+
}
|
|
406
|
+
if (options.scope === "key") {
|
|
407
|
+
return `key:${context.key}`;
|
|
408
|
+
}
|
|
409
|
+
if (options.scope === "fetcher") {
|
|
410
|
+
const existing = this.fetcherBuckets.get(context.fetcher);
|
|
411
|
+
if (existing) {
|
|
412
|
+
return existing;
|
|
413
|
+
}
|
|
414
|
+
const bucket = `fetcher:${this.nextFetcherBucketId}`;
|
|
415
|
+
this.nextFetcherBucketId += 1;
|
|
416
|
+
this.fetcherBuckets.set(context.fetcher, bucket);
|
|
417
|
+
return bucket;
|
|
418
|
+
}
|
|
419
|
+
return "global";
|
|
420
|
+
}
|
|
421
|
+
drain() {
|
|
422
|
+
if (this.drainTimer) {
|
|
423
|
+
clearTimeout(this.drainTimer);
|
|
424
|
+
this.drainTimer = void 0;
|
|
425
|
+
}
|
|
426
|
+
while (this.queue.length > 0) {
|
|
427
|
+
let nextIndex = -1;
|
|
428
|
+
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
429
|
+
for (let index = 0; index < this.queue.length; index += 1) {
|
|
430
|
+
const next2 = this.queue[index];
|
|
431
|
+
if (!next2) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const waitMs = this.waitTime(next2.bucketKey, next2.options);
|
|
435
|
+
if (waitMs <= 0) {
|
|
436
|
+
nextIndex = index;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
440
|
+
}
|
|
441
|
+
if (nextIndex < 0) {
|
|
442
|
+
if (Number.isFinite(nextWaitMs)) {
|
|
443
|
+
this.drainTimer = setTimeout(() => {
|
|
444
|
+
this.drainTimer = void 0;
|
|
445
|
+
this.drain();
|
|
446
|
+
}, nextWaitMs);
|
|
447
|
+
this.drainTimer.unref?.();
|
|
448
|
+
}
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const next = this.queue.splice(nextIndex, 1)[0];
|
|
452
|
+
if (!next) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const bucket = this.bucketState(next.bucketKey);
|
|
456
|
+
bucket.active += 1;
|
|
457
|
+
bucket.startedAt.push(Date.now());
|
|
458
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
459
|
+
bucket.active -= 1;
|
|
460
|
+
this.drain();
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
waitTime(bucketKey, options) {
|
|
465
|
+
const bucket = this.bucketState(bucketKey);
|
|
466
|
+
const now = Date.now();
|
|
467
|
+
if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
|
|
468
|
+
return 1;
|
|
469
|
+
}
|
|
470
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
this.prune(bucket, now, options.intervalMs);
|
|
474
|
+
if (bucket.startedAt.length < options.maxPerInterval) {
|
|
475
|
+
return 0;
|
|
476
|
+
}
|
|
477
|
+
const oldest = bucket.startedAt[0];
|
|
478
|
+
if (!oldest) {
|
|
479
|
+
return 0;
|
|
480
|
+
}
|
|
481
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
482
|
+
}
|
|
483
|
+
prune(bucket, now, intervalMs) {
|
|
484
|
+
while (bucket.startedAt.length > 0) {
|
|
485
|
+
const startedAt = bucket.startedAt[0];
|
|
486
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
bucket.startedAt.shift();
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
bucketState(bucketKey) {
|
|
493
|
+
const existing = this.buckets.get(bucketKey);
|
|
494
|
+
if (existing) {
|
|
495
|
+
return existing;
|
|
496
|
+
}
|
|
497
|
+
const bucket = { active: 0, startedAt: [] };
|
|
498
|
+
this.buckets.set(bucketKey, bucket);
|
|
499
|
+
return bucket;
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
204
503
|
// src/internal/MetricsCollector.ts
|
|
205
504
|
var MetricsCollector = class {
|
|
206
505
|
data = this.empty();
|
|
@@ -275,107 +574,6 @@ var MetricsCollector = class {
|
|
|
275
574
|
}
|
|
276
575
|
};
|
|
277
576
|
|
|
278
|
-
// src/internal/StoredValue.ts
|
|
279
|
-
function isStoredValueEnvelope(value) {
|
|
280
|
-
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
281
|
-
}
|
|
282
|
-
function createStoredValueEnvelope(options) {
|
|
283
|
-
const now = options.now ?? Date.now();
|
|
284
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
285
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
286
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
287
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
288
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
289
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
290
|
-
return {
|
|
291
|
-
__layercache: 1,
|
|
292
|
-
kind: options.kind,
|
|
293
|
-
value: options.value,
|
|
294
|
-
freshUntil,
|
|
295
|
-
staleUntil,
|
|
296
|
-
errorUntil,
|
|
297
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
298
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
299
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
303
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
304
|
-
return { state: "fresh", value: stored, stored };
|
|
305
|
-
}
|
|
306
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
307
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
308
|
-
}
|
|
309
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
310
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
311
|
-
}
|
|
312
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
313
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
314
|
-
}
|
|
315
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
316
|
-
}
|
|
317
|
-
function unwrapStoredValue(stored) {
|
|
318
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
319
|
-
return stored;
|
|
320
|
-
}
|
|
321
|
-
if (stored.kind === "empty") {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
return stored.value ?? null;
|
|
325
|
-
}
|
|
326
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
327
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
328
|
-
return void 0;
|
|
329
|
-
}
|
|
330
|
-
const expiry = maxExpiry(stored);
|
|
331
|
-
if (expiry === null) {
|
|
332
|
-
return void 0;
|
|
333
|
-
}
|
|
334
|
-
const remainingMs = expiry - now;
|
|
335
|
-
if (remainingMs <= 0) {
|
|
336
|
-
return 1;
|
|
337
|
-
}
|
|
338
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
339
|
-
}
|
|
340
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
341
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
342
|
-
return void 0;
|
|
343
|
-
}
|
|
344
|
-
const remainingMs = stored.freshUntil - now;
|
|
345
|
-
if (remainingMs <= 0) {
|
|
346
|
-
return 0;
|
|
347
|
-
}
|
|
348
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
349
|
-
}
|
|
350
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
351
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
352
|
-
return stored;
|
|
353
|
-
}
|
|
354
|
-
return createStoredValueEnvelope({
|
|
355
|
-
kind: stored.kind,
|
|
356
|
-
value: stored.value,
|
|
357
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
358
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
359
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
360
|
-
now
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
function maxExpiry(stored) {
|
|
364
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
365
|
-
(value) => value !== null
|
|
366
|
-
);
|
|
367
|
-
if (values.length === 0) {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
return Math.max(...values);
|
|
371
|
-
}
|
|
372
|
-
function normalizePositiveSeconds(value) {
|
|
373
|
-
if (!value || value <= 0) {
|
|
374
|
-
return void 0;
|
|
375
|
-
}
|
|
376
|
-
return value;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
577
|
// src/internal/TtlResolver.ts
|
|
380
578
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
381
579
|
var TtlResolver = class {
|
|
@@ -397,13 +595,14 @@ var TtlResolver = class {
|
|
|
397
595
|
clearProfiles() {
|
|
398
596
|
this.accessProfiles.clear();
|
|
399
597
|
}
|
|
400
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
598
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
599
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
401
600
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
402
601
|
layerName,
|
|
403
602
|
options?.negativeTtl,
|
|
404
603
|
globalNegativeTtl,
|
|
405
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
406
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
604
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
605
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
407
606
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
408
607
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
409
608
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -442,6 +641,29 @@ var TtlResolver = class {
|
|
|
442
641
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
443
642
|
return Math.max(1, Math.round(ttl + delta));
|
|
444
643
|
}
|
|
644
|
+
resolvePolicyTtl(key, value, policy) {
|
|
645
|
+
if (!policy) {
|
|
646
|
+
return void 0;
|
|
647
|
+
}
|
|
648
|
+
if (typeof policy === "function") {
|
|
649
|
+
return policy({ key, value });
|
|
650
|
+
}
|
|
651
|
+
const now = /* @__PURE__ */ new Date();
|
|
652
|
+
if (policy === "until-midnight") {
|
|
653
|
+
const nextMidnight = new Date(now);
|
|
654
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
655
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
656
|
+
}
|
|
657
|
+
if (policy === "next-hour") {
|
|
658
|
+
const nextHour = new Date(now);
|
|
659
|
+
nextHour.setMinutes(60, 0, 0);
|
|
660
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
661
|
+
}
|
|
662
|
+
const alignToSeconds = policy.alignTo;
|
|
663
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
664
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
665
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
666
|
+
}
|
|
445
667
|
readLayerNumber(layerName, value) {
|
|
446
668
|
if (typeof value === "number") {
|
|
447
669
|
return value;
|
|
@@ -464,90 +686,8 @@ var TtlResolver = class {
|
|
|
464
686
|
}
|
|
465
687
|
};
|
|
466
688
|
|
|
467
|
-
// src/invalidation/TagIndex.ts
|
|
468
|
-
var TagIndex = class {
|
|
469
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
470
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
471
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
472
|
-
maxKnownKeys;
|
|
473
|
-
constructor(options = {}) {
|
|
474
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
475
|
-
}
|
|
476
|
-
async touch(key) {
|
|
477
|
-
this.knownKeys.add(key);
|
|
478
|
-
this.pruneKnownKeysIfNeeded();
|
|
479
|
-
}
|
|
480
|
-
async track(key, tags) {
|
|
481
|
-
this.knownKeys.add(key);
|
|
482
|
-
this.pruneKnownKeysIfNeeded();
|
|
483
|
-
if (tags.length === 0) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const existingTags = this.keyToTags.get(key);
|
|
487
|
-
if (existingTags) {
|
|
488
|
-
for (const tag of existingTags) {
|
|
489
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const tagSet = new Set(tags);
|
|
493
|
-
this.keyToTags.set(key, tagSet);
|
|
494
|
-
for (const tag of tagSet) {
|
|
495
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
496
|
-
keys.add(key);
|
|
497
|
-
this.tagToKeys.set(tag, keys);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async remove(key) {
|
|
501
|
-
this.knownKeys.delete(key);
|
|
502
|
-
const tags = this.keyToTags.get(key);
|
|
503
|
-
if (!tags) {
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
for (const tag of tags) {
|
|
507
|
-
const keys = this.tagToKeys.get(tag);
|
|
508
|
-
if (!keys) {
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
keys.delete(key);
|
|
512
|
-
if (keys.size === 0) {
|
|
513
|
-
this.tagToKeys.delete(tag);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
this.keyToTags.delete(key);
|
|
517
|
-
}
|
|
518
|
-
async keysForTag(tag) {
|
|
519
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
520
|
-
}
|
|
521
|
-
async tagsForKey(key) {
|
|
522
|
-
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
523
|
-
}
|
|
524
|
-
async matchPattern(pattern) {
|
|
525
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
526
|
-
}
|
|
527
|
-
async clear() {
|
|
528
|
-
this.tagToKeys.clear();
|
|
529
|
-
this.keyToTags.clear();
|
|
530
|
-
this.knownKeys.clear();
|
|
531
|
-
}
|
|
532
|
-
pruneKnownKeysIfNeeded() {
|
|
533
|
-
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
537
|
-
let removed = 0;
|
|
538
|
-
for (const key of this.knownKeys) {
|
|
539
|
-
if (removed >= toRemove) {
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
this.knownKeys.delete(key);
|
|
543
|
-
this.keyToTags.delete(key);
|
|
544
|
-
removed += 1;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
689
|
// src/stampede/StampedeGuard.ts
|
|
550
|
-
import { Mutex } from "async-mutex";
|
|
690
|
+
import { Mutex as Mutex2 } from "async-mutex";
|
|
551
691
|
var StampedeGuard = class {
|
|
552
692
|
mutexes = /* @__PURE__ */ new Map();
|
|
553
693
|
async execute(key, task) {
|
|
@@ -564,7 +704,7 @@ var StampedeGuard = class {
|
|
|
564
704
|
getMutexEntry(key) {
|
|
565
705
|
let entry = this.mutexes.get(key);
|
|
566
706
|
if (!entry) {
|
|
567
|
-
entry = { mutex: new
|
|
707
|
+
entry = { mutex: new Mutex2(), references: 0 };
|
|
568
708
|
this.mutexes.set(key, entry);
|
|
569
709
|
}
|
|
570
710
|
entry.references += 1;
|
|
@@ -625,6 +765,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
625
765
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
626
766
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
627
767
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
768
|
+
this.currentGeneration = options.generation;
|
|
628
769
|
if (options.publishSetInvalidation !== void 0) {
|
|
629
770
|
console.warn(
|
|
630
771
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -633,21 +774,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
633
774
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
634
775
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
635
776
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
777
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
636
778
|
this.startup = this.initialize();
|
|
637
779
|
}
|
|
638
780
|
layers;
|
|
639
781
|
options;
|
|
640
782
|
stampedeGuard = new StampedeGuard();
|
|
641
783
|
metricsCollector = new MetricsCollector();
|
|
642
|
-
instanceId =
|
|
784
|
+
instanceId = createInstanceId();
|
|
643
785
|
startup;
|
|
644
786
|
unsubscribeInvalidation;
|
|
645
787
|
logger;
|
|
646
788
|
tagIndex;
|
|
789
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
647
790
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
648
791
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
649
792
|
ttlResolver;
|
|
650
793
|
circuitBreakerManager;
|
|
794
|
+
currentGeneration;
|
|
795
|
+
writeBehindQueue = [];
|
|
796
|
+
writeBehindTimer;
|
|
797
|
+
writeBehindFlushPromise;
|
|
651
798
|
isDisconnecting = false;
|
|
652
799
|
disconnectPromise;
|
|
653
800
|
/**
|
|
@@ -657,9 +804,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
657
804
|
* and no `fetcher` is provided.
|
|
658
805
|
*/
|
|
659
806
|
async get(key, fetcher, options) {
|
|
660
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
807
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
661
808
|
this.validateWriteOptions(options);
|
|
662
|
-
await this.
|
|
809
|
+
await this.awaitStartup("get");
|
|
663
810
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
664
811
|
if (hit.found) {
|
|
665
812
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -724,8 +871,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
724
871
|
* Returns true if the given key exists and is not expired in any layer.
|
|
725
872
|
*/
|
|
726
873
|
async has(key) {
|
|
727
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
728
|
-
await this.
|
|
874
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
875
|
+
await this.awaitStartup("has");
|
|
729
876
|
for (const layer of this.layers) {
|
|
730
877
|
if (this.shouldSkipLayer(layer)) {
|
|
731
878
|
continue;
|
|
@@ -755,8 +902,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
755
902
|
* that has it, or null if the key is not found / has no TTL.
|
|
756
903
|
*/
|
|
757
904
|
async ttl(key) {
|
|
758
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
759
|
-
await this.
|
|
905
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
906
|
+
await this.awaitStartup("ttl");
|
|
760
907
|
for (const layer of this.layers) {
|
|
761
908
|
if (this.shouldSkipLayer(layer)) {
|
|
762
909
|
continue;
|
|
@@ -777,17 +924,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
777
924
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
778
925
|
*/
|
|
779
926
|
async set(key, value, options) {
|
|
780
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
927
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
781
928
|
this.validateWriteOptions(options);
|
|
782
|
-
await this.
|
|
929
|
+
await this.awaitStartup("set");
|
|
783
930
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
784
931
|
}
|
|
785
932
|
/**
|
|
786
933
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
787
934
|
*/
|
|
788
935
|
async delete(key) {
|
|
789
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
790
|
-
await this.
|
|
936
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
937
|
+
await this.awaitStartup("delete");
|
|
791
938
|
await this.deleteKeys([normalizedKey]);
|
|
792
939
|
await this.publishInvalidation({
|
|
793
940
|
scope: "key",
|
|
@@ -797,7 +944,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
797
944
|
});
|
|
798
945
|
}
|
|
799
946
|
async clear() {
|
|
800
|
-
await this.
|
|
947
|
+
await this.awaitStartup("clear");
|
|
801
948
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
802
949
|
await this.tagIndex.clear();
|
|
803
950
|
this.ttlResolver.clearProfiles();
|
|
@@ -813,23 +960,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
813
960
|
if (keys.length === 0) {
|
|
814
961
|
return;
|
|
815
962
|
}
|
|
816
|
-
await this.
|
|
963
|
+
await this.awaitStartup("mdelete");
|
|
817
964
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
818
|
-
|
|
965
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
966
|
+
await this.deleteKeys(cacheKeys);
|
|
819
967
|
await this.publishInvalidation({
|
|
820
968
|
scope: "keys",
|
|
821
|
-
keys:
|
|
969
|
+
keys: cacheKeys,
|
|
822
970
|
sourceId: this.instanceId,
|
|
823
971
|
operation: "delete"
|
|
824
972
|
});
|
|
825
973
|
}
|
|
826
974
|
async mget(entries) {
|
|
975
|
+
this.assertActive("mget");
|
|
827
976
|
if (entries.length === 0) {
|
|
828
977
|
return [];
|
|
829
978
|
}
|
|
830
979
|
const normalizedEntries = entries.map((entry) => ({
|
|
831
980
|
...entry,
|
|
832
|
-
key: this.validateCacheKey(entry.key)
|
|
981
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
833
982
|
}));
|
|
834
983
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
835
984
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -855,7 +1004,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
855
1004
|
})
|
|
856
1005
|
);
|
|
857
1006
|
}
|
|
858
|
-
await this.
|
|
1007
|
+
await this.awaitStartup("mget");
|
|
859
1008
|
const pending = /* @__PURE__ */ new Set();
|
|
860
1009
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
861
1010
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -903,14 +1052,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
903
1052
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
904
1053
|
}
|
|
905
1054
|
async mset(entries) {
|
|
1055
|
+
this.assertActive("mset");
|
|
906
1056
|
const normalizedEntries = entries.map((entry) => ({
|
|
907
1057
|
...entry,
|
|
908
|
-
key: this.validateCacheKey(entry.key)
|
|
1058
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
909
1059
|
}));
|
|
910
1060
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
911
|
-
await
|
|
1061
|
+
await this.awaitStartup("mset");
|
|
1062
|
+
await this.writeBatch(normalizedEntries);
|
|
912
1063
|
}
|
|
913
1064
|
async warm(entries, options = {}) {
|
|
1065
|
+
this.assertActive("warm");
|
|
914
1066
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
915
1067
|
const total = entries.length;
|
|
916
1068
|
let completed = 0;
|
|
@@ -959,14 +1111,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
959
1111
|
return new CacheNamespace(this, prefix);
|
|
960
1112
|
}
|
|
961
1113
|
async invalidateByTag(tag) {
|
|
962
|
-
await this.
|
|
1114
|
+
await this.awaitStartup("invalidateByTag");
|
|
963
1115
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
964
1116
|
await this.deleteKeys(keys);
|
|
965
1117
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
966
1118
|
}
|
|
1119
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1120
|
+
if (tags.length === 0) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
await this.awaitStartup("invalidateByTags");
|
|
1124
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1125
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1126
|
+
await this.deleteKeys(keys);
|
|
1127
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1128
|
+
}
|
|
967
1129
|
async invalidateByPattern(pattern) {
|
|
968
|
-
await this.
|
|
969
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1130
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1131
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1132
|
+
await this.deleteKeys(keys);
|
|
1133
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1134
|
+
}
|
|
1135
|
+
async invalidateByPrefix(prefix) {
|
|
1136
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1137
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1138
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
970
1139
|
await this.deleteKeys(keys);
|
|
971
1140
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
972
1141
|
}
|
|
@@ -993,14 +1162,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
993
1162
|
getHitRate() {
|
|
994
1163
|
return this.metricsCollector.hitRate();
|
|
995
1164
|
}
|
|
1165
|
+
async healthCheck() {
|
|
1166
|
+
await this.startup;
|
|
1167
|
+
return Promise.all(
|
|
1168
|
+
this.layers.map(async (layer) => {
|
|
1169
|
+
const startedAt = performance.now();
|
|
1170
|
+
try {
|
|
1171
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1172
|
+
return {
|
|
1173
|
+
layer: layer.name,
|
|
1174
|
+
healthy,
|
|
1175
|
+
latencyMs: performance.now() - startedAt
|
|
1176
|
+
};
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
return {
|
|
1179
|
+
layer: layer.name,
|
|
1180
|
+
healthy: false,
|
|
1181
|
+
latencyMs: performance.now() - startedAt,
|
|
1182
|
+
error: this.formatError(error)
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
})
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
bumpGeneration(nextGeneration) {
|
|
1189
|
+
const current = this.currentGeneration ?? 0;
|
|
1190
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1191
|
+
return this.currentGeneration;
|
|
1192
|
+
}
|
|
996
1193
|
/**
|
|
997
1194
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
998
1195
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
999
1196
|
* Returns `null` if the key does not exist in any layer.
|
|
1000
1197
|
*/
|
|
1001
1198
|
async inspect(key) {
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1199
|
+
const userKey = this.validateCacheKey(key);
|
|
1200
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1201
|
+
await this.awaitStartup("inspect");
|
|
1004
1202
|
const foundInLayers = [];
|
|
1005
1203
|
let freshTtlSeconds = null;
|
|
1006
1204
|
let staleTtlSeconds = null;
|
|
@@ -1031,10 +1229,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1031
1229
|
return null;
|
|
1032
1230
|
}
|
|
1033
1231
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1034
|
-
return { key:
|
|
1232
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1035
1233
|
}
|
|
1036
1234
|
async exportState() {
|
|
1037
|
-
await this.
|
|
1235
|
+
await this.awaitStartup("exportState");
|
|
1038
1236
|
const exported = /* @__PURE__ */ new Map();
|
|
1039
1237
|
for (const layer of this.layers) {
|
|
1040
1238
|
if (!layer.keys) {
|
|
@@ -1042,15 +1240,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1042
1240
|
}
|
|
1043
1241
|
const keys = await layer.keys();
|
|
1044
1242
|
for (const key of keys) {
|
|
1045
|
-
|
|
1243
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1244
|
+
if (exported.has(exportedKey)) {
|
|
1046
1245
|
continue;
|
|
1047
1246
|
}
|
|
1048
1247
|
const stored = await this.readLayerEntry(layer, key);
|
|
1049
1248
|
if (stored === null) {
|
|
1050
1249
|
continue;
|
|
1051
1250
|
}
|
|
1052
|
-
exported.set(
|
|
1053
|
-
key,
|
|
1251
|
+
exported.set(exportedKey, {
|
|
1252
|
+
key: exportedKey,
|
|
1054
1253
|
value: stored,
|
|
1055
1254
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1056
1255
|
});
|
|
@@ -1059,20 +1258,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
1059
1258
|
return [...exported.values()];
|
|
1060
1259
|
}
|
|
1061
1260
|
async importState(entries) {
|
|
1062
|
-
await this.
|
|
1261
|
+
await this.awaitStartup("importState");
|
|
1063
1262
|
await Promise.all(
|
|
1064
1263
|
entries.map(async (entry) => {
|
|
1065
|
-
|
|
1066
|
-
await this.
|
|
1264
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1265
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1266
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1067
1267
|
})
|
|
1068
1268
|
);
|
|
1069
1269
|
}
|
|
1070
1270
|
async persistToFile(filePath) {
|
|
1271
|
+
this.assertActive("persistToFile");
|
|
1071
1272
|
const snapshot = await this.exportState();
|
|
1072
|
-
|
|
1273
|
+
const { promises: fs2 } = await import("fs");
|
|
1274
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1073
1275
|
}
|
|
1074
1276
|
async restoreFromFile(filePath) {
|
|
1075
|
-
|
|
1277
|
+
this.assertActive("restoreFromFile");
|
|
1278
|
+
const { promises: fs2 } = await import("fs");
|
|
1279
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
1076
1280
|
let parsed;
|
|
1077
1281
|
try {
|
|
1078
1282
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1095,7 +1299,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1095
1299
|
this.disconnectPromise = (async () => {
|
|
1096
1300
|
await this.startup;
|
|
1097
1301
|
await this.unsubscribeInvalidation?.();
|
|
1302
|
+
await this.flushWriteBehindQueue();
|
|
1098
1303
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1304
|
+
if (this.writeBehindTimer) {
|
|
1305
|
+
clearInterval(this.writeBehindTimer);
|
|
1306
|
+
this.writeBehindTimer = void 0;
|
|
1307
|
+
}
|
|
1308
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1099
1309
|
})();
|
|
1100
1310
|
}
|
|
1101
1311
|
await this.disconnectPromise;
|
|
@@ -1155,7 +1365,11 @@ var CacheStack = class extends EventEmitter {
|
|
|
1155
1365
|
const fetchStart = Date.now();
|
|
1156
1366
|
let fetched;
|
|
1157
1367
|
try {
|
|
1158
|
-
fetched = await
|
|
1368
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1369
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1370
|
+
{ key, fetcher },
|
|
1371
|
+
fetcher
|
|
1372
|
+
);
|
|
1159
1373
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1160
1374
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1161
1375
|
} catch (error) {
|
|
@@ -1189,6 +1403,61 @@ var CacheStack = class extends EventEmitter {
|
|
|
1189
1403
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1190
1404
|
}
|
|
1191
1405
|
}
|
|
1406
|
+
async writeBatch(entries) {
|
|
1407
|
+
const now = Date.now();
|
|
1408
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1409
|
+
const immediateOperations = [];
|
|
1410
|
+
const deferredOperations = [];
|
|
1411
|
+
for (const entry of entries) {
|
|
1412
|
+
for (const layer of this.layers) {
|
|
1413
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1417
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1418
|
+
bucket.push(layerEntry);
|
|
1419
|
+
entriesByLayer.set(layer, bucket);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1423
|
+
const operation = async () => {
|
|
1424
|
+
try {
|
|
1425
|
+
if (layer.setMany) {
|
|
1426
|
+
await layer.setMany(layerEntries);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
1434
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1435
|
+
deferredOperations.push(operation);
|
|
1436
|
+
} else {
|
|
1437
|
+
immediateOperations.push(operation);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1441
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1442
|
+
for (const entry of entries) {
|
|
1443
|
+
if (entry.options?.tags) {
|
|
1444
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1445
|
+
} else {
|
|
1446
|
+
await this.tagIndex.touch(entry.key);
|
|
1447
|
+
}
|
|
1448
|
+
this.metricsCollector.increment("sets");
|
|
1449
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1450
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1451
|
+
}
|
|
1452
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1453
|
+
await this.publishInvalidation({
|
|
1454
|
+
scope: "keys",
|
|
1455
|
+
keys: entries.map((entry) => entry.key),
|
|
1456
|
+
sourceId: this.instanceId,
|
|
1457
|
+
operation: "write"
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1192
1461
|
async readFromLayers(key, options, mode) {
|
|
1193
1462
|
let sawRetainableValue = false;
|
|
1194
1463
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1272,33 +1541,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1272
1541
|
}
|
|
1273
1542
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1274
1543
|
const now = Date.now();
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
options
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
});
|
|
1294
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1295
|
-
try {
|
|
1296
|
-
await layer.set(key, payload, ttl);
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1544
|
+
const immediateOperations = [];
|
|
1545
|
+
const deferredOperations = [];
|
|
1546
|
+
for (const layer of this.layers) {
|
|
1547
|
+
const operation = async () => {
|
|
1548
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1552
|
+
try {
|
|
1553
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1556
|
+
}
|
|
1557
|
+
};
|
|
1558
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1559
|
+
deferredOperations.push(operation);
|
|
1560
|
+
} else {
|
|
1561
|
+
immediateOperations.push(operation);
|
|
1299
1562
|
}
|
|
1300
|
-
}
|
|
1301
|
-
await this.executeLayerOperations(
|
|
1563
|
+
}
|
|
1564
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1565
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1302
1566
|
}
|
|
1303
1567
|
async executeLayerOperations(operations, context) {
|
|
1304
1568
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1322,8 +1586,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1322
1586
|
);
|
|
1323
1587
|
}
|
|
1324
1588
|
}
|
|
1325
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1326
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1589
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1590
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1591
|
+
key,
|
|
1592
|
+
layerName,
|
|
1593
|
+
kind,
|
|
1594
|
+
options,
|
|
1595
|
+
fallbackTtl,
|
|
1596
|
+
this.options.negativeTtl,
|
|
1597
|
+
void 0,
|
|
1598
|
+
value
|
|
1599
|
+
);
|
|
1327
1600
|
}
|
|
1328
1601
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1329
1602
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1352,7 +1625,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1352
1625
|
return {
|
|
1353
1626
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1354
1627
|
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1355
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
1628
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
1629
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1356
1630
|
};
|
|
1357
1631
|
}
|
|
1358
1632
|
async deleteKeys(keys) {
|
|
@@ -1412,11 +1686,110 @@ var CacheStack = class extends EventEmitter {
|
|
|
1412
1686
|
return String(error);
|
|
1413
1687
|
}
|
|
1414
1688
|
sleep(ms) {
|
|
1415
|
-
return new Promise((
|
|
1689
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
1416
1690
|
}
|
|
1417
1691
|
shouldBroadcastL1Invalidation() {
|
|
1418
1692
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1419
1693
|
}
|
|
1694
|
+
initializeWriteBehind(options) {
|
|
1695
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1699
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1703
|
+
void this.flushWriteBehindQueue();
|
|
1704
|
+
}, flushIntervalMs);
|
|
1705
|
+
this.writeBehindTimer.unref?.();
|
|
1706
|
+
}
|
|
1707
|
+
shouldWriteBehind(layer) {
|
|
1708
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
1709
|
+
}
|
|
1710
|
+
async enqueueWriteBehind(operation) {
|
|
1711
|
+
this.writeBehindQueue.push(operation);
|
|
1712
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1713
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
1714
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1715
|
+
await this.flushWriteBehindQueue();
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1719
|
+
await this.flushWriteBehindQueue();
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
async flushWriteBehindQueue() {
|
|
1723
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1724
|
+
await this.writeBehindFlushPromise;
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1728
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1729
|
+
this.writeBehindFlushPromise = (async () => {
|
|
1730
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
1731
|
+
})();
|
|
1732
|
+
await this.writeBehindFlushPromise;
|
|
1733
|
+
this.writeBehindFlushPromise = void 0;
|
|
1734
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1735
|
+
await this.flushWriteBehindQueue();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
1739
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
1740
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
1741
|
+
layer.name,
|
|
1742
|
+
options?.staleWhileRevalidate,
|
|
1743
|
+
this.options.staleWhileRevalidate
|
|
1744
|
+
);
|
|
1745
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
1746
|
+
const payload = createStoredValueEnvelope({
|
|
1747
|
+
kind,
|
|
1748
|
+
value,
|
|
1749
|
+
freshTtlSeconds: freshTtl,
|
|
1750
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1751
|
+
staleIfErrorSeconds: staleIfError,
|
|
1752
|
+
now
|
|
1753
|
+
});
|
|
1754
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1755
|
+
return {
|
|
1756
|
+
key,
|
|
1757
|
+
value: payload,
|
|
1758
|
+
ttl
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
intersectKeys(groups) {
|
|
1762
|
+
if (groups.length === 0) {
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
const [firstGroup, ...rest] = groups;
|
|
1766
|
+
if (!firstGroup) {
|
|
1767
|
+
return [];
|
|
1768
|
+
}
|
|
1769
|
+
const restSets = rest.map((group) => new Set(group));
|
|
1770
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
1771
|
+
}
|
|
1772
|
+
qualifyKey(key) {
|
|
1773
|
+
const prefix = this.generationPrefix();
|
|
1774
|
+
return prefix ? `${prefix}${key}` : key;
|
|
1775
|
+
}
|
|
1776
|
+
qualifyPattern(pattern) {
|
|
1777
|
+
const prefix = this.generationPrefix();
|
|
1778
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
1779
|
+
}
|
|
1780
|
+
stripQualifiedKey(key) {
|
|
1781
|
+
const prefix = this.generationPrefix();
|
|
1782
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
1783
|
+
return key;
|
|
1784
|
+
}
|
|
1785
|
+
return key.slice(prefix.length);
|
|
1786
|
+
}
|
|
1787
|
+
generationPrefix() {
|
|
1788
|
+
if (this.currentGeneration === void 0) {
|
|
1789
|
+
return "";
|
|
1790
|
+
}
|
|
1791
|
+
return `v${this.currentGeneration}:`;
|
|
1792
|
+
}
|
|
1420
1793
|
async deleteKeysFromLayers(layers, keys) {
|
|
1421
1794
|
await Promise.all(
|
|
1422
1795
|
layers.map(async (layer) => {
|
|
@@ -1458,8 +1831,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1458
1831
|
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1459
1832
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1460
1833
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1834
|
+
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
1835
|
+
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
1461
1836
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1462
1837
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1838
|
+
if (this.options.generation !== void 0) {
|
|
1839
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
1840
|
+
}
|
|
1463
1841
|
}
|
|
1464
1842
|
validateWriteOptions(options) {
|
|
1465
1843
|
if (!options) {
|
|
@@ -1471,8 +1849,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1471
1849
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1472
1850
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1473
1851
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1852
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1474
1853
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1475
1854
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1855
|
+
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
1476
1856
|
}
|
|
1477
1857
|
validateLayerNumberOption(name, value) {
|
|
1478
1858
|
if (value === void 0) {
|
|
@@ -1497,6 +1877,20 @@ var CacheStack = class extends EventEmitter {
|
|
|
1497
1877
|
throw new Error(`${name} must be a positive finite number.`);
|
|
1498
1878
|
}
|
|
1499
1879
|
}
|
|
1880
|
+
validateRateLimitOptions(name, options) {
|
|
1881
|
+
if (!options) {
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
1885
|
+
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
1886
|
+
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
1887
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
1888
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
1889
|
+
}
|
|
1890
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
1891
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1500
1894
|
validateNonNegativeNumber(name, value) {
|
|
1501
1895
|
if (!Number.isFinite(value) || value < 0) {
|
|
1502
1896
|
throw new Error(`${name} must be a non-negative finite number.`);
|
|
@@ -1514,6 +1908,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1514
1908
|
}
|
|
1515
1909
|
return key;
|
|
1516
1910
|
}
|
|
1911
|
+
validateTtlPolicy(name, policy) {
|
|
1912
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
if ("alignTo" in policy) {
|
|
1916
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1919
|
+
throw new Error(`${name} is invalid.`);
|
|
1920
|
+
}
|
|
1921
|
+
assertActive(operation) {
|
|
1922
|
+
if (this.isDisconnecting) {
|
|
1923
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
async awaitStartup(operation) {
|
|
1927
|
+
this.assertActive(operation);
|
|
1928
|
+
await this.startup;
|
|
1929
|
+
this.assertActive(operation);
|
|
1930
|
+
}
|
|
1517
1931
|
serializeOptions(options) {
|
|
1518
1932
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1519
1933
|
}
|
|
@@ -1619,18 +2033,23 @@ var CacheStack = class extends EventEmitter {
|
|
|
1619
2033
|
return value;
|
|
1620
2034
|
}
|
|
1621
2035
|
};
|
|
2036
|
+
function createInstanceId() {
|
|
2037
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2038
|
+
}
|
|
1622
2039
|
|
|
1623
2040
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1624
2041
|
var RedisInvalidationBus = class {
|
|
1625
2042
|
channel;
|
|
1626
2043
|
publisher;
|
|
1627
2044
|
subscriber;
|
|
2045
|
+
logger;
|
|
1628
2046
|
handlers = /* @__PURE__ */ new Set();
|
|
1629
2047
|
sharedListener;
|
|
1630
2048
|
constructor(options) {
|
|
1631
2049
|
this.publisher = options.publisher;
|
|
1632
2050
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1633
2051
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
2052
|
+
this.logger = options.logger;
|
|
1634
2053
|
}
|
|
1635
2054
|
async subscribe(handler) {
|
|
1636
2055
|
if (this.handlers.size === 0) {
|
|
@@ -1687,6 +2106,10 @@ var RedisInvalidationBus = class {
|
|
|
1687
2106
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1688
2107
|
}
|
|
1689
2108
|
reportError(message, error) {
|
|
2109
|
+
if (this.logger?.error) {
|
|
2110
|
+
this.logger.error(message, { error });
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
1690
2113
|
console.error(`[layercache] ${message}`, error);
|
|
1691
2114
|
}
|
|
1692
2115
|
};
|
|
@@ -1739,32 +2162,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1739
2162
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
1740
2163
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
1741
2164
|
return async (req, res, next) => {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
1748
|
-
const cached = await cache.get(key, void 0, options);
|
|
1749
|
-
if (cached !== null) {
|
|
1750
|
-
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1751
|
-
res.setHeader?.("x-cache", "HIT");
|
|
1752
|
-
if (res.json) {
|
|
1753
|
-
res.json(cached);
|
|
1754
|
-
} else {
|
|
1755
|
-
res.end?.(JSON.stringify(cached));
|
|
2165
|
+
try {
|
|
2166
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2167
|
+
if (!allowedMethods.has(method)) {
|
|
2168
|
+
next();
|
|
2169
|
+
return;
|
|
1756
2170
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
res.
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
2171
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
2172
|
+
const cached = await cache.get(key, void 0, options);
|
|
2173
|
+
if (cached !== null) {
|
|
2174
|
+
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2175
|
+
res.setHeader?.("x-cache", "HIT");
|
|
2176
|
+
if (res.json) {
|
|
2177
|
+
res.json(cached);
|
|
2178
|
+
} else {
|
|
2179
|
+
res.end?.(JSON.stringify(cached));
|
|
2180
|
+
}
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
const originalJson = res.json?.bind(res);
|
|
2184
|
+
if (originalJson) {
|
|
2185
|
+
res.json = (body) => {
|
|
2186
|
+
res.setHeader?.("x-cache", "MISS");
|
|
2187
|
+
void cache.set(key, body, options);
|
|
2188
|
+
return originalJson(body);
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
next();
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
next(error);
|
|
1766
2194
|
}
|
|
1767
|
-
next();
|
|
1768
2195
|
};
|
|
1769
2196
|
}
|
|
1770
2197
|
|
|
@@ -1777,6 +2204,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1777
2204
|
return (...args) => wrapped(...args);
|
|
1778
2205
|
}
|
|
1779
2206
|
|
|
2207
|
+
// src/integrations/opentelemetry.ts
|
|
2208
|
+
function createOpenTelemetryPlugin(cache, tracer) {
|
|
2209
|
+
const originals = {
|
|
2210
|
+
get: cache.get.bind(cache),
|
|
2211
|
+
set: cache.set.bind(cache),
|
|
2212
|
+
delete: cache.delete.bind(cache),
|
|
2213
|
+
mget: cache.mget.bind(cache),
|
|
2214
|
+
mset: cache.mset.bind(cache),
|
|
2215
|
+
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
2216
|
+
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
2217
|
+
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
2218
|
+
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
2219
|
+
};
|
|
2220
|
+
cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
|
|
2221
|
+
"layercache.key": String(args[0] ?? "")
|
|
2222
|
+
}));
|
|
2223
|
+
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
2224
|
+
"layercache.key": String(args[0] ?? "")
|
|
2225
|
+
}));
|
|
2226
|
+
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
2227
|
+
"layercache.key": String(args[0] ?? "")
|
|
2228
|
+
}));
|
|
2229
|
+
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
2230
|
+
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
2231
|
+
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
2232
|
+
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
2233
|
+
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
2234
|
+
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
2235
|
+
return {
|
|
2236
|
+
uninstall() {
|
|
2237
|
+
cache.get = originals.get;
|
|
2238
|
+
cache.set = originals.set;
|
|
2239
|
+
cache.delete = originals.delete;
|
|
2240
|
+
cache.mget = originals.mget;
|
|
2241
|
+
cache.mset = originals.mset;
|
|
2242
|
+
cache.invalidateByTag = originals.invalidateByTag;
|
|
2243
|
+
cache.invalidateByTags = originals.invalidateByTags;
|
|
2244
|
+
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
2245
|
+
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
2246
|
+
}
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
function instrument(name, tracer, method, attributes) {
|
|
2250
|
+
return (async (...args) => {
|
|
2251
|
+
const span = tracer.startSpan(name, { attributes: attributes?.(args) });
|
|
2252
|
+
try {
|
|
2253
|
+
const result = await method(...args);
|
|
2254
|
+
span.setAttribute?.("layercache.success", true);
|
|
2255
|
+
if (result === null) {
|
|
2256
|
+
span.setAttribute?.("layercache.result", "null");
|
|
2257
|
+
}
|
|
2258
|
+
return result;
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
span.setAttribute?.("layercache.success", false);
|
|
2261
|
+
span.recordException?.(error);
|
|
2262
|
+
throw error;
|
|
2263
|
+
} finally {
|
|
2264
|
+
span.end();
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
|
|
1780
2269
|
// src/integrations/trpc.ts
|
|
1781
2270
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1782
2271
|
return async (context) => {
|
|
@@ -1802,177 +2291,40 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
|
1802
2291
|
};
|
|
1803
2292
|
}
|
|
1804
2293
|
|
|
1805
|
-
// src/layers/MemoryLayer.ts
|
|
1806
|
-
var MemoryLayer = class {
|
|
1807
|
-
name;
|
|
1808
|
-
defaultTtl;
|
|
1809
|
-
isLocal = true;
|
|
1810
|
-
maxSize;
|
|
1811
|
-
evictionPolicy;
|
|
1812
|
-
entries = /* @__PURE__ */ new Map();
|
|
1813
|
-
constructor(options = {}) {
|
|
1814
|
-
this.name = options.name ?? "memory";
|
|
1815
|
-
this.defaultTtl = options.ttl;
|
|
1816
|
-
this.maxSize = options.maxSize ?? 1e3;
|
|
1817
|
-
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
1818
|
-
}
|
|
1819
|
-
async get(key) {
|
|
1820
|
-
const value = await this.getEntry(key);
|
|
1821
|
-
return unwrapStoredValue(value);
|
|
1822
|
-
}
|
|
1823
|
-
async getEntry(key) {
|
|
1824
|
-
const entry = this.entries.get(key);
|
|
1825
|
-
if (!entry) {
|
|
1826
|
-
return null;
|
|
1827
|
-
}
|
|
1828
|
-
if (this.isExpired(entry)) {
|
|
1829
|
-
this.entries.delete(key);
|
|
1830
|
-
return null;
|
|
1831
|
-
}
|
|
1832
|
-
if (this.evictionPolicy === "lru") {
|
|
1833
|
-
this.entries.delete(key);
|
|
1834
|
-
entry.accessCount += 1;
|
|
1835
|
-
this.entries.set(key, entry);
|
|
1836
|
-
} else if (this.evictionPolicy === "lfu") {
|
|
1837
|
-
entry.accessCount += 1;
|
|
1838
|
-
}
|
|
1839
|
-
return entry.value;
|
|
1840
|
-
}
|
|
1841
|
-
async getMany(keys) {
|
|
1842
|
-
const values = [];
|
|
1843
|
-
for (const key of keys) {
|
|
1844
|
-
values.push(await this.getEntry(key));
|
|
1845
|
-
}
|
|
1846
|
-
return values;
|
|
1847
|
-
}
|
|
1848
|
-
async set(key, value, ttl = this.defaultTtl) {
|
|
1849
|
-
this.entries.delete(key);
|
|
1850
|
-
this.entries.set(key, {
|
|
1851
|
-
value,
|
|
1852
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1853
|
-
accessCount: 0,
|
|
1854
|
-
insertedAt: Date.now()
|
|
1855
|
-
});
|
|
1856
|
-
while (this.entries.size > this.maxSize) {
|
|
1857
|
-
this.evict();
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
async has(key) {
|
|
1861
|
-
const entry = this.entries.get(key);
|
|
1862
|
-
if (!entry) {
|
|
1863
|
-
return false;
|
|
1864
|
-
}
|
|
1865
|
-
if (this.isExpired(entry)) {
|
|
1866
|
-
this.entries.delete(key);
|
|
1867
|
-
return false;
|
|
1868
|
-
}
|
|
1869
|
-
return true;
|
|
1870
|
-
}
|
|
1871
|
-
async ttl(key) {
|
|
1872
|
-
const entry = this.entries.get(key);
|
|
1873
|
-
if (!entry) {
|
|
1874
|
-
return null;
|
|
1875
|
-
}
|
|
1876
|
-
if (this.isExpired(entry)) {
|
|
1877
|
-
this.entries.delete(key);
|
|
1878
|
-
return null;
|
|
1879
|
-
}
|
|
1880
|
-
if (entry.expiresAt === null) {
|
|
1881
|
-
return null;
|
|
1882
|
-
}
|
|
1883
|
-
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
1884
|
-
}
|
|
1885
|
-
async size() {
|
|
1886
|
-
this.pruneExpired();
|
|
1887
|
-
return this.entries.size;
|
|
1888
|
-
}
|
|
1889
|
-
async delete(key) {
|
|
1890
|
-
this.entries.delete(key);
|
|
1891
|
-
}
|
|
1892
|
-
async deleteMany(keys) {
|
|
1893
|
-
for (const key of keys) {
|
|
1894
|
-
this.entries.delete(key);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
async clear() {
|
|
1898
|
-
this.entries.clear();
|
|
1899
|
-
}
|
|
1900
|
-
async keys() {
|
|
1901
|
-
this.pruneExpired();
|
|
1902
|
-
return [...this.entries.keys()];
|
|
1903
|
-
}
|
|
1904
|
-
exportState() {
|
|
1905
|
-
this.pruneExpired();
|
|
1906
|
-
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1907
|
-
key,
|
|
1908
|
-
value: entry.value,
|
|
1909
|
-
expiresAt: entry.expiresAt
|
|
1910
|
-
}));
|
|
1911
|
-
}
|
|
1912
|
-
importState(entries) {
|
|
1913
|
-
for (const entry of entries) {
|
|
1914
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
this.entries.set(entry.key, {
|
|
1918
|
-
value: entry.value,
|
|
1919
|
-
expiresAt: entry.expiresAt,
|
|
1920
|
-
accessCount: 0,
|
|
1921
|
-
insertedAt: Date.now()
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
while (this.entries.size > this.maxSize) {
|
|
1925
|
-
this.evict();
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
evict() {
|
|
1929
|
-
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1930
|
-
const oldestKey = this.entries.keys().next().value;
|
|
1931
|
-
if (oldestKey !== void 0) {
|
|
1932
|
-
this.entries.delete(oldestKey);
|
|
1933
|
-
}
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
let victimKey;
|
|
1937
|
-
let minCount = Number.POSITIVE_INFINITY;
|
|
1938
|
-
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1939
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1940
|
-
if (entry.accessCount < minCount || entry.accessCount === minCount && entry.insertedAt < minInsertedAt) {
|
|
1941
|
-
minCount = entry.accessCount;
|
|
1942
|
-
minInsertedAt = entry.insertedAt;
|
|
1943
|
-
victimKey = key;
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
if (victimKey !== void 0) {
|
|
1947
|
-
this.entries.delete(victimKey);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
pruneExpired() {
|
|
1951
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1952
|
-
if (this.isExpired(entry)) {
|
|
1953
|
-
this.entries.delete(key);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
isExpired(entry) {
|
|
1958
|
-
return entry.expiresAt !== null && entry.expiresAt <= Date.now();
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
|
|
1962
2294
|
// src/layers/RedisLayer.ts
|
|
1963
2295
|
import { promisify } from "util";
|
|
1964
2296
|
import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
|
|
1965
2297
|
|
|
1966
2298
|
// src/serialization/JsonSerializer.ts
|
|
2299
|
+
var DANGEROUS_JSON_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
|
|
1967
2300
|
var JsonSerializer = class {
|
|
1968
2301
|
serialize(value) {
|
|
1969
2302
|
return JSON.stringify(value);
|
|
1970
2303
|
}
|
|
1971
2304
|
deserialize(payload) {
|
|
1972
2305
|
const normalized = Buffer.isBuffer(payload) ? payload.toString("utf8") : payload;
|
|
1973
|
-
return JSON.parse(normalized);
|
|
2306
|
+
return sanitizeJsonValue(JSON.parse(normalized));
|
|
1974
2307
|
}
|
|
1975
2308
|
};
|
|
2309
|
+
function sanitizeJsonValue(value) {
|
|
2310
|
+
if (Array.isArray(value)) {
|
|
2311
|
+
return value.map((entry) => sanitizeJsonValue(entry));
|
|
2312
|
+
}
|
|
2313
|
+
if (!isPlainObject(value)) {
|
|
2314
|
+
return value;
|
|
2315
|
+
}
|
|
2316
|
+
const sanitized = {};
|
|
2317
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
2318
|
+
if (DANGEROUS_JSON_KEYS.has(key)) {
|
|
2319
|
+
continue;
|
|
2320
|
+
}
|
|
2321
|
+
sanitized[key] = sanitizeJsonValue(entry);
|
|
2322
|
+
}
|
|
2323
|
+
return sanitized;
|
|
2324
|
+
}
|
|
2325
|
+
function isPlainObject(value) {
|
|
2326
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
2327
|
+
}
|
|
1976
2328
|
|
|
1977
2329
|
// src/layers/RedisLayer.ts
|
|
1978
2330
|
var BATCH_DELETE_SIZE = 500;
|
|
@@ -1985,22 +2337,24 @@ var RedisLayer = class {
|
|
|
1985
2337
|
defaultTtl;
|
|
1986
2338
|
isLocal = false;
|
|
1987
2339
|
client;
|
|
1988
|
-
|
|
2340
|
+
serializers;
|
|
1989
2341
|
prefix;
|
|
1990
2342
|
allowUnprefixedClear;
|
|
1991
2343
|
scanCount;
|
|
1992
2344
|
compression;
|
|
1993
2345
|
compressionThreshold;
|
|
2346
|
+
disconnectOnDispose;
|
|
1994
2347
|
constructor(options) {
|
|
1995
2348
|
this.client = options.client;
|
|
1996
2349
|
this.defaultTtl = options.ttl;
|
|
1997
2350
|
this.name = options.name ?? "redis";
|
|
1998
|
-
this.
|
|
2351
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
1999
2352
|
this.prefix = options.prefix ?? "";
|
|
2000
2353
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
2001
2354
|
this.scanCount = options.scanCount ?? 100;
|
|
2002
2355
|
this.compression = options.compression;
|
|
2003
2356
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2357
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
2004
2358
|
}
|
|
2005
2359
|
async get(key) {
|
|
2006
2360
|
const payload = await this.getEntry(key);
|
|
@@ -2035,8 +2389,25 @@ var RedisLayer = class {
|
|
|
2035
2389
|
})
|
|
2036
2390
|
);
|
|
2037
2391
|
}
|
|
2392
|
+
async setMany(entries) {
|
|
2393
|
+
if (entries.length === 0) {
|
|
2394
|
+
return;
|
|
2395
|
+
}
|
|
2396
|
+
const pipeline = this.client.pipeline();
|
|
2397
|
+
for (const entry of entries) {
|
|
2398
|
+
const serialized = this.primarySerializer().serialize(entry.value);
|
|
2399
|
+
const payload = await this.encodePayload(serialized);
|
|
2400
|
+
const normalizedKey = this.withPrefix(entry.key);
|
|
2401
|
+
if (entry.ttl && entry.ttl > 0) {
|
|
2402
|
+
pipeline.set(normalizedKey, payload, "EX", entry.ttl);
|
|
2403
|
+
} else {
|
|
2404
|
+
pipeline.set(normalizedKey, payload);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
await pipeline.exec();
|
|
2408
|
+
}
|
|
2038
2409
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2039
|
-
const serialized = this.
|
|
2410
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2040
2411
|
const payload = await this.encodePayload(serialized);
|
|
2041
2412
|
const normalizedKey = this.withPrefix(key);
|
|
2042
2413
|
if (ttl && ttl > 0) {
|
|
@@ -2069,6 +2440,18 @@ var RedisLayer = class {
|
|
|
2069
2440
|
const keys = await this.keys();
|
|
2070
2441
|
return keys.length;
|
|
2071
2442
|
}
|
|
2443
|
+
async ping() {
|
|
2444
|
+
try {
|
|
2445
|
+
return await this.client.ping() === "PONG";
|
|
2446
|
+
} catch {
|
|
2447
|
+
return false;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
async dispose() {
|
|
2451
|
+
if (this.disconnectOnDispose) {
|
|
2452
|
+
this.client.disconnect();
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2072
2455
|
/**
|
|
2073
2456
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2074
2457
|
* loading millions of keys into memory at once.
|
|
@@ -2115,12 +2498,39 @@ var RedisLayer = class {
|
|
|
2115
2498
|
return `${this.prefix}${key}`;
|
|
2116
2499
|
}
|
|
2117
2500
|
async deserializeOrDelete(key, payload) {
|
|
2501
|
+
const decodedPayload = await this.decodePayload(payload);
|
|
2502
|
+
for (const serializer of this.serializers) {
|
|
2503
|
+
try {
|
|
2504
|
+
const value = serializer.deserialize(decodedPayload);
|
|
2505
|
+
if (serializer !== this.primarySerializer()) {
|
|
2506
|
+
await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
|
|
2507
|
+
}
|
|
2508
|
+
return value;
|
|
2509
|
+
} catch {
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2118
2512
|
try {
|
|
2119
|
-
return this.serializer.deserialize(await this.decodePayload(payload));
|
|
2120
|
-
} catch {
|
|
2121
2513
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2122
|
-
|
|
2514
|
+
} catch {
|
|
2515
|
+
}
|
|
2516
|
+
return null;
|
|
2517
|
+
}
|
|
2518
|
+
async rewriteWithPrimarySerializer(key, value) {
|
|
2519
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2520
|
+
const payload = await this.encodePayload(serialized);
|
|
2521
|
+
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
2522
|
+
if (ttl > 0) {
|
|
2523
|
+
await this.client.set(this.withPrefix(key), payload, "EX", ttl);
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
await this.client.set(this.withPrefix(key), payload);
|
|
2527
|
+
}
|
|
2528
|
+
primarySerializer() {
|
|
2529
|
+
const serializer = this.serializers[0];
|
|
2530
|
+
if (!serializer) {
|
|
2531
|
+
throw new Error("RedisLayer requires at least one serializer.");
|
|
2123
2532
|
}
|
|
2533
|
+
return serializer;
|
|
2124
2534
|
}
|
|
2125
2535
|
isSerializablePayload(payload) {
|
|
2126
2536
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
@@ -2160,8 +2570,8 @@ var RedisLayer = class {
|
|
|
2160
2570
|
|
|
2161
2571
|
// src/layers/DiskLayer.ts
|
|
2162
2572
|
import { createHash } from "crypto";
|
|
2163
|
-
import { promises as
|
|
2164
|
-
import { join } from "path";
|
|
2573
|
+
import { promises as fs } from "fs";
|
|
2574
|
+
import { join, resolve } from "path";
|
|
2165
2575
|
var DiskLayer = class {
|
|
2166
2576
|
name;
|
|
2167
2577
|
defaultTtl;
|
|
@@ -2169,12 +2579,13 @@ var DiskLayer = class {
|
|
|
2169
2579
|
directory;
|
|
2170
2580
|
serializer;
|
|
2171
2581
|
maxFiles;
|
|
2582
|
+
writeQueue = Promise.resolve();
|
|
2172
2583
|
constructor(options) {
|
|
2173
|
-
this.directory = options.directory;
|
|
2584
|
+
this.directory = this.resolveDirectory(options.directory);
|
|
2174
2585
|
this.defaultTtl = options.ttl;
|
|
2175
2586
|
this.name = options.name ?? "disk";
|
|
2176
2587
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2177
|
-
this.maxFiles = options.maxFiles;
|
|
2588
|
+
this.maxFiles = this.normalizeMaxFiles(options.maxFiles);
|
|
2178
2589
|
}
|
|
2179
2590
|
async get(key) {
|
|
2180
2591
|
return unwrapStoredValue(await this.getEntry(key));
|
|
@@ -2183,13 +2594,13 @@ var DiskLayer = class {
|
|
|
2183
2594
|
const filePath = this.keyToPath(key);
|
|
2184
2595
|
let raw;
|
|
2185
2596
|
try {
|
|
2186
|
-
raw = await
|
|
2597
|
+
raw = await fs.readFile(filePath);
|
|
2187
2598
|
} catch {
|
|
2188
2599
|
return null;
|
|
2189
2600
|
}
|
|
2190
2601
|
let entry;
|
|
2191
2602
|
try {
|
|
2192
|
-
entry = this.
|
|
2603
|
+
entry = this.deserializeEntry(raw);
|
|
2193
2604
|
} catch {
|
|
2194
2605
|
await this.safeDelete(filePath);
|
|
2195
2606
|
return null;
|
|
@@ -2201,16 +2612,29 @@ var DiskLayer = class {
|
|
|
2201
2612
|
return entry.value;
|
|
2202
2613
|
}
|
|
2203
2614
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2204
|
-
await
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2615
|
+
await this.enqueueWrite(async () => {
|
|
2616
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
2617
|
+
const entry = {
|
|
2618
|
+
key,
|
|
2619
|
+
value,
|
|
2620
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
2621
|
+
};
|
|
2622
|
+
const payload = this.serializer.serialize(entry);
|
|
2623
|
+
const targetPath = this.keyToPath(key);
|
|
2624
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2625
|
+
await fs.writeFile(tempPath, payload);
|
|
2626
|
+
await fs.rename(tempPath, targetPath);
|
|
2627
|
+
if (this.maxFiles !== void 0) {
|
|
2628
|
+
await this.enforceMaxFiles();
|
|
2629
|
+
}
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2632
|
+
async getMany(keys) {
|
|
2633
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
2634
|
+
}
|
|
2635
|
+
async setMany(entries) {
|
|
2636
|
+
for (const entry of entries) {
|
|
2637
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2214
2638
|
}
|
|
2215
2639
|
}
|
|
2216
2640
|
async has(key) {
|
|
@@ -2221,14 +2645,15 @@ var DiskLayer = class {
|
|
|
2221
2645
|
const filePath = this.keyToPath(key);
|
|
2222
2646
|
let raw;
|
|
2223
2647
|
try {
|
|
2224
|
-
raw = await
|
|
2648
|
+
raw = await fs.readFile(filePath);
|
|
2225
2649
|
} catch {
|
|
2226
2650
|
return null;
|
|
2227
2651
|
}
|
|
2228
2652
|
let entry;
|
|
2229
2653
|
try {
|
|
2230
|
-
entry = this.
|
|
2654
|
+
entry = this.deserializeEntry(raw);
|
|
2231
2655
|
} catch {
|
|
2656
|
+
await this.safeDelete(filePath);
|
|
2232
2657
|
return null;
|
|
2233
2658
|
}
|
|
2234
2659
|
if (entry.expiresAt === null) {
|
|
@@ -2241,21 +2666,25 @@ var DiskLayer = class {
|
|
|
2241
2666
|
return remaining;
|
|
2242
2667
|
}
|
|
2243
2668
|
async delete(key) {
|
|
2244
|
-
await this.safeDelete(this.keyToPath(key));
|
|
2669
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2245
2670
|
}
|
|
2246
2671
|
async deleteMany(keys) {
|
|
2247
|
-
await
|
|
2672
|
+
await this.enqueueWrite(async () => {
|
|
2673
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
2674
|
+
});
|
|
2248
2675
|
}
|
|
2249
2676
|
async clear() {
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2677
|
+
await this.enqueueWrite(async () => {
|
|
2678
|
+
let entries;
|
|
2679
|
+
try {
|
|
2680
|
+
entries = await fs.readdir(this.directory);
|
|
2681
|
+
} catch {
|
|
2682
|
+
return;
|
|
2683
|
+
}
|
|
2684
|
+
await Promise.all(
|
|
2685
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
|
|
2686
|
+
);
|
|
2687
|
+
});
|
|
2259
2688
|
}
|
|
2260
2689
|
/**
|
|
2261
2690
|
* Returns the original cache key strings stored on disk.
|
|
@@ -2264,7 +2693,7 @@ var DiskLayer = class {
|
|
|
2264
2693
|
async keys() {
|
|
2265
2694
|
let entries;
|
|
2266
2695
|
try {
|
|
2267
|
-
entries = await
|
|
2696
|
+
entries = await fs.readdir(this.directory);
|
|
2268
2697
|
} catch {
|
|
2269
2698
|
return [];
|
|
2270
2699
|
}
|
|
@@ -2275,13 +2704,13 @@ var DiskLayer = class {
|
|
|
2275
2704
|
const filePath = join(this.directory, name);
|
|
2276
2705
|
let raw;
|
|
2277
2706
|
try {
|
|
2278
|
-
raw = await
|
|
2707
|
+
raw = await fs.readFile(filePath);
|
|
2279
2708
|
} catch {
|
|
2280
2709
|
return;
|
|
2281
2710
|
}
|
|
2282
2711
|
let entry;
|
|
2283
2712
|
try {
|
|
2284
|
-
entry = this.
|
|
2713
|
+
entry = this.deserializeEntry(raw);
|
|
2285
2714
|
} catch {
|
|
2286
2715
|
await this.safeDelete(filePath);
|
|
2287
2716
|
return;
|
|
@@ -2299,16 +2728,56 @@ var DiskLayer = class {
|
|
|
2299
2728
|
const keys = await this.keys();
|
|
2300
2729
|
return keys.length;
|
|
2301
2730
|
}
|
|
2731
|
+
async ping() {
|
|
2732
|
+
try {
|
|
2733
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
2734
|
+
return true;
|
|
2735
|
+
} catch {
|
|
2736
|
+
return false;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
async dispose() {
|
|
2740
|
+
}
|
|
2302
2741
|
keyToPath(key) {
|
|
2303
2742
|
const hash = createHash("sha256").update(key).digest("hex");
|
|
2304
2743
|
return join(this.directory, `${hash}.lc`);
|
|
2305
2744
|
}
|
|
2745
|
+
resolveDirectory(directory) {
|
|
2746
|
+
if (typeof directory !== "string" || directory.trim().length === 0) {
|
|
2747
|
+
throw new Error("DiskLayer.directory must be a non-empty path.");
|
|
2748
|
+
}
|
|
2749
|
+
if (directory.includes("\0")) {
|
|
2750
|
+
throw new Error("DiskLayer.directory must not contain null bytes.");
|
|
2751
|
+
}
|
|
2752
|
+
return resolve(directory);
|
|
2753
|
+
}
|
|
2754
|
+
normalizeMaxFiles(maxFiles) {
|
|
2755
|
+
if (maxFiles === void 0) {
|
|
2756
|
+
return void 0;
|
|
2757
|
+
}
|
|
2758
|
+
if (!Number.isInteger(maxFiles) || maxFiles <= 0) {
|
|
2759
|
+
throw new Error("DiskLayer.maxFiles must be a positive integer.");
|
|
2760
|
+
}
|
|
2761
|
+
return maxFiles;
|
|
2762
|
+
}
|
|
2763
|
+
deserializeEntry(raw) {
|
|
2764
|
+
const entry = this.serializer.deserialize(raw);
|
|
2765
|
+
if (!isDiskEntry(entry)) {
|
|
2766
|
+
throw new Error("Invalid disk cache entry.");
|
|
2767
|
+
}
|
|
2768
|
+
return entry;
|
|
2769
|
+
}
|
|
2306
2770
|
async safeDelete(filePath) {
|
|
2307
2771
|
try {
|
|
2308
|
-
await
|
|
2772
|
+
await fs.unlink(filePath);
|
|
2309
2773
|
} catch {
|
|
2310
2774
|
}
|
|
2311
2775
|
}
|
|
2776
|
+
enqueueWrite(operation) {
|
|
2777
|
+
const next = this.writeQueue.then(operation, operation);
|
|
2778
|
+
this.writeQueue = next.catch(() => void 0);
|
|
2779
|
+
return next;
|
|
2780
|
+
}
|
|
2312
2781
|
/**
|
|
2313
2782
|
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
2314
2783
|
*/
|
|
@@ -2318,7 +2787,7 @@ var DiskLayer = class {
|
|
|
2318
2787
|
}
|
|
2319
2788
|
let entries;
|
|
2320
2789
|
try {
|
|
2321
|
-
entries = await
|
|
2790
|
+
entries = await fs.readdir(this.directory);
|
|
2322
2791
|
} catch {
|
|
2323
2792
|
return;
|
|
2324
2793
|
}
|
|
@@ -2330,7 +2799,7 @@ var DiskLayer = class {
|
|
|
2330
2799
|
lcFiles.map(async (name) => {
|
|
2331
2800
|
const filePath = join(this.directory, name);
|
|
2332
2801
|
try {
|
|
2333
|
-
const stat = await
|
|
2802
|
+
const stat = await fs.stat(filePath);
|
|
2334
2803
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
2335
2804
|
} catch {
|
|
2336
2805
|
return { filePath, mtimeMs: 0 };
|
|
@@ -2342,6 +2811,14 @@ var DiskLayer = class {
|
|
|
2342
2811
|
await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
|
|
2343
2812
|
}
|
|
2344
2813
|
};
|
|
2814
|
+
function isDiskEntry(value) {
|
|
2815
|
+
if (!value || typeof value !== "object") {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
const candidate = value;
|
|
2819
|
+
const validExpiry = candidate.expiresAt === null || typeof candidate.expiresAt === "number";
|
|
2820
|
+
return typeof candidate.key === "string" && validExpiry && "value" in candidate;
|
|
2821
|
+
}
|
|
2345
2822
|
|
|
2346
2823
|
// src/layers/MemcachedLayer.ts
|
|
2347
2824
|
var MemcachedLayer = class {
|
|
@@ -2414,13 +2891,19 @@ var MsgpackSerializer = class {
|
|
|
2414
2891
|
};
|
|
2415
2892
|
|
|
2416
2893
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2417
|
-
import { randomUUID
|
|
2894
|
+
import { randomUUID } from "crypto";
|
|
2418
2895
|
var RELEASE_SCRIPT = `
|
|
2419
2896
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2420
2897
|
return redis.call("del", KEYS[1])
|
|
2421
2898
|
end
|
|
2422
2899
|
return 0
|
|
2423
2900
|
`;
|
|
2901
|
+
var RENEW_SCRIPT = `
|
|
2902
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2903
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
2904
|
+
end
|
|
2905
|
+
return 0
|
|
2906
|
+
`;
|
|
2424
2907
|
var RedisSingleFlightCoordinator = class {
|
|
2425
2908
|
client;
|
|
2426
2909
|
prefix;
|
|
@@ -2430,17 +2913,32 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2430
2913
|
}
|
|
2431
2914
|
async execute(key, options, worker, waiter) {
|
|
2432
2915
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2433
|
-
const token =
|
|
2916
|
+
const token = randomUUID();
|
|
2434
2917
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2435
2918
|
if (acquired === "OK") {
|
|
2919
|
+
const renewTimer = this.startLeaseRenewal(lockKey, token, options);
|
|
2436
2920
|
try {
|
|
2437
2921
|
return await worker();
|
|
2438
2922
|
} finally {
|
|
2923
|
+
if (renewTimer) {
|
|
2924
|
+
clearInterval(renewTimer);
|
|
2925
|
+
}
|
|
2439
2926
|
await this.client.eval(RELEASE_SCRIPT, 1, lockKey, token);
|
|
2440
2927
|
}
|
|
2441
2928
|
}
|
|
2442
2929
|
return waiter();
|
|
2443
2930
|
}
|
|
2931
|
+
startLeaseRenewal(lockKey, token, options) {
|
|
2932
|
+
const renewIntervalMs = options.renewIntervalMs ?? Math.max(100, Math.floor(options.leaseMs / 2));
|
|
2933
|
+
if (renewIntervalMs <= 0 || renewIntervalMs >= options.leaseMs) {
|
|
2934
|
+
return void 0;
|
|
2935
|
+
}
|
|
2936
|
+
const timer = setInterval(() => {
|
|
2937
|
+
void this.client.eval(RENEW_SCRIPT, 1, lockKey, token, String(options.leaseMs)).catch(() => void 0);
|
|
2938
|
+
}, renewIntervalMs);
|
|
2939
|
+
timer.unref?.();
|
|
2940
|
+
return timer;
|
|
2941
|
+
}
|
|
2444
2942
|
};
|
|
2445
2943
|
|
|
2446
2944
|
// src/metrics/PrometheusExporter.ts
|
|
@@ -2542,6 +3040,8 @@ export {
|
|
|
2542
3040
|
createCachedMethodDecorator,
|
|
2543
3041
|
createExpressCacheMiddleware,
|
|
2544
3042
|
createFastifyLayercachePlugin,
|
|
3043
|
+
createHonoCacheMiddleware,
|
|
3044
|
+
createOpenTelemetryPlugin,
|
|
2545
3045
|
createPrometheusMetricsExporter,
|
|
2546
3046
|
createTrpcCacheMiddleware
|
|
2547
3047
|
};
|