layercache 1.2.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 +71 -5
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-GF47Y3XR.js} +13 -38
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +121 -21
- package/dist/cli.js +57 -2
- 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 +969 -195
- package/dist/index.d.cts +43 -567
- package/dist/index.d.ts +43 -567
- package/dist/index.js +849 -496
- package/package.json +7 -2
- package/packages/nestjs/dist/index.cjs +913 -375
- package/packages/nestjs/dist/index.d.cts +75 -0
- package/packages/nestjs/dist/index.d.ts +75 -0
- package/packages/nestjs/dist/index.js +901 -373
package/dist/index.js
CHANGED
|
@@ -1,14 +1,27 @@
|
|
|
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
|
|
24
|
+
import { Mutex } from "async-mutex";
|
|
12
25
|
var CacheNamespace = class _CacheNamespace {
|
|
13
26
|
constructor(cache, prefix) {
|
|
14
27
|
this.cache = cache;
|
|
@@ -16,57 +29,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
16
29
|
}
|
|
17
30
|
cache;
|
|
18
31
|
prefix;
|
|
32
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
metrics = emptyMetrics();
|
|
19
34
|
async get(key, fetcher, options) {
|
|
20
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
35
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
21
36
|
}
|
|
22
37
|
async getOrSet(key, fetcher, options) {
|
|
23
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
38
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
24
39
|
}
|
|
25
40
|
/**
|
|
26
41
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
27
42
|
*/
|
|
28
43
|
async getOrThrow(key, fetcher, options) {
|
|
29
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
44
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
30
45
|
}
|
|
31
46
|
async has(key) {
|
|
32
|
-
return this.cache.has(this.qualify(key));
|
|
47
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
33
48
|
}
|
|
34
49
|
async ttl(key) {
|
|
35
|
-
return this.cache.ttl(this.qualify(key));
|
|
50
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
36
51
|
}
|
|
37
52
|
async set(key, value, options) {
|
|
38
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
53
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
39
54
|
}
|
|
40
55
|
async delete(key) {
|
|
41
|
-
await this.cache.delete(this.qualify(key));
|
|
56
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
42
57
|
}
|
|
43
58
|
async mdelete(keys) {
|
|
44
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
59
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
45
60
|
}
|
|
46
61
|
async clear() {
|
|
47
|
-
await this.cache.
|
|
62
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
48
63
|
}
|
|
49
64
|
async mget(entries) {
|
|
50
|
-
return this.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
return this.trackMetrics(
|
|
66
|
+
() => this.cache.mget(
|
|
67
|
+
entries.map((entry) => ({
|
|
68
|
+
...entry,
|
|
69
|
+
key: this.qualify(entry.key)
|
|
70
|
+
}))
|
|
71
|
+
)
|
|
55
72
|
);
|
|
56
73
|
}
|
|
57
74
|
async mset(entries) {
|
|
58
|
-
await this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
await this.trackMetrics(
|
|
76
|
+
() => this.cache.mset(
|
|
77
|
+
entries.map((entry) => ({
|
|
78
|
+
...entry,
|
|
79
|
+
key: this.qualify(entry.key)
|
|
80
|
+
}))
|
|
81
|
+
)
|
|
63
82
|
);
|
|
64
83
|
}
|
|
65
84
|
async invalidateByTag(tag) {
|
|
66
|
-
await this.cache.invalidateByTag(tag);
|
|
85
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
86
|
+
}
|
|
87
|
+
async invalidateByTags(tags, mode = "any") {
|
|
88
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
67
89
|
}
|
|
68
90
|
async invalidateByPattern(pattern) {
|
|
69
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
91
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
92
|
+
}
|
|
93
|
+
async invalidateByPrefix(prefix) {
|
|
94
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
70
95
|
}
|
|
71
96
|
/**
|
|
72
97
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -87,10 +112,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
87
112
|
);
|
|
88
113
|
}
|
|
89
114
|
getMetrics() {
|
|
90
|
-
return this.
|
|
115
|
+
return cloneMetrics(this.metrics);
|
|
91
116
|
}
|
|
92
117
|
getHitRate() {
|
|
93
|
-
|
|
118
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
119
|
+
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
120
|
+
const byLayer = {};
|
|
121
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
122
|
+
for (const layer of layers) {
|
|
123
|
+
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
124
|
+
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
125
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
126
|
+
}
|
|
127
|
+
return { overall, byLayer };
|
|
94
128
|
}
|
|
95
129
|
/**
|
|
96
130
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -107,7 +141,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
107
141
|
qualify(key) {
|
|
108
142
|
return `${this.prefix}:${key}`;
|
|
109
143
|
}
|
|
144
|
+
async trackMetrics(operation) {
|
|
145
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
146
|
+
const before = this.cache.getMetrics();
|
|
147
|
+
const result = await operation();
|
|
148
|
+
const after = this.cache.getMetrics();
|
|
149
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
150
|
+
return result;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
getMetricsMutex() {
|
|
154
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
155
|
+
if (existing) {
|
|
156
|
+
return existing;
|
|
157
|
+
}
|
|
158
|
+
const mutex = new Mutex();
|
|
159
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
160
|
+
return mutex;
|
|
161
|
+
}
|
|
110
162
|
};
|
|
163
|
+
function emptyMetrics() {
|
|
164
|
+
return {
|
|
165
|
+
hits: 0,
|
|
166
|
+
misses: 0,
|
|
167
|
+
fetches: 0,
|
|
168
|
+
sets: 0,
|
|
169
|
+
deletes: 0,
|
|
170
|
+
backfills: 0,
|
|
171
|
+
invalidations: 0,
|
|
172
|
+
staleHits: 0,
|
|
173
|
+
refreshes: 0,
|
|
174
|
+
refreshErrors: 0,
|
|
175
|
+
writeFailures: 0,
|
|
176
|
+
singleFlightWaits: 0,
|
|
177
|
+
negativeCacheHits: 0,
|
|
178
|
+
circuitBreakerTrips: 0,
|
|
179
|
+
degradedOperations: 0,
|
|
180
|
+
hitsByLayer: {},
|
|
181
|
+
missesByLayer: {},
|
|
182
|
+
latencyByLayer: {},
|
|
183
|
+
resetAt: Date.now()
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function cloneMetrics(metrics) {
|
|
187
|
+
return {
|
|
188
|
+
...metrics,
|
|
189
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
190
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
191
|
+
latencyByLayer: Object.fromEntries(
|
|
192
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
193
|
+
)
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function diffMetrics(before, after) {
|
|
197
|
+
const latencyByLayer = Object.fromEntries(
|
|
198
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
199
|
+
layer,
|
|
200
|
+
{
|
|
201
|
+
avgMs: value.avgMs,
|
|
202
|
+
maxMs: value.maxMs,
|
|
203
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
204
|
+
}
|
|
205
|
+
])
|
|
206
|
+
);
|
|
207
|
+
return {
|
|
208
|
+
hits: after.hits - before.hits,
|
|
209
|
+
misses: after.misses - before.misses,
|
|
210
|
+
fetches: after.fetches - before.fetches,
|
|
211
|
+
sets: after.sets - before.sets,
|
|
212
|
+
deletes: after.deletes - before.deletes,
|
|
213
|
+
backfills: after.backfills - before.backfills,
|
|
214
|
+
invalidations: after.invalidations - before.invalidations,
|
|
215
|
+
staleHits: after.staleHits - before.staleHits,
|
|
216
|
+
refreshes: after.refreshes - before.refreshes,
|
|
217
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
218
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
219
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
220
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
221
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
222
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
223
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
224
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
225
|
+
latencyByLayer,
|
|
226
|
+
resetAt: after.resetAt
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
function addMetrics(base, delta) {
|
|
230
|
+
return {
|
|
231
|
+
hits: base.hits + delta.hits,
|
|
232
|
+
misses: base.misses + delta.misses,
|
|
233
|
+
fetches: base.fetches + delta.fetches,
|
|
234
|
+
sets: base.sets + delta.sets,
|
|
235
|
+
deletes: base.deletes + delta.deletes,
|
|
236
|
+
backfills: base.backfills + delta.backfills,
|
|
237
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
238
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
239
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
240
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
241
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
242
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
243
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
244
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
245
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
246
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
247
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
248
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
249
|
+
resetAt: base.resetAt
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function diffMap(before, after) {
|
|
253
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
254
|
+
const result = {};
|
|
255
|
+
for (const key of keys) {
|
|
256
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
257
|
+
}
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
function addMap(base, delta) {
|
|
261
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
262
|
+
const result = {};
|
|
263
|
+
for (const key of keys) {
|
|
264
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
111
268
|
|
|
112
269
|
// src/internal/CircuitBreakerManager.ts
|
|
113
270
|
var CircuitBreakerManager = class {
|
|
@@ -201,6 +358,95 @@ var CircuitBreakerManager = class {
|
|
|
201
358
|
}
|
|
202
359
|
};
|
|
203
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
|
+
|
|
204
450
|
// src/internal/MetricsCollector.ts
|
|
205
451
|
var MetricsCollector = class {
|
|
206
452
|
data = this.empty();
|
|
@@ -275,107 +521,6 @@ var MetricsCollector = class {
|
|
|
275
521
|
}
|
|
276
522
|
};
|
|
277
523
|
|
|
278
|
-
// src/internal/StoredValue.ts
|
|
279
|
-
function isStoredValueEnvelope(value) {
|
|
280
|
-
return typeof value === "object" && value !== null && "__layercache" in value && value.__layercache === 1 && "kind" in value;
|
|
281
|
-
}
|
|
282
|
-
function createStoredValueEnvelope(options) {
|
|
283
|
-
const now = options.now ?? Date.now();
|
|
284
|
-
const freshTtlSeconds = normalizePositiveSeconds(options.freshTtlSeconds);
|
|
285
|
-
const staleWhileRevalidateSeconds = normalizePositiveSeconds(options.staleWhileRevalidateSeconds);
|
|
286
|
-
const staleIfErrorSeconds = normalizePositiveSeconds(options.staleIfErrorSeconds);
|
|
287
|
-
const freshUntil = freshTtlSeconds ? now + freshTtlSeconds * 1e3 : null;
|
|
288
|
-
const staleUntil = freshUntil && staleWhileRevalidateSeconds ? freshUntil + staleWhileRevalidateSeconds * 1e3 : null;
|
|
289
|
-
const errorUntil = freshUntil && staleIfErrorSeconds ? freshUntil + staleIfErrorSeconds * 1e3 : null;
|
|
290
|
-
return {
|
|
291
|
-
__layercache: 1,
|
|
292
|
-
kind: options.kind,
|
|
293
|
-
value: options.value,
|
|
294
|
-
freshUntil,
|
|
295
|
-
staleUntil,
|
|
296
|
-
errorUntil,
|
|
297
|
-
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
298
|
-
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
299
|
-
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
function resolveStoredValue(stored, now = Date.now()) {
|
|
303
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
304
|
-
return { state: "fresh", value: stored, stored };
|
|
305
|
-
}
|
|
306
|
-
if (stored.freshUntil === null || stored.freshUntil > now) {
|
|
307
|
-
return { state: "fresh", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
308
|
-
}
|
|
309
|
-
if (stored.staleUntil !== null && stored.staleUntil > now) {
|
|
310
|
-
return { state: "stale-while-revalidate", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
311
|
-
}
|
|
312
|
-
if (stored.errorUntil !== null && stored.errorUntil > now) {
|
|
313
|
-
return { state: "stale-if-error", value: unwrapStoredValue(stored), stored, envelope: stored };
|
|
314
|
-
}
|
|
315
|
-
return { state: "expired", value: null, stored, envelope: stored };
|
|
316
|
-
}
|
|
317
|
-
function unwrapStoredValue(stored) {
|
|
318
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
319
|
-
return stored;
|
|
320
|
-
}
|
|
321
|
-
if (stored.kind === "empty") {
|
|
322
|
-
return null;
|
|
323
|
-
}
|
|
324
|
-
return stored.value ?? null;
|
|
325
|
-
}
|
|
326
|
-
function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
327
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
328
|
-
return void 0;
|
|
329
|
-
}
|
|
330
|
-
const expiry = maxExpiry(stored);
|
|
331
|
-
if (expiry === null) {
|
|
332
|
-
return void 0;
|
|
333
|
-
}
|
|
334
|
-
const remainingMs = expiry - now;
|
|
335
|
-
if (remainingMs <= 0) {
|
|
336
|
-
return 1;
|
|
337
|
-
}
|
|
338
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
339
|
-
}
|
|
340
|
-
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
341
|
-
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
342
|
-
return void 0;
|
|
343
|
-
}
|
|
344
|
-
const remainingMs = stored.freshUntil - now;
|
|
345
|
-
if (remainingMs <= 0) {
|
|
346
|
-
return 0;
|
|
347
|
-
}
|
|
348
|
-
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
349
|
-
}
|
|
350
|
-
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
351
|
-
if (!isStoredValueEnvelope(stored)) {
|
|
352
|
-
return stored;
|
|
353
|
-
}
|
|
354
|
-
return createStoredValueEnvelope({
|
|
355
|
-
kind: stored.kind,
|
|
356
|
-
value: stored.value,
|
|
357
|
-
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
358
|
-
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
359
|
-
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
360
|
-
now
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
function maxExpiry(stored) {
|
|
364
|
-
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
365
|
-
(value) => value !== null
|
|
366
|
-
);
|
|
367
|
-
if (values.length === 0) {
|
|
368
|
-
return null;
|
|
369
|
-
}
|
|
370
|
-
return Math.max(...values);
|
|
371
|
-
}
|
|
372
|
-
function normalizePositiveSeconds(value) {
|
|
373
|
-
if (!value || value <= 0) {
|
|
374
|
-
return void 0;
|
|
375
|
-
}
|
|
376
|
-
return value;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
524
|
// src/internal/TtlResolver.ts
|
|
380
525
|
var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
381
526
|
var TtlResolver = class {
|
|
@@ -397,13 +542,14 @@ var TtlResolver = class {
|
|
|
397
542
|
clearProfiles() {
|
|
398
543
|
this.accessProfiles.clear();
|
|
399
544
|
}
|
|
400
|
-
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;
|
|
401
547
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
402
548
|
layerName,
|
|
403
549
|
options?.negativeTtl,
|
|
404
550
|
globalNegativeTtl,
|
|
405
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
406
|
-
) : 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);
|
|
407
553
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
408
554
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
409
555
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -442,6 +588,29 @@ var TtlResolver = class {
|
|
|
442
588
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
443
589
|
return Math.max(1, Math.round(ttl + delta));
|
|
444
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
|
+
}
|
|
445
614
|
readLayerNumber(layerName, value) {
|
|
446
615
|
if (typeof value === "number") {
|
|
447
616
|
return value;
|
|
@@ -464,90 +633,8 @@ var TtlResolver = class {
|
|
|
464
633
|
}
|
|
465
634
|
};
|
|
466
635
|
|
|
467
|
-
// src/invalidation/TagIndex.ts
|
|
468
|
-
var TagIndex = class {
|
|
469
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
470
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
471
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
472
|
-
maxKnownKeys;
|
|
473
|
-
constructor(options = {}) {
|
|
474
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
475
|
-
}
|
|
476
|
-
async touch(key) {
|
|
477
|
-
this.knownKeys.add(key);
|
|
478
|
-
this.pruneKnownKeysIfNeeded();
|
|
479
|
-
}
|
|
480
|
-
async track(key, tags) {
|
|
481
|
-
this.knownKeys.add(key);
|
|
482
|
-
this.pruneKnownKeysIfNeeded();
|
|
483
|
-
if (tags.length === 0) {
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
const existingTags = this.keyToTags.get(key);
|
|
487
|
-
if (existingTags) {
|
|
488
|
-
for (const tag of existingTags) {
|
|
489
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
const tagSet = new Set(tags);
|
|
493
|
-
this.keyToTags.set(key, tagSet);
|
|
494
|
-
for (const tag of tagSet) {
|
|
495
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
496
|
-
keys.add(key);
|
|
497
|
-
this.tagToKeys.set(tag, keys);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
async remove(key) {
|
|
501
|
-
this.knownKeys.delete(key);
|
|
502
|
-
const tags = this.keyToTags.get(key);
|
|
503
|
-
if (!tags) {
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
for (const tag of tags) {
|
|
507
|
-
const keys = this.tagToKeys.get(tag);
|
|
508
|
-
if (!keys) {
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
keys.delete(key);
|
|
512
|
-
if (keys.size === 0) {
|
|
513
|
-
this.tagToKeys.delete(tag);
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
this.keyToTags.delete(key);
|
|
517
|
-
}
|
|
518
|
-
async keysForTag(tag) {
|
|
519
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
520
|
-
}
|
|
521
|
-
async tagsForKey(key) {
|
|
522
|
-
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
523
|
-
}
|
|
524
|
-
async matchPattern(pattern) {
|
|
525
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
526
|
-
}
|
|
527
|
-
async clear() {
|
|
528
|
-
this.tagToKeys.clear();
|
|
529
|
-
this.keyToTags.clear();
|
|
530
|
-
this.knownKeys.clear();
|
|
531
|
-
}
|
|
532
|
-
pruneKnownKeysIfNeeded() {
|
|
533
|
-
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
537
|
-
let removed = 0;
|
|
538
|
-
for (const key of this.knownKeys) {
|
|
539
|
-
if (removed >= toRemove) {
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
this.knownKeys.delete(key);
|
|
543
|
-
this.keyToTags.delete(key);
|
|
544
|
-
removed += 1;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
|
|
549
636
|
// src/stampede/StampedeGuard.ts
|
|
550
|
-
import { Mutex } from "async-mutex";
|
|
637
|
+
import { Mutex as Mutex2 } from "async-mutex";
|
|
551
638
|
var StampedeGuard = class {
|
|
552
639
|
mutexes = /* @__PURE__ */ new Map();
|
|
553
640
|
async execute(key, task) {
|
|
@@ -564,7 +651,7 @@ var StampedeGuard = class {
|
|
|
564
651
|
getMutexEntry(key) {
|
|
565
652
|
let entry = this.mutexes.get(key);
|
|
566
653
|
if (!entry) {
|
|
567
|
-
entry = { mutex: new
|
|
654
|
+
entry = { mutex: new Mutex2(), references: 0 };
|
|
568
655
|
this.mutexes.set(key, entry);
|
|
569
656
|
}
|
|
570
657
|
entry.references += 1;
|
|
@@ -625,6 +712,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
625
712
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
626
713
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
627
714
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
715
|
+
this.currentGeneration = options.generation;
|
|
628
716
|
if (options.publishSetInvalidation !== void 0) {
|
|
629
717
|
console.warn(
|
|
630
718
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -633,21 +721,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
633
721
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
634
722
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
635
723
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
724
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
636
725
|
this.startup = this.initialize();
|
|
637
726
|
}
|
|
638
727
|
layers;
|
|
639
728
|
options;
|
|
640
729
|
stampedeGuard = new StampedeGuard();
|
|
641
730
|
metricsCollector = new MetricsCollector();
|
|
642
|
-
instanceId =
|
|
731
|
+
instanceId = createInstanceId();
|
|
643
732
|
startup;
|
|
644
733
|
unsubscribeInvalidation;
|
|
645
734
|
logger;
|
|
646
735
|
tagIndex;
|
|
736
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
647
737
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
648
738
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
649
739
|
ttlResolver;
|
|
650
740
|
circuitBreakerManager;
|
|
741
|
+
currentGeneration;
|
|
742
|
+
writeBehindQueue = [];
|
|
743
|
+
writeBehindTimer;
|
|
744
|
+
writeBehindFlushPromise;
|
|
651
745
|
isDisconnecting = false;
|
|
652
746
|
disconnectPromise;
|
|
653
747
|
/**
|
|
@@ -657,9 +751,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
657
751
|
* and no `fetcher` is provided.
|
|
658
752
|
*/
|
|
659
753
|
async get(key, fetcher, options) {
|
|
660
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
754
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
661
755
|
this.validateWriteOptions(options);
|
|
662
|
-
await this.
|
|
756
|
+
await this.awaitStartup("get");
|
|
663
757
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
664
758
|
if (hit.found) {
|
|
665
759
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -724,8 +818,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
724
818
|
* Returns true if the given key exists and is not expired in any layer.
|
|
725
819
|
*/
|
|
726
820
|
async has(key) {
|
|
727
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
728
|
-
await this.
|
|
821
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
822
|
+
await this.awaitStartup("has");
|
|
729
823
|
for (const layer of this.layers) {
|
|
730
824
|
if (this.shouldSkipLayer(layer)) {
|
|
731
825
|
continue;
|
|
@@ -755,8 +849,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
755
849
|
* that has it, or null if the key is not found / has no TTL.
|
|
756
850
|
*/
|
|
757
851
|
async ttl(key) {
|
|
758
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
759
|
-
await this.
|
|
852
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
853
|
+
await this.awaitStartup("ttl");
|
|
760
854
|
for (const layer of this.layers) {
|
|
761
855
|
if (this.shouldSkipLayer(layer)) {
|
|
762
856
|
continue;
|
|
@@ -777,17 +871,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
777
871
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
778
872
|
*/
|
|
779
873
|
async set(key, value, options) {
|
|
780
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
874
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
781
875
|
this.validateWriteOptions(options);
|
|
782
|
-
await this.
|
|
876
|
+
await this.awaitStartup("set");
|
|
783
877
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
784
878
|
}
|
|
785
879
|
/**
|
|
786
880
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
787
881
|
*/
|
|
788
882
|
async delete(key) {
|
|
789
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
790
|
-
await this.
|
|
883
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
884
|
+
await this.awaitStartup("delete");
|
|
791
885
|
await this.deleteKeys([normalizedKey]);
|
|
792
886
|
await this.publishInvalidation({
|
|
793
887
|
scope: "key",
|
|
@@ -797,7 +891,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
797
891
|
});
|
|
798
892
|
}
|
|
799
893
|
async clear() {
|
|
800
|
-
await this.
|
|
894
|
+
await this.awaitStartup("clear");
|
|
801
895
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
802
896
|
await this.tagIndex.clear();
|
|
803
897
|
this.ttlResolver.clearProfiles();
|
|
@@ -813,23 +907,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
813
907
|
if (keys.length === 0) {
|
|
814
908
|
return;
|
|
815
909
|
}
|
|
816
|
-
await this.
|
|
910
|
+
await this.awaitStartup("mdelete");
|
|
817
911
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
818
|
-
|
|
912
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
913
|
+
await this.deleteKeys(cacheKeys);
|
|
819
914
|
await this.publishInvalidation({
|
|
820
915
|
scope: "keys",
|
|
821
|
-
keys:
|
|
916
|
+
keys: cacheKeys,
|
|
822
917
|
sourceId: this.instanceId,
|
|
823
918
|
operation: "delete"
|
|
824
919
|
});
|
|
825
920
|
}
|
|
826
921
|
async mget(entries) {
|
|
922
|
+
this.assertActive("mget");
|
|
827
923
|
if (entries.length === 0) {
|
|
828
924
|
return [];
|
|
829
925
|
}
|
|
830
926
|
const normalizedEntries = entries.map((entry) => ({
|
|
831
927
|
...entry,
|
|
832
|
-
key: this.validateCacheKey(entry.key)
|
|
928
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
833
929
|
}));
|
|
834
930
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
835
931
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -855,7 +951,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
855
951
|
})
|
|
856
952
|
);
|
|
857
953
|
}
|
|
858
|
-
await this.
|
|
954
|
+
await this.awaitStartup("mget");
|
|
859
955
|
const pending = /* @__PURE__ */ new Set();
|
|
860
956
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
861
957
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -903,14 +999,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
903
999
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
904
1000
|
}
|
|
905
1001
|
async mset(entries) {
|
|
1002
|
+
this.assertActive("mset");
|
|
906
1003
|
const normalizedEntries = entries.map((entry) => ({
|
|
907
1004
|
...entry,
|
|
908
|
-
key: this.validateCacheKey(entry.key)
|
|
1005
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
909
1006
|
}));
|
|
910
1007
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
911
|
-
await
|
|
1008
|
+
await this.awaitStartup("mset");
|
|
1009
|
+
await this.writeBatch(normalizedEntries);
|
|
912
1010
|
}
|
|
913
1011
|
async warm(entries, options = {}) {
|
|
1012
|
+
this.assertActive("warm");
|
|
914
1013
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
915
1014
|
const total = entries.length;
|
|
916
1015
|
let completed = 0;
|
|
@@ -959,14 +1058,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
959
1058
|
return new CacheNamespace(this, prefix);
|
|
960
1059
|
}
|
|
961
1060
|
async invalidateByTag(tag) {
|
|
962
|
-
await this.
|
|
1061
|
+
await this.awaitStartup("invalidateByTag");
|
|
963
1062
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
964
1063
|
await this.deleteKeys(keys);
|
|
965
1064
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
966
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
|
+
}
|
|
967
1076
|
async invalidateByPattern(pattern) {
|
|
968
|
-
await this.
|
|
969
|
-
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}*`);
|
|
970
1086
|
await this.deleteKeys(keys);
|
|
971
1087
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
972
1088
|
}
|
|
@@ -993,14 +1109,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
993
1109
|
getHitRate() {
|
|
994
1110
|
return this.metricsCollector.hitRate();
|
|
995
1111
|
}
|
|
1112
|
+
async healthCheck() {
|
|
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
|
+
}
|
|
996
1140
|
/**
|
|
997
1141
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
998
1142
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
999
1143
|
* Returns `null` if the key does not exist in any layer.
|
|
1000
1144
|
*/
|
|
1001
1145
|
async inspect(key) {
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1146
|
+
const userKey = this.validateCacheKey(key);
|
|
1147
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1148
|
+
await this.awaitStartup("inspect");
|
|
1004
1149
|
const foundInLayers = [];
|
|
1005
1150
|
let freshTtlSeconds = null;
|
|
1006
1151
|
let staleTtlSeconds = null;
|
|
@@ -1031,10 +1176,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1031
1176
|
return null;
|
|
1032
1177
|
}
|
|
1033
1178
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1034
|
-
return { key:
|
|
1179
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1035
1180
|
}
|
|
1036
1181
|
async exportState() {
|
|
1037
|
-
await this.
|
|
1182
|
+
await this.awaitStartup("exportState");
|
|
1038
1183
|
const exported = /* @__PURE__ */ new Map();
|
|
1039
1184
|
for (const layer of this.layers) {
|
|
1040
1185
|
if (!layer.keys) {
|
|
@@ -1042,15 +1187,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1042
1187
|
}
|
|
1043
1188
|
const keys = await layer.keys();
|
|
1044
1189
|
for (const key of keys) {
|
|
1045
|
-
|
|
1190
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1191
|
+
if (exported.has(exportedKey)) {
|
|
1046
1192
|
continue;
|
|
1047
1193
|
}
|
|
1048
1194
|
const stored = await this.readLayerEntry(layer, key);
|
|
1049
1195
|
if (stored === null) {
|
|
1050
1196
|
continue;
|
|
1051
1197
|
}
|
|
1052
|
-
exported.set(
|
|
1053
|
-
key,
|
|
1198
|
+
exported.set(exportedKey, {
|
|
1199
|
+
key: exportedKey,
|
|
1054
1200
|
value: stored,
|
|
1055
1201
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1056
1202
|
});
|
|
@@ -1059,20 +1205,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
1059
1205
|
return [...exported.values()];
|
|
1060
1206
|
}
|
|
1061
1207
|
async importState(entries) {
|
|
1062
|
-
await this.
|
|
1208
|
+
await this.awaitStartup("importState");
|
|
1063
1209
|
await Promise.all(
|
|
1064
1210
|
entries.map(async (entry) => {
|
|
1065
|
-
|
|
1066
|
-
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);
|
|
1067
1214
|
})
|
|
1068
1215
|
);
|
|
1069
1216
|
}
|
|
1070
1217
|
async persistToFile(filePath) {
|
|
1218
|
+
this.assertActive("persistToFile");
|
|
1071
1219
|
const snapshot = await this.exportState();
|
|
1072
|
-
|
|
1220
|
+
const { promises: fs2 } = await import("fs");
|
|
1221
|
+
await fs2.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1073
1222
|
}
|
|
1074
1223
|
async restoreFromFile(filePath) {
|
|
1075
|
-
|
|
1224
|
+
this.assertActive("restoreFromFile");
|
|
1225
|
+
const { promises: fs2 } = await import("fs");
|
|
1226
|
+
const raw = await fs2.readFile(filePath, "utf8");
|
|
1076
1227
|
let parsed;
|
|
1077
1228
|
try {
|
|
1078
1229
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1095,7 +1246,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1095
1246
|
this.disconnectPromise = (async () => {
|
|
1096
1247
|
await this.startup;
|
|
1097
1248
|
await this.unsubscribeInvalidation?.();
|
|
1249
|
+
await this.flushWriteBehindQueue();
|
|
1098
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()));
|
|
1099
1256
|
})();
|
|
1100
1257
|
}
|
|
1101
1258
|
await this.disconnectPromise;
|
|
@@ -1155,7 +1312,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1155
1312
|
const fetchStart = Date.now();
|
|
1156
1313
|
let fetched;
|
|
1157
1314
|
try {
|
|
1158
|
-
fetched = await
|
|
1315
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1316
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1317
|
+
fetcher
|
|
1318
|
+
);
|
|
1159
1319
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1160
1320
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1161
1321
|
} catch (error) {
|
|
@@ -1189,6 +1349,61 @@ var CacheStack = class extends EventEmitter {
|
|
|
1189
1349
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1190
1350
|
}
|
|
1191
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
|
+
}
|
|
1192
1407
|
async readFromLayers(key, options, mode) {
|
|
1193
1408
|
let sawRetainableValue = false;
|
|
1194
1409
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1272,33 +1487,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1272
1487
|
}
|
|
1273
1488
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1274
1489
|
const now = Date.now();
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
options
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
});
|
|
1294
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1295
|
-
try {
|
|
1296
|
-
await layer.set(key, payload, ttl);
|
|
1297
|
-
} catch (error) {
|
|
1298
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
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);
|
|
1299
1508
|
}
|
|
1300
|
-
}
|
|
1301
|
-
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)));
|
|
1302
1512
|
}
|
|
1303
1513
|
async executeLayerOperations(operations, context) {
|
|
1304
1514
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1322,8 +1532,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1322
1532
|
);
|
|
1323
1533
|
}
|
|
1324
1534
|
}
|
|
1325
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1326
|
-
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
|
+
);
|
|
1327
1546
|
}
|
|
1328
1547
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1329
1548
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1417,6 +1636,105 @@ var CacheStack = class extends EventEmitter {
|
|
|
1417
1636
|
shouldBroadcastL1Invalidation() {
|
|
1418
1637
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1419
1638
|
}
|
|
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
|
+
}
|
|
1420
1738
|
async deleteKeysFromLayers(layers, keys) {
|
|
1421
1739
|
await Promise.all(
|
|
1422
1740
|
layers.map(async (layer) => {
|
|
@@ -1460,6 +1778,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1460
1778
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1461
1779
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1462
1780
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1781
|
+
if (this.options.generation !== void 0) {
|
|
1782
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
1783
|
+
}
|
|
1463
1784
|
}
|
|
1464
1785
|
validateWriteOptions(options) {
|
|
1465
1786
|
if (!options) {
|
|
@@ -1471,6 +1792,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1471
1792
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1472
1793
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1473
1794
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1795
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1474
1796
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1475
1797
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1476
1798
|
}
|
|
@@ -1514,6 +1836,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1514
1836
|
}
|
|
1515
1837
|
return key;
|
|
1516
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
|
+
}
|
|
1517
1859
|
serializeOptions(options) {
|
|
1518
1860
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1519
1861
|
}
|
|
@@ -1619,18 +1961,23 @@ var CacheStack = class extends EventEmitter {
|
|
|
1619
1961
|
return value;
|
|
1620
1962
|
}
|
|
1621
1963
|
};
|
|
1964
|
+
function createInstanceId() {
|
|
1965
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
1966
|
+
}
|
|
1622
1967
|
|
|
1623
1968
|
// src/invalidation/RedisInvalidationBus.ts
|
|
1624
1969
|
var RedisInvalidationBus = class {
|
|
1625
1970
|
channel;
|
|
1626
1971
|
publisher;
|
|
1627
1972
|
subscriber;
|
|
1973
|
+
logger;
|
|
1628
1974
|
handlers = /* @__PURE__ */ new Set();
|
|
1629
1975
|
sharedListener;
|
|
1630
1976
|
constructor(options) {
|
|
1631
1977
|
this.publisher = options.publisher;
|
|
1632
1978
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
1633
1979
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
1980
|
+
this.logger = options.logger;
|
|
1634
1981
|
}
|
|
1635
1982
|
async subscribe(handler) {
|
|
1636
1983
|
if (this.handlers.size === 0) {
|
|
@@ -1687,6 +2034,10 @@ var RedisInvalidationBus = class {
|
|
|
1687
2034
|
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1688
2035
|
}
|
|
1689
2036
|
reportError(message, error) {
|
|
2037
|
+
if (this.logger?.error) {
|
|
2038
|
+
this.logger.error(message, { error });
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
1690
2041
|
console.error(`[layercache] ${message}`, error);
|
|
1691
2042
|
}
|
|
1692
2043
|
};
|
|
@@ -1739,32 +2090,36 @@ function createFastifyLayercachePlugin(cache, options = {}) {
|
|
|
1739
2090
|
function createExpressCacheMiddleware(cache, options = {}) {
|
|
1740
2091
|
const allowedMethods = new Set((options.methods ?? ["GET"]).map((m) => m.toUpperCase()));
|
|
1741
2092
|
return async (req, res, next) => {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
const key = options.keyResolver ? options.keyResolver(req) : `${method}:${req.originalUrl ?? req.url ?? "/"}`;
|
|
1748
|
-
const cached = await cache.get(key, void 0, options);
|
|
1749
|
-
if (cached !== null) {
|
|
1750
|
-
res.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1751
|
-
res.setHeader?.("x-cache", "HIT");
|
|
1752
|
-
if (res.json) {
|
|
1753
|
-
res.json(cached);
|
|
1754
|
-
} else {
|
|
1755
|
-
res.end?.(JSON.stringify(cached));
|
|
2093
|
+
try {
|
|
2094
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
2095
|
+
if (!allowedMethods.has(method)) {
|
|
2096
|
+
next();
|
|
2097
|
+
return;
|
|
1756
2098
|
}
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
res.
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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);
|
|
1766
2122
|
}
|
|
1767
|
-
next();
|
|
1768
2123
|
};
|
|
1769
2124
|
}
|
|
1770
2125
|
|
|
@@ -1777,6 +2132,68 @@ function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
|
1777
2132
|
return (...args) => wrapped(...args);
|
|
1778
2133
|
}
|
|
1779
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
|
+
|
|
1780
2197
|
// src/integrations/trpc.ts
|
|
1781
2198
|
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1782
2199
|
return async (context) => {
|
|
@@ -1802,163 +2219,6 @@ function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
|
1802
2219
|
};
|
|
1803
2220
|
}
|
|
1804
2221
|
|
|
1805
|
-
// src/layers/MemoryLayer.ts
|
|
1806
|
-
var MemoryLayer = class {
|
|
1807
|
-
name;
|
|
1808
|
-
defaultTtl;
|
|
1809
|
-
isLocal = true;
|
|
1810
|
-
maxSize;
|
|
1811
|
-
evictionPolicy;
|
|
1812
|
-
entries = /* @__PURE__ */ new Map();
|
|
1813
|
-
constructor(options = {}) {
|
|
1814
|
-
this.name = options.name ?? "memory";
|
|
1815
|
-
this.defaultTtl = options.ttl;
|
|
1816
|
-
this.maxSize = options.maxSize ?? 1e3;
|
|
1817
|
-
this.evictionPolicy = options.evictionPolicy ?? "lru";
|
|
1818
|
-
}
|
|
1819
|
-
async get(key) {
|
|
1820
|
-
const value = await this.getEntry(key);
|
|
1821
|
-
return unwrapStoredValue(value);
|
|
1822
|
-
}
|
|
1823
|
-
async getEntry(key) {
|
|
1824
|
-
const entry = this.entries.get(key);
|
|
1825
|
-
if (!entry) {
|
|
1826
|
-
return null;
|
|
1827
|
-
}
|
|
1828
|
-
if (this.isExpired(entry)) {
|
|
1829
|
-
this.entries.delete(key);
|
|
1830
|
-
return null;
|
|
1831
|
-
}
|
|
1832
|
-
if (this.evictionPolicy === "lru") {
|
|
1833
|
-
this.entries.delete(key);
|
|
1834
|
-
entry.accessCount += 1;
|
|
1835
|
-
this.entries.set(key, entry);
|
|
1836
|
-
} else if (this.evictionPolicy === "lfu") {
|
|
1837
|
-
entry.accessCount += 1;
|
|
1838
|
-
}
|
|
1839
|
-
return entry.value;
|
|
1840
|
-
}
|
|
1841
|
-
async getMany(keys) {
|
|
1842
|
-
const values = [];
|
|
1843
|
-
for (const key of keys) {
|
|
1844
|
-
values.push(await this.getEntry(key));
|
|
1845
|
-
}
|
|
1846
|
-
return values;
|
|
1847
|
-
}
|
|
1848
|
-
async set(key, value, ttl = this.defaultTtl) {
|
|
1849
|
-
this.entries.delete(key);
|
|
1850
|
-
this.entries.set(key, {
|
|
1851
|
-
value,
|
|
1852
|
-
expiresAt: ttl && ttl > 0 ? Date.now() + ttl * 1e3 : null,
|
|
1853
|
-
accessCount: 0,
|
|
1854
|
-
insertedAt: Date.now()
|
|
1855
|
-
});
|
|
1856
|
-
while (this.entries.size > this.maxSize) {
|
|
1857
|
-
this.evict();
|
|
1858
|
-
}
|
|
1859
|
-
}
|
|
1860
|
-
async has(key) {
|
|
1861
|
-
const entry = this.entries.get(key);
|
|
1862
|
-
if (!entry) {
|
|
1863
|
-
return false;
|
|
1864
|
-
}
|
|
1865
|
-
if (this.isExpired(entry)) {
|
|
1866
|
-
this.entries.delete(key);
|
|
1867
|
-
return false;
|
|
1868
|
-
}
|
|
1869
|
-
return true;
|
|
1870
|
-
}
|
|
1871
|
-
async ttl(key) {
|
|
1872
|
-
const entry = this.entries.get(key);
|
|
1873
|
-
if (!entry) {
|
|
1874
|
-
return null;
|
|
1875
|
-
}
|
|
1876
|
-
if (this.isExpired(entry)) {
|
|
1877
|
-
this.entries.delete(key);
|
|
1878
|
-
return null;
|
|
1879
|
-
}
|
|
1880
|
-
if (entry.expiresAt === null) {
|
|
1881
|
-
return null;
|
|
1882
|
-
}
|
|
1883
|
-
return Math.max(0, Math.ceil((entry.expiresAt - Date.now()) / 1e3));
|
|
1884
|
-
}
|
|
1885
|
-
async size() {
|
|
1886
|
-
this.pruneExpired();
|
|
1887
|
-
return this.entries.size;
|
|
1888
|
-
}
|
|
1889
|
-
async delete(key) {
|
|
1890
|
-
this.entries.delete(key);
|
|
1891
|
-
}
|
|
1892
|
-
async deleteMany(keys) {
|
|
1893
|
-
for (const key of keys) {
|
|
1894
|
-
this.entries.delete(key);
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
async clear() {
|
|
1898
|
-
this.entries.clear();
|
|
1899
|
-
}
|
|
1900
|
-
async keys() {
|
|
1901
|
-
this.pruneExpired();
|
|
1902
|
-
return [...this.entries.keys()];
|
|
1903
|
-
}
|
|
1904
|
-
exportState() {
|
|
1905
|
-
this.pruneExpired();
|
|
1906
|
-
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1907
|
-
key,
|
|
1908
|
-
value: entry.value,
|
|
1909
|
-
expiresAt: entry.expiresAt
|
|
1910
|
-
}));
|
|
1911
|
-
}
|
|
1912
|
-
importState(entries) {
|
|
1913
|
-
for (const entry of entries) {
|
|
1914
|
-
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1915
|
-
continue;
|
|
1916
|
-
}
|
|
1917
|
-
this.entries.set(entry.key, {
|
|
1918
|
-
value: entry.value,
|
|
1919
|
-
expiresAt: entry.expiresAt,
|
|
1920
|
-
accessCount: 0,
|
|
1921
|
-
insertedAt: Date.now()
|
|
1922
|
-
});
|
|
1923
|
-
}
|
|
1924
|
-
while (this.entries.size > this.maxSize) {
|
|
1925
|
-
this.evict();
|
|
1926
|
-
}
|
|
1927
|
-
}
|
|
1928
|
-
evict() {
|
|
1929
|
-
if (this.evictionPolicy === "lru" || this.evictionPolicy === "fifo") {
|
|
1930
|
-
const oldestKey = this.entries.keys().next().value;
|
|
1931
|
-
if (oldestKey !== void 0) {
|
|
1932
|
-
this.entries.delete(oldestKey);
|
|
1933
|
-
}
|
|
1934
|
-
return;
|
|
1935
|
-
}
|
|
1936
|
-
let victimKey;
|
|
1937
|
-
let minCount = Number.POSITIVE_INFINITY;
|
|
1938
|
-
let minInsertedAt = Number.POSITIVE_INFINITY;
|
|
1939
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1940
|
-
if (entry.accessCount < minCount || entry.accessCount === minCount && entry.insertedAt < minInsertedAt) {
|
|
1941
|
-
minCount = entry.accessCount;
|
|
1942
|
-
minInsertedAt = entry.insertedAt;
|
|
1943
|
-
victimKey = key;
|
|
1944
|
-
}
|
|
1945
|
-
}
|
|
1946
|
-
if (victimKey !== void 0) {
|
|
1947
|
-
this.entries.delete(victimKey);
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
pruneExpired() {
|
|
1951
|
-
for (const [key, entry] of this.entries.entries()) {
|
|
1952
|
-
if (this.isExpired(entry)) {
|
|
1953
|
-
this.entries.delete(key);
|
|
1954
|
-
}
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
isExpired(entry) {
|
|
1958
|
-
return entry.expiresAt !== null && entry.expiresAt <= Date.now();
|
|
1959
|
-
}
|
|
1960
|
-
};
|
|
1961
|
-
|
|
1962
2222
|
// src/layers/RedisLayer.ts
|
|
1963
2223
|
import { promisify } from "util";
|
|
1964
2224
|
import { brotliCompress, brotliDecompress, gunzip, gzip } from "zlib";
|
|
@@ -1985,22 +2245,24 @@ var RedisLayer = class {
|
|
|
1985
2245
|
defaultTtl;
|
|
1986
2246
|
isLocal = false;
|
|
1987
2247
|
client;
|
|
1988
|
-
|
|
2248
|
+
serializers;
|
|
1989
2249
|
prefix;
|
|
1990
2250
|
allowUnprefixedClear;
|
|
1991
2251
|
scanCount;
|
|
1992
2252
|
compression;
|
|
1993
2253
|
compressionThreshold;
|
|
2254
|
+
disconnectOnDispose;
|
|
1994
2255
|
constructor(options) {
|
|
1995
2256
|
this.client = options.client;
|
|
1996
2257
|
this.defaultTtl = options.ttl;
|
|
1997
2258
|
this.name = options.name ?? "redis";
|
|
1998
|
-
this.
|
|
2259
|
+
this.serializers = Array.isArray(options.serializer) ? options.serializer : [options.serializer ?? new JsonSerializer()];
|
|
1999
2260
|
this.prefix = options.prefix ?? "";
|
|
2000
2261
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
2001
2262
|
this.scanCount = options.scanCount ?? 100;
|
|
2002
2263
|
this.compression = options.compression;
|
|
2003
2264
|
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
2265
|
+
this.disconnectOnDispose = options.disconnectOnDispose ?? false;
|
|
2004
2266
|
}
|
|
2005
2267
|
async get(key) {
|
|
2006
2268
|
const payload = await this.getEntry(key);
|
|
@@ -2035,8 +2297,25 @@ var RedisLayer = class {
|
|
|
2035
2297
|
})
|
|
2036
2298
|
);
|
|
2037
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
|
+
}
|
|
2038
2317
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2039
|
-
const serialized = this.
|
|
2318
|
+
const serialized = this.primarySerializer().serialize(value);
|
|
2040
2319
|
const payload = await this.encodePayload(serialized);
|
|
2041
2320
|
const normalizedKey = this.withPrefix(key);
|
|
2042
2321
|
if (ttl && ttl > 0) {
|
|
@@ -2069,6 +2348,18 @@ var RedisLayer = class {
|
|
|
2069
2348
|
const keys = await this.keys();
|
|
2070
2349
|
return keys.length;
|
|
2071
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
|
+
}
|
|
2072
2363
|
/**
|
|
2073
2364
|
* Deletes all keys matching the layer's prefix in batches to avoid
|
|
2074
2365
|
* loading millions of keys into memory at once.
|
|
@@ -2115,12 +2406,39 @@ var RedisLayer = class {
|
|
|
2115
2406
|
return `${this.prefix}${key}`;
|
|
2116
2407
|
}
|
|
2117
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
|
+
}
|
|
2118
2420
|
try {
|
|
2119
|
-
return this.serializer.deserialize(await this.decodePayload(payload));
|
|
2120
|
-
} catch {
|
|
2121
2421
|
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
2122
|
-
|
|
2422
|
+
} catch {
|
|
2123
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;
|
|
2124
2442
|
}
|
|
2125
2443
|
isSerializablePayload(payload) {
|
|
2126
2444
|
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
@@ -2160,7 +2478,7 @@ var RedisLayer = class {
|
|
|
2160
2478
|
|
|
2161
2479
|
// src/layers/DiskLayer.ts
|
|
2162
2480
|
import { createHash } from "crypto";
|
|
2163
|
-
import { promises as
|
|
2481
|
+
import { promises as fs } from "fs";
|
|
2164
2482
|
import { join } from "path";
|
|
2165
2483
|
var DiskLayer = class {
|
|
2166
2484
|
name;
|
|
@@ -2169,6 +2487,7 @@ var DiskLayer = class {
|
|
|
2169
2487
|
directory;
|
|
2170
2488
|
serializer;
|
|
2171
2489
|
maxFiles;
|
|
2490
|
+
writeQueue = Promise.resolve();
|
|
2172
2491
|
constructor(options) {
|
|
2173
2492
|
this.directory = options.directory;
|
|
2174
2493
|
this.defaultTtl = options.ttl;
|
|
@@ -2183,7 +2502,7 @@ var DiskLayer = class {
|
|
|
2183
2502
|
const filePath = this.keyToPath(key);
|
|
2184
2503
|
let raw;
|
|
2185
2504
|
try {
|
|
2186
|
-
raw = await
|
|
2505
|
+
raw = await fs.readFile(filePath);
|
|
2187
2506
|
} catch {
|
|
2188
2507
|
return null;
|
|
2189
2508
|
}
|
|
@@ -2201,16 +2520,29 @@ var DiskLayer = class {
|
|
|
2201
2520
|
return entry.value;
|
|
2202
2521
|
}
|
|
2203
2522
|
async set(key, value, ttl = this.defaultTtl) {
|
|
2204
|
-
await
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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);
|
|
2214
2546
|
}
|
|
2215
2547
|
}
|
|
2216
2548
|
async has(key) {
|
|
@@ -2221,7 +2553,7 @@ var DiskLayer = class {
|
|
|
2221
2553
|
const filePath = this.keyToPath(key);
|
|
2222
2554
|
let raw;
|
|
2223
2555
|
try {
|
|
2224
|
-
raw = await
|
|
2556
|
+
raw = await fs.readFile(filePath);
|
|
2225
2557
|
} catch {
|
|
2226
2558
|
return null;
|
|
2227
2559
|
}
|
|
@@ -2241,21 +2573,25 @@ var DiskLayer = class {
|
|
|
2241
2573
|
return remaining;
|
|
2242
2574
|
}
|
|
2243
2575
|
async delete(key) {
|
|
2244
|
-
await this.safeDelete(this.keyToPath(key));
|
|
2576
|
+
await this.enqueueWrite(() => this.safeDelete(this.keyToPath(key)));
|
|
2245
2577
|
}
|
|
2246
2578
|
async deleteMany(keys) {
|
|
2247
|
-
await
|
|
2579
|
+
await this.enqueueWrite(async () => {
|
|
2580
|
+
await Promise.all(keys.map((key) => this.safeDelete(this.keyToPath(key))));
|
|
2581
|
+
});
|
|
2248
2582
|
}
|
|
2249
2583
|
async clear() {
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
+
});
|
|
2259
2595
|
}
|
|
2260
2596
|
/**
|
|
2261
2597
|
* Returns the original cache key strings stored on disk.
|
|
@@ -2264,7 +2600,7 @@ var DiskLayer = class {
|
|
|
2264
2600
|
async keys() {
|
|
2265
2601
|
let entries;
|
|
2266
2602
|
try {
|
|
2267
|
-
entries = await
|
|
2603
|
+
entries = await fs.readdir(this.directory);
|
|
2268
2604
|
} catch {
|
|
2269
2605
|
return [];
|
|
2270
2606
|
}
|
|
@@ -2275,7 +2611,7 @@ var DiskLayer = class {
|
|
|
2275
2611
|
const filePath = join(this.directory, name);
|
|
2276
2612
|
let raw;
|
|
2277
2613
|
try {
|
|
2278
|
-
raw = await
|
|
2614
|
+
raw = await fs.readFile(filePath);
|
|
2279
2615
|
} catch {
|
|
2280
2616
|
return;
|
|
2281
2617
|
}
|
|
@@ -2299,16 +2635,31 @@ var DiskLayer = class {
|
|
|
2299
2635
|
const keys = await this.keys();
|
|
2300
2636
|
return keys.length;
|
|
2301
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
|
+
}
|
|
2302
2648
|
keyToPath(key) {
|
|
2303
2649
|
const hash = createHash("sha256").update(key).digest("hex");
|
|
2304
2650
|
return join(this.directory, `${hash}.lc`);
|
|
2305
2651
|
}
|
|
2306
2652
|
async safeDelete(filePath) {
|
|
2307
2653
|
try {
|
|
2308
|
-
await
|
|
2654
|
+
await fs.unlink(filePath);
|
|
2309
2655
|
} catch {
|
|
2310
2656
|
}
|
|
2311
2657
|
}
|
|
2658
|
+
enqueueWrite(operation) {
|
|
2659
|
+
const next = this.writeQueue.then(operation, operation);
|
|
2660
|
+
this.writeQueue = next.catch(() => void 0);
|
|
2661
|
+
return next;
|
|
2662
|
+
}
|
|
2312
2663
|
/**
|
|
2313
2664
|
* Removes the oldest files (by mtime) when the directory exceeds maxFiles.
|
|
2314
2665
|
*/
|
|
@@ -2318,7 +2669,7 @@ var DiskLayer = class {
|
|
|
2318
2669
|
}
|
|
2319
2670
|
let entries;
|
|
2320
2671
|
try {
|
|
2321
|
-
entries = await
|
|
2672
|
+
entries = await fs.readdir(this.directory);
|
|
2322
2673
|
} catch {
|
|
2323
2674
|
return;
|
|
2324
2675
|
}
|
|
@@ -2330,7 +2681,7 @@ var DiskLayer = class {
|
|
|
2330
2681
|
lcFiles.map(async (name) => {
|
|
2331
2682
|
const filePath = join(this.directory, name);
|
|
2332
2683
|
try {
|
|
2333
|
-
const stat = await
|
|
2684
|
+
const stat = await fs.stat(filePath);
|
|
2334
2685
|
return { filePath, mtimeMs: stat.mtimeMs };
|
|
2335
2686
|
} catch {
|
|
2336
2687
|
return { filePath, mtimeMs: 0 };
|
|
@@ -2414,7 +2765,7 @@ var MsgpackSerializer = class {
|
|
|
2414
2765
|
};
|
|
2415
2766
|
|
|
2416
2767
|
// src/singleflight/RedisSingleFlightCoordinator.ts
|
|
2417
|
-
import { randomUUID
|
|
2768
|
+
import { randomUUID } from "crypto";
|
|
2418
2769
|
var RELEASE_SCRIPT = `
|
|
2419
2770
|
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
2420
2771
|
return redis.call("del", KEYS[1])
|
|
@@ -2430,7 +2781,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
2430
2781
|
}
|
|
2431
2782
|
async execute(key, options, worker, waiter) {
|
|
2432
2783
|
const lockKey = `${this.prefix}:${encodeURIComponent(key)}`;
|
|
2433
|
-
const token =
|
|
2784
|
+
const token = randomUUID();
|
|
2434
2785
|
const acquired = await this.client.set(lockKey, token, "PX", options.leaseMs, "NX");
|
|
2435
2786
|
if (acquired === "OK") {
|
|
2436
2787
|
try {
|
|
@@ -2542,6 +2893,8 @@ export {
|
|
|
2542
2893
|
createCachedMethodDecorator,
|
|
2543
2894
|
createExpressCacheMiddleware,
|
|
2544
2895
|
createFastifyLayercachePlugin,
|
|
2896
|
+
createHonoCacheMiddleware,
|
|
2897
|
+
createOpenTelemetryPlugin,
|
|
2545
2898
|
createPrometheusMetricsExporter,
|
|
2546
2899
|
createTrpcCacheMiddleware
|
|
2547
2900
|
};
|