@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.
Files changed (116) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +175 -0
  3. package/dist/cjs/cache/Cacher.d.ts +446 -0
  4. package/dist/cjs/cache/Cacher.js +523 -0
  5. package/dist/cjs/cache/exceptions/CacheDriverNotFoundException.d.ts +10 -0
  6. package/dist/cjs/cache/exceptions/CacheDriverNotFoundException.js +17 -0
  7. package/dist/cjs/cache/interfaces/CacherOptions.d.ts +42 -0
  8. package/dist/cjs/cache/interfaces/CacherOptions.js +2 -0
  9. package/dist/cjs/cache/lib/LikePrefix.d.ts +5 -0
  10. package/dist/cjs/cache/lib/LikePrefix.js +10 -0
  11. package/dist/cjs/cache/lib/LoadDriver.d.ts +6 -0
  12. package/dist/cjs/cache/lib/LoadDriver.js +66 -0
  13. package/dist/cjs/cache/lib/Serializer.d.ts +18 -0
  14. package/dist/cjs/cache/lib/Serializer.js +46 -0
  15. package/dist/cjs/cache/lib/WithTimeout.d.ts +7 -0
  16. package/dist/cjs/cache/lib/WithTimeout.js +23 -0
  17. package/dist/cjs/cache/options/FileCacheOptions.d.ts +54 -0
  18. package/dist/cjs/cache/options/FileCacheOptions.js +62 -0
  19. package/dist/cjs/cache/options/MemcacheCacheOptions.d.ts +57 -0
  20. package/dist/cjs/cache/options/MemcacheCacheOptions.js +66 -0
  21. package/dist/cjs/cache/options/MemoryCacheOptions.d.ts +39 -0
  22. package/dist/cjs/cache/options/MemoryCacheOptions.js +52 -0
  23. package/dist/cjs/cache/options/MongoCacheOptions.d.ts +72 -0
  24. package/dist/cjs/cache/options/MongoCacheOptions.js +81 -0
  25. package/dist/cjs/cache/options/MysqlCacheOptions.d.ts +73 -0
  26. package/dist/cjs/cache/options/MysqlCacheOptions.js +82 -0
  27. package/dist/cjs/cache/options/PostgresCacheOptions.d.ts +87 -0
  28. package/dist/cjs/cache/options/PostgresCacheOptions.js +92 -0
  29. package/dist/cjs/cache/options/RedisCacheOptions.d.ts +120 -0
  30. package/dist/cjs/cache/options/RedisCacheOptions.js +113 -0
  31. package/dist/cjs/cache/options/SqliteCacheOptions.d.ts +53 -0
  32. package/dist/cjs/cache/options/SqliteCacheOptions.js +61 -0
  33. package/dist/cjs/cache/stores/CacheStore.d.ts +44 -0
  34. package/dist/cjs/cache/stores/CacheStore.js +42 -0
  35. package/dist/cjs/cache/stores/CreateCacheStore.d.ts +7 -0
  36. package/dist/cjs/cache/stores/CreateCacheStore.js +37 -0
  37. package/dist/cjs/cache/stores/FileStore.d.ts +51 -0
  38. package/dist/cjs/cache/stores/FileStore.js +143 -0
  39. package/dist/cjs/cache/stores/MemcacheStore.d.ts +42 -0
  40. package/dist/cjs/cache/stores/MemcacheStore.js +69 -0
  41. package/dist/cjs/cache/stores/MemoryStore.d.ts +34 -0
  42. package/dist/cjs/cache/stores/MemoryStore.js +75 -0
  43. package/dist/cjs/cache/stores/MongoStore.d.ts +55 -0
  44. package/dist/cjs/cache/stores/MongoStore.js +72 -0
  45. package/dist/cjs/cache/stores/MysqlStore.d.ts +27 -0
  46. package/dist/cjs/cache/stores/MysqlStore.js +71 -0
  47. package/dist/cjs/cache/stores/PostgresStore.d.ts +30 -0
  48. package/dist/cjs/cache/stores/PostgresStore.js +85 -0
  49. package/dist/cjs/cache/stores/RedisStore.d.ts +58 -0
  50. package/dist/cjs/cache/stores/RedisStore.js +131 -0
  51. package/dist/cjs/cache/stores/SerializedCacheStore.d.ts +48 -0
  52. package/dist/cjs/cache/stores/SerializedCacheStore.js +80 -0
  53. package/dist/cjs/cache/stores/SqliteStore.d.ts +44 -0
  54. package/dist/cjs/cache/stores/SqliteStore.js +91 -0
  55. package/dist/cjs/cache/types/CacheStoreOptions.d.ts +15 -0
  56. package/dist/cjs/cache/types/CacheStoreOptions.js +2 -0
  57. package/dist/cjs/exports/Cache.d.ts +12 -0
  58. package/dist/cjs/exports/Cache.js +31 -0
  59. package/dist/cjs/package.json +1 -0
  60. package/dist/esm/cache/Cacher.js +518 -0
  61. package/dist/esm/cache/exceptions/CacheDriverNotFoundException.js +13 -0
  62. package/dist/esm/cache/interfaces/CacherOptions.js +1 -0
  63. package/dist/esm/cache/lib/LikePrefix.js +7 -0
  64. package/dist/esm/cache/lib/LoadDriver.js +30 -0
  65. package/dist/esm/cache/lib/Serializer.js +42 -0
  66. package/dist/esm/cache/lib/WithTimeout.js +20 -0
  67. package/dist/esm/cache/options/FileCacheOptions.js +58 -0
  68. package/dist/esm/cache/options/MemcacheCacheOptions.js +62 -0
  69. package/dist/esm/cache/options/MemoryCacheOptions.js +48 -0
  70. package/dist/esm/cache/options/MongoCacheOptions.js +77 -0
  71. package/dist/esm/cache/options/MysqlCacheOptions.js +78 -0
  72. package/dist/esm/cache/options/PostgresCacheOptions.js +88 -0
  73. package/dist/esm/cache/options/RedisCacheOptions.js +109 -0
  74. package/dist/esm/cache/options/SqliteCacheOptions.js +57 -0
  75. package/dist/esm/cache/stores/CacheStore.js +36 -0
  76. package/dist/esm/cache/stores/CreateCacheStore.js +34 -0
  77. package/dist/esm/cache/stores/FileStore.js +136 -0
  78. package/dist/esm/cache/stores/MemcacheStore.js +65 -0
  79. package/dist/esm/cache/stores/MemoryStore.js +71 -0
  80. package/dist/esm/cache/stores/MongoStore.js +68 -0
  81. package/dist/esm/cache/stores/MysqlStore.js +67 -0
  82. package/dist/esm/cache/stores/PostgresStore.js +81 -0
  83. package/dist/esm/cache/stores/RedisStore.js +127 -0
  84. package/dist/esm/cache/stores/SerializedCacheStore.js +76 -0
  85. package/dist/esm/cache/stores/SqliteStore.js +87 -0
  86. package/dist/esm/cache/types/CacheStoreOptions.js +1 -0
  87. package/dist/esm/exports/Cache.js +12 -0
  88. package/dist/types/cache/Cacher.d.ts +446 -0
  89. package/dist/types/cache/exceptions/CacheDriverNotFoundException.d.ts +10 -0
  90. package/dist/types/cache/interfaces/CacherOptions.d.ts +42 -0
  91. package/dist/types/cache/lib/LikePrefix.d.ts +5 -0
  92. package/dist/types/cache/lib/LoadDriver.d.ts +6 -0
  93. package/dist/types/cache/lib/Serializer.d.ts +18 -0
  94. package/dist/types/cache/lib/WithTimeout.d.ts +7 -0
  95. package/dist/types/cache/options/FileCacheOptions.d.ts +54 -0
  96. package/dist/types/cache/options/MemcacheCacheOptions.d.ts +57 -0
  97. package/dist/types/cache/options/MemoryCacheOptions.d.ts +39 -0
  98. package/dist/types/cache/options/MongoCacheOptions.d.ts +72 -0
  99. package/dist/types/cache/options/MysqlCacheOptions.d.ts +73 -0
  100. package/dist/types/cache/options/PostgresCacheOptions.d.ts +87 -0
  101. package/dist/types/cache/options/RedisCacheOptions.d.ts +120 -0
  102. package/dist/types/cache/options/SqliteCacheOptions.d.ts +53 -0
  103. package/dist/types/cache/stores/CacheStore.d.ts +44 -0
  104. package/dist/types/cache/stores/CreateCacheStore.d.ts +7 -0
  105. package/dist/types/cache/stores/FileStore.d.ts +51 -0
  106. package/dist/types/cache/stores/MemcacheStore.d.ts +42 -0
  107. package/dist/types/cache/stores/MemoryStore.d.ts +34 -0
  108. package/dist/types/cache/stores/MongoStore.d.ts +55 -0
  109. package/dist/types/cache/stores/MysqlStore.d.ts +27 -0
  110. package/dist/types/cache/stores/PostgresStore.d.ts +30 -0
  111. package/dist/types/cache/stores/RedisStore.d.ts +58 -0
  112. package/dist/types/cache/stores/SerializedCacheStore.d.ts +48 -0
  113. package/dist/types/cache/stores/SqliteStore.d.ts +44 -0
  114. package/dist/types/cache/types/CacheStoreOptions.d.ts +15 -0
  115. package/dist/types/exports/Cache.d.ts +12 -0
  116. package/package.json +72 -0
