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.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
CacheNamespace: () => CacheNamespace,
|
|
23
24
|
CacheStack: () => CacheStack,
|
|
24
25
|
JsonSerializer: () => JsonSerializer,
|
|
25
26
|
MemoryLayer: () => MemoryLayer,
|
|
@@ -30,12 +31,19 @@ __export(index_exports, {
|
|
|
30
31
|
RedisSingleFlightCoordinator: () => RedisSingleFlightCoordinator,
|
|
31
32
|
RedisTagIndex: () => RedisTagIndex,
|
|
32
33
|
StampedeGuard: () => StampedeGuard,
|
|
33
|
-
TagIndex: () => TagIndex
|
|
34
|
+
TagIndex: () => TagIndex,
|
|
35
|
+
cacheGraphqlResolver: () => cacheGraphqlResolver,
|
|
36
|
+
createCacheStatsHandler: () => createCacheStatsHandler,
|
|
37
|
+
createCachedMethodDecorator: () => createCachedMethodDecorator,
|
|
38
|
+
createFastifyLayercachePlugin: () => createFastifyLayercachePlugin,
|
|
39
|
+
createTrpcCacheMiddleware: () => createTrpcCacheMiddleware
|
|
34
40
|
});
|
|
35
41
|
module.exports = __toCommonJS(index_exports);
|
|
36
42
|
|
|
37
43
|
// src/CacheStack.ts
|
|
38
44
|
var import_node_crypto = require("crypto");
|
|
45
|
+
var import_node_fs = require("fs");
|
|
46
|
+
var import_node_events = require("events");
|
|
39
47
|
|
|
40
48
|
// src/internal/StoredValue.ts
|
|
41
49
|
function isStoredValueEnvelope(value) {
|
|
@@ -55,7 +63,10 @@ function createStoredValueEnvelope(options) {
|
|
|
55
63
|
value: options.value,
|
|
56
64
|
freshUntil,
|
|
57
65
|
staleUntil,
|
|
58
|
-
errorUntil
|
|
66
|
+
errorUntil,
|
|
67
|
+
freshTtlSeconds: freshTtlSeconds ?? null,
|
|
68
|
+
staleWhileRevalidateSeconds: staleWhileRevalidateSeconds ?? null,
|
|
69
|
+
staleIfErrorSeconds: staleIfErrorSeconds ?? null
|
|
59
70
|
};
|
|
60
71
|
}
|
|
61
72
|
function resolveStoredValue(stored, now = Date.now()) {
|
|
@@ -96,6 +107,29 @@ function remainingStoredTtlSeconds(stored, now = Date.now()) {
|
|
|
96
107
|
}
|
|
97
108
|
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
98
109
|
}
|
|
110
|
+
function remainingFreshTtlSeconds(stored, now = Date.now()) {
|
|
111
|
+
if (!isStoredValueEnvelope(stored) || stored.freshUntil === null) {
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
const remainingMs = stored.freshUntil - now;
|
|
115
|
+
if (remainingMs <= 0) {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
return Math.max(1, Math.ceil(remainingMs / 1e3));
|
|
119
|
+
}
|
|
120
|
+
function refreshStoredEnvelope(stored, now = Date.now()) {
|
|
121
|
+
if (!isStoredValueEnvelope(stored)) {
|
|
122
|
+
return stored;
|
|
123
|
+
}
|
|
124
|
+
return createStoredValueEnvelope({
|
|
125
|
+
kind: stored.kind,
|
|
126
|
+
value: stored.value,
|
|
127
|
+
freshTtlSeconds: stored.freshTtlSeconds ?? void 0,
|
|
128
|
+
staleWhileRevalidateSeconds: stored.staleWhileRevalidateSeconds ?? void 0,
|
|
129
|
+
staleIfErrorSeconds: stored.staleIfErrorSeconds ?? void 0,
|
|
130
|
+
now
|
|
131
|
+
});
|
|
132
|
+
}
|
|
99
133
|
function maxExpiry(stored) {
|
|
100
134
|
const values = [stored.freshUntil, stored.staleUntil, stored.errorUntil].filter(
|
|
101
135
|
(value) => value !== null
|
|
@@ -112,6 +146,61 @@ function normalizePositiveSeconds(value) {
|
|
|
112
146
|
return value;
|
|
113
147
|
}
|
|
114
148
|
|
|
149
|
+
// src/CacheNamespace.ts
|
|
150
|
+
var CacheNamespace = class {
|
|
151
|
+
constructor(cache, prefix) {
|
|
152
|
+
this.cache = cache;
|
|
153
|
+
this.prefix = prefix;
|
|
154
|
+
}
|
|
155
|
+
cache;
|
|
156
|
+
prefix;
|
|
157
|
+
async get(key, fetcher, options) {
|
|
158
|
+
return this.cache.get(this.qualify(key), fetcher, options);
|
|
159
|
+
}
|
|
160
|
+
async set(key, value, options) {
|
|
161
|
+
await this.cache.set(this.qualify(key), value, options);
|
|
162
|
+
}
|
|
163
|
+
async delete(key) {
|
|
164
|
+
await this.cache.delete(this.qualify(key));
|
|
165
|
+
}
|
|
166
|
+
async clear() {
|
|
167
|
+
await this.cache.invalidateByPattern(`${this.prefix}:*`);
|
|
168
|
+
}
|
|
169
|
+
async mget(entries) {
|
|
170
|
+
return this.cache.mget(entries.map((entry) => ({
|
|
171
|
+
...entry,
|
|
172
|
+
key: this.qualify(entry.key)
|
|
173
|
+
})));
|
|
174
|
+
}
|
|
175
|
+
async mset(entries) {
|
|
176
|
+
await this.cache.mset(entries.map((entry) => ({
|
|
177
|
+
...entry,
|
|
178
|
+
key: this.qualify(entry.key)
|
|
179
|
+
})));
|
|
180
|
+
}
|
|
181
|
+
async invalidateByTag(tag) {
|
|
182
|
+
await this.cache.invalidateByTag(tag);
|
|
183
|
+
}
|
|
184
|
+
async invalidateByPattern(pattern) {
|
|
185
|
+
await this.cache.invalidateByPattern(this.qualify(pattern));
|
|
186
|
+
}
|
|
187
|
+
wrap(keyPrefix, fetcher, options) {
|
|
188
|
+
return this.cache.wrap(`${this.prefix}:${keyPrefix}`, fetcher, options);
|
|
189
|
+
}
|
|
190
|
+
warm(entries, options) {
|
|
191
|
+
return this.cache.warm(entries.map((entry) => ({
|
|
192
|
+
...entry,
|
|
193
|
+
key: this.qualify(entry.key)
|
|
194
|
+
})), options);
|
|
195
|
+
}
|
|
196
|
+
getMetrics() {
|
|
197
|
+
return this.cache.getMetrics();
|
|
198
|
+
}
|
|
199
|
+
qualify(key) {
|
|
200
|
+
return `${this.prefix}:${key}`;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
115
204
|
// src/invalidation/PatternMatcher.ts
|
|
116
205
|
var PatternMatcher = class {
|
|
117
206
|
static matches(pattern, value) {
|
|
@@ -184,22 +273,24 @@ var import_async_mutex = require("async-mutex");
|
|
|
184
273
|
var StampedeGuard = class {
|
|
185
274
|
mutexes = /* @__PURE__ */ new Map();
|
|
186
275
|
async execute(key, task) {
|
|
187
|
-
const
|
|
276
|
+
const entry = this.getMutexEntry(key);
|
|
188
277
|
try {
|
|
189
|
-
return await mutex.runExclusive(task);
|
|
278
|
+
return await entry.mutex.runExclusive(task);
|
|
190
279
|
} finally {
|
|
191
|
-
|
|
280
|
+
entry.references -= 1;
|
|
281
|
+
if (entry.references === 0 && !entry.mutex.isLocked()) {
|
|
192
282
|
this.mutexes.delete(key);
|
|
193
283
|
}
|
|
194
284
|
}
|
|
195
285
|
}
|
|
196
|
-
|
|
197
|
-
let
|
|
198
|
-
if (!
|
|
199
|
-
|
|
200
|
-
this.mutexes.set(key,
|
|
286
|
+
getMutexEntry(key) {
|
|
287
|
+
let entry = this.mutexes.get(key);
|
|
288
|
+
if (!entry) {
|
|
289
|
+
entry = { mutex: new import_async_mutex.Mutex(), references: 0 };
|
|
290
|
+
this.mutexes.set(key, entry);
|
|
201
291
|
}
|
|
202
|
-
|
|
292
|
+
entry.references += 1;
|
|
293
|
+
return entry;
|
|
203
294
|
}
|
|
204
295
|
};
|
|
205
296
|
|
|
@@ -208,6 +299,7 @@ var DEFAULT_NEGATIVE_TTL_SECONDS = 60;
|
|
|
208
299
|
var DEFAULT_SINGLE_FLIGHT_LEASE_MS = 3e4;
|
|
209
300
|
var DEFAULT_SINGLE_FLIGHT_TIMEOUT_MS = 5e3;
|
|
210
301
|
var DEFAULT_SINGLE_FLIGHT_POLL_MS = 50;
|
|
302
|
+
var MAX_CACHE_KEY_LENGTH = 1024;
|
|
211
303
|
var EMPTY_METRICS = () => ({
|
|
212
304
|
hits: 0,
|
|
213
305
|
misses: 0,
|
|
@@ -220,7 +312,12 @@ var EMPTY_METRICS = () => ({
|
|
|
220
312
|
refreshes: 0,
|
|
221
313
|
refreshErrors: 0,
|
|
222
314
|
writeFailures: 0,
|
|
223
|
-
singleFlightWaits: 0
|
|
315
|
+
singleFlightWaits: 0,
|
|
316
|
+
negativeCacheHits: 0,
|
|
317
|
+
circuitBreakerTrips: 0,
|
|
318
|
+
degradedOperations: 0,
|
|
319
|
+
hitsByLayer: {},
|
|
320
|
+
missesByLayer: {}
|
|
224
321
|
});
|
|
225
322
|
var DebugLogger = class {
|
|
226
323
|
enabled;
|
|
@@ -228,20 +325,34 @@ var DebugLogger = class {
|
|
|
228
325
|
this.enabled = enabled;
|
|
229
326
|
}
|
|
230
327
|
debug(message, context) {
|
|
328
|
+
this.write("debug", message, context);
|
|
329
|
+
}
|
|
330
|
+
info(message, context) {
|
|
331
|
+
this.write("info", message, context);
|
|
332
|
+
}
|
|
333
|
+
warn(message, context) {
|
|
334
|
+
this.write("warn", message, context);
|
|
335
|
+
}
|
|
336
|
+
error(message, context) {
|
|
337
|
+
this.write("error", message, context);
|
|
338
|
+
}
|
|
339
|
+
write(level, message, context) {
|
|
231
340
|
if (!this.enabled) {
|
|
232
341
|
return;
|
|
233
342
|
}
|
|
234
343
|
const suffix = context ? ` ${JSON.stringify(context)}` : "";
|
|
235
|
-
console
|
|
344
|
+
console[level](`[layercache] ${message}${suffix}`);
|
|
236
345
|
}
|
|
237
346
|
};
|
|
238
|
-
var CacheStack = class {
|
|
347
|
+
var CacheStack = class extends import_node_events.EventEmitter {
|
|
239
348
|
constructor(layers, options = {}) {
|
|
349
|
+
super();
|
|
240
350
|
this.layers = layers;
|
|
241
351
|
this.options = options;
|
|
242
352
|
if (layers.length === 0) {
|
|
243
353
|
throw new Error("CacheStack requires at least one cache layer.");
|
|
244
354
|
}
|
|
355
|
+
this.validateConfiguration();
|
|
245
356
|
const debugEnv = process.env.DEBUG?.split(",").includes("layercache:debug") ?? false;
|
|
246
357
|
this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
|
|
247
358
|
this.tagIndex = options.tagIndex ?? new TagIndex();
|
|
@@ -257,33 +368,47 @@ var CacheStack = class {
|
|
|
257
368
|
logger;
|
|
258
369
|
tagIndex;
|
|
259
370
|
backgroundRefreshes = /* @__PURE__ */ new Map();
|
|
371
|
+
accessProfiles = /* @__PURE__ */ new Map();
|
|
372
|
+
layerDegradedUntil = /* @__PURE__ */ new Map();
|
|
373
|
+
circuitBreakers = /* @__PURE__ */ new Map();
|
|
374
|
+
isDisconnecting = false;
|
|
375
|
+
disconnectPromise;
|
|
260
376
|
async get(key, fetcher, options) {
|
|
377
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
378
|
+
this.validateWriteOptions(options);
|
|
261
379
|
await this.startup;
|
|
262
|
-
const hit = await this.readFromLayers(
|
|
380
|
+
const hit = await this.readFromLayers(normalizedKey, options, "allow-stale");
|
|
263
381
|
if (hit.found) {
|
|
382
|
+
this.recordAccess(normalizedKey);
|
|
383
|
+
if (this.isNegativeStoredValue(hit.stored)) {
|
|
384
|
+
this.metrics.negativeCacheHits += 1;
|
|
385
|
+
}
|
|
264
386
|
if (hit.state === "fresh") {
|
|
265
387
|
this.metrics.hits += 1;
|
|
388
|
+
await this.applyFreshReadPolicies(normalizedKey, hit, options, fetcher);
|
|
266
389
|
return hit.value;
|
|
267
390
|
}
|
|
268
391
|
if (hit.state === "stale-while-revalidate") {
|
|
269
392
|
this.metrics.hits += 1;
|
|
270
393
|
this.metrics.staleHits += 1;
|
|
394
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
271
395
|
if (fetcher) {
|
|
272
|
-
this.scheduleBackgroundRefresh(
|
|
396
|
+
this.scheduleBackgroundRefresh(normalizedKey, fetcher, options);
|
|
273
397
|
}
|
|
274
398
|
return hit.value;
|
|
275
399
|
}
|
|
276
400
|
if (!fetcher) {
|
|
277
401
|
this.metrics.hits += 1;
|
|
278
402
|
this.metrics.staleHits += 1;
|
|
403
|
+
this.emit("stale-serve", { key: normalizedKey, state: hit.state, layer: hit.layerName });
|
|
279
404
|
return hit.value;
|
|
280
405
|
}
|
|
281
406
|
try {
|
|
282
|
-
return await this.fetchWithGuards(
|
|
407
|
+
return await this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
283
408
|
} catch (error) {
|
|
284
409
|
this.metrics.staleHits += 1;
|
|
285
410
|
this.metrics.refreshErrors += 1;
|
|
286
|
-
this.logger.debug("stale-if-error", { key, error: this.formatError(error) });
|
|
411
|
+
this.logger.debug?.("stale-if-error", { key: normalizedKey, error: this.formatError(error) });
|
|
287
412
|
return hit.value;
|
|
288
413
|
}
|
|
289
414
|
}
|
|
@@ -291,71 +416,144 @@ var CacheStack = class {
|
|
|
291
416
|
if (!fetcher) {
|
|
292
417
|
return null;
|
|
293
418
|
}
|
|
294
|
-
return this.fetchWithGuards(
|
|
419
|
+
return this.fetchWithGuards(normalizedKey, fetcher, options);
|
|
295
420
|
}
|
|
296
421
|
async set(key, value, options) {
|
|
422
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
423
|
+
this.validateWriteOptions(options);
|
|
297
424
|
await this.startup;
|
|
298
|
-
await this.storeEntry(
|
|
425
|
+
await this.storeEntry(normalizedKey, "value", value, options);
|
|
299
426
|
}
|
|
300
427
|
async delete(key) {
|
|
428
|
+
const normalizedKey = this.validateCacheKey(key);
|
|
301
429
|
await this.startup;
|
|
302
|
-
await this.deleteKeys([
|
|
303
|
-
await this.publishInvalidation({ scope: "key", keys: [
|
|
430
|
+
await this.deleteKeys([normalizedKey]);
|
|
431
|
+
await this.publishInvalidation({ scope: "key", keys: [normalizedKey], sourceId: this.instanceId, operation: "delete" });
|
|
304
432
|
}
|
|
305
433
|
async clear() {
|
|
306
434
|
await this.startup;
|
|
307
435
|
await Promise.all(this.layers.map((layer) => layer.clear()));
|
|
308
436
|
await this.tagIndex.clear();
|
|
437
|
+
this.accessProfiles.clear();
|
|
309
438
|
this.metrics.invalidations += 1;
|
|
310
|
-
this.logger.debug("clear");
|
|
439
|
+
this.logger.debug?.("clear");
|
|
311
440
|
await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
|
|
312
441
|
}
|
|
313
442
|
async mget(entries) {
|
|
314
443
|
if (entries.length === 0) {
|
|
315
444
|
return [];
|
|
316
445
|
}
|
|
317
|
-
const
|
|
446
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
447
|
+
...entry,
|
|
448
|
+
key: this.validateCacheKey(entry.key)
|
|
449
|
+
}));
|
|
450
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
451
|
+
const canFastPath = normalizedEntries.every((entry) => entry.fetch === void 0 && entry.options === void 0);
|
|
318
452
|
if (!canFastPath) {
|
|
319
|
-
|
|
453
|
+
const pendingReads = /* @__PURE__ */ new Map();
|
|
454
|
+
return Promise.all(
|
|
455
|
+
normalizedEntries.map((entry) => {
|
|
456
|
+
const optionsSignature = this.serializeOptions(entry.options);
|
|
457
|
+
const existing = pendingReads.get(entry.key);
|
|
458
|
+
if (!existing) {
|
|
459
|
+
const promise = this.get(entry.key, entry.fetch, entry.options);
|
|
460
|
+
pendingReads.set(entry.key, {
|
|
461
|
+
promise,
|
|
462
|
+
fetch: entry.fetch,
|
|
463
|
+
optionsSignature
|
|
464
|
+
});
|
|
465
|
+
return promise;
|
|
466
|
+
}
|
|
467
|
+
if (existing.fetch !== entry.fetch || existing.optionsSignature !== optionsSignature) {
|
|
468
|
+
throw new Error(`mget received conflicting entries for key "${entry.key}".`);
|
|
469
|
+
}
|
|
470
|
+
return existing.promise;
|
|
471
|
+
})
|
|
472
|
+
);
|
|
320
473
|
}
|
|
321
474
|
await this.startup;
|
|
322
|
-
const pending = new Set(
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
475
|
+
const pending = /* @__PURE__ */ new Set();
|
|
476
|
+
const indexesByKey = /* @__PURE__ */ new Map();
|
|
477
|
+
const resultsByKey = /* @__PURE__ */ new Map();
|
|
478
|
+
for (let index = 0; index < normalizedEntries.length; index += 1) {
|
|
479
|
+
const key = normalizedEntries[index].key;
|
|
480
|
+
const indexes = indexesByKey.get(key) ?? [];
|
|
481
|
+
indexes.push(index);
|
|
482
|
+
indexesByKey.set(key, indexes);
|
|
483
|
+
pending.add(key);
|
|
484
|
+
}
|
|
485
|
+
for (let layerIndex = 0; layerIndex < this.layers.length; layerIndex += 1) {
|
|
486
|
+
const layer = this.layers[layerIndex];
|
|
487
|
+
const keys = [...pending];
|
|
488
|
+
if (keys.length === 0) {
|
|
327
489
|
break;
|
|
328
490
|
}
|
|
329
|
-
const keys = indexes.map((index) => entries[index].key);
|
|
330
491
|
const values = layer.getMany ? await layer.getMany(keys) : await Promise.all(keys.map((key) => this.readLayerEntry(layer, key)));
|
|
331
492
|
for (let offset = 0; offset < values.length; offset += 1) {
|
|
332
|
-
const
|
|
493
|
+
const key = keys[offset];
|
|
333
494
|
const stored = values[offset];
|
|
334
495
|
if (stored === null) {
|
|
335
496
|
continue;
|
|
336
497
|
}
|
|
337
498
|
const resolved = resolveStoredValue(stored);
|
|
338
499
|
if (resolved.state === "expired") {
|
|
339
|
-
await layer.delete(
|
|
500
|
+
await layer.delete(key);
|
|
340
501
|
continue;
|
|
341
502
|
}
|
|
342
|
-
await this.tagIndex.touch(
|
|
343
|
-
await this.backfill(
|
|
344
|
-
|
|
345
|
-
pending.delete(
|
|
346
|
-
this.metrics.hits += 1;
|
|
503
|
+
await this.tagIndex.touch(key);
|
|
504
|
+
await this.backfill(key, stored, layerIndex - 1);
|
|
505
|
+
resultsByKey.set(key, resolved.value);
|
|
506
|
+
pending.delete(key);
|
|
507
|
+
this.metrics.hits += indexesByKey.get(key)?.length ?? 1;
|
|
347
508
|
}
|
|
348
509
|
}
|
|
349
510
|
if (pending.size > 0) {
|
|
350
|
-
for (const
|
|
351
|
-
await this.tagIndex.remove(
|
|
352
|
-
this.metrics.misses += 1;
|
|
511
|
+
for (const key of pending) {
|
|
512
|
+
await this.tagIndex.remove(key);
|
|
513
|
+
this.metrics.misses += indexesByKey.get(key)?.length ?? 1;
|
|
353
514
|
}
|
|
354
515
|
}
|
|
355
|
-
return
|
|
516
|
+
return normalizedEntries.map((entry) => resultsByKey.get(entry.key) ?? null);
|
|
356
517
|
}
|
|
357
518
|
async mset(entries) {
|
|
358
|
-
|
|
519
|
+
const normalizedEntries = entries.map((entry) => ({
|
|
520
|
+
...entry,
|
|
521
|
+
key: this.validateCacheKey(entry.key)
|
|
522
|
+
}));
|
|
523
|
+
normalizedEntries.forEach((entry) => this.validateWriteOptions(entry.options));
|
|
524
|
+
await Promise.all(normalizedEntries.map((entry) => this.set(entry.key, entry.value, entry.options)));
|
|
525
|
+
}
|
|
526
|
+
async warm(entries, options = {}) {
|
|
527
|
+
const concurrency = Math.max(1, options.concurrency ?? 4);
|
|
528
|
+
const queue = [...entries].sort((left, right) => (right.priority ?? 0) - (left.priority ?? 0));
|
|
529
|
+
const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => {
|
|
530
|
+
while (queue.length > 0) {
|
|
531
|
+
const entry = queue.shift();
|
|
532
|
+
if (!entry) {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
try {
|
|
536
|
+
await this.get(entry.key, entry.fetcher, entry.options);
|
|
537
|
+
this.emit("warm", { key: entry.key });
|
|
538
|
+
} catch (error) {
|
|
539
|
+
this.emitError("warm", { key: entry.key, error: this.formatError(error) });
|
|
540
|
+
if (!options.continueOnError) {
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
await Promise.all(workers);
|
|
547
|
+
}
|
|
548
|
+
wrap(prefix, fetcher, options = {}) {
|
|
549
|
+
return (...args) => {
|
|
550
|
+
const suffix = options.keyResolver ? options.keyResolver(...args) : args.map((argument) => this.serializeKeyPart(argument)).join(":");
|
|
551
|
+
const key = suffix.length > 0 ? `${prefix}:${suffix}` : prefix;
|
|
552
|
+
return this.get(key, () => fetcher(...args), options);
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
namespace(prefix) {
|
|
556
|
+
return new CacheNamespace(this, prefix);
|
|
359
557
|
}
|
|
360
558
|
async invalidateByTag(tag) {
|
|
361
559
|
await this.startup;
|
|
@@ -372,13 +570,74 @@ var CacheStack = class {
|
|
|
372
570
|
getMetrics() {
|
|
373
571
|
return { ...this.metrics };
|
|
374
572
|
}
|
|
573
|
+
getStats() {
|
|
574
|
+
return {
|
|
575
|
+
metrics: this.getMetrics(),
|
|
576
|
+
layers: this.layers.map((layer) => ({
|
|
577
|
+
name: layer.name,
|
|
578
|
+
isLocal: Boolean(layer.isLocal),
|
|
579
|
+
degradedUntil: this.layerDegradedUntil.get(layer.name) ?? null
|
|
580
|
+
})),
|
|
581
|
+
backgroundRefreshes: this.backgroundRefreshes.size
|
|
582
|
+
};
|
|
583
|
+
}
|
|
375
584
|
resetMetrics() {
|
|
376
585
|
Object.assign(this.metrics, EMPTY_METRICS());
|
|
377
586
|
}
|
|
378
|
-
async
|
|
587
|
+
async exportState() {
|
|
588
|
+
await this.startup;
|
|
589
|
+
const exported = /* @__PURE__ */ new Map();
|
|
590
|
+
for (const layer of this.layers) {
|
|
591
|
+
if (!layer.keys) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
const keys = await layer.keys();
|
|
595
|
+
for (const key of keys) {
|
|
596
|
+
if (exported.has(key)) {
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
const stored = await this.readLayerEntry(layer, key);
|
|
600
|
+
if (stored === null) {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
exported.set(key, {
|
|
604
|
+
key,
|
|
605
|
+
value: stored,
|
|
606
|
+
ttl: remainingStoredTtlSeconds(stored)
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return [...exported.values()];
|
|
611
|
+
}
|
|
612
|
+
async importState(entries) {
|
|
379
613
|
await this.startup;
|
|
380
|
-
await
|
|
381
|
-
|
|
614
|
+
await Promise.all(entries.map(async (entry) => {
|
|
615
|
+
await Promise.all(this.layers.map((layer) => layer.set(entry.key, entry.value, entry.ttl)));
|
|
616
|
+
await this.tagIndex.touch(entry.key);
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
async persistToFile(filePath) {
|
|
620
|
+
const snapshot = await this.exportState();
|
|
621
|
+
await import_node_fs.promises.writeFile(filePath, JSON.stringify(snapshot, null, 2), "utf8");
|
|
622
|
+
}
|
|
623
|
+
async restoreFromFile(filePath) {
|
|
624
|
+
const raw = await import_node_fs.promises.readFile(filePath, "utf8");
|
|
625
|
+
const snapshot = JSON.parse(raw);
|
|
626
|
+
if (!this.isCacheSnapshotEntries(snapshot)) {
|
|
627
|
+
throw new Error("Invalid snapshot file: expected CacheSnapshotEntry[]");
|
|
628
|
+
}
|
|
629
|
+
await this.importState(snapshot);
|
|
630
|
+
}
|
|
631
|
+
async disconnect() {
|
|
632
|
+
if (!this.disconnectPromise) {
|
|
633
|
+
this.isDisconnecting = true;
|
|
634
|
+
this.disconnectPromise = (async () => {
|
|
635
|
+
await this.startup;
|
|
636
|
+
await this.unsubscribeInvalidation?.();
|
|
637
|
+
await Promise.allSettled([...this.backgroundRefreshes.values()]);
|
|
638
|
+
})();
|
|
639
|
+
}
|
|
640
|
+
await this.disconnectPromise;
|
|
382
641
|
}
|
|
383
642
|
async initialize() {
|
|
384
643
|
if (!this.options.invalidationBus) {
|
|
@@ -418,6 +677,7 @@ var CacheStack = class {
|
|
|
418
677
|
const pollIntervalMs = this.options.singleFlightPollMs ?? DEFAULT_SINGLE_FLIGHT_POLL_MS;
|
|
419
678
|
const deadline = Date.now() + timeoutMs;
|
|
420
679
|
this.metrics.singleFlightWaits += 1;
|
|
680
|
+
this.emit("stampede-dedupe", { key });
|
|
421
681
|
while (Date.now() < deadline) {
|
|
422
682
|
const hit = await this.readFromLayers(key, options, "fresh-only");
|
|
423
683
|
if (hit.found) {
|
|
@@ -429,8 +689,16 @@ var CacheStack = class {
|
|
|
429
689
|
return this.fetchAndPopulate(key, fetcher, options);
|
|
430
690
|
}
|
|
431
691
|
async fetchAndPopulate(key, fetcher, options) {
|
|
692
|
+
this.assertCircuitClosed(key, options?.circuitBreaker ?? this.options.circuitBreaker);
|
|
432
693
|
this.metrics.fetches += 1;
|
|
433
|
-
|
|
694
|
+
let fetched;
|
|
695
|
+
try {
|
|
696
|
+
fetched = await fetcher();
|
|
697
|
+
this.resetCircuitBreaker(key);
|
|
698
|
+
} catch (error) {
|
|
699
|
+
this.recordCircuitFailure(key, options?.circuitBreaker ?? this.options.circuitBreaker, error);
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
434
702
|
if (fetched === null || fetched === void 0) {
|
|
435
703
|
if (!this.shouldNegativeCache(options)) {
|
|
436
704
|
return null;
|
|
@@ -449,8 +717,9 @@ var CacheStack = class {
|
|
|
449
717
|
await this.tagIndex.touch(key);
|
|
450
718
|
}
|
|
451
719
|
this.metrics.sets += 1;
|
|
452
|
-
this.logger.debug("set", { key, kind, tags: options?.tags });
|
|
453
|
-
|
|
720
|
+
this.logger.debug?.("set", { key, kind, tags: options?.tags });
|
|
721
|
+
this.emit("set", { key, kind, tags: options?.tags });
|
|
722
|
+
if (this.shouldBroadcastL1Invalidation()) {
|
|
454
723
|
await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
|
|
455
724
|
}
|
|
456
725
|
}
|
|
@@ -460,6 +729,7 @@ var CacheStack = class {
|
|
|
460
729
|
const layer = this.layers[index];
|
|
461
730
|
const stored = await this.readLayerEntry(layer, key);
|
|
462
731
|
if (stored === null) {
|
|
732
|
+
this.incrementMetricMap(this.metrics.missesByLayer, layer.name);
|
|
463
733
|
continue;
|
|
464
734
|
}
|
|
465
735
|
const resolved = resolveStoredValue(stored);
|
|
@@ -473,20 +743,34 @@ var CacheStack = class {
|
|
|
473
743
|
}
|
|
474
744
|
await this.tagIndex.touch(key);
|
|
475
745
|
await this.backfill(key, stored, index - 1, options);
|
|
476
|
-
this.
|
|
477
|
-
|
|
746
|
+
this.incrementMetricMap(this.metrics.hitsByLayer, layer.name);
|
|
747
|
+
this.logger.debug?.("hit", { key, layer: layer.name, state: resolved.state });
|
|
748
|
+
this.emit("hit", { key, layer: layer.name, state: resolved.state });
|
|
749
|
+
return { found: true, value: resolved.value, stored, state: resolved.state, layerIndex: index, layerName: layer.name };
|
|
478
750
|
}
|
|
479
751
|
if (!sawRetainableValue) {
|
|
480
752
|
await this.tagIndex.remove(key);
|
|
481
753
|
}
|
|
482
|
-
this.logger.debug("miss", { key, mode });
|
|
754
|
+
this.logger.debug?.("miss", { key, mode });
|
|
755
|
+
this.emit("miss", { key, mode });
|
|
483
756
|
return { found: false, value: null, stored: null, state: "miss" };
|
|
484
757
|
}
|
|
485
758
|
async readLayerEntry(layer, key) {
|
|
759
|
+
if (this.shouldSkipLayer(layer)) {
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
486
762
|
if (layer.getEntry) {
|
|
487
|
-
|
|
763
|
+
try {
|
|
764
|
+
return await layer.getEntry(key);
|
|
765
|
+
} catch (error) {
|
|
766
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
try {
|
|
770
|
+
return await layer.get(key);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
return this.handleLayerFailure(layer, "read", error);
|
|
488
773
|
}
|
|
489
|
-
return layer.get(key);
|
|
490
774
|
}
|
|
491
775
|
async backfill(key, stored, upToIndex, options) {
|
|
492
776
|
if (upToIndex < 0) {
|
|
@@ -494,16 +778,28 @@ var CacheStack = class {
|
|
|
494
778
|
}
|
|
495
779
|
for (let index = 0; index <= upToIndex; index += 1) {
|
|
496
780
|
const layer = this.layers[index];
|
|
781
|
+
if (this.shouldSkipLayer(layer)) {
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
497
784
|
const ttl = remainingStoredTtlSeconds(stored) ?? this.resolveLayerSeconds(layer.name, options?.ttl, void 0, layer.defaultTtl);
|
|
498
|
-
|
|
785
|
+
try {
|
|
786
|
+
await layer.set(key, stored, ttl);
|
|
787
|
+
} catch (error) {
|
|
788
|
+
await this.handleLayerFailure(layer, "backfill", error);
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
499
791
|
this.metrics.backfills += 1;
|
|
500
|
-
this.logger.debug("backfill", { key, layer: layer.name });
|
|
792
|
+
this.logger.debug?.("backfill", { key, layer: layer.name });
|
|
793
|
+
this.emit("backfill", { key, layer: layer.name });
|
|
501
794
|
}
|
|
502
795
|
}
|
|
503
796
|
async writeAcrossLayers(key, kind, value, options) {
|
|
504
797
|
const now = Date.now();
|
|
505
798
|
const operations = this.layers.map((layer) => async () => {
|
|
506
|
-
|
|
799
|
+
if (this.shouldSkipLayer(layer)) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const freshTtl = this.resolveFreshTtl(key, layer.name, kind, options, layer.defaultTtl);
|
|
507
803
|
const staleWhileRevalidate = this.resolveLayerSeconds(
|
|
508
804
|
layer.name,
|
|
509
805
|
options?.staleWhileRevalidate,
|
|
@@ -523,7 +819,11 @@ var CacheStack = class {
|
|
|
523
819
|
now
|
|
524
820
|
});
|
|
525
821
|
const ttl = remainingStoredTtlSeconds(payload, now) ?? freshTtl;
|
|
526
|
-
|
|
822
|
+
try {
|
|
823
|
+
await layer.set(key, payload, ttl);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
await this.handleLayerFailure(layer, "write", error);
|
|
826
|
+
}
|
|
527
827
|
});
|
|
528
828
|
await this.executeLayerOperations(operations, { key, action: kind === "empty" ? "negative-set" : "set" });
|
|
529
829
|
}
|
|
@@ -538,7 +838,7 @@ var CacheStack = class {
|
|
|
538
838
|
return;
|
|
539
839
|
}
|
|
540
840
|
this.metrics.writeFailures += failures.length;
|
|
541
|
-
this.logger.debug("write-failure", {
|
|
841
|
+
this.logger.debug?.("write-failure", {
|
|
542
842
|
...context,
|
|
543
843
|
failures: failures.map((failure) => this.formatError(failure.reason))
|
|
544
844
|
});
|
|
@@ -549,15 +849,21 @@ var CacheStack = class {
|
|
|
549
849
|
);
|
|
550
850
|
}
|
|
551
851
|
}
|
|
552
|
-
resolveFreshTtl(layerName, kind, options, fallbackTtl) {
|
|
852
|
+
resolveFreshTtl(key, layerName, kind, options, fallbackTtl) {
|
|
553
853
|
const baseTtl = kind === "empty" ? this.resolveLayerSeconds(
|
|
554
854
|
layerName,
|
|
555
855
|
options?.negativeTtl,
|
|
556
856
|
this.options.negativeTtl,
|
|
557
857
|
this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl) ?? DEFAULT_NEGATIVE_TTL_SECONDS
|
|
558
858
|
) : this.resolveLayerSeconds(layerName, options?.ttl, void 0, fallbackTtl);
|
|
859
|
+
const adaptiveTtl = this.applyAdaptiveTtl(
|
|
860
|
+
key,
|
|
861
|
+
layerName,
|
|
862
|
+
baseTtl,
|
|
863
|
+
options?.adaptiveTtl ?? this.options.adaptiveTtl
|
|
864
|
+
);
|
|
559
865
|
const jitter = this.resolveLayerSeconds(layerName, options?.ttlJitter, this.options.ttlJitter);
|
|
560
|
-
return this.applyJitter(
|
|
866
|
+
return this.applyJitter(adaptiveTtl, jitter);
|
|
561
867
|
}
|
|
562
868
|
resolveLayerSeconds(layerName, override, globalDefault, fallback) {
|
|
563
869
|
if (override !== void 0) {
|
|
@@ -585,7 +891,7 @@ var CacheStack = class {
|
|
|
585
891
|
return options?.negativeCache ?? this.options.negativeCaching ?? false;
|
|
586
892
|
}
|
|
587
893
|
scheduleBackgroundRefresh(key, fetcher, options) {
|
|
588
|
-
if (this.backgroundRefreshes.has(key)) {
|
|
894
|
+
if (this.isDisconnecting || this.backgroundRefreshes.has(key)) {
|
|
589
895
|
return;
|
|
590
896
|
}
|
|
591
897
|
const refresh = (async () => {
|
|
@@ -594,7 +900,7 @@ var CacheStack = class {
|
|
|
594
900
|
await this.fetchWithGuards(key, fetcher, options);
|
|
595
901
|
} catch (error) {
|
|
596
902
|
this.metrics.refreshErrors += 1;
|
|
597
|
-
this.logger.debug("refresh-error", { key, error: this.formatError(error) });
|
|
903
|
+
this.logger.debug?.("refresh-error", { key, error: this.formatError(error) });
|
|
598
904
|
} finally {
|
|
599
905
|
this.backgroundRefreshes.delete(key);
|
|
600
906
|
}
|
|
@@ -612,21 +918,15 @@ var CacheStack = class {
|
|
|
612
918
|
if (keys.length === 0) {
|
|
613
919
|
return;
|
|
614
920
|
}
|
|
615
|
-
await
|
|
616
|
-
this.layers.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
|
-
);
|
|
921
|
+
await this.deleteKeysFromLayers(this.layers, keys);
|
|
624
922
|
for (const key of keys) {
|
|
625
923
|
await this.tagIndex.remove(key);
|
|
924
|
+
this.accessProfiles.delete(key);
|
|
626
925
|
}
|
|
627
926
|
this.metrics.deletes += keys.length;
|
|
628
927
|
this.metrics.invalidations += 1;
|
|
629
|
-
this.logger.debug("delete", { keys });
|
|
928
|
+
this.logger.debug?.("delete", { keys });
|
|
929
|
+
this.emit("delete", { keys });
|
|
630
930
|
}
|
|
631
931
|
async publishInvalidation(message) {
|
|
632
932
|
if (!this.options.invalidationBus) {
|
|
@@ -645,21 +945,15 @@ var CacheStack = class {
|
|
|
645
945
|
if (message.scope === "clear") {
|
|
646
946
|
await Promise.all(localLayers.map((layer) => layer.clear()));
|
|
647
947
|
await this.tagIndex.clear();
|
|
948
|
+
this.accessProfiles.clear();
|
|
648
949
|
return;
|
|
649
950
|
}
|
|
650
951
|
const keys = message.keys ?? [];
|
|
651
|
-
await
|
|
652
|
-
localLayers.map(async (layer) => {
|
|
653
|
-
if (layer.deleteMany) {
|
|
654
|
-
await layer.deleteMany(keys);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
await Promise.all(keys.map((key) => layer.delete(key)));
|
|
658
|
-
})
|
|
659
|
-
);
|
|
952
|
+
await this.deleteKeysFromLayers(localLayers, keys);
|
|
660
953
|
if (message.operation !== "write") {
|
|
661
954
|
for (const key of keys) {
|
|
662
955
|
await this.tagIndex.remove(key);
|
|
956
|
+
this.accessProfiles.delete(key);
|
|
663
957
|
}
|
|
664
958
|
}
|
|
665
959
|
}
|
|
@@ -672,6 +966,257 @@ var CacheStack = class {
|
|
|
672
966
|
sleep(ms) {
|
|
673
967
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
674
968
|
}
|
|
969
|
+
shouldBroadcastL1Invalidation() {
|
|
970
|
+
return this.options.broadcastL1Invalidation ?? this.options.publishSetInvalidation ?? true;
|
|
971
|
+
}
|
|
972
|
+
async deleteKeysFromLayers(layers, keys) {
|
|
973
|
+
await Promise.all(
|
|
974
|
+
layers.map(async (layer) => {
|
|
975
|
+
if (this.shouldSkipLayer(layer)) {
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (layer.deleteMany) {
|
|
979
|
+
try {
|
|
980
|
+
await layer.deleteMany(keys);
|
|
981
|
+
} catch (error) {
|
|
982
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
983
|
+
}
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
await Promise.all(keys.map(async (key) => {
|
|
987
|
+
try {
|
|
988
|
+
await layer.delete(key);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
await this.handleLayerFailure(layer, "delete", error);
|
|
991
|
+
}
|
|
992
|
+
}));
|
|
993
|
+
})
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
validateConfiguration() {
|
|
997
|
+
if (this.options.broadcastL1Invalidation !== void 0 && this.options.publishSetInvalidation !== void 0 && this.options.broadcastL1Invalidation !== this.options.publishSetInvalidation) {
|
|
998
|
+
throw new Error("broadcastL1Invalidation and publishSetInvalidation cannot conflict.");
|
|
999
|
+
}
|
|
1000
|
+
if (this.options.stampedePrevention === false && this.options.singleFlightCoordinator) {
|
|
1001
|
+
throw new Error("singleFlightCoordinator requires stampedePrevention to remain enabled.");
|
|
1002
|
+
}
|
|
1003
|
+
this.validateLayerNumberOption("negativeTtl", this.options.negativeTtl);
|
|
1004
|
+
this.validateLayerNumberOption("staleWhileRevalidate", this.options.staleWhileRevalidate);
|
|
1005
|
+
this.validateLayerNumberOption("staleIfError", this.options.staleIfError);
|
|
1006
|
+
this.validateLayerNumberOption("ttlJitter", this.options.ttlJitter);
|
|
1007
|
+
this.validateLayerNumberOption("refreshAhead", this.options.refreshAhead);
|
|
1008
|
+
this.validatePositiveNumber("singleFlightLeaseMs", this.options.singleFlightLeaseMs);
|
|
1009
|
+
this.validatePositiveNumber("singleFlightTimeoutMs", this.options.singleFlightTimeoutMs);
|
|
1010
|
+
this.validatePositiveNumber("singleFlightPollMs", this.options.singleFlightPollMs);
|
|
1011
|
+
this.validateAdaptiveTtlOptions(this.options.adaptiveTtl);
|
|
1012
|
+
this.validateCircuitBreakerOptions(this.options.circuitBreaker);
|
|
1013
|
+
}
|
|
1014
|
+
validateWriteOptions(options) {
|
|
1015
|
+
if (!options) {
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
this.validateLayerNumberOption("options.ttl", options.ttl);
|
|
1019
|
+
this.validateLayerNumberOption("options.negativeTtl", options.negativeTtl);
|
|
1020
|
+
this.validateLayerNumberOption("options.staleWhileRevalidate", options.staleWhileRevalidate);
|
|
1021
|
+
this.validateLayerNumberOption("options.staleIfError", options.staleIfError);
|
|
1022
|
+
this.validateLayerNumberOption("options.ttlJitter", options.ttlJitter);
|
|
1023
|
+
this.validateLayerNumberOption("options.refreshAhead", options.refreshAhead);
|
|
1024
|
+
this.validateAdaptiveTtlOptions(options.adaptiveTtl);
|
|
1025
|
+
this.validateCircuitBreakerOptions(options.circuitBreaker);
|
|
1026
|
+
}
|
|
1027
|
+
validateLayerNumberOption(name, value) {
|
|
1028
|
+
if (value === void 0) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
if (typeof value === "number") {
|
|
1032
|
+
this.validateNonNegativeNumber(name, value);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
for (const [layerName, layerValue] of Object.entries(value)) {
|
|
1036
|
+
if (layerValue === void 0) {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
this.validateNonNegativeNumber(`${name}.${layerName}`, layerValue);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
validatePositiveNumber(name, value) {
|
|
1043
|
+
if (value === void 0) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
1047
|
+
throw new Error(`${name} must be a positive finite number.`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
validateNonNegativeNumber(name, value) {
|
|
1051
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
1052
|
+
throw new Error(`${name} must be a non-negative finite number.`);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
validateCacheKey(key) {
|
|
1056
|
+
if (key.length === 0) {
|
|
1057
|
+
throw new Error("Cache key must not be empty.");
|
|
1058
|
+
}
|
|
1059
|
+
if (key.length > MAX_CACHE_KEY_LENGTH) {
|
|
1060
|
+
throw new Error(`Cache key length must be at most ${MAX_CACHE_KEY_LENGTH} characters.`);
|
|
1061
|
+
}
|
|
1062
|
+
if (/[\u0000-\u001F\u007F]/.test(key)) {
|
|
1063
|
+
throw new Error("Cache key contains unsupported control characters.");
|
|
1064
|
+
}
|
|
1065
|
+
return key;
|
|
1066
|
+
}
|
|
1067
|
+
serializeOptions(options) {
|
|
1068
|
+
return JSON.stringify(this.normalizeForSerialization(options) ?? null);
|
|
1069
|
+
}
|
|
1070
|
+
validateAdaptiveTtlOptions(options) {
|
|
1071
|
+
if (!options || options === true) {
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
this.validatePositiveNumber("adaptiveTtl.hotAfter", options.hotAfter);
|
|
1075
|
+
this.validateLayerNumberOption("adaptiveTtl.step", options.step);
|
|
1076
|
+
this.validateLayerNumberOption("adaptiveTtl.maxTtl", options.maxTtl);
|
|
1077
|
+
}
|
|
1078
|
+
validateCircuitBreakerOptions(options) {
|
|
1079
|
+
if (!options) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
this.validatePositiveNumber("circuitBreaker.failureThreshold", options.failureThreshold);
|
|
1083
|
+
this.validatePositiveNumber("circuitBreaker.cooldownMs", options.cooldownMs);
|
|
1084
|
+
}
|
|
1085
|
+
async applyFreshReadPolicies(key, hit, options, fetcher) {
|
|
1086
|
+
const refreshAhead = this.resolveLayerSeconds(hit.layerName, options?.refreshAhead, this.options.refreshAhead, 0) ?? 0;
|
|
1087
|
+
const remainingFreshTtl = remainingFreshTtlSeconds(hit.stored) ?? 0;
|
|
1088
|
+
if ((options?.slidingTtl ?? false) && isStoredValueEnvelope(hit.stored)) {
|
|
1089
|
+
const refreshed = refreshStoredEnvelope(hit.stored);
|
|
1090
|
+
const ttl = remainingStoredTtlSeconds(refreshed);
|
|
1091
|
+
for (let index = 0; index <= hit.layerIndex; index += 1) {
|
|
1092
|
+
const layer = this.layers[index];
|
|
1093
|
+
if (this.shouldSkipLayer(layer)) {
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
await layer.set(key, refreshed, ttl);
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
await this.handleLayerFailure(layer, "sliding-ttl", error);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (fetcher && refreshAhead > 0 && remainingFreshTtl > 0 && remainingFreshTtl <= refreshAhead) {
|
|
1104
|
+
this.scheduleBackgroundRefresh(key, fetcher, options);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
applyAdaptiveTtl(key, layerName, ttl, adaptiveTtl) {
|
|
1108
|
+
if (!ttl || !adaptiveTtl) {
|
|
1109
|
+
return ttl;
|
|
1110
|
+
}
|
|
1111
|
+
const profile = this.accessProfiles.get(key);
|
|
1112
|
+
if (!profile) {
|
|
1113
|
+
return ttl;
|
|
1114
|
+
}
|
|
1115
|
+
const config = adaptiveTtl === true ? {} : adaptiveTtl;
|
|
1116
|
+
const hotAfter = config.hotAfter ?? 3;
|
|
1117
|
+
if (profile.hits < hotAfter) {
|
|
1118
|
+
return ttl;
|
|
1119
|
+
}
|
|
1120
|
+
const step = this.resolveLayerSeconds(layerName, config.step, void 0, Math.max(1, Math.round(ttl / 2))) ?? 0;
|
|
1121
|
+
const maxTtl = this.resolveLayerSeconds(layerName, config.maxTtl, void 0, ttl + step * 4) ?? ttl;
|
|
1122
|
+
const multiplier = Math.floor(profile.hits / hotAfter);
|
|
1123
|
+
return Math.min(maxTtl, ttl + step * multiplier);
|
|
1124
|
+
}
|
|
1125
|
+
recordAccess(key) {
|
|
1126
|
+
const profile = this.accessProfiles.get(key) ?? { hits: 0, lastAccessAt: Date.now() };
|
|
1127
|
+
profile.hits += 1;
|
|
1128
|
+
profile.lastAccessAt = Date.now();
|
|
1129
|
+
this.accessProfiles.set(key, profile);
|
|
1130
|
+
}
|
|
1131
|
+
incrementMetricMap(target, key) {
|
|
1132
|
+
target[key] = (target[key] ?? 0) + 1;
|
|
1133
|
+
}
|
|
1134
|
+
shouldSkipLayer(layer) {
|
|
1135
|
+
const degradedUntil = this.layerDegradedUntil.get(layer.name);
|
|
1136
|
+
return degradedUntil !== void 0 && degradedUntil > Date.now();
|
|
1137
|
+
}
|
|
1138
|
+
async handleLayerFailure(layer, operation, error) {
|
|
1139
|
+
if (!this.isGracefulDegradationEnabled()) {
|
|
1140
|
+
throw error;
|
|
1141
|
+
}
|
|
1142
|
+
const retryAfterMs = typeof this.options.gracefulDegradation === "object" ? this.options.gracefulDegradation.retryAfterMs ?? 1e4 : 1e4;
|
|
1143
|
+
this.layerDegradedUntil.set(layer.name, Date.now() + retryAfterMs);
|
|
1144
|
+
this.metrics.degradedOperations += 1;
|
|
1145
|
+
this.logger.warn?.("layer-degraded", { layer: layer.name, operation, error: this.formatError(error) });
|
|
1146
|
+
this.emitError(operation, { layer: layer.name, degraded: true, error: this.formatError(error) });
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
isGracefulDegradationEnabled() {
|
|
1150
|
+
return Boolean(this.options.gracefulDegradation);
|
|
1151
|
+
}
|
|
1152
|
+
assertCircuitClosed(key, options) {
|
|
1153
|
+
const state = this.circuitBreakers.get(key);
|
|
1154
|
+
if (!state?.openUntil) {
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (state.openUntil <= Date.now()) {
|
|
1158
|
+
state.openUntil = null;
|
|
1159
|
+
state.failures = 0;
|
|
1160
|
+
this.circuitBreakers.set(key, state);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
this.emitError("circuit-breaker-open", { key, openUntil: state.openUntil });
|
|
1164
|
+
throw new Error(`Circuit breaker is open for key "${key}".`);
|
|
1165
|
+
}
|
|
1166
|
+
recordCircuitFailure(key, options, error) {
|
|
1167
|
+
if (!options) {
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
const failureThreshold = options.failureThreshold ?? 3;
|
|
1171
|
+
const cooldownMs = options.cooldownMs ?? 3e4;
|
|
1172
|
+
const state = this.circuitBreakers.get(key) ?? { failures: 0, openUntil: null };
|
|
1173
|
+
state.failures += 1;
|
|
1174
|
+
if (state.failures >= failureThreshold) {
|
|
1175
|
+
state.openUntil = Date.now() + cooldownMs;
|
|
1176
|
+
this.metrics.circuitBreakerTrips += 1;
|
|
1177
|
+
}
|
|
1178
|
+
this.circuitBreakers.set(key, state);
|
|
1179
|
+
this.emitError("fetch", { key, error: this.formatError(error), failures: state.failures });
|
|
1180
|
+
}
|
|
1181
|
+
resetCircuitBreaker(key) {
|
|
1182
|
+
this.circuitBreakers.delete(key);
|
|
1183
|
+
}
|
|
1184
|
+
isNegativeStoredValue(stored) {
|
|
1185
|
+
return isStoredValueEnvelope(stored) && stored.kind === "empty";
|
|
1186
|
+
}
|
|
1187
|
+
emitError(operation, context) {
|
|
1188
|
+
this.logger.error?.(operation, context);
|
|
1189
|
+
if (this.listenerCount("error") > 0) {
|
|
1190
|
+
this.emit("error", { operation, ...context });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
serializeKeyPart(value) {
|
|
1194
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1195
|
+
return String(value);
|
|
1196
|
+
}
|
|
1197
|
+
return JSON.stringify(this.normalizeForSerialization(value));
|
|
1198
|
+
}
|
|
1199
|
+
isCacheSnapshotEntries(value) {
|
|
1200
|
+
return Array.isArray(value) && value.every((entry) => {
|
|
1201
|
+
if (!entry || typeof entry !== "object") {
|
|
1202
|
+
return false;
|
|
1203
|
+
}
|
|
1204
|
+
const candidate = entry;
|
|
1205
|
+
return typeof candidate.key === "string";
|
|
1206
|
+
});
|
|
1207
|
+
}
|
|
1208
|
+
normalizeForSerialization(value) {
|
|
1209
|
+
if (Array.isArray(value)) {
|
|
1210
|
+
return value.map((entry) => this.normalizeForSerialization(entry));
|
|
1211
|
+
}
|
|
1212
|
+
if (value && typeof value === "object") {
|
|
1213
|
+
return Object.keys(value).sort().reduce((normalized, key) => {
|
|
1214
|
+
normalized[key] = this.normalizeForSerialization(value[key]);
|
|
1215
|
+
return normalized;
|
|
1216
|
+
}, {});
|
|
1217
|
+
}
|
|
1218
|
+
return value;
|
|
1219
|
+
}
|
|
675
1220
|
};
|
|
676
1221
|
|
|
677
1222
|
// src/invalidation/RedisInvalidationBus.ts
|
|
@@ -679,19 +1224,27 @@ var RedisInvalidationBus = class {
|
|
|
679
1224
|
channel;
|
|
680
1225
|
publisher;
|
|
681
1226
|
subscriber;
|
|
1227
|
+
activeListener;
|
|
682
1228
|
constructor(options) {
|
|
683
1229
|
this.publisher = options.publisher;
|
|
684
1230
|
this.subscriber = options.subscriber ?? options.publisher.duplicate();
|
|
685
1231
|
this.channel = options.channel ?? "layercache:invalidation";
|
|
686
1232
|
}
|
|
687
1233
|
async subscribe(handler) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
1234
|
+
if (this.activeListener) {
|
|
1235
|
+
throw new Error("RedisInvalidationBus already has an active subscription.");
|
|
1236
|
+
}
|
|
1237
|
+
const listener = (_channel, payload) => {
|
|
1238
|
+
void this.handleMessage(payload, handler);
|
|
691
1239
|
};
|
|
1240
|
+
this.activeListener = listener;
|
|
692
1241
|
this.subscriber.on("message", listener);
|
|
693
1242
|
await this.subscriber.subscribe(this.channel);
|
|
694
1243
|
return async () => {
|
|
1244
|
+
if (this.activeListener !== listener) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
this.activeListener = void 0;
|
|
695
1248
|
this.subscriber.off("message", listener);
|
|
696
1249
|
await this.subscriber.unsubscribe(this.channel);
|
|
697
1250
|
};
|
|
@@ -699,6 +1252,37 @@ var RedisInvalidationBus = class {
|
|
|
699
1252
|
async publish(message) {
|
|
700
1253
|
await this.publisher.publish(this.channel, JSON.stringify(message));
|
|
701
1254
|
}
|
|
1255
|
+
async handleMessage(payload, handler) {
|
|
1256
|
+
let message;
|
|
1257
|
+
try {
|
|
1258
|
+
const parsed = JSON.parse(payload);
|
|
1259
|
+
if (!this.isInvalidationMessage(parsed)) {
|
|
1260
|
+
throw new Error("Invalid invalidation payload shape.");
|
|
1261
|
+
}
|
|
1262
|
+
message = parsed;
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
this.reportError("invalid invalidation payload", error);
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
try {
|
|
1268
|
+
await handler(message);
|
|
1269
|
+
} catch (error) {
|
|
1270
|
+
this.reportError("invalidation handler failed", error);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
isInvalidationMessage(value) {
|
|
1274
|
+
if (!value || typeof value !== "object") {
|
|
1275
|
+
return false;
|
|
1276
|
+
}
|
|
1277
|
+
const candidate = value;
|
|
1278
|
+
const validScope = candidate.scope === "key" || candidate.scope === "keys" || candidate.scope === "clear";
|
|
1279
|
+
const validOperation = candidate.operation === void 0 || candidate.operation === "write" || candidate.operation === "delete" || candidate.operation === "invalidate" || candidate.operation === "clear";
|
|
1280
|
+
const validKeys = candidate.keys === void 0 || Array.isArray(candidate.keys) && candidate.keys.every((key) => typeof key === "string");
|
|
1281
|
+
return validScope && typeof candidate.sourceId === "string" && candidate.sourceId.length > 0 && validOperation && validKeys;
|
|
1282
|
+
}
|
|
1283
|
+
reportError(message, error) {
|
|
1284
|
+
console.error(`[layercache] ${message}`, error);
|
|
1285
|
+
}
|
|
702
1286
|
};
|
|
703
1287
|
|
|
704
1288
|
// src/invalidation/RedisTagIndex.ts
|
|
@@ -791,6 +1375,84 @@ var RedisTagIndex = class {
|
|
|
791
1375
|
}
|
|
792
1376
|
};
|
|
793
1377
|
|
|
1378
|
+
// src/http/createCacheStatsHandler.ts
|
|
1379
|
+
function createCacheStatsHandler(cache) {
|
|
1380
|
+
return async (_request, response) => {
|
|
1381
|
+
response.statusCode = 200;
|
|
1382
|
+
response.setHeader?.("content-type", "application/json; charset=utf-8");
|
|
1383
|
+
response.end(JSON.stringify(cache.getStats(), null, 2));
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
// src/decorators/createCachedMethodDecorator.ts
|
|
1388
|
+
function createCachedMethodDecorator(options) {
|
|
1389
|
+
const wrappedByInstance = /* @__PURE__ */ new WeakMap();
|
|
1390
|
+
return ((_, propertyKey, descriptor) => {
|
|
1391
|
+
const original = descriptor.value;
|
|
1392
|
+
if (typeof original !== "function") {
|
|
1393
|
+
throw new Error("createCachedMethodDecorator can only be applied to methods.");
|
|
1394
|
+
}
|
|
1395
|
+
descriptor.value = async function(...args) {
|
|
1396
|
+
const instance = this;
|
|
1397
|
+
let wrapped = wrappedByInstance.get(instance);
|
|
1398
|
+
if (!wrapped) {
|
|
1399
|
+
const cache = options.cache(instance);
|
|
1400
|
+
wrapped = cache.wrap(
|
|
1401
|
+
options.prefix ?? String(propertyKey),
|
|
1402
|
+
(...methodArgs) => Promise.resolve(original.apply(instance, methodArgs)),
|
|
1403
|
+
options
|
|
1404
|
+
);
|
|
1405
|
+
wrappedByInstance.set(instance, wrapped);
|
|
1406
|
+
}
|
|
1407
|
+
return wrapped(...args);
|
|
1408
|
+
};
|
|
1409
|
+
});
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/integrations/fastify.ts
|
|
1413
|
+
function createFastifyLayercachePlugin(cache, options = {}) {
|
|
1414
|
+
return async (fastify) => {
|
|
1415
|
+
fastify.decorate("cache", cache);
|
|
1416
|
+
if (options.exposeStatsRoute !== false && fastify.get) {
|
|
1417
|
+
fastify.get(options.statsPath ?? "/cache/stats", async () => cache.getStats());
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/integrations/graphql.ts
|
|
1423
|
+
function cacheGraphqlResolver(cache, prefix, resolver, options = {}) {
|
|
1424
|
+
const wrapped = cache.wrap(prefix, resolver, {
|
|
1425
|
+
...options,
|
|
1426
|
+
keyResolver: options.keyResolver
|
|
1427
|
+
});
|
|
1428
|
+
return (...args) => wrapped(...args);
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// src/integrations/trpc.ts
|
|
1432
|
+
function createTrpcCacheMiddleware(cache, prefix, options = {}) {
|
|
1433
|
+
return async (context) => {
|
|
1434
|
+
const key = options.keyResolver ? `${prefix}:${options.keyResolver(context.rawInput, context.path, context.type)}` : `${prefix}:${context.path ?? "procedure"}:${JSON.stringify(context.rawInput ?? null)}`;
|
|
1435
|
+
let didFetch = false;
|
|
1436
|
+
let fetchedResult = null;
|
|
1437
|
+
const cached = await cache.get(
|
|
1438
|
+
key,
|
|
1439
|
+
async () => {
|
|
1440
|
+
didFetch = true;
|
|
1441
|
+
fetchedResult = await context.next();
|
|
1442
|
+
return fetchedResult;
|
|
1443
|
+
},
|
|
1444
|
+
options
|
|
1445
|
+
);
|
|
1446
|
+
if (cached !== null) {
|
|
1447
|
+
return cached;
|
|
1448
|
+
}
|
|
1449
|
+
if (didFetch) {
|
|
1450
|
+
return fetchedResult;
|
|
1451
|
+
}
|
|
1452
|
+
return context.next();
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
|
|
794
1456
|
// src/layers/MemoryLayer.ts
|
|
795
1457
|
var MemoryLayer = class {
|
|
796
1458
|
name;
|
|
@@ -856,6 +1518,32 @@ var MemoryLayer = class {
|
|
|
856
1518
|
this.pruneExpired();
|
|
857
1519
|
return [...this.entries.keys()];
|
|
858
1520
|
}
|
|
1521
|
+
exportState() {
|
|
1522
|
+
this.pruneExpired();
|
|
1523
|
+
return [...this.entries.entries()].map(([key, entry]) => ({
|
|
1524
|
+
key,
|
|
1525
|
+
value: entry.value,
|
|
1526
|
+
expiresAt: entry.expiresAt
|
|
1527
|
+
}));
|
|
1528
|
+
}
|
|
1529
|
+
importState(entries) {
|
|
1530
|
+
for (const entry of entries) {
|
|
1531
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
1532
|
+
continue;
|
|
1533
|
+
}
|
|
1534
|
+
this.entries.set(entry.key, {
|
|
1535
|
+
value: entry.value,
|
|
1536
|
+
expiresAt: entry.expiresAt
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
while (this.entries.size > this.maxSize) {
|
|
1540
|
+
const oldestKey = this.entries.keys().next().value;
|
|
1541
|
+
if (!oldestKey) {
|
|
1542
|
+
break;
|
|
1543
|
+
}
|
|
1544
|
+
this.entries.delete(oldestKey);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
859
1547
|
pruneExpired() {
|
|
860
1548
|
for (const [key, entry] of this.entries.entries()) {
|
|
861
1549
|
if (this.isExpired(entry)) {
|
|
@@ -868,6 +1556,9 @@ var MemoryLayer = class {
|
|
|
868
1556
|
}
|
|
869
1557
|
};
|
|
870
1558
|
|
|
1559
|
+
// src/layers/RedisLayer.ts
|
|
1560
|
+
var import_node_zlib = require("zlib");
|
|
1561
|
+
|
|
871
1562
|
// src/serialization/JsonSerializer.ts
|
|
872
1563
|
var JsonSerializer = class {
|
|
873
1564
|
serialize(value) {
|
|
@@ -889,6 +1580,8 @@ var RedisLayer = class {
|
|
|
889
1580
|
prefix;
|
|
890
1581
|
allowUnprefixedClear;
|
|
891
1582
|
scanCount;
|
|
1583
|
+
compression;
|
|
1584
|
+
compressionThreshold;
|
|
892
1585
|
constructor(options) {
|
|
893
1586
|
this.client = options.client;
|
|
894
1587
|
this.defaultTtl = options.ttl;
|
|
@@ -897,6 +1590,8 @@ var RedisLayer = class {
|
|
|
897
1590
|
this.prefix = options.prefix ?? "";
|
|
898
1591
|
this.allowUnprefixedClear = options.allowUnprefixedClear ?? false;
|
|
899
1592
|
this.scanCount = options.scanCount ?? 100;
|
|
1593
|
+
this.compression = options.compression;
|
|
1594
|
+
this.compressionThreshold = options.compressionThreshold ?? 1024;
|
|
900
1595
|
}
|
|
901
1596
|
async get(key) {
|
|
902
1597
|
const payload = await this.getEntry(key);
|
|
@@ -907,7 +1602,7 @@ var RedisLayer = class {
|
|
|
907
1602
|
if (payload === null) {
|
|
908
1603
|
return null;
|
|
909
1604
|
}
|
|
910
|
-
return this.
|
|
1605
|
+
return this.deserializeOrDelete(key, payload);
|
|
911
1606
|
}
|
|
912
1607
|
async getMany(keys) {
|
|
913
1608
|
if (keys.length === 0) {
|
|
@@ -921,16 +1616,18 @@ var RedisLayer = class {
|
|
|
921
1616
|
if (results === null) {
|
|
922
1617
|
return keys.map(() => null);
|
|
923
1618
|
}
|
|
924
|
-
return
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
1619
|
+
return Promise.all(
|
|
1620
|
+
results.map(async (result, index) => {
|
|
1621
|
+
const [error, payload] = result;
|
|
1622
|
+
if (error || payload === null || !this.isSerializablePayload(payload)) {
|
|
1623
|
+
return null;
|
|
1624
|
+
}
|
|
1625
|
+
return this.deserializeOrDelete(keys[index], payload);
|
|
1626
|
+
})
|
|
1627
|
+
);
|
|
931
1628
|
}
|
|
932
1629
|
async set(key, value, ttl = this.defaultTtl) {
|
|
933
|
-
const payload = this.serializer.serialize(value);
|
|
1630
|
+
const payload = this.encodePayload(this.serializer.serialize(value));
|
|
934
1631
|
const normalizedKey = this.withPrefix(key);
|
|
935
1632
|
if (ttl && ttl > 0) {
|
|
936
1633
|
await this.client.set(normalizedKey, payload, "EX", ttl);
|
|
@@ -977,6 +1674,41 @@ var RedisLayer = class {
|
|
|
977
1674
|
withPrefix(key) {
|
|
978
1675
|
return `${this.prefix}${key}`;
|
|
979
1676
|
}
|
|
1677
|
+
async deserializeOrDelete(key, payload) {
|
|
1678
|
+
try {
|
|
1679
|
+
return this.serializer.deserialize(this.decodePayload(payload));
|
|
1680
|
+
} catch {
|
|
1681
|
+
await this.client.del(this.withPrefix(key)).catch(() => void 0);
|
|
1682
|
+
return null;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
isSerializablePayload(payload) {
|
|
1686
|
+
return typeof payload === "string" || Buffer.isBuffer(payload);
|
|
1687
|
+
}
|
|
1688
|
+
encodePayload(payload) {
|
|
1689
|
+
if (!this.compression) {
|
|
1690
|
+
return payload;
|
|
1691
|
+
}
|
|
1692
|
+
const source = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
1693
|
+
if (source.byteLength < this.compressionThreshold) {
|
|
1694
|
+
return payload;
|
|
1695
|
+
}
|
|
1696
|
+
const header = Buffer.from(`LCZ1:${this.compression}:`);
|
|
1697
|
+
const compressed = this.compression === "gzip" ? (0, import_node_zlib.gzipSync)(source) : (0, import_node_zlib.brotliCompressSync)(source);
|
|
1698
|
+
return Buffer.concat([header, compressed]);
|
|
1699
|
+
}
|
|
1700
|
+
decodePayload(payload) {
|
|
1701
|
+
if (!Buffer.isBuffer(payload)) {
|
|
1702
|
+
return payload;
|
|
1703
|
+
}
|
|
1704
|
+
if (payload.subarray(0, 10).toString() === "LCZ1:gzip:") {
|
|
1705
|
+
return (0, import_node_zlib.gunzipSync)(payload.subarray(10));
|
|
1706
|
+
}
|
|
1707
|
+
if (payload.subarray(0, 12).toString() === "LCZ1:brotli:") {
|
|
1708
|
+
return (0, import_node_zlib.brotliDecompressSync)(payload.subarray(12));
|
|
1709
|
+
}
|
|
1710
|
+
return payload;
|
|
1711
|
+
}
|
|
980
1712
|
};
|
|
981
1713
|
|
|
982
1714
|
// src/serialization/MsgpackSerializer.ts
|
|
@@ -1022,6 +1754,7 @@ var RedisSingleFlightCoordinator = class {
|
|
|
1022
1754
|
};
|
|
1023
1755
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1024
1756
|
0 && (module.exports = {
|
|
1757
|
+
CacheNamespace,
|
|
1025
1758
|
CacheStack,
|
|
1026
1759
|
JsonSerializer,
|
|
1027
1760
|
MemoryLayer,
|
|
@@ -1032,5 +1765,10 @@ var RedisSingleFlightCoordinator = class {
|
|
|
1032
1765
|
RedisSingleFlightCoordinator,
|
|
1033
1766
|
RedisTagIndex,
|
|
1034
1767
|
StampedeGuard,
|
|
1035
|
-
TagIndex
|
|
1768
|
+
TagIndex,
|
|
1769
|
+
cacheGraphqlResolver,
|
|
1770
|
+
createCacheStatsHandler,
|
|
1771
|
+
createCachedMethodDecorator,
|
|
1772
|
+
createFastifyLayercachePlugin,
|
|
1773
|
+
createTrpcCacheMiddleware
|
|
1036
1774
|
});
|