layercache 1.0.1 → 1.0.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 +286 -7
- package/dist/chunk-IILH5XTS.js +103 -0
- package/dist/cli.cjs +228 -0
- package/dist/cli.d.cts +4 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +96 -0
- package/dist/index.cjs +833 -95
- package/dist/index.d.cts +182 -4
- package/dist/index.d.ts +182 -4
- package/dist/index.js +821 -183
- package/package.json +5 -2
- package/packages/nestjs/dist/index.cjs +652 -81
- package/packages/nestjs/dist/index.d.cts +204 -2
- package/packages/nestjs/dist/index.d.ts +204 -2
- package/packages/nestjs/dist/index.js +651 -81
package/dist/index.js
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PatternMatcher,
|
|
3
|
+
RedisTagIndex
|
|
4
|
+
} from "./chunk-IILH5XTS.js";
|
|
5
|
+
|
|
1
6
|
// src/CacheStack.ts
|
|
2
7
|
import { randomUUID } from "crypto";
|
|
8
|
+
import { promises as fs } from "fs";
|
|
9
|
+
import { EventEmitter } from "events";
|
|
3
10
|
|
|
4
11
|
// src/internal/StoredValue.ts
|
|
5
12
|
function isStoredValueEnvelope(value) {
|
|
@@ -19,7 +26,10 @@ function createStoredValueEnvelope(options) {
|
|
|
19
26
|
value: options.value,
|
|
20
27
|
freshUntil,
|
|
21
28
|
staleUntil,
|
|
22
|
-
errorUntil
|
|
29
|
+
errorUntil,
|
|
30
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
31
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
32
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
23
33
|
};
|
|
24
34
|
}
|
|
25
35
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -60,6 +70,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
60
70
|
}
|
|
61
71
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
62
72
|
}
|
|
73
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
74
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
const remainingMs = stored.freshUntil - now;
|
|
78
|
+
if (remainingMs <= 0) {
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
82
|
+
}
|
|
83
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
84
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
85
|
+
return stored;
|
|
86
|
+
}
|
|
87
|
+
return createStoredValueEnvelope({
|
|
88
|
+
kind: stored.kind,
|
|
89
|
+
value: stored.value,
|
|
90
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
91
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
92
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
93
|
+
now
|
|
94
|
+
});
|
|
95
|
+
}
|
|
63
96
|
function maxExpiry(stored) {
|
|
64
97
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
65
98
|
(value) => value !== null
|
|
@@ -76,12 +109,58 @@ function normalizePositiveSeconds(value) {
|
|
|
76
109
|
return value;
|
|
77
110
|
}
|
|
78
111
|
|
|
79
|
-
// src/
|
|
80
|
-
var
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
112
|
+
// src/CacheNamespace.ts
|
|
113
|
+
var CacheNamespace = class {
|
|
114
|
+
constructor(cache, prefix) {
|
|
115
|
+
this.cache = cache;
|
|
116
|
+
this.prefix = prefix;
|
|
117
|
+
}
|
|
118
|
+
cache;
|
|
119
|
+
prefix;
|
|
120
|
+
async get(key, fetcher, options) {
|
|
121
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
122
|
+
}
|
|
123
|
+
async set(key, value, options) {
|
|
124
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
125
|
+
}
|
|
126
|
+
async delete(key) {
|
|
127
|
+
await this.cache.delete(this.qualify(key));
|
|
128
|
+
}
|
|
129
|
+
async clear() {
|
|
130
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
131
|
+
}
|
|
132
|
+
async mget(entries) {
|
|
133
|
+
return this.cache.mget(entries.map((entry) => ({
|
|
134
|
+
...entry,
|
|
135
|
+
key: this.qualify(entry.key)
|
|
136
|
+
})));
|
|
137
|
+
}
|
|
138
|
+
async mset(entries) {
|
|
139
|
+
await this.cache.mset(entries.map((entry) => ({
|
|
140
|
+
...entry,
|
|
141
|
+
key: this.qualify(entry.key)
|
|
142
|
+
})));
|
|
143
|
+
}
|
|
144
|
+
async invalidateByTag(tag) {
|
|
145
|
+
await this.cache.invalidateByTag(tag);
|
|
146
|
+
}
|
|
147
|
+
async invalidateByPattern(pattern) {
|
|
148
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
149
|
+
}
|
|
150
|
+
wrap(keyPrefix, fetcher, options) {
|
|
151
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
152
|
+
}
|
|
153
|
+
warm(entries, options) {
|
|
154
|
+
return this.cache.warm(entries.map((entry) => ({
|
|
155
|
+
...entry,
|
|
156
|
+
key: this.qualify(entry.key)
|
|
157
|
+
})), options);
|
|
158
|
+
}
|
|
159
|
+
getMetrics() {
|
|
160
|
+
return this.cache.getMetrics();
|
|
161
|
+
}
|
|
162
|
+
qualify(key) {
|
|
163
|
+
return `${this.prefix}:${key}`;
|
|
85
164
|
}
|
|
86
165
|
};
|
|
87
166
|
|
|
@@ -148,22 +227,24 @@ import { Mutex } from "async-mutex";
|
|
|
148
227
|
var StampedeGuard = class {
|
|
149
228
|
mutexes = /* @__PURE__ */ new Map();
|
|
150
229
|
async execute(key, task) {
|
|
151
|
-
const
|
|
230
|
+
const entry = this.getMutexEntry(key);
|
|
152
231
|
try {
|
|
153
|
-
return await mutex.runExclusive(task);
|
|
232
|
+
return await entry.mutex.runExclusive(task);
|
|
154
233
|
} finally {
|
|
155
|
-
|
|
234
|
+
entry.references -= 1;
|
|
235
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
156
236
|
this.mutexes.delete(key);
|
|
157
237
|
}
|
|
158
238
|
}
|
|
159
239
|
}
|
|
160
|
-
|
|
161
|
-
let
|
|
162
|
-
if (!
|
|
163
|
-
|
|
164
|
-
this.mutexes.set(key,
|
|
240
|
+
getMutexEntry(key) {
|
|
241
|
+
let entry = this.mutexes.get(key);
|
|
242
|
+
if (!entry) {
|
|
243
|
+
entry = { mutex: new Mutex(), references: 0 };
|
|
244
|
+
this.mutexes.set(key, entry);
|
|
165
245
|
}
|
|
166
|
-
|
|
246
|
+
entry.references += 1;
|
|
247
|
+
return entry;
|
|
167
248
|
}
|
|
168
249
|
};
|
|
169
250
|
|
|
@@ -172,6 +253,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
|
172
253
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
173
254
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
174
255
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
256
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
175
257
|
var EMPTY_METRICS = () => ({
|
|
176
258
|
hits: 0,
|
|
177
259
|
misses: 0,
|
|
@@ -184,7 +266,12 @@ var EMPTY_METRICS = () => ({
|
|
|
184
266
|
refreshes: 0,
|
|
185
267
|
refreshErrors: 0,
|
|
186
268
|
writeFailures: 0,
|
|
187
|
-
singleFlightWaits: 0
|
|
269
|
+
singleFlightWaits: 0,
|
|
270
|
+
negativeCacheHits: 0,
|
|
271
|
+
circuitBreakerTrips: 0,
|
|
272
|
+
degradedOperations: 0,
|
|
273
|
+
hitsByLayer: {},
|
|
274
|
+
missesByLayer: {}
|
|
188
275
|
});
|
|
189
276
|
var DebugLogger = class {
|
|
190
277
|
enabled;
|
|
@@ -192,20 +279,34 @@ var DebugLogger = class {
|
|
|
192
279
|
this.enabled = enabled;
|
|
193
280
|
}
|
|
194
281
|
debug(message, context) {
|
|
282
|
+
this.write("debug", message, context);
|
|
283
|
+
}
|
|
284
|
+
info(message, context) {
|
|
285
|
+
this.write("info", message, context);
|
|
286
|
+
}
|
|
287
|
+
warn(message, context) {
|
|
288
|
+
this.write("warn", message, context);
|
|
289
|
+
}
|
|
290
|
+
error(message, context) {
|
|
291
|
+
this.write("error", message, context);
|
|
292
|
+
}
|
|
293
|
+
write(level, message, context) {
|
|
195
294
|
if (!this.enabled) {
|
|
196
295
|
return;
|
|
197
296
|
}
|
|
198
297
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
199
|
-
console
|
|
298
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
200
299
|
}
|
|
201
300
|
};
|
|
202
|
-
var CacheStack = class {
|
|
301
|
+
var CacheStack = class extends EventEmitter {
|
|
203
302
|
constructor(layers, options = {}) {
|
|
303
|
+
super();
|
|
204
304
|
this.layers = layers;
|
|
205
305
|
this.options = options;
|
|
206
306
|
if (layers.length === 0) {
|
|
207
307
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
208
308
|
}
|
|
309
|
+
this.validateConfiguration();
|
|
209
310
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
210
311
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
211
312
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -221,33 +322,47 @@ var CacheStack = class {
|
|
|
221
322
|
logger;
|
|
222
323
|
tagIndex;
|
|
223
324
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
325
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
326
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
327
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
328
|
+
isDisconnecting = false;
|
|
329
|
+
disconnectPromise;
|
|
224
330
|
async get(key, fetcher, options) {
|
|
331
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
332
|
+
this.validateWriteOptions(options);
|
|
225
333
|
await this.startup;
|
|
226
|
-
const hit = await this.readFromLayers(
|
|
334
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
227
335
|
if (hit.found) {
|
|
336
|
+
this.recordAccess(normalizedKey);
|
|
337
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
338
|
+
this.metrics.negativeCacheHits += 1;
|
|
339
|
+
}
|
|
228
340
|
if (hit.state === "fresh") {
|
|
229
341
|
this.metrics.hits += 1;
|
|
342
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
230
343
|
return hit.value;
|
|
231
344
|
}
|
|
232
345
|
if (hit.state === "stale-while-revalidate") {
|
|
233
346
|
this.metrics.hits += 1;
|
|
234
347
|
this.metrics.staleHits += 1;
|
|
348
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
235
349
|
if (fetcher) {
|
|
236
|
-
this.scheduleBackgroundRefresh(
|
|
350
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
237
351
|
}
|
|
238
352
|
return hit.value;
|
|
239
353
|
}
|
|
240
354
|
if (!fetcher) {
|
|
241
355
|
this.metrics.hits += 1;
|
|
242
356
|
this.metrics.staleHits += 1;
|
|
357
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
243
358
|
return hit.value;
|
|
244
359
|
}
|
|
245
360
|
try {
|
|
246
|
-
return await this.fetchWithGuards(
|
|
361
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
247
362
|
} catch (error) {
|
|
248
363
|
this.metrics.staleHits += 1;
|
|
249
364
|
this.metrics.refreshErrors += 1;
|
|
250
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
365
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
251
366
|
return hit.value;
|
|
252
367
|
}
|
|
253
368
|
}
|
|
@@ -255,71 +370,144 @@ var CacheStack = class {
|
|
|
255
370
|
if (!fetcher) {
|
|
256
371
|
return null;
|
|
257
372
|
}
|
|
258
|
-
return this.fetchWithGuards(
|
|
373
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
259
374
|
}
|
|
260
375
|
async set(key, value, options) {
|
|
376
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
377
|
+
this.validateWriteOptions(options);
|
|
261
378
|
await this.startup;
|
|
262
|
-
await this.storeEntry(
|
|
379
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
263
380
|
}
|
|
264
381
|
async delete(key) {
|
|
382
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
265
383
|
await this.startup;
|
|
266
|
-
await this.deleteKeys([
|
|
267
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
384
|
+
await this.deleteKeys([normalizedKey]);
|
|
385
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
268
386
|
}
|
|
269
387
|
async clear() {
|
|
270
388
|
await this.startup;
|
|
271
389
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
272
390
|
await this.tagIndex.clear();
|
|
391
|
+
this.accessProfiles.clear();
|
|
273
392
|
this.metrics.invalidations += 1;
|
|
274
|
-
this.logger.debug("clear");
|
|
393
|
+
this.logger.debug?.("clear");
|
|
275
394
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
276
395
|
}
|
|
277
396
|
async mget(entries) {
|
|
278
397
|
if (entries.length === 0) {
|
|
279
398
|
return [];
|
|
280
399
|
}
|
|
281
|
-
const
|
|
400
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
401
|
+
...entry,
|
|
402
|
+
key: this.validateCacheKey(entry.key)
|
|
403
|
+
}));
|
|
404
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
405
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
282
406
|
if (!canFastPath) {
|
|
283
|
-
|
|
407
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
408
|
+
return Promise.all(
|
|
409
|
+
normalizedEntries.map((entry) => {
|
|
410
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
411
|
+
const existing = pendingReads.get(entry.key);
|
|
412
|
+
if (!existing) {
|
|
413
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
414
|
+
pendingReads.set(entry.key, {
|
|
415
|
+
promise,
|
|
416
|
+
fetch: entry.fetch,
|
|
417
|
+
optionsSignature
|
|
418
|
+
});
|
|
419
|
+
return promise;
|
|
420
|
+
}
|
|
421
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
422
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
423
|
+
}
|
|
424
|
+
return existing.promise;
|
|
425
|
+
})
|
|
426
|
+
);
|
|
284
427
|
}
|
|
285
428
|
await this.startup;
|
|
286
|
-
const pending = new Set(
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
429
|
+
const pending = /* @__PURE__ */ new Set();
|
|
430
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
431
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
432
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
433
|
+
const key = normalizedEntries[index].key;
|
|
434
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
435
|
+
indexes.push(index);
|
|
436
|
+
indexesByKey.set(key, indexes);
|
|
437
|
+
pending.add(key);
|
|
438
|
+
}
|
|
439
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
440
|
+
const layer = this.layers[layerIndex];
|
|
441
|
+
const keys = [...pending];
|
|
442
|
+
if (keys.length === 0) {
|
|
291
443
|
break;
|
|
292
444
|
}
|
|
293
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
294
445
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
295
446
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
296
|
-
const
|
|
447
|
+
const key = keys[offset];
|
|
297
448
|
const stored = values[offset];
|
|
298
449
|
if (stored === null) {
|
|
299
450
|
continue;
|
|
300
451
|
}
|
|
301
452
|
const resolved = resolveStoredValue(stored);
|
|
302
453
|
if (resolved.state === "expired") {
|
|
303
|
-
await layer.delete(
|
|
454
|
+
await layer.delete(key);
|
|
304
455
|
continue;
|
|
305
456
|
}
|
|
306
|
-
await this.tagIndex.touch(
|
|
307
|
-
await this.backfill(
|
|
308
|
-
|
|
309
|
-
pending.delete(
|
|
310
|
-
this.metrics.hits += 1;
|
|
457
|
+
await this.tagIndex.touch(key);
|
|
458
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
459
|
+
resultsByKey.set(key, resolved.value);
|
|
460
|
+
pending.delete(key);
|
|
461
|
+
this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
|
|
311
462
|
}
|
|
312
463
|
}
|
|
313
464
|
if (pending.size > 0) {
|
|
314
|
-
for (const
|
|
315
|
-
await this.tagIndex.remove(
|
|
316
|
-
this.metrics.misses += 1;
|
|
465
|
+
for (const key of pending) {
|
|
466
|
+
await this.tagIndex.remove(key);
|
|
467
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
317
468
|
}
|
|
318
469
|
}
|
|
319
|
-
return
|
|
470
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
320
471
|
}
|
|
321
472
|
async mset(entries) {
|
|
322
|
-
|
|
473
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
474
|
+
...entry,
|
|
475
|
+
key: this.validateCacheKey(entry.key)
|
|
476
|
+
}));
|
|
477
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
478
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
479
|
+
}
|
|
480
|
+
async warm(entries, options = {}) {
|
|
481
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
482
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
483
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
484
|
+
while (queue.length > 0) {
|
|
485
|
+
const entry = queue.shift();
|
|
486
|
+
if (!entry) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
491
|
+
this.emit("warm", { key: entry.key });
|
|
492
|
+
} catch (error) {
|
|
493
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
494
|
+
if (!options.continueOnError) {
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
await Promise.all(workers);
|
|
501
|
+
}
|
|
502
|
+
wrap(prefix, fetcher, options = {}) {
|
|
503
|
+
return (...args) => {
|
|
504
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
505
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
506
|
+
return this.get(key, () => fetcher(...args), options);
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
namespace(prefix) {
|
|
510
|
+
return new CacheNamespace(this, prefix);
|
|
323
511
|
}
|
|
324
512
|
async invalidateByTag(tag) {
|
|
325
513
|
await this.startup;
|
|
@@ -336,13 +524,74 @@ var CacheStack = class {
|
|
|
336
524
|
getMetrics() {
|
|
337
525
|
return { ...this.metrics };
|
|
338
526
|
}
|
|
527
|
+
getStats() {
|
|
528
|
+
return {
|
|
529
|
+
metrics: this.getMetrics(),
|
|
530
|
+
layers: this.layers.map((layer) => ({
|
|
531
|
+
name: layer.name,
|
|
532
|
+
isLocal: Boolean(layer.isLocal),
|
|
533
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
534
|
+
})),
|
|
535
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
536
|
+
};
|
|
537
|
+
}
|
|
339
538
|
resetMetrics() {
|
|
340
539
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
341
540
|
}
|
|
342
|
-
async
|
|
541
|
+
async exportState() {
|
|
542
|
+
await this.startup;
|
|
543
|
+
const exported = /* @__PURE__ */ new Map();
|
|
544
|
+
for (const layer of this.layers) {
|
|
545
|
+
if (!layer.keys) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
const keys = await layer.keys();
|
|
549
|
+
for (const key of keys) {
|
|
550
|
+
if (exported.has(key)) {
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
554
|
+
if (stored === null) {
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
exported.set(key, {
|
|
558
|
+
key,
|
|
559
|
+
value: stored,
|
|
560
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return [...exported.values()];
|
|
565
|
+
}
|
|
566
|
+
async importState(entries) {
|
|
343
567
|
await this.startup;
|
|
344
|
-
await
|
|
345
|
-
|
|
568
|
+
await Promise.all(entries.map(async (entry) => {
|
|
569
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
570
|
+
await this.tagIndex.touch(entry.key);
|
|
571
|
+
}));
|
|
572
|
+
}
|
|
573
|
+
async persistToFile(filePath) {
|
|
574
|
+
const snapshot = await this.exportState();
|
|
575
|
+
await fs.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
576
|
+
}
|
|
577
|
+
async restoreFromFile(filePath) {
|
|
578
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
579
|
+
const snapshot = JSON.parse(raw);
|
|
580
|
+
if (!this.isCacheSnapshotEntries(snapshot)) {
|
|
581
|
+
throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
|
|
582
|
+
}
|
|
583
|
+
await this.importState(snapshot);
|
|
584
|
+
}
|
|
585
|
+
async disconnect() {
|
|
586
|
+
if (!this.disconnectPromise) {
|
|
587
|
+
this.isDisconnecting = true;
|
|
588
|
+
this.disconnectPromise = (async () => {
|
|
589
|
+
await this.startup;
|
|
590
|
+
await this.unsubscribeInvalidation?.();
|
|
591
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
592
|
+
})();
|
|
593
|
+
}
|
|
594
|
+
await this.disconnectPromise;
|
|
346
595
|
}
|
|
347
596
|
async initialize() {
|
|
348
597
|
if (!this.options.invalidationBus) {
|
|
@@ -382,6 +631,7 @@ var CacheStack = class {
|
|
|
382
631
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
383
632
|
const deadline = Date.now() + timeoutMs;
|
|
384
633
|
this.metrics.singleFlightWaits += 1;
|
|
634
|
+
this.emit("stampede-dedupe", { key });
|
|
385
635
|
while (Date.now() < deadline) {
|
|
386
636
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
387
637
|
if (hit.found) {
|
|
@@ -393,8 +643,16 @@ var CacheStack = class {
|
|
|
393
643
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
394
644
|
}
|
|
395
645
|
async fetchAndPopulate(key, fetcher, options) {
|
|
646
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
396
647
|
this.metrics.fetches += 1;
|
|
397
|
-
|
|
648
|
+
let fetched;
|
|
649
|
+
try {
|
|
650
|
+
fetched = await fetcher();
|
|
651
|
+
this.resetCircuitBreaker(key);
|
|
652
|
+
} catch (error) {
|
|
653
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
654
|
+
throw error;
|
|
655
|
+
}
|
|
398
656
|
if (fetched === null || fetched === void 0) {
|
|
399
657
|
if (!this.shouldNegativeCache(options)) {
|
|
400
658
|
return null;
|
|
@@ -413,8 +671,9 @@ var CacheStack = class {
|
|
|
413
671
|
await this.tagIndex.touch(key);
|
|
414
672
|
}
|
|
415
673
|
this.metrics.sets += 1;
|
|
416
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
417
|
-
|
|
674
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
675
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
676
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
418
677
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
419
678
|
}
|
|
420
679
|
}
|
|
@@ -424,6 +683,7 @@ var CacheStack = class {
|
|
|
424
683
|
const layer = this.layers[index];
|
|
425
684
|
const stored = await this.readLayerEntry(layer, key);
|
|
426
685
|
if (stored === null) {
|
|
686
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
427
687
|
continue;
|
|
428
688
|
}
|
|
429
689
|
const resolved = resolveStoredValue(stored);
|
|
@@ -437,20 +697,34 @@ var CacheStack = class {
|
|
|
437
697
|
}
|
|
438
698
|
await this.tagIndex.touch(key);
|
|
439
699
|
await this.backfill(key, stored, index - 1, options);
|
|
440
|
-
this.
|
|
441
|
-
|
|
700
|
+
this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
|
|
701
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
702
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
703
|
+
return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
|
|
442
704
|
}
|
|
443
705
|
if (!sawRetainableValue) {
|
|
444
706
|
await this.tagIndex.remove(key);
|
|
445
707
|
}
|
|
446
|
-
this.logger.debug("miss", { key, mode });
|
|
708
|
+
this.logger.debug?.("miss", { key, mode });
|
|
709
|
+
this.emit("miss", { key, mode });
|
|
447
710
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
448
711
|
}
|
|
449
712
|
async readLayerEntry(layer, key) {
|
|
713
|
+
if (this.shouldSkipLayer(layer)) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
450
716
|
if (layer.getEntry) {
|
|
451
|
-
|
|
717
|
+
try {
|
|
718
|
+
return await layer.getEntry(key);
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
try {
|
|
724
|
+
return await layer.get(key);
|
|
725
|
+
} catch (error) {
|
|
726
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
452
727
|
}
|
|
453
|
-
return layer.get(key);
|
|
454
728
|
}
|
|
455
729
|
async backfill(key, stored, upToIndex, options) {
|
|
456
730
|
if (upToIndex < 0) {
|
|
@@ -458,16 +732,28 @@ var CacheStack = class {
|
|
|
458
732
|
}
|
|
459
733
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
460
734
|
const layer = this.layers[index];
|
|
735
|
+
if (this.shouldSkipLayer(layer)) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
461
738
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
462
|
-
|
|
739
|
+
try {
|
|
740
|
+
await layer.set(key, stored, ttl);
|
|
741
|
+
} catch (error) {
|
|
742
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
463
745
|
this.metrics.backfills += 1;
|
|
464
|
-
this.logger.debug("backfill", { key, layer: layer.name });
|
|
746
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
747
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
465
748
|
}
|
|
466
749
|
}
|
|
467
750
|
async writeAcrossLayers(key, kind, value, options) {
|
|
468
751
|
const now = Date.now();
|
|
469
752
|
const operations = this.layers.map((layer) => async () => {
|
|
470
|
-
|
|
753
|
+
if (this.shouldSkipLayer(layer)) {
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
471
757
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
472
758
|
layer.name,
|
|
473
759
|
options?.staleWhileRevalidate,
|
|
@@ -487,7 +773,11 @@ var CacheStack = class {
|
|
|
487
773
|
now
|
|
488
774
|
});
|
|
489
775
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
490
|
-
|
|
776
|
+
try {
|
|
777
|
+
await layer.set(key, payload, ttl);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
780
|
+
}
|
|
491
781
|
});
|
|
492
782
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
493
783
|
}
|
|
@@ -502,7 +792,7 @@ var CacheStack = class {
|
|
|
502
792
|
return;
|
|
503
793
|
}
|
|
504
794
|
this.metrics.writeFailures += failures.length;
|
|
505
|
-
this.logger.debug("write-failure", {
|
|
795
|
+
this.logger.debug?.("write-failure", {
|
|
506
796
|
...context,
|
|
507
797
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
508
798
|
});
|
|
@@ -513,15 +803,21 @@ var CacheStack = class {
|
|
|
513
803
|
);
|
|
514
804
|
}
|
|
515
805
|
}
|
|
516
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
806
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
517
807
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
518
808
|
layerName,
|
|
519
809
|
options?.negativeTtl,
|
|
520
810
|
this.options.negativeTtl,
|
|
521
811
|
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
522
812
|
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
813
|
+
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
814
|
+
key,
|
|
815
|
+
layerName,
|
|
816
|
+
baseTtl,
|
|
817
|
+
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
818
|
+
);
|
|
523
819
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
524
|
-
return this.applyJitter(
|
|
820
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
525
821
|
}
|
|
526
822
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
527
823
|
if (override !== void 0) {
|
|
@@ -549,7 +845,7 @@ var CacheStack = class {
|
|
|
549
845
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
550
846
|
}
|
|
551
847
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
552
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
848
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
553
849
|
return;
|
|
554
850
|
}
|
|
555
851
|
const refresh = (async () => {
|
|
@@ -558,7 +854,7 @@ var CacheStack = class {
|
|
|
558
854
|
await this.fetchWithGuards(key, fetcher, options);
|
|
559
855
|
} catch (error) {
|
|
560
856
|
this.metrics.refreshErrors += 1;
|
|
561
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
857
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
562
858
|
} finally {
|
|
563
859
|
this.backgroundRefreshes.delete(key);
|
|
564
860
|
}
|
|
@@ -576,21 +872,15 @@ var CacheStack = class {
|
|
|
576
872
|
if (keys.length === 0) {
|
|
577
873
|
return;
|
|
578
874
|
}
|
|
579
|
-
await
|
|
580
|
-
this.layers.map(async (layer) => {
|
|
581
|
-
if (layer.deleteMany) {
|
|
582
|
-
await layer.deleteMany(keys);
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
586
|
-
})
|
|
587
|
-
);
|
|
875
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
588
876
|
for (const key of keys) {
|
|
589
877
|
await this.tagIndex.remove(key);
|
|
878
|
+
this.accessProfiles.delete(key);
|
|
590
879
|
}
|
|
591
880
|
this.metrics.deletes += keys.length;
|
|
592
881
|
this.metrics.invalidations += 1;
|
|
593
|
-
this.logger.debug("delete", { keys });
|
|
882
|
+
this.logger.debug?.("delete", { keys });
|
|
883
|
+
this.emit("delete", { keys });
|
|
594
884
|
}
|
|
595
885
|
async publishInvalidation(message) {
|
|
596
886
|
if (!this.options.invalidationBus) {
|
|
@@ -609,21 +899,15 @@ var CacheStack = class {
|
|
|
609
899
|
if (message.scope === "clear") {
|
|
610
900
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
611
901
|
await this.tagIndex.clear();
|
|
902
|
+
this.accessProfiles.clear();
|
|
612
903
|
return;
|
|
613
904
|
}
|
|
614
905
|
const keys = message.keys ?? [];
|
|
615
|
-
await
|
|
616
|
-
localLayers.map(async (layer) => {
|
|
617
|
-
if (layer.deleteMany) {
|
|
618
|
-
await layer.deleteMany(keys);
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
622
|
-
})
|
|
623
|
-
);
|
|
906
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
624
907
|
if (message.operation !== "write") {
|
|
625
908
|
for (const key of keys) {
|
|
626
909
|
await this.tagIndex.remove(key);
|
|
910
|
+
this.accessProfiles.delete(key);
|
|
627
911
|
}
|
|
628
912
|
}
|
|
629
913
|
}
|
|
@@ -636,6 +920,257 @@ var CacheStack = class {
|
|
|
636
920
|
sleep(ms) {
|
|
637
921
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
638
922
|
}
|
|
923
|
+
shouldBroadcastL1Invalidation() {
|
|
924
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
925
|
+
}
|
|
926
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
927
|
+
await Promise.all(
|
|
928
|
+
layers.map(async (layer) => {
|
|
929
|
+
if (this.shouldSkipLayer(layer)) {
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
if (layer.deleteMany) {
|
|
933
|
+
try {
|
|
934
|
+
await layer.deleteMany(keys);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
937
|
+
}
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
await Promise.all(keys.map(async (key) => {
|
|
941
|
+
try {
|
|
942
|
+
await layer.delete(key);
|
|
943
|
+
} catch (error) {
|
|
944
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
945
|
+
}
|
|
946
|
+
}));
|
|
947
|
+
})
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
validateConfiguration() {
|
|
951
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
952
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
953
|
+
}
|
|
954
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
955
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
956
|
+
}
|
|
957
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
958
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
959
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
960
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
961
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
962
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
963
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
964
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
965
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
966
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
967
|
+
}
|
|
968
|
+
validateWriteOptions(options) {
|
|
969
|
+
if (!options) {
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
973
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
974
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
975
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
976
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
977
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
978
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
979
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
980
|
+
}
|
|
981
|
+
validateLayerNumberOption(name, value) {
|
|
982
|
+
if (value === void 0) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
if (typeof value === "number") {
|
|
986
|
+
this.validateNonNegativeNumber(name, value);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
990
|
+
if (layerValue === void 0) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
validatePositiveNumber(name, value) {
|
|
997
|
+
if (value === void 0) {
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1001
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
validateNonNegativeNumber(name, value) {
|
|
1005
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1006
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
validateCacheKey(key) {
|
|
1010
|
+
if (key.length === 0) {
|
|
1011
|
+
throw new Error("Cache key must not be empty.");
|
|
1012
|
+
}
|
|
1013
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1014
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1015
|
+
}
|
|
1016
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1017
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1018
|
+
}
|
|
1019
|
+
return key;
|
|
1020
|
+
}
|
|
1021
|
+
serializeOptions(options) {
|
|
1022
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1023
|
+
}
|
|
1024
|
+
validateAdaptiveTtlOptions(options) {
|
|
1025
|
+
if (!options || options === true) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1029
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1030
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1031
|
+
}
|
|
1032
|
+
validateCircuitBreakerOptions(options) {
|
|
1033
|
+
if (!options) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1037
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1038
|
+
}
|
|
1039
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1040
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1041
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1042
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1043
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1044
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1045
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1046
|
+
const layer = this.layers[index];
|
|
1047
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
try {
|
|
1051
|
+
await layer.set(key, refreshed, ttl);
|
|
1052
|
+
} catch (error) {
|
|
1053
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1058
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1062
|
+
if (!ttl || !adaptiveTtl) {
|
|
1063
|
+
return ttl;
|
|
1064
|
+
}
|
|
1065
|
+
const profile = this.accessProfiles.get(key);
|
|
1066
|
+
if (!profile) {
|
|
1067
|
+
return ttl;
|
|
1068
|
+
}
|
|
1069
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1070
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
1071
|
+
if (profile.hits < hotAfter) {
|
|
1072
|
+
return ttl;
|
|
1073
|
+
}
|
|
1074
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1075
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1076
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1077
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1078
|
+
}
|
|
1079
|
+
recordAccess(key) {
|
|
1080
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1081
|
+
profile.hits += 1;
|
|
1082
|
+
profile.lastAccessAt = Date.now();
|
|
1083
|
+
this.accessProfiles.set(key, profile);
|
|
1084
|
+
}
|
|
1085
|
+
incrementMetricMap(target, key) {
|
|
1086
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
1087
|
+
}
|
|
1088
|
+
shouldSkipLayer(layer) {
|
|
1089
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1090
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1091
|
+
}
|
|
1092
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1093
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1094
|
+
throw error;
|
|
1095
|
+
}
|
|
1096
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1097
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1098
|
+
this.metrics.degradedOperations += 1;
|
|
1099
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1100
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1101
|
+
return null;
|
|
1102
|
+
}
|
|
1103
|
+
isGracefulDegradationEnabled() {
|
|
1104
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1105
|
+
}
|
|
1106
|
+
assertCircuitClosed(key, options) {
|
|
1107
|
+
const state = this.circuitBreakers.get(key);
|
|
1108
|
+
if (!state?.openUntil) {
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (state.openUntil <= Date.now()) {
|
|
1112
|
+
state.openUntil = null;
|
|
1113
|
+
state.failures = 0;
|
|
1114
|
+
this.circuitBreakers.set(key, state);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1118
|
+
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1119
|
+
}
|
|
1120
|
+
recordCircuitFailure(key, options, error) {
|
|
1121
|
+
if (!options) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
1125
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1126
|
+
const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1127
|
+
state.failures += 1;
|
|
1128
|
+
if (state.failures >= failureThreshold) {
|
|
1129
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
1130
|
+
this.metrics.circuitBreakerTrips += 1;
|
|
1131
|
+
}
|
|
1132
|
+
this.circuitBreakers.set(key, state);
|
|
1133
|
+
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1134
|
+
}
|
|
1135
|
+
resetCircuitBreaker(key) {
|
|
1136
|
+
this.circuitBreakers.delete(key);
|
|
1137
|
+
}
|
|
1138
|
+
isNegativeStoredValue(stored) {
|
|
1139
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1140
|
+
}
|
|
1141
|
+
emitError(operation, context) {
|
|
1142
|
+
this.logger.error?.(operation, context);
|
|
1143
|
+
if (this.listenerCount("error") > 0) {
|
|
1144
|
+
this.emit("error", { operation, ...context });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
serializeKeyPart(value) {
|
|
1148
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1149
|
+
return String(value);
|
|
1150
|
+
}
|
|
1151
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1152
|
+
}
|
|
1153
|
+
isCacheSnapshotEntries(value) {
|
|
1154
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1155
|
+
if (!entry || typeof entry !== "object") {
|
|
1156
|
+
return false;
|
|
1157
|
+
}
|
|
1158
|
+
const candidate = entry;
|
|
1159
|
+
return typeof candidate.key === "string";
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
normalizeForSerialization(value) {
|
|
1163
|
+
if (Array.isArray(value)) {
|
|
1164
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1165
|
+
}
|
|
1166
|
+
if (value && typeof value === "object") {
|
|
1167
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1168
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1169
|
+
return normalized;
|
|
1170
|
+
}, {});
|
|
1171
|
+
}
|
|
1172
|
+
return value;
|
|
1173
|
+
}
|
|
639
1174
|
};
|
|
640
1175
|
|
|
641
1176
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -643,19 +1178,27 @@ var RedisInvalidationBus = class {
|
|
|
643
1178
|
channel;
|
|
644
1179
|
publisher;
|
|
645
1180
|
subscriber;
|
|
1181
|
+
activeListener;
|
|
646
1182
|
constructor(options) {
|
|
647
1183
|
this.publisher = options.publisher;
|
|
648
1184
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
649
1185
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
650
1186
|
}
|
|
651
1187
|
async subscribe(handler) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1188
|
+
if (this.activeListener) {
|
|
1189
|
+
throw new Error("RedisInvalidationBus already has an active subscription.");
|
|
1190
|
+
}
|
|
1191
|
+
const listener = (_channel, payload) => {
|
|
1192
|
+
void this.handleMessage(payload, handler);
|
|
655
1193
|
};
|
|
1194
|
+
this.activeListener = listener;
|
|
656
1195
|
this.subscriber.on("message", listener);
|
|
657
1196
|
await this.subscriber.subscribe(this.channel);
|
|
658
1197
|
return async () => {
|
|
1198
|
+
if (this.activeListener !== listener) {
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
this.activeListener = void 0;
|
|
659
1202
|
this.subscriber.off("message", listener);
|
|
660
1203
|
await this.subscriber.unsubscribe(this.channel);
|
|
661
1204
|
};
|
|
@@ -663,98 +1206,117 @@ var RedisInvalidationBus = class {
|
|
|
663
1206
|
async publish(message) {
|
|
664
1207
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
665
1208
|
}
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
scanCount;
|
|
673
|
-
constructor(options) {
|
|
674
|
-
this.client = options.client;
|
|
675
|
-
this.prefix = options.prefix ?? "layercache:tag-index";
|
|
676
|
-
this.scanCount = options.scanCount ?? 100;
|
|
677
|
-
}
|
|
678
|
-
async touch(key) {
|
|
679
|
-
await this.client.sadd(this.knownKeysKey(), key);
|
|
680
|
-
}
|
|
681
|
-
async track(key, tags) {
|
|
682
|
-
const keyTagsKey = this.keyTagsKey(key);
|
|
683
|
-
const existingTags = await this.client.smembers(keyTagsKey);
|
|
684
|
-
const pipeline = this.client.pipeline();
|
|
685
|
-
pipeline.sadd(this.knownKeysKey(), key);
|
|
686
|
-
for (const tag of existingTags) {
|
|
687
|
-
pipeline.srem(this.tagKeysKey(tag), key);
|
|
688
|
-
}
|
|
689
|
-
pipeline.del(keyTagsKey);
|
|
690
|
-
if (tags.length > 0) {
|
|
691
|
-
pipeline.sadd(keyTagsKey, ...tags);
|
|
692
|
-
for (const tag of new Set(tags)) {
|
|
693
|
-
pipeline.sadd(this.tagKeysKey(tag), key);
|
|
1209
|
+
async handleMessage(payload, handler) {
|
|
1210
|
+
let message;
|
|
1211
|
+
try {
|
|
1212
|
+
const parsed = JSON.parse(payload);
|
|
1213
|
+
if (!this.isInvalidationMessage(parsed)) {
|
|
1214
|
+
throw new Error("Invalid invalidation payload shape.");
|
|
694
1215
|
}
|
|
1216
|
+
message = parsed;
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
this.reportError("invalid invalidation payload", error);
|
|
1219
|
+
return;
|
|
695
1220
|
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
const existingTags = await this.client.smembers(keyTagsKey);
|
|
701
|
-
const pipeline = this.client.pipeline();
|
|
702
|
-
pipeline.srem(this.knownKeysKey(), key);
|
|
703
|
-
pipeline.del(keyTagsKey);
|
|
704
|
-
for (const tag of existingTags) {
|
|
705
|
-
pipeline.srem(this.tagKeysKey(tag), key);
|
|
1221
|
+
try {
|
|
1222
|
+
await handler(message);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
this.reportError("invalidation handler failed", error);
|
|
706
1225
|
}
|
|
707
|
-
await pipeline.exec();
|
|
708
|
-
}
|
|
709
|
-
async keysForTag(tag) {
|
|
710
|
-
return this.client.smembers(this.tagKeysKey(tag));
|
|
711
|
-
}
|
|
712
|
-
async matchPattern(pattern) {
|
|
713
|
-
const matches = [];
|
|
714
|
-
let cursor = "0";
|
|
715
|
-
do {
|
|
716
|
-
const [nextCursor, keys] = await this.client.sscan(
|
|
717
|
-
this.knownKeysKey(),
|
|
718
|
-
cursor,
|
|
719
|
-
"MATCH",
|
|
720
|
-
pattern,
|
|
721
|
-
"COUNT",
|
|
722
|
-
this.scanCount
|
|
723
|
-
);
|
|
724
|
-
cursor = nextCursor;
|
|
725
|
-
matches.push(...keys.filter((key) => PatternMatcher.matches(pattern, key)));
|
|
726
|
-
} while (cursor !== "0");
|
|
727
|
-
return matches;
|
|
728
1226
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
return;
|
|
1227
|
+
isInvalidationMessage(value) {
|
|
1228
|
+
if (!value || typeof value !== "object") {
|
|
1229
|
+
return false;
|
|
733
1230
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
const pattern = `${this.prefix}:*`;
|
|
740
|
-
do {
|
|
741
|
-
const [nextCursor, keys] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", this.scanCount);
|
|
742
|
-
cursor = nextCursor;
|
|
743
|
-
matches.push(...keys);
|
|
744
|
-
} while (cursor !== "0");
|
|
745
|
-
return matches;
|
|
746
|
-
}
|
|
747
|
-
knownKeysKey() {
|
|
748
|
-
return `${this.prefix}:keys`;
|
|
749
|
-
}
|
|
750
|
-
keyTagsKey(key) {
|
|
751
|
-
return `${this.prefix}:key:${encodeURIComponent(key)}`;
|
|
1231
|
+
const candidate = value;
|
|
1232
|
+
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
1233
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
1234
|
+
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
1235
|
+
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
752
1236
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1237
|
+
reportError(message, error) {
|
|
1238
|
+
console.error(`[layercache] ${message}`, error);
|
|
755
1239
|
}
|
|
756
1240
|
};
|
|
757
1241
|
|
|
1242
|
+
// src/http/createCacheStatsHandler.ts
|
|
1243
|
+
function createCacheStatsHandler(cache) {
|
|
1244
|
+
return async (_request, response) => {
|
|
1245
|
+
response.statusCode = 200;
|
|
1246
|
+
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1247
|
+
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/decorators/createCachedMethodDecorator.ts
|
|
1252
|
+
function createCachedMethodDecorator(options) {
|
|
1253
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
1254
|
+
return ((_, propertyKey, descriptor) => {
|
|
1255
|
+
const original = descriptor.value;
|
|
1256
|
+
if (typeof original !== "function") {
|
|
1257
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
1258
|
+
}
|
|
1259
|
+
descriptor.value = async function(...args) {
|
|
1260
|
+
const instance = this;
|
|
1261
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
1262
|
+
if (!wrapped) {
|
|
1263
|
+
const cache = options.cache(instance);
|
|
1264
|
+
wrapped = cache.wrap(
|
|
1265
|
+
options.prefix ?? String(propertyKey),
|
|
1266
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
1267
|
+
options
|
|
1268
|
+
);
|
|
1269
|
+
wrappedByInstance.set(instance, wrapped);
|
|
1270
|
+
}
|
|
1271
|
+
return wrapped(...args);
|
|
1272
|
+
};
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// src/integrations/fastify.ts
|
|
1277
|
+
function createFastifyLayercachePlugin(cache, options = {}) {
|
|
1278
|
+
return async (fastify) => {
|
|
1279
|
+
fastify.decorate("cache", cache);
|
|
1280
|
+
if (options.exposeStatsRoute !== false && fastify.get) {
|
|
1281
|
+
fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
// src/integrations/graphql.ts
|
|
1287
|
+
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1288
|
+
const wrapped = cache.wrap(prefix, resolver, {
|
|
1289
|
+
...options,
|
|
1290
|
+
keyResolver: options.keyResolver
|
|
1291
|
+
});
|
|
1292
|
+
return (...args) => wrapped(...args);
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// src/integrations/trpc.ts
|
|
1296
|
+
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1297
|
+
return async (context) => {
|
|
1298
|
+
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
1299
|
+
let didFetch = false;
|
|
1300
|
+
let fetchedResult = null;
|
|
1301
|
+
const cached = await cache.get(
|
|
1302
|
+
key,
|
|
1303
|
+
async () => {
|
|
1304
|
+
didFetch = true;
|
|
1305
|
+
fetchedResult = await context.next();
|
|
1306
|
+
return fetchedResult;
|
|
1307
|
+
},
|
|
1308
|
+
options
|
|
1309
|
+
);
|
|
1310
|
+
if (cached !== null) {
|
|
1311
|
+
return cached;
|
|
1312
|
+
}
|
|
1313
|
+
if (didFetch) {
|
|
1314
|
+
return fetchedResult;
|
|
1315
|
+
}
|
|
1316
|
+
return context.next();
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
758
1320
|
// src/layers/MemoryLayer.ts
|
|
759
1321
|
var MemoryLayer = class {
|
|
760
1322
|
name;
|
|
@@ -820,6 +1382,32 @@ var MemoryLayer = class {
|
|
|
820
1382
|
this.pruneExpired();
|
|
821
1383
|
return [...this.entries.keys()];
|
|
822
1384
|
}
|
|
1385
|
+
exportState() {
|
|
1386
|
+
this.pruneExpired();
|
|
1387
|
+
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1388
|
+
key,
|
|
1389
|
+
value: entry.value,
|
|
1390
|
+
expiresAt: entry.expiresAt
|
|
1391
|
+
}));
|
|
1392
|
+
}
|
|
1393
|
+
importState(entries) {
|
|
1394
|
+
for (const entry of entries) {
|
|
1395
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
this.entries.set(entry.key, {
|
|
1399
|
+
value: entry.value,
|
|
1400
|
+
expiresAt: entry.expiresAt
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
while (this.entries.size > this.maxSize) {
|
|
1404
|
+
const oldestKey = this.entries.keys().next().value;
|
|
1405
|
+
if (!oldestKey) {
|
|
1406
|
+
break;
|
|
1407
|
+
}
|
|
1408
|
+
this.entries.delete(oldestKey);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
823
1411
|
pruneExpired() {
|
|
824
1412
|
for (const [key, entry] of this.entries.entries()) {
|
|
825
1413
|
if (this.isExpired(entry)) {
|
|
@@ -832,6 +1420,9 @@ var MemoryLayer = class {
|
|
|
832
1420
|
}
|
|
833
1421
|
};
|
|
834
1422
|
|
|
1423
|
+
// src/layers/RedisLayer.ts
|
|
1424
|
+
import { brotliCompressSync, brotliDecompressSync, gzipSync, gunzipSync } from "zlib";
|
|
1425
|
+
|
|
835
1426
|
// src/serialization/JsonSerializer.ts
|
|
836
1427
|
var JsonSerializer = class {
|
|
837
1428
|
serialize(value) {
|
|
@@ -853,6 +1444,8 @@ var RedisLayer = class {
|
|
|
853
1444
|
prefix;
|
|
854
1445
|
allowUnprefixedClear;
|
|
855
1446
|
scanCount;
|
|
1447
|
+
compression;
|
|
1448
|
+
compressionThreshold;
|
|
856
1449
|
constructor(options) {
|
|
857
1450
|
this.client = options.client;
|
|
858
1451
|
this.defaultTtl = options.ttl;
|
|
@@ -861,6 +1454,8 @@ var RedisLayer = class {
|
|
|
861
1454
|
this.prefix = options.prefix ?? "";
|
|
862
1455
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
863
1456
|
this.scanCount = options.scanCount ?? 100;
|
|
1457
|
+
this.compression = options.compression;
|
|
1458
|
+
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
864
1459
|
}
|
|
865
1460
|
async get(key) {
|
|
866
1461
|
const payload = await this.getEntry(key);
|
|
@@ -871,7 +1466,7 @@ var RedisLayer = class {
|
|
|
871
1466
|
if (payload === null) {
|
|
872
1467
|
return null;
|
|
873
1468
|
}
|
|
874
|
-
return this.
|
|
1469
|
+
return this.deserializeOrDelete(key, payload);
|
|
875
1470
|
}
|
|
876
1471
|
async getMany(keys) {
|
|
877
1472
|
if (keys.length === 0) {
|
|
@@ -885,16 +1480,18 @@ var RedisLayer = class {
|
|
|
885
1480
|
if (results === null) {
|
|
886
1481
|
return keys.map(() => null);
|
|
887
1482
|
}
|
|
888
|
-
return
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1483
|
+
return Promise.all(
|
|
1484
|
+
results.map(async (result, index) => {
|
|
1485
|
+
const [error, payload] = result;
|
|
1486
|
+
if (error || payload === null || !this.isSerializablePayload(payload)) {
|
|
1487
|
+
return null;
|
|
1488
|
+
}
|
|
1489
|
+
return this.deserializeOrDelete(keys[index], payload);
|
|
1490
|
+
})
|
|
1491
|
+
);
|
|
895
1492
|
}
|
|
896
1493
|
async set(key, value, ttl = this.defaultTtl) {
|
|
897
|
-
const payload = this.serializer.serialize(value);
|
|
1494
|
+
const payload = this.encodePayload(this.serializer.serialize(value));
|
|
898
1495
|
const normalizedKey = this.withPrefix(key);
|
|
899
1496
|
if (ttl && ttl > 0) {
|
|
900
1497
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -941,6 +1538,41 @@ var RedisLayer = class {
|
|
|
941
1538
|
withPrefix(key) {
|
|
942
1539
|
return `${this.prefix}${key}`;
|
|
943
1540
|
}
|
|
1541
|
+
async deserializeOrDelete(key, payload) {
|
|
1542
|
+
try {
|
|
1543
|
+
return this.serializer.deserialize(this.decodePayload(payload));
|
|
1544
|
+
} catch {
|
|
1545
|
+
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
1546
|
+
return null;
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
isSerializablePayload(payload) {
|
|
1550
|
+
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
1551
|
+
}
|
|
1552
|
+
encodePayload(payload) {
|
|
1553
|
+
if (!this.compression) {
|
|
1554
|
+
return payload;
|
|
1555
|
+
}
|
|
1556
|
+
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
1557
|
+
if (source.byteLength < this.compressionThreshold) {
|
|
1558
|
+
return payload;
|
|
1559
|
+
}
|
|
1560
|
+
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
1561
|
+
const compressed = this.compression === "gzip" ? gzipSync(source) : brotliCompressSync(source);
|
|
1562
|
+
return Buffer.concat([header, compressed]);
|
|
1563
|
+
}
|
|
1564
|
+
decodePayload(payload) {
|
|
1565
|
+
if (!Buffer.isBuffer(payload)) {
|
|
1566
|
+
return payload;
|
|
1567
|
+
}
|
|
1568
|
+
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
1569
|
+
return gunzipSync(payload.subarray(10));
|
|
1570
|
+
}
|
|
1571
|
+
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
1572
|
+
return brotliDecompressSync(payload.subarray(12));
|
|
1573
|
+
}
|
|
1574
|
+
return payload;
|
|
1575
|
+
}
|
|
944
1576
|
};
|
|
945
1577
|
|
|
946
1578
|
// src/serialization/MsgpackSerializer.ts
|
|
@@ -985,6 +1617,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
985
1617
|
}
|
|
986
1618
|
};
|
|
987
1619
|
export {
|
|
1620
|
+
CacheNamespace,
|
|
988
1621
|
CacheStack,
|
|
989
1622
|
JsonSerializer,
|
|
990
1623
|
MemoryLayer,
|
|
@@ -995,5 +1628,10 @@ export {
|
|
|
995
1628
|
RedisSingleFlightCoordinator,
|
|
996
1629
|
RedisTagIndex,
|
|
997
1630
|
StampedeGuard,
|
|
998
|
-
TagIndex
|
|
1631
|
+
TagIndex,
|
|
1632
|
+
cacheGraphqlResolver,
|
|
1633
|
+
createCacheStatsHandler,
|
|
1634
|
+
createCachedMethodDecorator,
|
|
1635
|
+
createFastifyLayercachePlugin,
|
|
1636
|
+
createTrpcCacheMiddleware
|
|
999
1637
|
};
|