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