@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,109 @@
|
|
|
1
|
+
import type { CacheStore } from '../types'
|
|
2
|
+
|
|
3
|
+
interface CacheDbRow { value: string; expires_at: number | null }
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Database-backed cache store using a SQLite/SQL table with optional TTL.
|
|
7
|
+
* The table is created automatically on first use.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const store = new DatabaseCacheStore(db, 'cache')
|
|
12
|
+
* await store.set('key', { foo: 'bar' }, 300)
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export class DatabaseCacheStore implements CacheStore {
|
|
16
|
+
private db: any
|
|
17
|
+
private table: string
|
|
18
|
+
private _ready = false
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a new DatabaseCacheStore.
|
|
22
|
+
*
|
|
23
|
+
* @param db - A database client with `exec`, `run`, and `queryOne` methods.
|
|
24
|
+
* @param table - The SQL table name for storing cache entries. Defaults to `'cache'`.
|
|
25
|
+
* @throws Error if the table name contains invalid characters.
|
|
26
|
+
*/
|
|
27
|
+
constructor(db: any, table = 'cache') {
|
|
28
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error(`Invalid table name: "${table}"`)
|
|
29
|
+
this.db = db
|
|
30
|
+
this.table = table
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private async _ensureTable() {
|
|
34
|
+
if (this._ready) return
|
|
35
|
+
try {
|
|
36
|
+
await this.db.exec(`CREATE TABLE IF NOT EXISTS "${this.table}" (key TEXT PRIMARY KEY, value TEXT, expires_at INTEGER)`)
|
|
37
|
+
this._ready = true
|
|
38
|
+
} catch {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Retrieve a cached value by key. Expired entries are deleted and `null` is returned.
|
|
43
|
+
*
|
|
44
|
+
* @param key - The cache key.
|
|
45
|
+
* @returns The stored value parsed from JSON, or `null`.
|
|
46
|
+
*/
|
|
47
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
48
|
+
await this._ensureTable()
|
|
49
|
+
const row = await this.db.queryOne(`SELECT value, expires_at FROM "${this.table}" WHERE key = ?`, [key]) as CacheDbRow | null
|
|
50
|
+
if (!row) return null
|
|
51
|
+
if (row.expires_at && Date.now() > row.expires_at) {
|
|
52
|
+
await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key])
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
try { return JSON.parse(row.value) } catch { return row.value as T }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Store a value under the given key, reptekirg any existing entry.
|
|
60
|
+
*
|
|
61
|
+
* @param key - The cache key.
|
|
62
|
+
* @param value - The value to cache (serialized to JSON).
|
|
63
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
64
|
+
*/
|
|
65
|
+
async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
|
|
66
|
+
await this._ensureTable()
|
|
67
|
+
const val = JSON.stringify(value)
|
|
68
|
+
const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null
|
|
69
|
+
await this.db.run(
|
|
70
|
+
`INSERT OR REPLACE INTO "${this.table}" (key, value, expires_at) VALUES (?, ?, ?)`,
|
|
71
|
+
[key, val, expiresAt]
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check whether a key exists and is not expired.
|
|
77
|
+
*
|
|
78
|
+
* @param key - The cache key.
|
|
79
|
+
* @returns `true` if the key exists and has not expired.
|
|
80
|
+
*/
|
|
81
|
+
async has(key: string): Promise<boolean> {
|
|
82
|
+
return (await this.get(key)) !== null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete a key from the database.
|
|
87
|
+
*
|
|
88
|
+
* @param key - The cache key to remove.
|
|
89
|
+
* @returns Always returns `true`.
|
|
90
|
+
*/
|
|
91
|
+
async delete(key: string): Promise<boolean> {
|
|
92
|
+
await this._ensureTable()
|
|
93
|
+
await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key])
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Remove all entries from the cache table.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* await store.flush()
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
async flush(): Promise<void> {
|
|
106
|
+
await this._ensureTable()
|
|
107
|
+
await this.db.run(`DELETE FROM "${this.table}"`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { CacheStore } from '../types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory cache store with optional TTL expiration. Data is stored in a Map
|
|
5
|
+
* and lost when the process exits. Ideal for development and testing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* const store = new MemoryCacheStore()
|
|
10
|
+
* await store.set('key', 'value', 60)
|
|
11
|
+
* const val = await store.get<string>('key') // 'value'
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export class MemoryCacheStore implements CacheStore {
|
|
15
|
+
private data = new Map<string, { value: unknown; expiresAt: number | null }>()
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Retrieve a cached value by key. Returns `null` if the key does not exist
|
|
19
|
+
* or has expired.
|
|
20
|
+
*
|
|
21
|
+
* @param key - The cache key.
|
|
22
|
+
* @returns The stored value cast to `T`, or `null`.
|
|
23
|
+
*/
|
|
24
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
25
|
+
const entry = this.data.get(key)
|
|
26
|
+
if (!entry) return null
|
|
27
|
+
if (entry.expiresAt && Date.now() > entry.expiresAt) {
|
|
28
|
+
this.data.delete(key)
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
return entry.value as T
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Store a value under the given key with an optional TTL.
|
|
36
|
+
*
|
|
37
|
+
* @param key - The cache key.
|
|
38
|
+
* @param value - The value to cache.
|
|
39
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
40
|
+
*/
|
|
41
|
+
async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
|
|
42
|
+
this.data.set(key, {
|
|
43
|
+
value,
|
|
44
|
+
expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check whether a key exists and is not expired.
|
|
50
|
+
*
|
|
51
|
+
* @param key - The cache key.
|
|
52
|
+
* @returns `true` if the key exists and has not expired.
|
|
53
|
+
*/
|
|
54
|
+
async has(key: string): Promise<boolean> {
|
|
55
|
+
return (await this.get(key)) !== null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete a key from the store.
|
|
60
|
+
*
|
|
61
|
+
* @param key - The cache key to remove.
|
|
62
|
+
* @returns `true` if the key was present and deleted.
|
|
63
|
+
*/
|
|
64
|
+
async delete(key: string): Promise<boolean> {
|
|
65
|
+
return this.data.delete(key)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove all entries from the store.
|
|
70
|
+
*/
|
|
71
|
+
async flush(): Promise<void> {
|
|
72
|
+
this.data.clear()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { CacheStore } from '../types'
|
|
2
|
+
|
|
3
|
+
interface RedisClient {
|
|
4
|
+
get(key: string): Promise<string | null>
|
|
5
|
+
set(key: string, value: string): Promise<unknown>
|
|
6
|
+
expire(key: string, seconds: number): Promise<unknown>
|
|
7
|
+
exists(key: string): Promise<number>
|
|
8
|
+
del(key: string): Promise<unknown>
|
|
9
|
+
send(command: string, args: unknown[]): Promise<unknown>
|
|
10
|
+
connected?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Redis-backed cache store with key prefixing and optional TTL.
|
|
15
|
+
* Delegates all operations to a Redis client and prefixes keys to avoid collisions.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* const store = new RedisCacheStore(redisClient, 'app:cache:')
|
|
20
|
+
* await store.set('user:1', { name: 'Alice' }, 300)
|
|
21
|
+
* const user = await store.get<{ name: string }>('user:1')
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export class RedisCacheStore implements CacheStore {
|
|
25
|
+
private redis: RedisClient
|
|
26
|
+
private prefix: string
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a new RedisCacheStore.
|
|
30
|
+
*
|
|
31
|
+
* @param redis - A Redis client implementing the {@link RedisClient} interface.
|
|
32
|
+
* @param prefix - A string prepended to every cache key. Defaults to `'cache:'`.
|
|
33
|
+
*/
|
|
34
|
+
constructor(redis: RedisClient, prefix = 'cache:') {
|
|
35
|
+
this.redis = redis
|
|
36
|
+
this.prefix = prefix
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Retrieve a cached value by key. Returns `null` if the key does not exist.
|
|
41
|
+
* The stored JSON string is parsed back into the original type.
|
|
42
|
+
*
|
|
43
|
+
* @param key - The cache key (without prefix).
|
|
44
|
+
* @returns The stored value cast to `T`, or `null` if not found.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const value = await store.get<string>('greeting') // 'hello' or null
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
async get<T = unknown>(key: string): Promise<T | null> {
|
|
52
|
+
const val = await this.redis.get(this.prefix + key)
|
|
53
|
+
if (val === null) return null
|
|
54
|
+
try { return JSON.parse(val) as T } catch { return val as unknown as T }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Store a value under the given key with an optional TTL.
|
|
59
|
+
* The value is serialized to JSON before being sent to Redis.
|
|
60
|
+
*
|
|
61
|
+
* @param key - The cache key (without prefix).
|
|
62
|
+
* @param value - The value to cache (serialized to JSON).
|
|
63
|
+
* @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* await store.set('token', 'abc123', 3600) // expires in 1 hour
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
async set(key: string, value: unknown, ttlSeconds?: number): Promise<void> {
|
|
71
|
+
const val = JSON.stringify(value)
|
|
72
|
+
if (ttlSeconds) {
|
|
73
|
+
await this.redis.set(this.prefix + key, val)
|
|
74
|
+
await this.redis.expire(this.prefix + key, ttlSeconds)
|
|
75
|
+
} else {
|
|
76
|
+
await this.redis.set(this.prefix + key, val)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check whether a key exists in Redis.
|
|
82
|
+
*
|
|
83
|
+
* @param key - The cache key (without prefix).
|
|
84
|
+
* @returns `true` if the key exists.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* if (await store.has('session:abc')) { ... }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
async has(key: string): Promise<boolean> {
|
|
92
|
+
return (await this.redis.exists(this.prefix + key)) > 0
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete a key from Redis.
|
|
97
|
+
*
|
|
98
|
+
* @param key - The cache key (without prefix) to remove.
|
|
99
|
+
* @returns Always returns `true`.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* await store.delete('expired-token')
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
async delete(key: string): Promise<boolean> {
|
|
107
|
+
await this.redis.del(this.prefix + key)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Flush the entire Redis database. Use with caution as this removes all keys,
|
|
113
|
+
* not just those managed by this store.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* await store.flush()
|
|
118
|
+
* ```
|
|
119
|
+
*/
|
|
120
|
+
async flush(): Promise<void> {
|
|
121
|
+
await this.redis.send('FLUSHDB', [])
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
|
|
9
|
+
export interface CacheConfig {
|
|
10
|
+
default?: string
|
|
11
|
+
stores?: Record<string, CacheStore>
|
|
12
|
+
ttl?: number // default TTL in seconds
|
|
13
|
+
}
|