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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __export = (target, all) => {
|
|
7
9
|
for (var name in all)
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
var __decorateClass = (decorators, target, key, kind) => {
|
|
20
30
|
var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
|
|
@@ -72,9 +82,182 @@ function Cacheable(options) {
|
|
|
72
82
|
var import_common = require("@nestjs/common");
|
|
73
83
|
|
|
74
84
|
// ../../src/CacheStack.ts
|
|
75
|
-
var import_node_crypto = require("crypto");
|
|
76
85
|
var import_node_events = require("events");
|
|
77
|
-
|
|
86
|
+
|
|
87
|
+
// ../../node_modules/async-mutex/index.mjs
|
|
88
|
+
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
|
|
89
|
+
var E_ALREADY_LOCKED = new Error("mutex already locked");
|
|
90
|
+
var E_CANCELED = new Error("request for lock canceled");
|
|
91
|
+
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
|
|
92
|
+
function adopt(value) {
|
|
93
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
94
|
+
resolve(value);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
98
|
+
function fulfilled(value) {
|
|
99
|
+
try {
|
|
100
|
+
step(generator.next(value));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
reject(e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function rejected(value) {
|
|
106
|
+
try {
|
|
107
|
+
step(generator["throw"](value));
|
|
108
|
+
} catch (e) {
|
|
109
|
+
reject(e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function step(result) {
|
|
113
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
114
|
+
}
|
|
115
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
var Semaphore = class {
|
|
119
|
+
constructor(_value, _cancelError = E_CANCELED) {
|
|
120
|
+
this._value = _value;
|
|
121
|
+
this._cancelError = _cancelError;
|
|
122
|
+
this._weightedQueues = [];
|
|
123
|
+
this._weightedWaiters = [];
|
|
124
|
+
}
|
|
125
|
+
acquire(weight = 1) {
|
|
126
|
+
if (weight <= 0)
|
|
127
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
if (!this._weightedQueues[weight - 1])
|
|
130
|
+
this._weightedQueues[weight - 1] = [];
|
|
131
|
+
this._weightedQueues[weight - 1].push({ resolve, reject });
|
|
132
|
+
this._dispatch();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
runExclusive(callback, weight = 1) {
|
|
136
|
+
return __awaiter$2(this, void 0, void 0, function* () {
|
|
137
|
+
const [value, release] = yield this.acquire(weight);
|
|
138
|
+
try {
|
|
139
|
+
return yield callback(value);
|
|
140
|
+
} finally {
|
|
141
|
+
release();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
waitForUnlock(weight = 1) {
|
|
146
|
+
if (weight <= 0)
|
|
147
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
if (!this._weightedWaiters[weight - 1])
|
|
150
|
+
this._weightedWaiters[weight - 1] = [];
|
|
151
|
+
this._weightedWaiters[weight - 1].push(resolve);
|
|
152
|
+
this._dispatch();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
isLocked() {
|
|
156
|
+
return this._value <= 0;
|
|
157
|
+
}
|
|
158
|
+
getValue() {
|
|
159
|
+
return this._value;
|
|
160
|
+
}
|
|
161
|
+
setValue(value) {
|
|
162
|
+
this._value = value;
|
|
163
|
+
this._dispatch();
|
|
164
|
+
}
|
|
165
|
+
release(weight = 1) {
|
|
166
|
+
if (weight <= 0)
|
|
167
|
+
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
168
|
+
this._value += weight;
|
|
169
|
+
this._dispatch();
|
|
170
|
+
}
|
|
171
|
+
cancel() {
|
|
172
|
+
this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
|
|
173
|
+
this._weightedQueues = [];
|
|
174
|
+
}
|
|
175
|
+
_dispatch() {
|
|
176
|
+
var _a;
|
|
177
|
+
for (let weight = this._value; weight > 0; weight--) {
|
|
178
|
+
const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
|
|
179
|
+
if (!queueEntry)
|
|
180
|
+
continue;
|
|
181
|
+
const previousValue = this._value;
|
|
182
|
+
const previousWeight = weight;
|
|
183
|
+
this._value -= weight;
|
|
184
|
+
weight = this._value + 1;
|
|
185
|
+
queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
|
|
186
|
+
}
|
|
187
|
+
this._drainUnlockWaiters();
|
|
188
|
+
}
|
|
189
|
+
_newReleaser(weight) {
|
|
190
|
+
let called = false;
|
|
191
|
+
return () => {
|
|
192
|
+
if (called)
|
|
193
|
+
return;
|
|
194
|
+
called = true;
|
|
195
|
+
this.release(weight);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
_drainUnlockWaiters() {
|
|
199
|
+
for (let weight = this._value; weight > 0; weight--) {
|
|
200
|
+
if (!this._weightedWaiters[weight - 1])
|
|
201
|
+
continue;
|
|
202
|
+
this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
|
|
203
|
+
this._weightedWaiters[weight - 1] = [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
var __awaiter$1 = function(thisArg, _arguments, P, generator) {
|
|
208
|
+
function adopt(value) {
|
|
209
|
+
return value instanceof P ? value : new P(function(resolve) {
|
|
210
|
+
resolve(value);
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return new (P || (P = Promise))(function(resolve, reject) {
|
|
214
|
+
function fulfilled(value) {
|
|
215
|
+
try {
|
|
216
|
+
step(generator.next(value));
|
|
217
|
+
} catch (e) {
|
|
218
|
+
reject(e);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function rejected(value) {
|
|
222
|
+
try {
|
|
223
|
+
step(generator["throw"](value));
|
|
224
|
+
} catch (e) {
|
|
225
|
+
reject(e);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function step(result) {
|
|
229
|
+
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
230
|
+
}
|
|
231
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
var Mutex = class {
|
|
235
|
+
constructor(cancelError) {
|
|
236
|
+
this._semaphore = new Semaphore(1, cancelError);
|
|
237
|
+
}
|
|
238
|
+
acquire() {
|
|
239
|
+
return __awaiter$1(this, void 0, void 0, function* () {
|
|
240
|
+
const [, releaser] = yield this._semaphore.acquire();
|
|
241
|
+
return releaser;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
runExclusive(callback) {
|
|
245
|
+
return this._semaphore.runExclusive(() => callback());
|
|
246
|
+
}
|
|
247
|
+
isLocked() {
|
|
248
|
+
return this._semaphore.isLocked();
|
|
249
|
+
}
|
|
250
|
+
waitForUnlock() {
|
|
251
|
+
return this._semaphore.waitForUnlock();
|
|
252
|
+
}
|
|
253
|
+
release() {
|
|
254
|
+
if (this._semaphore.isLocked())
|
|
255
|
+
this._semaphore.release();
|
|
256
|
+
}
|
|
257
|
+
cancel() {
|
|
258
|
+
return this._semaphore.cancel();
|
|
259
|
+
}
|
|
260
|
+
};
|
|
78
261
|
|
|
79
262
|
// ../../src/CacheNamespace.ts
|
|
80
263
|
var CacheNamespace = class _CacheNamespace {
|
|
@@ -84,57 +267,69 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
84
267
|
}
|
|
85
268
|
cache;
|
|
86
269
|
prefix;
|
|
270
|
+
static metricsMutexes = /* @__PURE__ */ new WeakMap();
|
|
271
|
+
metrics = emptyMetrics();
|
|
87
272
|
async get(key, fetcher, options) {
|
|
88
|
-
return this.cache.get(this.qualify(key), fetcher, options);
|
|
273
|
+
return this.trackMetrics(() => this.cache.get(this.qualify(key), fetcher, options));
|
|
89
274
|
}
|
|
90
275
|
async getOrSet(key, fetcher, options) {
|
|
91
|
-
return this.cache.getOrSet(this.qualify(key), fetcher, options);
|
|
276
|
+
return this.trackMetrics(() => this.cache.getOrSet(this.qualify(key), fetcher, options));
|
|
92
277
|
}
|
|
93
278
|
/**
|
|
94
279
|
* Like `get()`, but throws `CacheMissError` instead of returning `null`.
|
|
95
280
|
*/
|
|
96
281
|
async getOrThrow(key, fetcher, options) {
|
|
97
|
-
return this.cache.getOrThrow(this.qualify(key), fetcher, options);
|
|
282
|
+
return this.trackMetrics(() => this.cache.getOrThrow(this.qualify(key), fetcher, options));
|
|
98
283
|
}
|
|
99
284
|
async has(key) {
|
|
100
|
-
return this.cache.has(this.qualify(key));
|
|
285
|
+
return this.trackMetrics(() => this.cache.has(this.qualify(key)));
|
|
101
286
|
}
|
|
102
287
|
async ttl(key) {
|
|
103
|
-
return this.cache.ttl(this.qualify(key));
|
|
288
|
+
return this.trackMetrics(() => this.cache.ttl(this.qualify(key)));
|
|
104
289
|
}
|
|
105
290
|
async set(key, value, options) {
|
|
106
|
-
await this.cache.set(this.qualify(key), value, options);
|
|
291
|
+
await this.trackMetrics(() => this.cache.set(this.qualify(key), value, options));
|
|
107
292
|
}
|
|
108
293
|
async delete(key) {
|
|
109
|
-
await this.cache.delete(this.qualify(key));
|
|
294
|
+
await this.trackMetrics(() => this.cache.delete(this.qualify(key)));
|
|
110
295
|
}
|
|
111
296
|
async mdelete(keys) {
|
|
112
|
-
await this.cache.mdelete(keys.map((k) => this.qualify(k)));
|
|
297
|
+
await this.trackMetrics(() => this.cache.mdelete(keys.map((k) => this.qualify(k))));
|
|
113
298
|
}
|
|
114
299
|
async clear() {
|
|
115
|
-
await this.cache.
|
|
300
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.prefix));
|
|
116
301
|
}
|
|
117
302
|
async mget(entries) {
|
|
118
|
-
return this.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
303
|
+
return this.trackMetrics(
|
|
304
|
+
() => this.cache.mget(
|
|
305
|
+
entries.map((entry) => ({
|
|
306
|
+
...entry,
|
|
307
|
+
key: this.qualify(entry.key)
|
|
308
|
+
}))
|
|
309
|
+
)
|
|
123
310
|
);
|
|
124
311
|
}
|
|
125
312
|
async mset(entries) {
|
|
126
|
-
await this.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
313
|
+
await this.trackMetrics(
|
|
314
|
+
() => this.cache.mset(
|
|
315
|
+
entries.map((entry) => ({
|
|
316
|
+
...entry,
|
|
317
|
+
key: this.qualify(entry.key)
|
|
318
|
+
}))
|
|
319
|
+
)
|
|
131
320
|
);
|
|
132
321
|
}
|
|
133
322
|
async invalidateByTag(tag) {
|
|
134
|
-
await this.cache.invalidateByTag(tag);
|
|
323
|
+
await this.trackMetrics(() => this.cache.invalidateByTag(tag));
|
|
324
|
+
}
|
|
325
|
+
async invalidateByTags(tags, mode = "any") {
|
|
326
|
+
await this.trackMetrics(() => this.cache.invalidateByTags(tags, mode));
|
|
135
327
|
}
|
|
136
328
|
async invalidateByPattern(pattern) {
|
|
137
|
-
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
329
|
+
await this.trackMetrics(() => this.cache.invalidateByPattern(this.qualify(pattern)));
|
|
330
|
+
}
|
|
331
|
+
async invalidateByPrefix(prefix) {
|
|
332
|
+
await this.trackMetrics(() => this.cache.invalidateByPrefix(this.qualify(prefix)));
|
|
138
333
|
}
|
|
139
334
|
/**
|
|
140
335
|
* Returns detailed metadata about a single cache key within this namespace.
|
|
@@ -155,10 +350,19 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
155
350
|
);
|
|
156
351
|
}
|
|
157
352
|
getMetrics() {
|
|
158
|
-
return this.
|
|
353
|
+
return cloneMetrics(this.metrics);
|
|
159
354
|
}
|
|
160
355
|
getHitRate() {
|
|
161
|
-
|
|
356
|
+
const total = this.metrics.hits + this.metrics.misses;
|
|
357
|
+
const overall = total === 0 ? 0 : this.metrics.hits / total;
|
|
358
|
+
const byLayer = {};
|
|
359
|
+
const layers = /* @__PURE__ */ new Set([...Object.keys(this.metrics.hitsByLayer), ...Object.keys(this.metrics.missesByLayer)]);
|
|
360
|
+
for (const layer of layers) {
|
|
361
|
+
const hits = this.metrics.hitsByLayer[layer] ?? 0;
|
|
362
|
+
const misses = this.metrics.missesByLayer[layer] ?? 0;
|
|
363
|
+
byLayer[layer] = hits + misses === 0 ? 0 : hits / (hits + misses);
|
|
364
|
+
}
|
|
365
|
+
return { overall, byLayer };
|
|
162
366
|
}
|
|
163
367
|
/**
|
|
164
368
|
* Creates a nested namespace. Keys are prefixed with `parentPrefix:childPrefix:`.
|
|
@@ -175,7 +379,130 @@ var CacheNamespace = class _CacheNamespace {
|
|
|
175
379
|
qualify(key) {
|
|
176
380
|
return `${this.prefix}:${key}`;
|
|
177
381
|
}
|
|
382
|
+
async trackMetrics(operation) {
|
|
383
|
+
return this.getMetricsMutex().runExclusive(async () => {
|
|
384
|
+
const before = this.cache.getMetrics();
|
|
385
|
+
const result = await operation();
|
|
386
|
+
const after = this.cache.getMetrics();
|
|
387
|
+
this.metrics = addMetrics(this.metrics, diffMetrics(before, after));
|
|
388
|
+
return result;
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
getMetricsMutex() {
|
|
392
|
+
const existing = _CacheNamespace.metricsMutexes.get(this.cache);
|
|
393
|
+
if (existing) {
|
|
394
|
+
return existing;
|
|
395
|
+
}
|
|
396
|
+
const mutex = new Mutex();
|
|
397
|
+
_CacheNamespace.metricsMutexes.set(this.cache, mutex);
|
|
398
|
+
return mutex;
|
|
399
|
+
}
|
|
178
400
|
};
|
|
401
|
+
function emptyMetrics() {
|
|
402
|
+
return {
|
|
403
|
+
hits: 0,
|
|
404
|
+
misses: 0,
|
|
405
|
+
fetches: 0,
|
|
406
|
+
sets: 0,
|
|
407
|
+
deletes: 0,
|
|
408
|
+
backfills: 0,
|
|
409
|
+
invalidations: 0,
|
|
410
|
+
staleHits: 0,
|
|
411
|
+
refreshes: 0,
|
|
412
|
+
refreshErrors: 0,
|
|
413
|
+
writeFailures: 0,
|
|
414
|
+
singleFlightWaits: 0,
|
|
415
|
+
negativeCacheHits: 0,
|
|
416
|
+
circuitBreakerTrips: 0,
|
|
417
|
+
degradedOperations: 0,
|
|
418
|
+
hitsByLayer: {},
|
|
419
|
+
missesByLayer: {},
|
|
420
|
+
latencyByLayer: {},
|
|
421
|
+
resetAt: Date.now()
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function cloneMetrics(metrics) {
|
|
425
|
+
return {
|
|
426
|
+
...metrics,
|
|
427
|
+
hitsByLayer: { ...metrics.hitsByLayer },
|
|
428
|
+
missesByLayer: { ...metrics.missesByLayer },
|
|
429
|
+
latencyByLayer: Object.fromEntries(
|
|
430
|
+
Object.entries(metrics.latencyByLayer).map(([key, value]) => [key, { ...value }])
|
|
431
|
+
)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function diffMetrics(before, after) {
|
|
435
|
+
const latencyByLayer = Object.fromEntries(
|
|
436
|
+
Object.entries(after.latencyByLayer).map(([layer, value]) => [
|
|
437
|
+
layer,
|
|
438
|
+
{
|
|
439
|
+
avgMs: value.avgMs,
|
|
440
|
+
maxMs: value.maxMs,
|
|
441
|
+
count: Math.max(0, value.count - (before.latencyByLayer[layer]?.count ?? 0))
|
|
442
|
+
}
|
|
443
|
+
])
|
|
444
|
+
);
|
|
445
|
+
return {
|
|
446
|
+
hits: after.hits - before.hits,
|
|
447
|
+
misses: after.misses - before.misses,
|
|
448
|
+
fetches: after.fetches - before.fetches,
|
|
449
|
+
sets: after.sets - before.sets,
|
|
450
|
+
deletes: after.deletes - before.deletes,
|
|
451
|
+
backfills: after.backfills - before.backfills,
|
|
452
|
+
invalidations: after.invalidations - before.invalidations,
|
|
453
|
+
staleHits: after.staleHits - before.staleHits,
|
|
454
|
+
refreshes: after.refreshes - before.refreshes,
|
|
455
|
+
refreshErrors: after.refreshErrors - before.refreshErrors,
|
|
456
|
+
writeFailures: after.writeFailures - before.writeFailures,
|
|
457
|
+
singleFlightWaits: after.singleFlightWaits - before.singleFlightWaits,
|
|
458
|
+
negativeCacheHits: after.negativeCacheHits - before.negativeCacheHits,
|
|
459
|
+
circuitBreakerTrips: after.circuitBreakerTrips - before.circuitBreakerTrips,
|
|
460
|
+
degradedOperations: after.degradedOperations - before.degradedOperations,
|
|
461
|
+
hitsByLayer: diffMap(before.hitsByLayer, after.hitsByLayer),
|
|
462
|
+
missesByLayer: diffMap(before.missesByLayer, after.missesByLayer),
|
|
463
|
+
latencyByLayer,
|
|
464
|
+
resetAt: after.resetAt
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function addMetrics(base, delta) {
|
|
468
|
+
return {
|
|
469
|
+
hits: base.hits + delta.hits,
|
|
470
|
+
misses: base.misses + delta.misses,
|
|
471
|
+
fetches: base.fetches + delta.fetches,
|
|
472
|
+
sets: base.sets + delta.sets,
|
|
473
|
+
deletes: base.deletes + delta.deletes,
|
|
474
|
+
backfills: base.backfills + delta.backfills,
|
|
475
|
+
invalidations: base.invalidations + delta.invalidations,
|
|
476
|
+
staleHits: base.staleHits + delta.staleHits,
|
|
477
|
+
refreshes: base.refreshes + delta.refreshes,
|
|
478
|
+
refreshErrors: base.refreshErrors + delta.refreshErrors,
|
|
479
|
+
writeFailures: base.writeFailures + delta.writeFailures,
|
|
480
|
+
singleFlightWaits: base.singleFlightWaits + delta.singleFlightWaits,
|
|
481
|
+
negativeCacheHits: base.negativeCacheHits + delta.negativeCacheHits,
|
|
482
|
+
circuitBreakerTrips: base.circuitBreakerTrips + delta.circuitBreakerTrips,
|
|
483
|
+
degradedOperations: base.degradedOperations + delta.degradedOperations,
|
|
484
|
+
hitsByLayer: addMap(base.hitsByLayer, delta.hitsByLayer),
|
|
485
|
+
missesByLayer: addMap(base.missesByLayer, delta.missesByLayer),
|
|
486
|
+
latencyByLayer: cloneMetrics(delta).latencyByLayer,
|
|
487
|
+
resetAt: base.resetAt
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function diffMap(before, after) {
|
|
491
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
492
|
+
const result = {};
|
|
493
|
+
for (const key of keys) {
|
|
494
|
+
result[key] = (after[key] ?? 0) - (before[key] ?? 0);
|
|
495
|
+
}
|
|
496
|
+
return result;
|
|
497
|
+
}
|
|
498
|
+
function addMap(base, delta) {
|
|
499
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(base), ...Object.keys(delta)]);
|
|
500
|
+
const result = {};
|
|
501
|
+
for (const key of keys) {
|
|
502
|
+
result[key] = (base[key] ?? 0) + (delta[key] ?? 0);
|
|
503
|
+
}
|
|
504
|
+
return result;
|
|
505
|
+
}
|
|
179
506
|
|
|
180
507
|
// ../../src/internal/CircuitBreakerManager.ts
|
|
181
508
|
var CircuitBreakerManager = class {
|
|
@@ -269,6 +596,95 @@ var CircuitBreakerManager = class {
|
|
|
269
596
|
}
|
|
270
597
|
};
|
|
271
598
|
|
|
599
|
+
// ../../src/internal/FetchRateLimiter.ts
|
|
600
|
+
var FetchRateLimiter = class {
|
|
601
|
+
active = 0;
|
|
602
|
+
queue = [];
|
|
603
|
+
startedAt = [];
|
|
604
|
+
drainTimer;
|
|
605
|
+
async schedule(options, task) {
|
|
606
|
+
if (!options) {
|
|
607
|
+
return task();
|
|
608
|
+
}
|
|
609
|
+
const normalized = this.normalize(options);
|
|
610
|
+
if (!normalized) {
|
|
611
|
+
return task();
|
|
612
|
+
}
|
|
613
|
+
return new Promise((resolve, reject) => {
|
|
614
|
+
this.queue.push({ options: normalized, task, resolve, reject });
|
|
615
|
+
this.drain();
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
normalize(options) {
|
|
619
|
+
const maxConcurrent = options.maxConcurrent;
|
|
620
|
+
const intervalMs = options.intervalMs;
|
|
621
|
+
const maxPerInterval = options.maxPerInterval;
|
|
622
|
+
if (!maxConcurrent && !(intervalMs && maxPerInterval)) {
|
|
623
|
+
return void 0;
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
maxConcurrent,
|
|
627
|
+
intervalMs,
|
|
628
|
+
maxPerInterval
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
drain() {
|
|
632
|
+
if (this.drainTimer) {
|
|
633
|
+
clearTimeout(this.drainTimer);
|
|
634
|
+
this.drainTimer = void 0;
|
|
635
|
+
}
|
|
636
|
+
while (this.queue.length > 0) {
|
|
637
|
+
const next = this.queue[0];
|
|
638
|
+
if (!next) {
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const waitMs = this.waitTime(next.options);
|
|
642
|
+
if (waitMs > 0) {
|
|
643
|
+
this.drainTimer = setTimeout(() => {
|
|
644
|
+
this.drainTimer = void 0;
|
|
645
|
+
this.drain();
|
|
646
|
+
}, waitMs);
|
|
647
|
+
this.drainTimer.unref?.();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
this.queue.shift();
|
|
651
|
+
this.active += 1;
|
|
652
|
+
this.startedAt.push(Date.now());
|
|
653
|
+
void next.task().then(next.resolve, next.reject).finally(() => {
|
|
654
|
+
this.active -= 1;
|
|
655
|
+
this.drain();
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
waitTime(options) {
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
if (options.maxConcurrent && this.active >= options.maxConcurrent) {
|
|
662
|
+
return 1;
|
|
663
|
+
}
|
|
664
|
+
if (!options.intervalMs || !options.maxPerInterval) {
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
this.prune(now, options.intervalMs);
|
|
668
|
+
if (this.startedAt.length < options.maxPerInterval) {
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
const oldest = this.startedAt[0];
|
|
672
|
+
if (!oldest) {
|
|
673
|
+
return 0;
|
|
674
|
+
}
|
|
675
|
+
return Math.max(1, options.intervalMs - (now - oldest));
|
|
676
|
+
}
|
|
677
|
+
prune(now, intervalMs) {
|
|
678
|
+
while (this.startedAt.length > 0) {
|
|
679
|
+
const startedAt = this.startedAt[0];
|
|
680
|
+
if (startedAt === void 0 || now - startedAt < intervalMs) {
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
this.startedAt.shift();
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
272
688
|
// ../../src/internal/MetricsCollector.ts
|
|
273
689
|
var MetricsCollector = class {
|
|
274
690
|
data = this.empty();
|
|
@@ -465,13 +881,14 @@ var TtlResolver = class {
|
|
|
465
881
|
clearProfiles() {
|
|
466
882
|
this.accessProfiles.clear();
|
|
467
883
|
}
|
|
468
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl) {
|
|
884
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, globalNegativeTtl, globalTtl, value) {
|
|
885
|
+
const policyTtl = kind === "value" ? this.resolvePolicyTtl(key, value, options?.ttlPolicy) : void 0;
|
|
469
886
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
470
887
|
layerName,
|
|
471
888
|
options?.negativeTtl,
|
|
472
889
|
globalNegativeTtl,
|
|
473
|
-
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
474
|
-
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, fallbackTtl);
|
|
890
|
+
this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
891
|
+
) : this.resolveLayerSeconds(layerName, options?.ttl, globalTtl, policyTtl ?? fallbackTtl);
|
|
475
892
|
const adaptiveTtl = this.applyAdaptiveTtl(key, layerName, baseTtl, options?.adaptiveTtl);
|
|
476
893
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, void 0);
|
|
477
894
|
return this.applyJitter(adaptiveTtl, jitter);
|
|
@@ -510,6 +927,29 @@ var TtlResolver = class {
|
|
|
510
927
|
const delta = (Math.random() * 2 - 1) * jitter;
|
|
511
928
|
return Math.max(1, Math.round(ttl + delta));
|
|
512
929
|
}
|
|
930
|
+
resolvePolicyTtl(key, value, policy) {
|
|
931
|
+
if (!policy) {
|
|
932
|
+
return void 0;
|
|
933
|
+
}
|
|
934
|
+
if (typeof policy === "function") {
|
|
935
|
+
return policy({ key, value });
|
|
936
|
+
}
|
|
937
|
+
const now = /* @__PURE__ */ new Date();
|
|
938
|
+
if (policy === "until-midnight") {
|
|
939
|
+
const nextMidnight = new Date(now);
|
|
940
|
+
nextMidnight.setHours(24, 0, 0, 0);
|
|
941
|
+
return Math.max(1, Math.ceil((nextMidnight.getTime() - now.getTime()) / 1e3));
|
|
942
|
+
}
|
|
943
|
+
if (policy === "next-hour") {
|
|
944
|
+
const nextHour = new Date(now);
|
|
945
|
+
nextHour.setMinutes(60, 0, 0);
|
|
946
|
+
return Math.max(1, Math.ceil((nextHour.getTime() - now.getTime()) / 1e3));
|
|
947
|
+
}
|
|
948
|
+
const alignToSeconds = policy.alignTo;
|
|
949
|
+
const currentSeconds = Math.floor(Date.now() / 1e3);
|
|
950
|
+
const nextBoundary = Math.ceil((currentSeconds + 1) / alignToSeconds) * alignToSeconds;
|
|
951
|
+
return Math.max(1, nextBoundary - currentSeconds);
|
|
952
|
+
}
|
|
513
953
|
readLayerNumber(layerName, value) {
|
|
514
954
|
if (typeof value === "number") {
|
|
515
955
|
return value;
|
|
@@ -524,306 +964,146 @@ var TtlResolver = class {
|
|
|
524
964
|
let removed = 0;
|
|
525
965
|
for (const key of this.accessProfiles.keys()) {
|
|
526
966
|
if (removed >= toRemove) {
|
|
527
|
-
break;
|
|
528
|
-
}
|
|
529
|
-
this.accessProfiles.delete(key);
|
|
530
|
-
removed += 1;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
// ../../src/invalidation/PatternMatcher.ts
|
|
536
|
-
var PatternMatcher = class _PatternMatcher {
|
|
537
|
-
/**
|
|
538
|
-
* Tests whether a glob-style pattern matches a value.
|
|
539
|
-
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
540
|
-
* Uses a linear-time algorithm to avoid ReDoS vulnerabilities.
|
|
541
|
-
*/
|
|
542
|
-
static matches(pattern, value) {
|
|
543
|
-
return _PatternMatcher.matchLinear(pattern, value);
|
|
544
|
-
}
|
|
545
|
-
/**
|
|
546
|
-
* Linear-time glob matching using dynamic programming.
|
|
547
|
-
* Avoids catastrophic backtracking that RegExp-based glob matching can cause.
|
|
548
|
-
*/
|
|
549
|
-
static matchLinear(pattern, value) {
|
|
550
|
-
const m = pattern.length;
|
|
551
|
-
const n = value.length;
|
|
552
|
-
const dp = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(false));
|
|
553
|
-
dp[0][0] = true;
|
|
554
|
-
for (let i = 1; i <= m; i++) {
|
|
555
|
-
if (pattern[i - 1] === "*") {
|
|
556
|
-
dp[i][0] = dp[i - 1]?.[0];
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
for (let i = 1; i <= m; i++) {
|
|
560
|
-
for (let j = 1; j <= n; j++) {
|
|
561
|
-
const pc = pattern[i - 1];
|
|
562
|
-
if (pc === "*") {
|
|
563
|
-
dp[i][j] = dp[i - 1]?.[j] || dp[i]?.[j - 1];
|
|
564
|
-
} else if (pc === "?" || pc === value[j - 1]) {
|
|
565
|
-
dp[i][j] = dp[i - 1]?.[j - 1];
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
return dp[m]?.[n];
|
|
570
|
-
}
|
|
571
|
-
};
|
|
572
|
-
|
|
573
|
-
// ../../src/invalidation/TagIndex.ts
|
|
574
|
-
var TagIndex = class {
|
|
575
|
-
tagToKeys = /* @__PURE__ */ new Map();
|
|
576
|
-
keyToTags = /* @__PURE__ */ new Map();
|
|
577
|
-
knownKeys = /* @__PURE__ */ new Set();
|
|
578
|
-
maxKnownKeys;
|
|
579
|
-
constructor(options = {}) {
|
|
580
|
-
this.maxKnownKeys = options.maxKnownKeys;
|
|
581
|
-
}
|
|
582
|
-
async touch(key) {
|
|
583
|
-
this.knownKeys.add(key);
|
|
584
|
-
this.pruneKnownKeysIfNeeded();
|
|
585
|
-
}
|
|
586
|
-
async track(key, tags) {
|
|
587
|
-
this.knownKeys.add(key);
|
|
588
|
-
this.pruneKnownKeysIfNeeded();
|
|
589
|
-
if (tags.length === 0) {
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
const existingTags = this.keyToTags.get(key);
|
|
593
|
-
if (existingTags) {
|
|
594
|
-
for (const tag of existingTags) {
|
|
595
|
-
this.tagToKeys.get(tag)?.delete(key);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
const tagSet = new Set(tags);
|
|
599
|
-
this.keyToTags.set(key, tagSet);
|
|
600
|
-
for (const tag of tagSet) {
|
|
601
|
-
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
602
|
-
keys.add(key);
|
|
603
|
-
this.tagToKeys.set(tag, keys);
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
async remove(key) {
|
|
607
|
-
this.knownKeys.delete(key);
|
|
608
|
-
const tags = this.keyToTags.get(key);
|
|
609
|
-
if (!tags) {
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
612
|
-
for (const tag of tags) {
|
|
613
|
-
const keys = this.tagToKeys.get(tag);
|
|
614
|
-
if (!keys) {
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
keys.delete(key);
|
|
618
|
-
if (keys.size === 0) {
|
|
619
|
-
this.tagToKeys.delete(tag);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
this.keyToTags.delete(key);
|
|
623
|
-
}
|
|
624
|
-
async keysForTag(tag) {
|
|
625
|
-
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
626
|
-
}
|
|
627
|
-
async tagsForKey(key) {
|
|
628
|
-
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
629
|
-
}
|
|
630
|
-
async matchPattern(pattern) {
|
|
631
|
-
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
632
|
-
}
|
|
633
|
-
async clear() {
|
|
634
|
-
this.tagToKeys.clear();
|
|
635
|
-
this.keyToTags.clear();
|
|
636
|
-
this.knownKeys.clear();
|
|
637
|
-
}
|
|
638
|
-
pruneKnownKeysIfNeeded() {
|
|
639
|
-
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
643
|
-
let removed = 0;
|
|
644
|
-
for (const key of this.knownKeys) {
|
|
645
|
-
if (removed >= toRemove) {
|
|
646
|
-
break;
|
|
647
|
-
}
|
|
648
|
-
this.knownKeys.delete(key);
|
|
649
|
-
this.keyToTags.delete(key);
|
|
650
|
-
removed += 1;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
};
|
|
654
|
-
|
|
655
|
-
// ../../node_modules/async-mutex/index.mjs
|
|
656
|
-
var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
|
|
657
|
-
var E_ALREADY_LOCKED = new Error("mutex already locked");
|
|
658
|
-
var E_CANCELED = new Error("request for lock canceled");
|
|
659
|
-
var __awaiter$2 = function(thisArg, _arguments, P, generator) {
|
|
660
|
-
function adopt(value) {
|
|
661
|
-
return value instanceof P ? value : new P(function(resolve) {
|
|
662
|
-
resolve(value);
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
return new (P || (P = Promise))(function(resolve, reject) {
|
|
666
|
-
function fulfilled(value) {
|
|
667
|
-
try {
|
|
668
|
-
step(generator.next(value));
|
|
669
|
-
} catch (e) {
|
|
670
|
-
reject(e);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
function rejected(value) {
|
|
674
|
-
try {
|
|
675
|
-
step(generator["throw"](value));
|
|
676
|
-
} catch (e) {
|
|
677
|
-
reject(e);
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
function step(result) {
|
|
681
|
-
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
682
|
-
}
|
|
683
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
684
|
-
});
|
|
685
|
-
};
|
|
686
|
-
var Semaphore = class {
|
|
687
|
-
constructor(_value, _cancelError = E_CANCELED) {
|
|
688
|
-
this._value = _value;
|
|
689
|
-
this._cancelError = _cancelError;
|
|
690
|
-
this._weightedQueues = [];
|
|
691
|
-
this._weightedWaiters = [];
|
|
692
|
-
}
|
|
693
|
-
acquire(weight = 1) {
|
|
694
|
-
if (weight <= 0)
|
|
695
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
696
|
-
return new Promise((resolve, reject) => {
|
|
697
|
-
if (!this._weightedQueues[weight - 1])
|
|
698
|
-
this._weightedQueues[weight - 1] = [];
|
|
699
|
-
this._weightedQueues[weight - 1].push({ resolve, reject });
|
|
700
|
-
this._dispatch();
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
runExclusive(callback, weight = 1) {
|
|
704
|
-
return __awaiter$2(this, void 0, void 0, function* () {
|
|
705
|
-
const [value, release] = yield this.acquire(weight);
|
|
706
|
-
try {
|
|
707
|
-
return yield callback(value);
|
|
708
|
-
} finally {
|
|
709
|
-
release();
|
|
710
|
-
}
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
waitForUnlock(weight = 1) {
|
|
714
|
-
if (weight <= 0)
|
|
715
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
716
|
-
return new Promise((resolve) => {
|
|
717
|
-
if (!this._weightedWaiters[weight - 1])
|
|
718
|
-
this._weightedWaiters[weight - 1] = [];
|
|
719
|
-
this._weightedWaiters[weight - 1].push(resolve);
|
|
720
|
-
this._dispatch();
|
|
721
|
-
});
|
|
722
|
-
}
|
|
723
|
-
isLocked() {
|
|
724
|
-
return this._value <= 0;
|
|
725
|
-
}
|
|
726
|
-
getValue() {
|
|
727
|
-
return this._value;
|
|
728
|
-
}
|
|
729
|
-
setValue(value) {
|
|
730
|
-
this._value = value;
|
|
731
|
-
this._dispatch();
|
|
732
|
-
}
|
|
733
|
-
release(weight = 1) {
|
|
734
|
-
if (weight <= 0)
|
|
735
|
-
throw new Error(`invalid weight ${weight}: must be positive`);
|
|
736
|
-
this._value += weight;
|
|
737
|
-
this._dispatch();
|
|
738
|
-
}
|
|
739
|
-
cancel() {
|
|
740
|
-
this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
|
|
741
|
-
this._weightedQueues = [];
|
|
742
|
-
}
|
|
743
|
-
_dispatch() {
|
|
744
|
-
var _a;
|
|
745
|
-
for (let weight = this._value; weight > 0; weight--) {
|
|
746
|
-
const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
|
|
747
|
-
if (!queueEntry)
|
|
748
|
-
continue;
|
|
749
|
-
const previousValue = this._value;
|
|
750
|
-
const previousWeight = weight;
|
|
751
|
-
this._value -= weight;
|
|
752
|
-
weight = this._value + 1;
|
|
753
|
-
queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
|
|
754
|
-
}
|
|
755
|
-
this._drainUnlockWaiters();
|
|
756
|
-
}
|
|
757
|
-
_newReleaser(weight) {
|
|
758
|
-
let called = false;
|
|
759
|
-
return () => {
|
|
760
|
-
if (called)
|
|
761
|
-
return;
|
|
762
|
-
called = true;
|
|
763
|
-
this.release(weight);
|
|
764
|
-
};
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
this.accessProfiles.delete(key);
|
|
970
|
+
removed += 1;
|
|
971
|
+
}
|
|
765
972
|
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
// ../../src/invalidation/PatternMatcher.ts
|
|
976
|
+
var PatternMatcher = class _PatternMatcher {
|
|
977
|
+
/**
|
|
978
|
+
* Tests whether a glob-style pattern matches a value.
|
|
979
|
+
* Supports `*` (any sequence of characters) and `?` (any single character).
|
|
980
|
+
* Uses a two-pointer algorithm to avoid ReDoS vulnerabilities and
|
|
981
|
+
* quadratic memory usage on long patterns/keys.
|
|
982
|
+
*/
|
|
983
|
+
static matches(pattern, value) {
|
|
984
|
+
return _PatternMatcher.matchLinear(pattern, value);
|
|
985
|
+
}
|
|
986
|
+
/**
|
|
987
|
+
* Linear-time glob matching with O(1) extra memory.
|
|
988
|
+
*/
|
|
989
|
+
static matchLinear(pattern, value) {
|
|
990
|
+
let patternIndex = 0;
|
|
991
|
+
let valueIndex = 0;
|
|
992
|
+
let starIndex = -1;
|
|
993
|
+
let backtrackValueIndex = 0;
|
|
994
|
+
while (valueIndex < value.length) {
|
|
995
|
+
const patternChar = pattern[patternIndex];
|
|
996
|
+
const valueChar = value[valueIndex];
|
|
997
|
+
if (patternChar === "*" && patternIndex < pattern.length) {
|
|
998
|
+
starIndex = patternIndex;
|
|
999
|
+
patternIndex += 1;
|
|
1000
|
+
backtrackValueIndex = valueIndex;
|
|
769
1001
|
continue;
|
|
770
|
-
|
|
771
|
-
|
|
1002
|
+
}
|
|
1003
|
+
if (patternChar === "?" || patternChar === valueChar) {
|
|
1004
|
+
patternIndex += 1;
|
|
1005
|
+
valueIndex += 1;
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
1008
|
+
if (starIndex !== -1) {
|
|
1009
|
+
patternIndex = starIndex + 1;
|
|
1010
|
+
backtrackValueIndex += 1;
|
|
1011
|
+
valueIndex = backtrackValueIndex;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
return false;
|
|
772
1015
|
}
|
|
1016
|
+
while (patternIndex < pattern.length && pattern[patternIndex] === "*") {
|
|
1017
|
+
patternIndex += 1;
|
|
1018
|
+
}
|
|
1019
|
+
return patternIndex === pattern.length;
|
|
773
1020
|
}
|
|
774
1021
|
};
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
1022
|
+
|
|
1023
|
+
// ../../src/invalidation/TagIndex.ts
|
|
1024
|
+
var TagIndex = class {
|
|
1025
|
+
tagToKeys = /* @__PURE__ */ new Map();
|
|
1026
|
+
keyToTags = /* @__PURE__ */ new Map();
|
|
1027
|
+
knownKeys = /* @__PURE__ */ new Set();
|
|
1028
|
+
maxKnownKeys;
|
|
1029
|
+
constructor(options = {}) {
|
|
1030
|
+
this.maxKnownKeys = options.maxKnownKeys;
|
|
780
1031
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
1032
|
+
async touch(key) {
|
|
1033
|
+
this.knownKeys.add(key);
|
|
1034
|
+
this.pruneKnownKeysIfNeeded();
|
|
1035
|
+
}
|
|
1036
|
+
async track(key, tags) {
|
|
1037
|
+
this.knownKeys.add(key);
|
|
1038
|
+
this.pruneKnownKeysIfNeeded();
|
|
1039
|
+
if (tags.length === 0) {
|
|
1040
|
+
return;
|
|
788
1041
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
reject(e);
|
|
1042
|
+
const existingTags = this.keyToTags.get(key);
|
|
1043
|
+
if (existingTags) {
|
|
1044
|
+
for (const tag of existingTags) {
|
|
1045
|
+
this.tagToKeys.get(tag)?.delete(key);
|
|
794
1046
|
}
|
|
795
1047
|
}
|
|
796
|
-
|
|
797
|
-
|
|
1048
|
+
const tagSet = new Set(tags);
|
|
1049
|
+
this.keyToTags.set(key, tagSet);
|
|
1050
|
+
for (const tag of tagSet) {
|
|
1051
|
+
const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
|
|
1052
|
+
keys.add(key);
|
|
1053
|
+
this.tagToKeys.set(tag, keys);
|
|
798
1054
|
}
|
|
799
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
800
|
-
});
|
|
801
|
-
};
|
|
802
|
-
var Mutex = class {
|
|
803
|
-
constructor(cancelError) {
|
|
804
|
-
this._semaphore = new Semaphore(1, cancelError);
|
|
805
1055
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
const [, releaser] = yield this._semaphore.acquire();
|
|
809
|
-
return releaser;
|
|
810
|
-
});
|
|
1056
|
+
async remove(key) {
|
|
1057
|
+
this.removeKey(key);
|
|
811
1058
|
}
|
|
812
|
-
|
|
813
|
-
return this.
|
|
1059
|
+
async keysForTag(tag) {
|
|
1060
|
+
return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
|
|
814
1061
|
}
|
|
815
|
-
|
|
816
|
-
return this.
|
|
1062
|
+
async keysForPrefix(prefix) {
|
|
1063
|
+
return [...this.knownKeys].filter((key) => key.startsWith(prefix));
|
|
817
1064
|
}
|
|
818
|
-
|
|
819
|
-
return this.
|
|
1065
|
+
async tagsForKey(key) {
|
|
1066
|
+
return [...this.keyToTags.get(key) ?? /* @__PURE__ */ new Set()];
|
|
820
1067
|
}
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
this._semaphore.release();
|
|
1068
|
+
async matchPattern(pattern) {
|
|
1069
|
+
return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
|
|
824
1070
|
}
|
|
825
|
-
|
|
826
|
-
|
|
1071
|
+
async clear() {
|
|
1072
|
+
this.tagToKeys.clear();
|
|
1073
|
+
this.keyToTags.clear();
|
|
1074
|
+
this.knownKeys.clear();
|
|
1075
|
+
}
|
|
1076
|
+
pruneKnownKeysIfNeeded() {
|
|
1077
|
+
if (this.maxKnownKeys === void 0 || this.knownKeys.size <= this.maxKnownKeys) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const toRemove = Math.ceil(this.maxKnownKeys * 0.1);
|
|
1081
|
+
let removed = 0;
|
|
1082
|
+
for (const key of this.knownKeys) {
|
|
1083
|
+
if (removed >= toRemove) {
|
|
1084
|
+
break;
|
|
1085
|
+
}
|
|
1086
|
+
this.removeKey(key);
|
|
1087
|
+
removed += 1;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
removeKey(key) {
|
|
1091
|
+
this.knownKeys.delete(key);
|
|
1092
|
+
const tags = this.keyToTags.get(key);
|
|
1093
|
+
if (!tags) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
for (const tag of tags) {
|
|
1097
|
+
const keys = this.tagToKeys.get(tag);
|
|
1098
|
+
if (!keys) {
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
keys.delete(key);
|
|
1102
|
+
if (keys.size === 0) {
|
|
1103
|
+
this.tagToKeys.delete(tag);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
this.keyToTags.delete(key);
|
|
827
1107
|
}
|
|
828
1108
|
};
|
|
829
1109
|
|
|
@@ -905,6 +1185,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
905
1185
|
const maxProfileEntries = options.maxProfileEntries ?? DEFAULT_MAX_PROFILE_ENTRIES;
|
|
906
1186
|
this.ttlResolver = new TtlResolver({ maxProfileEntries });
|
|
907
1187
|
this.circuitBreakerManager = new CircuitBreakerManager({ maxEntries: maxProfileEntries });
|
|
1188
|
+
this.currentGeneration = options.generation;
|
|
908
1189
|
if (options.publishSetInvalidation !== void 0) {
|
|
909
1190
|
console.warn(
|
|
910
1191
|
"[layercache] CacheStackOptions.publishSetInvalidation is deprecated. Use broadcastL1Invalidation instead."
|
|
@@ -913,21 +1194,27 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
913
1194
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
914
1195
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
915
1196
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
1197
|
+
this.initializeWriteBehind(options.writeBehind);
|
|
916
1198
|
this.startup = this.initialize();
|
|
917
1199
|
}
|
|
918
1200
|
layers;
|
|
919
1201
|
options;
|
|
920
1202
|
stampedeGuard = new StampedeGuard();
|
|
921
1203
|
metricsCollector = new MetricsCollector();
|
|
922
|
-
instanceId = (
|
|
1204
|
+
instanceId = createInstanceId();
|
|
923
1205
|
startup;
|
|
924
1206
|
unsubscribeInvalidation;
|
|
925
1207
|
logger;
|
|
926
1208
|
tagIndex;
|
|
1209
|
+
fetchRateLimiter = new FetchRateLimiter();
|
|
927
1210
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
928
1211
|
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
929
1212
|
ttlResolver;
|
|
930
1213
|
circuitBreakerManager;
|
|
1214
|
+
currentGeneration;
|
|
1215
|
+
writeBehindQueue = [];
|
|
1216
|
+
writeBehindTimer;
|
|
1217
|
+
writeBehindFlushPromise;
|
|
931
1218
|
isDisconnecting = false;
|
|
932
1219
|
disconnectPromise;
|
|
933
1220
|
/**
|
|
@@ -937,9 +1224,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
937
1224
|
* and no `fetcher` is provided.
|
|
938
1225
|
*/
|
|
939
1226
|
async get(key, fetcher, options) {
|
|
940
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1227
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
941
1228
|
this.validateWriteOptions(options);
|
|
942
|
-
await this.
|
|
1229
|
+
await this.awaitStartup("get");
|
|
943
1230
|
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
944
1231
|
if (hit.found) {
|
|
945
1232
|
this.ttlResolver.recordAccess(normalizedKey);
|
|
@@ -1004,8 +1291,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1004
1291
|
* Returns true if the given key exists and is not expired in any layer.
|
|
1005
1292
|
*/
|
|
1006
1293
|
async has(key) {
|
|
1007
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1008
|
-
await this.
|
|
1294
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1295
|
+
await this.awaitStartup("has");
|
|
1009
1296
|
for (const layer of this.layers) {
|
|
1010
1297
|
if (this.shouldSkipLayer(layer)) {
|
|
1011
1298
|
continue;
|
|
@@ -1035,8 +1322,8 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1035
1322
|
* that has it, or null if the key is not found / has no TTL.
|
|
1036
1323
|
*/
|
|
1037
1324
|
async ttl(key) {
|
|
1038
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1039
|
-
await this.
|
|
1325
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1326
|
+
await this.awaitStartup("ttl");
|
|
1040
1327
|
for (const layer of this.layers) {
|
|
1041
1328
|
if (this.shouldSkipLayer(layer)) {
|
|
1042
1329
|
continue;
|
|
@@ -1057,17 +1344,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1057
1344
|
* Stores a value in all cache layers. Overwrites any existing value.
|
|
1058
1345
|
*/
|
|
1059
1346
|
async set(key, value, options) {
|
|
1060
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1347
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1061
1348
|
this.validateWriteOptions(options);
|
|
1062
|
-
await this.
|
|
1349
|
+
await this.awaitStartup("set");
|
|
1063
1350
|
await this.storeEntry(normalizedKey, "value", value, options);
|
|
1064
1351
|
}
|
|
1065
1352
|
/**
|
|
1066
1353
|
* Deletes the key from all layers and publishes an invalidation message.
|
|
1067
1354
|
*/
|
|
1068
1355
|
async delete(key) {
|
|
1069
|
-
const normalizedKey = this.validateCacheKey(key);
|
|
1070
|
-
await this.
|
|
1356
|
+
const normalizedKey = this.qualifyKey(this.validateCacheKey(key));
|
|
1357
|
+
await this.awaitStartup("delete");
|
|
1071
1358
|
await this.deleteKeys([normalizedKey]);
|
|
1072
1359
|
await this.publishInvalidation({
|
|
1073
1360
|
scope: "key",
|
|
@@ -1077,7 +1364,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1077
1364
|
});
|
|
1078
1365
|
}
|
|
1079
1366
|
async clear() {
|
|
1080
|
-
await this.
|
|
1367
|
+
await this.awaitStartup("clear");
|
|
1081
1368
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
1082
1369
|
await this.tagIndex.clear();
|
|
1083
1370
|
this.ttlResolver.clearProfiles();
|
|
@@ -1093,23 +1380,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1093
1380
|
if (keys.length === 0) {
|
|
1094
1381
|
return;
|
|
1095
1382
|
}
|
|
1096
|
-
await this.
|
|
1383
|
+
await this.awaitStartup("mdelete");
|
|
1097
1384
|
const normalizedKeys = keys.map((k) => this.validateCacheKey(k));
|
|
1098
|
-
|
|
1385
|
+
const cacheKeys = normalizedKeys.map((key) => this.qualifyKey(key));
|
|
1386
|
+
await this.deleteKeys(cacheKeys);
|
|
1099
1387
|
await this.publishInvalidation({
|
|
1100
1388
|
scope: "keys",
|
|
1101
|
-
keys:
|
|
1389
|
+
keys: cacheKeys,
|
|
1102
1390
|
sourceId: this.instanceId,
|
|
1103
1391
|
operation: "delete"
|
|
1104
1392
|
});
|
|
1105
1393
|
}
|
|
1106
1394
|
async mget(entries) {
|
|
1395
|
+
this.assertActive("mget");
|
|
1107
1396
|
if (entries.length === 0) {
|
|
1108
1397
|
return [];
|
|
1109
1398
|
}
|
|
1110
1399
|
const normalizedEntries = entries.map((entry) => ({
|
|
1111
1400
|
...entry,
|
|
1112
|
-
key: this.validateCacheKey(entry.key)
|
|
1401
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1113
1402
|
}));
|
|
1114
1403
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1115
1404
|
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
@@ -1135,7 +1424,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1135
1424
|
})
|
|
1136
1425
|
);
|
|
1137
1426
|
}
|
|
1138
|
-
await this.
|
|
1427
|
+
await this.awaitStartup("mget");
|
|
1139
1428
|
const pending = /* @__PURE__ */ new Set();
|
|
1140
1429
|
const indexesByKey = /* @__PURE__ */ new Map();
|
|
1141
1430
|
const resultsByKey = /* @__PURE__ */ new Map();
|
|
@@ -1183,14 +1472,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1183
1472
|
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
1184
1473
|
}
|
|
1185
1474
|
async mset(entries) {
|
|
1475
|
+
this.assertActive("mset");
|
|
1186
1476
|
const normalizedEntries = entries.map((entry) => ({
|
|
1187
1477
|
...entry,
|
|
1188
|
-
key: this.validateCacheKey(entry.key)
|
|
1478
|
+
key: this.qualifyKey(this.validateCacheKey(entry.key))
|
|
1189
1479
|
}));
|
|
1190
1480
|
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
1191
|
-
await
|
|
1481
|
+
await this.awaitStartup("mset");
|
|
1482
|
+
await this.writeBatch(normalizedEntries);
|
|
1192
1483
|
}
|
|
1193
1484
|
async warm(entries, options = {}) {
|
|
1485
|
+
this.assertActive("warm");
|
|
1194
1486
|
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
1195
1487
|
const total = entries.length;
|
|
1196
1488
|
let completed = 0;
|
|
@@ -1239,14 +1531,31 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1239
1531
|
return new CacheNamespace(this, prefix);
|
|
1240
1532
|
}
|
|
1241
1533
|
async invalidateByTag(tag) {
|
|
1242
|
-
await this.
|
|
1534
|
+
await this.awaitStartup("invalidateByTag");
|
|
1243
1535
|
const keys = await this.tagIndex.keysForTag(tag);
|
|
1244
1536
|
await this.deleteKeys(keys);
|
|
1245
1537
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1246
1538
|
}
|
|
1539
|
+
async invalidateByTags(tags, mode = "any") {
|
|
1540
|
+
if (tags.length === 0) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
await this.awaitStartup("invalidateByTags");
|
|
1544
|
+
const keysByTag = await Promise.all(tags.map((tag) => this.tagIndex.keysForTag(tag)));
|
|
1545
|
+
const keys = mode === "all" ? this.intersectKeys(keysByTag) : [...new Set(keysByTag.flat())];
|
|
1546
|
+
await this.deleteKeys(keys);
|
|
1547
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1548
|
+
}
|
|
1247
1549
|
async invalidateByPattern(pattern) {
|
|
1248
|
-
await this.
|
|
1249
|
-
const keys = await this.tagIndex.matchPattern(pattern);
|
|
1550
|
+
await this.awaitStartup("invalidateByPattern");
|
|
1551
|
+
const keys = await this.tagIndex.matchPattern(this.qualifyPattern(pattern));
|
|
1552
|
+
await this.deleteKeys(keys);
|
|
1553
|
+
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1554
|
+
}
|
|
1555
|
+
async invalidateByPrefix(prefix) {
|
|
1556
|
+
await this.awaitStartup("invalidateByPrefix");
|
|
1557
|
+
const qualifiedPrefix = this.qualifyKey(this.validateCacheKey(prefix));
|
|
1558
|
+
const keys = this.tagIndex.keysForPrefix ? await this.tagIndex.keysForPrefix(qualifiedPrefix) : await this.tagIndex.matchPattern(`${qualifiedPrefix}*`);
|
|
1250
1559
|
await this.deleteKeys(keys);
|
|
1251
1560
|
await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
|
|
1252
1561
|
}
|
|
@@ -1273,14 +1582,43 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1273
1582
|
getHitRate() {
|
|
1274
1583
|
return this.metricsCollector.hitRate();
|
|
1275
1584
|
}
|
|
1585
|
+
async healthCheck() {
|
|
1586
|
+
await this.startup;
|
|
1587
|
+
return Promise.all(
|
|
1588
|
+
this.layers.map(async (layer) => {
|
|
1589
|
+
const startedAt = performance.now();
|
|
1590
|
+
try {
|
|
1591
|
+
const healthy = layer.ping ? await layer.ping() : true;
|
|
1592
|
+
return {
|
|
1593
|
+
layer: layer.name,
|
|
1594
|
+
healthy,
|
|
1595
|
+
latencyMs: performance.now() - startedAt
|
|
1596
|
+
};
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
return {
|
|
1599
|
+
layer: layer.name,
|
|
1600
|
+
healthy: false,
|
|
1601
|
+
latencyMs: performance.now() - startedAt,
|
|
1602
|
+
error: this.formatError(error)
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
})
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
bumpGeneration(nextGeneration) {
|
|
1609
|
+
const current = this.currentGeneration ?? 0;
|
|
1610
|
+
this.currentGeneration = nextGeneration ?? current + 1;
|
|
1611
|
+
return this.currentGeneration;
|
|
1612
|
+
}
|
|
1276
1613
|
/**
|
|
1277
1614
|
* Returns detailed metadata about a single cache key: which layers contain it,
|
|
1278
1615
|
* remaining fresh/stale/error TTLs, and associated tags.
|
|
1279
1616
|
* Returns `null` if the key does not exist in any layer.
|
|
1280
1617
|
*/
|
|
1281
1618
|
async inspect(key) {
|
|
1282
|
-
const
|
|
1283
|
-
|
|
1619
|
+
const userKey = this.validateCacheKey(key);
|
|
1620
|
+
const normalizedKey = this.qualifyKey(userKey);
|
|
1621
|
+
await this.awaitStartup("inspect");
|
|
1284
1622
|
const foundInLayers = [];
|
|
1285
1623
|
let freshTtlSeconds = null;
|
|
1286
1624
|
let staleTtlSeconds = null;
|
|
@@ -1311,10 +1649,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1311
1649
|
return null;
|
|
1312
1650
|
}
|
|
1313
1651
|
const tags = await this.getTagsForKey(normalizedKey);
|
|
1314
|
-
return { key:
|
|
1652
|
+
return { key: userKey, foundInLayers, freshTtlSeconds, staleTtlSeconds, errorTtlSeconds, isStale, tags };
|
|
1315
1653
|
}
|
|
1316
1654
|
async exportState() {
|
|
1317
|
-
await this.
|
|
1655
|
+
await this.awaitStartup("exportState");
|
|
1318
1656
|
const exported = /* @__PURE__ */ new Map();
|
|
1319
1657
|
for (const layer of this.layers) {
|
|
1320
1658
|
if (!layer.keys) {
|
|
@@ -1322,15 +1660,16 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1322
1660
|
}
|
|
1323
1661
|
const keys = await layer.keys();
|
|
1324
1662
|
for (const key of keys) {
|
|
1325
|
-
|
|
1663
|
+
const exportedKey = this.stripQualifiedKey(key);
|
|
1664
|
+
if (exported.has(exportedKey)) {
|
|
1326
1665
|
continue;
|
|
1327
1666
|
}
|
|
1328
1667
|
const stored = await this.readLayerEntry(layer, key);
|
|
1329
1668
|
if (stored === null) {
|
|
1330
1669
|
continue;
|
|
1331
1670
|
}
|
|
1332
|
-
exported.set(
|
|
1333
|
-
key,
|
|
1671
|
+
exported.set(exportedKey, {
|
|
1672
|
+
key: exportedKey,
|
|
1334
1673
|
value: stored,
|
|
1335
1674
|
ttl: remainingStoredTtlSeconds(stored)
|
|
1336
1675
|
});
|
|
@@ -1339,20 +1678,25 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1339
1678
|
return [...exported.values()];
|
|
1340
1679
|
}
|
|
1341
1680
|
async importState(entries) {
|
|
1342
|
-
await this.
|
|
1681
|
+
await this.awaitStartup("importState");
|
|
1343
1682
|
await Promise.all(
|
|
1344
1683
|
entries.map(async (entry) => {
|
|
1345
|
-
|
|
1346
|
-
await this.
|
|
1684
|
+
const qualifiedKey = this.qualifyKey(entry.key);
|
|
1685
|
+
await Promise.all(this.layers.map((layer) => layer.set(qualifiedKey, entry.value, entry.ttl)));
|
|
1686
|
+
await this.tagIndex.touch(qualifiedKey);
|
|
1347
1687
|
})
|
|
1348
1688
|
);
|
|
1349
1689
|
}
|
|
1350
1690
|
async persistToFile(filePath) {
|
|
1691
|
+
this.assertActive("persistToFile");
|
|
1351
1692
|
const snapshot = await this.exportState();
|
|
1352
|
-
|
|
1693
|
+
const { promises: fs } = await import("fs");
|
|
1694
|
+
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
1353
1695
|
}
|
|
1354
1696
|
async restoreFromFile(filePath) {
|
|
1355
|
-
|
|
1697
|
+
this.assertActive("restoreFromFile");
|
|
1698
|
+
const { promises: fs } = await import("fs");
|
|
1699
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
1356
1700
|
let parsed;
|
|
1357
1701
|
try {
|
|
1358
1702
|
parsed = JSON.parse(raw, (_key, value) => {
|
|
@@ -1375,7 +1719,13 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1375
1719
|
this.disconnectPromise = (async () => {
|
|
1376
1720
|
await this.startup;
|
|
1377
1721
|
await this.unsubscribeInvalidation?.();
|
|
1722
|
+
await this.flushWriteBehindQueue();
|
|
1378
1723
|
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
1724
|
+
if (this.writeBehindTimer) {
|
|
1725
|
+
clearInterval(this.writeBehindTimer);
|
|
1726
|
+
this.writeBehindTimer = void 0;
|
|
1727
|
+
}
|
|
1728
|
+
await Promise.allSettled(this.layers.map((layer) => layer.dispose?.() ?? Promise.resolve()));
|
|
1379
1729
|
})();
|
|
1380
1730
|
}
|
|
1381
1731
|
await this.disconnectPromise;
|
|
@@ -1435,7 +1785,10 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1435
1785
|
const fetchStart = Date.now();
|
|
1436
1786
|
let fetched;
|
|
1437
1787
|
try {
|
|
1438
|
-
fetched = await
|
|
1788
|
+
fetched = await this.fetchRateLimiter.schedule(
|
|
1789
|
+
options?.fetcherRateLimit ?? this.options.fetcherRateLimit,
|
|
1790
|
+
fetcher
|
|
1791
|
+
);
|
|
1439
1792
|
this.circuitBreakerManager.recordSuccess(key);
|
|
1440
1793
|
this.logger.debug?.("fetch", { key, durationMs: Date.now() - fetchStart });
|
|
1441
1794
|
} catch (error) {
|
|
@@ -1469,6 +1822,61 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1469
1822
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
1470
1823
|
}
|
|
1471
1824
|
}
|
|
1825
|
+
async writeBatch(entries) {
|
|
1826
|
+
const now = Date.now();
|
|
1827
|
+
const entriesByLayer = /* @__PURE__ */ new Map();
|
|
1828
|
+
const immediateOperations = [];
|
|
1829
|
+
const deferredOperations = [];
|
|
1830
|
+
for (const entry of entries) {
|
|
1831
|
+
for (const layer of this.layers) {
|
|
1832
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1835
|
+
const layerEntry = this.buildLayerSetEntry(layer, entry.key, "value", entry.value, entry.options, now);
|
|
1836
|
+
const bucket = entriesByLayer.get(layer) ?? [];
|
|
1837
|
+
bucket.push(layerEntry);
|
|
1838
|
+
entriesByLayer.set(layer, bucket);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
for (const [layer, layerEntries] of entriesByLayer.entries()) {
|
|
1842
|
+
const operation = async () => {
|
|
1843
|
+
try {
|
|
1844
|
+
if (layer.setMany) {
|
|
1845
|
+
await layer.setMany(layerEntries);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
await Promise.all(layerEntries.map((entry) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
1849
|
+
} catch (error) {
|
|
1850
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1854
|
+
deferredOperations.push(operation);
|
|
1855
|
+
} else {
|
|
1856
|
+
immediateOperations.push(operation);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
await this.executeLayerOperations(immediateOperations, { key: "batch", action: "mset" });
|
|
1860
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1861
|
+
for (const entry of entries) {
|
|
1862
|
+
if (entry.options?.tags) {
|
|
1863
|
+
await this.tagIndex.track(entry.key, entry.options.tags);
|
|
1864
|
+
} else {
|
|
1865
|
+
await this.tagIndex.touch(entry.key);
|
|
1866
|
+
}
|
|
1867
|
+
this.metricsCollector.increment("sets");
|
|
1868
|
+
this.logger.debug?.("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1869
|
+
this.emit("set", { key: entry.key, kind: "value", tags: entry.options?.tags });
|
|
1870
|
+
}
|
|
1871
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
1872
|
+
await this.publishInvalidation({
|
|
1873
|
+
scope: "keys",
|
|
1874
|
+
keys: entries.map((entry) => entry.key),
|
|
1875
|
+
sourceId: this.instanceId,
|
|
1876
|
+
operation: "write"
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1472
1880
|
async readFromLayers(key, options, mode) {
|
|
1473
1881
|
let sawRetainableValue = false;
|
|
1474
1882
|
for (let index = 0; index < this.layers.length; index += 1) {
|
|
@@ -1552,33 +1960,28 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1552
1960
|
}
|
|
1553
1961
|
async writeAcrossLayers(key, kind, value, options) {
|
|
1554
1962
|
const now = Date.now();
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
options
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
});
|
|
1574
|
-
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
1575
|
-
try {
|
|
1576
|
-
await layer.set(key, payload, ttl);
|
|
1577
|
-
} catch (error) {
|
|
1578
|
-
await this.handleLayerFailure(layer, "write", error);
|
|
1963
|
+
const immediateOperations = [];
|
|
1964
|
+
const deferredOperations = [];
|
|
1965
|
+
for (const layer of this.layers) {
|
|
1966
|
+
const operation = async () => {
|
|
1967
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const entry = this.buildLayerSetEntry(layer, key, kind, value, options, now);
|
|
1971
|
+
try {
|
|
1972
|
+
await layer.set(entry.key, entry.value, entry.ttl);
|
|
1973
|
+
} catch (error) {
|
|
1974
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
1975
|
+
}
|
|
1976
|
+
};
|
|
1977
|
+
if (this.shouldWriteBehind(layer)) {
|
|
1978
|
+
deferredOperations.push(operation);
|
|
1979
|
+
} else {
|
|
1980
|
+
immediateOperations.push(operation);
|
|
1579
1981
|
}
|
|
1580
|
-
}
|
|
1581
|
-
await this.executeLayerOperations(
|
|
1982
|
+
}
|
|
1983
|
+
await this.executeLayerOperations(immediateOperations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
1984
|
+
await Promise.all(deferredOperations.map((operation) => this.enqueueWriteBehind(operation)));
|
|
1582
1985
|
}
|
|
1583
1986
|
async executeLayerOperations(operations, context) {
|
|
1584
1987
|
if (this.options.writePolicy !== "best-effort") {
|
|
@@ -1602,8 +2005,17 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1602
2005
|
);
|
|
1603
2006
|
}
|
|
1604
2007
|
}
|
|
1605
|
-
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
1606
|
-
return this.ttlResolver.resolveFreshTtl(
|
|
2008
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl, value) {
|
|
2009
|
+
return this.ttlResolver.resolveFreshTtl(
|
|
2010
|
+
key,
|
|
2011
|
+
layerName,
|
|
2012
|
+
kind,
|
|
2013
|
+
options,
|
|
2014
|
+
fallbackTtl,
|
|
2015
|
+
this.options.negativeTtl,
|
|
2016
|
+
void 0,
|
|
2017
|
+
value
|
|
2018
|
+
);
|
|
1607
2019
|
}
|
|
1608
2020
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
1609
2021
|
return this.ttlResolver.resolveLayerSeconds(layerName, override, globalDefault, fallback);
|
|
@@ -1697,6 +2109,105 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1697
2109
|
shouldBroadcastL1Invalidation() {
|
|
1698
2110
|
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
1699
2111
|
}
|
|
2112
|
+
initializeWriteBehind(options) {
|
|
2113
|
+
if (this.options.writeStrategy !== "write-behind") {
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const flushIntervalMs = options?.flushIntervalMs;
|
|
2117
|
+
if (!flushIntervalMs || flushIntervalMs <= 0) {
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
this.writeBehindTimer = setInterval(() => {
|
|
2121
|
+
void this.flushWriteBehindQueue();
|
|
2122
|
+
}, flushIntervalMs);
|
|
2123
|
+
this.writeBehindTimer.unref?.();
|
|
2124
|
+
}
|
|
2125
|
+
shouldWriteBehind(layer) {
|
|
2126
|
+
return this.options.writeStrategy === "write-behind" && !layer.isLocal;
|
|
2127
|
+
}
|
|
2128
|
+
async enqueueWriteBehind(operation) {
|
|
2129
|
+
this.writeBehindQueue.push(operation);
|
|
2130
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2131
|
+
const maxQueueSize = this.options.writeBehind?.maxQueueSize ?? batchSize * 10;
|
|
2132
|
+
if (this.writeBehindQueue.length >= batchSize) {
|
|
2133
|
+
await this.flushWriteBehindQueue();
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
if (this.writeBehindQueue.length >= maxQueueSize) {
|
|
2137
|
+
await this.flushWriteBehindQueue();
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
async flushWriteBehindQueue() {
|
|
2141
|
+
if (this.writeBehindFlushPromise || this.writeBehindQueue.length === 0) {
|
|
2142
|
+
await this.writeBehindFlushPromise;
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
const batchSize = this.options.writeBehind?.batchSize ?? 100;
|
|
2146
|
+
const batch = this.writeBehindQueue.splice(0, batchSize);
|
|
2147
|
+
this.writeBehindFlushPromise = (async () => {
|
|
2148
|
+
await Promise.allSettled(batch.map((operation) => operation()));
|
|
2149
|
+
})();
|
|
2150
|
+
await this.writeBehindFlushPromise;
|
|
2151
|
+
this.writeBehindFlushPromise = void 0;
|
|
2152
|
+
if (this.writeBehindQueue.length > 0) {
|
|
2153
|
+
await this.flushWriteBehindQueue();
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
buildLayerSetEntry(layer, key, kind, value, options, now) {
|
|
2157
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl, value);
|
|
2158
|
+
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
2159
|
+
layer.name,
|
|
2160
|
+
options?.staleWhileRevalidate,
|
|
2161
|
+
this.options.staleWhileRevalidate
|
|
2162
|
+
);
|
|
2163
|
+
const staleIfError = this.resolveLayerSeconds(layer.name, options?.staleIfError, this.options.staleIfError);
|
|
2164
|
+
const payload = createStoredValueEnvelope({
|
|
2165
|
+
kind,
|
|
2166
|
+
value,
|
|
2167
|
+
freshTtlSeconds: freshTtl,
|
|
2168
|
+
staleWhileRevalidateSeconds: staleWhileRevalidate,
|
|
2169
|
+
staleIfErrorSeconds: staleIfError,
|
|
2170
|
+
now
|
|
2171
|
+
});
|
|
2172
|
+
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
2173
|
+
return {
|
|
2174
|
+
key,
|
|
2175
|
+
value: payload,
|
|
2176
|
+
ttl
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
intersectKeys(groups) {
|
|
2180
|
+
if (groups.length === 0) {
|
|
2181
|
+
return [];
|
|
2182
|
+
}
|
|
2183
|
+
const [firstGroup, ...rest] = groups;
|
|
2184
|
+
if (!firstGroup) {
|
|
2185
|
+
return [];
|
|
2186
|
+
}
|
|
2187
|
+
const restSets = rest.map((group) => new Set(group));
|
|
2188
|
+
return [...new Set(firstGroup)].filter((key) => restSets.every((group) => group.has(key)));
|
|
2189
|
+
}
|
|
2190
|
+
qualifyKey(key) {
|
|
2191
|
+
const prefix = this.generationPrefix();
|
|
2192
|
+
return prefix ? `${prefix}${key}` : key;
|
|
2193
|
+
}
|
|
2194
|
+
qualifyPattern(pattern) {
|
|
2195
|
+
const prefix = this.generationPrefix();
|
|
2196
|
+
return prefix ? `${prefix}${pattern}` : pattern;
|
|
2197
|
+
}
|
|
2198
|
+
stripQualifiedKey(key) {
|
|
2199
|
+
const prefix = this.generationPrefix();
|
|
2200
|
+
if (!prefix || !key.startsWith(prefix)) {
|
|
2201
|
+
return key;
|
|
2202
|
+
}
|
|
2203
|
+
return key.slice(prefix.length);
|
|
2204
|
+
}
|
|
2205
|
+
generationPrefix() {
|
|
2206
|
+
if (this.currentGeneration === void 0) {
|
|
2207
|
+
return "";
|
|
2208
|
+
}
|
|
2209
|
+
return `v${this.currentGeneration}:`;
|
|
2210
|
+
}
|
|
1700
2211
|
async deleteKeysFromLayers(layers, keys) {
|
|
1701
2212
|
await Promise.all(
|
|
1702
2213
|
layers.map(async (layer) => {
|
|
@@ -1740,6 +2251,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1740
2251
|
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1741
2252
|
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1742
2253
|
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
2254
|
+
if (this.options.generation !== void 0) {
|
|
2255
|
+
this.validateNonNegativeNumber("generation", this.options.generation);
|
|
2256
|
+
}
|
|
1743
2257
|
}
|
|
1744
2258
|
validateWriteOptions(options) {
|
|
1745
2259
|
if (!options) {
|
|
@@ -1751,6 +2265,7 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1751
2265
|
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1752
2266
|
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1753
2267
|
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
2268
|
+
this.validateTtlPolicy("options.ttlPolicy", options.ttlPolicy);
|
|
1754
2269
|
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1755
2270
|
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1756
2271
|
}
|
|
@@ -1794,6 +2309,26 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1794
2309
|
}
|
|
1795
2310
|
return key;
|
|
1796
2311
|
}
|
|
2312
|
+
validateTtlPolicy(name, policy) {
|
|
2313
|
+
if (!policy || typeof policy === "function" || policy === "until-midnight" || policy === "next-hour") {
|
|
2314
|
+
return;
|
|
2315
|
+
}
|
|
2316
|
+
if ("alignTo" in policy) {
|
|
2317
|
+
this.validatePositiveNumber(`${name}.alignTo`, policy.alignTo);
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
throw new Error(`${name} is invalid.`);
|
|
2321
|
+
}
|
|
2322
|
+
assertActive(operation) {
|
|
2323
|
+
if (this.isDisconnecting) {
|
|
2324
|
+
throw new Error(`CacheStack is disconnecting; cannot perform ${operation}.`);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
async awaitStartup(operation) {
|
|
2328
|
+
this.assertActive(operation);
|
|
2329
|
+
await this.startup;
|
|
2330
|
+
this.assertActive(operation);
|
|
2331
|
+
}
|
|
1797
2332
|
serializeOptions(options) {
|
|
1798
2333
|
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1799
2334
|
}
|
|
@@ -1899,6 +2434,9 @@ var CacheStack = class extends import_node_events.EventEmitter {
|
|
|
1899
2434
|
return value;
|
|
1900
2435
|
}
|
|
1901
2436
|
};
|
|
2437
|
+
function createInstanceId() {
|
|
2438
|
+
return globalThis.crypto?.randomUUID?.() ?? `layercache-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
2439
|
+
}
|
|
1902
2440
|
|
|
1903
2441
|
// src/module.ts
|
|
1904
2442
|
var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
|