@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.
@@ -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 };