@arkstack/cache 0.5.3
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 +21 -0
- package/README.md +71 -0
- package/dist/CacheManager-Do82q7KO.js +709 -0
- package/dist/commands/CacheClearCommand.d.ts +13 -0
- package/dist/commands/CacheClearCommand.js +19 -0
- package/dist/index.d.ts +526 -0
- package/dist/index.js +2 -0
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +21 -0
- package/package.json +57 -0
- package/stubs/migrations/20260601000000_create_cache_table.ts.stub +21 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import { mkdir, readFile, readdir, rm, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { config } from "@arkstack/common";
|
|
6
|
+
//#region src/Contracts/Store.ts
|
|
7
|
+
/**
|
|
8
|
+
* The low level contract every cache store driver must satisfy.
|
|
9
|
+
*
|
|
10
|
+
* Stores are intentionally thin: they only deal in raw key/value persistence
|
|
11
|
+
* and expiration. The higher level convenience API (`remember`, `add`, `pull`,
|
|
12
|
+
* default value resolution, etc.) lives in {@link Repository}, which wraps a
|
|
13
|
+
* store. This keeps drivers simple and behaviour consistent across backends.
|
|
14
|
+
*/
|
|
15
|
+
var Store = class {};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/drivers/DatabaseStore.ts
|
|
18
|
+
/**
|
|
19
|
+
* A cache store that persists entries in a relational table via
|
|
20
|
+
* `@arkstack/database`.
|
|
21
|
+
*
|
|
22
|
+
* The table is expected to have `key` (string, primary), `value` (text), and
|
|
23
|
+
* `expiration` (nullable integer epoch seconds) columns. Run
|
|
24
|
+
* `ark publish --tag cache-migrations` to add the migration that creates it.
|
|
25
|
+
* `@arkstack/database` is an optional peer dependency and is imported lazily.
|
|
26
|
+
*/
|
|
27
|
+
var DatabaseStore = class extends Store {
|
|
28
|
+
databaseConfig;
|
|
29
|
+
prefix;
|
|
30
|
+
db;
|
|
31
|
+
constructor(databaseConfig, prefix = "") {
|
|
32
|
+
super();
|
|
33
|
+
this.databaseConfig = databaseConfig;
|
|
34
|
+
this.prefix = prefix;
|
|
35
|
+
}
|
|
36
|
+
getPrefix() {
|
|
37
|
+
return this.prefix;
|
|
38
|
+
}
|
|
39
|
+
prefixed(key) {
|
|
40
|
+
return this.prefix + key;
|
|
41
|
+
}
|
|
42
|
+
get table() {
|
|
43
|
+
return this.databaseConfig.table;
|
|
44
|
+
}
|
|
45
|
+
async database() {
|
|
46
|
+
if (this.db) return this.db;
|
|
47
|
+
try {
|
|
48
|
+
const { DB } = await import("@arkstack/database");
|
|
49
|
+
this.db = DB;
|
|
50
|
+
} catch {
|
|
51
|
+
throw new Error("The database cache store requires the \"@arkstack/database\" package.");
|
|
52
|
+
}
|
|
53
|
+
return this.db;
|
|
54
|
+
}
|
|
55
|
+
async get(key) {
|
|
56
|
+
const db = await this.database();
|
|
57
|
+
const row = await db.table(this.table).where({ key: this.prefixed(key) }).first();
|
|
58
|
+
if (!row) return null;
|
|
59
|
+
if (row.expiration !== null && row.expiration <= Math.floor(Date.now() / 1e3)) {
|
|
60
|
+
await db.table(this.table).where({ key: this.prefixed(key) }).delete();
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
return JSON.parse(row.value);
|
|
65
|
+
} catch {
|
|
66
|
+
return row.value;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async put(key, value, seconds = null) {
|
|
70
|
+
const db = await this.database();
|
|
71
|
+
const expiration = seconds === null ? null : Math.floor(Date.now() / 1e3) + seconds;
|
|
72
|
+
await db.table(this.table).updateOrInsert({ key: this.prefixed(key) }, {
|
|
73
|
+
value: JSON.stringify(value),
|
|
74
|
+
expiration
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
async forever(key, value) {
|
|
79
|
+
return this.put(key, value, null);
|
|
80
|
+
}
|
|
81
|
+
async increment(key, value = 1) {
|
|
82
|
+
const current = await this.get(key);
|
|
83
|
+
const base = current === null ? 0 : current;
|
|
84
|
+
if (typeof base !== "number" || Number.isNaN(base)) return false;
|
|
85
|
+
const next = base + value;
|
|
86
|
+
const db = await this.database();
|
|
87
|
+
const existing = await db.table(this.table).where({ key: this.prefixed(key) }).first();
|
|
88
|
+
await db.table(this.table).updateOrInsert({ key: this.prefixed(key) }, {
|
|
89
|
+
value: JSON.stringify(next),
|
|
90
|
+
expiration: existing?.expiration ?? null
|
|
91
|
+
});
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
async decrement(key, value = 1) {
|
|
95
|
+
return this.increment(key, -value);
|
|
96
|
+
}
|
|
97
|
+
async forget(key) {
|
|
98
|
+
await (await this.database()).table(this.table).where({ key: this.prefixed(key) }).delete();
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
async flush() {
|
|
102
|
+
await (await this.database()).table(this.table).delete();
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region src/drivers/FileStore.ts
|
|
108
|
+
/**
|
|
109
|
+
* A cache store that persists each entry as a JSON file on disk.
|
|
110
|
+
*
|
|
111
|
+
* Keys are hashed to produce safe, fixed length file names. Expired files are
|
|
112
|
+
* removed lazily on read and ignored otherwise.
|
|
113
|
+
*/
|
|
114
|
+
var FileStore = class extends Store {
|
|
115
|
+
directory;
|
|
116
|
+
prefix;
|
|
117
|
+
constructor(directory, prefix = "") {
|
|
118
|
+
super();
|
|
119
|
+
this.directory = directory;
|
|
120
|
+
this.prefix = prefix;
|
|
121
|
+
}
|
|
122
|
+
getPrefix() {
|
|
123
|
+
return this.prefix;
|
|
124
|
+
}
|
|
125
|
+
pathFor(key) {
|
|
126
|
+
const hash = createHash("sha1").update(this.prefix + key).digest("hex");
|
|
127
|
+
return path.join(this.directory, `${hash}.json`);
|
|
128
|
+
}
|
|
129
|
+
async ensureDirectory() {
|
|
130
|
+
if (!existsSync(this.directory)) await mkdir(this.directory, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
async get(key) {
|
|
133
|
+
const file = this.pathFor(key);
|
|
134
|
+
if (!existsSync(file)) return null;
|
|
135
|
+
try {
|
|
136
|
+
const payload = JSON.parse(await readFile(file, "utf8"));
|
|
137
|
+
if (payload.expiresAt !== null && payload.expiresAt <= Date.now()) {
|
|
138
|
+
await unlink(file).catch(() => void 0);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
return payload.value;
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async put(key, value, seconds = null) {
|
|
147
|
+
await this.ensureDirectory();
|
|
148
|
+
const payload = {
|
|
149
|
+
value,
|
|
150
|
+
expiresAt: seconds === null ? null : Date.now() + seconds * 1e3
|
|
151
|
+
};
|
|
152
|
+
await writeFile(this.pathFor(key), JSON.stringify(payload), "utf8");
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
async forever(key, value) {
|
|
156
|
+
return this.put(key, value, null);
|
|
157
|
+
}
|
|
158
|
+
async increment(key, value = 1) {
|
|
159
|
+
const current = await this.get(key);
|
|
160
|
+
const base = current === null ? 0 : current;
|
|
161
|
+
if (typeof base !== "number" || Number.isNaN(base)) return false;
|
|
162
|
+
const next = base + value;
|
|
163
|
+
const file = this.pathFor(key);
|
|
164
|
+
let expiresAt = null;
|
|
165
|
+
if (existsSync(file)) try {
|
|
166
|
+
expiresAt = JSON.parse(await readFile(file, "utf8")).expiresAt;
|
|
167
|
+
} catch {
|
|
168
|
+
expiresAt = null;
|
|
169
|
+
}
|
|
170
|
+
await this.ensureDirectory();
|
|
171
|
+
await writeFile(file, JSON.stringify({
|
|
172
|
+
value: next,
|
|
173
|
+
expiresAt
|
|
174
|
+
}), "utf8");
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
async decrement(key, value = 1) {
|
|
178
|
+
return this.increment(key, -value);
|
|
179
|
+
}
|
|
180
|
+
async forget(key) {
|
|
181
|
+
const file = this.pathFor(key);
|
|
182
|
+
if (!existsSync(file)) return false;
|
|
183
|
+
await unlink(file).catch(() => void 0);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
async flush() {
|
|
187
|
+
if (!existsSync(this.directory)) return true;
|
|
188
|
+
const files = await readdir(this.directory);
|
|
189
|
+
await Promise.all(files.filter((name) => name.endsWith(".json")).map((name) => rm(path.join(this.directory, name), { force: true })));
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/drivers/MemoryStore.ts
|
|
195
|
+
/**
|
|
196
|
+
* An in-process cache store backed by a `Map`.
|
|
197
|
+
*
|
|
198
|
+
* The backing map is shared across every `MemoryStore` instance that uses the
|
|
199
|
+
* same prefix, so resolving the store twice within a process sees the same data
|
|
200
|
+
* (matching how the other backends behave). Great for development and tests; it
|
|
201
|
+
* is not shared across processes.
|
|
202
|
+
*/
|
|
203
|
+
var MemoryStore = class MemoryStore extends Store {
|
|
204
|
+
prefix;
|
|
205
|
+
static stores = /* @__PURE__ */ new Map();
|
|
206
|
+
entries;
|
|
207
|
+
constructor(prefix = "") {
|
|
208
|
+
super();
|
|
209
|
+
this.prefix = prefix;
|
|
210
|
+
if (!MemoryStore.stores.has(prefix)) MemoryStore.stores.set(prefix, /* @__PURE__ */ new Map());
|
|
211
|
+
this.entries = MemoryStore.stores.get(prefix);
|
|
212
|
+
}
|
|
213
|
+
getPrefix() {
|
|
214
|
+
return this.prefix;
|
|
215
|
+
}
|
|
216
|
+
async get(key) {
|
|
217
|
+
const entry = this.entries.get(key);
|
|
218
|
+
if (!entry) return null;
|
|
219
|
+
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
|
|
220
|
+
this.entries.delete(key);
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return entry.value;
|
|
224
|
+
}
|
|
225
|
+
async put(key, value, seconds = null) {
|
|
226
|
+
this.entries.set(key, {
|
|
227
|
+
value,
|
|
228
|
+
expiresAt: seconds === null ? null : Date.now() + seconds * 1e3
|
|
229
|
+
});
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
async forever(key, value) {
|
|
233
|
+
return this.put(key, value, null);
|
|
234
|
+
}
|
|
235
|
+
async increment(key, value = 1) {
|
|
236
|
+
const current = await this.get(key);
|
|
237
|
+
const base = current === null ? 0 : current;
|
|
238
|
+
if (typeof base !== "number" || Number.isNaN(base)) return false;
|
|
239
|
+
const next = base + value;
|
|
240
|
+
const entry = this.entries.get(key);
|
|
241
|
+
this.entries.set(key, {
|
|
242
|
+
value: next,
|
|
243
|
+
expiresAt: entry?.expiresAt ?? null
|
|
244
|
+
});
|
|
245
|
+
return next;
|
|
246
|
+
}
|
|
247
|
+
async decrement(key, value = 1) {
|
|
248
|
+
return this.increment(key, -value);
|
|
249
|
+
}
|
|
250
|
+
async forget(key) {
|
|
251
|
+
return this.entries.delete(key);
|
|
252
|
+
}
|
|
253
|
+
async flush() {
|
|
254
|
+
this.entries.clear();
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/drivers/RedisStore.ts
|
|
260
|
+
/**
|
|
261
|
+
* A Redis backed cache store using an ioredis compatible client.
|
|
262
|
+
*
|
|
263
|
+
* Values are JSON serialized. Numeric helpers use native `INCRBY`/`DECRBY` so
|
|
264
|
+
* counters stay atomic across processes. `ioredis` is an optional peer
|
|
265
|
+
* dependency; it is imported lazily so applications that never use the redis
|
|
266
|
+
* store don't need it installed.
|
|
267
|
+
*/
|
|
268
|
+
var RedisStore = class extends Store {
|
|
269
|
+
redisConfig;
|
|
270
|
+
prefix;
|
|
271
|
+
client;
|
|
272
|
+
constructor(redisConfig, prefix = "") {
|
|
273
|
+
super();
|
|
274
|
+
this.redisConfig = redisConfig;
|
|
275
|
+
this.prefix = prefix;
|
|
276
|
+
}
|
|
277
|
+
getPrefix() {
|
|
278
|
+
return [this.prefix, this.redisConfig.prefix].filter(Boolean).join("");
|
|
279
|
+
}
|
|
280
|
+
prefixed(key) {
|
|
281
|
+
return this.getPrefix() + key;
|
|
282
|
+
}
|
|
283
|
+
async connection() {
|
|
284
|
+
if (this.client) return this.client;
|
|
285
|
+
let Redis;
|
|
286
|
+
try {
|
|
287
|
+
Redis = (await import("ioredis")).default;
|
|
288
|
+
} catch {
|
|
289
|
+
throw new Error("The redis cache store requires the \"ioredis\" package. Install it with your package manager.");
|
|
290
|
+
}
|
|
291
|
+
this.client = this.redisConfig.url ? new Redis(this.redisConfig.url) : new Redis({
|
|
292
|
+
host: this.redisConfig.host ?? "127.0.0.1",
|
|
293
|
+
port: this.redisConfig.port ?? 6379,
|
|
294
|
+
password: this.redisConfig.password,
|
|
295
|
+
db: this.redisConfig.db ?? 0
|
|
296
|
+
});
|
|
297
|
+
return this.client;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Disconnect the underlying client. Useful for tests and graceful shutdown.
|
|
301
|
+
*/
|
|
302
|
+
async disconnect() {
|
|
303
|
+
await this.client?.quit();
|
|
304
|
+
this.client = void 0;
|
|
305
|
+
}
|
|
306
|
+
async get(key) {
|
|
307
|
+
const raw = await (await this.connection()).get(this.prefixed(key));
|
|
308
|
+
if (raw === null) return null;
|
|
309
|
+
try {
|
|
310
|
+
return JSON.parse(raw);
|
|
311
|
+
} catch {
|
|
312
|
+
return raw;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async put(key, value, seconds = null) {
|
|
316
|
+
const client = await this.connection();
|
|
317
|
+
const serialized = JSON.stringify(value);
|
|
318
|
+
if (seconds === null) await client.set(this.prefixed(key), serialized);
|
|
319
|
+
else await client.set(this.prefixed(key), serialized, "EX", seconds);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
async forever(key, value) {
|
|
323
|
+
return this.put(key, value, null);
|
|
324
|
+
}
|
|
325
|
+
async increment(key, value = 1) {
|
|
326
|
+
return (await this.connection()).incrby(this.prefixed(key), value);
|
|
327
|
+
}
|
|
328
|
+
async decrement(key, value = 1) {
|
|
329
|
+
return (await this.connection()).decrby(this.prefixed(key), value);
|
|
330
|
+
}
|
|
331
|
+
async forget(key) {
|
|
332
|
+
return await (await this.connection()).del(this.prefixed(key)) > 0;
|
|
333
|
+
}
|
|
334
|
+
async flush() {
|
|
335
|
+
const client = await this.connection();
|
|
336
|
+
const pattern = `${this.getPrefix()}*`;
|
|
337
|
+
let cursor = "0";
|
|
338
|
+
do {
|
|
339
|
+
const [next, keys] = await client.scan(cursor, "MATCH", pattern, "COUNT", 100);
|
|
340
|
+
cursor = next;
|
|
341
|
+
if (keys.length > 0) await client.del(...keys);
|
|
342
|
+
} while (cursor !== "0");
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/Repository.ts
|
|
348
|
+
/**
|
|
349
|
+
* Resolve a possibly callable default/value into a concrete value.
|
|
350
|
+
*/
|
|
351
|
+
const resolve = async (value) => {
|
|
352
|
+
return typeof value === "function" ? await value() : value;
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Normalize a {@link CacheTtl} into a whole number of seconds, or `null` for a
|
|
356
|
+
* forever entry. A non positive duration is treated as already expired and
|
|
357
|
+
* returns `0`, signalling the caller to skip writing.
|
|
358
|
+
*/
|
|
359
|
+
const ttlToSeconds = (ttl) => {
|
|
360
|
+
if (ttl === null || ttl === void 0) return null;
|
|
361
|
+
if (ttl instanceof Date) return Math.max(0, Math.round((ttl.getTime() - Date.now()) / 1e3));
|
|
362
|
+
return Math.max(0, Math.round(ttl));
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* A high level wrapper around a {@link Store} that provides the developer facing
|
|
366
|
+
* cache API. A repository is what `Cache.store()` returns.
|
|
367
|
+
*/
|
|
368
|
+
var Repository = class {
|
|
369
|
+
store;
|
|
370
|
+
constructor(store) {
|
|
371
|
+
this.store = store;
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Get the underlying store driver.
|
|
375
|
+
*/
|
|
376
|
+
getStore() {
|
|
377
|
+
return this.store;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Determine whether an item exists in the cache and has not expired.
|
|
381
|
+
*
|
|
382
|
+
* @param key
|
|
383
|
+
* @returns
|
|
384
|
+
*/
|
|
385
|
+
async has(key) {
|
|
386
|
+
return await this.store.get(key) !== null;
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Determine whether an item is missing from the cache.
|
|
390
|
+
*
|
|
391
|
+
* @param key
|
|
392
|
+
* @returns
|
|
393
|
+
*/
|
|
394
|
+
async missing(key) {
|
|
395
|
+
return !await this.has(key);
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Retrieve an item from the cache, falling back to `defaultValue` (which may
|
|
399
|
+
* be a callback) when the item is absent.
|
|
400
|
+
*
|
|
401
|
+
* @param key
|
|
402
|
+
* @returns
|
|
403
|
+
*/
|
|
404
|
+
async get(key, defaultValue = null) {
|
|
405
|
+
const value = await this.store.get(key);
|
|
406
|
+
if (value === null || value === void 0) return resolve(defaultValue);
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Retrieve an item and delete it from the cache in a single call.
|
|
411
|
+
*
|
|
412
|
+
* @param key
|
|
413
|
+
* @param defaultValue
|
|
414
|
+
* @returns
|
|
415
|
+
*/
|
|
416
|
+
async pull(key, defaultValue = null) {
|
|
417
|
+
const value = await this.get(key, defaultValue);
|
|
418
|
+
await this.forget(key);
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* Store an item in the cache. A `null`/omitted ttl stores the item forever;
|
|
423
|
+
* a non positive ttl is a no-op that also forgets any existing entry.
|
|
424
|
+
*
|
|
425
|
+
* @param key
|
|
426
|
+
* @param defaultValue
|
|
427
|
+
* @returns
|
|
428
|
+
*/
|
|
429
|
+
async put(key, value, ttl = null) {
|
|
430
|
+
const seconds = ttlToSeconds(ttl);
|
|
431
|
+
if (seconds !== null && seconds <= 0) {
|
|
432
|
+
await this.forget(key);
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
return this.store.put(key, value, seconds);
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Alias for {@link put}.
|
|
439
|
+
*
|
|
440
|
+
* @param key
|
|
441
|
+
* @param defaultValue
|
|
442
|
+
* @returns
|
|
443
|
+
*/
|
|
444
|
+
async set(key, value, ttl = null) {
|
|
445
|
+
return this.put(key, value, ttl);
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Store an item only if it is not already present. Returns `false` when the
|
|
449
|
+
* key already exists.
|
|
450
|
+
*
|
|
451
|
+
* @param key
|
|
452
|
+
* @param defaultValue
|
|
453
|
+
* @returns
|
|
454
|
+
*/
|
|
455
|
+
async add(key, value, ttl = null) {
|
|
456
|
+
if (await this.has(key)) return false;
|
|
457
|
+
return this.put(key, value, ttl);
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Store an item that never expires.
|
|
461
|
+
*
|
|
462
|
+
* @param key
|
|
463
|
+
* @param defaultValue
|
|
464
|
+
* @returns
|
|
465
|
+
*/
|
|
466
|
+
async forever(key, value) {
|
|
467
|
+
return this.store.forever(key, value);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Increment a stored numeric value.
|
|
471
|
+
*
|
|
472
|
+
* @param key
|
|
473
|
+
* @param defaultValue
|
|
474
|
+
* @returns
|
|
475
|
+
*/
|
|
476
|
+
async increment(key, value = 1) {
|
|
477
|
+
return this.store.increment(key, value);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Decrement a stored numeric value.
|
|
481
|
+
*
|
|
482
|
+
* @param key
|
|
483
|
+
* @param defaultValue
|
|
484
|
+
* @returns
|
|
485
|
+
*/
|
|
486
|
+
async decrement(key, value = 1) {
|
|
487
|
+
return this.store.decrement(key, value);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Get an item from the cache, or execute the callback, store its result for
|
|
491
|
+
* the given ttl, and return it.
|
|
492
|
+
*
|
|
493
|
+
* @param key
|
|
494
|
+
* @param defaultValue
|
|
495
|
+
* @returns
|
|
496
|
+
*/
|
|
497
|
+
async remember(key, ttl, callback) {
|
|
498
|
+
const existing = await this.store.get(key);
|
|
499
|
+
if (existing !== null && existing !== void 0) return existing;
|
|
500
|
+
const value = await callback();
|
|
501
|
+
await this.put(key, value, ttl);
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get an item from the cache, or execute the callback and store its result
|
|
506
|
+
* forever.
|
|
507
|
+
*
|
|
508
|
+
* @param key
|
|
509
|
+
* @param defaultValue
|
|
510
|
+
* @returns
|
|
511
|
+
*/
|
|
512
|
+
async rememberForever(key, callback) {
|
|
513
|
+
const existing = await this.store.get(key);
|
|
514
|
+
if (existing !== null && existing !== void 0) return existing;
|
|
515
|
+
const value = await callback();
|
|
516
|
+
await this.forever(key, value);
|
|
517
|
+
return value;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Alias for {@link rememberForever}.
|
|
521
|
+
*
|
|
522
|
+
* @param key
|
|
523
|
+
* @param callback
|
|
524
|
+
* @returns
|
|
525
|
+
*/
|
|
526
|
+
async sear(key, callback) {
|
|
527
|
+
return this.rememberForever(key, callback);
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Remove an item from the cache.
|
|
531
|
+
*
|
|
532
|
+
* @param key
|
|
533
|
+
* @param callback
|
|
534
|
+
* @returns
|
|
535
|
+
*/
|
|
536
|
+
async forget(key) {
|
|
537
|
+
return this.store.forget(key);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Alias for {@link forget}.
|
|
541
|
+
*
|
|
542
|
+
* @param key
|
|
543
|
+
* @param callback
|
|
544
|
+
* @returns
|
|
545
|
+
*/
|
|
546
|
+
async delete(key) {
|
|
547
|
+
return this.forget(key);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Remove every item from the cache store.
|
|
551
|
+
*
|
|
552
|
+
* @param key
|
|
553
|
+
* @param callback
|
|
554
|
+
* @returns
|
|
555
|
+
*/
|
|
556
|
+
async flush() {
|
|
557
|
+
return this.store.flush();
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Alias for {@link flush}.
|
|
561
|
+
*
|
|
562
|
+
* @param key
|
|
563
|
+
* @param callback
|
|
564
|
+
* @returns
|
|
565
|
+
*/
|
|
566
|
+
async clear() {
|
|
567
|
+
return this.flush();
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/config.ts
|
|
572
|
+
/**
|
|
573
|
+
* Read a value from the `cache` configuration namespace with a fallback.
|
|
574
|
+
*
|
|
575
|
+
* Mirrors the framework `config()` helper but is scoped to the cache config and
|
|
576
|
+
* never throws when the config file is missing, returning the default instead.
|
|
577
|
+
*
|
|
578
|
+
* @param key Dot path within the cache config.
|
|
579
|
+
* @param defaultValue Value returned when the key is not set.
|
|
580
|
+
*/
|
|
581
|
+
const configure = (key, defaultValue) => {
|
|
582
|
+
try {
|
|
583
|
+
return config(`cache.${key}`, defaultValue);
|
|
584
|
+
} catch {
|
|
585
|
+
return defaultValue;
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
//#endregion
|
|
589
|
+
//#region src/CacheManager.ts
|
|
590
|
+
/**
|
|
591
|
+
* The cache manager and primary entry point of `@arkstack/cache`.
|
|
592
|
+
*
|
|
593
|
+
* Resolves named cache stores from configuration, memoizes the resulting
|
|
594
|
+
* repositories, and exposes static convenience methods that proxy the default
|
|
595
|
+
* store, e.g.:
|
|
596
|
+
*
|
|
597
|
+
* ```ts
|
|
598
|
+
* await Cache.put('user:1', user, 60)
|
|
599
|
+
* await Cache.store('redis').remember('stats', 300, computeStats)
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
var Cache = class {
|
|
603
|
+
static repositories = {};
|
|
604
|
+
static customDrivers = {};
|
|
605
|
+
/**
|
|
606
|
+
* Resolve a cache repository for the given store name (or the default store
|
|
607
|
+
* when omitted). Repositories are memoized per name.
|
|
608
|
+
*/
|
|
609
|
+
static store(name) {
|
|
610
|
+
const store = name ?? configure("default", "memory");
|
|
611
|
+
if (!this.repositories[store]) this.repositories[store] = this.resolve(store);
|
|
612
|
+
return this.repositories[store];
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* Alias for {@link store}.
|
|
616
|
+
*/
|
|
617
|
+
static driver(name) {
|
|
618
|
+
return this.store(name);
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Register a custom store driver factory, used when a store's `driver` does
|
|
622
|
+
* not match a built in one.
|
|
623
|
+
*/
|
|
624
|
+
static extend(driver, factory) {
|
|
625
|
+
this.customDrivers[driver] = factory;
|
|
626
|
+
return this;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Clear the memoized repositories. Mainly useful between tests or after the
|
|
630
|
+
* configuration changes at runtime.
|
|
631
|
+
*/
|
|
632
|
+
static clearResolved() {
|
|
633
|
+
this.repositories = {};
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Build a repository for a configured store from scratch.
|
|
637
|
+
*/
|
|
638
|
+
static resolve(name) {
|
|
639
|
+
const config = configure(`stores.${name}`, void 0);
|
|
640
|
+
if (!config) throw new Error(`Cache store "${name}" is not configured.`);
|
|
641
|
+
return new Repository(this.createStore(config));
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Instantiate the concrete {@link Store} for a store config.
|
|
645
|
+
*/
|
|
646
|
+
static createStore(config) {
|
|
647
|
+
const prefix = configure("prefix", "");
|
|
648
|
+
switch (config.driver) {
|
|
649
|
+
case "memory":
|
|
650
|
+
case "array": return new MemoryStore(prefix);
|
|
651
|
+
case "file": return new FileStore(config.path, prefix);
|
|
652
|
+
case "redis": return new RedisStore(config, prefix);
|
|
653
|
+
case "database": return new DatabaseStore(config, prefix);
|
|
654
|
+
default:
|
|
655
|
+
if (this.customDrivers[config.driver]) return this.customDrivers[config.driver](config);
|
|
656
|
+
throw new Error(`Unsupported cache driver: ${config.driver}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
static has(key) {
|
|
660
|
+
return this.store().has(key);
|
|
661
|
+
}
|
|
662
|
+
static missing(key) {
|
|
663
|
+
return this.store().missing(key);
|
|
664
|
+
}
|
|
665
|
+
static get(key, defaultValue) {
|
|
666
|
+
return this.store().get(key, defaultValue);
|
|
667
|
+
}
|
|
668
|
+
static pull(key, defaultValue) {
|
|
669
|
+
return this.store().pull(key, defaultValue);
|
|
670
|
+
}
|
|
671
|
+
static put(key, value, ttl) {
|
|
672
|
+
return this.store().put(key, value, ttl);
|
|
673
|
+
}
|
|
674
|
+
static set(key, value, ttl) {
|
|
675
|
+
return this.store().set(key, value, ttl);
|
|
676
|
+
}
|
|
677
|
+
static add(key, value, ttl) {
|
|
678
|
+
return this.store().add(key, value, ttl);
|
|
679
|
+
}
|
|
680
|
+
static forever(key, value) {
|
|
681
|
+
return this.store().forever(key, value);
|
|
682
|
+
}
|
|
683
|
+
static increment(key, value) {
|
|
684
|
+
return this.store().increment(key, value);
|
|
685
|
+
}
|
|
686
|
+
static decrement(key, value) {
|
|
687
|
+
return this.store().decrement(key, value);
|
|
688
|
+
}
|
|
689
|
+
static remember(key, ttl, callback) {
|
|
690
|
+
return this.store().remember(key, ttl, callback);
|
|
691
|
+
}
|
|
692
|
+
static rememberForever(key, callback) {
|
|
693
|
+
return this.store().rememberForever(key, callback);
|
|
694
|
+
}
|
|
695
|
+
static forget(key) {
|
|
696
|
+
return this.store().forget(key);
|
|
697
|
+
}
|
|
698
|
+
static delete(key) {
|
|
699
|
+
return this.store().delete(key);
|
|
700
|
+
}
|
|
701
|
+
static flush() {
|
|
702
|
+
return this.store().flush();
|
|
703
|
+
}
|
|
704
|
+
static clear() {
|
|
705
|
+
return this.store().clear();
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
//#endregion
|
|
709
|
+
export { RedisStore as a, DatabaseStore as c, ttlToSeconds as i, Store as l, configure as n, MemoryStore as o, Repository as r, FileStore as s, Cache as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Command } from "@h3ravel/musket";
|
|
2
|
+
|
|
3
|
+
//#region src/commands/CacheClearCommand.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Flush all entries from a cache store.
|
|
6
|
+
*/
|
|
7
|
+
declare class CacheClearCommand extends Command {
|
|
8
|
+
protected signature: string;
|
|
9
|
+
protected description: string;
|
|
10
|
+
handle(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
13
|
+
export { CacheClearCommand };
|