@@ -0,0 +1,87 @@
1
+ import { SerializedCacheStore } from './SerializedCacheStore.js';
2
+ import { loadDriver } from '../lib/LoadDriver.js';
3
+ /**
4
+ * The table name of 2.x: the alphanumeric characters, starting with a letter
5
+ * @param name
6
+ */
7
+ function tableName(name) {
8
+ const sanitized = name.replace(/[^a-zA-Z0-9_]/g, '');
9
+ if (!sanitized)
10
+ throw new Error('Invalid table name: must contain alphanumeric characters');
11
+ return /^[a-zA-Z]/.test(sanitized) ? sanitized : `_${sanitized}`;
12
+ }
13
+ /**
14
+ * The SQLite store
15
+ * Table format: (key VARCHAR(255) PRIMARY KEY, value TEXT), with "{namespace}:{key}" keys
16
+ */
17
+ export class SqliteStore extends SerializedCacheStore {
18
+ constructor(options) {
19
+ super(options.namespace);
20
+ this.options = options;
21
+ this.table = tableName(options.table);
22
+ }
23
+ /**
24
+ * Query the rows
25
+ * @param sql
26
+ * @param params
27
+ * @protected
28
+ */
29
+ all(sql, params) {
30
+ return new Promise((resolve, reject) => this.database.all(sql, params, (error, rows) => error ? reject(error) : resolve(rows)));
31
+ }
32
+ /**
33
+ * Run a statement, resolve the number of changed rows
34
+ * @param sql
35
+ * @param params
36
+ * @protected
37
+ */
38
+ run(sql, params = []) {
39
+ return new Promise((resolve, reject) => this.database.run(sql, params, function (error) {
40
+ error ? reject(error) : resolve(this.changes);
41
+ }));
42
+ }
43
+ async connect() {
44
+ const driver = await loadDriver('Database', 'sqlite3');
45
+ await new Promise((resolve, reject) => {
46
+ this.database = new driver.Database(this.options.database, (error) => error ? reject(error) : resolve());
47
+ });
48
+ this.database.configure('busyTimeout', this.options.busyTimeout ?? 10000);
49
+ await this.run(`CREATE TABLE IF NOT EXISTS ${this.table}(key VARCHAR(255) PRIMARY KEY, value TEXT)`);
50
+ }
51
+ async disconnect() {
52
+ if (!this.database)
53
+ return;
54
+ await new Promise((resolve, reject) => this.database.close((error) => error ? reject(error) : resolve()));
55
+ }
56
+ async read(key) {
57
+ const rows = await this.all(`SELECT value FROM ${this.table} WHERE key = ?`, [key]);
58
+ return rows[0]?.value;
59
+ }
60
+ async readMany(keys) {
61
+ const rows = await this.all(`SELECT key, value FROM ${this.table} WHERE key IN (SELECT value FROM json_each(?))`, [JSON.stringify(keys)]);
62
+ const values = new Map(rows.map((row) => [row.key, row.value]));
63
+ return keys.map((key) => values.get(key));
64
+ }
65
+ async write(entry) {
66
+ await this.run(`INSERT INTO ${this.table} (key, value) VALUES (?, ?) ON CONFLICT (key) DO UPDATE SET value = excluded.value`, [entry.key, entry.data]);
67
+ }
68
+ async writeMany(entries) {
69
+ await this.run(`INSERT INTO ${this.table} (key, value) SELECT json_extract(value, '$[0]'), json_extract(value, '$[1]') FROM json_each(?) WHERE 1 ON CONFLICT (key) DO UPDATE SET value = excluded.value`, [JSON.stringify(entries.map((entry) => [entry.key, entry.data]))]);
70
+ }
71
+ async remove(key) {
72
+ return (await this.run(`DELETE FROM ${this.table} WHERE key = ?`, [key])) > 0;
73
+ }
74
+ async removeMany(keys) {
75
+ return (await this.run(`DELETE FROM ${this.table} WHERE key IN (SELECT value FROM json_each(?))`, [JSON.stringify(keys)])) > 0;
76
+ }
77
+ async clear() {
78
+ if (this.namespace) {
79
+ //LIKE ignores the case in SQLite
80
+ const prefix = `${this.namespace}:`;
81
+ await this.run(`DELETE FROM ${this.table} WHERE substr(key, 1, length(?1)) = ?1`, [prefix]);
82
+ }
83
+ else {
84
+ await this.run(`DELETE FROM ${this.table}`);
85
+ }
86
+ }
87
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export * from '../cache/Cacher.js';
2
+ export * from '../cache/interfaces/CacherOptions.js';
3
+ export * from '../cache/options/FileCacheOptions.js';
4
+ export * from '../cache/options/MemoryCacheOptions.js';
5
+ export * from '../cache/options/MemcacheCacheOptions.js';
6
+ export * from '../cache/options/MongoCacheOptions.js';
7
+ export * from '../cache/options/MysqlCacheOptions.js';
8
+ export * from '../cache/options/PostgresCacheOptions.js';
9
+ export * from '../cache/options/RedisCacheOptions.js';
10
+ export * from '../cache/options/SqliteCacheOptions.js';
11
+ export * from '../cache/types/CacheStoreOptions.js';
12
+ export * from '../cache/exceptions/CacheDriverNotFoundException.js';
@@ -0,0 +1,446 @@
1
+ import { CacherOptions } from './interfaces/CacherOptions.js';
2
+ import { Component, ComponentOptionsBuilder } from '@lakutata/core';
3
+ import { CacheStoreOptions } from './types/CacheStoreOptions.js';
4
+ import { CacheStore } from './stores/CacheStore.js';
5
+ import { CacheEntry } from './lib/Serializer.js';
6
+ /**
7
+ * Build the component options of a {@link Cacher}, to register it in the `components` of an application (or a module)
8
+ * under the name of your choice. The options are validated when the component is created, not here.
9
+ * @param options The cache options: its stores, its default TTL, its refresh and write behavior. Without options, one
10
+ * in-memory store whose entries never expire.
11
+ * @returns The component options (`{class: Cacher, ...options}`).
12
+ * @example
13
+ * ```typescript
14
+ * import {Application} from 'lakutata'
15
+ * import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
16
+ *
17
+ * Application
18
+ * .run(() => ({
19
+ * id: 'cache.app',
20
+ * name: 'Cache',
21
+ * components: {
22
+ * //An in-memory cache whose entries expire after one minute
23
+ * cache: buildCacherOptions({ttl: 60000})
24
+ * }
25
+ * }))
26
+ * .onLaunched(async (app: Application): Promise<void> => {
27
+ * const cache: Cacher = await app.getObject('cache')
28
+ * await cache.set('greeting', 'Hello')
29
+ * console.log(await cache.get<string>('greeting')) //Hello
30
+ * })
31
+ * ```
32
+ */
33
+ export declare const buildCacherOptions: ComponentOptionsBuilder<CacherOptions>;
34
+ /**
35
+ * An entry written by {@link Cacher.multipleSet}.
36
+ */
37
+ export type MultipleSetInput = {
38
+ /**
39
+ * The key of the entry.
40
+ */
41
+ key: string;
42
+ /**
43
+ * The value to cache. The stores other than the memory one keep it as JSON (see {@link Cacher}).
44
+ */
45
+ value: any;
46
+ /**
47
+ * The entry's time to live, in milliseconds; `0` never expires.
48
+ * @default the cache's `ttl` option (never expires without it)
49
+ */
50
+ ttl?: number;
51
+ };
52
+ /**
53
+ * The data of a `'set'` event of a {@link Cacher}.
54
+ */
55
+ export type OnSetEventData = {
56
+ /**
57
+ * The key written.
58
+ */
59
+ key: string;
60
+ /**
61
+ * The value written.
62
+ */
63
+ value: any;
64
+ /**
65
+ * The error of the write when it failed; absent when it succeeded.
66
+ */
67
+ error?: Error;
68
+ };
69
+ /**
70
+ * The data of a `'del'` event of a {@link Cacher}.
71
+ */
72
+ export type OnDeleteEventData = {
73
+ /**
74
+ * The key deleted.
75
+ */
76
+ key: string;
77
+ /**
78
+ * The error of the deletion when it failed; absent when it succeeded.
79
+ */
80
+ error?: Error;
81
+ };
82
+ /**
83
+ * The data of a `'refresh'` event of a {@link Cacher}: a value recomputed in the background by
84
+ * {@link Cacher.wrap}.
85
+ */
86
+ export type OnRefreshEventData = {
87
+ /**
88
+ * The key refreshed.
89
+ */
90
+ key: string;
91
+ /**
92
+ * The new value; the value still cached when the refresh failed.
93
+ */
94
+ value: any;
95
+ /**
96
+ * The error thrown by the computation or by a store when the refresh failed; absent when it succeeded.
97
+ */
98
+ error?: Error;
99
+ };
100
+ /**
101
+ * The options of {@link Cacher.wrap}.
102
+ */
103
+ export type WrapOptions<T = any> = {
104
+ /**
105
+ * The time to live of the computed value, in milliseconds (`0` never expires), or a function of the value returning
106
+ * it.
107
+ * @default the cache's `ttl` option (never expires without it)
108
+ */
109
+ ttl?: number | ((value: T) => number);
110
+ /**
111
+ * When a cached value has less time to live than this, in milliseconds, `wrap()` returns it and recomputes it in
112
+ * the background; or a function of the cached value returning it. Ignored for the entries which never expire.
113
+ * @default the cache's `refreshThreshold` option (no background refresh without it)
114
+ */
115
+ refreshThreshold?: number | ((value: T) => number);
116
+ };
117
+ type StoreHit = {
118
+ entry: CacheEntry;
119
+ index: number;
120
+ };
121
+ /**
122
+ * The cache component: a key-value cache with expiration, backed by one or more stores (memory, file, Redis,
123
+ * Memcache, MongoDB, SQLite, PostgreSQL, MySQL). Register it in the `components` of an application with
124
+ * {@link buildCacherOptions} (or `{class: Cacher, ...options}`) and get it with `getObject()` or `@Inject`; it is a
125
+ * singleton. Without stores, it caches in memory.
126
+ *
127
+ * The stores connect when the component is created: when one fails to connect, the others are disconnected and the
128
+ * creation fails with its error (the application does not start). A store whose driver package is not installed
129
+ * fails with {@link CacheDriverNotFoundException}. The stores disconnect when the component is destroyed, after the
130
+ * background refreshes in progress and the file store's pending writes.
131
+ *
132
+ * With several stores, they are tiers, the first one the fastest: a read returns the entry of the first store holding
133
+ * it, a write or a deletion goes to all of them. Only {@link Cacher.wrap} copies an entry found in a lower store to
134
+ * the stores above it.
135
+ *
136
+ * The TTLs are in milliseconds: an entry whose TTL is `0` or unset (without the `ttl` option) never expires; an expired
137
+ * entry is a miss. The memory store keeps the values by reference (a cached object changed afterwards is changed in the
138
+ * cache); the other stores keep them as JSON: the `Buffer`s are kept, but a `Date` comes back as its ISO string, a
139
+ * class instance as a plain object, a `Map` or a `Set` as `{}`, and a `bigint` fails the write.
140
+ *
141
+ * A store failing to read is a miss (its error is emitted as an `'error'` event); a store failing to write or delete
142
+ * rejects the call, unless the `nonBlocking` option is set.
143
+ * @example
144
+ * ```typescript
145
+ * import {Application, Component} from 'lakutata'
146
+ * import {Inject} from 'lakutata/decorator/di'
147
+ * import {buildCacherOptions, type Cacher} from 'lakutata/com/cacher'
148
+ *
149
+ * type Product = {id: number, name: string}
150
+ *
151
+ * class Catalog extends Component {
152
+ * @Inject('cache')
153
+ * protected readonly cache: Cacher
154
+ *
155
+ * public async product(id: number): Promise<Product> {
156
+ * //Computed once, then read from the cache for 5 minutes (recomputed in the background in its last minute)
157
+ * return this.cache.wrap(`product:${id}`, async (): Promise<Product> => ({id: id, name: `Product ${id}`}), {
158
+ * ttl: 300000,
159
+ * refreshThreshold: 60000
160
+ * })
161
+ * }
162
+ *
163
+ * public async rename(id: number, name: string): Promise<void> {
164
+ * await this.cache.set(`product:${id}`, {id: id, name: name}, 300000)
165
+ * }
166
+ *
167
+ * public async remove(id: number): Promise<void> {
168
+ * await this.cache.del(`product:${id}`)
169
+ * }
170
+ * }
171
+ *
172
+ * Application
173
+ * .run(() => ({
174
+ * id: 'catalog.app',
175
+ * name: 'Catalog',
176
+ * components: {
177
+ * //A local file tier in front of Redis (the "redis" package installed)
178
+ * cache: buildCacherOptions({
179
+ * stores: [
180
+ * {type: 'file', filename: './cache/catalog.json'},
181
+ * {type: 'redis', host: '127.0.0.1', namespace: 'catalog'}
182
+ * ],
183
+ * ttl: 600000
184
+ * }),
185
+ * catalog: {class: Catalog}
186
+ * }
187
+ * }))
188
+ * .onLaunched(async (app: Application): Promise<void> => {
189
+ * const cache: Cacher = await app.getObject('cache')
190
+ * cache.on('error', (error: Error): void => console.error('Cache store error', error))
191
+ * const catalog: Catalog = await app.getObject('catalog')
192
+ * console.log(await catalog.product(1))
193
+ * })
194
+ * ```
195
+ */
196
+ export declare class Cacher extends Component {
197
+ #private;
198
+ /**
199
+ * The stores, the fastest first: the options of one store or an array of them (`type` chooses the store).
200
+ * @default one in-memory store
201
+ */
202
+ protected readonly stores?: CacheStoreOptions[] | CacheStoreOptions;
203
+ /**
204
+ * The default time to live of the entries, in milliseconds (a positive integer), used when a write gives none.
205
+ * @default none: the entries never expire
206
+ */
207
+ protected readonly ttl?: number;
208
+ /**
209
+ * The default refresh threshold of {@link Cacher.wrap}, in milliseconds (a positive integer): a cached value with
210
+ * less time to live is recomputed in the background.
211
+ * @default none: no background refresh
212
+ */
213
+ protected readonly refreshThreshold?: number;
214
+ /**
215
+ * Whether a background refresh writes the new value to all the stores, instead of the store holding the value and
216
+ * the stores above it.
217
+ * @default false
218
+ */
219
+ protected readonly refreshAllStores?: boolean;
220
+ /**
221
+ * Whether the writes and the deletions return without waiting for the stores (their failures emitted as events,
222
+ * the calls never rejecting), and, with several stores, the reads query them all at once and return the first
223
+ * entry found (not necessarily the first store's).
224
+ * @default false
225
+ */
226
+ protected readonly nonBlocking?: boolean;
227
+ /**
228
+ * An identifier of the cache. It is accepted for compatibility and not used.
229
+ */
230
+ protected readonly cacheId?: string;
231
+ protected init(): Promise<void>;
232
+ protected destroy(): Promise<void>;
233
+ /**
234
+ * Emit a store error as an `'error'` event; dropped when the event has no listener.
235
+ * @param error The error.
236
+ * @protected
237
+ */
238
+ protected report(error: unknown): void;
239
+ /**
240
+ * Resolve the time to live of a value.
241
+ * @param ttl The TTL in milliseconds, a function of the value returning it, or undefined for the cache's `ttl`
242
+ * option.
243
+ * @param value The value cached.
244
+ * @returns The TTL in milliseconds, undefined when the entry never expires.
245
+ * @protected
246
+ */
247
+ protected resolveTTL<T>(ttl: number | ((value: T) => number) | undefined, value: T): number | undefined;
248
+ /**
249
+ * Read an entry from one store; a failing store is a miss, its error emitted as an `'error'` event.
250
+ * @param store The store.
251
+ * @param key The key.
252
+ * @returns The entry, undefined when it is missing, expired or the store failed.
253
+ * @protected
254
+ */
255
+ protected readStore(store: CacheStore, key: string): Promise<CacheEntry | undefined>;
256
+ /**
257
+ * Read an entry from the first store holding it, in the stores' order; with `nonBlocking` and several stores, from
258
+ * the first store answering with it.
259
+ * @param key The key.
260
+ * @returns The entry and the index of its store, undefined on a miss.
261
+ * @protected
262
+ */
263
+ protected read(key: string): Promise<StoreHit | undefined>;
264
+ /**
265
+ * Run an operation on stores, in parallel. With `nonBlocking`, it resolves at once and a failure is only passed to
266
+ * `onError`.
267
+ * @param stores The stores.
268
+ * @param operation The operation run on each store.
269
+ * @param onError Called with the first failure.
270
+ * @throws {Error} The first failure of the operation, without `nonBlocking`.
271
+ * @protected
272
+ */
273
+ protected runOnStores(stores: CacheStore[], operation: (store: CacheStore) => Promise<unknown>, onError: (error: Error) => void): Promise<void>;
274
+ /**
275
+ * Cache a value under a key, in all the stores, replacing the previous one. A `'set'` event is emitted once
276
+ * written; when a store fails, a `'set'` event carrying the error is emitted and the call rejects.
277
+ *
278
+ * With `nonBlocking`, the call resolves and the `'set'` event is emitted before the stores are written; a failure
279
+ * then emits a second `'set'` event carrying the error.
280
+ * @param key The key.
281
+ * @param value The value. The stores other than the memory one keep it as JSON.
282
+ * @param ttl The time to live, in milliseconds; `0` never expires. Defaults to the cache's `ttl` option (never
283
+ * expires without it).
284
+ * @returns The value given.
285
+ * @throws {Error} The failure of a store (its driver's error, or a `TypeError` for a value JSON cannot write),
286
+ * without `nonBlocking`.
287
+ */
288
+ set<T>(key: string, value: T, ttl?: number): Promise<T>;
289
+ /**
290
+ * Cache several values, in all the stores (in one batch per store where the store allows it). No `'set'` event is
291
+ * emitted; a failure is emitted as an `'error'` event and rejects the call (unless `nonBlocking`).
292
+ * @param options The entries; an entry without `ttl` gets the cache's `ttl` option.
293
+ * @returns The entries written, their TTL resolved.
294
+ * @throws {Error} The failure of a store, without `nonBlocking`.
295
+ */
296
+ multipleSet(options: MultipleSetInput[]): Promise<MultipleSetInput[]>;
297
+ /**
298
+ * Read the value of a key, from the first store holding it. It does not copy the value to the stores above (see
299
+ * {@link Cacher.wrap}). A failing store is a miss (its error emitted as an `'error'` event): it never rejects.
300
+ * @param key The key.
301
+ * @returns The value; undefined when the key is missing or expired (or when undefined was cached).
302
+ */
303
+ get<T = any>(key: string): Promise<T>;
304
+ /**
305
+ * Read the values of several keys; each key from the first store holding it (the missing keys only are asked to
306
+ * the next store). A failing store misses all its keys (its error emitted as an `'error'` event): it never rejects.
307
+ * @param keys The keys.
308
+ * @returns The values, in the order of the keys: undefined for the missing and expired ones.
309
+ */
310
+ multipleGet(keys: string[]): Promise<any[]>;
311
+ /**
312
+ * Read when a key expires. Despite its name, it returns a point in time, not a remaining duration: compute the
313
+ * time left with `expires - Date.now()`.
314
+ * @param key The key.
315
+ * @returns The expiration time, in milliseconds since the epoch; `-1` when the key is missing, expired or never
316
+ * expires.
317
+ */
318
+ getTTL(key: string): Promise<number>;
319
+ /**
320
+ * Read the value of a key, or compute it and cache it when it is missing or expired (the cache-aside pattern).
321
+ *
322
+ * - The concurrent calls on the same key share one call and its result, whatever their `fn` and options.
323
+ * - A value found in a lower store is copied to the stores above it, with the time it has left (or the `ttl` of
324
+ * the call when it never expires).
325
+ * - With a refresh threshold, a cached value with less time to live than the threshold is returned at once and
326
+ * recomputed in the background (one refresh per key at a time), then written to its store and the stores above
327
+ * (to all the stores with `refreshAllStores`); a `'refresh'` event reports the new value or the failure, the
328
+ * cached value kept then. A threshold higher than the TTL refreshes the value on each call.
329
+ * - A computed value is written with {@link Cacher.set} (a `'set'` event); `undefined` is cached too.
330
+ * @param key The key.
331
+ * @param fn Computes the value when it is missing; its failure rejects the call and nothing is cached.
332
+ * @param options The time to live in milliseconds (`0` never expires), or the TTL and the refresh threshold.
333
+ * Default to the cache's `ttl` and `refreshThreshold` options.
334
+ * @returns The cached or computed value.
335
+ * @throws {Error} The error thrown by `fn`, or the failure of a store writing the computed value (without
336
+ * `nonBlocking`).
337
+ * @example
338
+ * ```typescript
339
+ * import type {Cacher} from 'lakutata/com/cacher'
340
+ *
341
+ * declare const cache: Cacher
342
+ * declare function loadRates(): Promise<Record<string, number>>
343
+ *
344
+ * //Loaded at most once per hour; in its last 5 minutes, served while reloaded in the background
345
+ * const rates: Record<string, number> = await cache.wrap('rates', loadRates, {ttl: 3600000, refreshThreshold: 300000})
346
+ * //A TTL depending on the value
347
+ * const user: {id: number, guest: boolean} = await cache.wrap('user:1', async () => ({id: 1, guest: true}), {
348
+ * ttl: (value: {id: number, guest: boolean}): number => value.guest ? 60000 : 3600000
349
+ * })
350
+ * ```
351
+ */
352
+ wrap<T>(key: string, fn: () => T | Promise<T>, options?: number | WrapOptions<T>): Promise<T>;
353
+ /**
354
+ * Run one {@link Cacher.wrap} call: read the key, compute and write it on a miss, copy it to the stores above or
355
+ * start its background refresh on a hit.
356
+ * @param key The key.
357
+ * @param fn Computes the value.
358
+ * @param options The TTL and the refresh threshold.
359
+ * @returns The cached or computed value.
360
+ * @throws {Error} The error thrown by `fn`, or the failure of a store writing the computed value.
361
+ * @protected
362
+ */
363
+ protected computeWrap<T>(key: string, fn: () => T | Promise<T>, options: WrapOptions<T>): Promise<T>;
364
+ /**
365
+ * Recompute a value in the background, unless the key is being refreshed already, and emit a `'refresh'` event
366
+ * with the new value or the failure. The component's destruction waits for it.
367
+ * @param key The key.
368
+ * @param fn Computes the value.
369
+ * @param options The TTL of the new value.
370
+ * @param current The value cached, reported by the event when the refresh fails.
371
+ * @param index The index of the store holding the value: the new value is written to it and to the stores above
372
+ * (to all the stores with `refreshAllStores`).
373
+ * @protected
374
+ */
375
+ protected refresh<T>(key: string, fn: () => T | Promise<T>, options: WrapOptions<T>, current: T, index: number): void;
376
+ /**
377
+ * Delete a key from all the stores. A `'del'` event is emitted once deleted; when a store fails, a `'del'` event
378
+ * carrying the error is emitted and the call rejects. With `nonBlocking`, it resolves and emits the `'del'` event
379
+ * before the stores are done, a failure emitting a second `'del'` event carrying the error.
380
+ * @param key The key; a missing key is not an error.
381
+ * @returns Always true, whether the key existed or not.
382
+ * @throws {Error} The failure of a store, without `nonBlocking`.
383
+ */
384
+ del(key: string): Promise<boolean>;
385
+ /**
386
+ * Delete several keys from all the stores. No `'del'` event is emitted; a failure is emitted as an `'error'` event
387
+ * and rejects the call (unless `nonBlocking`).
388
+ * @param keys The keys; the missing ones are not an error.
389
+ * @returns Always true, whether the keys existed or not.
390
+ * @throws {Error} The failure of a store, without `nonBlocking`.
391
+ */
392
+ multipleDel(keys: string[]): Promise<boolean>;
393
+ /**
394
+ * Delete all the entries of each store: the entries of its namespace when it has one, otherwise all its entries
395
+ * (except, in Redis, the keys containing the `keyPrefixSeparator`, which belong to the namespaces). A Memcache
396
+ * store flushes the whole server, its namespace ignored. A `'clear'` event is emitted once done; when a store fails,
397
+ * a `'clear'` event carrying the error is emitted and the call rejects (unless `nonBlocking`, which resolves and
398
+ * emits the event at once).
399
+ * @returns Always true.
400
+ * @throws {Error} The failure of a store, without `nonBlocking`.
401
+ */
402
+ clear(): Promise<boolean>;
403
+ /**
404
+ * Listen to the writes of {@link Cacher.set} and {@link Cacher.wrap}, successful or failed (`data.error`). The
405
+ * listener is called asynchronously, after the emitting.
406
+ * @param event `'set'`.
407
+ * @param listener Called with the key, the value and the error of a failed write.
408
+ * @returns The component, for chaining.
409
+ */
410
+ on(event: 'set', listener: (data: OnSetEventData) => void): this;
411
+ /**
412
+ * Listen to the deletions of {@link Cacher.del}, successful or failed (`data.error`). The listener is called
413
+ * asynchronously, after the emitting.
414
+ * @param event `'del'`.
415
+ * @param listener Called with the key and the error of a failed deletion.
416
+ * @returns The component, for chaining.
417
+ */
418
+ on(event: 'del', listener: (data: OnDeleteEventData) => void): this;
419
+ /**
420
+ * Listen to the clearings of {@link Cacher.clear}, successful or failed. The listener is called asynchronously,
421
+ * after the emitting.
422
+ * @param event `'clear'`.
423
+ * @param listener Called without argument on a success, with the error on a failure.
424
+ * @returns The component, for chaining.
425
+ */
426
+ on(event: 'clear', listener: (error?: Error) => void): this;
427
+ /**
428
+ * Listen to the background refreshes of {@link Cacher.wrap}, successful or failed (`data.error`). The listener is
429
+ * called asynchronously, after the emitting.
430
+ * @param event `'refresh'`.
431
+ * @param listener Called with the key, the new value (the cached one on a failure) and the error of a failure.
432
+ * @returns The component, for chaining.
433
+ */
434
+ on(event: 'refresh', listener: (data: OnRefreshEventData) => void): this;
435
+ /**
436
+ * Listen to the store errors which reject no call: the failed reads (counted as misses), the failures of
437
+ * {@link Cacher.multipleSet} and {@link Cacher.multipleDel}, of the copies to the upper stores, and the errors of
438
+ * the connections (a lost Redis connection, a failed file write…). Without listener, these errors are dropped.
439
+ * The listener is called asynchronously, after the emitting.
440
+ * @param event `'error'`.
441
+ * @param listener Called with the error.
442
+ * @returns The component, for chaining.
443
+ */
444
+ on(event: 'error', listener: (error: Error) => void): this;
445
+ }
446
+ export {};
@@ -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,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,5 @@
1
+ /**
2
+ * The LIKE pattern (with the "!" escape character) of the keys in a namespace
3
+ * @param namespace
4
+ */
5
+ export declare function likePrefix(namespace: string): string;
@@ -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>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The stored form of a cache entry
3
+ * The strings starting with a colon get one more colon, the buffers are written as ":base64:" strings (the format of 2.x)
4
+ */
5
+ export type CacheEntry<T = unknown> = {
6
+ value: T;
7
+ expires?: number;
8
+ };
9
+ /**
10
+ * Serialize data
11
+ * @param data
12
+ */
13
+ export declare function serialize(data: unknown): string;
14
+ /**
15
+ * Deserialize data
16
+ * @param text
17
+ */
18
+ export declare function deserialize<T = unknown>(text: string): T;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Reject a promise which does not settle in time
3
+ * @param promise
4
+ * @param timeout ms
5
+ * @param message
6
+ */
7
+ export declare function withTimeout<T>(promise: Promise<T>, timeout: number, message: string): Promise<T>;