@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,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
+ }
@@ -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'
@@ -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
+ }