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
|
@@ -46,63 +46,260 @@ function Cacheable(options) {
|
|
|
46
46
|
import { Global, Inject, Module } from "@nestjs/common";
|
|
47
47
|
|
|
48
48
|
// ../../src/CacheStack.ts
|
|
49
|
-
import { randomUUID } from "crypto";
|
|
50
49
|
import { EventEmitter } from "events";
|
|
51
|
-
|
|
50
|
+
|
|
51
|
+
// ../../node_modules/async-mutex/index.mjs
|
|
52
|
+
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
|
|
53
|
+
var E_ALREADY_LOCKED = new Error("mutex already locked");
|
|
54
|
+
var E_CANCELED = new Error("request for lock canceled");
|
|
55
|
+
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
|
|
56
|
+
function adopt(value) {
|
|
57
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
58
|
+
resolve(value);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
62
|
+
function fulfilled(value) {
|
|
63
|
+
try {
|
|
64
|
+
step(generator.next(value));
|
|
65
|
+
} catch (e) {
|
|
66
|
+
reject(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function rejected(value) {
|
|
70
|
+
try {
|
|
71
|
+
step(generator["throw"](value));
|
|
72
|
+
} catch (e) {
|
|
73
|
+
reject(e);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function step(result) {
|
|
77
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
78
|
+
}
|
|
79
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
var Semaphore = class {
|
|
83
|
+
constructor(_value, _cancelError = E_CANCELED) {
|
|
84
|
+
this._value = _value;
|
|
85
|
+
this._cancelError = _cancelError;
|
|
86
|
+
this._weightedQueues = [];
|
|
87
|
+
this._weightedWaiters = [];
|
|
88
|
+
}
|
|
89
|
+
acquire(weight = 1) {
|
|
90
|
+
if (weight <= 0)
|
|
91
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
92
|
+
return new Promise((resolve, reject) => {
|
|
93
|
+
if (!this._weightedQueues[weight - 1])
|
|
94
|
+
this._weightedQueues[weight - 1] = [];
|
|
95
|
+
this._weightedQueues[weight - 1].push({ resolve, reject });
|
|
96
|
+
this._dispatch();
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
runExclusive(callback, weight = 1) {
|
|
100
|
+
return __awaiter$2(this, void 0, void 0, function* () {
|
|
101
|
+
const [value, release] = yield this.acquire(weight);
|
|
102
|
+
try {
|
|
103
|
+
return yield callback(value);
|
|
104
|
+
} finally {
|
|
105
|
+
release();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
waitForUnlock(weight = 1) {
|
|
110
|
+
if (weight <= 0)
|
|
111
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
if (!this._weightedWaiters[weight - 1])
|
|
114
|
+
this._weightedWaiters[weight - 1] = [];
|
|
115
|
+
this._weightedWaiters[weight - 1].push(resolve);
|
|
116
|
+
this._dispatch();
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
isLocked() {
|
|
120
|
+
return this._value <= 0;
|
|
121
|
+
}
|
|
122
|
+
getValue() {
|
|
123
|
+
return this._value;
|
|
124
|
+
}
|
|
125
|
+
setValue(value) {
|
|
126
|
+
this._value = value;
|
|
127
|
+
this._dispatch();
|
|
128
|
+
}
|
|
129
|
+
release(weight = 1) {
|
|
130
|
+
if (weight <= 0)
|
|
131
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
132
|
+
this._value += weight;
|
|
133
|
+
this._dispatch();
|
|
134
|
+
}
|
|
135
|
+
cancel() {
|
|
136
|
+
this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
|
|
137
|
+
this._weightedQueues = [];
|
|
138
|
+
}
|
|
139
|
+
_dispatch() {
|
|
140
|
+
var _a;
|
|
141
|
+
for (let weight = this._value; weight > 0; weight--) {
|
|
142
|
+
const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
|
|
143
|
+
if (!queueEntry)
|
|
144
|
+
continue;
|
|
145
|
+
const previousValue = this._value;
|
|
146
|
+
const previousWeight = weight;
|
|
147
|
+
this._value -= weight;
|
|
148
|
+
weight = this._value + 1;
|
|
149
|
+
queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
|
|
150
|
+
}
|
|
151
|
+
this._drainUnlockWaiters();
|
|
152
|
+
}
|
|
153
|
+
_newReleaser(weight) {
|
|
154
|
+
let called = false;
|
|
155
|
+
return () => {
|
|
156
|
+
if (called)
|
|
157
|
+
return;
|
|
158
|
+
called = true;
|
|
159
|
+
this.release(weight);
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
_drainUnlockWaiters() {
|
|
163
|
+
for (let weight = this._value; weight > 0; weight--) {
|
|
164
|
+
if (!this._weightedWaiters[weight - 1])
|
|
165
|
+
continue;
|
|
166
|
+
this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
|
|
167
|
+
this._weightedWaiters[weight - 1] = [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var __awaiter$1 = function(thisArg, _arguments, P, generator) {
|
|
172
|
+
function adopt(value) {
|
|
173
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
174
|
+
resolve(value);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
178
|
+
function fulfilled(value) {
|
|
179
|
+
try {
|
|
180
|
+
step(generator.next(value));
|
|
181
|
+
} catch (e) {
|
|
182
|
+
reject(e);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function rejected(value) {
|
|
186
|
+
try {
|
|
187
|
+
step(generator["throw"](value));
|
|
188
|
+
} catch (e) {
|
|
189
|
+
reject(e);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function step(result) {
|
|
193
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
194
|
+
}
|
|
195
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
196
|
+
});
|
|
197
|
+
};
|
|
198
|
+
var Mutex = class {
|
|
199
|
+
constructor(cancelError) {
|
|
200
|
+
this._semaphore = new Semaphore(1, cancelError);
|
|
201
|
+
}
|
|
202
|
+
acquire() {
|
|
203
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
204
|
+
const [, releaser] = yield this._semaphore.acquire();
|
|
205
|
+
return releaser;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
runExclusive(callback) {
|
|
209
|
+
return this._semaphore.runExclusive(() => callback());
|
|
210
|
+
}
|
|
211
|
+
isLocked() {
|
|
212
|
+
return this._semaphore.isLocked();
|
|
213
|
+
}
|
|
214
|
+
waitForUnlock() {
|
|
215
|
+
return this._semaphore.waitForUnlock();
|
|
216
|
+
}
|
|
217
|
+
release() {
|
|
218
|
+
if (this._semaphore.isLocked())
|
|
219
|
+
this._semaphore.release();
|
|
220
|
+
}
|
|
221
|
+
cancel() {
|
|
222
|
+
return this._semaphore.cancel();
|
|
223
|
+
}
|
|
224
|
+
};
|
|
52
225
|
|
|
53
226
|
// ../../src/CacheNamespace.ts
|
|
54
|
-
var CacheNamespace = class {
|
|
227
|
+
var CacheNamespace = class _CacheNamespace {
|
|
55
228
|
constructor(cache, prefix) {
|
|
56
229
|
this.cache = cache;
|
|
57
230
|
this.prefix = prefix;
|
|
58
231
|
}
|
|
59
232
|
cache;
|
|
60
233
|
prefix;
|
|
234
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
235
|
+
metrics = emptyMetrics();
|
|
61
236
|
async get(key, fetcher, options) {
|
|
62
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
237
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
63
238
|
}
|
|
64
239
|
async getOrSet(key, fetcher, options) {
|
|
65
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
240
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
244
|
+
*/
|
|
245
|
+
async getOrThrow(key, fetcher, options) {
|
|
246
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
66
247
|
}
|
|
67
248
|
async has(key) {
|
|
68
|
-
return this.cache.has(this.qualify(key));
|
|
249
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
69
250
|
}
|
|
70
251
|
async ttl(key) {
|
|
71
|
-
return this.cache.ttl(this.qualify(key));
|
|
252
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
72
253
|
}
|
|
73
254
|
async set(key, value, options) {
|
|
74
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
255
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
75
256
|
}
|
|
76
257
|
async delete(key) {
|
|
77
|
-
await this.cache.delete(this.qualify(key));
|
|
258
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
78
259
|
}
|
|
79
260
|
async mdelete(keys) {
|
|
80
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
261
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
81
262
|
}
|
|
82
263
|
async clear() {
|
|
83
|
-
await this.cache.
|
|
264
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
84
265
|
}
|
|
85
266
|
async mget(entries) {
|
|
86
|
-
return this.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
267
|
+
return this.trackMetrics(
|
|
268
|
+
() => this.cache.mget(
|
|
269
|
+
entries.map((entry) => ({
|
|
270
|
+
...entry,
|
|
271
|
+
key: this.qualify(entry.key)
|
|
272
|
+
}))
|
|
273
|
+
)
|
|
91
274
|
);
|
|
92
275
|
}
|
|
93
276
|
async mset(entries) {
|
|
94
|
-
await this.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
277
|
+
await this.trackMetrics(
|
|
278
|
+
() => this.cache.mset(
|
|
279
|
+
entries.map((entry) => ({
|
|
280
|
+
...entry,
|
|
281
|
+
key: this.qualify(entry.key)
|
|
282
|
+
}))
|
|
283
|
+
)
|
|
99
284
|
);
|
|
100
285
|
}
|
|
101
286
|
async invalidateByTag(tag) {
|
|
102
|
-
await this.cache.invalidateByTag(tag);
|
|
287
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
288
|
+
}
|
|
289
|
+
async invalidateByTags(tags, mode = "any") {
|
|
290
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
103
291
|
}
|
|
104
292
|
async invalidateByPattern(pattern) {
|
|
105
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
293
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
294
|
+
}
|
|
295
|
+
async invalidateByPrefix(prefix) {
|
|
296
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Returns detailed metadata about a single cache key within this namespace.
|
|
300
|
+
*/
|
|
301
|
+
async inspect(key) {
|
|
302
|
+
return this.cache.inspect(this.qualify(key));
|
|
106
303
|
}
|
|
107
304
|
wrap(keyPrefix, fetcher, options) {
|
|
108
305
|
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
@@ -117,15 +314,159 @@ var CacheNamespace = class {
|
|
|
117
314
|
);
|
|
118
315
|
}
|
|
119
316
|
getMetrics() {
|
|
120
|
-
return this.
|
|
317
|
+
return cloneMetrics(this.metrics);
|
|
121
318
|
}
|
|
122
319
|
getHitRate() {
|
|
123
|
-
|
|
320
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
321
|
+
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
322
|
+
const byLayer = {};
|
|
323
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
324
|
+
for (const layer of layers) {
|
|
325
|
+
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
326
|
+
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
327
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
328
|
+
}
|
|
329
|
+
return { overall, byLayer };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
333
|
+
*
|
|
334
|
+
* ```ts
|
|
335
|
+
* const tenant = cache.namespace('tenant:abc')
|
|
336
|
+
* const posts = tenant.namespace('posts')
|
|
337
|
+
* // keys become: "tenant:abc:posts:mykey"
|
|
338
|
+
* ```
|
|
339
|
+
*/
|
|
340
|
+
namespace(childPrefix) {
|
|
341
|
+
return new _CacheNamespace(this.cache, `${this.prefix}:${childPrefix}`);
|
|
124
342
|
}
|
|
125
343
|
qualify(key) {
|
|
126
344
|
return `${this.prefix}:${key}`;
|
|
127
345
|
}
|
|
346
|
+
async trackMetrics(operation) {
|
|
347
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
348
|
+
const before = this.cache.getMetrics();
|
|
349
|
+
const result = await operation();
|
|
350
|
+
const after = this.cache.getMetrics();
|
|
351
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
352
|
+
return result;
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
getMetricsMutex() {
|
|
356
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
357
|
+
if (existing) {
|
|
358
|
+
return existing;
|
|
359
|
+
}
|
|
360
|
+
const mutex = new Mutex();
|
|
361
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
362
|
+
return mutex;
|
|
363
|
+
}
|
|
128
364
|
};
|
|
365
|
+
function emptyMetrics() {
|
|
366
|
+
return {
|
|
367
|
+
hits: 0,
|
|
368
|
+
misses: 0,
|
|
369
|
+
fetches: 0,
|
|
370
|
+
sets: 0,
|
|
371
|
+
deletes: 0,
|
|
372
|
+
backfills: 0,
|
|
373
|
+
invalidations: 0,
|
|
374
|
+
staleHits: 0,
|
|
375
|
+
refreshes: 0,
|
|
376
|
+
refreshErrors: 0,
|
|
377
|
+
writeFailures: 0,
|
|
378
|
+
singleFlightWaits: 0,
|
|
379
|
+
negativeCacheHits: 0,
|
|
380
|
+
circuitBreakerTrips: 0,
|
|
381
|
+
degradedOperations: 0,
|
|
382
|
+
hitsByLayer: {},
|
|
383
|
+
missesByLayer: {},
|
|
384
|
+
latencyByLayer: {},
|
|
385
|
+
resetAt: Date.now()
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
function cloneMetrics(metrics) {
|
|
389
|
+
return {
|
|
390
|
+
...metrics,
|
|
391
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
392
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
393
|
+
latencyByLayer: Object.fromEntries(
|
|
394
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
395
|
+
)
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
function diffMetrics(before, after) {
|
|
399
|
+
const latencyByLayer = Object.fromEntries(
|
|
400
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
401
|
+
layer,
|
|
402
|
+
{
|
|
403
|
+
avgMs: value.avgMs,
|
|
404
|
+
maxMs: value.maxMs,
|
|
405
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
406
|
+
}
|
|
407
|
+
])
|
|
408
|
+
);
|
|
409
|
+
return {
|
|
410
|
+
hits: after.hits - before.hits,
|
|
411
|
+
misses: after.misses - before.misses,
|
|
412
|
+
fetches: after.fetches - before.fetches,
|
|
413
|
+
sets: after.sets - before.sets,
|
|
414
|
+
deletes: after.deletes - before.deletes,
|
|
415
|
+
backfills: after.backfills - before.backfills,
|
|
416
|
+
invalidations: after.invalidations - before.invalidations,
|
|
417
|
+
staleHits: after.staleHits - before.staleHits,
|
|
418
|
+
refreshes: after.refreshes - before.refreshes,
|
|
419
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
420
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
421
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
422
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
423
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
424
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
425
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
426
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
427
|
+
latencyByLayer,
|
|
428
|
+
resetAt: after.resetAt
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
function addMetrics(base, delta) {
|
|
432
|
+
return {
|
|
433
|
+
hits: base.hits + delta.hits,
|
|
434
|
+
misses: base.misses + delta.misses,
|
|
435
|
+
fetches: base.fetches + delta.fetches,
|
|
436
|
+
sets: base.sets + delta.sets,
|
|
437
|
+
deletes: base.deletes + delta.deletes,
|
|
438
|
+
backfills: base.backfills + delta.backfills,
|
|
439
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
440
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
441
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
442
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
443
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
444
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
445
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
446
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
447
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
448
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
449
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
450
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
451
|
+
resetAt: base.resetAt
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function diffMap(before, after) {
|
|
455
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
456
|
+
const result = {};
|
|
457
|
+
for (const key of keys) {
|
|
458
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
459
|
+
}
|
|
460
|
+
return result;
|
|
461
|
+
}
|
|
462
|
+
function addMap(base, delta) {
|
|
463
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
464
|
+
const result = {};
|
|
465
|
+
for (const key of keys) {
|
|
466
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
467
|
+
}
|
|
468
|
+
return result;
|
|
469
|
+
}
|
|
129
470
|
|
|
130
471
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
131
472
|
var CircuitBreakerManager = class {
|
|
@@ -219,11 +560,105 @@ var CircuitBreakerManager = class {
|
|
|
219
560
|
}
|
|
220
561
|
};
|
|
221
562
|
|
|
563
|
+
// ../../src/internal/FetchRateLimiter.ts
|
|
564
|
+
var FetchRateLimiter = class {
|
|
565
|
+
active = 0;
|
|
566
|
+
queue = [];
|
|
567
|
+
startedAt = [];
|
|
568
|
+
drainTimer;
|
|
569
|
+
async schedule(options, task) {
|
|
570
|
+
if (!options) {
|
|
571
|
+
return task();
|
|
572
|
+
}
|
|
573
|
+
const normalized = this.normalize(options);
|
|
574
|
+
if (!normalized) {
|
|
575
|
+
return task();
|
|
576
|
+
}
|
|
577
|
+
return new Promise((resolve, reject) => {
|
|
578
|
+
this.queue.push({ options: normalized, task, resolve, reject });
|
|
579
|
+
this.drain();
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
normalize(options) {
|
|
583
|
+
const maxConcurrent = options.maxConcurrent;
|
|
584
|
+
const intervalMs = options.intervalMs;
|
|
585
|
+
const maxPerInterval = options.maxPerInterval;
|
|
586
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
587
|
+
return void 0;
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
maxConcurrent,
|
|
591
|
+
intervalMs,
|
|
592
|
+
maxPerInterval
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
drain() {
|
|
596
|
+
if (this.drainTimer) {
|
|
597
|
+
clearTimeout(this.drainTimer);
|
|
598
|
+
this.drainTimer = void 0;
|
|
599
|
+
}
|
|
600
|
+
while (this.queue.length > 0) {
|
|
601
|
+
const next = this.queue[0];
|
|
602
|
+
if (!next) {
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const waitMs = this.waitTime(next.options);
|
|
606
|
+
if (waitMs > 0) {
|
|
607
|
+
this.drainTimer = setTimeout(() => {
|
|
608
|
+
this.drainTimer = void 0;
|
|
609
|
+
this.drain();
|
|
610
|
+
}, waitMs);
|
|
611
|
+
this.drainTimer.unref?.();
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.queue.shift();
|
|
615
|
+
this.active += 1;
|
|
616
|
+
this.startedAt.push(Date.now());
|
|
617
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
618
|
+
this.active -= 1;
|
|
619
|
+
this.drain();
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
waitTime(options) {
|
|
624
|
+
const now = Date.now();
|
|
625
|
+
if (options.maxConcurrent && this.active >= options.maxConcurrent) {
|
|
626
|
+
return 1;
|
|
627
|
+
}
|
|
628
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
629
|
+
return 0;
|
|
630
|
+
}
|
|
631
|
+
this.prune(now, options.intervalMs);
|
|
632
|
+
if (this.startedAt.length < options.maxPerInterval) {
|
|
633
|
+
return 0;
|
|
634
|
+
}
|
|
635
|
+
const oldest = this.startedAt[0];
|
|
636
|
+
if (!oldest) {
|
|
637
|
+
return 0;
|
|
638
|
+
}
|
|
639
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
640
|
+
}
|
|
641
|
+
prune(now, intervalMs) {
|
|
642
|
+
while (this.startedAt.length > 0) {
|
|
643
|
+
const startedAt = this.startedAt[0];
|
|
644
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
this.startedAt.shift();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
|
|
222
652
|
// ../../src/internal/MetricsCollector.ts
|
|
223
653
|
var MetricsCollector = class {
|
|
224
654
|
data = this.empty();
|
|
225
655
|
get snapshot() {
|
|
226
|
-
return {
|
|
656
|
+
return {
|
|
657
|
+
...this.data,
|
|
658
|
+
hitsByLayer: { ...this.data.hitsByLayer },
|
|
659
|
+
missesByLayer: { ...this.data.missesByLayer },
|
|
660
|
+
latencyByLayer: Object.fromEntries(Object.entries(this.data.latencyByLayer).map(([k, v]) => [k, { ...v }]))
|
|
661
|
+
};
|
|
227
662
|
}
|
|
228
663
|
increment(field, amount = 1) {
|
|
229
664
|
;
|
|
@@ -232,6 +667,22 @@ var MetricsCollector = class {
|
|
|
232
667
|
incrementLayer(map, layerName) {
|
|
233
668
|
this.data[map][layerName] = (this.data[map][layerName] ?? 0) + 1;
|
|
234
669
|
}
|
|
670
|
+
/**
|
|
671
|
+
* Records a read latency sample for the given layer.
|
|
672
|
+
* Maintains a rolling average and max using Welford's online algorithm.
|
|
673
|
+
*/
|
|
674
|
+
recordLatency(layerName, durationMs) {
|
|
675
|
+
const existing = this.data.latencyByLayer[layerName];
|
|
676
|
+
if (!existing) {
|
|
677
|
+
this.data.latencyByLayer[layerName] = { avgMs: durationMs, maxMs: durationMs, count: 1 };
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
existing.count += 1;
|
|
681
|
+
existing.avgMs += (durationMs - existing.avgMs) / existing.count;
|
|
682
|
+
if (durationMs > existing.maxMs) {
|
|
683
|
+
existing.maxMs = durationMs;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
235
686
|
reset() {
|
|
236
687
|
this.data = this.empty();
|
|
237
688
|
}
|
|
@@ -266,6 +717,7 @@ var MetricsCollector = class {
|
|
|
266
717
|
degradedOperations: 0,
|
|
267
718
|
hitsByLayer: {},
|
|
268
719
|
missesByLayer: {},
|
|
720
|
+
latencyByLayer: {},
|
|
269
721
|
resetAt: Date.now()
|
|
270
722
|
};
|
|
271
723
|
}
|
|
@@ -393,13 +845,14 @@ var TtlResolver = class {
|
|
|
393
845
|
clearProfiles() {
|
|
394
846
|
this.accessProfiles.clear();
|
|
395
847
|
}
|
|
396
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
848
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
849
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
397
850
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
398
851
|
layerName,
|
|
399
852
|
options?.negativeTtl,
|
|
400
853
|
globalNegativeTtl,
|
|
401
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
402
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
854
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
855
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
403
856
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
404
857
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
405
858
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -438,6 +891,29 @@ var TtlResolver = class {
|
|
|
438
891
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
439
892
|
return Math.max(1, Math.round(ttl + delta));
|
|
440
893
|
}
|
|
894
|
+
resolvePolicyTtl(key, value, policy) {
|
|
895
|
+
if (!policy) {
|
|
896
|
+
return void 0;
|
|
897
|
+
}
|
|
898
|
+
if (typeof policy === "function") {
|
|
899
|
+
return policy({ key, value });
|
|
900
|
+
}
|
|
901
|
+
const now = /* @__PURE__ */ new Date();
|
|
902
|
+
if (policy === "until-midnight") {
|
|
903
|
+
const nextMidnight = new Date(now);
|
|
904
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
905
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
906
|
+
}
|
|
907
|
+
if (policy === "next-hour") {
|
|
908
|
+
const nextHour = new Date(now);
|
|
909
|
+
nextHour.setMinutes(60, 0, 0);
|
|
910
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
911
|
+
}
|
|
912
|
+
const alignToSeconds = policy.alignTo;
|
|
913
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
914
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
915
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
916
|
+
}
|
|
441
917
|
readLayerNumber(layerName, value) {
|
|
442
918
|
if (typeof value === "number") {
|
|
443
919
|
return value;
|
|
@@ -465,269 +941,133 @@ var PatternMatcher = class _PatternMatcher {
|
|
|
465
941
|
/**
|
|
466
942
|
* Tests whether a glob-style pattern matches a value.
|
|
467
943
|
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
468
|
-
* Uses a
|
|
944
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
945
|
+
* quadratic memory usage on long patterns/keys.
|
|
469
946
|
*/
|
|
470
947
|
static matches(pattern, value) {
|
|
471
948
|
return _PatternMatcher.matchLinear(pattern, value);
|
|
472
949
|
}
|
|
473
950
|
/**
|
|
474
|
-
* Linear-time glob matching
|
|
475
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
951
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
476
952
|
*/
|
|
477
953
|
static matchLinear(pattern, value) {
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const pc = pattern[i - 1];
|
|
490
|
-
if (pc === "*") {
|
|
491
|
-
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
492
|
-
} else if (pc === "?" || pc === value[j - 1]) {
|
|
493
|
-
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
return dp[m]?.[n];
|
|
498
|
-
}
|
|
499
|
-
};
|
|
500
|
-
|
|
501
|
-
// ../../src/invalidation/TagIndex.ts
|
|
502
|
-
var TagIndex = class {
|
|
503
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
504
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
505
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
506
|
-
async touch(key) {
|
|
507
|
-
this.knownKeys.add(key);
|
|
508
|
-
}
|
|
509
|
-
async track(key, tags) {
|
|
510
|
-
this.knownKeys.add(key);
|
|
511
|
-
if (tags.length === 0) {
|
|
512
|
-
return;
|
|
513
|
-
}
|
|
514
|
-
const existingTags = this.keyToTags.get(key);
|
|
515
|
-
if (existingTags) {
|
|
516
|
-
for (const tag of existingTags) {
|
|
517
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
const tagSet = new Set(tags);
|
|
521
|
-
this.keyToTags.set(key, tagSet);
|
|
522
|
-
for (const tag of tagSet) {
|
|
523
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
524
|
-
keys.add(key);
|
|
525
|
-
this.tagToKeys.set(tag, keys);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
async remove(key) {
|
|
529
|
-
this.knownKeys.delete(key);
|
|
530
|
-
const tags = this.keyToTags.get(key);
|
|
531
|
-
if (!tags) {
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
for (const tag of tags) {
|
|
535
|
-
const keys = this.tagToKeys.get(tag);
|
|
536
|
-
if (!keys) {
|
|
954
|
+
let patternIndex = 0;
|
|
955
|
+
let valueIndex = 0;
|
|
956
|
+
let starIndex = -1;
|
|
957
|
+
let backtrackValueIndex = 0;
|
|
958
|
+
while (valueIndex < value.length) {
|
|
959
|
+
const patternChar = pattern[patternIndex];
|
|
960
|
+
const valueChar = value[valueIndex];
|
|
961
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
962
|
+
starIndex = patternIndex;
|
|
963
|
+
patternIndex += 1;
|
|
964
|
+
backtrackValueIndex = valueIndex;
|
|
537
965
|
continue;
|
|
538
966
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
this.keyToTags.delete(key);
|
|
545
|
-
}
|
|
546
|
-
async keysForTag(tag) {
|
|
547
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
548
|
-
}
|
|
549
|
-
async matchPattern(pattern) {
|
|
550
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
551
|
-
}
|
|
552
|
-
async clear() {
|
|
553
|
-
this.tagToKeys.clear();
|
|
554
|
-
this.keyToTags.clear();
|
|
555
|
-
this.knownKeys.clear();
|
|
556
|
-
}
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
// ../../node_modules/async-mutex/index.mjs
|
|
560
|
-
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
|
|
561
|
-
var E_ALREADY_LOCKED = new Error("mutex already locked");
|
|
562
|
-
var E_CANCELED = new Error("request for lock canceled");
|
|
563
|
-
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
|
|
564
|
-
function adopt(value) {
|
|
565
|
-
return value instanceof P ? value : new P(function(resolve) {
|
|
566
|
-
resolve(value);
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
return new (P || (P = Promise))(function(resolve, reject) {
|
|
570
|
-
function fulfilled(value) {
|
|
571
|
-
try {
|
|
572
|
-
step(generator.next(value));
|
|
573
|
-
} catch (e) {
|
|
574
|
-
reject(e);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
function rejected(value) {
|
|
578
|
-
try {
|
|
579
|
-
step(generator["throw"](value));
|
|
580
|
-
} catch (e) {
|
|
581
|
-
reject(e);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
function step(result) {
|
|
585
|
-
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
586
|
-
}
|
|
587
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
588
|
-
});
|
|
589
|
-
};
|
|
590
|
-
var Semaphore = class {
|
|
591
|
-
constructor(_value, _cancelError = E_CANCELED) {
|
|
592
|
-
this._value = _value;
|
|
593
|
-
this._cancelError = _cancelError;
|
|
594
|
-
this._weightedQueues = [];
|
|
595
|
-
this._weightedWaiters = [];
|
|
596
|
-
}
|
|
597
|
-
acquire(weight = 1) {
|
|
598
|
-
if (weight <= 0)
|
|
599
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
600
|
-
return new Promise((resolve, reject) => {
|
|
601
|
-
if (!this._weightedQueues[weight - 1])
|
|
602
|
-
this._weightedQueues[weight - 1] = [];
|
|
603
|
-
this._weightedQueues[weight - 1].push({ resolve, reject });
|
|
604
|
-
this._dispatch();
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
runExclusive(callback, weight = 1) {
|
|
608
|
-
return __awaiter$2(this, void 0, void 0, function* () {
|
|
609
|
-
const [value, release] = yield this.acquire(weight);
|
|
610
|
-
try {
|
|
611
|
-
return yield callback(value);
|
|
612
|
-
} finally {
|
|
613
|
-
release();
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
}
|
|
617
|
-
waitForUnlock(weight = 1) {
|
|
618
|
-
if (weight <= 0)
|
|
619
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
620
|
-
return new Promise((resolve) => {
|
|
621
|
-
if (!this._weightedWaiters[weight - 1])
|
|
622
|
-
this._weightedWaiters[weight - 1] = [];
|
|
623
|
-
this._weightedWaiters[weight - 1].push(resolve);
|
|
624
|
-
this._dispatch();
|
|
625
|
-
});
|
|
626
|
-
}
|
|
627
|
-
isLocked() {
|
|
628
|
-
return this._value <= 0;
|
|
629
|
-
}
|
|
630
|
-
getValue() {
|
|
631
|
-
return this._value;
|
|
632
|
-
}
|
|
633
|
-
setValue(value) {
|
|
634
|
-
this._value = value;
|
|
635
|
-
this._dispatch();
|
|
636
|
-
}
|
|
637
|
-
release(weight = 1) {
|
|
638
|
-
if (weight <= 0)
|
|
639
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
640
|
-
this._value += weight;
|
|
641
|
-
this._dispatch();
|
|
642
|
-
}
|
|
643
|
-
cancel() {
|
|
644
|
-
this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
|
|
645
|
-
this._weightedQueues = [];
|
|
646
|
-
}
|
|
647
|
-
_dispatch() {
|
|
648
|
-
var _a;
|
|
649
|
-
for (let weight = this._value; weight > 0; weight--) {
|
|
650
|
-
const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
|
|
651
|
-
if (!queueEntry)
|
|
967
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
968
|
+
patternIndex += 1;
|
|
969
|
+
valueIndex += 1;
|
|
652
970
|
continue;
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
}
|
|
659
|
-
this._drainUnlockWaiters();
|
|
660
|
-
}
|
|
661
|
-
_newReleaser(weight) {
|
|
662
|
-
let called = false;
|
|
663
|
-
return () => {
|
|
664
|
-
if (called)
|
|
665
|
-
return;
|
|
666
|
-
called = true;
|
|
667
|
-
this.release(weight);
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
_drainUnlockWaiters() {
|
|
671
|
-
for (let weight = this._value; weight > 0; weight--) {
|
|
672
|
-
if (!this._weightedWaiters[weight - 1])
|
|
971
|
+
}
|
|
972
|
+
if (starIndex !== -1) {
|
|
973
|
+
patternIndex = starIndex + 1;
|
|
974
|
+
backtrackValueIndex += 1;
|
|
975
|
+
valueIndex = backtrackValueIndex;
|
|
673
976
|
continue;
|
|
674
|
-
|
|
675
|
-
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
676
979
|
}
|
|
980
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
981
|
+
patternIndex += 1;
|
|
982
|
+
}
|
|
983
|
+
return patternIndex === pattern.length;
|
|
677
984
|
}
|
|
678
985
|
};
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
986
|
+
|
|
987
|
+
// ../../src/invalidation/TagIndex.ts
|
|
988
|
+
var TagIndex = class {
|
|
989
|
+
tagToKeys = /* @__PURE__ */ new Map();
|
|
990
|
+
keyToTags = /* @__PURE__ */ new Map();
|
|
991
|
+
knownKeys = /* @__PURE__ */ new Set();
|
|
992
|
+
maxKnownKeys;
|
|
993
|
+
constructor(options = {}) {
|
|
994
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
684
995
|
}
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
996
|
+
async touch(key) {
|
|
997
|
+
this.knownKeys.add(key);
|
|
998
|
+
this.pruneKnownKeysIfNeeded();
|
|
999
|
+
}
|
|
1000
|
+
async track(key, tags) {
|
|
1001
|
+
this.knownKeys.add(key);
|
|
1002
|
+
this.pruneKnownKeysIfNeeded();
|
|
1003
|
+
if (tags.length === 0) {
|
|
1004
|
+
return;
|
|
692
1005
|
}
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
reject(e);
|
|
1006
|
+
const existingTags = this.keyToTags.get(key);
|
|
1007
|
+
if (existingTags) {
|
|
1008
|
+
for (const tag of existingTags) {
|
|
1009
|
+
this.tagToKeys.get(tag)?.delete(key);
|
|
698
1010
|
}
|
|
699
1011
|
}
|
|
700
|
-
|
|
701
|
-
|
|
1012
|
+
const tagSet = new Set(tags);
|
|
1013
|
+
this.keyToTags.set(key, tagSet);
|
|
1014
|
+
for (const tag of tagSet) {
|
|
1015
|
+
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1016
|
+
keys.add(key);
|
|
1017
|
+
this.tagToKeys.set(tag, keys);
|
|
702
1018
|
}
|
|
703
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
704
|
-
});
|
|
705
|
-
};
|
|
706
|
-
var Mutex = class {
|
|
707
|
-
constructor(cancelError) {
|
|
708
|
-
this._semaphore = new Semaphore(1, cancelError);
|
|
709
1019
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const [, releaser] = yield this._semaphore.acquire();
|
|
713
|
-
return releaser;
|
|
714
|
-
});
|
|
1020
|
+
async remove(key) {
|
|
1021
|
+
this.removeKey(key);
|
|
715
1022
|
}
|
|
716
|
-
|
|
717
|
-
return this.
|
|
1023
|
+
async keysForTag(tag) {
|
|
1024
|
+
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
718
1025
|
}
|
|
719
|
-
|
|
720
|
-
return this.
|
|
1026
|
+
async keysForPrefix(prefix) {
|
|
1027
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
721
1028
|
}
|
|
722
|
-
|
|
723
|
-
return this.
|
|
1029
|
+
async tagsForKey(key) {
|
|
1030
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
724
1031
|
}
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
this._semaphore.release();
|
|
1032
|
+
async matchPattern(pattern) {
|
|
1033
|
+
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
728
1034
|
}
|
|
729
|
-
|
|
730
|
-
|
|
1035
|
+
async clear() {
|
|
1036
|
+
this.tagToKeys.clear();
|
|
1037
|
+
this.keyToTags.clear();
|
|
1038
|
+
this.knownKeys.clear();
|
|
1039
|
+
}
|
|
1040
|
+
pruneKnownKeysIfNeeded() {
|
|
1041
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
1045
|
+
let removed = 0;
|
|
1046
|
+
for (const key of this.knownKeys) {
|
|
1047
|
+
if (removed >= toRemove) {
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
this.removeKey(key);
|
|
1051
|
+
removed += 1;
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
removeKey(key) {
|
|
1055
|
+
this.knownKeys.delete(key);
|
|
1056
|
+
const tags = this.keyToTags.get(key);
|
|
1057
|
+
if (!tags) {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
for (const tag of tags) {
|
|
1061
|
+
const keys = this.tagToKeys.get(tag);
|
|
1062
|
+
if (!keys) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
keys.delete(key);
|
|
1066
|
+
if (keys.size === 0) {
|
|
1067
|
+
this.tagToKeys.delete(tag);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
this.keyToTags.delete(key);
|
|
731
1071
|
}
|
|
732
1072
|
};
|
|
733
1073
|
|
|
@@ -756,6 +1096,16 @@ var StampedeGuard = class {
|
|
|
756
1096
|
}
|
|
757
1097
|
};
|
|
758
1098
|
|
|
1099
|
+
// ../../src/types.ts
|
|
1100
|
+
var CacheMissError = class extends Error {
|
|
1101
|
+
key;
|
|
1102
|
+
constructor(key) {
|
|
1103
|
+
super(`Cache miss for key "${key}".`);
|
|
1104
|
+
this.name = "CacheMissError";
|
|
1105
|
+
this.key = key;
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
|
|
759
1109
|
// ../../src/CacheStack.ts
|
|
760
1110
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
761
1111
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
@@ -799,6 +1149,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
799
1149
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
800
1150
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
801
1151
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
1152
|
+
this.currentGeneration = options.generation;
|
|
802
1153
|
if (options.publishSetInvalidation !== void 0) {
|
|
803
1154
|
console.warn(
|
|
804
1155
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -807,21 +1158,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
807
1158
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
808
1159
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
809
1160
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1161
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
810
1162
|
this.startup = this.initialize();
|
|
811
1163
|
}
|
|
812
1164
|
layers;
|
|
813
1165
|
options;
|
|
814
1166
|
stampedeGuard = new StampedeGuard();
|
|
815
1167
|
metricsCollector = new MetricsCollector();
|
|
816
|
-
instanceId =
|
|
1168
|
+
instanceId = createInstanceId();
|
|
817
1169
|
startup;
|
|
818
1170
|
unsubscribeInvalidation;
|
|
819
1171
|
logger;
|
|
820
1172
|
tagIndex;
|
|
1173
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
821
1174
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
822
1175
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
823
1176
|
ttlResolver;
|
|
824
1177
|
circuitBreakerManager;
|
|
1178
|
+
currentGeneration;
|
|
1179
|
+
writeBehindQueue = [];
|
|
1180
|
+
writeBehindTimer;
|
|
1181
|
+
writeBehindFlushPromise;
|
|
825
1182
|
isDisconnecting = false;
|
|
826
1183
|
disconnectPromise;
|
|
827
1184
|
/**
|
|
@@ -831,9 +1188,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
831
1188
|
* and no `fetcher` is provided.
|
|
832
1189
|
*/
|
|
833
1190
|
async get(key, fetcher, options) {
|
|
834
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1191
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
835
1192
|
this.validateWriteOptions(options);
|
|
836
|
-
await this.
|
|
1193
|
+
await this.awaitStartup("get");
|
|
837
1194
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
838
1195
|
if (hit.found) {
|
|
839
1196
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -882,12 +1239,24 @@ var CacheStack = class extends EventEmitter {
|
|
|
882
1239
|
async getOrSet(key, fetcher, options) {
|
|
883
1240
|
return this.get(key, fetcher, options);
|
|
884
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
1244
|
+
* Useful when the value is expected to exist or the fetcher is expected to
|
|
1245
|
+
* return non-null.
|
|
1246
|
+
*/
|
|
1247
|
+
async getOrThrow(key, fetcher, options) {
|
|
1248
|
+
const value = await this.get(key, fetcher, options);
|
|
1249
|
+
if (value === null) {
|
|
1250
|
+
throw new CacheMissError(key);
|
|
1251
|
+
}
|
|
1252
|
+
return value;
|
|
1253
|
+
}
|
|
885
1254
|
/**
|
|
886
1255
|
* Returns true if the given key exists and is not expired in any layer.
|
|
887
1256
|
*/
|
|
888
1257
|
async has(key) {
|
|
889
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
890
|
-
await this.
|
|
1258
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1259
|
+
await this.awaitStartup("has");
|
|
891
1260
|
for (const layer of this.layers) {
|
|
892
1261
|
if (this.shouldSkipLayer(layer)) {
|
|
893
1262
|
continue;
|
|
@@ -917,8 +1286,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
917
1286
|
* that has it, or null if the key is not found / has no TTL.
|
|
918
1287
|
*/
|
|
919
1288
|
async ttl(key) {
|
|
920
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
921
|
-
await this.
|
|
1289
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1290
|
+
await this.awaitStartup("ttl");
|
|
922
1291
|
for (const layer of this.layers) {
|
|
923
1292
|
if (this.shouldSkipLayer(layer)) {
|
|
924
1293
|
continue;
|
|
@@ -939,17 +1308,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
939
1308
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
940
1309
|
*/
|
|
941
1310
|
async set(key, value, options) {
|
|
942
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1311
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
943
1312
|
this.validateWriteOptions(options);
|
|
944
|
-
await this.
|
|
1313
|
+
await this.awaitStartup("set");
|
|
945
1314
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
946
1315
|
}
|
|
947
1316
|
/**
|
|
948
1317
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
949
1318
|
*/
|
|
950
1319
|
async delete(key) {
|
|
951
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
952
|
-
await this.
|
|
1320
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1321
|
+
await this.awaitStartup("delete");
|
|
953
1322
|
await this.deleteKeys([normalizedKey]);
|
|
954
1323
|
await this.publishInvalidation({
|
|
955
1324
|
scope: "key",
|
|
@@ -959,7 +1328,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
959
1328
|
});
|
|
960
1329
|
}
|
|
961
1330
|
async clear() {
|
|
962
|
-
await this.
|
|
1331
|
+
await this.awaitStartup("clear");
|
|
963
1332
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
964
1333
|
await this.tagIndex.clear();
|
|
965
1334
|
this.ttlResolver.clearProfiles();
|
|
@@ -975,23 +1344,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
975
1344
|
if (keys.length === 0) {
|
|
976
1345
|
return;
|
|
977
1346
|
}
|
|
978
|
-
await this.
|
|
1347
|
+
await this.awaitStartup("mdelete");
|
|
979
1348
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
980
|
-
|
|
1349
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1350
|
+
await this.deleteKeys(cacheKeys);
|
|
981
1351
|
await this.publishInvalidation({
|
|
982
1352
|
scope: "keys",
|
|
983
|
-
keys:
|
|
1353
|
+
keys: cacheKeys,
|
|
984
1354
|
sourceId: this.instanceId,
|
|
985
1355
|
operation: "delete"
|
|
986
1356
|
});
|
|
987
1357
|
}
|
|
988
1358
|
async mget(entries) {
|
|
1359
|
+
this.assertActive("mget");
|
|
989
1360
|
if (entries.length === 0) {
|
|
990
1361
|
return [];
|
|
991
1362
|
}
|
|
992
1363
|
const normalizedEntries = entries.map((entry) => ({
|
|
993
1364
|
...entry,
|
|
994
|
-
key: this.validateCacheKey(entry.key)
|
|
1365
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
995
1366
|
}));
|
|
996
1367
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
997
1368
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1017,7 +1388,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1017
1388
|
})
|
|
1018
1389
|
);
|
|
1019
1390
|
}
|
|
1020
|
-
await this.
|
|
1391
|
+
await this.awaitStartup("mget");
|
|
1021
1392
|
const pending = /* @__PURE__ */ new Set();
|
|
1022
1393
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1023
1394
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -1065,14 +1436,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1065
1436
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
1066
1437
|
}
|
|
1067
1438
|
async mset(entries) {
|
|
1439
|
+
this.assertActive("mset");
|
|
1068
1440
|
const normalizedEntries = entries.map((entry) => ({
|
|
1069
1441
|
...entry,
|
|
1070
|
-
key: this.validateCacheKey(entry.key)
|
|
1442
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1071
1443
|
}));
|
|
1072
1444
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1073
|
-
await
|
|
1445
|
+
await this.awaitStartup("mset");
|
|
1446
|
+
await this.writeBatch(normalizedEntries);
|
|
1074
1447
|
}
|
|
1075
1448
|
async warm(entries, options = {}) {
|
|
1449
|
+
this.assertActive("warm");
|
|
1076
1450
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1077
1451
|
const total = entries.length;
|
|
1078
1452
|
let completed = 0;
|
|
@@ -1121,14 +1495,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
1121
1495
|
return new CacheNamespace(this, prefix);
|
|
1122
1496
|
}
|
|
1123
1497
|
async invalidateByTag(tag) {
|
|
1124
|
-
await this.
|
|
1498
|
+
await this.awaitStartup("invalidateByTag");
|
|
1125
1499
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1126
1500
|
await this.deleteKeys(keys);
|
|
1127
1501
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1128
1502
|
}
|
|
1503
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1504
|
+
if (tags.length === 0) {
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
await this.awaitStartup("invalidateByTags");
|
|
1508
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1509
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1510
|
+
await this.deleteKeys(keys);
|
|
1511
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1512
|
+
}
|
|
1129
1513
|
async invalidateByPattern(pattern) {
|
|
1130
|
-
await this.
|
|
1131
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1514
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1515
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1516
|
+
await this.deleteKeys(keys);
|
|
1517
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1518
|
+
}
|
|
1519
|
+
async invalidateByPrefix(prefix) {
|
|
1520
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1521
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1522
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
1132
1523
|
await this.deleteKeys(keys);
|
|
1133
1524
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1134
1525
|
}
|
|
@@ -1155,8 +1546,77 @@ var CacheStack = class extends EventEmitter {
|
|
|
1155
1546
|
getHitRate() {
|
|
1156
1547
|
return this.metricsCollector.hitRate();
|
|
1157
1548
|
}
|
|
1158
|
-
async
|
|
1549
|
+
async healthCheck() {
|
|
1159
1550
|
await this.startup;
|
|
1551
|
+
return Promise.all(
|
|
1552
|
+
this.layers.map(async (layer) => {
|
|
1553
|
+
const startedAt = performance.now();
|
|
1554
|
+
try {
|
|
1555
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1556
|
+
return {
|
|
1557
|
+
layer: layer.name,
|
|
1558
|
+
healthy,
|
|
1559
|
+
latencyMs: performance.now() - startedAt
|
|
1560
|
+
};
|
|
1561
|
+
} catch (error) {
|
|
1562
|
+
return {
|
|
1563
|
+
layer: layer.name,
|
|
1564
|
+
healthy: false,
|
|
1565
|
+
latencyMs: performance.now() - startedAt,
|
|
1566
|
+
error: this.formatError(error)
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
bumpGeneration(nextGeneration) {
|
|
1573
|
+
const current = this.currentGeneration ?? 0;
|
|
1574
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1575
|
+
return this.currentGeneration;
|
|
1576
|
+
}
|
|
1577
|
+
/**
|
|
1578
|
+
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1579
|
+
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1580
|
+
* Returns `null` if the key does not exist in any layer.
|
|
1581
|
+
*/
|
|
1582
|
+
async inspect(key) {
|
|
1583
|
+
const userKey = this.validateCacheKey(key);
|
|
1584
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1585
|
+
await this.awaitStartup("inspect");
|
|
1586
|
+
const foundInLayers = [];
|
|
1587
|
+
let freshTtlSeconds = null;
|
|
1588
|
+
let staleTtlSeconds = null;
|
|
1589
|
+
let errorTtlSeconds = null;
|
|
1590
|
+
let isStale = false;
|
|
1591
|
+
for (const layer of this.layers) {
|
|
1592
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1593
|
+
continue;
|
|
1594
|
+
}
|
|
1595
|
+
const stored = await this.readLayerEntry(layer, normalizedKey);
|
|
1596
|
+
if (stored === null) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
const resolved = resolveStoredValue(stored);
|
|
1600
|
+
if (resolved.state === "expired") {
|
|
1601
|
+
continue;
|
|
1602
|
+
}
|
|
1603
|
+
foundInLayers.push(layer.name);
|
|
1604
|
+
if (foundInLayers.length === 1 && resolved.envelope) {
|
|
1605
|
+
const now = Date.now();
|
|
1606
|
+
freshTtlSeconds = resolved.envelope.freshUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.freshUntil - now) / 1e3)) : null;
|
|
1607
|
+
staleTtlSeconds = resolved.envelope.staleUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.staleUntil - now) / 1e3)) : null;
|
|
1608
|
+
errorTtlSeconds = resolved.envelope.errorUntil !== null ? Math.max(0, Math.ceil((resolved.envelope.errorUntil - now) / 1e3)) : null;
|
|
1609
|
+
isStale = resolved.state === "stale-while-revalidate" || resolved.state === "stale-if-error";
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (foundInLayers.length === 0) {
|
|
1613
|
+
return null;
|
|
1614
|
+
}
|
|
1615
|
+
const tags = await this.getTagsForKey(normalizedKey);
|
|
1616
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1617
|
+
}
|
|
1618
|
+
async exportState() {
|
|
1619
|
+
await this.awaitStartup("exportState");
|
|
1160
1620
|
const exported = /* @__PURE__ */ new Map();
|
|
1161
1621
|
for (const layer of this.layers) {
|
|
1162
1622
|
if (!layer.keys) {
|
|
@@ -1164,15 +1624,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1164
1624
|
}
|
|
1165
1625
|
const keys = await layer.keys();
|
|
1166
1626
|
for (const key of keys) {
|
|
1167
|
-
|
|
1627
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1628
|
+
if (exported.has(exportedKey)) {
|
|
1168
1629
|
continue;
|
|
1169
1630
|
}
|
|
1170
1631
|
const stored = await this.readLayerEntry(layer, key);
|
|
1171
1632
|
if (stored === null) {
|
|
1172
1633
|
continue;
|
|
1173
1634
|
}
|
|
1174
|
-
exported.set(
|
|
1175
|
-
key,
|
|
1635
|
+
exported.set(exportedKey, {
|
|
1636
|
+
key: exportedKey,
|
|
1176
1637
|
value: stored,
|
|
1177
1638
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1178
1639
|
});
|
|
@@ -1181,19 +1642,24 @@ var CacheStack = class extends EventEmitter {
|
|
|
1181
1642
|
return [...exported.values()];
|
|
1182
1643
|
}
|
|
1183
1644
|
async importState(entries) {
|
|
1184
|
-
await this.
|
|
1645
|
+
await this.awaitStartup("importState");
|
|
1185
1646
|
await Promise.all(
|
|
1186
1647
|
entries.map(async (entry) => {
|
|
1187
|
-
|
|
1188
|
-
await this.
|
|
1648
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1649
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1650
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1189
1651
|
})
|
|
1190
1652
|
);
|
|
1191
1653
|
}
|
|
1192
1654
|
async persistToFile(filePath) {
|
|
1655
|
+
this.assertActive("persistToFile");
|
|
1193
1656
|
const snapshot = await this.exportState();
|
|
1657
|
+
const { promises: fs } = await import("fs");
|
|
1194
1658
|
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1195
1659
|
}
|
|
1196
1660
|
async restoreFromFile(filePath) {
|
|
1661
|
+
this.assertActive("restoreFromFile");
|
|
1662
|
+
const { promises: fs } = await import("fs");
|
|
1197
1663
|
const raw = await fs.readFile(filePath, "utf8");
|
|
1198
1664
|
let parsed;
|
|
1199
1665
|
try {
|
|
@@ -1217,7 +1683,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1217
1683
|
this.disconnectPromise = (async () => {
|
|
1218
1684
|
await this.startup;
|
|
1219
1685
|
await this.unsubscribeInvalidation?.();
|
|
1686
|
+
await this.flushWriteBehindQueue();
|
|
1220
1687
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1688
|
+
if (this.writeBehindTimer) {
|
|
1689
|
+
clearInterval(this.writeBehindTimer);
|
|
1690
|
+
this.writeBehindTimer = void 0;
|
|
1691
|
+
}
|
|
1692
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1221
1693
|
})();
|
|
1222
1694
|
}
|
|
1223
1695
|
await this.disconnectPromise;
|
|
@@ -1277,7 +1749,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1277
1749
|
const fetchStart = Date.now();
|
|
1278
1750
|
let fetched;
|
|
1279
1751
|
try {
|
|
1280
|
-
fetched = await
|
|
1752
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1753
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1754
|
+
fetcher
|
|
1755
|
+
);
|
|
1281
1756
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1282
1757
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1283
1758
|
} catch (error) {
|
|
@@ -1291,6 +1766,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1291
1766
|
await this.storeEntry(key, "empty", null, options);
|
|
1292
1767
|
return null;
|
|
1293
1768
|
}
|
|
1769
|
+
if (options?.shouldCache && !options.shouldCache(fetched)) {
|
|
1770
|
+
return fetched;
|
|
1771
|
+
}
|
|
1294
1772
|
await this.storeEntry(key, "value", fetched, options);
|
|
1295
1773
|
return fetched;
|
|
1296
1774
|
}
|
|
@@ -1308,12 +1786,70 @@ var CacheStack = class extends EventEmitter {
|
|
|
1308
1786
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1309
1787
|
}
|
|
1310
1788
|
}
|
|
1789
|
+
async writeBatch(entries) {
|
|
1790
|
+
const now = Date.now();
|
|
1791
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1792
|
+
const immediateOperations = [];
|
|
1793
|
+
const deferredOperations = [];
|
|
1794
|
+
for (const entry of entries) {
|
|
1795
|
+
for (const layer of this.layers) {
|
|
1796
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1797
|
+
continue;
|
|
1798
|
+
}
|
|
1799
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1800
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1801
|
+
bucket.push(layerEntry);
|
|
1802
|
+
entriesByLayer.set(layer, bucket);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1806
|
+
const operation = async () => {
|
|
1807
|
+
try {
|
|
1808
|
+
if (layer.setMany) {
|
|
1809
|
+
await layer.setMany(layerEntries);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1813
|
+
} catch (error) {
|
|
1814
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1818
|
+
deferredOperations.push(operation);
|
|
1819
|
+
} else {
|
|
1820
|
+
immediateOperations.push(operation);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1824
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1825
|
+
for (const entry of entries) {
|
|
1826
|
+
if (entry.options?.tags) {
|
|
1827
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1828
|
+
} else {
|
|
1829
|
+
await this.tagIndex.touch(entry.key);
|
|
1830
|
+
}
|
|
1831
|
+
this.metricsCollector.increment("sets");
|
|
1832
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1833
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1834
|
+
}
|
|
1835
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1836
|
+
await this.publishInvalidation({
|
|
1837
|
+
scope: "keys",
|
|
1838
|
+
keys: entries.map((entry) => entry.key),
|
|
1839
|
+
sourceId: this.instanceId,
|
|
1840
|
+
operation: "write"
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1311
1844
|
async readFromLayers(key, options, mode) {
|
|
1312
1845
|
let sawRetainableValue = false;
|
|
1313
1846
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
1314
1847
|
const layer = this.layers[index];
|
|
1315
1848
|
if (!layer) continue;
|
|
1849
|
+
const readStart = performance.now();
|
|
1316
1850
|
const stored = await this.readLayerEntry(layer, key);
|
|
1851
|
+
const readDuration = performance.now() - readStart;
|
|
1852
|
+
this.metricsCollector.recordLatency(layer.name, readDuration);
|
|
1317
1853
|
if (stored === null) {
|
|
1318
1854
|
this.metricsCollector.incrementLayer("missesByLayer", layer.name);
|
|
1319
1855
|
continue;
|
|
@@ -1388,33 +1924,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1388
1924
|
}
|
|
1389
1925
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1390
1926
|
const now = Date.now();
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
options
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
});
|
|
1410
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1411
|
-
try {
|
|
1412
|
-
await layer.set(key, payload, ttl);
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1927
|
+
const immediateOperations = [];
|
|
1928
|
+
const deferredOperations = [];
|
|
1929
|
+
for (const layer of this.layers) {
|
|
1930
|
+
const operation = async () => {
|
|
1931
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1935
|
+
try {
|
|
1936
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1939
|
+
}
|
|
1940
|
+
};
|
|
1941
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1942
|
+
deferredOperations.push(operation);
|
|
1943
|
+
} else {
|
|
1944
|
+
immediateOperations.push(operation);
|
|
1415
1945
|
}
|
|
1416
|
-
}
|
|
1417
|
-
await this.executeLayerOperations(
|
|
1946
|
+
}
|
|
1947
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1948
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1418
1949
|
}
|
|
1419
1950
|
async executeLayerOperations(operations, context) {
|
|
1420
1951
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1438,8 +1969,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1438
1969
|
);
|
|
1439
1970
|
}
|
|
1440
1971
|
}
|
|
1441
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1442
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
1972
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
1973
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
1974
|
+
key,
|
|
1975
|
+
layerName,
|
|
1976
|
+
kind,
|
|
1977
|
+
options,
|
|
1978
|
+
fallbackTtl,
|
|
1979
|
+
this.options.negativeTtl,
|
|
1980
|
+
void 0,
|
|
1981
|
+
value
|
|
1982
|
+
);
|
|
1443
1983
|
}
|
|
1444
1984
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1445
1985
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1515,6 +2055,12 @@ var CacheStack = class extends EventEmitter {
|
|
|
1515
2055
|
}
|
|
1516
2056
|
}
|
|
1517
2057
|
}
|
|
2058
|
+
async getTagsForKey(key) {
|
|
2059
|
+
if (this.tagIndex.tagsForKey) {
|
|
2060
|
+
return this.tagIndex.tagsForKey(key);
|
|
2061
|
+
}
|
|
2062
|
+
return [];
|
|
2063
|
+
}
|
|
1518
2064
|
formatError(error) {
|
|
1519
2065
|
if (error instanceof Error) {
|
|
1520
2066
|
return error.message;
|
|
@@ -1527,6 +2073,105 @@ var CacheStack = class extends EventEmitter {
|
|
|
1527
2073
|
shouldBroadcastL1Invalidation() {
|
|
1528
2074
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1529
2075
|
}
|
|
2076
|
+
initializeWriteBehind(options) {
|
|
2077
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
2081
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
2082
|
+
return;
|
|
2083
|
+
}
|
|
2084
|
+
this.writeBehindTimer = setInterval(() => {
|
|
2085
|
+
void this.flushWriteBehindQueue();
|
|
2086
|
+
}, flushIntervalMs);
|
|
2087
|
+
this.writeBehindTimer.unref?.();
|
|
2088
|
+
}
|
|
2089
|
+
shouldWriteBehind(layer) {
|
|
2090
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2091
|
+
}
|
|
2092
|
+
async enqueueWriteBehind(operation) {
|
|
2093
|
+
this.writeBehindQueue.push(operation);
|
|
2094
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2095
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2096
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
2097
|
+
await this.flushWriteBehindQueue();
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2101
|
+
await this.flushWriteBehindQueue();
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
async flushWriteBehindQueue() {
|
|
2105
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
2106
|
+
await this.writeBehindFlushPromise;
|
|
2107
|
+
return;
|
|
2108
|
+
}
|
|
2109
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2110
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2111
|
+
this.writeBehindFlushPromise = (async () => {
|
|
2112
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2113
|
+
})();
|
|
2114
|
+
await this.writeBehindFlushPromise;
|
|
2115
|
+
this.writeBehindFlushPromise = void 0;
|
|
2116
|
+
if (this.writeBehindQueue.length > 0) {
|
|
2117
|
+
await this.flushWriteBehindQueue();
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2121
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2122
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2123
|
+
layer.name,
|
|
2124
|
+
options?.staleWhileRevalidate,
|
|
2125
|
+
this.options.staleWhileRevalidate
|
|
2126
|
+
);
|
|
2127
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2128
|
+
const payload = createStoredValueEnvelope({
|
|
2129
|
+
kind,
|
|
2130
|
+
value,
|
|
2131
|
+
freshTtlSeconds: freshTtl,
|
|
2132
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2133
|
+
staleIfErrorSeconds: staleIfError,
|
|
2134
|
+
now
|
|
2135
|
+
});
|
|
2136
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
2137
|
+
return {
|
|
2138
|
+
key,
|
|
2139
|
+
value: payload,
|
|
2140
|
+
ttl
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
intersectKeys(groups) {
|
|
2144
|
+
if (groups.length === 0) {
|
|
2145
|
+
return [];
|
|
2146
|
+
}
|
|
2147
|
+
const [firstGroup, ...rest] = groups;
|
|
2148
|
+
if (!firstGroup) {
|
|
2149
|
+
return [];
|
|
2150
|
+
}
|
|
2151
|
+
const restSets = rest.map((group) => new Set(group));
|
|
2152
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2153
|
+
}
|
|
2154
|
+
qualifyKey(key) {
|
|
2155
|
+
const prefix = this.generationPrefix();
|
|
2156
|
+
return prefix ? `${prefix}${key}` : key;
|
|
2157
|
+
}
|
|
2158
|
+
qualifyPattern(pattern) {
|
|
2159
|
+
const prefix = this.generationPrefix();
|
|
2160
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2161
|
+
}
|
|
2162
|
+
stripQualifiedKey(key) {
|
|
2163
|
+
const prefix = this.generationPrefix();
|
|
2164
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2165
|
+
return key;
|
|
2166
|
+
}
|
|
2167
|
+
return key.slice(prefix.length);
|
|
2168
|
+
}
|
|
2169
|
+
generationPrefix() {
|
|
2170
|
+
if (this.currentGeneration === void 0) {
|
|
2171
|
+
return "";
|
|
2172
|
+
}
|
|
2173
|
+
return `v${this.currentGeneration}:`;
|
|
2174
|
+
}
|
|
1530
2175
|
async deleteKeysFromLayers(layers, keys) {
|
|
1531
2176
|
await Promise.all(
|
|
1532
2177
|
layers.map(async (layer) => {
|
|
@@ -1570,6 +2215,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1570
2215
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1571
2216
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1572
2217
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2218
|
+
if (this.options.generation !== void 0) {
|
|
2219
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2220
|
+
}
|
|
1573
2221
|
}
|
|
1574
2222
|
validateWriteOptions(options) {
|
|
1575
2223
|
if (!options) {
|
|
@@ -1581,6 +2229,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1581
2229
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1582
2230
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1583
2231
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2232
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1584
2233
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1585
2234
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1586
2235
|
}
|
|
@@ -1624,6 +2273,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1624
2273
|
}
|
|
1625
2274
|
return key;
|
|
1626
2275
|
}
|
|
2276
|
+
validateTtlPolicy(name, policy) {
|
|
2277
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if ("alignTo" in policy) {
|
|
2281
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
throw new Error(`${name} is invalid.`);
|
|
2285
|
+
}
|
|
2286
|
+
assertActive(operation) {
|
|
2287
|
+
if (this.isDisconnecting) {
|
|
2288
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
async awaitStartup(operation) {
|
|
2292
|
+
this.assertActive(operation);
|
|
2293
|
+
await this.startup;
|
|
2294
|
+
this.assertActive(operation);
|
|
2295
|
+
}
|
|
1627
2296
|
serializeOptions(options) {
|
|
1628
2297
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1629
2298
|
}
|
|
@@ -1729,6 +2398,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1729
2398
|
return value;
|
|
1730
2399
|
}
|
|
1731
2400
|
};
|
|
2401
|
+
function createInstanceId() {
|
|
2402
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2403
|
+
}
|
|
1732
2404
|
|
|
1733
2405
|
// src/module.ts
|
|
1734
2406
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|
|
@@ -1745,6 +2417,22 @@ var CacheStackModule = class {
|
|
|
1745
2417
|
exports: [provider]
|
|
1746
2418
|
};
|
|
1747
2419
|
}
|
|
2420
|
+
static forRootAsync(options) {
|
|
2421
|
+
const provider = {
|
|
2422
|
+
provide: CACHE_STACK,
|
|
2423
|
+
inject: options.inject ?? [],
|
|
2424
|
+
useFactory: async (...args) => {
|
|
2425
|
+
const resolved = await options.useFactory(...args);
|
|
2426
|
+
return new CacheStack(resolved.layers, resolved.bridgeOptions);
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
return {
|
|
2430
|
+
global: true,
|
|
2431
|
+
module: CacheStackModule,
|
|
2432
|
+
providers: [provider],
|
|
2433
|
+
exports: [provider]
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
1748
2436
|
};
|
|
1749
2437
|
CacheStackModule = __decorateClass([
|
|
1750
2438
|
Global(),
|