@tekir/cache 0.1.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 +31 -0
- package/dist/cache.d.ts +109 -0
- package/dist/cache.js +133 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/provider.d.ts +21 -0
- package/dist/provider.js +86 -0
- package/dist/stores/database.d.ts +63 -0
- package/dist/stores/database.js +104 -0
- package/dist/stores/memory.d.ts +49 -0
- package/dist/stores/memory.js +68 -0
- package/dist/stores/redis.d.ts +94 -0
- package/dist/stores/redis.js +112 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js +1 -0
- package/package.json +64 -0
- package/src/cache.ts +143 -0
- package/src/index.ts +6 -0
- package/src/provider.ts +93 -0
- package/src/stores/database.ts +109 -0
- package/src/stores/memory.ts +74 -0
- package/src/stores/redis.ts +123 -0
- package/src/types.ts +13 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory cache store with optional TTL expiration. Data is stored in a Map
|
|
3
|
+
* and lost when the process exits. Ideal for development and testing.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const store = new MemoryCacheStore()
|
|
8
|
+
* await store.set('key', 'value', 60)
|
|
9
|
+
* const val = await store.get<string>('key') // 'value'
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export class MemoryCacheStore {
|
|
13
|
+
data = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Retrieve a cached value by key. Returns `null` if the key does not exist
|
|
16
|
+
* or has expired.
|
|
17
|
+
*
|
|
18
|
+
* @param key - The cache key.
|
|
19
|
+
* @returns The stored value cast to `T`, or `null`.
|
|
20
|
+
*/
|
|
21
|
+
async get(key) {
|
|
22
|
+
const entry = this.data.get(key);
|
|
23
|
+
if (!entry)
|
|
24
|
+
return null;
|
|
25
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
26
|
+
this.data.delete(key);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
return entry.value;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Store a value under the given key with an optional TTL.
|
|
33
|
+
*
|
|
34
|
+
* @param key - The cache key.
|
|
35
|
+
* @param value - The value to cache.
|
|
36
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
37
|
+
*/
|
|
38
|
+
async set(key, value, ttlSeconds) {
|
|
39
|
+
this.data.set(key, {
|
|
40
|
+
value,
|
|
41
|
+
expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Check whether a key exists and is not expired.
|
|
46
|
+
*
|
|
47
|
+
* @param key - The cache key.
|
|
48
|
+
* @returns `true` if the key exists and has not expired.
|
|
49
|
+
*/
|
|
50
|
+
async has(key) {
|
|
51
|
+
return (await this.get(key)) !== null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Delete a key from the store.
|
|
55
|
+
*
|
|
56
|
+
* @param key - The cache key to remove.
|
|
57
|
+
* @returns `true` if the key was present and deleted.
|
|
58
|
+
*/
|
|
59
|
+
async delete(key) {
|
|
60
|
+
return this.data.delete(key);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Remove all entries from the store.
|
|
64
|
+
*/
|
|
65
|
+
async flush() {
|
|
66
|
+
this.data.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CacheStore } from '../types';
|
|
2
|
+
interface RedisClient {
|
|
3
|
+
get(key: string): Promise<string | null>;
|
|
4
|
+
set(key: string, value: string): Promise<unknown>;
|
|
5
|
+
expire(key: string, seconds: number): Promise<unknown>;
|
|
6
|
+
exists(key: string): Promise<number>;
|
|
7
|
+
del(key: string): Promise<unknown>;
|
|
8
|
+
send(command: string, args: unknown[]): Promise<unknown>;
|
|
9
|
+
connected?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Redis-backed cache store with key prefixing and optional TTL.
|
|
13
|
+
* Delegates all operations to a Redis client and prefixes keys to avoid collisions.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const store = new RedisCacheStore(redisClient, 'app:cache:')
|
|
18
|
+
* await store.set('user:1', { name: 'Alice' }, 300)
|
|
19
|
+
* const user = await store.get<{ name: string }>('user:1')
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export declare class RedisCacheStore implements CacheStore {
|
|
23
|
+
private redis;
|
|
24
|
+
private prefix;
|
|
25
|
+
/**
|
|
26
|
+
* Create a new RedisCacheStore.
|
|
27
|
+
*
|
|
28
|
+
* @param redis - A Redis client implementing the {@link RedisClient} interface.
|
|
29
|
+
* @param prefix - A string prepended to every cache key. Defaults to `'cache:'`.
|
|
30
|
+
*/
|
|
31
|
+
constructor(redis: RedisClient, prefix?: string);
|
|
32
|
+
/**
|
|
33
|
+
* Retrieve a cached value by key. Returns `null` if the key does not exist.
|
|
34
|
+
* The stored JSON string is parsed back into the original type.
|
|
35
|
+
*
|
|
36
|
+
* @param key - The cache key (without prefix).
|
|
37
|
+
* @returns The stored value cast to `T`, or `null` if not found.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const value = await store.get<string>('greeting') // 'hello' or null
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
45
|
+
/**
|
|
46
|
+
* Store a value under the given key with an optional TTL.
|
|
47
|
+
* The value is serialized to JSON before being sent to Redis.
|
|
48
|
+
*
|
|
49
|
+
* @param key - The cache key (without prefix).
|
|
50
|
+
* @param value - The value to cache (serialized to JSON).
|
|
51
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* await store.set('token', 'abc123', 3600) // expires in 1 hour
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Check whether a key exists in Redis.
|
|
61
|
+
*
|
|
62
|
+
* @param key - The cache key (without prefix).
|
|
63
|
+
* @returns `true` if the key exists.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* if (await store.has('session:abc')) { ... }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
has(key: string): Promise<boolean>;
|
|
71
|
+
/**
|
|
72
|
+
* Delete a key from Redis.
|
|
73
|
+
*
|
|
74
|
+
* @param key - The cache key (without prefix) to remove.
|
|
75
|
+
* @returns Always returns `true`.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* await store.delete('expired-token')
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
delete(key: string): Promise<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Flush the entire Redis database. Use with caution as this removes all keys,
|
|
85
|
+
* not just those managed by this store.
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* ```ts
|
|
89
|
+
* await store.flush()
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
flush(): Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export {};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis-backed cache store with key prefixing and optional TTL.
|
|
3
|
+
* Delegates all operations to a Redis client and prefixes keys to avoid collisions.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const store = new RedisCacheStore(redisClient, 'app:cache:')
|
|
8
|
+
* await store.set('user:1', { name: 'Alice' }, 300)
|
|
9
|
+
* const user = await store.get<{ name: string }>('user:1')
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export class RedisCacheStore {
|
|
13
|
+
redis;
|
|
14
|
+
prefix;
|
|
15
|
+
/**
|
|
16
|
+
* Create a new RedisCacheStore.
|
|
17
|
+
*
|
|
18
|
+
* @param redis - A Redis client implementing the {@link RedisClient} interface.
|
|
19
|
+
* @param prefix - A string prepended to every cache key. Defaults to `'cache:'`.
|
|
20
|
+
*/
|
|
21
|
+
constructor(redis, prefix = 'cache:') {
|
|
22
|
+
this.redis = redis;
|
|
23
|
+
this.prefix = prefix;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve a cached value by key. Returns `null` if the key does not exist.
|
|
27
|
+
* The stored JSON string is parsed back into the original type.
|
|
28
|
+
*
|
|
29
|
+
* @param key - The cache key (without prefix).
|
|
30
|
+
* @returns The stored value cast to `T`, or `null` if not found.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* const value = await store.get<string>('greeting') // 'hello' or null
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
async get(key) {
|
|
38
|
+
const val = await this.redis.get(this.prefix + key);
|
|
39
|
+
if (val === null)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(val);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return val;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Store a value under the given key with an optional TTL.
|
|
50
|
+
* The value is serialized to JSON before being sent to Redis.
|
|
51
|
+
*
|
|
52
|
+
* @param key - The cache key (without prefix).
|
|
53
|
+
* @param value - The value to cache (serialized to JSON).
|
|
54
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* await store.set('token', 'abc123', 3600) // expires in 1 hour
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
async set(key, value, ttlSeconds) {
|
|
62
|
+
const val = JSON.stringify(value);
|
|
63
|
+
if (ttlSeconds) {
|
|
64
|
+
await this.redis.set(this.prefix + key, val);
|
|
65
|
+
await this.redis.expire(this.prefix + key, ttlSeconds);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
await this.redis.set(this.prefix + key, val);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Check whether a key exists in Redis.
|
|
73
|
+
*
|
|
74
|
+
* @param key - The cache key (without prefix).
|
|
75
|
+
* @returns `true` if the key exists.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* if (await store.has('session:abc')) { ... }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
async has(key) {
|
|
83
|
+
return (await this.redis.exists(this.prefix + key)) > 0;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Delete a key from Redis.
|
|
87
|
+
*
|
|
88
|
+
* @param key - The cache key (without prefix) to remove.
|
|
89
|
+
* @returns Always returns `true`.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* await store.delete('expired-token')
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
async delete(key) {
|
|
97
|
+
await this.redis.del(this.prefix + key);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Flush the entire Redis database. Use with caution as this removes all keys,
|
|
102
|
+
* not just those managed by this store.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* await store.flush()
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
async flush() {
|
|
110
|
+
await this.redis.send('FLUSHDB', []);
|
|
111
|
+
}
|
|
112
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface CacheStore {
|
|
2
|
+
get<T = unknown>(key: string): Promise<T | null>;
|
|
3
|
+
set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
|
|
4
|
+
has(key: string): Promise<boolean>;
|
|
5
|
+
delete(key: string): Promise<boolean>;
|
|
6
|
+
flush(): Promise<void>;
|
|
7
|
+
}
|
|
8
|
+
export interface CacheConfig {
|
|
9
|
+
default?: string;
|
|
10
|
+
stores?: Record<string, CacheStore>;
|
|
11
|
+
ttl?: number;
|
|
12
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tekir/cache",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In-memory, Redis, and database caching abstraction",
|
|
5
|
+
"author": "dev@tekir.io",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/tekir-io/tekir.git",
|
|
10
|
+
"directory": "packages/tekir-cache"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/tekir-io/tekir/tree/main/packages/tekir-cache",
|
|
13
|
+
"keywords": [
|
|
14
|
+
"tekir",
|
|
15
|
+
"bun",
|
|
16
|
+
"typescript",
|
|
17
|
+
"framework",
|
|
18
|
+
"nodejs",
|
|
19
|
+
"fullstack"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "dist/index.js",
|
|
23
|
+
"types": "src/index.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"src",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE",
|
|
28
|
+
"dist"
|
|
29
|
+
],
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@tekir/core": "^0.1.0"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@tekir/redis": "^0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@tekir/redis": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "rm -rf dist && tsc --noEmit false",
|
|
54
|
+
"prepublishOnly": "bun run build"
|
|
55
|
+
},
|
|
56
|
+
"exports": {
|
|
57
|
+
".": {
|
|
58
|
+
"bun": "./src/index.ts",
|
|
59
|
+
"types": "./src/index.ts",
|
|
60
|
+
"import": "./dist/index.js",
|
|
61
|
+
"default": "./dist/index.js"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { CacheConfig, CacheStore } from './types'
|
|
2
|
+
import { MemoryCacheStore } from './stores/memory'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Multi-store cache manager that delegates to configured {@link CacheStore}
|
|
7
|
+
* implementations. Supports named stores, a default TTL, and convenience
|
|
8
|
+
* methods like {@link getOrSet} and {@link pull}.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const cache = new Cache({ stores: { memory: new MemoryCacheStore() }, ttl: 60 })
|
|
13
|
+
* await cache.set('key', 'value')
|
|
14
|
+
* const val = await cache.get<string>('key')
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class Cache {
|
|
18
|
+
private stores: Record<string, CacheStore>
|
|
19
|
+
private defaultStore: string
|
|
20
|
+
private defaultTtl: number
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create a new Cache instance.
|
|
24
|
+
*
|
|
25
|
+
* @param config - Cache configuration including stores, default store name, and TTL.
|
|
26
|
+
*/
|
|
27
|
+
constructor(config: CacheConfig = {}) {
|
|
28
|
+
this.stores = config.stores || { memory: new MemoryCacheStore() }
|
|
29
|
+
this.defaultStore = config.default || Object.keys(this.stores)[0]
|
|
30
|
+
this.defaultTtl = config.ttl || 3600
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve a specific named store, or the default store if no name is given.
|
|
35
|
+
*
|
|
36
|
+
* @param name - The store name. Omit to use the default store.
|
|
37
|
+
* @returns The resolved {@link CacheStore} instance.
|
|
38
|
+
* @throws Error if the requested store is not configured.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const redis = cache.store('redis')
|
|
43
|
+
* await redis.get('key')
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
store(name?: string): CacheStore {
|
|
47
|
+
const storeName = name || this.defaultStore
|
|
48
|
+
const s = this.stores[storeName]
|
|
49
|
+
if (!s) throw new Error(`Cache store "${storeName}" not configured`)
|
|
50
|
+
return s
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get a value from the default store.
|
|
55
|
+
*
|
|
56
|
+
* @param key - The cache key.
|
|
57
|
+
* @returns The cached value, or `null` if not found or expired.
|
|
58
|
+
*/
|
|
59
|
+
async get<T = unknown>(key: string): Promise<T | null> { return this.store().get<T>(key) }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set a value in the default store.
|
|
63
|
+
*
|
|
64
|
+
* @param key - The cache key.
|
|
65
|
+
* @param value - The value to store.
|
|
66
|
+
* @param ttl - Time-to-live in seconds. Falls back to the default TTL.
|
|
67
|
+
*/
|
|
68
|
+
async set(key: string, value: unknown, ttl?: number): Promise<void> { return this.store().set(key, value, ttl ?? this.defaultTtl) }
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check whether a key exists (and is not expired) in the default store.
|
|
72
|
+
*
|
|
73
|
+
* @param key - The cache key.
|
|
74
|
+
* @returns `true` if the key exists.
|
|
75
|
+
*/
|
|
76
|
+
async has(key: string): Promise<boolean> { return this.store().has(key) }
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete a key from the default store.
|
|
80
|
+
*
|
|
81
|
+
* @param key - The cache key.
|
|
82
|
+
* @returns `true` if the key was deleted.
|
|
83
|
+
*/
|
|
84
|
+
async delete(key: string): Promise<boolean> { return this.store().delete(key) }
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Flush all entries from the default store.
|
|
88
|
+
*/
|
|
89
|
+
async flush(): Promise<void> { return this.store().flush() }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get a cached value or compute and store it if missing. Inspired by AdonisJS.
|
|
93
|
+
*
|
|
94
|
+
* @param key - The cache key.
|
|
95
|
+
* @param ttl - Time-to-live in seconds for the computed value.
|
|
96
|
+
* @param factory - An async function that produces the value when not cached.
|
|
97
|
+
* @returns The cached or freshly-computed value.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* const users = await cache.getOrSet('users', 300, () => db.query('SELECT * FROM users'))
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
async getOrSet<T>(key: string, ttl: number, factory: () => Promise<T>): Promise<T> {
|
|
105
|
+
const cached = await this.get<T>(key)
|
|
106
|
+
if (cached !== null) return cached
|
|
107
|
+
const value = await factory()
|
|
108
|
+
await this.set(key, value, ttl)
|
|
109
|
+
return value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get a value and immediately delete it from the cache (atomic get-and-remove).
|
|
114
|
+
*
|
|
115
|
+
* @param key - The cache key.
|
|
116
|
+
* @returns The cached value, or `null` if not found.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const token = await cache.pull<string>('one-time-token')
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
async pull<T = unknown>(key: string): Promise<T | null> {
|
|
124
|
+
const value = await this.get<T>(key)
|
|
125
|
+
if (value !== null) await this.delete(key)
|
|
126
|
+
return value
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a new {@link Cache} instance with the given configuration.
|
|
132
|
+
*
|
|
133
|
+
* @param config - Optional cache configuration.
|
|
134
|
+
* @returns A new Cache instance.
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* const cache = createCache({ ttl: 120 })
|
|
139
|
+
* ```
|
|
140
|
+
*/
|
|
141
|
+
export function createCache(config?: CacheConfig): Cache {
|
|
142
|
+
return new Cache(config)
|
|
143
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CacheStore, CacheConfig } from './types'
|
|
2
|
+
export { MemoryCacheStore } from './stores/memory'
|
|
3
|
+
export { RedisCacheStore } from './stores/redis'
|
|
4
|
+
export { DatabaseCacheStore } from './stores/database'
|
|
5
|
+
export { Cache, createCache } from './cache'
|
|
6
|
+
export { CacheProvider } from './provider'
|
package/src/provider.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { App } from '@tekir/core'
|
|
2
|
+
import type { CacheStore } from './types'
|
|
3
|
+
import { Cache } from './cache'
|
|
4
|
+
import { MemoryCacheStore } from './stores/memory'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Service provider that registers a {@link Cache} instance into the application
|
|
8
|
+
* container. Reads the `cache` configuration to create stores for each configured
|
|
9
|
+
* driver (`memory`, `redis`, or `database`).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // In your kernel:
|
|
14
|
+
* app.register(new CacheProvider())
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export class CacheProvider {
|
|
18
|
+
/**
|
|
19
|
+
* Register the cache service with the application. Reads `cache.stores`,
|
|
20
|
+
* `cache.ttl`, and `cache.default` from the application config.
|
|
21
|
+
*
|
|
22
|
+
* @param app - The application instance.
|
|
23
|
+
*/
|
|
24
|
+
async register(app: App) {
|
|
25
|
+
const config = app.use('config')
|
|
26
|
+
if (!config('cache')) return
|
|
27
|
+
|
|
28
|
+
const storesConfig = config('cache.stores', {}) as Record<string, any>
|
|
29
|
+
const stores: Record<string, CacheStore> = {}
|
|
30
|
+
|
|
31
|
+
for (const [name, storeConfig] of Object.entries(storesConfig)) {
|
|
32
|
+
// Already a CacheStore instance (backwards compat)
|
|
33
|
+
if (storeConfig && typeof storeConfig.get === 'function') {
|
|
34
|
+
stores[name] = storeConfig as CacheStore
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const driver = storeConfig?.driver || name
|
|
39
|
+
|
|
40
|
+
if (driver === 'memory') {
|
|
41
|
+
stores[name] = new MemoryCacheStore()
|
|
42
|
+
|
|
43
|
+
} else if (driver === 'redis') {
|
|
44
|
+
let redis: any
|
|
45
|
+
try { redis = app.use('redis') } catch {}
|
|
46
|
+
if (!redis) {
|
|
47
|
+
// Fallback: create instance from config
|
|
48
|
+
let Redis: any
|
|
49
|
+
try {
|
|
50
|
+
Redis = (await import('@tekir/redis')).Redis
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`[@tekir/cache] Store "${name}" uses the redis driver but @tekir/redis is not installed. ` +
|
|
54
|
+
'Run: bun add @tekir/redis and register RedisProvider before CacheProvider.'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
redis = new Redis({ ...config('redis', {}), ...storeConfig })
|
|
58
|
+
}
|
|
59
|
+
const { RedisCacheStore } = await import('./stores/redis')
|
|
60
|
+
stores[name] = new RedisCacheStore(redis, storeConfig?.prefix)
|
|
61
|
+
|
|
62
|
+
} else if (driver === 'database') {
|
|
63
|
+
let db: any
|
|
64
|
+
try { db = app.use('db') } catch {}
|
|
65
|
+
if (!db) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`[@tekir/cache] Store "${name}" uses the database driver but no database service is registered. ` +
|
|
68
|
+
'Add DatabaseProvider to your kernel before CacheProvider.'
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
const { DatabaseCacheStore } = await import('./stores/database')
|
|
72
|
+
stores[name] = new DatabaseCacheStore(db, storeConfig?.table)
|
|
73
|
+
|
|
74
|
+
} else {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`[@tekir/cache] Unknown cache driver "${driver}" for store "${name}". ` +
|
|
77
|
+
'Supported drivers: memory, redis, database'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback to memory if no stores configured
|
|
83
|
+
if (Object.keys(stores).length === 0) {
|
|
84
|
+
stores.memory = new MemoryCacheStore()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
app.instance('cache', new Cache({
|
|
88
|
+
stores,
|
|
89
|
+
ttl: config('cache.ttl', 60) as number,
|
|
90
|
+
default: config('cache.default', Object.keys(stores)[0]) as string,
|
|
91
|
+
}))
|
|
92
|
+
}
|
|
93
|
+
}
|