@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,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
+ }