@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,136 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
4
|
+
import { deserialize, serialize } from '../lib/Serializer.js';
|
|
5
|
+
/**
|
|
6
|
+
* The file store, the entries are kept in memory and written to the file in batches
|
|
7
|
+
* File format: {"cache":[[key,{"expire":ms,"value":serialized entry}]],"lastExpire":ms}
|
|
8
|
+
*/
|
|
9
|
+
export class FileStore extends SerializedCacheStore {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options.namespace);
|
|
12
|
+
this.records = new Map();
|
|
13
|
+
this.lastExpire = Date.now();
|
|
14
|
+
this.saving = Promise.resolve();
|
|
15
|
+
this.filename = path.resolve(options.filename);
|
|
16
|
+
this.expiredCheckDelay = options.expiredCheckDelay ?? 3600000;
|
|
17
|
+
this.writeDelay = options.writeDelay ?? 100;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Whether a record has expired
|
|
21
|
+
* @param record
|
|
22
|
+
* @protected
|
|
23
|
+
*/
|
|
24
|
+
isExpired(record) {
|
|
25
|
+
return typeof record.expire === 'number' && record.expire <= Date.now();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Remove the expired records, at most once in each expiredCheckDelay
|
|
29
|
+
* @protected
|
|
30
|
+
*/
|
|
31
|
+
removeExpired() {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - this.lastExpire <= this.expiredCheckDelay)
|
|
34
|
+
return;
|
|
35
|
+
this.lastExpire = now;
|
|
36
|
+
for (const [key, record] of this.records)
|
|
37
|
+
if (this.isExpired(record))
|
|
38
|
+
this.records.delete(key);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Save the records after the write delay
|
|
42
|
+
* @protected
|
|
43
|
+
*/
|
|
44
|
+
scheduleSave() {
|
|
45
|
+
if (this.saveTimer)
|
|
46
|
+
return;
|
|
47
|
+
this.saveTimer = setTimeout(() => {
|
|
48
|
+
this.saveTimer = undefined;
|
|
49
|
+
this.saving = this.saving.then(() => this.save()).catch((error) => this.onError(error));
|
|
50
|
+
}, this.writeDelay);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Write the records to the file, through a temporary file
|
|
54
|
+
* @protected
|
|
55
|
+
*/
|
|
56
|
+
async save() {
|
|
57
|
+
this.removeExpired();
|
|
58
|
+
const content = serialize({ cache: [...this.records], lastExpire: this.lastExpire });
|
|
59
|
+
const temporary = `${this.filename}.${process.pid}.tmp`;
|
|
60
|
+
await mkdir(path.dirname(this.filename), { recursive: true });
|
|
61
|
+
await writeFile(temporary, content);
|
|
62
|
+
await rename(temporary, this.filename);
|
|
63
|
+
}
|
|
64
|
+
async connect() {
|
|
65
|
+
let text;
|
|
66
|
+
try {
|
|
67
|
+
text = await readFile(this.filename, 'utf8');
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if (e.code === 'ENOENT')
|
|
71
|
+
return;
|
|
72
|
+
throw e;
|
|
73
|
+
}
|
|
74
|
+
const content = deserialize(text);
|
|
75
|
+
this.records = new Map(Array.isArray(content.cache) ? content.cache : Object.entries(content.cache));
|
|
76
|
+
if (typeof content.lastExpire === 'number')
|
|
77
|
+
this.lastExpire = content.lastExpire;
|
|
78
|
+
}
|
|
79
|
+
async disconnect() {
|
|
80
|
+
if (this.saveTimer) {
|
|
81
|
+
clearTimeout(this.saveTimer);
|
|
82
|
+
this.saveTimer = undefined;
|
|
83
|
+
this.saving = this.saving.then(() => this.save());
|
|
84
|
+
}
|
|
85
|
+
await this.saving;
|
|
86
|
+
}
|
|
87
|
+
async read(key) {
|
|
88
|
+
const record = this.records.get(key);
|
|
89
|
+
if (record === undefined)
|
|
90
|
+
return undefined;
|
|
91
|
+
if (this.isExpired(record)) {
|
|
92
|
+
await this.remove(key);
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
return record.value;
|
|
96
|
+
}
|
|
97
|
+
async readMany(keys) {
|
|
98
|
+
return Promise.all(keys.map((key) => this.read(key)));
|
|
99
|
+
}
|
|
100
|
+
async write(entry) {
|
|
101
|
+
this.records.set(entry.key, { expire: entry.expires, value: entry.data });
|
|
102
|
+
this.scheduleSave();
|
|
103
|
+
}
|
|
104
|
+
async writeMany(entries) {
|
|
105
|
+
for (const entry of entries)
|
|
106
|
+
this.records.set(entry.key, { expire: entry.expires, value: entry.data });
|
|
107
|
+
this.scheduleSave();
|
|
108
|
+
}
|
|
109
|
+
async remove(key) {
|
|
110
|
+
if (!this.records.delete(key))
|
|
111
|
+
return false;
|
|
112
|
+
this.scheduleSave();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
async removeMany(keys) {
|
|
116
|
+
let removed = false;
|
|
117
|
+
for (const key of keys)
|
|
118
|
+
removed = this.records.delete(key) || removed;
|
|
119
|
+
if (removed)
|
|
120
|
+
this.scheduleSave();
|
|
121
|
+
return removed;
|
|
122
|
+
}
|
|
123
|
+
async clear() {
|
|
124
|
+
if (this.namespace) {
|
|
125
|
+
const prefix = `${this.namespace}:`;
|
|
126
|
+
for (const key of this.records.keys())
|
|
127
|
+
if (key.startsWith(prefix))
|
|
128
|
+
this.records.delete(key);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
this.records.clear();
|
|
132
|
+
}
|
|
133
|
+
this.lastExpire = Date.now();
|
|
134
|
+
this.scheduleSave();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
2
|
+
import { loadDriver } from '../lib/LoadDriver.js';
|
|
3
|
+
//Larger expiration times are unix timestamps for memcached
|
|
4
|
+
const MAX_RELATIVE_EXPIRATION = 60 * 60 * 24 * 30;
|
|
5
|
+
/**
|
|
6
|
+
* The Memcache store
|
|
7
|
+
* Key format: "{namespace}:{namespace}:{key}", or "{key}" without namespace
|
|
8
|
+
*/
|
|
9
|
+
export class MemcacheStore extends SerializedCacheStore {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
super(options.namespace);
|
|
12
|
+
this.options = options;
|
|
13
|
+
}
|
|
14
|
+
storeKey(key) {
|
|
15
|
+
return this.namespace ? `${this.namespace.trim()}:${`${this.namespace}:${key}`.trim()}` : key;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The memcached expiration of an entry, the entries expire after their expiration time
|
|
19
|
+
* @param entry
|
|
20
|
+
* @protected
|
|
21
|
+
*/
|
|
22
|
+
expiration(entry) {
|
|
23
|
+
if (!entry.ttl || !entry.expires)
|
|
24
|
+
return 0;
|
|
25
|
+
const seconds = Math.ceil(entry.ttl / 1000) + 1;
|
|
26
|
+
return seconds > MAX_RELATIVE_EXPIRATION ? Math.ceil(entry.expires / 1000) + 1 : seconds;
|
|
27
|
+
}
|
|
28
|
+
async connect() {
|
|
29
|
+
const driver = await loadDriver('Client', 'memjs');
|
|
30
|
+
const options = this.options;
|
|
31
|
+
this.client = driver.Client.create(`${options.host}:${options.port ?? 11211}`, {
|
|
32
|
+
username: options.username,
|
|
33
|
+
password: options.password
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
async disconnect() {
|
|
37
|
+
this.client?.close();
|
|
38
|
+
}
|
|
39
|
+
async read(key) {
|
|
40
|
+
const { value } = await this.client.get(key);
|
|
41
|
+
return value === null ? undefined : value.toString();
|
|
42
|
+
}
|
|
43
|
+
async readMany(keys) {
|
|
44
|
+
return Promise.all(keys.map((key) => this.read(key)));
|
|
45
|
+
}
|
|
46
|
+
async write(entry) {
|
|
47
|
+
await this.client.set(entry.key, entry.data, { expires: this.expiration(entry) });
|
|
48
|
+
}
|
|
49
|
+
async writeMany(entries) {
|
|
50
|
+
await Promise.all(entries.map((entry) => this.write(entry)));
|
|
51
|
+
}
|
|
52
|
+
async remove(key) {
|
|
53
|
+
return await this.client.delete(key);
|
|
54
|
+
}
|
|
55
|
+
async removeMany(keys) {
|
|
56
|
+
const removed = await Promise.all(keys.map((key) => this.remove(key)));
|
|
57
|
+
return removed.some((result) => result);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Memcached cannot list the keys, all the entries are flushed
|
|
61
|
+
*/
|
|
62
|
+
async clear() {
|
|
63
|
+
await this.client.flush();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CacheStore, expiresOf, isExpired } from './CacheStore.js';
|
|
2
|
+
/**
|
|
3
|
+
* The in-memory store, the values are kept by reference
|
|
4
|
+
*/
|
|
5
|
+
export class MemoryStore extends CacheStore {
|
|
6
|
+
/**
|
|
7
|
+
* @param sweepInterval ms, remove the expired entries in each interval
|
|
8
|
+
*/
|
|
9
|
+
constructor(sweepInterval = 60000) {
|
|
10
|
+
super();
|
|
11
|
+
this.sweepInterval = sweepInterval;
|
|
12
|
+
this.entries = new Map();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Remove the expired entries
|
|
16
|
+
* @protected
|
|
17
|
+
*/
|
|
18
|
+
sweep() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const [key, entry] of this.entries)
|
|
21
|
+
if (typeof entry.expires === 'number' && now > entry.expires)
|
|
22
|
+
this.entries.delete(key);
|
|
23
|
+
}
|
|
24
|
+
async connect() {
|
|
25
|
+
this.sweeper = setInterval(() => this.sweep(), this.sweepInterval);
|
|
26
|
+
this.sweeper.unref();
|
|
27
|
+
}
|
|
28
|
+
async disconnect() {
|
|
29
|
+
clearInterval(this.sweeper);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Read an entry, the expired entries are removed
|
|
33
|
+
* @param key
|
|
34
|
+
* @protected
|
|
35
|
+
*/
|
|
36
|
+
read(key) {
|
|
37
|
+
const entry = this.entries.get(key);
|
|
38
|
+
if (entry === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
if (isExpired(entry)) {
|
|
41
|
+
this.entries.delete(key);
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
return entry;
|
|
45
|
+
}
|
|
46
|
+
async get(key) {
|
|
47
|
+
return this.read(key);
|
|
48
|
+
}
|
|
49
|
+
async getMany(keys) {
|
|
50
|
+
return keys.map((key) => this.read(key));
|
|
51
|
+
}
|
|
52
|
+
async set(key, value, ttl) {
|
|
53
|
+
this.entries.set(key, { value: value, expires: expiresOf(ttl) });
|
|
54
|
+
}
|
|
55
|
+
async setMany(entries) {
|
|
56
|
+
for (const entry of entries)
|
|
57
|
+
this.entries.set(entry.key, { value: entry.value, expires: expiresOf(entry.ttl) });
|
|
58
|
+
}
|
|
59
|
+
async delete(key) {
|
|
60
|
+
return this.entries.delete(key);
|
|
61
|
+
}
|
|
62
|
+
async deleteMany(keys) {
|
|
63
|
+
let deleted = false;
|
|
64
|
+
for (const key of keys)
|
|
65
|
+
deleted = this.entries.delete(key) || deleted;
|
|
66
|
+
return deleted;
|
|
67
|
+
}
|
|
68
|
+
async clear() {
|
|
69
|
+
this.entries.clear();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
2
|
+
import { loadDriver } from '../lib/LoadDriver.js';
|
|
3
|
+
/**
|
|
4
|
+
* Escape the regular expression characters
|
|
5
|
+
* @param text
|
|
6
|
+
*/
|
|
7
|
+
function escapeRegExp(text) {
|
|
8
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The MongoDB store
|
|
12
|
+
* Document format: {key: "{namespace}:{key}", value: serialized entry, expiresAt: Date | null}, removed by a TTL index
|
|
13
|
+
*/
|
|
14
|
+
export class MongoStore extends SerializedCacheStore {
|
|
15
|
+
constructor(options) {
|
|
16
|
+
super(options.namespace);
|
|
17
|
+
this.options = options;
|
|
18
|
+
}
|
|
19
|
+
async connect() {
|
|
20
|
+
const driver = await loadDriver('MongoClient', 'mongodb');
|
|
21
|
+
const options = this.options;
|
|
22
|
+
const auth = options.username ? `${encodeURIComponent(options.username)}:${encodeURIComponent(options.password ?? '')}@` : '';
|
|
23
|
+
this.client = new driver.MongoClient(`mongodb://${auth}${options.host}:${options.port ?? 27017}`);
|
|
24
|
+
await this.client.connect();
|
|
25
|
+
this.collection = this.client.db(options.database).collection(options.collection);
|
|
26
|
+
await this.collection.createIndex({ key: 1 }, { unique: true, background: true });
|
|
27
|
+
await this.collection.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0, background: true });
|
|
28
|
+
}
|
|
29
|
+
async disconnect() {
|
|
30
|
+
await this.client?.close();
|
|
31
|
+
}
|
|
32
|
+
async read(key) {
|
|
33
|
+
const document = await this.collection.findOne({ key: { $eq: key } }, { projection: { _id: 0, value: 1 } });
|
|
34
|
+
return document ? document.value : undefined;
|
|
35
|
+
}
|
|
36
|
+
async readMany(keys) {
|
|
37
|
+
const documents = await this.collection.find({ key: { $in: keys } }, { projection: { _id: 0, key: 1, value: 1 } }).toArray();
|
|
38
|
+
const values = new Map(documents.map((document) => [document.key, document.value]));
|
|
39
|
+
return keys.map((key) => values.get(key));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The upsert of an entry
|
|
43
|
+
* @param entry
|
|
44
|
+
* @protected
|
|
45
|
+
*/
|
|
46
|
+
update(entry) {
|
|
47
|
+
return [{ key: { $eq: entry.key } }, { $set: { key: entry.key, value: entry.data, expiresAt: entry.expires ? new Date(entry.expires) : null } }];
|
|
48
|
+
}
|
|
49
|
+
async write(entry) {
|
|
50
|
+
const [filter, update] = this.update(entry);
|
|
51
|
+
await this.collection.updateOne(filter, update, { upsert: true });
|
|
52
|
+
}
|
|
53
|
+
async writeMany(entries) {
|
|
54
|
+
await this.collection.bulkWrite(entries.map((entry) => {
|
|
55
|
+
const [filter, update] = this.update(entry);
|
|
56
|
+
return { updateOne: { filter: filter, update: update, upsert: true } };
|
|
57
|
+
}), { ordered: false });
|
|
58
|
+
}
|
|
59
|
+
async remove(key) {
|
|
60
|
+
return (await this.collection.deleteOne({ key: { $eq: key } })).deletedCount > 0;
|
|
61
|
+
}
|
|
62
|
+
async removeMany(keys) {
|
|
63
|
+
return (await this.collection.deleteMany({ key: { $in: keys } })).deletedCount > 0;
|
|
64
|
+
}
|
|
65
|
+
async clear() {
|
|
66
|
+
await this.collection.deleteMany(this.namespace ? { key: { $regex: `^${escapeRegExp(this.namespace)}:` } } : {});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
2
|
+
import { loadDriver } from '../lib/LoadDriver.js';
|
|
3
|
+
import { likePrefix } from '../lib/LikePrefix.js';
|
|
4
|
+
/**
|
|
5
|
+
* The MySQL store
|
|
6
|
+
* Table format: (id VARCHAR(255) PRIMARY KEY, value TEXT), with "{namespace}:{key}" ids
|
|
7
|
+
*/
|
|
8
|
+
export class MysqlStore extends SerializedCacheStore {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options.namespace);
|
|
11
|
+
this.options = options;
|
|
12
|
+
}
|
|
13
|
+
async connect() {
|
|
14
|
+
const driver = await loadDriver('createPool', 'mysql2');
|
|
15
|
+
const options = this.options;
|
|
16
|
+
this.pool = driver.createPool({
|
|
17
|
+
host: options.host,
|
|
18
|
+
port: options.port,
|
|
19
|
+
user: options.username,
|
|
20
|
+
password: options.password,
|
|
21
|
+
database: options.database
|
|
22
|
+
}).promise();
|
|
23
|
+
this.table = this.pool.escapeId(options.table);
|
|
24
|
+
try {
|
|
25
|
+
//The binary collation keeps the keys case sensitive, the tables of 2.x are used as they are
|
|
26
|
+
await this.pool.query(`CREATE TABLE IF NOT EXISTS ${this.table}(id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin PRIMARY KEY, value MEDIUMTEXT)`);
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
await this.pool.end().catch(() => undefined);
|
|
30
|
+
throw e;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async disconnect() {
|
|
34
|
+
await this.pool?.end();
|
|
35
|
+
}
|
|
36
|
+
async read(key) {
|
|
37
|
+
const [rows] = await this.pool.query(`SELECT value FROM ${this.table} WHERE id = ?`, [key]);
|
|
38
|
+
return rows[0]?.value;
|
|
39
|
+
}
|
|
40
|
+
async readMany(keys) {
|
|
41
|
+
const [rows] = await this.pool.query(`SELECT id, value FROM ${this.table} WHERE id IN (?)`, [keys]);
|
|
42
|
+
const values = new Map(rows.map((row) => [row.id, row.value]));
|
|
43
|
+
return keys.map((key) => values.get(key));
|
|
44
|
+
}
|
|
45
|
+
async write(entry) {
|
|
46
|
+
await this.pool.query(`INSERT INTO ${this.table} (id, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value)`, [entry.key, entry.data]);
|
|
47
|
+
}
|
|
48
|
+
async writeMany(entries) {
|
|
49
|
+
await this.pool.query(`INSERT INTO ${this.table} (id, value) VALUES ? ON DUPLICATE KEY UPDATE value = VALUES(value)`, [entries.map((entry) => [entry.key, entry.data])]);
|
|
50
|
+
}
|
|
51
|
+
async remove(key) {
|
|
52
|
+
const [result] = await this.pool.query(`DELETE FROM ${this.table} WHERE id = ?`, [key]);
|
|
53
|
+
return result.affectedRows > 0;
|
|
54
|
+
}
|
|
55
|
+
async removeMany(keys) {
|
|
56
|
+
const [result] = await this.pool.query(`DELETE FROM ${this.table} WHERE id IN (?)`, [keys]);
|
|
57
|
+
return result.affectedRows > 0;
|
|
58
|
+
}
|
|
59
|
+
async clear() {
|
|
60
|
+
if (this.namespace) {
|
|
61
|
+
await this.pool.query(`DELETE FROM ${this.table} WHERE id LIKE ? ESCAPE '!'`, [likePrefix(this.namespace)]);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await this.pool.query(`DELETE FROM ${this.table}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
2
|
+
import { loadDriver } from '../lib/LoadDriver.js';
|
|
3
|
+
import { likePrefix } from '../lib/LikePrefix.js';
|
|
4
|
+
/**
|
|
5
|
+
* An identifier of the SQL text, the plain names are kept unquoted as in 2.x (and so folded to lower case)
|
|
6
|
+
* @param name
|
|
7
|
+
*/
|
|
8
|
+
function identifier(name) {
|
|
9
|
+
return /^[A-Za-z_][A-Za-z0-9_$]*$/.test(name) ? name : `"${name.replace(/"/g, '""')}"`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The PostgreSQL store
|
|
13
|
+
* Table format: (key VARCHAR(255) PRIMARY KEY, value TEXT), with "{namespace}:{key}" keys
|
|
14
|
+
*/
|
|
15
|
+
export class PostgresStore extends SerializedCacheStore {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super(options.namespace);
|
|
18
|
+
this.options = options;
|
|
19
|
+
}
|
|
20
|
+
async connect() {
|
|
21
|
+
const driver = await loadDriver('Pool', 'pg');
|
|
22
|
+
const options = this.options;
|
|
23
|
+
this.pool = new driver.Pool({
|
|
24
|
+
host: options.host,
|
|
25
|
+
port: options.port,
|
|
26
|
+
user: options.username,
|
|
27
|
+
password: options.password,
|
|
28
|
+
database: options.database,
|
|
29
|
+
max: options.maxPoolSize
|
|
30
|
+
});
|
|
31
|
+
this.pool.on('error', (error) => this.onError(error));
|
|
32
|
+
const schema = identifier(options.schema ?? 'public');
|
|
33
|
+
this.table = `${schema}.${identifier(options.table)}`;
|
|
34
|
+
try {
|
|
35
|
+
if (schema !== 'public')
|
|
36
|
+
await this.pool.query(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
|
|
37
|
+
await this.pool.query(`CREATE TABLE IF NOT EXISTS ${this.table}(key VARCHAR(255) PRIMARY KEY, value TEXT)`);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
//A concurrent creation of the same table
|
|
41
|
+
if (e.code !== '23505') {
|
|
42
|
+
await this.pool.end().catch(() => undefined);
|
|
43
|
+
throw e;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async disconnect() {
|
|
48
|
+
await this.pool?.end();
|
|
49
|
+
}
|
|
50
|
+
async read(key) {
|
|
51
|
+
const { rows } = await this.pool.query(`SELECT value FROM ${this.table} WHERE key = $1`, [key]);
|
|
52
|
+
return rows[0]?.value;
|
|
53
|
+
}
|
|
54
|
+
async readMany(keys) {
|
|
55
|
+
const { rows } = await this.pool.query(`SELECT key, value FROM ${this.table} WHERE key = ANY($1)`, [keys]);
|
|
56
|
+
const values = new Map(rows.map((row) => [row.key, row.value]));
|
|
57
|
+
return keys.map((key) => values.get(key));
|
|
58
|
+
}
|
|
59
|
+
async write(entry) {
|
|
60
|
+
await this.pool.query(`INSERT INTO ${this.table} (key, value) VALUES ($1, $2) ON CONFLICT (key) DO UPDATE SET value = excluded.value`, [entry.key, entry.data]);
|
|
61
|
+
}
|
|
62
|
+
async writeMany(entries) {
|
|
63
|
+
//The last of the duplicate keys wins, one statement cannot update a row twice
|
|
64
|
+
const latest = new Map(entries.map((entry) => [entry.key, entry.data]));
|
|
65
|
+
await this.pool.query(`INSERT INTO ${this.table} (key, value) SELECT * FROM UNNEST($1::text[], $2::text[]) ON CONFLICT (key) DO UPDATE SET value = excluded.value`, [[...latest.keys()], [...latest.values()]]);
|
|
66
|
+
}
|
|
67
|
+
async remove(key) {
|
|
68
|
+
return !!(await this.pool.query(`DELETE FROM ${this.table} WHERE key = $1`, [key])).rowCount;
|
|
69
|
+
}
|
|
70
|
+
async removeMany(keys) {
|
|
71
|
+
return !!(await this.pool.query(`DELETE FROM ${this.table} WHERE key = ANY($1)`, [keys])).rowCount;
|
|
72
|
+
}
|
|
73
|
+
async clear() {
|
|
74
|
+
if (this.namespace) {
|
|
75
|
+
await this.pool.query(`DELETE FROM ${this.table} WHERE key LIKE $1 ESCAPE '!'`, [likePrefix(this.namespace)]);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
await this.pool.query(`DELETE FROM ${this.table}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { SerializedCacheStore } from './SerializedCacheStore.js';
|
|
2
|
+
import { loadDriver } from '../lib/LoadDriver.js';
|
|
3
|
+
import { withTimeout } from '../lib/WithTimeout.js';
|
|
4
|
+
/**
|
|
5
|
+
* Escape the glob pattern characters of a SCAN match
|
|
6
|
+
* @param text
|
|
7
|
+
*/
|
|
8
|
+
function escapeGlob(text) {
|
|
9
|
+
return text.replace(/[*?[\]\\]/g, '\\$&');
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The Redis store
|
|
13
|
+
* Key format: "{namespace}{keyPrefixSeparator}{namespace}:{key}", or "{key}" without namespace
|
|
14
|
+
*/
|
|
15
|
+
export class RedisStore extends SerializedCacheStore {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
super(options.namespace);
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.separator = options.keyPrefixSeparator ?? '::';
|
|
20
|
+
this.clearBatchSize = options.clearBatchSize || 1000;
|
|
21
|
+
}
|
|
22
|
+
storeKey(key) {
|
|
23
|
+
return this.namespace ? `${this.namespace}${this.separator}${this.namespace}:${key}` : key;
|
|
24
|
+
}
|
|
25
|
+
async connect() {
|
|
26
|
+
const driver = await loadDriver('createClient', 'redis', '@redis/client');
|
|
27
|
+
const options = this.options;
|
|
28
|
+
const throwOnConnectError = options.throwOnConnectError !== false;
|
|
29
|
+
let connected = false;
|
|
30
|
+
this.client = driver.createClient({
|
|
31
|
+
username: options.username,
|
|
32
|
+
password: options.password,
|
|
33
|
+
database: options.database,
|
|
34
|
+
//Fail the commands while disconnected, a cache miss is better than a wait
|
|
35
|
+
disableOfflineQueue: true,
|
|
36
|
+
socket: {
|
|
37
|
+
host: options.host,
|
|
38
|
+
port: options.port,
|
|
39
|
+
tls: options.tls || undefined,
|
|
40
|
+
keepAlive: options.keepAlive,
|
|
41
|
+
keepAliveInitialDelay: options.keepAlive,
|
|
42
|
+
connectTimeout: options.connectTimeout,
|
|
43
|
+
reconnectStrategy: (retries, cause) => {
|
|
44
|
+
//The first connection fails at once when its errors are thrown
|
|
45
|
+
if (!connected && throwOnConnectError)
|
|
46
|
+
return cause;
|
|
47
|
+
return options.reconnect === false ? false : Math.min(retries * 100, 3000);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
this.client.on('error', (error) => this.onError(error));
|
|
52
|
+
this.client.on('ready', () => {
|
|
53
|
+
connected = true;
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
await withTimeout(this.client.connect(), options.connectTimeout ?? 5000, `Redis connection timed out after ${options.connectTimeout ?? 5000}ms`);
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (!throwOnConnectError)
|
|
60
|
+
return this.onError(e);
|
|
61
|
+
await this.disconnect();
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async disconnect() {
|
|
66
|
+
const client = this.client;
|
|
67
|
+
if (!client?.isOpen)
|
|
68
|
+
return;
|
|
69
|
+
//An offline client cannot quit gracefully
|
|
70
|
+
if (!client.isReady)
|
|
71
|
+
return client.destroy ? client.destroy() : void await client.disconnect().catch(() => undefined);
|
|
72
|
+
await (client.quit ? client.quit() : client.close());
|
|
73
|
+
}
|
|
74
|
+
async read(key) {
|
|
75
|
+
return (await this.client.get(key)) ?? undefined;
|
|
76
|
+
}
|
|
77
|
+
async readMany(keys) {
|
|
78
|
+
return (await this.client.mGet(keys)).map((data) => data ?? undefined);
|
|
79
|
+
}
|
|
80
|
+
async write(entry) {
|
|
81
|
+
entry.ttl ? await this.client.pSetEx(entry.key, Math.ceil(entry.ttl), entry.data) : await this.client.set(entry.key, entry.data);
|
|
82
|
+
}
|
|
83
|
+
async writeMany(entries) {
|
|
84
|
+
const multi = this.client.multi();
|
|
85
|
+
for (const entry of entries)
|
|
86
|
+
entry.ttl ? multi.pSetEx(entry.key, Math.ceil(entry.ttl), entry.data) : multi.set(entry.key, entry.data);
|
|
87
|
+
await multi.exec();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Delete keys
|
|
91
|
+
* @param keys
|
|
92
|
+
* @protected
|
|
93
|
+
*/
|
|
94
|
+
async unlink(keys) {
|
|
95
|
+
return this.options.useUnlink === false ? await this.client.del(keys) : await this.client.unlink(keys);
|
|
96
|
+
}
|
|
97
|
+
async remove(key) {
|
|
98
|
+
return (await this.unlink(key)) > 0;
|
|
99
|
+
}
|
|
100
|
+
async removeMany(keys) {
|
|
101
|
+
return (await this.unlink(keys)) > 0;
|
|
102
|
+
}
|
|
103
|
+
async clear() {
|
|
104
|
+
if (!this.namespace && this.options.noNamespaceAffectsAll) {
|
|
105
|
+
await this.client.flushDb();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const match = this.namespace ? `${escapeGlob(this.namespace + this.separator)}*` : '*';
|
|
109
|
+
let batch = [];
|
|
110
|
+
const deletions = [];
|
|
111
|
+
for await (const scanned of this.client.scanIterator({ MATCH: match, COUNT: this.clearBatchSize, TYPE: 'string' })) {
|
|
112
|
+
for (const key of Array.isArray(scanned) ? scanned : [scanned]) {
|
|
113
|
+
//Without namespace, the namespaced keys are kept
|
|
114
|
+
if (!this.namespace && key.includes(this.separator))
|
|
115
|
+
continue;
|
|
116
|
+
batch.push(key);
|
|
117
|
+
if (batch.length >= this.clearBatchSize) {
|
|
118
|
+
deletions.push(this.unlink(batch));
|
|
119
|
+
batch = [];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (batch.length)
|
|
124
|
+
deletions.push(this.unlink(batch));
|
|
125
|
+
await Promise.all(deletions);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { CacheStore, expiresOf, isExpired } from './CacheStore.js';
|
|
2
|
+
import { deserialize, serialize } from '../lib/Serializer.js';
|
|
3
|
+
/**
|
|
4
|
+
* A store keeping the entries serialized, under namespaced keys
|
|
5
|
+
*/
|
|
6
|
+
export class SerializedCacheStore extends CacheStore {
|
|
7
|
+
constructor(namespace) {
|
|
8
|
+
super();
|
|
9
|
+
this.namespace = namespace || undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The key of an entry in the backend
|
|
13
|
+
* @param key
|
|
14
|
+
* @protected
|
|
15
|
+
*/
|
|
16
|
+
storeKey(key) {
|
|
17
|
+
return this.namespace ? `${this.namespace}:${key}` : key;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Parse an entry read from the backend, the expired entries are removed
|
|
21
|
+
* @param storeKey
|
|
22
|
+
* @param data
|
|
23
|
+
* @protected
|
|
24
|
+
*/
|
|
25
|
+
parse(storeKey, data) {
|
|
26
|
+
if (data === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const entry = deserialize(data);
|
|
29
|
+
if (entry === null || typeof entry !== 'object')
|
|
30
|
+
return undefined;
|
|
31
|
+
if (isExpired(entry)) {
|
|
32
|
+
this.remove(storeKey).catch(() => undefined);
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return entry;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Serialize an entry
|
|
39
|
+
* @param key
|
|
40
|
+
* @param value
|
|
41
|
+
* @param ttl
|
|
42
|
+
* @protected
|
|
43
|
+
*/
|
|
44
|
+
serialize(key, value, ttl) {
|
|
45
|
+
ttl = ttl || undefined;
|
|
46
|
+
const expires = expiresOf(ttl);
|
|
47
|
+
return { key: this.storeKey(key), data: serialize({ value: value, expires: expires }), ttl: ttl, expires: expires };
|
|
48
|
+
}
|
|
49
|
+
async get(key) {
|
|
50
|
+
const storeKey = this.storeKey(key);
|
|
51
|
+
return this.parse(storeKey, await this.read(storeKey));
|
|
52
|
+
}
|
|
53
|
+
async getMany(keys) {
|
|
54
|
+
if (!keys.length)
|
|
55
|
+
return [];
|
|
56
|
+
const storeKeys = keys.map((key) => this.storeKey(key));
|
|
57
|
+
const data = await this.readMany(storeKeys);
|
|
58
|
+
return storeKeys.map((storeKey, index) => this.parse(storeKey, data[index]));
|
|
59
|
+
}
|
|
60
|
+
async set(key, value, ttl) {
|
|
61
|
+
await this.write(this.serialize(key, value, ttl));
|
|
62
|
+
}
|
|
63
|
+
async setMany(entries) {
|
|
64
|
+
if (!entries.length)
|
|
65
|
+
return;
|
|
66
|
+
await this.writeMany(entries.map((entry) => this.serialize(entry.key, entry.value, entry.ttl)));
|
|
67
|
+
}
|
|
68
|
+
async delete(key) {
|
|
69
|
+
return await this.remove(this.storeKey(key));
|
|
70
|
+
}
|
|
71
|
+
async deleteMany(keys) {
|
|
72
|
+
if (!keys.length)
|
|
73
|
+
return false;
|
|
74
|
+
return await this.removeMany(keys.map((key) => this.storeKey(key)));
|
|
75
|
+
}
|
|
76
|
+
}
|