@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,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,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;
|