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