@lakutata/cache 3.0.0-beta.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 +23 -0
- package/README.md +175 -0
- package/dist/cjs/cache/Cacher.d.ts +446 -0
- package/dist/cjs/cache/Cacher.js +523 -0
- package/dist/cjs/cache/exceptions/CacheDriverNotFoundException.d.ts +10 -0
- package/dist/cjs/cache/exceptions/CacheDriverNotFoundException.js +17 -0
- package/dist/cjs/cache/interfaces/CacherOptions.d.ts +42 -0
- package/dist/cjs/cache/interfaces/CacherOptions.js +2 -0
- package/dist/cjs/cache/lib/LikePrefix.d.ts +5 -0
- package/dist/cjs/cache/lib/LikePrefix.js +10 -0
- package/dist/cjs/cache/lib/LoadDriver.d.ts +6 -0
- package/dist/cjs/cache/lib/LoadDriver.js +66 -0
- package/dist/cjs/cache/lib/Serializer.d.ts +18 -0
- package/dist/cjs/cache/lib/Serializer.js +46 -0
- package/dist/cjs/cache/lib/WithTimeout.d.ts +7 -0
- package/dist/cjs/cache/lib/WithTimeout.js +23 -0
- package/dist/cjs/cache/options/FileCacheOptions.d.ts +54 -0
- package/dist/cjs/cache/options/FileCacheOptions.js +62 -0
- package/dist/cjs/cache/options/MemcacheCacheOptions.d.ts +57 -0
- package/dist/cjs/cache/options/MemcacheCacheOptions.js +66 -0
- package/dist/cjs/cache/options/MemoryCacheOptions.d.ts +39 -0
- package/dist/cjs/cache/options/MemoryCacheOptions.js +52 -0
- package/dist/cjs/cache/options/MongoCacheOptions.d.ts +72 -0
- package/dist/cjs/cache/options/MongoCacheOptions.js +81 -0
- package/dist/cjs/cache/options/MysqlCacheOptions.d.ts +73 -0
- package/dist/cjs/cache/options/MysqlCacheOptions.js +82 -0
- package/dist/cjs/cache/options/PostgresCacheOptions.d.ts +87 -0
- package/dist/cjs/cache/options/PostgresCacheOptions.js +92 -0
- package/dist/cjs/cache/options/RedisCacheOptions.d.ts +120 -0
- package/dist/cjs/cache/options/RedisCacheOptions.js +113 -0
- package/dist/cjs/cache/options/SqliteCacheOptions.d.ts +53 -0
- package/dist/cjs/cache/options/SqliteCacheOptions.js +61 -0
- package/dist/cjs/cache/stores/CacheStore.d.ts +44 -0
- package/dist/cjs/cache/stores/CacheStore.js +42 -0
- package/dist/cjs/cache/stores/CreateCacheStore.d.ts +7 -0
- package/dist/cjs/cache/stores/CreateCacheStore.js +37 -0
- package/dist/cjs/cache/stores/FileStore.d.ts +51 -0
- package/dist/cjs/cache/stores/FileStore.js +143 -0
- package/dist/cjs/cache/stores/MemcacheStore.d.ts +42 -0
- package/dist/cjs/cache/stores/MemcacheStore.js +69 -0
- package/dist/cjs/cache/stores/MemoryStore.d.ts +34 -0
- package/dist/cjs/cache/stores/MemoryStore.js +75 -0
- package/dist/cjs/cache/stores/MongoStore.d.ts +55 -0
- package/dist/cjs/cache/stores/MongoStore.js +72 -0
- package/dist/cjs/cache/stores/MysqlStore.d.ts +27 -0
- package/dist/cjs/cache/stores/MysqlStore.js +71 -0
- package/dist/cjs/cache/stores/PostgresStore.d.ts +30 -0
- package/dist/cjs/cache/stores/PostgresStore.js +85 -0
- package/dist/cjs/cache/stores/RedisStore.d.ts +58 -0
- package/dist/cjs/cache/stores/RedisStore.js +131 -0
- package/dist/cjs/cache/stores/SerializedCacheStore.d.ts +48 -0
- package/dist/cjs/cache/stores/SerializedCacheStore.js +80 -0
- package/dist/cjs/cache/stores/SqliteStore.d.ts +44 -0
- package/dist/cjs/cache/stores/SqliteStore.js +91 -0
- package/dist/cjs/cache/types/CacheStoreOptions.d.ts +15 -0
- package/dist/cjs/cache/types/CacheStoreOptions.js +2 -0
- package/dist/cjs/exports/Cache.d.ts +12 -0
- package/dist/cjs/exports/Cache.js +31 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/cache/Cacher.js +518 -0
- package/dist/esm/cache/exceptions/CacheDriverNotFoundException.js +13 -0
- package/dist/esm/cache/interfaces/CacherOptions.js +1 -0
- package/dist/esm/cache/lib/LikePrefix.js +7 -0
- package/dist/esm/cache/lib/LoadDriver.js +30 -0
- package/dist/esm/cache/lib/Serializer.js +42 -0
- package/dist/esm/cache/lib/WithTimeout.js +20 -0
- package/dist/esm/cache/options/FileCacheOptions.js +58 -0
- package/dist/esm/cache/options/MemcacheCacheOptions.js +62 -0
- package/dist/esm/cache/options/MemoryCacheOptions.js +48 -0
- package/dist/esm/cache/options/MongoCacheOptions.js +77 -0
- package/dist/esm/cache/options/MysqlCacheOptions.js +78 -0
- package/dist/esm/cache/options/PostgresCacheOptions.js +88 -0
- package/dist/esm/cache/options/RedisCacheOptions.js +109 -0
- package/dist/esm/cache/options/SqliteCacheOptions.js +57 -0
- package/dist/esm/cache/stores/CacheStore.js +36 -0
- package/dist/esm/cache/stores/CreateCacheStore.js +34 -0
- package/dist/esm/cache/stores/FileStore.js +136 -0
- package/dist/esm/cache/stores/MemcacheStore.js +65 -0
- package/dist/esm/cache/stores/MemoryStore.js +71 -0
- package/dist/esm/cache/stores/MongoStore.js +68 -0
- package/dist/esm/cache/stores/MysqlStore.js +67 -0
- package/dist/esm/cache/stores/PostgresStore.js +81 -0
- package/dist/esm/cache/stores/RedisStore.js +127 -0
- package/dist/esm/cache/stores/SerializedCacheStore.js +76 -0
- package/dist/esm/cache/stores/SqliteStore.js +87 -0
- package/dist/esm/cache/types/CacheStoreOptions.js +1 -0
- package/dist/esm/exports/Cache.js +12 -0
- package/dist/types/cache/Cacher.d.ts +446 -0
- package/dist/types/cache/exceptions/CacheDriverNotFoundException.d.ts +10 -0
- package/dist/types/cache/interfaces/CacherOptions.d.ts +42 -0
- package/dist/types/cache/lib/LikePrefix.d.ts +5 -0
- package/dist/types/cache/lib/LoadDriver.d.ts +6 -0
- package/dist/types/cache/lib/Serializer.d.ts +18 -0
- package/dist/types/cache/lib/WithTimeout.d.ts +7 -0
- package/dist/types/cache/options/FileCacheOptions.d.ts +54 -0
- package/dist/types/cache/options/MemcacheCacheOptions.d.ts +57 -0
- package/dist/types/cache/options/MemoryCacheOptions.d.ts +39 -0
- package/dist/types/cache/options/MongoCacheOptions.d.ts +72 -0
- package/dist/types/cache/options/MysqlCacheOptions.d.ts +73 -0
- package/dist/types/cache/options/PostgresCacheOptions.d.ts +87 -0
- package/dist/types/cache/options/RedisCacheOptions.d.ts +120 -0
- package/dist/types/cache/options/SqliteCacheOptions.d.ts +53 -0
- package/dist/types/cache/stores/CacheStore.d.ts +44 -0
- package/dist/types/cache/stores/CreateCacheStore.d.ts +7 -0
- package/dist/types/cache/stores/FileStore.d.ts +51 -0
- package/dist/types/cache/stores/MemcacheStore.d.ts +42 -0
- package/dist/types/cache/stores/MemoryStore.d.ts +34 -0
- package/dist/types/cache/stores/MongoStore.d.ts +55 -0
- package/dist/types/cache/stores/MysqlStore.d.ts +27 -0
- package/dist/types/cache/stores/PostgresStore.d.ts +30 -0
- package/dist/types/cache/stores/RedisStore.d.ts +58 -0
- package/dist/types/cache/stores/SerializedCacheStore.d.ts +48 -0
- package/dist/types/cache/stores/SqliteStore.d.ts +44 -0
- package/dist/types/cache/types/CacheStoreOptions.d.ts +15 -0
- package/dist/types/exports/Cache.d.ts +12 -0
- package/package.json +72 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.Cacher = exports.buildCacherOptions = void 0;
|
|
13
|
+
const core_1 = require("@lakutata/core");
|
|
14
|
+
const di_1 = require("@lakutata/core/decorator/di");
|
|
15
|
+
const utils_1 = require("@lakutata/utils");
|
|
16
|
+
const FileCacheOptions_js_1 = require("./options/FileCacheOptions.js");
|
|
17
|
+
const MemoryCacheOptions_js_1 = require("./options/MemoryCacheOptions.js");
|
|
18
|
+
const RedisCacheOptions_js_1 = require("./options/RedisCacheOptions.js");
|
|
19
|
+
const MemcacheCacheOptions_js_1 = require("./options/MemcacheCacheOptions.js");
|
|
20
|
+
const MongoCacheOptions_js_1 = require("./options/MongoCacheOptions.js");
|
|
21
|
+
const SqliteCacheOptions_js_1 = require("./options/SqliteCacheOptions.js");
|
|
22
|
+
const PostgresCacheOptions_js_1 = require("./options/PostgresCacheOptions.js");
|
|
23
|
+
const MysqlCacheOptions_js_1 = require("./options/MysqlCacheOptions.js");
|
|
24
|
+
const MemoryStore_js_1 = require("./stores/MemoryStore.js");
|
|
25
|
+
const CreateCacheStore_js_1 = require("./stores/CreateCacheStore.js");
|
|
26
|
+
/**
|
|
27
|
+
* Build the component options of a {@link Cacher}, to register it in the `components` of an application (or a module)
|
|
28
|
+
* under the name of your choice. The options are validated when the component is created, not here.
|
|
29
|
+
* @param options The cache options: its stores, its default TTL, its refresh and write behavior. Without options, one
|
|
30
|
+
* in-memory store whose entries never expire.
|
|
31
|
+
* @returns The component options (`{class: Cacher, ...options}`).
|
|
32
|
+
* @example
|
|
33
|
+
* ```typescript
|
|
34
|
+
* import {Application} from 'lakutata'
|
|
35
|
+
* import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
|
|
36
|
+
*
|
|
37
|
+
* Application
|
|
38
|
+
* .run(() => ({
|
|
39
|
+
* id: 'cache.app',
|
|
40
|
+
* name: 'Cache',
|
|
41
|
+
* components: {
|
|
42
|
+
* //An in-memory cache whose entries expire after one minute
|
|
43
|
+
* cache: buildCacherOptions({ttl: 60000})
|
|
44
|
+
* }
|
|
45
|
+
* }))
|
|
46
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
47
|
+
* const cache: Cacher = await app.getObject('cache')
|
|
48
|
+
* await cache.set('greeting', 'Hello')
|
|
49
|
+
* console.log(await cache.get<string>('greeting')) //Hello
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
const buildCacherOptions = (options) => ({
|
|
54
|
+
class: Cacher,
|
|
55
|
+
stores: options?.stores,
|
|
56
|
+
ttl: options?.ttl,
|
|
57
|
+
refreshThreshold: options?.refreshThreshold,
|
|
58
|
+
refreshAllStores: options?.refreshAllStores,
|
|
59
|
+
nonBlocking: options?.nonBlocking,
|
|
60
|
+
cacheId: options?.cacheId
|
|
61
|
+
});
|
|
62
|
+
exports.buildCacherOptions = buildCacherOptions;
|
|
63
|
+
/**
|
|
64
|
+
* Resolve with the first defined result, or undefined when none is
|
|
65
|
+
* @param promises
|
|
66
|
+
*/
|
|
67
|
+
function firstDefined(promises) {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
let remaining = promises.length;
|
|
70
|
+
if (!remaining)
|
|
71
|
+
return resolve(undefined);
|
|
72
|
+
for (const promise of promises) {
|
|
73
|
+
//A rejected promise gives no value
|
|
74
|
+
promise.then((value) => {
|
|
75
|
+
if (value !== undefined)
|
|
76
|
+
return resolve(value);
|
|
77
|
+
if (!--remaining)
|
|
78
|
+
resolve(undefined);
|
|
79
|
+
}, () => {
|
|
80
|
+
if (!--remaining)
|
|
81
|
+
resolve(undefined);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The cache component: a key-value cache with expiration, backed by one or more stores (memory, file, Redis,
|
|
88
|
+
* Memcache, MongoDB, SQLite, PostgreSQL, MySQL). Register it in the `components` of an application with
|
|
89
|
+
* {@link buildCacherOptions} (or `{class: Cacher, ...options}`) and get it with `getObject()` or `@Inject`; it is a
|
|
90
|
+
* singleton. Without stores, it caches in memory.
|
|
91
|
+
*
|
|
92
|
+
* The stores connect when the component is created: when one fails to connect, the others are disconnected and the
|
|
93
|
+
* creation fails with its error (the application does not start). A store whose driver package is not installed
|
|
94
|
+
* fails with {@link CacheDriverNotFoundException}. The stores disconnect when the component is destroyed, after the
|
|
95
|
+
* background refreshes in progress and the file store's pending writes.
|
|
96
|
+
*
|
|
97
|
+
* With several stores, they are tiers, the first one the fastest: a read returns the entry of the first store holding
|
|
98
|
+
* it, a write or a deletion goes to all of them. Only {@link Cacher.wrap} copies an entry found in a lower store to
|
|
99
|
+
* the stores above it.
|
|
100
|
+
*
|
|
101
|
+
* The TTLs are in milliseconds: an entry whose TTL is `0` or unset (without the `ttl` option) never expires; an expired
|
|
102
|
+
* entry is a miss. The memory store keeps the values by reference (a cached object changed afterwards is changed in the
|
|
103
|
+
* cache); the other stores keep them as JSON: the `Buffer`s are kept, but a `Date` comes back as its ISO string, a
|
|
104
|
+
* class instance as a plain object, a `Map` or a `Set` as `{}`, and a `bigint` fails the write.
|
|
105
|
+
*
|
|
106
|
+
* A store failing to read is a miss (its error is emitted as an `'error'` event); a store failing to write or delete
|
|
107
|
+
* rejects the call, unless the `nonBlocking` option is set.
|
|
108
|
+
* @example
|
|
109
|
+
* ```typescript
|
|
110
|
+
* import {Application, Component} from 'lakutata'
|
|
111
|
+
* import {Inject} from 'lakutata/decorator/di'
|
|
112
|
+
* import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
|
|
113
|
+
*
|
|
114
|
+
* type Product = {id: number, name: string}
|
|
115
|
+
*
|
|
116
|
+
* class Catalog extends Component {
|
|
117
|
+
* @Inject('cache')
|
|
118
|
+
* protected readonly cache: Cacher
|
|
119
|
+
*
|
|
120
|
+
* public async product(id: number): Promise<Product> {
|
|
121
|
+
* //Computed once, then read from the cache for 5 minutes (recomputed in the background in its last minute)
|
|
122
|
+
* return this.cache.wrap(`product:${id}`, async (): Promise<Product> => ({id: id, name: `Product ${id}`}), {
|
|
123
|
+
* ttl: 300000,
|
|
124
|
+
* refreshThreshold: 60000
|
|
125
|
+
* })
|
|
126
|
+
* }
|
|
127
|
+
*
|
|
128
|
+
* public async rename(id: number, name: string): Promise<void> {
|
|
129
|
+
* await this.cache.set(`product:${id}`, {id: id, name: name}, 300000)
|
|
130
|
+
* }
|
|
131
|
+
*
|
|
132
|
+
* public async remove(id: number): Promise<void> {
|
|
133
|
+
* await this.cache.del(`product:${id}`)
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
*
|
|
137
|
+
* Application
|
|
138
|
+
* .run(() => ({
|
|
139
|
+
* id: 'catalog.app',
|
|
140
|
+
* name: 'Catalog',
|
|
141
|
+
* components: {
|
|
142
|
+
* //A local file tier in front of Redis (the "redis" package installed)
|
|
143
|
+
* cache: buildCacherOptions({
|
|
144
|
+
* stores: [
|
|
145
|
+
* {type: 'file', filename: './cache/catalog.json'},
|
|
146
|
+
* {type: 'redis', host: '127.0.0.1', namespace: 'catalog'}
|
|
147
|
+
* ],
|
|
148
|
+
* ttl: 600000
|
|
149
|
+
* }),
|
|
150
|
+
* catalog: {class: Catalog}
|
|
151
|
+
* }
|
|
152
|
+
* }))
|
|
153
|
+
* .onLaunched(async (app: Application): Promise<void> => {
|
|
154
|
+
* const cache: Cacher = await app.getObject('cache')
|
|
155
|
+
* cache.on('error', (error: Error): void => console.error('Cache store error', error))
|
|
156
|
+
* const catalog: Catalog = await app.getObject('catalog')
|
|
157
|
+
* console.log(await catalog.product(1))
|
|
158
|
+
* })
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
class Cacher extends core_1.Component {
|
|
162
|
+
#stores = [];
|
|
163
|
+
#wrapping = new Map();
|
|
164
|
+
#refreshing = new Map();
|
|
165
|
+
async init() {
|
|
166
|
+
const storeConfigs = this.stores ? Array.isArray(this.stores) ? this.stores : [this.stores] : [];
|
|
167
|
+
const stores = storeConfigs
|
|
168
|
+
.map((storeOptions) => (0, CreateCacheStore_js_1.createCacheStore)(storeOptions))
|
|
169
|
+
.filter((store) => !!store);
|
|
170
|
+
if (!stores.length)
|
|
171
|
+
stores.push(new MemoryStore_js_1.MemoryStore());
|
|
172
|
+
for (const store of stores)
|
|
173
|
+
store.onError = (error) => this.report(error);
|
|
174
|
+
const connections = await Promise.allSettled(stores.map((store) => store.connect()));
|
|
175
|
+
const failure = connections.find((connection) => connection.status === 'rejected');
|
|
176
|
+
if (failure) {
|
|
177
|
+
await Promise.allSettled(stores.map((store) => store.disconnect()));
|
|
178
|
+
throw failure.reason;
|
|
179
|
+
}
|
|
180
|
+
this.#stores = stores;
|
|
181
|
+
}
|
|
182
|
+
async destroy() {
|
|
183
|
+
await Promise.allSettled(this.#refreshing.values());
|
|
184
|
+
await Promise.all(this.#stores.map((store) => store.disconnect()));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Emit a store error as an `'error'` event; dropped when the event has no listener.
|
|
188
|
+
* @param error The error.
|
|
189
|
+
* @protected
|
|
190
|
+
*/
|
|
191
|
+
report(error) {
|
|
192
|
+
if (this.listenerCount('error'))
|
|
193
|
+
this.emit('error', error);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resolve the time to live of a value.
|
|
197
|
+
* @param ttl The TTL in milliseconds, a function of the value returning it, or undefined for the cache's `ttl`
|
|
198
|
+
* option.
|
|
199
|
+
* @param value The value cached.
|
|
200
|
+
* @returns The TTL in milliseconds, undefined when the entry never expires.
|
|
201
|
+
* @protected
|
|
202
|
+
*/
|
|
203
|
+
resolveTTL(ttl, value) {
|
|
204
|
+
return (typeof ttl === 'function' ? ttl(value) : ttl) ?? this.ttl;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Read an entry from one store; a failing store is a miss, its error emitted as an `'error'` event.
|
|
208
|
+
* @param store The store.
|
|
209
|
+
* @param key The key.
|
|
210
|
+
* @returns The entry, undefined when it is missing, expired or the store failed.
|
|
211
|
+
* @protected
|
|
212
|
+
*/
|
|
213
|
+
async readStore(store, key) {
|
|
214
|
+
try {
|
|
215
|
+
return await store.get(key);
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
this.report(e);
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Read an entry from the first store holding it, in the stores' order; with `nonBlocking` and several stores, from
|
|
224
|
+
* the first store answering with it.
|
|
225
|
+
* @param key The key.
|
|
226
|
+
* @returns The entry and the index of its store, undefined on a miss.
|
|
227
|
+
* @protected
|
|
228
|
+
*/
|
|
229
|
+
async read(key) {
|
|
230
|
+
const stores = this.#stores;
|
|
231
|
+
if (this.nonBlocking && stores.length > 1)
|
|
232
|
+
return firstDefined(stores.map(async (store, index) => {
|
|
233
|
+
const entry = await this.readStore(store, key);
|
|
234
|
+
return entry ? { entry: entry, index: index } : undefined;
|
|
235
|
+
}));
|
|
236
|
+
for (let index = 0; index < stores.length; index++) {
|
|
237
|
+
const entry = await this.readStore(stores[index], key);
|
|
238
|
+
if (entry)
|
|
239
|
+
return { entry: entry, index: index };
|
|
240
|
+
}
|
|
241
|
+
return undefined;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Run an operation on stores, in parallel. With `nonBlocking`, it resolves at once and a failure is only passed to
|
|
245
|
+
* `onError`.
|
|
246
|
+
* @param stores The stores.
|
|
247
|
+
* @param operation The operation run on each store.
|
|
248
|
+
* @param onError Called with the first failure.
|
|
249
|
+
* @throws {Error} The first failure of the operation, without `nonBlocking`.
|
|
250
|
+
* @protected
|
|
251
|
+
*/
|
|
252
|
+
async runOnStores(stores, operation, onError) {
|
|
253
|
+
const promise = stores.length === 1 ? operation(stores[0]) : Promise.all(stores.map(operation));
|
|
254
|
+
if (this.nonBlocking) {
|
|
255
|
+
promise.catch(onError);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
await promise;
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
onError(e);
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Cache a value under a key, in all the stores, replacing the previous one. A `'set'` event is emitted once
|
|
268
|
+
* written; when a store fails, a `'set'` event carrying the error is emitted and the call rejects.
|
|
269
|
+
*
|
|
270
|
+
* With `nonBlocking`, the call resolves and the `'set'` event is emitted before the stores are written; a failure
|
|
271
|
+
* then emits a second `'set'` event carrying the error.
|
|
272
|
+
* @param key The key.
|
|
273
|
+
* @param value The value. The stores other than the memory one keep it as JSON.
|
|
274
|
+
* @param ttl The time to live, in milliseconds; `0` never expires. Defaults to the cache's `ttl` option (never
|
|
275
|
+
* expires without it).
|
|
276
|
+
* @returns The value given.
|
|
277
|
+
* @throws {Error} The failure of a store (its driver's error, or a `TypeError` for a value JSON cannot write),
|
|
278
|
+
* without `nonBlocking`.
|
|
279
|
+
*/
|
|
280
|
+
async set(key, value, ttl) {
|
|
281
|
+
ttl = ttl ?? this.ttl;
|
|
282
|
+
await this.runOnStores(this.#stores, (store) => store.set(key, value, ttl), (error) => this.emit('set', { key: key, value: value, error: error }));
|
|
283
|
+
this.emit('set', { key: key, value: value });
|
|
284
|
+
return value;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Cache several values, in all the stores (in one batch per store where the store allows it). No `'set'` event is
|
|
288
|
+
* emitted; a failure is emitted as an `'error'` event and rejects the call (unless `nonBlocking`).
|
|
289
|
+
* @param options The entries; an entry without `ttl` gets the cache's `ttl` option.
|
|
290
|
+
* @returns The entries written, their TTL resolved.
|
|
291
|
+
* @throws {Error} The failure of a store, without `nonBlocking`.
|
|
292
|
+
*/
|
|
293
|
+
async multipleSet(options) {
|
|
294
|
+
const list = options.map((option) => ({ key: option.key, value: option.value, ttl: option.ttl ?? this.ttl }));
|
|
295
|
+
await this.runOnStores(this.#stores, (store) => store.setMany(list), (error) => this.report(error));
|
|
296
|
+
return list;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Read the value of a key, from the first store holding it. It does not copy the value to the stores above (see
|
|
300
|
+
* {@link Cacher.wrap}). A failing store is a miss (its error emitted as an `'error'` event): it never rejects.
|
|
301
|
+
* @param key The key.
|
|
302
|
+
* @returns The value; undefined when the key is missing or expired (or when undefined was cached).
|
|
303
|
+
*/
|
|
304
|
+
async get(key) {
|
|
305
|
+
if (this.#stores.length === 1)
|
|
306
|
+
return (0, utils_1.As)((await this.readStore(this.#stores[0], key))?.value);
|
|
307
|
+
return (0, utils_1.As)((await this.read(key))?.entry.value);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Read the values of several keys; each key from the first store holding it (the missing keys only are asked to
|
|
311
|
+
* the next store). A failing store misses all its keys (its error emitted as an `'error'` event): it never rejects.
|
|
312
|
+
* @param keys The keys.
|
|
313
|
+
* @returns The values, in the order of the keys: undefined for the missing and expired ones.
|
|
314
|
+
*/
|
|
315
|
+
async multipleGet(keys) {
|
|
316
|
+
const readMany = async (store, storeKeys) => {
|
|
317
|
+
try {
|
|
318
|
+
return await store.getMany(storeKeys);
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
this.report(e);
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
const stores = this.#stores;
|
|
326
|
+
if (this.nonBlocking && stores.length > 1) {
|
|
327
|
+
const results = stores.map((store) => readMany(store, keys));
|
|
328
|
+
return Promise.all(keys.map(async (key, index) => (await firstDefined(results.map(async (result) => (await result)[index])))?.value));
|
|
329
|
+
}
|
|
330
|
+
const values = new Array(keys.length).fill(undefined);
|
|
331
|
+
let missing = keys.map((key, index) => index);
|
|
332
|
+
for (const store of stores) {
|
|
333
|
+
const entries = await readMany(store, missing.map((index) => keys[index]));
|
|
334
|
+
missing = missing.filter((index, position) => {
|
|
335
|
+
if (!entries[position])
|
|
336
|
+
return true;
|
|
337
|
+
values[index] = entries[position].value;
|
|
338
|
+
return false;
|
|
339
|
+
});
|
|
340
|
+
if (!missing.length)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
return values;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Read when a key expires. Despite its name, it returns a point in time, not a remaining duration: compute the
|
|
347
|
+
* time left with `expires - Date.now()`.
|
|
348
|
+
* @param key The key.
|
|
349
|
+
* @returns The expiration time, in milliseconds since the epoch; `-1` when the key is missing, expired or never
|
|
350
|
+
* expires.
|
|
351
|
+
*/
|
|
352
|
+
async getTTL(key) {
|
|
353
|
+
return (await this.read(key))?.entry.expires ?? -1;
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Read the value of a key, or compute it and cache it when it is missing or expired (the cache-aside pattern).
|
|
357
|
+
*
|
|
358
|
+
* - The concurrent calls on the same key share one call and its result, whatever their `fn` and options.
|
|
359
|
+
* - A value found in a lower store is copied to the stores above it, with the time it has left (or the `ttl` of
|
|
360
|
+
* the call when it never expires).
|
|
361
|
+
* - With a refresh threshold, a cached value with less time to live than the threshold is returned at once and
|
|
362
|
+
* recomputed in the background (one refresh per key at a time), then written to its store and the stores above
|
|
363
|
+
* (to all the stores with `refreshAllStores`); a `'refresh'` event reports the new value or the failure, the
|
|
364
|
+
* cached value kept then. A threshold higher than the TTL refreshes the value on each call.
|
|
365
|
+
* - A computed value is written with {@link Cacher.set} (a `'set'` event); `undefined` is cached too.
|
|
366
|
+
* @param key The key.
|
|
367
|
+
* @param fn Computes the value when it is missing; its failure rejects the call and nothing is cached.
|
|
368
|
+
* @param options The time to live in milliseconds (`0` never expires), or the TTL and the refresh threshold.
|
|
369
|
+
* Default to the cache's `ttl` and `refreshThreshold` options.
|
|
370
|
+
* @returns The cached or computed value.
|
|
371
|
+
* @throws {Error} The error thrown by `fn`, or the failure of a store writing the computed value (without
|
|
372
|
+
* `nonBlocking`).
|
|
373
|
+
* @example
|
|
374
|
+
* ```typescript
|
|
375
|
+
* import type {Cacher} from 'lakutata/com/cacher'
|
|
376
|
+
*
|
|
377
|
+
* declare const cache: Cacher
|
|
378
|
+
* declare function loadRates(): Promise<Record<string, number>>
|
|
379
|
+
*
|
|
380
|
+
* //Loaded at most once per hour; in its last 5 minutes, served while reloaded in the background
|
|
381
|
+
* const rates: Record<string, number> = await cache.wrap('rates', loadRates, {ttl: 3600000, refreshThreshold: 300000})
|
|
382
|
+
* //A TTL depending on the value
|
|
383
|
+
* const user: {id: number, guest: boolean} = await cache.wrap('user:1', async () => ({id: 1, guest: true}), {
|
|
384
|
+
* ttl: (value: {id: number, guest: boolean}): number => value.guest ? 60000 : 3600000
|
|
385
|
+
* })
|
|
386
|
+
* ```
|
|
387
|
+
*/
|
|
388
|
+
async wrap(key, fn, options) {
|
|
389
|
+
const pending = this.#wrapping.get(key);
|
|
390
|
+
if (pending)
|
|
391
|
+
return pending;
|
|
392
|
+
const promise = this.computeWrap(key, fn, typeof options === 'object' ? options : { ttl: options })
|
|
393
|
+
.finally(() => this.#wrapping.delete(key));
|
|
394
|
+
this.#wrapping.set(key, promise);
|
|
395
|
+
return promise;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Run one {@link Cacher.wrap} call: read the key, compute and write it on a miss, copy it to the stores above or
|
|
399
|
+
* start its background refresh on a hit.
|
|
400
|
+
* @param key The key.
|
|
401
|
+
* @param fn Computes the value.
|
|
402
|
+
* @param options The TTL and the refresh threshold.
|
|
403
|
+
* @returns The cached or computed value.
|
|
404
|
+
* @throws {Error} The error thrown by `fn`, or the failure of a store writing the computed value.
|
|
405
|
+
* @protected
|
|
406
|
+
*/
|
|
407
|
+
async computeWrap(key, fn, options) {
|
|
408
|
+
const hit = await this.read(key);
|
|
409
|
+
if (!hit) {
|
|
410
|
+
const value = await fn();
|
|
411
|
+
return await this.set(key, value, this.resolveTTL(options.ttl, value));
|
|
412
|
+
}
|
|
413
|
+
const value = hit.entry.value;
|
|
414
|
+
const remaining = typeof hit.entry.expires === 'number' ? Math.max(1, hit.entry.expires - Date.now()) : undefined;
|
|
415
|
+
const threshold = (typeof options.refreshThreshold === 'function' ? options.refreshThreshold(value) : options.refreshThreshold) ?? this.refreshThreshold;
|
|
416
|
+
if (remaining !== undefined && threshold !== undefined && remaining < threshold) {
|
|
417
|
+
this.refresh(key, fn, options, value, hit.index);
|
|
418
|
+
}
|
|
419
|
+
else if (hit.index > 0) {
|
|
420
|
+
//The upper stores keep the value until it expires in the lower one
|
|
421
|
+
const ttl = remaining ?? this.resolveTTL(options.ttl, value);
|
|
422
|
+
await Promise.all(this.#stores.slice(0, hit.index).map((store) => store.set(key, value, ttl)))
|
|
423
|
+
.catch((error) => this.report(error));
|
|
424
|
+
}
|
|
425
|
+
return value;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Recompute a value in the background, unless the key is being refreshed already, and emit a `'refresh'` event
|
|
429
|
+
* with the new value or the failure. The component's destruction waits for it.
|
|
430
|
+
* @param key The key.
|
|
431
|
+
* @param fn Computes the value.
|
|
432
|
+
* @param options The TTL of the new value.
|
|
433
|
+
* @param current The value cached, reported by the event when the refresh fails.
|
|
434
|
+
* @param index The index of the store holding the value: the new value is written to it and to the stores above
|
|
435
|
+
* (to all the stores with `refreshAllStores`).
|
|
436
|
+
* @protected
|
|
437
|
+
*/
|
|
438
|
+
refresh(key, fn, options, current, index) {
|
|
439
|
+
if (this.#refreshing.has(key))
|
|
440
|
+
return;
|
|
441
|
+
const stores = this.refreshAllStores ? this.#stores : this.#stores.slice(0, index + 1);
|
|
442
|
+
this.#refreshing.set(key, (async () => {
|
|
443
|
+
try {
|
|
444
|
+
const value = await fn();
|
|
445
|
+
const ttl = this.resolveTTL(options.ttl, value);
|
|
446
|
+
await Promise.all(stores.map((store) => store.set(key, value, ttl)));
|
|
447
|
+
this.emit('refresh', { key: key, value: value });
|
|
448
|
+
}
|
|
449
|
+
catch (e) {
|
|
450
|
+
this.emit('refresh', { key: key, value: current, error: e });
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
this.#refreshing.delete(key);
|
|
454
|
+
}
|
|
455
|
+
})());
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Delete a key from all the stores. A `'del'` event is emitted once deleted; when a store fails, a `'del'` event
|
|
459
|
+
* carrying the error is emitted and the call rejects. With `nonBlocking`, it resolves and emits the `'del'` event
|
|
460
|
+
* before the stores are done, a failure emitting a second `'del'` event carrying the error.
|
|
461
|
+
* @param key The key; a missing key is not an error.
|
|
462
|
+
* @returns Always true, whether the key existed or not.
|
|
463
|
+
* @throws {Error} The failure of a store, without `nonBlocking`.
|
|
464
|
+
*/
|
|
465
|
+
async del(key) {
|
|
466
|
+
await this.runOnStores(this.#stores, (store) => store.delete(key), (error) => this.emit('del', { key: key, error: error }));
|
|
467
|
+
this.emit('del', { key: key });
|
|
468
|
+
return true;
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Delete several keys from all the stores. No `'del'` event is emitted; a failure is emitted as an `'error'` event
|
|
472
|
+
* and rejects the call (unless `nonBlocking`).
|
|
473
|
+
* @param keys The keys; the missing ones are not an error.
|
|
474
|
+
* @returns Always true, whether the keys existed or not.
|
|
475
|
+
* @throws {Error} The failure of a store, without `nonBlocking`.
|
|
476
|
+
*/
|
|
477
|
+
async multipleDel(keys) {
|
|
478
|
+
await this.runOnStores(this.#stores, (store) => store.deleteMany(keys), (error) => this.report(error));
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Delete all the entries of each store: the entries of its namespace when it has one, otherwise all its entries
|
|
483
|
+
* (except, in Redis, the keys containing the `keyPrefixSeparator`, which belong to the namespaces). A Memcache
|
|
484
|
+
* store flushes the whole server, its namespace ignored. A `'clear'` event is emitted once done; when a store fails,
|
|
485
|
+
* a `'clear'` event carrying the error is emitted and the call rejects (unless `nonBlocking`, which resolves and
|
|
486
|
+
* emits the event at once).
|
|
487
|
+
* @returns Always true.
|
|
488
|
+
* @throws {Error} The failure of a store, without `nonBlocking`.
|
|
489
|
+
*/
|
|
490
|
+
async clear() {
|
|
491
|
+
await this.runOnStores(this.#stores, (store) => store.clear(), (error) => this.emit('clear', error));
|
|
492
|
+
this.emit('clear');
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
on(event, listener) {
|
|
496
|
+
return super.on(event, listener);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
exports.Cacher = Cacher;
|
|
500
|
+
__decorate([
|
|
501
|
+
(0, di_1.Configurable)(core_1.DTO.Alternatives(core_1.DTO.Array(core_1.DTO.Alternatives(MemoryCacheOptions_js_1.MemoryCacheOptions.Schema(), FileCacheOptions_js_1.FileCacheOptions.Schema(), RedisCacheOptions_js_1.RedisCacheOptions.Schema(), MemcacheCacheOptions_js_1.MemcacheCacheOptions.Schema(), MongoCacheOptions_js_1.MongoCacheOptions.Schema(), SqliteCacheOptions_js_1.SqliteCacheOptions.Schema(), PostgresCacheOptions_js_1.PostgresCacheOptions.Schema(), MysqlCacheOptions_js_1.MysqlCacheOptions.Schema())), MemoryCacheOptions_js_1.MemoryCacheOptions.Schema(), FileCacheOptions_js_1.FileCacheOptions.Schema(), RedisCacheOptions_js_1.RedisCacheOptions.Schema(), MemcacheCacheOptions_js_1.MemcacheCacheOptions.Schema(), MongoCacheOptions_js_1.MongoCacheOptions.Schema(), SqliteCacheOptions_js_1.SqliteCacheOptions.Schema(), PostgresCacheOptions_js_1.PostgresCacheOptions.Schema(), MysqlCacheOptions_js_1.MysqlCacheOptions.Schema()).optional()),
|
|
502
|
+
__metadata("design:type", Object)
|
|
503
|
+
], Cacher.prototype, "stores", void 0);
|
|
504
|
+
__decorate([
|
|
505
|
+
(0, di_1.Configurable)(core_1.DTO.Number().positive().integer().optional()),
|
|
506
|
+
__metadata("design:type", Number)
|
|
507
|
+
], Cacher.prototype, "ttl", void 0);
|
|
508
|
+
__decorate([
|
|
509
|
+
(0, di_1.Configurable)(core_1.DTO.Number().positive().integer().optional()),
|
|
510
|
+
__metadata("design:type", Number)
|
|
511
|
+
], Cacher.prototype, "refreshThreshold", void 0);
|
|
512
|
+
__decorate([
|
|
513
|
+
(0, di_1.Configurable)(core_1.DTO.Boolean().optional()),
|
|
514
|
+
__metadata("design:type", Boolean)
|
|
515
|
+
], Cacher.prototype, "refreshAllStores", void 0);
|
|
516
|
+
__decorate([
|
|
517
|
+
(0, di_1.Configurable)(core_1.DTO.Boolean().optional()),
|
|
518
|
+
__metadata("design:type", Boolean)
|
|
519
|
+
], Cacher.prototype, "nonBlocking", void 0);
|
|
520
|
+
__decorate([
|
|
521
|
+
(0, di_1.Configurable)(core_1.DTO.String().optional()),
|
|
522
|
+
__metadata("design:type", String)
|
|
523
|
+
], Cacher.prototype, "cacheId", void 0);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the cache starts, by a store whose driver package is not installed: `redis` (or `@redis/client`) for
|
|
4
|
+
* Redis, `memjs` for Memcache, `mongodb` for MongoDB, `sqlite3` for SQLite, `pg` for PostgreSQL, `mysql2` for MySQL.
|
|
5
|
+
* Its message names the package to install. It fails the cache component's creation (and so the application's
|
|
6
|
+
* start). Its `errno` is `'E_CACHE_DRIVER_NOT_FOUND'`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class CacheDriverNotFoundException extends Exception {
|
|
9
|
+
errno: string | number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CacheDriverNotFoundException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when the cache starts, by a store whose driver package is not installed: `redis` (or `@redis/client`) for
|
|
7
|
+
* Redis, `memjs` for Memcache, `mongodb` for MongoDB, `sqlite3` for SQLite, `pg` for PostgreSQL, `mysql2` for MySQL.
|
|
8
|
+
* Its message names the package to install. It fails the cache component's creation (and so the application's
|
|
9
|
+
* start). Its `errno` is `'E_CACHE_DRIVER_NOT_FOUND'`.
|
|
10
|
+
*/
|
|
11
|
+
class CacheDriverNotFoundException extends core_1.Exception {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
this.errno = 'E_CACHE_DRIVER_NOT_FOUND';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
exports.CacheDriverNotFoundException = CacheDriverNotFoundException;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CacheStoreOptions } from '../types/CacheStoreOptions.js';
|
|
2
|
+
/**
|
|
3
|
+
* The options of a `Cacher` component, given to `buildCacherOptions` (or next to `class: Cacher` in the
|
|
4
|
+
* `components` of an application). They are validated when the component is created.
|
|
5
|
+
*/
|
|
6
|
+
export interface CacherOptions {
|
|
7
|
+
/**
|
|
8
|
+
* The stores, the fastest first: the options of one store or an array of them, `type` choosing the store (`'file'`,
|
|
9
|
+
* `'redis'`, `'memcache'`, `'mongo'`, `'sqlite'`, `'postgres'`, `'mysql'`). A read returns the entry of the first
|
|
10
|
+
* store holding it; a write or a deletion goes to all of them. The in-memory store is used only without stores.
|
|
11
|
+
* @default one in-memory store
|
|
12
|
+
*/
|
|
13
|
+
stores?: CacheStoreOptions[] | CacheStoreOptions;
|
|
14
|
+
/**
|
|
15
|
+
* The default time to live of the entries, in milliseconds (a positive integer), used when a write gives none.
|
|
16
|
+
* @default none: the entries never expire
|
|
17
|
+
*/
|
|
18
|
+
ttl?: number;
|
|
19
|
+
/**
|
|
20
|
+
* The default refresh threshold of `wrap()`, in milliseconds (a positive integer): a cached value with less time to
|
|
21
|
+
* live is returned and recomputed in the background.
|
|
22
|
+
* @default none: no background refresh
|
|
23
|
+
*/
|
|
24
|
+
refreshThreshold?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Whether a background refresh of `wrap()` writes the new value to all the stores, instead of the store holding the
|
|
27
|
+
* value and the stores above it.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
refreshAllStores?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Whether the writes and the deletions return without waiting for the stores (their failures emitted as events,
|
|
33
|
+
* the calls never rejecting), and, with several stores, the reads query them all at once and return the first
|
|
34
|
+
* entry found (not necessarily the first store's).
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
nonBlocking?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* An identifier of the cache. It is accepted for compatibility and not used.
|
|
40
|
+
*/
|
|
41
|
+
cacheId?: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.likePrefix = likePrefix;
|
|
4
|
+
/**
|
|
5
|
+
* The LIKE pattern (with the "!" escape character) of the keys in a namespace
|
|
6
|
+
* @param namespace
|
|
7
|
+
*/
|
|
8
|
+
function likePrefix(namespace) {
|
|
9
|
+
return `${namespace.replace(/[!%_]/g, '!$&')}:%`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load the driver package of a store, the first installed of the candidates
|
|
3
|
+
* @param member a member of the driver API, to find it in the ESM namespace or the CommonJS exports
|
|
4
|
+
* @param packageNames
|
|
5
|
+
*/
|
|
6
|
+
export declare function loadDriver<T = any>(member: string, ...packageNames: string[]): Promise<T>;
|