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
|
@@ -46,9 +46,182 @@ 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
227
|
var CacheNamespace = class _CacheNamespace {
|
|
@@ -58,57 +231,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
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));
|
|
66
241
|
}
|
|
67
242
|
/**
|
|
68
243
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
69
244
|
*/
|
|
70
245
|
async getOrThrow(key, fetcher, options) {
|
|
71
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
246
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
72
247
|
}
|
|
73
248
|
async has(key) {
|
|
74
|
-
return this.cache.has(this.qualify(key));
|
|
249
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
75
250
|
}
|
|
76
251
|
async ttl(key) {
|
|
77
|
-
return this.cache.ttl(this.qualify(key));
|
|
252
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
78
253
|
}
|
|
79
254
|
async set(key, value, options) {
|
|
80
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
255
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
81
256
|
}
|
|
82
257
|
async delete(key) {
|
|
83
|
-
await this.cache.delete(this.qualify(key));
|
|
258
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
84
259
|
}
|
|
85
260
|
async mdelete(keys) {
|
|
86
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
261
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
87
262
|
}
|
|
88
263
|
async clear() {
|
|
89
|
-
await this.cache.
|
|
264
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
90
265
|
}
|
|
91
266
|
async mget(entries) {
|
|
92
|
-
return this.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
267
|
+
return this.trackMetrics(
|
|
268
|
+
() => this.cache.mget(
|
|
269
|
+
entries.map((entry) => ({
|
|
270
|
+
...entry,
|
|
271
|
+
key: this.qualify(entry.key)
|
|
272
|
+
}))
|
|
273
|
+
)
|
|
97
274
|
);
|
|
98
275
|
}
|
|
99
276
|
async mset(entries) {
|
|
100
|
-
await this.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
277
|
+
await this.trackMetrics(
|
|
278
|
+
() => this.cache.mset(
|
|
279
|
+
entries.map((entry) => ({
|
|
280
|
+
...entry,
|
|
281
|
+
key: this.qualify(entry.key)
|
|
282
|
+
}))
|
|
283
|
+
)
|
|
105
284
|
);
|
|
106
285
|
}
|
|
107
286
|
async invalidateByTag(tag) {
|
|
108
|
-
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));
|
|
109
291
|
}
|
|
110
292
|
async invalidateByPattern(pattern) {
|
|
111
|
-
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)));
|
|
112
297
|
}
|
|
113
298
|
/**
|
|
114
299
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -129,10 +314,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
129
314
|
);
|
|
130
315
|
}
|
|
131
316
|
getMetrics() {
|
|
132
|
-
return this.
|
|
317
|
+
return cloneMetrics(this.metrics);
|
|
133
318
|
}
|
|
134
319
|
getHitRate() {
|
|
135
|
-
|
|
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 };
|
|
136
330
|
}
|
|
137
331
|
/**
|
|
138
332
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -149,7 +343,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
149
343
|
qualify(key) {
|
|
150
344
|
return `${this.prefix}:${key}`;
|
|
151
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
|
+
}
|
|
152
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
|
+
}
|
|
153
470
|
|
|
154
471
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
155
472
|
var CircuitBreakerManager = class {
|
|
@@ -243,6 +560,95 @@ var CircuitBreakerManager = class {
|
|
|
243
560
|
}
|
|
244
561
|
};
|
|
245
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
|
+
|
|
246
652
|
// ../../src/internal/MetricsCollector.ts
|
|
247
653
|
var MetricsCollector = class {
|
|
248
654
|
data = this.empty();
|
|
@@ -439,13 +845,14 @@ var TtlResolver = class {
|
|
|
439
845
|
clearProfiles() {
|
|
440
846
|
this.accessProfiles.clear();
|
|
441
847
|
}
|
|
442
|
-
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;
|
|
443
850
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
444
851
|
layerName,
|
|
445
852
|
options?.negativeTtl,
|
|
446
853
|
globalNegativeTtl,
|
|
447
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
448
|
-
) : 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);
|
|
449
856
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
450
857
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
451
858
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -484,6 +891,29 @@ var TtlResolver = class {
|
|
|
484
891
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
485
892
|
return Math.max(1, Math.round(ttl + delta));
|
|
486
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
|
+
}
|
|
487
917
|
readLayerNumber(layerName, value) {
|
|
488
918
|
if (typeof value === "number") {
|
|
489
919
|
return value;
|
|
@@ -498,306 +928,146 @@ var TtlResolver = class {
|
|
|
498
928
|
let removed = 0;
|
|
499
929
|
for (const key of this.accessProfiles.keys()) {
|
|
500
930
|
if (removed >= toRemove) {
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
this.accessProfiles.delete(key);
|
|
504
|
-
removed += 1;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
|
|
509
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
510
|
-
var PatternMatcher = class _PatternMatcher {
|
|
511
|
-
/**
|
|
512
|
-
* Tests whether a glob-style pattern matches a value.
|
|
513
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
514
|
-
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
515
|
-
*/
|
|
516
|
-
static matches(pattern, value) {
|
|
517
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Linear-time glob matching using dynamic programming.
|
|
521
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
522
|
-
*/
|
|
523
|
-
static matchLinear(pattern, value) {
|
|
524
|
-
const m = pattern.length;
|
|
525
|
-
const n = value.length;
|
|
526
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
527
|
-
dp[0][0] = true;
|
|
528
|
-
for (let i = 1; i <= m; i++) {
|
|
529
|
-
if (pattern[i - 1] === "*") {
|
|
530
|
-
dp[i][0] = dp[i - 1]?.[0];
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
for (let i = 1; i <= m; i++) {
|
|
534
|
-
for (let j = 1; j <= n; j++) {
|
|
535
|
-
const pc = pattern[i - 1];
|
|
536
|
-
if (pc === "*") {
|
|
537
|
-
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
538
|
-
} else if (pc === "?" || pc === value[j - 1]) {
|
|
539
|
-
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
return dp[m]?.[n];
|
|
544
|
-
}
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
// ../../src/invalidation/TagIndex.ts
|
|
548
|
-
var TagIndex = class {
|
|
549
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
550
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
551
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
552
|
-
maxKnownKeys;
|
|
553
|
-
constructor(options = {}) {
|
|
554
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
555
|
-
}
|
|
556
|
-
async touch(key) {
|
|
557
|
-
this.knownKeys.add(key);
|
|
558
|
-
this.pruneKnownKeysIfNeeded();
|
|
559
|
-
}
|
|
560
|
-
async track(key, tags) {
|
|
561
|
-
this.knownKeys.add(key);
|
|
562
|
-
this.pruneKnownKeysIfNeeded();
|
|
563
|
-
if (tags.length === 0) {
|
|
564
|
-
return;
|
|
565
|
-
}
|
|
566
|
-
const existingTags = this.keyToTags.get(key);
|
|
567
|
-
if (existingTags) {
|
|
568
|
-
for (const tag of existingTags) {
|
|
569
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
const tagSet = new Set(tags);
|
|
573
|
-
this.keyToTags.set(key, tagSet);
|
|
574
|
-
for (const tag of tagSet) {
|
|
575
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
576
|
-
keys.add(key);
|
|
577
|
-
this.tagToKeys.set(tag, keys);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
async remove(key) {
|
|
581
|
-
this.knownKeys.delete(key);
|
|
582
|
-
const tags = this.keyToTags.get(key);
|
|
583
|
-
if (!tags) {
|
|
584
|
-
return;
|
|
585
|
-
}
|
|
586
|
-
for (const tag of tags) {
|
|
587
|
-
const keys = this.tagToKeys.get(tag);
|
|
588
|
-
if (!keys) {
|
|
589
|
-
continue;
|
|
590
|
-
}
|
|
591
|
-
keys.delete(key);
|
|
592
|
-
if (keys.size === 0) {
|
|
593
|
-
this.tagToKeys.delete(tag);
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
this.keyToTags.delete(key);
|
|
597
|
-
}
|
|
598
|
-
async keysForTag(tag) {
|
|
599
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
600
|
-
}
|
|
601
|
-
async tagsForKey(key) {
|
|
602
|
-
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
603
|
-
}
|
|
604
|
-
async matchPattern(pattern) {
|
|
605
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
606
|
-
}
|
|
607
|
-
async clear() {
|
|
608
|
-
this.tagToKeys.clear();
|
|
609
|
-
this.keyToTags.clear();
|
|
610
|
-
this.knownKeys.clear();
|
|
611
|
-
}
|
|
612
|
-
pruneKnownKeysIfNeeded() {
|
|
613
|
-
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
617
|
-
let removed = 0;
|
|
618
|
-
for (const key of this.knownKeys) {
|
|
619
|
-
if (removed >= toRemove) {
|
|
620
|
-
break;
|
|
621
|
-
}
|
|
622
|
-
this.knownKeys.delete(key);
|
|
623
|
-
this.keyToTags.delete(key);
|
|
624
|
-
removed += 1;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
// ../../node_modules/async-mutex/index.mjs
|
|
630
|
-
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
|
|
631
|
-
var E_ALREADY_LOCKED = new Error("mutex already locked");
|
|
632
|
-
var E_CANCELED = new Error("request for lock canceled");
|
|
633
|
-
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
|
|
634
|
-
function adopt(value) {
|
|
635
|
-
return value instanceof P ? value : new P(function(resolve) {
|
|
636
|
-
resolve(value);
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
return new (P || (P = Promise))(function(resolve, reject) {
|
|
640
|
-
function fulfilled(value) {
|
|
641
|
-
try {
|
|
642
|
-
step(generator.next(value));
|
|
643
|
-
} catch (e) {
|
|
644
|
-
reject(e);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
function rejected(value) {
|
|
648
|
-
try {
|
|
649
|
-
step(generator["throw"](value));
|
|
650
|
-
} catch (e) {
|
|
651
|
-
reject(e);
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
function step(result) {
|
|
655
|
-
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
656
|
-
}
|
|
657
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
658
|
-
});
|
|
659
|
-
};
|
|
660
|
-
var Semaphore = class {
|
|
661
|
-
constructor(_value, _cancelError = E_CANCELED) {
|
|
662
|
-
this._value = _value;
|
|
663
|
-
this._cancelError = _cancelError;
|
|
664
|
-
this._weightedQueues = [];
|
|
665
|
-
this._weightedWaiters = [];
|
|
666
|
-
}
|
|
667
|
-
acquire(weight = 1) {
|
|
668
|
-
if (weight <= 0)
|
|
669
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
670
|
-
return new Promise((resolve, reject) => {
|
|
671
|
-
if (!this._weightedQueues[weight - 1])
|
|
672
|
-
this._weightedQueues[weight - 1] = [];
|
|
673
|
-
this._weightedQueues[weight - 1].push({ resolve, reject });
|
|
674
|
-
this._dispatch();
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
runExclusive(callback, weight = 1) {
|
|
678
|
-
return __awaiter$2(this, void 0, void 0, function* () {
|
|
679
|
-
const [value, release] = yield this.acquire(weight);
|
|
680
|
-
try {
|
|
681
|
-
return yield callback(value);
|
|
682
|
-
} finally {
|
|
683
|
-
release();
|
|
684
|
-
}
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
waitForUnlock(weight = 1) {
|
|
688
|
-
if (weight <= 0)
|
|
689
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
690
|
-
return new Promise((resolve) => {
|
|
691
|
-
if (!this._weightedWaiters[weight - 1])
|
|
692
|
-
this._weightedWaiters[weight - 1] = [];
|
|
693
|
-
this._weightedWaiters[weight - 1].push(resolve);
|
|
694
|
-
this._dispatch();
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
isLocked() {
|
|
698
|
-
return this._value <= 0;
|
|
699
|
-
}
|
|
700
|
-
getValue() {
|
|
701
|
-
return this._value;
|
|
702
|
-
}
|
|
703
|
-
setValue(value) {
|
|
704
|
-
this._value = value;
|
|
705
|
-
this._dispatch();
|
|
706
|
-
}
|
|
707
|
-
release(weight = 1) {
|
|
708
|
-
if (weight <= 0)
|
|
709
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
710
|
-
this._value += weight;
|
|
711
|
-
this._dispatch();
|
|
712
|
-
}
|
|
713
|
-
cancel() {
|
|
714
|
-
this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
|
|
715
|
-
this._weightedQueues = [];
|
|
716
|
-
}
|
|
717
|
-
_dispatch() {
|
|
718
|
-
var _a;
|
|
719
|
-
for (let weight = this._value; weight > 0; weight--) {
|
|
720
|
-
const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
|
|
721
|
-
if (!queueEntry)
|
|
722
|
-
continue;
|
|
723
|
-
const previousValue = this._value;
|
|
724
|
-
const previousWeight = weight;
|
|
725
|
-
this._value -= weight;
|
|
726
|
-
weight = this._value + 1;
|
|
727
|
-
queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
|
|
728
|
-
}
|
|
729
|
-
this._drainUnlockWaiters();
|
|
730
|
-
}
|
|
731
|
-
_newReleaser(weight) {
|
|
732
|
-
let called = false;
|
|
733
|
-
return () => {
|
|
734
|
-
if (called)
|
|
735
|
-
return;
|
|
736
|
-
called = true;
|
|
737
|
-
this.release(weight);
|
|
738
|
-
};
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
this.accessProfiles.delete(key);
|
|
934
|
+
removed += 1;
|
|
935
|
+
}
|
|
739
936
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
940
|
+
var PatternMatcher = class _PatternMatcher {
|
|
941
|
+
/**
|
|
942
|
+
* Tests whether a glob-style pattern matches a value.
|
|
943
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
944
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
945
|
+
* quadratic memory usage on long patterns/keys.
|
|
946
|
+
*/
|
|
947
|
+
static matches(pattern, value) {
|
|
948
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
952
|
+
*/
|
|
953
|
+
static matchLinear(pattern, value) {
|
|
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;
|
|
743
965
|
continue;
|
|
744
|
-
|
|
745
|
-
|
|
966
|
+
}
|
|
967
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
968
|
+
patternIndex += 1;
|
|
969
|
+
valueIndex += 1;
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
if (starIndex !== -1) {
|
|
973
|
+
patternIndex = starIndex + 1;
|
|
974
|
+
backtrackValueIndex += 1;
|
|
975
|
+
valueIndex = backtrackValueIndex;
|
|
976
|
+
continue;
|
|
977
|
+
}
|
|
978
|
+
return false;
|
|
746
979
|
}
|
|
980
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
981
|
+
patternIndex += 1;
|
|
982
|
+
}
|
|
983
|
+
return patternIndex === pattern.length;
|
|
747
984
|
}
|
|
748
985
|
};
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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;
|
|
754
995
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
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;
|
|
762
1005
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
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);
|
|
768
1010
|
}
|
|
769
1011
|
}
|
|
770
|
-
|
|
771
|
-
|
|
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);
|
|
772
1018
|
}
|
|
773
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
774
|
-
});
|
|
775
|
-
};
|
|
776
|
-
var Mutex = class {
|
|
777
|
-
constructor(cancelError) {
|
|
778
|
-
this._semaphore = new Semaphore(1, cancelError);
|
|
779
1019
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const [, releaser] = yield this._semaphore.acquire();
|
|
783
|
-
return releaser;
|
|
784
|
-
});
|
|
1020
|
+
async remove(key) {
|
|
1021
|
+
this.removeKey(key);
|
|
785
1022
|
}
|
|
786
|
-
|
|
787
|
-
return this.
|
|
1023
|
+
async keysForTag(tag) {
|
|
1024
|
+
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
788
1025
|
}
|
|
789
|
-
|
|
790
|
-
return this.
|
|
1026
|
+
async keysForPrefix(prefix) {
|
|
1027
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
791
1028
|
}
|
|
792
|
-
|
|
793
|
-
return this.
|
|
1029
|
+
async tagsForKey(key) {
|
|
1030
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
794
1031
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
this._semaphore.release();
|
|
1032
|
+
async matchPattern(pattern) {
|
|
1033
|
+
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
798
1034
|
}
|
|
799
|
-
|
|
800
|
-
|
|
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);
|
|
801
1071
|
}
|
|
802
1072
|
};
|
|
803
1073
|
|
|
@@ -879,6 +1149,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
879
1149
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
880
1150
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
881
1151
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
1152
|
+
this.currentGeneration = options.generation;
|
|
882
1153
|
if (options.publishSetInvalidation !== void 0) {
|
|
883
1154
|
console.warn(
|
|
884
1155
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -887,21 +1158,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
887
1158
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
888
1159
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
889
1160
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1161
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
890
1162
|
this.startup = this.initialize();
|
|
891
1163
|
}
|
|
892
1164
|
layers;
|
|
893
1165
|
options;
|
|
894
1166
|
stampedeGuard = new StampedeGuard();
|
|
895
1167
|
metricsCollector = new MetricsCollector();
|
|
896
|
-
instanceId =
|
|
1168
|
+
instanceId = createInstanceId();
|
|
897
1169
|
startup;
|
|
898
1170
|
unsubscribeInvalidation;
|
|
899
1171
|
logger;
|
|
900
1172
|
tagIndex;
|
|
1173
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
901
1174
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
902
1175
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
903
1176
|
ttlResolver;
|
|
904
1177
|
circuitBreakerManager;
|
|
1178
|
+
currentGeneration;
|
|
1179
|
+
writeBehindQueue = [];
|
|
1180
|
+
writeBehindTimer;
|
|
1181
|
+
writeBehindFlushPromise;
|
|
905
1182
|
isDisconnecting = false;
|
|
906
1183
|
disconnectPromise;
|
|
907
1184
|
/**
|
|
@@ -911,9 +1188,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
911
1188
|
* and no `fetcher` is provided.
|
|
912
1189
|
*/
|
|
913
1190
|
async get(key, fetcher, options) {
|
|
914
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1191
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
915
1192
|
this.validateWriteOptions(options);
|
|
916
|
-
await this.
|
|
1193
|
+
await this.awaitStartup("get");
|
|
917
1194
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
918
1195
|
if (hit.found) {
|
|
919
1196
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -978,8 +1255,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
978
1255
|
* Returns true if the given key exists and is not expired in any layer.
|
|
979
1256
|
*/
|
|
980
1257
|
async has(key) {
|
|
981
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
982
|
-
await this.
|
|
1258
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1259
|
+
await this.awaitStartup("has");
|
|
983
1260
|
for (const layer of this.layers) {
|
|
984
1261
|
if (this.shouldSkipLayer(layer)) {
|
|
985
1262
|
continue;
|
|
@@ -1009,8 +1286,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1009
1286
|
* that has it, or null if the key is not found / has no TTL.
|
|
1010
1287
|
*/
|
|
1011
1288
|
async ttl(key) {
|
|
1012
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1013
|
-
await this.
|
|
1289
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1290
|
+
await this.awaitStartup("ttl");
|
|
1014
1291
|
for (const layer of this.layers) {
|
|
1015
1292
|
if (this.shouldSkipLayer(layer)) {
|
|
1016
1293
|
continue;
|
|
@@ -1031,17 +1308,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1031
1308
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1032
1309
|
*/
|
|
1033
1310
|
async set(key, value, options) {
|
|
1034
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1311
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1035
1312
|
this.validateWriteOptions(options);
|
|
1036
|
-
await this.
|
|
1313
|
+
await this.awaitStartup("set");
|
|
1037
1314
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
1038
1315
|
}
|
|
1039
1316
|
/**
|
|
1040
1317
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1041
1318
|
*/
|
|
1042
1319
|
async delete(key) {
|
|
1043
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1044
|
-
await this.
|
|
1320
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1321
|
+
await this.awaitStartup("delete");
|
|
1045
1322
|
await this.deleteKeys([normalizedKey]);
|
|
1046
1323
|
await this.publishInvalidation({
|
|
1047
1324
|
scope: "key",
|
|
@@ -1051,7 +1328,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1051
1328
|
});
|
|
1052
1329
|
}
|
|
1053
1330
|
async clear() {
|
|
1054
|
-
await this.
|
|
1331
|
+
await this.awaitStartup("clear");
|
|
1055
1332
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1056
1333
|
await this.tagIndex.clear();
|
|
1057
1334
|
this.ttlResolver.clearProfiles();
|
|
@@ -1067,23 +1344,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
1067
1344
|
if (keys.length === 0) {
|
|
1068
1345
|
return;
|
|
1069
1346
|
}
|
|
1070
|
-
await this.
|
|
1347
|
+
await this.awaitStartup("mdelete");
|
|
1071
1348
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1072
|
-
|
|
1349
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1350
|
+
await this.deleteKeys(cacheKeys);
|
|
1073
1351
|
await this.publishInvalidation({
|
|
1074
1352
|
scope: "keys",
|
|
1075
|
-
keys:
|
|
1353
|
+
keys: cacheKeys,
|
|
1076
1354
|
sourceId: this.instanceId,
|
|
1077
1355
|
operation: "delete"
|
|
1078
1356
|
});
|
|
1079
1357
|
}
|
|
1080
1358
|
async mget(entries) {
|
|
1359
|
+
this.assertActive("mget");
|
|
1081
1360
|
if (entries.length === 0) {
|
|
1082
1361
|
return [];
|
|
1083
1362
|
}
|
|
1084
1363
|
const normalizedEntries = entries.map((entry) => ({
|
|
1085
1364
|
...entry,
|
|
1086
|
-
key: this.validateCacheKey(entry.key)
|
|
1365
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1087
1366
|
}));
|
|
1088
1367
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1089
1368
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1109,7 +1388,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1109
1388
|
})
|
|
1110
1389
|
);
|
|
1111
1390
|
}
|
|
1112
|
-
await this.
|
|
1391
|
+
await this.awaitStartup("mget");
|
|
1113
1392
|
const pending = /* @__PURE__ */ new Set();
|
|
1114
1393
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1115
1394
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -1157,14 +1436,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1157
1436
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
1158
1437
|
}
|
|
1159
1438
|
async mset(entries) {
|
|
1439
|
+
this.assertActive("mset");
|
|
1160
1440
|
const normalizedEntries = entries.map((entry) => ({
|
|
1161
1441
|
...entry,
|
|
1162
|
-
key: this.validateCacheKey(entry.key)
|
|
1442
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1163
1443
|
}));
|
|
1164
1444
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1165
|
-
await
|
|
1445
|
+
await this.awaitStartup("mset");
|
|
1446
|
+
await this.writeBatch(normalizedEntries);
|
|
1166
1447
|
}
|
|
1167
1448
|
async warm(entries, options = {}) {
|
|
1449
|
+
this.assertActive("warm");
|
|
1168
1450
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1169
1451
|
const total = entries.length;
|
|
1170
1452
|
let completed = 0;
|
|
@@ -1213,14 +1495,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
1213
1495
|
return new CacheNamespace(this, prefix);
|
|
1214
1496
|
}
|
|
1215
1497
|
async invalidateByTag(tag) {
|
|
1216
|
-
await this.
|
|
1498
|
+
await this.awaitStartup("invalidateByTag");
|
|
1217
1499
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1218
1500
|
await this.deleteKeys(keys);
|
|
1219
1501
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1220
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
|
+
}
|
|
1221
1513
|
async invalidateByPattern(pattern) {
|
|
1222
|
-
await this.
|
|
1223
|
-
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}*`);
|
|
1224
1523
|
await this.deleteKeys(keys);
|
|
1225
1524
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1226
1525
|
}
|
|
@@ -1247,14 +1546,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
1247
1546
|
getHitRate() {
|
|
1248
1547
|
return this.metricsCollector.hitRate();
|
|
1249
1548
|
}
|
|
1549
|
+
async healthCheck() {
|
|
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
|
+
}
|
|
1250
1577
|
/**
|
|
1251
1578
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1252
1579
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1253
1580
|
* Returns `null` if the key does not exist in any layer.
|
|
1254
1581
|
*/
|
|
1255
1582
|
async inspect(key) {
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1583
|
+
const userKey = this.validateCacheKey(key);
|
|
1584
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1585
|
+
await this.awaitStartup("inspect");
|
|
1258
1586
|
const foundInLayers = [];
|
|
1259
1587
|
let freshTtlSeconds = null;
|
|
1260
1588
|
let staleTtlSeconds = null;
|
|
@@ -1285,10 +1613,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1285
1613
|
return null;
|
|
1286
1614
|
}
|
|
1287
1615
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1288
|
-
return { key:
|
|
1616
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1289
1617
|
}
|
|
1290
1618
|
async exportState() {
|
|
1291
|
-
await this.
|
|
1619
|
+
await this.awaitStartup("exportState");
|
|
1292
1620
|
const exported = /* @__PURE__ */ new Map();
|
|
1293
1621
|
for (const layer of this.layers) {
|
|
1294
1622
|
if (!layer.keys) {
|
|
@@ -1296,15 +1624,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1296
1624
|
}
|
|
1297
1625
|
const keys = await layer.keys();
|
|
1298
1626
|
for (const key of keys) {
|
|
1299
|
-
|
|
1627
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1628
|
+
if (exported.has(exportedKey)) {
|
|
1300
1629
|
continue;
|
|
1301
1630
|
}
|
|
1302
1631
|
const stored = await this.readLayerEntry(layer, key);
|
|
1303
1632
|
if (stored === null) {
|
|
1304
1633
|
continue;
|
|
1305
1634
|
}
|
|
1306
|
-
exported.set(
|
|
1307
|
-
key,
|
|
1635
|
+
exported.set(exportedKey, {
|
|
1636
|
+
key: exportedKey,
|
|
1308
1637
|
value: stored,
|
|
1309
1638
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1310
1639
|
});
|
|
@@ -1313,19 +1642,24 @@ var CacheStack = class extends EventEmitter {
|
|
|
1313
1642
|
return [...exported.values()];
|
|
1314
1643
|
}
|
|
1315
1644
|
async importState(entries) {
|
|
1316
|
-
await this.
|
|
1645
|
+
await this.awaitStartup("importState");
|
|
1317
1646
|
await Promise.all(
|
|
1318
1647
|
entries.map(async (entry) => {
|
|
1319
|
-
|
|
1320
|
-
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);
|
|
1321
1651
|
})
|
|
1322
1652
|
);
|
|
1323
1653
|
}
|
|
1324
1654
|
async persistToFile(filePath) {
|
|
1655
|
+
this.assertActive("persistToFile");
|
|
1325
1656
|
const snapshot = await this.exportState();
|
|
1657
|
+
const { promises: fs } = await import("fs");
|
|
1326
1658
|
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1327
1659
|
}
|
|
1328
1660
|
async restoreFromFile(filePath) {
|
|
1661
|
+
this.assertActive("restoreFromFile");
|
|
1662
|
+
const { promises: fs } = await import("fs");
|
|
1329
1663
|
const raw = await fs.readFile(filePath, "utf8");
|
|
1330
1664
|
let parsed;
|
|
1331
1665
|
try {
|
|
@@ -1349,7 +1683,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1349
1683
|
this.disconnectPromise = (async () => {
|
|
1350
1684
|
await this.startup;
|
|
1351
1685
|
await this.unsubscribeInvalidation?.();
|
|
1686
|
+
await this.flushWriteBehindQueue();
|
|
1352
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()));
|
|
1353
1693
|
})();
|
|
1354
1694
|
}
|
|
1355
1695
|
await this.disconnectPromise;
|
|
@@ -1409,7 +1749,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1409
1749
|
const fetchStart = Date.now();
|
|
1410
1750
|
let fetched;
|
|
1411
1751
|
try {
|
|
1412
|
-
fetched = await
|
|
1752
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1753
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1754
|
+
fetcher
|
|
1755
|
+
);
|
|
1413
1756
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1414
1757
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1415
1758
|
} catch (error) {
|
|
@@ -1443,6 +1786,61 @@ var CacheStack = class extends EventEmitter {
|
|
|
1443
1786
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1444
1787
|
}
|
|
1445
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
|
+
}
|
|
1446
1844
|
async readFromLayers(key, options, mode) {
|
|
1447
1845
|
let sawRetainableValue = false;
|
|
1448
1846
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1526,33 +1924,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1526
1924
|
}
|
|
1527
1925
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1528
1926
|
const now = Date.now();
|
|
1529
|
-
const
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
options
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
});
|
|
1548
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1549
|
-
try {
|
|
1550
|
-
await layer.set(key, payload, ttl);
|
|
1551
|
-
} catch (error) {
|
|
1552
|
-
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);
|
|
1553
1945
|
}
|
|
1554
|
-
}
|
|
1555
|
-
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)));
|
|
1556
1949
|
}
|
|
1557
1950
|
async executeLayerOperations(operations, context) {
|
|
1558
1951
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1576,8 +1969,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1576
1969
|
);
|
|
1577
1970
|
}
|
|
1578
1971
|
}
|
|
1579
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1580
|
-
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
|
+
);
|
|
1581
1983
|
}
|
|
1582
1984
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1583
1985
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1671,6 +2073,105 @@ var CacheStack = class extends EventEmitter {
|
|
|
1671
2073
|
shouldBroadcastL1Invalidation() {
|
|
1672
2074
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1673
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
|
+
}
|
|
1674
2175
|
async deleteKeysFromLayers(layers, keys) {
|
|
1675
2176
|
await Promise.all(
|
|
1676
2177
|
layers.map(async (layer) => {
|
|
@@ -1714,6 +2215,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1714
2215
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1715
2216
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1716
2217
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2218
|
+
if (this.options.generation !== void 0) {
|
|
2219
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2220
|
+
}
|
|
1717
2221
|
}
|
|
1718
2222
|
validateWriteOptions(options) {
|
|
1719
2223
|
if (!options) {
|
|
@@ -1725,6 +2229,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1725
2229
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1726
2230
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1727
2231
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2232
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1728
2233
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1729
2234
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1730
2235
|
}
|
|
@@ -1768,6 +2273,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1768
2273
|
}
|
|
1769
2274
|
return key;
|
|
1770
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
|
+
}
|
|
1771
2296
|
serializeOptions(options) {
|
|
1772
2297
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1773
2298
|
}
|
|
@@ -1873,6 +2398,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1873
2398
|
return value;
|
|
1874
2399
|
}
|
|
1875
2400
|
};
|
|
2401
|
+
function createInstanceId() {
|
|
2402
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2403
|
+
}
|
|
1876
2404
|
|
|
1877
2405
|
// src/module.ts
|
|
1878
2406
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|