layercache 1.0.0

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/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "layercache",
3
+ "version": "1.0.0",
4
+ "description": "Unified multi-layer caching for Node.js with memory, Redis, stampede prevention, and invalidation helpers.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "packages/nestjs/dist",
20
+ "examples",
21
+ "benchmarks"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "workspaces": [
27
+ "packages/nestjs"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup src/index.ts --format esm,cjs --dts",
31
+ "build:nestjs": "npm --workspace @cachestack/nestjs run build",
32
+ "build:all": "npm run build && npm run build:nestjs",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "bench:latency": "tsx benchmarks/latency.ts",
36
+ "bench:stampede": "tsx benchmarks/stampede.ts"
37
+ },
38
+ "dependencies": {
39
+ "@msgpack/msgpack": "^3.0.0",
40
+ "async-mutex": "^0.4.1"
41
+ },
42
+ "peerDependencies": {
43
+ "ioredis": ">=5.0.0"
44
+ },
45
+ "optionalDependencies": {
46
+ "ioredis": ">=5.0.0"
47
+ },
48
+ "devDependencies": {
49
+ "@nestjs/common": "^11.1.0",
50
+ "@nestjs/core": "^11.1.0",
51
+ "@types/node": "^22.15.2",
52
+ "ioredis": "^5.6.1",
53
+ "ioredis-mock": "^8.13.0",
54
+ "reflect-metadata": "^0.2.2",
55
+ "rxjs": "^7.8.1",
56
+ "tsup": "^8.5.0",
57
+ "tsx": "^4.19.3",
58
+ "typescript": "^5.8.3",
59
+ "vitest": "^1.6.1"
60
+ }
61
+ }
@@ -0,0 +1,571 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __decorateClass = (decorators, target, key, kind) => {
20
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
21
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
22
+ if (decorator = decorators[i])
23
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
24
+ if (kind && result) __defProp(target, key, result);
25
+ return result;
26
+ };
27
+
28
+ // src/index.ts
29
+ var index_exports = {};
30
+ __export(index_exports, {
31
+ CACHE_STACK: () => CACHE_STACK,
32
+ CacheStackModule: () => CacheStackModule,
33
+ InjectCacheStack: () => InjectCacheStack
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/constants.ts
38
+ var CACHE_STACK = /* @__PURE__ */ Symbol("CACHE_STACK");
39
+
40
+ // src/module.ts
41
+ var import_common = require("@nestjs/common");
42
+
43
+ // ../../src/CacheStack.ts
44
+ var import_node_crypto = require("crypto");
45
+
46
+ // ../../src/invalidation/PatternMatcher.ts
47
+ var PatternMatcher = class {
48
+ static matches(pattern, value) {
49
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
50
+ const regex = new RegExp(`^${escaped.replace(/\*/g, ".*").replace(/\?/g, ".")}$`);
51
+ return regex.test(value);
52
+ }
53
+ };
54
+
55
+ // ../../src/invalidation/TagIndex.ts
56
+ var TagIndex = class {
57
+ tagToKeys = /* @__PURE__ */ new Map();
58
+ keyToTags = /* @__PURE__ */ new Map();
59
+ knownKeys = /* @__PURE__ */ new Set();
60
+ async touch(key) {
61
+ this.knownKeys.add(key);
62
+ }
63
+ async track(key, tags) {
64
+ this.knownKeys.add(key);
65
+ if (tags.length === 0) {
66
+ return;
67
+ }
68
+ const existingTags = this.keyToTags.get(key);
69
+ if (existingTags) {
70
+ for (const tag of existingTags) {
71
+ this.tagToKeys.get(tag)?.delete(key);
72
+ }
73
+ }
74
+ const tagSet = new Set(tags);
75
+ this.keyToTags.set(key, tagSet);
76
+ for (const tag of tagSet) {
77
+ const keys = this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set();
78
+ keys.add(key);
79
+ this.tagToKeys.set(tag, keys);
80
+ }
81
+ }
82
+ async remove(key) {
83
+ this.knownKeys.delete(key);
84
+ const tags = this.keyToTags.get(key);
85
+ if (!tags) {
86
+ return;
87
+ }
88
+ for (const tag of tags) {
89
+ const keys = this.tagToKeys.get(tag);
90
+ if (!keys) {
91
+ continue;
92
+ }
93
+ keys.delete(key);
94
+ if (keys.size === 0) {
95
+ this.tagToKeys.delete(tag);
96
+ }
97
+ }
98
+ this.keyToTags.delete(key);
99
+ }
100
+ async keysForTag(tag) {
101
+ return [...this.tagToKeys.get(tag) ?? /* @__PURE__ */ new Set()];
102
+ }
103
+ async matchPattern(pattern) {
104
+ return [...this.knownKeys].filter((key) => PatternMatcher.matches(pattern, key));
105
+ }
106
+ async clear() {
107
+ this.tagToKeys.clear();
108
+ this.keyToTags.clear();
109
+ this.knownKeys.clear();
110
+ }
111
+ };
112
+
113
+ // ../../node_modules/async-mutex/index.mjs
114
+ var E_TIMEOUT = new Error("timeout while waiting for mutex to become available");
115
+ var E_ALREADY_LOCKED = new Error("mutex already locked");
116
+ var E_CANCELED = new Error("request for lock canceled");
117
+ var __awaiter$2 = function(thisArg, _arguments, P, generator) {
118
+ function adopt(value) {
119
+ return value instanceof P ? value : new P(function(resolve) {
120
+ resolve(value);
121
+ });
122
+ }
123
+ return new (P || (P = Promise))(function(resolve, reject) {
124
+ function fulfilled(value) {
125
+ try {
126
+ step(generator.next(value));
127
+ } catch (e) {
128
+ reject(e);
129
+ }
130
+ }
131
+ function rejected(value) {
132
+ try {
133
+ step(generator["throw"](value));
134
+ } catch (e) {
135
+ reject(e);
136
+ }
137
+ }
138
+ function step(result) {
139
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
140
+ }
141
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
142
+ });
143
+ };
144
+ var Semaphore = class {
145
+ constructor(_value, _cancelError = E_CANCELED) {
146
+ this._value = _value;
147
+ this._cancelError = _cancelError;
148
+ this._weightedQueues = [];
149
+ this._weightedWaiters = [];
150
+ }
151
+ acquire(weight = 1) {
152
+ if (weight <= 0)
153
+ throw new Error(`invalid weight ${weight}: must be positive`);
154
+ return new Promise((resolve, reject) => {
155
+ if (!this._weightedQueues[weight - 1])
156
+ this._weightedQueues[weight - 1] = [];
157
+ this._weightedQueues[weight - 1].push({ resolve, reject });
158
+ this._dispatch();
159
+ });
160
+ }
161
+ runExclusive(callback, weight = 1) {
162
+ return __awaiter$2(this, void 0, void 0, function* () {
163
+ const [value, release] = yield this.acquire(weight);
164
+ try {
165
+ return yield callback(value);
166
+ } finally {
167
+ release();
168
+ }
169
+ });
170
+ }
171
+ waitForUnlock(weight = 1) {
172
+ if (weight <= 0)
173
+ throw new Error(`invalid weight ${weight}: must be positive`);
174
+ return new Promise((resolve) => {
175
+ if (!this._weightedWaiters[weight - 1])
176
+ this._weightedWaiters[weight - 1] = [];
177
+ this._weightedWaiters[weight - 1].push(resolve);
178
+ this._dispatch();
179
+ });
180
+ }
181
+ isLocked() {
182
+ return this._value <= 0;
183
+ }
184
+ getValue() {
185
+ return this._value;
186
+ }
187
+ setValue(value) {
188
+ this._value = value;
189
+ this._dispatch();
190
+ }
191
+ release(weight = 1) {
192
+ if (weight <= 0)
193
+ throw new Error(`invalid weight ${weight}: must be positive`);
194
+ this._value += weight;
195
+ this._dispatch();
196
+ }
197
+ cancel() {
198
+ this._weightedQueues.forEach((queue) => queue.forEach((entry) => entry.reject(this._cancelError)));
199
+ this._weightedQueues = [];
200
+ }
201
+ _dispatch() {
202
+ var _a;
203
+ for (let weight = this._value; weight > 0; weight--) {
204
+ const queueEntry = (_a = this._weightedQueues[weight - 1]) === null || _a === void 0 ? void 0 : _a.shift();
205
+ if (!queueEntry)
206
+ continue;
207
+ const previousValue = this._value;
208
+ const previousWeight = weight;
209
+ this._value -= weight;
210
+ weight = this._value + 1;
211
+ queueEntry.resolve([previousValue, this._newReleaser(previousWeight)]);
212
+ }
213
+ this._drainUnlockWaiters();
214
+ }
215
+ _newReleaser(weight) {
216
+ let called = false;
217
+ return () => {
218
+ if (called)
219
+ return;
220
+ called = true;
221
+ this.release(weight);
222
+ };
223
+ }
224
+ _drainUnlockWaiters() {
225
+ for (let weight = this._value; weight > 0; weight--) {
226
+ if (!this._weightedWaiters[weight - 1])
227
+ continue;
228
+ this._weightedWaiters[weight - 1].forEach((waiter) => waiter());
229
+ this._weightedWaiters[weight - 1] = [];
230
+ }
231
+ }
232
+ };
233
+ var __awaiter$1 = function(thisArg, _arguments, P, generator) {
234
+ function adopt(value) {
235
+ return value instanceof P ? value : new P(function(resolve) {
236
+ resolve(value);
237
+ });
238
+ }
239
+ return new (P || (P = Promise))(function(resolve, reject) {
240
+ function fulfilled(value) {
241
+ try {
242
+ step(generator.next(value));
243
+ } catch (e) {
244
+ reject(e);
245
+ }
246
+ }
247
+ function rejected(value) {
248
+ try {
249
+ step(generator["throw"](value));
250
+ } catch (e) {
251
+ reject(e);
252
+ }
253
+ }
254
+ function step(result) {
255
+ result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
256
+ }
257
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
258
+ });
259
+ };
260
+ var Mutex = class {
261
+ constructor(cancelError) {
262
+ this._semaphore = new Semaphore(1, cancelError);
263
+ }
264
+ acquire() {
265
+ return __awaiter$1(this, void 0, void 0, function* () {
266
+ const [, releaser] = yield this._semaphore.acquire();
267
+ return releaser;
268
+ });
269
+ }
270
+ runExclusive(callback) {
271
+ return this._semaphore.runExclusive(() => callback());
272
+ }
273
+ isLocked() {
274
+ return this._semaphore.isLocked();
275
+ }
276
+ waitForUnlock() {
277
+ return this._semaphore.waitForUnlock();
278
+ }
279
+ release() {
280
+ if (this._semaphore.isLocked())
281
+ this._semaphore.release();
282
+ }
283
+ cancel() {
284
+ return this._semaphore.cancel();
285
+ }
286
+ };
287
+
288
+ // ../../src/stampede/StampedeGuard.ts
289
+ var StampedeGuard = class {
290
+ mutexes = /* @__PURE__ */ new Map();
291
+ async execute(key, task) {
292
+ const mutex = this.getMutex(key);
293
+ try {
294
+ return await mutex.runExclusive(task);
295
+ } finally {
296
+ if (!mutex.isLocked()) {
297
+ this.mutexes.delete(key);
298
+ }
299
+ }
300
+ }
301
+ getMutex(key) {
302
+ let mutex = this.mutexes.get(key);
303
+ if (!mutex) {
304
+ mutex = new Mutex();
305
+ this.mutexes.set(key, mutex);
306
+ }
307
+ return mutex;
308
+ }
309
+ };
310
+
311
+ // ../../src/CacheStack.ts
312
+ var EMPTY_METRICS = () => ({
313
+ hits: 0,
314
+ misses: 0,
315
+ fetches: 0,
316
+ sets: 0,
317
+ deletes: 0,
318
+ backfills: 0,
319
+ invalidations: 0
320
+ });
321
+ var DebugLogger = class {
322
+ enabled;
323
+ constructor(enabled) {
324
+ this.enabled = enabled;
325
+ }
326
+ debug(message, context) {
327
+ if (!this.enabled) {
328
+ return;
329
+ }
330
+ const suffix = context ? ` ${JSON.stringify(context)}` : "";
331
+ console.debug(`[cachestack] ${message}${suffix}`);
332
+ }
333
+ };
334
+ var CacheStack = class {
335
+ constructor(layers, options = {}) {
336
+ this.layers = layers;
337
+ this.options = options;
338
+ if (layers.length === 0) {
339
+ throw new Error("CacheStack requires at least one cache layer.");
340
+ }
341
+ const debugEnv = process.env.DEBUG?.split(",").includes("cachestack:debug") ?? false;
342
+ this.logger = typeof options.logger === "object" ? options.logger : new DebugLogger(Boolean(options.logger) || debugEnv);
343
+ this.tagIndex = options.tagIndex ?? new TagIndex();
344
+ this.startup = this.initialize();
345
+ }
346
+ layers;
347
+ options;
348
+ stampedeGuard = new StampedeGuard();
349
+ metrics = EMPTY_METRICS();
350
+ instanceId = (0, import_node_crypto.randomUUID)();
351
+ startup;
352
+ unsubscribeInvalidation;
353
+ logger;
354
+ tagIndex;
355
+ async get(key, fetcher, options) {
356
+ await this.startup;
357
+ const hit = await this.getFromLayers(key, options);
358
+ if (hit.found) {
359
+ this.metrics.hits += 1;
360
+ return hit.value;
361
+ }
362
+ this.metrics.misses += 1;
363
+ if (!fetcher) {
364
+ return null;
365
+ }
366
+ const runFetch = async () => {
367
+ const secondHit = await this.getFromLayers(key, options);
368
+ if (secondHit.found) {
369
+ this.metrics.hits += 1;
370
+ return secondHit.value;
371
+ }
372
+ this.metrics.fetches += 1;
373
+ const fetched = await fetcher();
374
+ if (fetched === null || fetched === void 0) {
375
+ return null;
376
+ }
377
+ await this.set(key, fetched, options);
378
+ return fetched;
379
+ };
380
+ if (this.options.stampedePrevention === false) {
381
+ return runFetch();
382
+ }
383
+ return this.stampedeGuard.execute(key, runFetch);
384
+ }
385
+ async set(key, value, options) {
386
+ await this.startup;
387
+ await this.setAcrossLayers(key, value, options);
388
+ if (options?.tags) {
389
+ await this.tagIndex.track(key, options.tags);
390
+ } else {
391
+ await this.tagIndex.touch(key);
392
+ }
393
+ this.metrics.sets += 1;
394
+ this.logger.debug("set", { key, tags: options?.tags });
395
+ if (this.options.publishSetInvalidation !== false) {
396
+ await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "write" });
397
+ }
398
+ }
399
+ async delete(key) {
400
+ await this.startup;
401
+ await this.deleteKeys([key]);
402
+ await this.publishInvalidation({ scope: "key", keys: [key], sourceId: this.instanceId, operation: "delete" });
403
+ }
404
+ async clear() {
405
+ await this.startup;
406
+ await Promise.all(this.layers.map((layer) => layer.clear()));
407
+ await this.tagIndex.clear();
408
+ this.metrics.invalidations += 1;
409
+ this.logger.debug("clear");
410
+ await this.publishInvalidation({ scope: "clear", sourceId: this.instanceId, operation: "clear" });
411
+ }
412
+ async mget(entries) {
413
+ return Promise.all(entries.map((entry) => this.get(entry.key, entry.fetch, entry.options)));
414
+ }
415
+ async mset(entries) {
416
+ await Promise.all(entries.map((entry) => this.set(entry.key, entry.value, entry.options)));
417
+ }
418
+ async invalidateByTag(tag) {
419
+ await this.startup;
420
+ const keys = await this.tagIndex.keysForTag(tag);
421
+ await this.deleteKeys(keys);
422
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
423
+ }
424
+ async invalidateByPattern(pattern) {
425
+ await this.startup;
426
+ const keys = await this.tagIndex.matchPattern(pattern);
427
+ await this.deleteKeys(keys);
428
+ await this.publishInvalidation({ scope: "keys", keys, sourceId: this.instanceId, operation: "invalidate" });
429
+ }
430
+ getMetrics() {
431
+ return { ...this.metrics };
432
+ }
433
+ resetMetrics() {
434
+ Object.assign(this.metrics, EMPTY_METRICS());
435
+ }
436
+ async disconnect() {
437
+ await this.startup;
438
+ await this.unsubscribeInvalidation?.();
439
+ }
440
+ async initialize() {
441
+ if (!this.options.invalidationBus) {
442
+ return;
443
+ }
444
+ this.unsubscribeInvalidation = await this.options.invalidationBus.subscribe(async (message) => {
445
+ await this.handleInvalidationMessage(message);
446
+ });
447
+ }
448
+ async getFromLayers(key, options) {
449
+ for (let index = 0; index < this.layers.length; index += 1) {
450
+ const layer = this.layers[index];
451
+ const value = await layer.get(key);
452
+ if (value === null) {
453
+ continue;
454
+ }
455
+ await this.tagIndex.touch(key);
456
+ await this.backfill(key, value, index - 1, options);
457
+ this.logger.debug("hit", { key, layer: layer.name });
458
+ return { found: true, value };
459
+ }
460
+ await this.tagIndex.remove(key);
461
+ this.logger.debug("miss", { key });
462
+ return { found: false, value: null };
463
+ }
464
+ async backfill(key, value, upToIndex, options) {
465
+ if (upToIndex < 0) {
466
+ return;
467
+ }
468
+ for (let index = 0; index <= upToIndex; index += 1) {
469
+ const layer = this.layers[index];
470
+ await layer.set(key, value, this.resolveTtl(layer.name, layer.defaultTtl, options?.ttl));
471
+ this.metrics.backfills += 1;
472
+ this.logger.debug("backfill", { key, layer: layer.name });
473
+ }
474
+ }
475
+ async setAcrossLayers(key, value, options) {
476
+ await Promise.all(
477
+ this.layers.map((layer) => layer.set(key, value, this.resolveTtl(layer.name, layer.defaultTtl, options?.ttl)))
478
+ );
479
+ }
480
+ resolveTtl(layerName, fallbackTtl, ttlOverride) {
481
+ if (ttlOverride === void 0) {
482
+ return fallbackTtl;
483
+ }
484
+ if (typeof ttlOverride === "number") {
485
+ return ttlOverride;
486
+ }
487
+ return ttlOverride[layerName] ?? fallbackTtl;
488
+ }
489
+ async deleteKeys(keys) {
490
+ if (keys.length === 0) {
491
+ return;
492
+ }
493
+ await Promise.all(
494
+ this.layers.map(async (layer) => {
495
+ if (layer.deleteMany) {
496
+ await layer.deleteMany(keys);
497
+ return;
498
+ }
499
+ await Promise.all(keys.map((key) => layer.delete(key)));
500
+ })
501
+ );
502
+ for (const key of keys) {
503
+ await this.tagIndex.remove(key);
504
+ }
505
+ this.metrics.deletes += keys.length;
506
+ this.metrics.invalidations += 1;
507
+ this.logger.debug("delete", { keys });
508
+ }
509
+ async publishInvalidation(message) {
510
+ if (!this.options.invalidationBus) {
511
+ return;
512
+ }
513
+ await this.options.invalidationBus.publish(message);
514
+ }
515
+ async handleInvalidationMessage(message) {
516
+ if (message.sourceId === this.instanceId) {
517
+ return;
518
+ }
519
+ const localLayers = this.layers.filter((layer) => layer.isLocal);
520
+ if (localLayers.length === 0) {
521
+ return;
522
+ }
523
+ if (message.scope === "clear") {
524
+ await Promise.all(localLayers.map((layer) => layer.clear()));
525
+ await this.tagIndex.clear();
526
+ return;
527
+ }
528
+ const keys = message.keys ?? [];
529
+ await Promise.all(
530
+ localLayers.map(async (layer) => {
531
+ if (layer.deleteMany) {
532
+ await layer.deleteMany(keys);
533
+ return;
534
+ }
535
+ await Promise.all(keys.map((key) => layer.delete(key)));
536
+ })
537
+ );
538
+ if (message.operation !== "write") {
539
+ for (const key of keys) {
540
+ await this.tagIndex.remove(key);
541
+ }
542
+ }
543
+ }
544
+ };
545
+
546
+ // src/module.ts
547
+ var InjectCacheStack = () => (0, import_common.Inject)(CACHE_STACK);
548
+ var CacheStackModule = class {
549
+ static forRoot(options) {
550
+ const provider = {
551
+ provide: CACHE_STACK,
552
+ useFactory: () => new CacheStack(options.layers, options.bridgeOptions)
553
+ };
554
+ return {
555
+ global: true,
556
+ module: CacheStackModule,
557
+ providers: [provider],
558
+ exports: [provider]
559
+ };
560
+ }
561
+ };
562
+ CacheStackModule = __decorateClass([
563
+ (0, import_common.Global)(),
564
+ (0, import_common.Module)({})
565
+ ], CacheStackModule);
566
+ // Annotate the CommonJS export names for ESM import in node:
567
+ 0 && (module.exports = {
568
+ CACHE_STACK,
569
+ CacheStackModule,
570
+ InjectCacheStack
571
+ });
@@ -0,0 +1,55 @@
1
+ import { DynamicModule } from '@nestjs/common';
2
+
3
+ declare const CACHE_STACK: unique symbol;
4
+
5
+ interface CacheLayer {
6
+ readonly name: string;
7
+ readonly defaultTtl?: number;
8
+ readonly isLocal?: boolean;
9
+ get<T>(key: string): Promise<T | null>;
10
+ set(key: string, value: unknown, ttl?: number): Promise<void>;
11
+ delete(key: string): Promise<void>;
12
+ clear(): Promise<void>;
13
+ deleteMany?(keys: string[]): Promise<void>;
14
+ keys?(): Promise<string[]>;
15
+ }
16
+ interface CacheLogger {
17
+ debug(message: string, context?: Record<string, unknown>): void;
18
+ }
19
+ interface CacheTagIndex {
20
+ touch(key: string): Promise<void>;
21
+ track(key: string, tags: string[]): Promise<void>;
22
+ remove(key: string): Promise<void>;
23
+ keysForTag(tag: string): Promise<string[]>;
24
+ matchPattern(pattern: string): Promise<string[]>;
25
+ clear(): Promise<void>;
26
+ }
27
+ interface InvalidationMessage {
28
+ scope: 'key' | 'keys' | 'clear';
29
+ sourceId: string;
30
+ keys?: string[];
31
+ operation?: 'write' | 'delete' | 'invalidate' | 'clear';
32
+ }
33
+ interface InvalidationBus {
34
+ subscribe(handler: (message: InvalidationMessage) => Promise<void> | void): Promise<() => Promise<void> | void>;
35
+ publish(message: InvalidationMessage): Promise<void>;
36
+ }
37
+ interface CacheStackOptions {
38
+ logger?: CacheLogger | boolean;
39
+ metrics?: boolean;
40
+ stampedePrevention?: boolean;
41
+ invalidationBus?: InvalidationBus;
42
+ tagIndex?: CacheTagIndex;
43
+ publishSetInvalidation?: boolean;
44
+ }
45
+
46
+ interface CacheStackModuleOptions {
47
+ layers: CacheLayer[];
48
+ bridgeOptions?: CacheStackOptions;
49
+ }
50
+ declare const InjectCacheStack: () => ParameterDecorator & PropertyDecorator;
51
+ declare class CacheStackModule {
52
+ static forRoot(options: CacheStackModuleOptions): DynamicModule;
53
+ }
54
+
55
+ export { CACHE_STACK, CacheStackModule, type CacheStackModuleOptions, InjectCacheStack };