layercache 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -8
- package/dist/chunk-46UH7LNM.js +312 -0
- package/dist/{chunk-BWM4MU2X.js → chunk-IXCMHVHP.js} +62 -56
- package/dist/chunk-ZMDB5KOK.js +159 -0
- package/dist/cli.cjs +170 -39
- package/dist/cli.js +57 -2
- package/dist/edge-DLpdQN0W.d.cts +672 -0
- package/dist/edge-DLpdQN0W.d.ts +672 -0
- package/dist/edge.cjs +399 -0
- package/dist/edge.d.cts +2 -0
- package/dist/edge.d.ts +2 -0
- package/dist/edge.js +14 -0
- package/dist/index.cjs +1173 -221
- package/dist/index.d.cts +51 -568
- package/dist/index.d.ts +51 -568
- package/dist/index.js +1005 -505
- package/package.json +8 -3
- package/packages/nestjs/dist/index.cjs +980 -370
- package/packages/nestjs/dist/index.d.cts +80 -0
- package/packages/nestjs/dist/index.d.ts +80 -0
- package/packages/nestjs/dist/index.js +968 -368
|
@@ -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,148 @@ var CircuitBreakerManager = class {
|
|
|
243
560
|
}
|
|
244
561
|
};
|
|
245
562
|
|
|
563
|
+
// ../../src/internal/FetchRateLimiter.ts
|
|
564
|
+
var FetchRateLimiter = class {
|
|
565
|
+
queue = [];
|
|
566
|
+
buckets = /* @__PURE__ */ new Map();
|
|
567
|
+
fetcherBuckets = /* @__PURE__ */ new WeakMap();
|
|
568
|
+
nextFetcherBucketId = 0;
|
|
569
|
+
drainTimer;
|
|
570
|
+
async schedule(options, context, task) {
|
|
571
|
+
if (!options) {
|
|
572
|
+
return task();
|
|
573
|
+
}
|
|
574
|
+
const normalized = this.normalize(options);
|
|
575
|
+
if (!normalized) {
|
|
576
|
+
return task();
|
|
577
|
+
}
|
|
578
|
+
return new Promise((resolve, reject) => {
|
|
579
|
+
this.queue.push({
|
|
580
|
+
bucketKey: this.resolveBucketKey(normalized, context),
|
|
581
|
+
options: normalized,
|
|
582
|
+
task,
|
|
583
|
+
resolve,
|
|
584
|
+
reject
|
|
585
|
+
});
|
|
586
|
+
this.drain();
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
normalize(options) {
|
|
590
|
+
const maxConcurrent = options.maxConcurrent;
|
|
591
|
+
const intervalMs = options.intervalMs;
|
|
592
|
+
const maxPerInterval = options.maxPerInterval;
|
|
593
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
594
|
+
return void 0;
|
|
595
|
+
}
|
|
596
|
+
return {
|
|
597
|
+
maxConcurrent,
|
|
598
|
+
intervalMs,
|
|
599
|
+
maxPerInterval,
|
|
600
|
+
scope: options.scope ?? "global",
|
|
601
|
+
bucketKey: options.bucketKey
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
resolveBucketKey(options, context) {
|
|
605
|
+
if (options.bucketKey) {
|
|
606
|
+
return `custom:${options.bucketKey}`;
|
|
607
|
+
}
|
|
608
|
+
if (options.scope === "key") {
|
|
609
|
+
return `key:${context.key}`;
|
|
610
|
+
}
|
|
611
|
+
if (options.scope === "fetcher") {
|
|
612
|
+
const existing = this.fetcherBuckets.get(context.fetcher);
|
|
613
|
+
if (existing) {
|
|
614
|
+
return existing;
|
|
615
|
+
}
|
|
616
|
+
const bucket = `fetcher:${this.nextFetcherBucketId}`;
|
|
617
|
+
this.nextFetcherBucketId += 1;
|
|
618
|
+
this.fetcherBuckets.set(context.fetcher, bucket);
|
|
619
|
+
return bucket;
|
|
620
|
+
}
|
|
621
|
+
return "global";
|
|
622
|
+
}
|
|
623
|
+
drain() {
|
|
624
|
+
if (this.drainTimer) {
|
|
625
|
+
clearTimeout(this.drainTimer);
|
|
626
|
+
this.drainTimer = void 0;
|
|
627
|
+
}
|
|
628
|
+
while (this.queue.length > 0) {
|
|
629
|
+
let nextIndex = -1;
|
|
630
|
+
let nextWaitMs = Number.POSITIVE_INFINITY;
|
|
631
|
+
for (let index = 0; index < this.queue.length; index += 1) {
|
|
632
|
+
const next2 = this.queue[index];
|
|
633
|
+
if (!next2) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const waitMs = this.waitTime(next2.bucketKey, next2.options);
|
|
637
|
+
if (waitMs <= 0) {
|
|
638
|
+
nextIndex = index;
|
|
639
|
+
break;
|
|
640
|
+
}
|
|
641
|
+
nextWaitMs = Math.min(nextWaitMs, waitMs);
|
|
642
|
+
}
|
|
643
|
+
if (nextIndex < 0) {
|
|
644
|
+
if (Number.isFinite(nextWaitMs)) {
|
|
645
|
+
this.drainTimer = setTimeout(() => {
|
|
646
|
+
this.drainTimer = void 0;
|
|
647
|
+
this.drain();
|
|
648
|
+
}, nextWaitMs);
|
|
649
|
+
this.drainTimer.unref?.();
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const next = this.queue.splice(nextIndex, 1)[0];
|
|
654
|
+
if (!next) {
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const bucket = this.bucketState(next.bucketKey);
|
|
658
|
+
bucket.active += 1;
|
|
659
|
+
bucket.startedAt.push(Date.now());
|
|
660
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
661
|
+
bucket.active -= 1;
|
|
662
|
+
this.drain();
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
waitTime(bucketKey, options) {
|
|
667
|
+
const bucket = this.bucketState(bucketKey);
|
|
668
|
+
const now = Date.now();
|
|
669
|
+
if (options.maxConcurrent && bucket.active >= options.maxConcurrent) {
|
|
670
|
+
return 1;
|
|
671
|
+
}
|
|
672
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
this.prune(bucket, now, options.intervalMs);
|
|
676
|
+
if (bucket.startedAt.length < options.maxPerInterval) {
|
|
677
|
+
return 0;
|
|
678
|
+
}
|
|
679
|
+
const oldest = bucket.startedAt[0];
|
|
680
|
+
if (!oldest) {
|
|
681
|
+
return 0;
|
|
682
|
+
}
|
|
683
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
684
|
+
}
|
|
685
|
+
prune(bucket, now, intervalMs) {
|
|
686
|
+
while (bucket.startedAt.length > 0) {
|
|
687
|
+
const startedAt = bucket.startedAt[0];
|
|
688
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
bucket.startedAt.shift();
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
bucketState(bucketKey) {
|
|
695
|
+
const existing = this.buckets.get(bucketKey);
|
|
696
|
+
if (existing) {
|
|
697
|
+
return existing;
|
|
698
|
+
}
|
|
699
|
+
const bucket = { active: 0, startedAt: [] };
|
|
700
|
+
this.buckets.set(bucketKey, bucket);
|
|
701
|
+
return bucket;
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
|
|
246
705
|
// ../../src/internal/MetricsCollector.ts
|
|
247
706
|
var MetricsCollector = class {
|
|
248
707
|
data = this.empty();
|
|
@@ -439,13 +898,14 @@ var TtlResolver = class {
|
|
|
439
898
|
clearProfiles() {
|
|
440
899
|
this.accessProfiles.clear();
|
|
441
900
|
}
|
|
442
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
901
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
902
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
443
903
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
444
904
|
layerName,
|
|
445
905
|
options?.negativeTtl,
|
|
446
906
|
globalNegativeTtl,
|
|
447
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
448
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
907
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
908
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
449
909
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
450
910
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
451
911
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -484,6 +944,29 @@ var TtlResolver = class {
|
|
|
484
944
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
485
945
|
return Math.max(1, Math.round(ttl + delta));
|
|
486
946
|
}
|
|
947
|
+
resolvePolicyTtl(key, value, policy) {
|
|
948
|
+
if (!policy) {
|
|
949
|
+
return void 0;
|
|
950
|
+
}
|
|
951
|
+
if (typeof policy === "function") {
|
|
952
|
+
return policy({ key, value });
|
|
953
|
+
}
|
|
954
|
+
const now = /* @__PURE__ */ new Date();
|
|
955
|
+
if (policy === "until-midnight") {
|
|
956
|
+
const nextMidnight = new Date(now);
|
|
957
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
958
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
959
|
+
}
|
|
960
|
+
if (policy === "next-hour") {
|
|
961
|
+
const nextHour = new Date(now);
|
|
962
|
+
nextHour.setMinutes(60, 0, 0);
|
|
963
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
964
|
+
}
|
|
965
|
+
const alignToSeconds = policy.alignTo;
|
|
966
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
967
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
968
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
969
|
+
}
|
|
487
970
|
readLayerNumber(layerName, value) {
|
|
488
971
|
if (typeof value === "number") {
|
|
489
972
|
return value;
|
|
@@ -504,300 +987,140 @@ var TtlResolver = class {
|
|
|
504
987
|
removed += 1;
|
|
505
988
|
}
|
|
506
989
|
}
|
|
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
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
};
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
993
|
+
var PatternMatcher = class _PatternMatcher {
|
|
994
|
+
/**
|
|
995
|
+
* Tests whether a glob-style pattern matches a value.
|
|
996
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
997
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
998
|
+
* quadratic memory usage on long patterns/keys.
|
|
999
|
+
*/
|
|
1000
|
+
static matches(pattern, value) {
|
|
1001
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
739
1002
|
}
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
1005
|
+
*/
|
|
1006
|
+
static matchLinear(pattern, value) {
|
|
1007
|
+
let patternIndex = 0;
|
|
1008
|
+
let valueIndex = 0;
|
|
1009
|
+
let starIndex = -1;
|
|
1010
|
+
let backtrackValueIndex = 0;
|
|
1011
|
+
while (valueIndex < value.length) {
|
|
1012
|
+
const patternChar = pattern[patternIndex];
|
|
1013
|
+
const valueChar = value[valueIndex];
|
|
1014
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
1015
|
+
starIndex = patternIndex;
|
|
1016
|
+
patternIndex += 1;
|
|
1017
|
+
backtrackValueIndex = valueIndex;
|
|
743
1018
|
continue;
|
|
744
|
-
|
|
745
|
-
|
|
1019
|
+
}
|
|
1020
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
1021
|
+
patternIndex += 1;
|
|
1022
|
+
valueIndex += 1;
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (starIndex !== -1) {
|
|
1026
|
+
patternIndex = starIndex + 1;
|
|
1027
|
+
backtrackValueIndex += 1;
|
|
1028
|
+
valueIndex = backtrackValueIndex;
|
|
1029
|
+
continue;
|
|
1030
|
+
}
|
|
1031
|
+
return false;
|
|
746
1032
|
}
|
|
1033
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1034
|
+
patternIndex += 1;
|
|
1035
|
+
}
|
|
1036
|
+
return patternIndex === pattern.length;
|
|
747
1037
|
}
|
|
748
1038
|
};
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1039
|
+
|
|
1040
|
+
// ../../src/invalidation/TagIndex.ts
|
|
1041
|
+
var TagIndex = class {
|
|
1042
|
+
tagToKeys = /* @__PURE__ */ new Map();
|
|
1043
|
+
keyToTags = /* @__PURE__ */ new Map();
|
|
1044
|
+
knownKeys = /* @__PURE__ */ new Set();
|
|
1045
|
+
maxKnownKeys;
|
|
1046
|
+
constructor(options = {}) {
|
|
1047
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
754
1048
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
1049
|
+
async touch(key) {
|
|
1050
|
+
this.knownKeys.add(key);
|
|
1051
|
+
this.pruneKnownKeysIfNeeded();
|
|
1052
|
+
}
|
|
1053
|
+
async track(key, tags) {
|
|
1054
|
+
this.knownKeys.add(key);
|
|
1055
|
+
this.pruneKnownKeysIfNeeded();
|
|
1056
|
+
if (tags.length === 0) {
|
|
1057
|
+
return;
|
|
762
1058
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
reject(e);
|
|
1059
|
+
const existingTags = this.keyToTags.get(key);
|
|
1060
|
+
if (existingTags) {
|
|
1061
|
+
for (const tag of existingTags) {
|
|
1062
|
+
this.tagToKeys.get(tag)?.delete(key);
|
|
768
1063
|
}
|
|
769
1064
|
}
|
|
770
|
-
|
|
771
|
-
|
|
1065
|
+
const tagSet = new Set(tags);
|
|
1066
|
+
this.keyToTags.set(key, tagSet);
|
|
1067
|
+
for (const tag of tagSet) {
|
|
1068
|
+
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1069
|
+
keys.add(key);
|
|
1070
|
+
this.tagToKeys.set(tag, keys);
|
|
772
1071
|
}
|
|
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
1072
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const [, releaser] = yield this._semaphore.acquire();
|
|
783
|
-
return releaser;
|
|
784
|
-
});
|
|
1073
|
+
async remove(key) {
|
|
1074
|
+
this.removeKey(key);
|
|
785
1075
|
}
|
|
786
|
-
|
|
787
|
-
return this.
|
|
1076
|
+
async keysForTag(tag) {
|
|
1077
|
+
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
788
1078
|
}
|
|
789
|
-
|
|
790
|
-
return this.
|
|
1079
|
+
async keysForPrefix(prefix) {
|
|
1080
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
791
1081
|
}
|
|
792
|
-
|
|
793
|
-
return this.
|
|
1082
|
+
async tagsForKey(key) {
|
|
1083
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
794
1084
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
this._semaphore.release();
|
|
1085
|
+
async matchPattern(pattern) {
|
|
1086
|
+
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
798
1087
|
}
|
|
799
|
-
|
|
800
|
-
|
|
1088
|
+
async clear() {
|
|
1089
|
+
this.tagToKeys.clear();
|
|
1090
|
+
this.keyToTags.clear();
|
|
1091
|
+
this.knownKeys.clear();
|
|
1092
|
+
}
|
|
1093
|
+
pruneKnownKeysIfNeeded() {
|
|
1094
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
1098
|
+
let removed = 0;
|
|
1099
|
+
for (const key of this.knownKeys) {
|
|
1100
|
+
if (removed >= toRemove) {
|
|
1101
|
+
break;
|
|
1102
|
+
}
|
|
1103
|
+
this.removeKey(key);
|
|
1104
|
+
removed += 1;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
removeKey(key) {
|
|
1108
|
+
this.knownKeys.delete(key);
|
|
1109
|
+
const tags = this.keyToTags.get(key);
|
|
1110
|
+
if (!tags) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
for (const tag of tags) {
|
|
1114
|
+
const keys = this.tagToKeys.get(tag);
|
|
1115
|
+
if (!keys) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
keys.delete(key);
|
|
1119
|
+
if (keys.size === 0) {
|
|
1120
|
+
this.tagToKeys.delete(tag);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
this.keyToTags.delete(key);
|
|
801
1124
|
}
|
|
802
1125
|
};
|
|
803
1126
|
|
|
@@ -879,6 +1202,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
879
1202
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
880
1203
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
881
1204
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
1205
|
+
this.currentGeneration = options.generation;
|
|
882
1206
|
if (options.publishSetInvalidation !== void 0) {
|
|
883
1207
|
console.warn(
|
|
884
1208
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -887,21 +1211,27 @@ var CacheStack = class extends EventEmitter {
|
|
|
887
1211
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
888
1212
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
889
1213
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1214
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
890
1215
|
this.startup = this.initialize();
|
|
891
1216
|
}
|
|
892
1217
|
layers;
|
|
893
1218
|
options;
|
|
894
1219
|
stampedeGuard = new StampedeGuard();
|
|
895
1220
|
metricsCollector = new MetricsCollector();
|
|
896
|
-
instanceId =
|
|
1221
|
+
instanceId = createInstanceId();
|
|
897
1222
|
startup;
|
|
898
1223
|
unsubscribeInvalidation;
|
|
899
1224
|
logger;
|
|
900
1225
|
tagIndex;
|
|
1226
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
901
1227
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
902
1228
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
903
1229
|
ttlResolver;
|
|
904
1230
|
circuitBreakerManager;
|
|
1231
|
+
currentGeneration;
|
|
1232
|
+
writeBehindQueue = [];
|
|
1233
|
+
writeBehindTimer;
|
|
1234
|
+
writeBehindFlushPromise;
|
|
905
1235
|
isDisconnecting = false;
|
|
906
1236
|
disconnectPromise;
|
|
907
1237
|
/**
|
|
@@ -911,9 +1241,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
911
1241
|
* and no `fetcher` is provided.
|
|
912
1242
|
*/
|
|
913
1243
|
async get(key, fetcher, options) {
|
|
914
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1244
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
915
1245
|
this.validateWriteOptions(options);
|
|
916
|
-
await this.
|
|
1246
|
+
await this.awaitStartup("get");
|
|
917
1247
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
918
1248
|
if (hit.found) {
|
|
919
1249
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -978,8 +1308,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
978
1308
|
* Returns true if the given key exists and is not expired in any layer.
|
|
979
1309
|
*/
|
|
980
1310
|
async has(key) {
|
|
981
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
982
|
-
await this.
|
|
1311
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1312
|
+
await this.awaitStartup("has");
|
|
983
1313
|
for (const layer of this.layers) {
|
|
984
1314
|
if (this.shouldSkipLayer(layer)) {
|
|
985
1315
|
continue;
|
|
@@ -1009,8 +1339,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1009
1339
|
* that has it, or null if the key is not found / has no TTL.
|
|
1010
1340
|
*/
|
|
1011
1341
|
async ttl(key) {
|
|
1012
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1013
|
-
await this.
|
|
1342
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1343
|
+
await this.awaitStartup("ttl");
|
|
1014
1344
|
for (const layer of this.layers) {
|
|
1015
1345
|
if (this.shouldSkipLayer(layer)) {
|
|
1016
1346
|
continue;
|
|
@@ -1031,17 +1361,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1031
1361
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1032
1362
|
*/
|
|
1033
1363
|
async set(key, value, options) {
|
|
1034
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1364
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1035
1365
|
this.validateWriteOptions(options);
|
|
1036
|
-
await this.
|
|
1366
|
+
await this.awaitStartup("set");
|
|
1037
1367
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
1038
1368
|
}
|
|
1039
1369
|
/**
|
|
1040
1370
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1041
1371
|
*/
|
|
1042
1372
|
async delete(key) {
|
|
1043
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1044
|
-
await this.
|
|
1373
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1374
|
+
await this.awaitStartup("delete");
|
|
1045
1375
|
await this.deleteKeys([normalizedKey]);
|
|
1046
1376
|
await this.publishInvalidation({
|
|
1047
1377
|
scope: "key",
|
|
@@ -1051,7 +1381,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1051
1381
|
});
|
|
1052
1382
|
}
|
|
1053
1383
|
async clear() {
|
|
1054
|
-
await this.
|
|
1384
|
+
await this.awaitStartup("clear");
|
|
1055
1385
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1056
1386
|
await this.tagIndex.clear();
|
|
1057
1387
|
this.ttlResolver.clearProfiles();
|
|
@@ -1067,23 +1397,25 @@ var CacheStack = class extends EventEmitter {
|
|
|
1067
1397
|
if (keys.length === 0) {
|
|
1068
1398
|
return;
|
|
1069
1399
|
}
|
|
1070
|
-
await this.
|
|
1400
|
+
await this.awaitStartup("mdelete");
|
|
1071
1401
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1072
|
-
|
|
1402
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1403
|
+
await this.deleteKeys(cacheKeys);
|
|
1073
1404
|
await this.publishInvalidation({
|
|
1074
1405
|
scope: "keys",
|
|
1075
|
-
keys:
|
|
1406
|
+
keys: cacheKeys,
|
|
1076
1407
|
sourceId: this.instanceId,
|
|
1077
1408
|
operation: "delete"
|
|
1078
1409
|
});
|
|
1079
1410
|
}
|
|
1080
1411
|
async mget(entries) {
|
|
1412
|
+
this.assertActive("mget");
|
|
1081
1413
|
if (entries.length === 0) {
|
|
1082
1414
|
return [];
|
|
1083
1415
|
}
|
|
1084
1416
|
const normalizedEntries = entries.map((entry) => ({
|
|
1085
1417
|
...entry,
|
|
1086
|
-
key: this.validateCacheKey(entry.key)
|
|
1418
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1087
1419
|
}));
|
|
1088
1420
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1089
1421
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1109,7 +1441,7 @@ var CacheStack = class extends EventEmitter {
|
|
|
1109
1441
|
})
|
|
1110
1442
|
);
|
|
1111
1443
|
}
|
|
1112
|
-
await this.
|
|
1444
|
+
await this.awaitStartup("mget");
|
|
1113
1445
|
const pending = /* @__PURE__ */ new Set();
|
|
1114
1446
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1115
1447
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -1157,14 +1489,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1157
1489
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
1158
1490
|
}
|
|
1159
1491
|
async mset(entries) {
|
|
1492
|
+
this.assertActive("mset");
|
|
1160
1493
|
const normalizedEntries = entries.map((entry) => ({
|
|
1161
1494
|
...entry,
|
|
1162
|
-
key: this.validateCacheKey(entry.key)
|
|
1495
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1163
1496
|
}));
|
|
1164
1497
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1165
|
-
await
|
|
1498
|
+
await this.awaitStartup("mset");
|
|
1499
|
+
await this.writeBatch(normalizedEntries);
|
|
1166
1500
|
}
|
|
1167
1501
|
async warm(entries, options = {}) {
|
|
1502
|
+
this.assertActive("warm");
|
|
1168
1503
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1169
1504
|
const total = entries.length;
|
|
1170
1505
|
let completed = 0;
|
|
@@ -1213,14 +1548,31 @@ var CacheStack = class extends EventEmitter {
|
|
|
1213
1548
|
return new CacheNamespace(this, prefix);
|
|
1214
1549
|
}
|
|
1215
1550
|
async invalidateByTag(tag) {
|
|
1216
|
-
await this.
|
|
1551
|
+
await this.awaitStartup("invalidateByTag");
|
|
1217
1552
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1218
1553
|
await this.deleteKeys(keys);
|
|
1219
1554
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1220
1555
|
}
|
|
1556
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1557
|
+
if (tags.length === 0) {
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
await this.awaitStartup("invalidateByTags");
|
|
1561
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1562
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1563
|
+
await this.deleteKeys(keys);
|
|
1564
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1565
|
+
}
|
|
1221
1566
|
async invalidateByPattern(pattern) {
|
|
1222
|
-
await this.
|
|
1223
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1567
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1568
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1569
|
+
await this.deleteKeys(keys);
|
|
1570
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1571
|
+
}
|
|
1572
|
+
async invalidateByPrefix(prefix) {
|
|
1573
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1574
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1575
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
1224
1576
|
await this.deleteKeys(keys);
|
|
1225
1577
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1226
1578
|
}
|
|
@@ -1247,14 +1599,43 @@ var CacheStack = class extends EventEmitter {
|
|
|
1247
1599
|
getHitRate() {
|
|
1248
1600
|
return this.metricsCollector.hitRate();
|
|
1249
1601
|
}
|
|
1602
|
+
async healthCheck() {
|
|
1603
|
+
await this.startup;
|
|
1604
|
+
return Promise.all(
|
|
1605
|
+
this.layers.map(async (layer) => {
|
|
1606
|
+
const startedAt = performance.now();
|
|
1607
|
+
try {
|
|
1608
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1609
|
+
return {
|
|
1610
|
+
layer: layer.name,
|
|
1611
|
+
healthy,
|
|
1612
|
+
latencyMs: performance.now() - startedAt
|
|
1613
|
+
};
|
|
1614
|
+
} catch (error) {
|
|
1615
|
+
return {
|
|
1616
|
+
layer: layer.name,
|
|
1617
|
+
healthy: false,
|
|
1618
|
+
latencyMs: performance.now() - startedAt,
|
|
1619
|
+
error: this.formatError(error)
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
})
|
|
1623
|
+
);
|
|
1624
|
+
}
|
|
1625
|
+
bumpGeneration(nextGeneration) {
|
|
1626
|
+
const current = this.currentGeneration ?? 0;
|
|
1627
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1628
|
+
return this.currentGeneration;
|
|
1629
|
+
}
|
|
1250
1630
|
/**
|
|
1251
1631
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1252
1632
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1253
1633
|
* Returns `null` if the key does not exist in any layer.
|
|
1254
1634
|
*/
|
|
1255
1635
|
async inspect(key) {
|
|
1256
|
-
const
|
|
1257
|
-
|
|
1636
|
+
const userKey = this.validateCacheKey(key);
|
|
1637
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1638
|
+
await this.awaitStartup("inspect");
|
|
1258
1639
|
const foundInLayers = [];
|
|
1259
1640
|
let freshTtlSeconds = null;
|
|
1260
1641
|
let staleTtlSeconds = null;
|
|
@@ -1285,10 +1666,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1285
1666
|
return null;
|
|
1286
1667
|
}
|
|
1287
1668
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1288
|
-
return { key:
|
|
1669
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1289
1670
|
}
|
|
1290
1671
|
async exportState() {
|
|
1291
|
-
await this.
|
|
1672
|
+
await this.awaitStartup("exportState");
|
|
1292
1673
|
const exported = /* @__PURE__ */ new Map();
|
|
1293
1674
|
for (const layer of this.layers) {
|
|
1294
1675
|
if (!layer.keys) {
|
|
@@ -1296,15 +1677,16 @@ var CacheStack = class extends EventEmitter {
|
|
|
1296
1677
|
}
|
|
1297
1678
|
const keys = await layer.keys();
|
|
1298
1679
|
for (const key of keys) {
|
|
1299
|
-
|
|
1680
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1681
|
+
if (exported.has(exportedKey)) {
|
|
1300
1682
|
continue;
|
|
1301
1683
|
}
|
|
1302
1684
|
const stored = await this.readLayerEntry(layer, key);
|
|
1303
1685
|
if (stored === null) {
|
|
1304
1686
|
continue;
|
|
1305
1687
|
}
|
|
1306
|
-
exported.set(
|
|
1307
|
-
key,
|
|
1688
|
+
exported.set(exportedKey, {
|
|
1689
|
+
key: exportedKey,
|
|
1308
1690
|
value: stored,
|
|
1309
1691
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1310
1692
|
});
|
|
@@ -1313,19 +1695,24 @@ var CacheStack = class extends EventEmitter {
|
|
|
1313
1695
|
return [...exported.values()];
|
|
1314
1696
|
}
|
|
1315
1697
|
async importState(entries) {
|
|
1316
|
-
await this.
|
|
1698
|
+
await this.awaitStartup("importState");
|
|
1317
1699
|
await Promise.all(
|
|
1318
1700
|
entries.map(async (entry) => {
|
|
1319
|
-
|
|
1320
|
-
await this.
|
|
1701
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1702
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1703
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1321
1704
|
})
|
|
1322
1705
|
);
|
|
1323
1706
|
}
|
|
1324
1707
|
async persistToFile(filePath) {
|
|
1708
|
+
this.assertActive("persistToFile");
|
|
1325
1709
|
const snapshot = await this.exportState();
|
|
1710
|
+
const { promises: fs } = await import("fs");
|
|
1326
1711
|
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1327
1712
|
}
|
|
1328
1713
|
async restoreFromFile(filePath) {
|
|
1714
|
+
this.assertActive("restoreFromFile");
|
|
1715
|
+
const { promises: fs } = await import("fs");
|
|
1329
1716
|
const raw = await fs.readFile(filePath, "utf8");
|
|
1330
1717
|
let parsed;
|
|
1331
1718
|
try {
|
|
@@ -1349,7 +1736,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1349
1736
|
this.disconnectPromise = (async () => {
|
|
1350
1737
|
await this.startup;
|
|
1351
1738
|
await this.unsubscribeInvalidation?.();
|
|
1739
|
+
await this.flushWriteBehindQueue();
|
|
1352
1740
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1741
|
+
if (this.writeBehindTimer) {
|
|
1742
|
+
clearInterval(this.writeBehindTimer);
|
|
1743
|
+
this.writeBehindTimer = void 0;
|
|
1744
|
+
}
|
|
1745
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1353
1746
|
})();
|
|
1354
1747
|
}
|
|
1355
1748
|
await this.disconnectPromise;
|
|
@@ -1409,7 +1802,11 @@ var CacheStack = class extends EventEmitter {
|
|
|
1409
1802
|
const fetchStart = Date.now();
|
|
1410
1803
|
let fetched;
|
|
1411
1804
|
try {
|
|
1412
|
-
fetched = await
|
|
1805
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1806
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1807
|
+
{ key, fetcher },
|
|
1808
|
+
fetcher
|
|
1809
|
+
);
|
|
1413
1810
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1414
1811
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1415
1812
|
} catch (error) {
|
|
@@ -1443,6 +1840,61 @@ var CacheStack = class extends EventEmitter {
|
|
|
1443
1840
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1444
1841
|
}
|
|
1445
1842
|
}
|
|
1843
|
+
async writeBatch(entries) {
|
|
1844
|
+
const now = Date.now();
|
|
1845
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1846
|
+
const immediateOperations = [];
|
|
1847
|
+
const deferredOperations = [];
|
|
1848
|
+
for (const entry of entries) {
|
|
1849
|
+
for (const layer of this.layers) {
|
|
1850
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1851
|
+
continue;
|
|
1852
|
+
}
|
|
1853
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1854
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1855
|
+
bucket.push(layerEntry);
|
|
1856
|
+
entriesByLayer.set(layer, bucket);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1860
|
+
const operation = async () => {
|
|
1861
|
+
try {
|
|
1862
|
+
if (layer.setMany) {
|
|
1863
|
+
await layer.setMany(layerEntries);
|
|
1864
|
+
return;
|
|
1865
|
+
}
|
|
1866
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1867
|
+
} catch (error) {
|
|
1868
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1872
|
+
deferredOperations.push(operation);
|
|
1873
|
+
} else {
|
|
1874
|
+
immediateOperations.push(operation);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1878
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1879
|
+
for (const entry of entries) {
|
|
1880
|
+
if (entry.options?.tags) {
|
|
1881
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1882
|
+
} else {
|
|
1883
|
+
await this.tagIndex.touch(entry.key);
|
|
1884
|
+
}
|
|
1885
|
+
this.metricsCollector.increment("sets");
|
|
1886
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1887
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1888
|
+
}
|
|
1889
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1890
|
+
await this.publishInvalidation({
|
|
1891
|
+
scope: "keys",
|
|
1892
|
+
keys: entries.map((entry) => entry.key),
|
|
1893
|
+
sourceId: this.instanceId,
|
|
1894
|
+
operation: "write"
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1446
1898
|
async readFromLayers(key, options, mode) {
|
|
1447
1899
|
let sawRetainableValue = false;
|
|
1448
1900
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1526,33 +1978,28 @@ var CacheStack = class extends EventEmitter {
|
|
|
1526
1978
|
}
|
|
1527
1979
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1528
1980
|
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);
|
|
1981
|
+
const immediateOperations = [];
|
|
1982
|
+
const deferredOperations = [];
|
|
1983
|
+
for (const layer of this.layers) {
|
|
1984
|
+
const operation = async () => {
|
|
1985
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1989
|
+
try {
|
|
1990
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1991
|
+
} catch (error) {
|
|
1992
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1996
|
+
deferredOperations.push(operation);
|
|
1997
|
+
} else {
|
|
1998
|
+
immediateOperations.push(operation);
|
|
1553
1999
|
}
|
|
1554
|
-
}
|
|
1555
|
-
await this.executeLayerOperations(
|
|
2000
|
+
}
|
|
2001
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
2002
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1556
2003
|
}
|
|
1557
2004
|
async executeLayerOperations(operations, context) {
|
|
1558
2005
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1576,8 +2023,17 @@ var CacheStack = class extends EventEmitter {
|
|
|
1576
2023
|
);
|
|
1577
2024
|
}
|
|
1578
2025
|
}
|
|
1579
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1580
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
2026
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2027
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
2028
|
+
key,
|
|
2029
|
+
layerName,
|
|
2030
|
+
kind,
|
|
2031
|
+
options,
|
|
2032
|
+
fallbackTtl,
|
|
2033
|
+
this.options.negativeTtl,
|
|
2034
|
+
void 0,
|
|
2035
|
+
value
|
|
2036
|
+
);
|
|
1581
2037
|
}
|
|
1582
2038
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1583
2039
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1606,7 +2062,8 @@ var CacheStack = class extends EventEmitter {
|
|
|
1606
2062
|
return {
|
|
1607
2063
|
leaseMs: this.options.singleFlightLeaseMs ?? DEFAULT_SINGLE_FLIGHT_LEASE_MS,
|
|
1608
2064
|
waitTimeoutMs: this.options.singleFlightTimeoutMs ?? DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS,
|
|
1609
|
-
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS
|
|
2065
|
+
pollIntervalMs: this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS,
|
|
2066
|
+
renewIntervalMs: this.options.singleFlightRenewIntervalMs
|
|
1610
2067
|
};
|
|
1611
2068
|
}
|
|
1612
2069
|
async deleteKeys(keys) {
|
|
@@ -1671,6 +2128,105 @@ var CacheStack = class extends EventEmitter {
|
|
|
1671
2128
|
shouldBroadcastL1Invalidation() {
|
|
1672
2129
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1673
2130
|
}
|
|
2131
|
+
initializeWriteBehind(options) {
|
|
2132
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
2133
|
+
return;
|
|
2134
|
+
}
|
|
2135
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
2136
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
this.writeBehindTimer = setInterval(() => {
|
|
2140
|
+
void this.flushWriteBehindQueue();
|
|
2141
|
+
}, flushIntervalMs);
|
|
2142
|
+
this.writeBehindTimer.unref?.();
|
|
2143
|
+
}
|
|
2144
|
+
shouldWriteBehind(layer) {
|
|
2145
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2146
|
+
}
|
|
2147
|
+
async enqueueWriteBehind(operation) {
|
|
2148
|
+
this.writeBehindQueue.push(operation);
|
|
2149
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2150
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2151
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
2152
|
+
await this.flushWriteBehindQueue();
|
|
2153
|
+
return;
|
|
2154
|
+
}
|
|
2155
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2156
|
+
await this.flushWriteBehindQueue();
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
async flushWriteBehindQueue() {
|
|
2160
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
2161
|
+
await this.writeBehindFlushPromise;
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2165
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2166
|
+
this.writeBehindFlushPromise = (async () => {
|
|
2167
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2168
|
+
})();
|
|
2169
|
+
await this.writeBehindFlushPromise;
|
|
2170
|
+
this.writeBehindFlushPromise = void 0;
|
|
2171
|
+
if (this.writeBehindQueue.length > 0) {
|
|
2172
|
+
await this.flushWriteBehindQueue();
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2176
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2177
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2178
|
+
layer.name,
|
|
2179
|
+
options?.staleWhileRevalidate,
|
|
2180
|
+
this.options.staleWhileRevalidate
|
|
2181
|
+
);
|
|
2182
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2183
|
+
const payload = createStoredValueEnvelope({
|
|
2184
|
+
kind,
|
|
2185
|
+
value,
|
|
2186
|
+
freshTtlSeconds: freshTtl,
|
|
2187
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2188
|
+
staleIfErrorSeconds: staleIfError,
|
|
2189
|
+
now
|
|
2190
|
+
});
|
|
2191
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
2192
|
+
return {
|
|
2193
|
+
key,
|
|
2194
|
+
value: payload,
|
|
2195
|
+
ttl
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
intersectKeys(groups) {
|
|
2199
|
+
if (groups.length === 0) {
|
|
2200
|
+
return [];
|
|
2201
|
+
}
|
|
2202
|
+
const [firstGroup, ...rest] = groups;
|
|
2203
|
+
if (!firstGroup) {
|
|
2204
|
+
return [];
|
|
2205
|
+
}
|
|
2206
|
+
const restSets = rest.map((group) => new Set(group));
|
|
2207
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2208
|
+
}
|
|
2209
|
+
qualifyKey(key) {
|
|
2210
|
+
const prefix = this.generationPrefix();
|
|
2211
|
+
return prefix ? `${prefix}${key}` : key;
|
|
2212
|
+
}
|
|
2213
|
+
qualifyPattern(pattern) {
|
|
2214
|
+
const prefix = this.generationPrefix();
|
|
2215
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2216
|
+
}
|
|
2217
|
+
stripQualifiedKey(key) {
|
|
2218
|
+
const prefix = this.generationPrefix();
|
|
2219
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2220
|
+
return key;
|
|
2221
|
+
}
|
|
2222
|
+
return key.slice(prefix.length);
|
|
2223
|
+
}
|
|
2224
|
+
generationPrefix() {
|
|
2225
|
+
if (this.currentGeneration === void 0) {
|
|
2226
|
+
return "";
|
|
2227
|
+
}
|
|
2228
|
+
return `v${this.currentGeneration}:`;
|
|
2229
|
+
}
|
|
1674
2230
|
async deleteKeysFromLayers(layers, keys) {
|
|
1675
2231
|
await Promise.all(
|
|
1676
2232
|
layers.map(async (layer) => {
|
|
@@ -1712,8 +2268,13 @@ var CacheStack = class extends EventEmitter {
|
|
|
1712
2268
|
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1713
2269
|
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1714
2270
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
2271
|
+
this.validatePositiveNumber("singleFlightRenewIntervalMs", this.options.singleFlightRenewIntervalMs);
|
|
2272
|
+
this.validateRateLimitOptions("fetcherRateLimit", this.options.fetcherRateLimit);
|
|
1715
2273
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1716
2274
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2275
|
+
if (this.options.generation !== void 0) {
|
|
2276
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2277
|
+
}
|
|
1717
2278
|
}
|
|
1718
2279
|
validateWriteOptions(options) {
|
|
1719
2280
|
if (!options) {
|
|
@@ -1725,8 +2286,10 @@ var CacheStack = class extends EventEmitter {
|
|
|
1725
2286
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1726
2287
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1727
2288
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2289
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1728
2290
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1729
2291
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
2292
|
+
this.validateRateLimitOptions("options.fetcherRateLimit", options.fetcherRateLimit);
|
|
1730
2293
|
}
|
|
1731
2294
|
validateLayerNumberOption(name, value) {
|
|
1732
2295
|
if (value === void 0) {
|
|
@@ -1751,6 +2314,20 @@ var CacheStack = class extends EventEmitter {
|
|
|
1751
2314
|
throw new Error(`${name} must be a positive finite number.`);
|
|
1752
2315
|
}
|
|
1753
2316
|
}
|
|
2317
|
+
validateRateLimitOptions(name, options) {
|
|
2318
|
+
if (!options) {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
this.validatePositiveNumber(`${name}.maxConcurrent`, options.maxConcurrent);
|
|
2322
|
+
this.validatePositiveNumber(`${name}.intervalMs`, options.intervalMs);
|
|
2323
|
+
this.validatePositiveNumber(`${name}.maxPerInterval`, options.maxPerInterval);
|
|
2324
|
+
if (options.scope && !["global", "key", "fetcher"].includes(options.scope)) {
|
|
2325
|
+
throw new Error(`${name}.scope must be one of "global", "key", or "fetcher".`);
|
|
2326
|
+
}
|
|
2327
|
+
if (options.bucketKey !== void 0 && options.bucketKey.length === 0) {
|
|
2328
|
+
throw new Error(`${name}.bucketKey must not be empty.`);
|
|
2329
|
+
}
|
|
2330
|
+
}
|
|
1754
2331
|
validateNonNegativeNumber(name, value) {
|
|
1755
2332
|
if (!Number.isFinite(value) || value < 0) {
|
|
1756
2333
|
throw new Error(`${name} must be a non-negative finite number.`);
|
|
@@ -1768,6 +2345,26 @@ var CacheStack = class extends EventEmitter {
|
|
|
1768
2345
|
}
|
|
1769
2346
|
return key;
|
|
1770
2347
|
}
|
|
2348
|
+
validateTtlPolicy(name, policy) {
|
|
2349
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if ("alignTo" in policy) {
|
|
2353
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2354
|
+
return;
|
|
2355
|
+
}
|
|
2356
|
+
throw new Error(`${name} is invalid.`);
|
|
2357
|
+
}
|
|
2358
|
+
assertActive(operation) {
|
|
2359
|
+
if (this.isDisconnecting) {
|
|
2360
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
async awaitStartup(operation) {
|
|
2364
|
+
this.assertActive(operation);
|
|
2365
|
+
await this.startup;
|
|
2366
|
+
this.assertActive(operation);
|
|
2367
|
+
}
|
|
1771
2368
|
serializeOptions(options) {
|
|
1772
2369
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1773
2370
|
}
|
|
@@ -1873,6 +2470,9 @@ var CacheStack = class extends EventEmitter {
|
|
|
1873
2470
|
return value;
|
|
1874
2471
|
}
|
|
1875
2472
|
};
|
|
2473
|
+
function createInstanceId() {
|
|
2474
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2475
|
+
}
|
|
1876
2476
|
|
|
1877
2477
|
// src/module.ts
|
|
1878
2478
|
var InjectCacheStack = () => Inject(CACHE_STACK);
|