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