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.js
CHANGED
|
@@ -1,66 +1,103 @@
|
|
|
1
1
|
import {
|
|
2
|
-
PatternMatcher,
|
|
3
2
|
RedisTagIndex
|
|
4
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-GF47Y3XR.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
|
|
12
|
-
|
|
24
|
+
import { Mutex } from "async-mutex";
|
|
25
|
+
var CacheNamespace = class _CacheNamespace {
|
|
13
26
|
constructor(cache, prefix) {
|
|
14
27
|
this.cache = cache;
|
|
15
28
|
this.prefix = prefix;
|
|
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));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
42
|
+
*/
|
|
43
|
+
async getOrThrow(key, fetcher, options) {
|
|
44
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
24
45
|
}
|
|
25
46
|
async has(key) {
|
|
26
|
-
return this.cache.has(this.qualify(key));
|
|
47
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
27
48
|
}
|
|
28
49
|
async ttl(key) {
|
|
29
|
-
return this.cache.ttl(this.qualify(key));
|
|
50
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
30
51
|
}
|
|
31
52
|
async set(key, value, options) {
|
|
32
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
53
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
33
54
|
}
|
|
34
55
|
async delete(key) {
|
|
35
|
-
await this.cache.delete(this.qualify(key));
|
|
56
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
36
57
|
}
|
|
37
58
|
async mdelete(keys) {
|
|
38
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
59
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
39
60
|
}
|
|
40
61
|
async clear() {
|
|
41
|
-
await this.cache.
|
|
62
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
42
63
|
}
|
|
43
64
|
async mget(entries) {
|
|
44
|
-
return this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
return this.trackMetrics(
|
|
66
|
+
() => this.cache.mget(
|
|
67
|
+
entries.map((entry) => ({
|
|
68
|
+
...entry,
|
|
69
|
+
key: this.qualify(entry.key)
|
|
70
|
+
}))
|
|
71
|
+
)
|
|
49
72
|
);
|
|
50
73
|
}
|
|
51
74
|
async mset(entries) {
|
|
52
|
-
await this.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
await this.trackMetrics(
|
|
76
|
+
() => this.cache.mset(
|
|
77
|
+
entries.map((entry) => ({
|
|
78
|
+
...entry,
|
|
79
|
+
key: this.qualify(entry.key)
|
|
80
|
+
}))
|
|
81
|
+
)
|
|
57
82
|
);
|
|
58
83
|
}
|
|
59
84
|
async invalidateByTag(tag) {
|
|
60
|
-
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));
|
|
61
89
|
}
|
|
62
90
|
async invalidateByPattern(pattern) {
|
|
63
|
-
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)));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
98
|
+
*/
|
|
99
|
+
async inspect(key) {
|
|
100
|
+
return this.cache.inspect(this.qualify(key));
|
|
64
101
|
}
|
|
65
102
|
wrap(keyPrefix, fetcher, options) {
|
|
66
103
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
@@ -75,15 +112,159 @@ var CacheNamespace = class {
|
|
|
75
112
|
);
|
|
76
113
|
}
|
|
77
114
|
getMetrics() {
|
|
78
|
-
return this.
|
|
115
|
+
return cloneMetrics(this.metrics);
|
|
79
116
|
}
|
|
80
117
|
getHitRate() {
|
|
81
|
-
|
|
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 };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
131
|
+
*
|
|
132
|
+
* ```ts
|
|
133
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
134
|
+
* const posts = tenant.namespace('posts')
|
|
135
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
namespace(childPrefix) {
|
|
139
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
82
140
|
}
|
|
83
141
|
qualify(key) {
|
|
84
142
|
return `${this.prefix}:${key}`;
|
|
85
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
|
+
}
|
|
86
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
|
+
}
|
|
87
268
|
|
|
88
269
|
// src/internal/CircuitBreakerManager.ts
|
|
89
270
|
var CircuitBreakerManager = class {
|
|
@@ -177,11 +358,105 @@ var CircuitBreakerManager = class {
|
|
|
177
358
|
}
|
|
178
359
|
};
|
|
179
360
|
|
|
361
|
+
// src/internal/FetchRateLimiter.ts
|
|
362
|
+
var FetchRateLimiter = class {
|
|
363
|
+
active = 0;
|
|
364
|
+
queue = [];
|
|
365
|
+
startedAt = [];
|
|
366
|
+
drainTimer;
|
|
367
|
+
async schedule(options, task) {
|
|
368
|
+
if (!options) {
|
|
369
|
+
return task();
|
|
370
|
+
}
|
|
371
|
+
const normalized = this.normalize(options);
|
|
372
|
+
if (!normalized) {
|
|
373
|
+
return task();
|
|
374
|
+
}
|
|
375
|
+
return new Promise((resolve, reject) => {
|
|
376
|
+
this.queue.push({ options: normalized, task, resolve, reject });
|
|
377
|
+
this.drain();
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
normalize(options) {
|
|
381
|
+
const maxConcurrent = options.maxConcurrent;
|
|
382
|
+
const intervalMs = options.intervalMs;
|
|
383
|
+
const maxPerInterval = options.maxPerInterval;
|
|
384
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
385
|
+
return void 0;
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
maxConcurrent,
|
|
389
|
+
intervalMs,
|
|
390
|
+
maxPerInterval
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
drain() {
|
|
394
|
+
if (this.drainTimer) {
|
|
395
|
+
clearTimeout(this.drainTimer);
|
|
396
|
+
this.drainTimer = void 0;
|
|
397
|
+
}
|
|
398
|
+
while (this.queue.length > 0) {
|
|
399
|
+
const next = this.queue[0];
|
|
400
|
+
if (!next) {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const waitMs = this.waitTime(next.options);
|
|
404
|
+
if (waitMs > 0) {
|
|
405
|
+
this.drainTimer = setTimeout(() => {
|
|
406
|
+
this.drainTimer = void 0;
|
|
407
|
+
this.drain();
|
|
408
|
+
}, waitMs);
|
|
409
|
+
this.drainTimer.unref?.();
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
this.queue.shift();
|
|
413
|
+
this.active += 1;
|
|
414
|
+
this.startedAt.push(Date.now());
|
|
415
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
416
|
+
this.active -= 1;
|
|
417
|
+
this.drain();
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
waitTime(options) {
|
|
422
|
+
const now = Date.now();
|
|
423
|
+
if (options.maxConcurrent && this.active >= options.maxConcurrent) {
|
|
424
|
+
return 1;
|
|
425
|
+
}
|
|
426
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
427
|
+
return 0;
|
|
428
|
+
}
|
|
429
|
+
this.prune(now, options.intervalMs);
|
|
430
|
+
if (this.startedAt.length < options.maxPerInterval) {
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
const oldest = this.startedAt[0];
|
|
434
|
+
if (!oldest) {
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
438
|
+
}
|
|
439
|
+
prune(now, intervalMs) {
|
|
440
|
+
while (this.startedAt.length > 0) {
|
|
441
|
+
const startedAt = this.startedAt[0];
|
|
442
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
this.startedAt.shift();
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
180
450
|
// src/internal/MetricsCollector.ts
|
|
181
451
|
var MetricsCollector = class {
|
|
182
452
|
data = this.empty();
|
|
183
453
|
get snapshot() {
|
|
184
|
-
return {
|
|
454
|
+
return {
|
|
455
|
+
...this.data,
|
|
456
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
457
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
458
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
459
|
+
};
|
|
185
460
|
}
|
|
186
461
|
increment(field, amount = 1) {
|
|
187
462
|
;
|
|
@@ -190,6 +465,22 @@ var MetricsCollector = class {
|
|
|
190
465
|
incrementLayer(map, layerName) {
|
|
191
466
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
192
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* Records a read latency sample for the given layer.
|
|
470
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
471
|
+
*/
|
|
472
|
+
recordLatency(layerName, durationMs) {
|
|
473
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
474
|
+
if (!existing) {
|
|
475
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
existing.count += 1;
|
|
479
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
480
|
+
if (durationMs > existing.maxMs) {
|
|
481
|
+
existing.maxMs = durationMs;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
193
484
|
reset() {
|
|
194
485
|
this.data = this.empty();
|
|
195
486
|
}
|
|
@@ -224,112 +515,12 @@ var MetricsCollector = class {
|
|
|
224
515
|
degradedOperations: 0,
|
|
225
516
|
hitsByLayer: {},
|
|
226
517
|
missesByLayer: {},
|
|
518
|
+
latencyByLayer: {},
|
|
227
519
|
resetAt: Date.now()
|
|
228
520
|
};
|
|
229
521
|
}
|
|
230
522
|
};
|
|
231
523
|
|
|
232
|
-
// src/internal/StoredValue.ts
|
|
233
|
-
function isStoredValueEnvelope(value) {
|
|
234
|
-
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
235
|
-
}
|
|
236
|
-
function createStoredValueEnvelope(options) {
|
|
237
|
-
const now = options.now ?? Date.now();
|
|
238
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
239
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
240
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
241
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
242
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
243
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
244
|
-
return {
|
|
245
|
-
__layercache: 1,
|
|
246
|
-
kind: options.kind,
|
|
247
|
-
value: options.value,
|
|
248
|
-
freshUntil,
|
|
249
|
-
staleUntil,
|
|
250
|
-
errorUntil,
|
|
251
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
252
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
253
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
257
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
258
|
-
return { state: "fresh", value: stored, stored };
|
|
259
|
-
}
|
|
260
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
261
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
262
|
-
}
|
|
263
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
264
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
265
|
-
}
|
|
266
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
267
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
268
|
-
}
|
|
269
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
270
|
-
}
|
|
271
|
-
function unwrapStoredValue(stored) {
|
|
272
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
273
|
-
return stored;
|
|
274
|
-
}
|
|
275
|
-
if (stored.kind === "empty") {
|
|
276
|
-
return null;
|
|
277
|
-
}
|
|
278
|
-
return stored.value ?? null;
|
|
279
|
-
}
|
|
280
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
281
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
282
|
-
return void 0;
|
|
283
|
-
}
|
|
284
|
-
const expiry = maxExpiry(stored);
|
|
285
|
-
if (expiry === null) {
|
|
286
|
-
return void 0;
|
|
287
|
-
}
|
|
288
|
-
const remainingMs = expiry - now;
|
|
289
|
-
if (remainingMs <= 0) {
|
|
290
|
-
return 1;
|
|
291
|
-
}
|
|
292
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
293
|
-
}
|
|
294
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
295
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
296
|
-
return void 0;
|
|
297
|
-
}
|
|
298
|
-
const remainingMs = stored.freshUntil - now;
|
|
299
|
-
if (remainingMs <= 0) {
|
|
300
|
-
return 0;
|
|
301
|
-
}
|
|
302
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
303
|
-
}
|
|
304
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
305
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
306
|
-
return stored;
|
|
307
|
-
}
|
|
308
|
-
return createStoredValueEnvelope({
|
|
309
|
-
kind: stored.kind,
|
|
310
|
-
value: stored.value,
|
|
311
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
312
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
313
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
314
|
-
now
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
function maxExpiry(stored) {
|
|
318
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
319
|
-
(value) => value !== null
|
|
320
|
-
);
|
|
321
|
-
if (values.length === 0) {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
return Math.max(...values);
|
|
325
|
-
}
|
|
326
|
-
function normalizePositiveSeconds(value) {
|
|
327
|
-
if (!value || value <= 0) {
|
|
328
|
-
return void 0;
|
|
329
|
-
}
|
|
330
|
-
return value;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
524
|
// src/internal/TtlResolver.ts
|
|
334
525
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
335
526
|
var TtlResolver = class {
|
|
@@ -351,13 +542,14 @@ var TtlResolver = class {
|
|
|
351
542
|
clearProfiles() {
|
|
352
543
|
this.accessProfiles.clear();
|
|
353
544
|
}
|
|
354
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
545
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
546
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
355
547
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
356
548
|
layerName,
|
|
357
549
|
options?.negativeTtl,
|
|
358
550
|
globalNegativeTtl,
|
|
359
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
360
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
551
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
552
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
361
553
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
362
554
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
363
555
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -396,6 +588,29 @@ var TtlResolver = class {
|
|
|
396
588
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
397
589
|
return Math.max(1, Math.round(ttl + delta));
|
|
398
590
|
}
|
|
591
|
+
resolvePolicyTtl(key, value, policy) {
|
|
592
|
+
if (!policy) {
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
if (typeof policy === "function") {
|
|
596
|
+
return policy({ key, value });
|
|
597
|
+
}
|
|
598
|
+
const now = /* @__PURE__ */ new Date();
|
|
599
|
+
if (policy === "until-midnight") {
|
|
600
|
+
const nextMidnight = new Date(now);
|
|
601
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
602
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
603
|
+
}
|
|
604
|
+
if (policy === "next-hour") {
|
|
605
|
+
const nextHour = new Date(now);
|
|
606
|
+
nextHour.setMinutes(60, 0, 0);
|
|
607
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
608
|
+
}
|
|
609
|
+
const alignToSeconds = policy.alignTo;
|
|
610
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
611
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
612
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
613
|
+
}
|
|
399
614
|
readLayerNumber(layerName, value) {
|
|
400
615
|
if (typeof value === "number") {
|
|
401
616
|
return value;
|
|
@@ -418,66 +633,8 @@ var TtlResolver = class {
|
|
|
418
633
|
}
|
|
419
634
|
};
|
|
420
635
|
|
|
421
|
-
// src/invalidation/TagIndex.ts
|
|
422
|
-
var TagIndex = class {
|
|
423
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
424
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
425
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
426
|
-
async touch(key) {
|
|
427
|
-
this.knownKeys.add(key);
|
|
428
|
-
}
|
|
429
|
-
async track(key, tags) {
|
|
430
|
-
this.knownKeys.add(key);
|
|
431
|
-
if (tags.length === 0) {
|
|
432
|
-
return;
|
|
433
|
-
}
|
|
434
|
-
const existingTags = this.keyToTags.get(key);
|
|
435
|
-
if (existingTags) {
|
|
436
|
-
for (const tag of existingTags) {
|
|
437
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
const tagSet = new Set(tags);
|
|
441
|
-
this.keyToTags.set(key, tagSet);
|
|
442
|
-
for (const tag of tagSet) {
|
|
443
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
444
|
-
keys.add(key);
|
|
445
|
-
this.tagToKeys.set(tag, keys);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
async remove(key) {
|
|
449
|
-
this.knownKeys.delete(key);
|
|
450
|
-
const tags = this.keyToTags.get(key);
|
|
451
|
-
if (!tags) {
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
for (const tag of tags) {
|
|
455
|
-
const keys = this.tagToKeys.get(tag);
|
|
456
|
-
if (!keys) {
|
|
457
|
-
continue;
|
|
458
|
-
}
|
|
459
|
-
keys.delete(key);
|
|
460
|
-
if (keys.size === 0) {
|
|
461
|
-
this.tagToKeys.delete(tag);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
this.keyToTags.delete(key);
|
|
465
|
-
}
|
|
466
|
-
async keysForTag(tag) {
|
|
467
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
468
|
-
}
|
|
469
|
-
async matchPattern(pattern) {
|
|
470
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
471
|
-
}
|
|
472
|
-
async clear() {
|
|
473
|
-
this.tagToKeys.clear();
|
|
474
|
-
this.keyToTags.clear();
|
|
475
|
-
this.knownKeys.clear();
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
|
|
479
636
|
// src/stampede/StampedeGuard.ts
|
|
480
|
-
import { Mutex } from "async-mutex";
|
|
637
|
+
import { Mutex as Mutex2 } from "async-mutex";
|
|
481
638
|
var StampedeGuard = class {
|
|
482
639
|
mutexes = /* @__PURE__ */ new Map();
|
|
483
640
|
async execute(key, task) {
|
|
@@ -494,7 +651,7 @@ var StampedeGuard = class {
|
|
|
494
651
|
getMutexEntry(key) {
|
|
495
652
|
let entry = this.mutexes.get(key);
|
|
496
653
|
if (!entry) {
|
|
497
|
-
entry = { mutex: new
|
|
654
|
+
entry = { mutex: new Mutex2(), references: 0 };
|
|
498
655
|
this.mutexes.set(key, entry);
|
|
499
656
|
}
|
|
500
657
|
entry.references += 1;
|
|
@@ -502,6 +659,16 @@ var StampedeGuard = class {
|
|
|
502
659
|
}
|
|
503
660
|
};
|
|
504
661
|
|
|
662
|
+
// src/types.ts
|
|
663
|
+
var CacheMissError = class extends Error {
|
|
664
|
+
key;
|
|
665
|
+
constructor(key) {
|
|
666
|
+
super(`Cache miss for key "${key}".`);
|
|
667
|
+
this.name = "CacheMissError";
|
|
668
|
+
this.key = key;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
|
|
505
672
|
// src/CacheStack.ts
|
|
506
673
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
507
674
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
@@ -545,6 +712,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
545
712
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
546
713
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
547
714
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
715
|
+
this.currentGeneration = options.generation;
|
|
548
716
|
if (options.publishSetInvalidation !== void 0) {
|
|
549
717
|
console.warn(
|
|
550
718
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -553,21 +721,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
553
721
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
554
722
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
555
723
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
724
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
556
725
|
this.startup = this.initialize();
|
|
557
726
|
}
|
|
558
727
|
layers;
|
|
559
728
|
options;
|
|
560
729
|
stampedeGuard = new StampedeGuard();
|
|
561
730
|
metricsCollector = new MetricsCollector();
|
|
562
|
-
instanceId =
|
|
731
|
+
instanceId = createInstanceId();
|
|
563
732
|
startup;
|
|
564
733
|
unsubscribeInvalidation;
|
|
565
734
|
logger;
|
|
566
735
|
tagIndex;
|
|
736
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
567
737
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
568
738
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
569
739
|
ttlResolver;
|
|
570
740
|
circuitBreakerManager;
|
|
741
|
+
currentGeneration;
|
|
742
|
+
writeBehindQueue = [];
|
|
743
|
+
writeBehindTimer;
|
|
744
|
+
writeBehindFlushPromise;
|
|
571
745
|
isDisconnecting = false;
|
|
572
746
|
disconnectPromise;
|
|
573
747
|
/**
|
|
@@ -577,9 +751,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
577
751
|
* and no `fetcher` is provided.
|
|
578
752
|
*/
|
|
579
753
|
async get(key, fetcher, options) {
|
|
580
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
754
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
581
755
|
this.validateWriteOptions(options);
|
|
582
|
-
await this.
|
|
756
|
+
await this.awaitStartup("get");
|
|
583
757
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
584
758
|
if (hit.found) {
|
|
585
759
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -628,12 +802,24 @@ var CacheStack = class extends EventEmitter {
|
|
|
628
802
|
async getOrSet(key, fetcher, options) {
|
|
629
803
|
return this.get(key, fetcher, options);
|
|
630
804
|
}
|
|
805
|
+
/**
|
|
806
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
807
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
808
|
+
* return non-null.
|
|
809
|
+
*/
|
|
810
|
+
async getOrThrow(key, fetcher, options) {
|
|
811
|
+
const value = await this.get(key, fetcher, options);
|
|
812
|
+
if (value === null) {
|
|
813
|
+
throw new CacheMissError(key);
|
|
814
|
+
}
|
|
815
|
+
return value;
|
|
816
|
+
}
|
|
631
817
|
/**
|
|
632
818
|
* Returns true if the given key exists and is not expired in any layer.
|
|
633
819
|
*/
|
|
634
820
|
async has(key) {
|
|
635
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
636
|
-
await this.
|
|
821
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
822
|
+
await this.awaitStartup("has");
|
|
637
823
|
for (const layer of this.layers) {
|
|
638
824
|
if (this.shouldSkipLayer(layer)) {
|
|
639
825
|
continue;
|
|
@@ -663,8 +849,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
663
849
|
* that has it, or null if the key is not found / has no TTL.
|
|
664
850
|
*/
|
|
665
851
|
async ttl(key) {
|
|
666
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
667
|
-
await this.
|
|
852
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
853
|
+
await this.awaitStartup("ttl");
|
|
668
854
|
for (const layer of this.layers) {
|
|
669
855
|
if (this.shouldSkipLayer(layer)) {
|
|
670
856
|
continue;
|
|
@@ -685,17 +871,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
685
871
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
686
872
|
*/
|
|
687
873
|
async set(key, value, options) {
|
|
688
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
874
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
689
875
|
this.validateWriteOptions(options);
|
|
690
|
-
await this.
|
|
876
|
+
await this.awaitStartup("set");
|
|
691
877
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
692
878
|
}
|
|
693
879
|
/**
|
|
694
880
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
695
881
|
*/
|
|
696
882
|
async delete(key) {
|
|
697
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
698
|
-
await this.
|
|
883
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
884
|
+
await this.awaitStartup("delete");
|
|
699
885
|
await this.deleteKeys([normalizedKey]);
|
|
700
886
|
await this.publishInvalidation({
|
|
701
887
|
scope: "key",
|
|
@@ -705,7 +891,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
705
891
|
});
|
|
706
892
|
}
|
|
707
893
|
async clear() {
|
|
708
|
-
await this.
|
|
894
|
+
await this.awaitStartup("clear");
|
|
709
895
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
710
896
|
await this.tagIndex.clear();
|
|
711
897
|
this.ttlResolver.clearProfiles();
|
|
@@ -721,23 +907,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
721
907
|
if (keys.length === 0) {
|
|
722
908
|
return;
|
|
723
909
|
}
|
|
724
|
-
await this.
|
|
910
|
+
await this.awaitStartup("mdelete");
|
|
725
911
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
726
|
-
|
|
912
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
913
|
+
await this.deleteKeys(cacheKeys);
|
|
727
914
|
await this.publishInvalidation({
|
|
728
915
|
scope: "keys",
|
|
729
|
-
keys:
|
|
916
|
+
keys: cacheKeys,
|
|
730
917
|
sourceId: this.instanceId,
|
|
731
918
|
operation: "delete"
|
|
732
919
|
});
|
|
733
920
|
}
|
|
734
921
|
async mget(entries) {
|
|
922
|
+
this.assertActive("mget");
|
|
735
923
|
if (entries.length === 0) {
|
|
736
924
|
return [];
|
|
737
925
|
}
|
|
738
926
|
const normalizedEntries = entries.map((entry) => ({
|
|
739
927
|
...entry,
|
|
740
|
-
key: this.validateCacheKey(entry.key)
|
|
928
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
741
929
|
}));
|
|
742
930
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
743
931
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -763,7 +951,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
763
951
|
})
|
|
764
952
|
);
|
|
765
953
|
}
|
|
766
|
-
await this.
|
|
954
|
+
await this.awaitStartup("mget");
|
|
767
955
|
const pending = /* @__PURE__ */ new Set();
|
|
768
956
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
769
957
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -811,14 +999,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
811
999
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
812
1000
|
}
|
|
813
1001
|
async mset(entries) {
|
|
1002
|
+
this.assertActive("mset");
|
|
814
1003
|
const normalizedEntries = entries.map((entry) => ({
|
|
815
1004
|
...entry,
|
|
816
|
-
key: this.validateCacheKey(entry.key)
|
|
1005
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
817
1006
|
}));
|
|
818
1007
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
819
|
-
await
|
|
1008
|
+
await this.awaitStartup("mset");
|
|
1009
|
+
await this.writeBatch(normalizedEntries);
|
|
820
1010
|
}
|
|
821
1011
|
async warm(entries, options = {}) {
|
|
1012
|
+
this.assertActive("warm");
|
|
822
1013
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
823
1014
|
const total = entries.length;
|
|
824
1015
|
let completed = 0;
|
|
@@ -867,14 +1058,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
867
1058
|
return new CacheNamespace(this, prefix);
|
|
868
1059
|
}
|
|
869
1060
|
async invalidateByTag(tag) {
|
|
870
|
-
await this.
|
|
1061
|
+
await this.awaitStartup("invalidateByTag");
|
|
871
1062
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
872
1063
|
await this.deleteKeys(keys);
|
|
873
1064
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
874
1065
|
}
|
|
1066
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1067
|
+
if (tags.length === 0) {
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
await this.awaitStartup("invalidateByTags");
|
|
1071
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1072
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1073
|
+
await this.deleteKeys(keys);
|
|
1074
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1075
|
+
}
|
|
875
1076
|
async invalidateByPattern(pattern) {
|
|
876
|
-
await this.
|
|
877
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1077
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1078
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1079
|
+
await this.deleteKeys(keys);
|
|
1080
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1081
|
+
}
|
|
1082
|
+
async invalidateByPrefix(prefix) {
|
|
1083
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1084
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1085
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
878
1086
|
await this.deleteKeys(keys);
|
|
879
1087
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
880
1088
|
}
|
|
@@ -901,8 +1109,77 @@ var CacheStack = class extends EventEmitter {
|
|
|
901
1109
|
getHitRate() {
|
|
902
1110
|
return this.metricsCollector.hitRate();
|
|
903
1111
|
}
|
|
904
|
-
async
|
|
1112
|
+
async healthCheck() {
|
|
905
1113
|
await this.startup;
|
|
1114
|
+
return Promise.all(
|
|
1115
|
+
this.layers.map(async (layer) => {
|
|
1116
|
+
const startedAt = performance.now();
|
|
1117
|
+
try {
|
|
1118
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1119
|
+
return {
|
|
1120
|
+
layer: layer.name,
|
|
1121
|
+
healthy,
|
|
1122
|
+
latencyMs: performance.now() - startedAt
|
|
1123
|
+
};
|
|
1124
|
+
} catch (error) {
|
|
1125
|
+
return {
|
|
1126
|
+
layer: layer.name,
|
|
1127
|
+
healthy: false,
|
|
1128
|
+
latencyMs: performance.now() - startedAt,
|
|
1129
|
+
error: this.formatError(error)
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
})
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
bumpGeneration(nextGeneration) {
|
|
1136
|
+
const current = this.currentGeneration ?? 0;
|
|
1137
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1138
|
+
return this.currentGeneration;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1142
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1143
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1144
|
+
*/
|
|
1145
|
+
async inspect(key) {
|
|
1146
|
+
const userKey = this.validateCacheKey(key);
|
|
1147
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1148
|
+
await this.awaitStartup("inspect");
|
|
1149
|
+
const foundInLayers = [];
|
|
1150
|
+
let freshTtlSeconds = null;
|
|
1151
|
+
let staleTtlSeconds = null;
|
|
1152
|
+
let errorTtlSeconds = null;
|
|
1153
|
+
let isStale = false;
|
|
1154
|
+
for (const layer of this.layers) {
|
|
1155
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1159
|
+
if (stored === null) {
|
|
1160
|
+
continue;
|
|
1161
|
+
}
|
|
1162
|
+
const resolved = resolveStoredValue(stored);
|
|
1163
|
+
if (resolved.state === "expired") {
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
foundInLayers.push(layer.name);
|
|
1167
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1168
|
+
const now = Date.now();
|
|
1169
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1170
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1171
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1172
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
if (foundInLayers.length === 0) {
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1179
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1180
|
+
}
|
|
1181
|
+
async exportState() {
|
|
1182
|
+
await this.awaitStartup("exportState");
|
|
906
1183
|
const exported = /* @__PURE__ */ new Map();
|
|
907
1184
|
for (const layer of this.layers) {
|
|
908
1185
|
if (!layer.keys) {
|
|
@@ -910,15 +1187,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
910
1187
|
}
|
|
911
1188
|
const keys = await layer.keys();
|
|
912
1189
|
for (const key of keys) {
|
|
913
|
-
|
|
1190
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1191
|
+
if (exported.has(exportedKey)) {
|
|
914
1192
|
continue;
|
|
915
1193
|
}
|
|
916
1194
|
const stored = await this.readLayerEntry(layer, key);
|
|
917
1195
|
if (stored === null) {
|
|
918
1196
|
continue;
|
|
919
1197
|
}
|
|
920
|
-
exported.set(
|
|
921
|
-
key,
|
|
1198
|
+
exported.set(exportedKey, {
|
|
1199
|
+
key: exportedKey,
|
|
922
1200
|
value: stored,
|
|
923
1201
|
ttl: remainingStoredTtlSeconds(stored)
|
|
924
1202
|
});
|
|
@@ -927,20 +1205,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
927
1205
|
return [...exported.values()];
|
|
928
1206
|
}
|
|
929
1207
|
async importState(entries) {
|
|
930
|
-
await this.
|
|
1208
|
+
await this.awaitStartup("importState");
|
|
931
1209
|
await Promise.all(
|
|
932
1210
|
entries.map(async (entry) => {
|
|
933
|
-
|
|
934
|
-
await this.
|
|
1211
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1212
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1213
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
935
1214
|
})
|
|
936
1215
|
);
|
|
937
1216
|
}
|
|
938
1217
|
async persistToFile(filePath) {
|
|
1218
|
+
this.assertActive("persistToFile");
|
|
939
1219
|
const snapshot = await this.exportState();
|
|
940
|
-
|
|
1220
|
+
const { promises: fs2 } = await import("fs");
|
|
1221
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
941
1222
|
}
|
|
942
1223
|
async restoreFromFile(filePath) {
|
|
943
|
-
|
|
1224
|
+
this.assertActive("restoreFromFile");
|
|
1225
|
+
const { promises: fs2 } = await import("fs");
|
|
1226
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
944
1227
|
let parsed;
|
|
945
1228
|
try {
|
|
946
1229
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -963,7 +1246,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
963
1246
|
this.disconnectPromise = (async () => {
|
|
964
1247
|
await this.startup;
|
|
965
1248
|
await this.unsubscribeInvalidation?.();
|
|
1249
|
+
await this.flushWriteBehindQueue();
|
|
966
1250
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1251
|
+
if (this.writeBehindTimer) {
|
|
1252
|
+
clearInterval(this.writeBehindTimer);
|
|
1253
|
+
this.writeBehindTimer = void 0;
|
|
1254
|
+
}
|
|
1255
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
967
1256
|
})();
|
|
968
1257
|
}
|
|
969
1258
|
await this.disconnectPromise;
|
|
@@ -1023,7 +1312,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1023
1312
|
const fetchStart = Date.now();
|
|
1024
1313
|
let fetched;
|
|
1025
1314
|
try {
|
|
1026
|
-
fetched = await
|
|
1315
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1316
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1317
|
+
fetcher
|
|
1318
|
+
);
|
|
1027
1319
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1028
1320
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1029
1321
|
} catch (error) {
|
|
@@ -1037,6 +1329,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1037
1329
|
await this.storeEntry(key, "empty", null, options);
|
|
1038
1330
|
return null;
|
|
1039
1331
|
}
|
|
1332
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1333
|
+
return fetched;
|
|
1334
|
+
}
|
|
1040
1335
|
await this.storeEntry(key, "value", fetched, options);
|
|
1041
1336
|
return fetched;
|
|
1042
1337
|
}
|
|
@@ -1054,12 +1349,70 @@ var CacheStack = class extends EventEmitter {
|
|
|
1054
1349
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1055
1350
|
}
|
|
1056
1351
|
}
|
|
1352
|
+
async writeBatch(entries) {
|
|
1353
|
+
const now = Date.now();
|
|
1354
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1355
|
+
const immediateOperations = [];
|
|
1356
|
+
const deferredOperations = [];
|
|
1357
|
+
for (const entry of entries) {
|
|
1358
|
+
for (const layer of this.layers) {
|
|
1359
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1363
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1364
|
+
bucket.push(layerEntry);
|
|
1365
|
+
entriesByLayer.set(layer, bucket);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1369
|
+
const operation = async () => {
|
|
1370
|
+
try {
|
|
1371
|
+
if (layer.setMany) {
|
|
1372
|
+
await layer.setMany(layerEntries);
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
1380
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1381
|
+
deferredOperations.push(operation);
|
|
1382
|
+
} else {
|
|
1383
|
+
immediateOperations.push(operation);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1387
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1388
|
+
for (const entry of entries) {
|
|
1389
|
+
if (entry.options?.tags) {
|
|
1390
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1391
|
+
} else {
|
|
1392
|
+
await this.tagIndex.touch(entry.key);
|
|
1393
|
+
}
|
|
1394
|
+
this.metricsCollector.increment("sets");
|
|
1395
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1396
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1397
|
+
}
|
|
1398
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1399
|
+
await this.publishInvalidation({
|
|
1400
|
+
scope: "keys",
|
|
1401
|
+
keys: entries.map((entry) => entry.key),
|
|
1402
|
+
sourceId: this.instanceId,
|
|
1403
|
+
operation: "write"
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1057
1407
|
async readFromLayers(key, options, mode) {
|
|
1058
1408
|
let sawRetainableValue = false;
|
|
1059
1409
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
1060
1410
|
const layer = this.layers[index];
|
|
1061
1411
|
if (!layer) continue;
|
|
1412
|
+
const readStart = performance.now();
|
|
1062
1413
|
const stored = await this.readLayerEntry(layer, key);
|
|
1414
|
+
const readDuration = performance.now() - readStart;
|
|
1415
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1063
1416
|
if (stored === null) {
|
|
1064
1417
|
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1065
1418
|
continue;
|
|
@@ -1134,33 +1487,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1134
1487
|
}
|
|
1135
1488
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1136
1489
|
const now = Date.now();
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
options
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
});
|
|
1156
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1157
|
-
try {
|
|
1158
|
-
await layer.set(key, payload, ttl);
|
|
1159
|
-
} catch (error) {
|
|
1160
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1490
|
+
const immediateOperations = [];
|
|
1491
|
+
const deferredOperations = [];
|
|
1492
|
+
for (const layer of this.layers) {
|
|
1493
|
+
const operation = async () => {
|
|
1494
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1498
|
+
try {
|
|
1499
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1502
|
+
}
|
|
1503
|
+
};
|
|
1504
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1505
|
+
deferredOperations.push(operation);
|
|
1506
|
+
} else {
|
|
1507
|
+
immediateOperations.push(operation);
|
|
1161
1508
|
}
|
|
1162
|
-
}
|
|
1163
|
-
await this.executeLayerOperations(
|
|
1509
|
+
}
|
|
1510
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1511
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1164
1512
|
}
|
|
1165
1513
|
async executeLayerOperations(operations, context) {
|
|
1166
1514
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1184,8 +1532,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1184
1532
|
);
|
|
1185
1533
|
}
|
|
1186
1534
|
}
|
|
1187
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1188
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1535
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1536
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1537
|
+
key,
|
|
1538
|
+
layerName,
|
|
1539
|
+
kind,
|
|
1540
|
+
options,
|
|
1541
|
+
fallbackTtl,
|
|
1542
|
+
this.options.negativeTtl,
|
|
1543
|
+
void 0,
|
|
1544
|
+
value
|
|
1545
|
+
);
|
|
1189
1546
|
}
|
|
1190
1547
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1191
1548
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1261,6 +1618,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
1261
1618
|
}
|
|
1262
1619
|
}
|
|
1263
1620
|
}
|
|
1621
|
+
async getTagsForKey(key) {
|
|
1622
|
+
if (this.tagIndex.tagsForKey) {
|
|
1623
|
+
return this.tagIndex.tagsForKey(key);
|
|
1624
|
+
}
|
|
1625
|
+
return [];
|
|
1626
|
+
}
|
|
1264
1627
|
formatError(error) {
|
|
1265
1628
|
if (error instanceof Error) {
|
|
1266
1629
|
return error.message;
|
|
@@ -1273,11 +1636,110 @@ var CacheStack = class extends EventEmitter {
|
|
|
1273
1636
|
shouldBroadcastL1Invalidation() {
|
|
1274
1637
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1275
1638
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1639
|
+
initializeWriteBehind(options) {
|
|
1640
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
1644
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
this.writeBehindTimer = setInterval(() => {
|
|
1648
|
+
void this.flushWriteBehindQueue();
|
|
1649
|
+
}, flushIntervalMs);
|
|
1650
|
+
this.writeBehindTimer.unref?.();
|
|
1651
|
+
}
|
|
1652
|
+
shouldWriteBehind(layer) {
|
|
1653
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
1654
|
+
}
|
|
1655
|
+
async enqueueWriteBehind(operation) {
|
|
1656
|
+
this.writeBehindQueue.push(operation);
|
|
1657
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1658
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
1659
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
1660
|
+
await this.flushWriteBehindQueue();
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
1664
|
+
await this.flushWriteBehindQueue();
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
async flushWriteBehindQueue() {
|
|
1668
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
1669
|
+
await this.writeBehindFlushPromise;
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
1673
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
1674
|
+
this.writeBehindFlushPromise = (async () => {
|
|
1675
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
1676
|
+
})();
|
|
1677
|
+
await this.writeBehindFlushPromise;
|
|
1678
|
+
this.writeBehindFlushPromise = void 0;
|
|
1679
|
+
if (this.writeBehindQueue.length > 0) {
|
|
1680
|
+
await this.flushWriteBehindQueue();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
1684
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
1685
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
1686
|
+
layer.name,
|
|
1687
|
+
options?.staleWhileRevalidate,
|
|
1688
|
+
this.options.staleWhileRevalidate
|
|
1689
|
+
);
|
|
1690
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
1691
|
+
const payload = createStoredValueEnvelope({
|
|
1692
|
+
kind,
|
|
1693
|
+
value,
|
|
1694
|
+
freshTtlSeconds: freshTtl,
|
|
1695
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
1696
|
+
staleIfErrorSeconds: staleIfError,
|
|
1697
|
+
now
|
|
1698
|
+
});
|
|
1699
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1700
|
+
return {
|
|
1701
|
+
key,
|
|
1702
|
+
value: payload,
|
|
1703
|
+
ttl
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
intersectKeys(groups) {
|
|
1707
|
+
if (groups.length === 0) {
|
|
1708
|
+
return [];
|
|
1709
|
+
}
|
|
1710
|
+
const [firstGroup, ...rest] = groups;
|
|
1711
|
+
if (!firstGroup) {
|
|
1712
|
+
return [];
|
|
1713
|
+
}
|
|
1714
|
+
const restSets = rest.map((group) => new Set(group));
|
|
1715
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
1716
|
+
}
|
|
1717
|
+
qualifyKey(key) {
|
|
1718
|
+
const prefix = this.generationPrefix();
|
|
1719
|
+
return prefix ? `${prefix}${key}` : key;
|
|
1720
|
+
}
|
|
1721
|
+
qualifyPattern(pattern) {
|
|
1722
|
+
const prefix = this.generationPrefix();
|
|
1723
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
1724
|
+
}
|
|
1725
|
+
stripQualifiedKey(key) {
|
|
1726
|
+
const prefix = this.generationPrefix();
|
|
1727
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
1728
|
+
return key;
|
|
1729
|
+
}
|
|
1730
|
+
return key.slice(prefix.length);
|
|
1731
|
+
}
|
|
1732
|
+
generationPrefix() {
|
|
1733
|
+
if (this.currentGeneration === void 0) {
|
|
1734
|
+
return "";
|
|
1735
|
+
}
|
|
1736
|
+
return `v${this.currentGeneration}:`;
|
|
1737
|
+
}
|
|
1738
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
1739
|
+
await Promise.all(
|
|
1740
|
+
layers.map(async (layer) => {
|
|
1741
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1742
|
+
return;
|
|
1281
1743
|
}
|
|
1282
1744
|
if (layer.deleteMany) {
|
|
1283
1745
|
try {
|
|
@@ -1316,6 +1778,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1316
1778
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1317
1779
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1318
1780
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1781
|
+
if (this.options.generation !== void 0) {
|
|
1782
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
1783
|
+
}
|
|
1319
1784
|
}
|
|
1320
1785
|
validateWriteOptions(options) {
|
|
1321
1786
|
if (!options) {
|
|
@@ -1327,6 +1792,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1327
1792
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1328
1793
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1329
1794
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1795
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1330
1796
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1331
1797
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1332
1798
|
}
|
|
@@ -1370,6 +1836,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1370
1836
|
}
|
|
1371
1837
|
return key;
|
|
1372
1838
|
}
|
|
1839
|
+
validateTtlPolicy(name, policy) {
|
|
1840
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if ("alignTo" in policy) {
|
|
1844
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
throw new Error(`${name} is invalid.`);
|
|
1848
|
+
}
|
|
1849
|
+
assertActive(operation) {
|
|
1850
|
+
if (this.isDisconnecting) {
|
|
1851
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
async awaitStartup(operation) {
|
|
1855
|
+
this.assertActive(operation);
|
|
1856
|
+
await this.startup;
|
|
1857
|
+
this.assertActive(operation);
|
|
1858
|
+
}
|
|
1373
1859
|
serializeOptions(options) {
|
|
1374
1860
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1375
1861
|
}
|
|
@@ -1475,41 +1961,47 @@ var CacheStack = class extends EventEmitter {
|
|
|
1475
1961
|
return value;
|
|
1476
1962
|
}
|
|
1477
1963
|
};
|
|
1964
|
+
function createInstanceId() {
|
|
1965
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1966
|
+
}
|
|
1478
1967
|
|
|
1479
1968
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1480
1969
|
var RedisInvalidationBus = class {
|
|
1481
1970
|
channel;
|
|
1482
1971
|
publisher;
|
|
1483
1972
|
subscriber;
|
|
1484
|
-
|
|
1973
|
+
logger;
|
|
1974
|
+
handlers = /* @__PURE__ */ new Set();
|
|
1975
|
+
sharedListener;
|
|
1485
1976
|
constructor(options) {
|
|
1486
1977
|
this.publisher = options.publisher;
|
|
1487
1978
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1488
1979
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
1980
|
+
this.logger = options.logger;
|
|
1489
1981
|
}
|
|
1490
1982
|
async subscribe(handler) {
|
|
1491
|
-
if (this.
|
|
1492
|
-
|
|
1983
|
+
if (this.handlers.size === 0) {
|
|
1984
|
+
const listener = (_channel, payload) => {
|
|
1985
|
+
void this.dispatchToHandlers(payload);
|
|
1986
|
+
};
|
|
1987
|
+
this.sharedListener = listener;
|
|
1988
|
+
this.subscriber.on("message", listener);
|
|
1989
|
+
await this.subscriber.subscribe(this.channel);
|
|
1493
1990
|
}
|
|
1494
|
-
|
|
1495
|
-
void this.handleMessage(payload, handler);
|
|
1496
|
-
};
|
|
1497
|
-
this.activeListener = listener;
|
|
1498
|
-
this.subscriber.on("message", listener);
|
|
1499
|
-
await this.subscriber.subscribe(this.channel);
|
|
1991
|
+
this.handlers.add(handler);
|
|
1500
1992
|
return async () => {
|
|
1501
|
-
|
|
1502
|
-
|
|
1993
|
+
this.handlers.delete(handler);
|
|
1994
|
+
if (this.handlers.size === 0 && this.sharedListener) {
|
|
1995
|
+
this.subscriber.off("message", this.sharedListener);
|
|
1996
|
+
this.sharedListener = void 0;
|
|
1997
|
+
await this.subscriber.unsubscribe(this.channel);
|
|
1503
1998
|
}
|
|
1504
|
-
this.activeListener = void 0;
|
|
1505
|
-
this.subscriber.off("message", listener);
|
|
1506
|
-
await this.subscriber.unsubscribe(this.channel);
|
|
1507
1999
|
};
|
|
1508
2000
|
}
|
|
1509
2001
|
async publish(message) {
|
|
1510
2002
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
1511
2003
|
}
|
|
1512
|
-
async
|
|
2004
|
+
async dispatchToHandlers(payload) {
|
|
1513
2005
|
let message;
|
|
1514
2006
|
try {
|
|
1515
2007
|
const parsed = JSON.parse(payload);
|
|
@@ -1521,11 +2013,15 @@ var RedisInvalidationBus = class {
|
|
|
1521
2013
|
this.reportError("invalid invalidation payload", error);
|
|
1522
2014
|
return;
|
|
1523
2015
|
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2016
|
+
await Promise.all(
|
|
2017
|
+
[...this.handlers].map(async (handler) => {
|
|
2018
|
+
try {
|
|
2019
|
+
await handler(message);
|
|
2020
|
+
} catch (error) {
|
|
2021
|
+
this.reportError("invalidation handler failed", error);
|
|
2022
|
+
}
|
|
2023
|
+
})
|
|
2024
|
+
);
|
|
1529
2025
|
}
|
|
1530
2026
|
isInvalidationMessage(value) {
|
|
1531
2027
|
if (!value || typeof value !== "object") {
|
|
@@ -1538,6 +2034,10 @@ var RedisInvalidationBus = class {
|
|
|
1538
2034
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1539
2035
|
}
|
|
1540
2036
|
reportError(message, error) {
|
|
2037
|
+
if (this.logger?.error) {
|
|
2038
|
+
this.logger.error(message, { error });
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1541
2041
|
console.error(`[layercache] ${message}`, error);
|
|
1542
2042
|
}
|
|
1543
2043
|
};
|
|
@@ -1586,6 +2086,43 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1586
2086
|
};
|
|
1587
2087
|
}
|
|
1588
2088
|
|
|
2089
|
+
// src/integrations/express.ts
|
|
2090
|
+
function createExpressCacheMiddleware(cache, options = {}) {
|
|
2091
|
+
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
2092
|
+
return async (req, res, next) => {
|
|
2093
|
+
try {
|
|
2094
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2095
|
+
if (!allowedMethods.has(method)) {
|
|
2096
|
+
next();
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
2100
|
+
const cached = await cache.get(key, void 0, options);
|
|
2101
|
+
if (cached !== null) {
|
|
2102
|
+
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
2103
|
+
res.setHeader?.("x-cache", "HIT");
|
|
2104
|
+
if (res.json) {
|
|
2105
|
+
res.json(cached);
|
|
2106
|
+
} else {
|
|
2107
|
+
res.end?.(JSON.stringify(cached));
|
|
2108
|
+
}
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
const originalJson = res.json?.bind(res);
|
|
2112
|
+
if (originalJson) {
|
|
2113
|
+
res.json = (body) => {
|
|
2114
|
+
res.setHeader?.("x-cache", "MISS");
|
|
2115
|
+
void cache.set(key, body, options);
|
|
2116
|
+
return originalJson(body);
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
next();
|
|
2120
|
+
} catch (error) {
|
|
2121
|
+
next(error);
|
|
2122
|
+
}
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
|
|
1589
2126
|
// src/integrations/graphql.ts
|
|
1590
2127
|
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1591
2128
|
const wrapped = cache.wrap(prefix, resolver, {
|
|
@@ -1595,6 +2132,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1595
2132
|
return (...args) => wrapped(...args);
|
|
1596
2133
|
}
|
|
1597
2134
|
|
|
2135
|
+
// src/integrations/opentelemetry.ts
|
|
2136
|
+
function createOpenTelemetryPlugin(cache, tracer) {
|
|
2137
|
+
const originals = {
|
|
2138
|
+
get: cache.get.bind(cache),
|
|
2139
|
+
set: cache.set.bind(cache),
|
|
2140
|
+
delete: cache.delete.bind(cache),
|
|
2141
|
+
mget: cache.mget.bind(cache),
|
|
2142
|
+
mset: cache.mset.bind(cache),
|
|
2143
|
+
invalidateByTag: cache.invalidateByTag.bind(cache),
|
|
2144
|
+
invalidateByTags: cache.invalidateByTags.bind(cache),
|
|
2145
|
+
invalidateByPattern: cache.invalidateByPattern.bind(cache),
|
|
2146
|
+
invalidateByPrefix: cache.invalidateByPrefix.bind(cache)
|
|
2147
|
+
};
|
|
2148
|
+
cache.get = instrument("layercache.get", tracer, originals.get, (args) => ({
|
|
2149
|
+
"layercache.key": String(args[0] ?? "")
|
|
2150
|
+
}));
|
|
2151
|
+
cache.set = instrument("layercache.set", tracer, originals.set, (args) => ({
|
|
2152
|
+
"layercache.key": String(args[0] ?? "")
|
|
2153
|
+
}));
|
|
2154
|
+
cache.delete = instrument("layercache.delete", tracer, originals.delete, (args) => ({
|
|
2155
|
+
"layercache.key": String(args[0] ?? "")
|
|
2156
|
+
}));
|
|
2157
|
+
cache.mget = instrument("layercache.mget", tracer, originals.mget);
|
|
2158
|
+
cache.mset = instrument("layercache.mset", tracer, originals.mset);
|
|
2159
|
+
cache.invalidateByTag = instrument("layercache.invalidate_by_tag", tracer, originals.invalidateByTag);
|
|
2160
|
+
cache.invalidateByTags = instrument("layercache.invalidate_by_tags", tracer, originals.invalidateByTags);
|
|
2161
|
+
cache.invalidateByPattern = instrument("layercache.invalidate_by_pattern", tracer, originals.invalidateByPattern);
|
|
2162
|
+
cache.invalidateByPrefix = instrument("layercache.invalidate_by_prefix", tracer, originals.invalidateByPrefix);
|
|
2163
|
+
return {
|
|
2164
|
+
uninstall() {
|
|
2165
|
+
cache.get = originals.get;
|
|
2166
|
+
cache.set = originals.set;
|
|
2167
|
+
cache.delete = originals.delete;
|
|
2168
|
+
cache.mget = originals.mget;
|
|
2169
|
+
cache.mset = originals.mset;
|
|
2170
|
+
cache.invalidateByTag = originals.invalidateByTag;
|
|
2171
|
+
cache.invalidateByTags = originals.invalidateByTags;
|
|
2172
|
+
cache.invalidateByPattern = originals.invalidateByPattern;
|
|
2173
|
+
cache.invalidateByPrefix = originals.invalidateByPrefix;
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
function instrument(name, tracer, method, attributes) {
|
|
2178
|
+
return (async (...args) => {
|
|
2179
|
+
const span = tracer.startSpan(name, { attributes: attributes?.(args) });
|
|
2180
|
+
try {
|
|
2181
|
+
const result = await method(...args);
|
|
2182
|
+
span.setAttribute?.("layercache.success", true);
|
|
2183
|
+
if (result === null) {
|
|
2184
|
+
span.setAttribute?.("layercache.result", "null");
|
|
2185
|
+
}
|
|
2186
|
+
return result;
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
span.setAttribute?.("layercache.success", false);
|
|
2189
|
+
span.recordException?.(error);
|
|
2190
|
+
throw error;
|
|
2191
|
+
} finally {
|
|
2192
|
+
span.end();
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
|
|
1598
2197
|
// src/integrations/trpc.ts
|
|
1599
2198
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1600
2199
|
return async (context) => {
|
|
@@ -1620,165 +2219,9 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
|
1620
2219
|
};
|
|
1621
2220
|
}
|
|
1622
2221
|
|
|
1623
|
-
// src/layers/MemoryLayer.ts
|
|
1624
|
-
var MemoryLayer = class {
|
|
1625
|
-
name;
|
|
1626
|
-
defaultTtl;
|
|
1627
|
-
isLocal = true;
|
|
1628
|
-
maxSize;
|
|
1629
|
-
evictionPolicy;
|
|
1630
|
-
entries = /* @__PURE__ */ new Map();
|
|
1631
|
-
constructor(options = {}) {
|
|
1632
|
-
this.name = options.name ?? "memory";
|
|
1633
|
-
this.defaultTtl = options.ttl;
|
|
1634
|
-
this.maxSize = options.maxSize ?? 1e3;
|
|
1635
|
-
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
1636
|
-
}
|
|
1637
|
-
async get(key) {
|
|
1638
|
-
const value = await this.getEntry(key);
|
|
1639
|
-
return unwrapStoredValue(value);
|
|
1640
|
-
}
|
|
1641
|
-
async getEntry(key) {
|
|
1642
|
-
const entry = this.entries.get(key);
|
|
1643
|
-
if (!entry) {
|
|
1644
|
-
return null;
|
|
1645
|
-
}
|
|
1646
|
-
if (this.isExpired(entry)) {
|
|
1647
|
-
this.entries.delete(key);
|
|
1648
|
-
return null;
|
|
1649
|
-
}
|
|
1650
|
-
if (this.evictionPolicy === "lru") {
|
|
1651
|
-
this.entries.delete(key);
|
|
1652
|
-
entry.frequency += 1;
|
|
1653
|
-
this.entries.set(key, entry);
|
|
1654
|
-
} else {
|
|
1655
|
-
entry.frequency += 1;
|
|
1656
|
-
}
|
|
1657
|
-
return entry.value;
|
|
1658
|
-
}
|
|
1659
|
-
async getMany(keys) {
|
|
1660
|
-
const values = [];
|
|
1661
|
-
for (const key of keys) {
|
|
1662
|
-
values.push(await this.getEntry(key));
|
|
1663
|
-
}
|
|
1664
|
-
return values;
|
|
1665
|
-
}
|
|
1666
|
-
async set(key, value, ttl = this.defaultTtl) {
|
|
1667
|
-
this.entries.delete(key);
|
|
1668
|
-
this.entries.set(key, {
|
|
1669
|
-
value,
|
|
1670
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1671
|
-
frequency: 0,
|
|
1672
|
-
insertedAt: Date.now()
|
|
1673
|
-
});
|
|
1674
|
-
while (this.entries.size > this.maxSize) {
|
|
1675
|
-
this.evict();
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
async has(key) {
|
|
1679
|
-
const entry = this.entries.get(key);
|
|
1680
|
-
if (!entry) {
|
|
1681
|
-
return false;
|
|
1682
|
-
}
|
|
1683
|
-
if (this.isExpired(entry)) {
|
|
1684
|
-
this.entries.delete(key);
|
|
1685
|
-
return false;
|
|
1686
|
-
}
|
|
1687
|
-
return true;
|
|
1688
|
-
}
|
|
1689
|
-
async ttl(key) {
|
|
1690
|
-
const entry = this.entries.get(key);
|
|
1691
|
-
if (!entry) {
|
|
1692
|
-
return null;
|
|
1693
|
-
}
|
|
1694
|
-
if (this.isExpired(entry)) {
|
|
1695
|
-
this.entries.delete(key);
|
|
1696
|
-
return null;
|
|
1697
|
-
}
|
|
1698
|
-
if (entry.expiresAt === null) {
|
|
1699
|
-
return null;
|
|
1700
|
-
}
|
|
1701
|
-
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
1702
|
-
}
|
|
1703
|
-
async size() {
|
|
1704
|
-
this.pruneExpired();
|
|
1705
|
-
return this.entries.size;
|
|
1706
|
-
}
|
|
1707
|
-
async delete(key) {
|
|
1708
|
-
this.entries.delete(key);
|
|
1709
|
-
}
|
|
1710
|
-
async deleteMany(keys) {
|
|
1711
|
-
for (const key of keys) {
|
|
1712
|
-
this.entries.delete(key);
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
async clear() {
|
|
1716
|
-
this.entries.clear();
|
|
1717
|
-
}
|
|
1718
|
-
async keys() {
|
|
1719
|
-
this.pruneExpired();
|
|
1720
|
-
return [...this.entries.keys()];
|
|
1721
|
-
}
|
|
1722
|
-
exportState() {
|
|
1723
|
-
this.pruneExpired();
|
|
1724
|
-
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1725
|
-
key,
|
|
1726
|
-
value: entry.value,
|
|
1727
|
-
expiresAt: entry.expiresAt
|
|
1728
|
-
}));
|
|
1729
|
-
}
|
|
1730
|
-
importState(entries) {
|
|
1731
|
-
for (const entry of entries) {
|
|
1732
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
this.entries.set(entry.key, {
|
|
1736
|
-
value: entry.value,
|
|
1737
|
-
expiresAt: entry.expiresAt,
|
|
1738
|
-
frequency: 0,
|
|
1739
|
-
insertedAt: Date.now()
|
|
1740
|
-
});
|
|
1741
|
-
}
|
|
1742
|
-
while (this.entries.size > this.maxSize) {
|
|
1743
|
-
this.evict();
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
evict() {
|
|
1747
|
-
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1748
|
-
const oldestKey = this.entries.keys().next().value;
|
|
1749
|
-
if (oldestKey !== void 0) {
|
|
1750
|
-
this.entries.delete(oldestKey);
|
|
1751
|
-
}
|
|
1752
|
-
return;
|
|
1753
|
-
}
|
|
1754
|
-
let victimKey;
|
|
1755
|
-
let minFreq = Number.POSITIVE_INFINITY;
|
|
1756
|
-
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1757
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1758
|
-
if (entry.frequency < minFreq || entry.frequency === minFreq && entry.insertedAt < minInsertedAt) {
|
|
1759
|
-
minFreq = entry.frequency;
|
|
1760
|
-
minInsertedAt = entry.insertedAt;
|
|
1761
|
-
victimKey = key;
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
if (victimKey !== void 0) {
|
|
1765
|
-
this.entries.delete(victimKey);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
pruneExpired() {
|
|
1769
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1770
|
-
if (this.isExpired(entry)) {
|
|
1771
|
-
this.entries.delete(key);
|
|
1772
|
-
}
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
isExpired(entry) {
|
|
1776
|
-
return entry.expiresAt !== null && entry.expiresAt <= Date.now();
|
|
1777
|
-
}
|
|
1778
|
-
};
|
|
1779
|
-
|
|
1780
2222
|
// src/layers/RedisLayer.ts
|
|
1781
|
-
import {
|
|
2223
|
+
import { promisify } from "util";
|
|
2224
|
+
import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
|
|
1782
2225
|
|
|
1783
2226
|
// src/serialization/JsonSerializer.ts
|
|
1784
2227
|
var JsonSerializer = class {
|
|
@@ -1793,27 +2236,33 @@ var JsonSerializer = class {
|
|
|
1793
2236
|
|
|
1794
2237
|
// src/layers/RedisLayer.ts
|
|
1795
2238
|
var BATCH_DELETE_SIZE = 500;
|
|
2239
|
+
var gzipAsync = promisify(gzip);
|
|
2240
|
+
var gunzipAsync = promisify(gunzip);
|
|
2241
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
2242
|
+
var brotliDecompressAsync = promisify(brotliDecompress);
|
|
1796
2243
|
var RedisLayer = class {
|
|
1797
2244
|
name;
|
|
1798
2245
|
defaultTtl;
|
|
1799
2246
|
isLocal = false;
|
|
1800
2247
|
client;
|
|
1801
|
-
|
|
2248
|
+
serializers;
|
|
1802
2249
|
prefix;
|
|
1803
2250
|
allowUnprefixedClear;
|
|
1804
2251
|
scanCount;
|
|
1805
2252
|
compression;
|
|
1806
2253
|
compressionThreshold;
|
|
2254
|
+
disconnectOnDispose;
|
|
1807
2255
|
constructor(options) {
|
|
1808
2256
|
this.client = options.client;
|
|
1809
2257
|
this.defaultTtl = options.ttl;
|
|
1810
2258
|
this.name = options.name ?? "redis";
|
|
1811
|
-
this.
|
|
2259
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
1812
2260
|
this.prefix = options.prefix ?? "";
|
|
1813
2261
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
1814
2262
|
this.scanCount = options.scanCount ?? 100;
|
|
1815
2263
|
this.compression = options.compression;
|
|
1816
2264
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2265
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
1817
2266
|
}
|
|
1818
2267
|
async get(key) {
|
|
1819
2268
|
const payload = await this.getEntry(key);
|
|
@@ -1848,8 +2297,26 @@ var RedisLayer = class {
|
|
|
1848
2297
|
})
|
|
1849
2298
|
);
|
|
1850
2299
|
}
|
|
2300
|
+
async setMany(entries) {
|
|
2301
|
+
if (entries.length === 0) {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
const pipeline = this.client.pipeline();
|
|
2305
|
+
for (const entry of entries) {
|
|
2306
|
+
const serialized = this.primarySerializer().serialize(entry.value);
|
|
2307
|
+
const payload = await this.encodePayload(serialized);
|
|
2308
|
+
const normalizedKey = this.withPrefix(entry.key);
|
|
2309
|
+
if (entry.ttl && entry.ttl > 0) {
|
|
2310
|
+
pipeline.set(normalizedKey, payload, "EX", entry.ttl);
|
|
2311
|
+
} else {
|
|
2312
|
+
pipeline.set(normalizedKey, payload);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
await pipeline.exec();
|
|
2316
|
+
}
|
|
1851
2317
|
async set(key, value, ttl = this.defaultTtl) {
|
|
1852
|
-
const
|
|
2318
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2319
|
+
const payload = await this.encodePayload(serialized);
|
|
1853
2320
|
const normalizedKey = this.withPrefix(key);
|
|
1854
2321
|
if (ttl && ttl > 0) {
|
|
1855
2322
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -1881,6 +2348,18 @@ var RedisLayer = class {
|
|
|
1881
2348
|
const keys = await this.keys();
|
|
1882
2349
|
return keys.length;
|
|
1883
2350
|
}
|
|
2351
|
+
async ping() {
|
|
2352
|
+
try {
|
|
2353
|
+
return await this.client.ping() === "PONG";
|
|
2354
|
+
} catch {
|
|
2355
|
+
return false;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
async dispose() {
|
|
2359
|
+
if (this.disconnectOnDispose) {
|
|
2360
|
+
this.client.disconnect();
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
1884
2363
|
/**
|
|
1885
2364
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
1886
2365
|
* loading millions of keys into memory at once.
|
|
@@ -1927,17 +2406,48 @@ var RedisLayer = class {
|
|
|
1927
2406
|
return `${this.prefix}${key}`;
|
|
1928
2407
|
}
|
|
1929
2408
|
async deserializeOrDelete(key, payload) {
|
|
2409
|
+
const decodedPayload = await this.decodePayload(payload);
|
|
2410
|
+
for (const serializer of this.serializers) {
|
|
2411
|
+
try {
|
|
2412
|
+
const value = serializer.deserialize(decodedPayload);
|
|
2413
|
+
if (serializer !== this.primarySerializer()) {
|
|
2414
|
+
await this.rewriteWithPrimarySerializer(key, value).catch(() => void 0);
|
|
2415
|
+
}
|
|
2416
|
+
return value;
|
|
2417
|
+
} catch {
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
1930
2420
|
try {
|
|
1931
|
-
return this.serializer.deserialize(this.decodePayload(payload));
|
|
1932
|
-
} catch {
|
|
1933
2421
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
1934
|
-
|
|
2422
|
+
} catch {
|
|
1935
2423
|
}
|
|
2424
|
+
return null;
|
|
2425
|
+
}
|
|
2426
|
+
async rewriteWithPrimarySerializer(key, value) {
|
|
2427
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2428
|
+
const payload = await this.encodePayload(serialized);
|
|
2429
|
+
const ttl = await this.client.ttl(this.withPrefix(key));
|
|
2430
|
+
if (ttl > 0) {
|
|
2431
|
+
await this.client.set(this.withPrefix(key), payload, "EX", ttl);
|
|
2432
|
+
return;
|
|
2433
|
+
}
|
|
2434
|
+
await this.client.set(this.withPrefix(key), payload);
|
|
2435
|
+
}
|
|
2436
|
+
primarySerializer() {
|
|
2437
|
+
const serializer = this.serializers[0];
|
|
2438
|
+
if (!serializer) {
|
|
2439
|
+
throw new Error("RedisLayer requires at least one serializer.");
|
|
2440
|
+
}
|
|
2441
|
+
return serializer;
|
|
1936
2442
|
}
|
|
1937
2443
|
isSerializablePayload(payload) {
|
|
1938
2444
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
1939
2445
|
}
|
|
1940
|
-
|
|
2446
|
+
/**
|
|
2447
|
+
* Compresses the payload asynchronously if compression is enabled and the
|
|
2448
|
+
* payload exceeds the threshold. This avoids blocking the event loop.
|
|
2449
|
+
*/
|
|
2450
|
+
async encodePayload(payload) {
|
|
1941
2451
|
if (!this.compression) {
|
|
1942
2452
|
return payload;
|
|
1943
2453
|
}
|
|
@@ -1946,18 +2456,21 @@ var RedisLayer = class {
|
|
|
1946
2456
|
return payload;
|
|
1947
2457
|
}
|
|
1948
2458
|
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
1949
|
-
const compressed = this.compression === "gzip" ?
|
|
2459
|
+
const compressed = this.compression === "gzip" ? await gzipAsync(source) : await brotliCompressAsync(source);
|
|
1950
2460
|
return Buffer.concat([header, compressed]);
|
|
1951
2461
|
}
|
|
1952
|
-
|
|
2462
|
+
/**
|
|
2463
|
+
* Decompresses the payload asynchronously if a compression header is present.
|
|
2464
|
+
*/
|
|
2465
|
+
async decodePayload(payload) {
|
|
1953
2466
|
if (!Buffer.isBuffer(payload)) {
|
|
1954
2467
|
return payload;
|
|
1955
2468
|
}
|
|
1956
2469
|
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
1957
|
-
return
|
|
2470
|
+
return gunzipAsync(payload.subarray(10));
|
|
1958
2471
|
}
|
|
1959
2472
|
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
1960
|
-
return
|
|
2473
|
+
return brotliDecompressAsync(payload.subarray(12));
|
|
1961
2474
|
}
|
|
1962
2475
|
return payload;
|
|
1963
2476
|
}
|
|
@@ -1965,7 +2478,7 @@ var RedisLayer = class {
|
|
|
1965
2478
|
|
|
1966
2479
|
// src/layers/DiskLayer.ts
|
|
1967
2480
|
import { createHash } from "crypto";
|
|
1968
|
-
import { promises as
|
|
2481
|
+
import { promises as fs } from "fs";
|
|
1969
2482
|
import { join } from "path";
|
|
1970
2483
|
var DiskLayer = class {
|
|
1971
2484
|
name;
|
|
@@ -1973,11 +2486,14 @@ var DiskLayer = class {
|
|
|
1973
2486
|
isLocal = true;
|
|
1974
2487
|
directory;
|
|
1975
2488
|
serializer;
|
|
2489
|
+
maxFiles;
|
|
2490
|
+
writeQueue = Promise.resolve();
|
|
1976
2491
|
constructor(options) {
|
|
1977
2492
|
this.directory = options.directory;
|
|
1978
2493
|
this.defaultTtl = options.ttl;
|
|
1979
2494
|
this.name = options.name ?? "disk";
|
|
1980
2495
|
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2496
|
+
this.maxFiles = options.maxFiles;
|
|
1981
2497
|
}
|
|
1982
2498
|
async get(key) {
|
|
1983
2499
|
return unwrapStoredValue(await this.getEntry(key));
|
|
@@ -1986,7 +2502,7 @@ var DiskLayer = class {
|
|
|
1986
2502
|
const filePath = this.keyToPath(key);
|
|
1987
2503
|
let raw;
|
|
1988
2504
|
try {
|
|
1989
|
-
raw = await
|
|
2505
|
+
raw = await fs.readFile(filePath);
|
|
1990
2506
|
} catch {
|
|
1991
2507
|
return null;
|
|
1992
2508
|
}
|
|
@@ -2004,13 +2520,30 @@ var DiskLayer = class {
|
|
|
2004
2520
|
return entry.value;
|
|
2005
2521
|
}
|
|
2006
2522
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2007
|
-
await
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2523
|
+
await this.enqueueWrite(async () => {
|
|
2524
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
2525
|
+
const entry = {
|
|
2526
|
+
key,
|
|
2527
|
+
value,
|
|
2528
|
+
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null
|
|
2529
|
+
};
|
|
2530
|
+
const payload = this.serializer.serialize(entry);
|
|
2531
|
+
const targetPath = this.keyToPath(key);
|
|
2532
|
+
const tempPath = `${targetPath}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
2533
|
+
await fs.writeFile(tempPath, payload);
|
|
2534
|
+
await fs.rename(tempPath, targetPath);
|
|
2535
|
+
if (this.maxFiles !== void 0) {
|
|
2536
|
+
await this.enforceMaxFiles();
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
async getMany(keys) {
|
|
2541
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
2542
|
+
}
|
|
2543
|
+
async setMany(entries) {
|
|
2544
|
+
for (const entry of entries) {
|
|
2545
|
+
await this.set(entry.key, entry.value, entry.ttl);
|
|
2546
|
+
}
|
|
2014
2547
|
}
|
|
2015
2548
|
async has(key) {
|
|
2016
2549
|
const value = await this.getEntry(key);
|
|
@@ -2020,7 +2553,7 @@ var DiskLayer = class {
|
|
|
2020
2553
|
const filePath = this.keyToPath(key);
|
|
2021
2554
|
let raw;
|
|
2022
2555
|
try {
|
|
2023
|
-
raw = await
|
|
2556
|
+
raw = await fs.readFile(filePath);
|
|
2024
2557
|
} catch {
|
|
2025
2558
|
return null;
|
|
2026
2559
|
}
|
|
@@ -2040,45 +2573,125 @@ var DiskLayer = class {
|
|
|
2040
2573
|
return remaining;
|
|
2041
2574
|
}
|
|
2042
2575
|
async delete(key) {
|
|
2043
|
-
await this.safeDelete(this.keyToPath(key));
|
|
2576
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2044
2577
|
}
|
|
2045
2578
|
async deleteMany(keys) {
|
|
2046
|
-
await
|
|
2579
|
+
await this.enqueueWrite(async () => {
|
|
2580
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
2581
|
+
});
|
|
2047
2582
|
}
|
|
2048
2583
|
async clear() {
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2584
|
+
await this.enqueueWrite(async () => {
|
|
2585
|
+
let entries;
|
|
2586
|
+
try {
|
|
2587
|
+
entries = await fs.readdir(this.directory);
|
|
2588
|
+
} catch {
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
await Promise.all(
|
|
2592
|
+
entries.filter((name) => name.endsWith(".lc")).map((name) => this.safeDelete(join(this.directory, name)))
|
|
2593
|
+
);
|
|
2594
|
+
});
|
|
2058
2595
|
}
|
|
2596
|
+
/**
|
|
2597
|
+
* Returns the original cache key strings stored on disk.
|
|
2598
|
+
* Expired entries are skipped and cleaned up during the scan.
|
|
2599
|
+
*/
|
|
2059
2600
|
async keys() {
|
|
2060
2601
|
let entries;
|
|
2061
2602
|
try {
|
|
2062
|
-
entries = await
|
|
2603
|
+
entries = await fs.readdir(this.directory);
|
|
2063
2604
|
} catch {
|
|
2064
2605
|
return [];
|
|
2065
2606
|
}
|
|
2066
|
-
|
|
2607
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
2608
|
+
const keys = [];
|
|
2609
|
+
await Promise.all(
|
|
2610
|
+
lcFiles.map(async (name) => {
|
|
2611
|
+
const filePath = join(this.directory, name);
|
|
2612
|
+
let raw;
|
|
2613
|
+
try {
|
|
2614
|
+
raw = await fs.readFile(filePath);
|
|
2615
|
+
} catch {
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2618
|
+
let entry;
|
|
2619
|
+
try {
|
|
2620
|
+
entry = this.serializer.deserialize(raw);
|
|
2621
|
+
} catch {
|
|
2622
|
+
await this.safeDelete(filePath);
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
2626
|
+
await this.safeDelete(filePath);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
keys.push(entry.key);
|
|
2630
|
+
})
|
|
2631
|
+
);
|
|
2632
|
+
return keys;
|
|
2067
2633
|
}
|
|
2068
2634
|
async size() {
|
|
2069
2635
|
const keys = await this.keys();
|
|
2070
2636
|
return keys.length;
|
|
2071
2637
|
}
|
|
2638
|
+
async ping() {
|
|
2639
|
+
try {
|
|
2640
|
+
await fs.mkdir(this.directory, { recursive: true });
|
|
2641
|
+
return true;
|
|
2642
|
+
} catch {
|
|
2643
|
+
return false;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
async dispose() {
|
|
2647
|
+
}
|
|
2072
2648
|
keyToPath(key) {
|
|
2073
2649
|
const hash = createHash("sha256").update(key).digest("hex");
|
|
2074
2650
|
return join(this.directory, `${hash}.lc`);
|
|
2075
2651
|
}
|
|
2076
2652
|
async safeDelete(filePath) {
|
|
2077
2653
|
try {
|
|
2078
|
-
await
|
|
2654
|
+
await fs.unlink(filePath);
|
|
2079
2655
|
} catch {
|
|
2080
2656
|
}
|
|
2081
2657
|
}
|
|
2658
|
+
enqueueWrite(operation) {
|
|
2659
|
+
const next = this.writeQueue.then(operation, operation);
|
|
2660
|
+
this.writeQueue = next.catch(() => void 0);
|
|
2661
|
+
return next;
|
|
2662
|
+
}
|
|
2663
|
+
/**
|
|
2664
|
+
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
2665
|
+
*/
|
|
2666
|
+
async enforceMaxFiles() {
|
|
2667
|
+
if (this.maxFiles === void 0) {
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
let entries;
|
|
2671
|
+
try {
|
|
2672
|
+
entries = await fs.readdir(this.directory);
|
|
2673
|
+
} catch {
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
const lcFiles = entries.filter((name) => name.endsWith(".lc"));
|
|
2677
|
+
if (lcFiles.length <= this.maxFiles) {
|
|
2678
|
+
return;
|
|
2679
|
+
}
|
|
2680
|
+
const withStats = await Promise.all(
|
|
2681
|
+
lcFiles.map(async (name) => {
|
|
2682
|
+
const filePath = join(this.directory, name);
|
|
2683
|
+
try {
|
|
2684
|
+
const stat = await fs.stat(filePath);
|
|
2685
|
+
return { filePath, mtimeMs: stat.mtimeMs };
|
|
2686
|
+
} catch {
|
|
2687
|
+
return { filePath, mtimeMs: 0 };
|
|
2688
|
+
}
|
|
2689
|
+
})
|
|
2690
|
+
);
|
|
2691
|
+
withStats.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
|
2692
|
+
const toEvict = withStats.slice(0, lcFiles.length - this.maxFiles);
|
|
2693
|
+
await Promise.all(toEvict.map(({ filePath }) => this.safeDelete(filePath)));
|
|
2694
|
+
}
|
|
2082
2695
|
};
|
|
2083
2696
|
|
|
2084
2697
|
// src/layers/MemcachedLayer.ts
|
|
@@ -2088,29 +2701,41 @@ var MemcachedLayer = class {
|
|
|
2088
2701
|
isLocal = false;
|
|
2089
2702
|
client;
|
|
2090
2703
|
keyPrefix;
|
|
2704
|
+
serializer;
|
|
2091
2705
|
constructor(options) {
|
|
2092
2706
|
this.client = options.client;
|
|
2093
2707
|
this.defaultTtl = options.ttl;
|
|
2094
2708
|
this.name = options.name ?? "memcached";
|
|
2095
2709
|
this.keyPrefix = options.keyPrefix ?? "";
|
|
2710
|
+
this.serializer = options.serializer ?? new JsonSerializer();
|
|
2096
2711
|
}
|
|
2097
2712
|
async get(key) {
|
|
2713
|
+
return unwrapStoredValue(await this.getEntry(key));
|
|
2714
|
+
}
|
|
2715
|
+
async getEntry(key) {
|
|
2098
2716
|
const result = await this.client.get(this.withPrefix(key));
|
|
2099
2717
|
if (!result || result.value === null) {
|
|
2100
2718
|
return null;
|
|
2101
2719
|
}
|
|
2102
2720
|
try {
|
|
2103
|
-
return
|
|
2721
|
+
return this.serializer.deserialize(result.value);
|
|
2104
2722
|
} catch {
|
|
2105
2723
|
return null;
|
|
2106
2724
|
}
|
|
2107
2725
|
}
|
|
2726
|
+
async getMany(keys) {
|
|
2727
|
+
return Promise.all(keys.map((key) => this.getEntry(key)));
|
|
2728
|
+
}
|
|
2108
2729
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2109
|
-
const payload =
|
|
2730
|
+
const payload = this.serializer.serialize(value);
|
|
2110
2731
|
await this.client.set(this.withPrefix(key), payload, {
|
|
2111
2732
|
expires: ttl && ttl > 0 ? ttl : void 0
|
|
2112
2733
|
});
|
|
2113
2734
|
}
|
|
2735
|
+
async has(key) {
|
|
2736
|
+
const result = await this.client.get(this.withPrefix(key));
|
|
2737
|
+
return result !== null && result.value !== null;
|
|
2738
|
+
}
|
|
2114
2739
|
async delete(key) {
|
|
2115
2740
|
await this.client.delete(this.withPrefix(key));
|
|
2116
2741
|
}
|
|
@@ -2140,7 +2765,7 @@ var MsgpackSerializer = class {
|
|
|
2140
2765
|
};
|
|
2141
2766
|
|
|
2142
2767
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2143
|
-
import { randomUUID
|
|
2768
|
+
import { randomUUID } from "crypto";
|
|
2144
2769
|
var RELEASE_SCRIPT = `
|
|
2145
2770
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2146
2771
|
return redis.call("del", KEYS[1])
|
|
@@ -2156,7 +2781,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2156
2781
|
}
|
|
2157
2782
|
async execute(key, options, worker, waiter) {
|
|
2158
2783
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2159
|
-
const token =
|
|
2784
|
+
const token = randomUUID();
|
|
2160
2785
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2161
2786
|
if (acquired === "OK") {
|
|
2162
2787
|
try {
|
|
@@ -2204,6 +2829,12 @@ function createPrometheusMetricsExporter(stacks) {
|
|
|
2204
2829
|
lines.push("# TYPE layercache_hits_by_layer_total counter");
|
|
2205
2830
|
lines.push("# HELP layercache_misses_by_layer_total Misses broken down by layer");
|
|
2206
2831
|
lines.push("# TYPE layercache_misses_by_layer_total counter");
|
|
2832
|
+
lines.push("# HELP layercache_layer_latency_avg_ms Average read latency per layer in milliseconds");
|
|
2833
|
+
lines.push("# TYPE layercache_layer_latency_avg_ms gauge");
|
|
2834
|
+
lines.push("# HELP layercache_layer_latency_max_ms Maximum read latency per layer in milliseconds");
|
|
2835
|
+
lines.push("# TYPE layercache_layer_latency_max_ms gauge");
|
|
2836
|
+
lines.push("# HELP layercache_layer_latency_count Number of read latency samples per layer");
|
|
2837
|
+
lines.push("# TYPE layercache_layer_latency_count counter");
|
|
2207
2838
|
for (const { stack, name } of entries) {
|
|
2208
2839
|
const m = stack.getMetrics();
|
|
2209
2840
|
const hr = stack.getHitRate();
|
|
@@ -2227,6 +2858,12 @@ function createPrometheusMetricsExporter(stacks) {
|
|
|
2227
2858
|
for (const [layerName, count] of Object.entries(m.missesByLayer)) {
|
|
2228
2859
|
lines.push(`layercache_misses_by_layer_total{${label},layer="${sanitizeLabel(layerName)}"} ${count}`);
|
|
2229
2860
|
}
|
|
2861
|
+
for (const [layerName, latency] of Object.entries(m.latencyByLayer)) {
|
|
2862
|
+
const layerLabel = `${label},layer="${sanitizeLabel(layerName)}"`;
|
|
2863
|
+
lines.push(`layercache_layer_latency_avg_ms{${layerLabel}} ${latency.avgMs.toFixed(4)}`);
|
|
2864
|
+
lines.push(`layercache_layer_latency_max_ms{${layerLabel}} ${latency.maxMs.toFixed(4)}`);
|
|
2865
|
+
lines.push(`layercache_layer_latency_count{${layerLabel}} ${latency.count}`);
|
|
2866
|
+
}
|
|
2230
2867
|
}
|
|
2231
2868
|
lines.push("");
|
|
2232
2869
|
return lines.join("\n");
|
|
@@ -2236,6 +2873,7 @@ function sanitizeLabel(value) {
|
|
|
2236
2873
|
return value.replace(/["\\\n]/g, "_");
|
|
2237
2874
|
}
|
|
2238
2875
|
export {
|
|
2876
|
+
CacheMissError,
|
|
2239
2877
|
CacheNamespace,
|
|
2240
2878
|
CacheStack,
|
|
2241
2879
|
DiskLayer,
|
|
@@ -2253,7 +2891,10 @@ export {
|
|
|
2253
2891
|
cacheGraphqlResolver,
|
|
2254
2892
|
createCacheStatsHandler,
|
|
2255
2893
|
createCachedMethodDecorator,
|
|
2894
|
+
createExpressCacheMiddleware,
|
|
2256
2895
|
createFastifyLayercachePlugin,
|
|
2896
|
+
createHonoCacheMiddleware,
|
|
2897
|
+
createOpenTelemetryPlugin,
|
|
2257
2898
|
createPrometheusMetricsExporter,
|
|
2258
2899
|
createTrpcCacheMiddleware
|
|
2259
2900
|
};
|