@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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tekir
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ <p align="center">
2
+ <img src="https://tekir.io/logo.svg" width="80" alt="tekir" />
3
+ </p>
4
+
5
+ <h1 align="center">@tekir/cache</h1>
6
+
7
+ <p align="center">In-memory, Redis, and database caching abstraction</p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/@tekir/cache"><img src="https://img.shields.io/npm/v/@tekir/cache.svg" alt="npm version" /></a>
11
+ <a href="https://www.npmjs.com/package/@tekir/cache"><img src="https://img.shields.io/npm/dm/@tekir/cache.svg" alt="npm downloads" /></a>
12
+ <a href="https://github.com/tekir-io/tekir/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@tekir/cache.svg" alt="license" /></a>
13
+ </p>
14
+
15
+ <p align="center">
16
+ <a href="https://tekir.io">Website</a> · <a href="https://docs.tekir.io">Documentation</a> · <a href="https://github.com/tekir-io/tekir">GitHub</a>
17
+ </p>
18
+
19
+ ---
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ bun add @tekir/cache
25
+ ```
26
+
27
+ For full usage and configuration, see the [documentation](https://docs.tekir.io/advanced/cache).
28
+
29
+ ## License
30
+
31
+ MIT
@@ -0,0 +1,109 @@
1
+ import type { CacheConfig, CacheStore } from './types';
2
+ /**
3
+ * Multi-store cache manager that delegates to configured {@link CacheStore}
4
+ * implementations. Supports named stores, a default TTL, and convenience
5
+ * methods like {@link getOrSet} and {@link pull}.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const cache = new Cache({ stores: { memory: new MemoryCacheStore() }, ttl: 60 })
10
+ * await cache.set('key', 'value')
11
+ * const val = await cache.get<string>('key')
12
+ * ```
13
+ */
14
+ export declare class Cache {
15
+ private stores;
16
+ private defaultStore;
17
+ private defaultTtl;
18
+ /**
19
+ * Create a new Cache instance.
20
+ *
21
+ * @param config - Cache configuration including stores, default store name, and TTL.
22
+ */
23
+ constructor(config?: CacheConfig);
24
+ /**
25
+ * Retrieve a specific named store, or the default store if no name is given.
26
+ *
27
+ * @param name - The store name. Omit to use the default store.
28
+ * @returns The resolved {@link CacheStore} instance.
29
+ * @throws Error if the requested store is not configured.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * const redis = cache.store('redis')
34
+ * await redis.get('key')
35
+ * ```
36
+ */
37
+ store(name?: string): CacheStore;
38
+ /**
39
+ * Get a value from the default store.
40
+ *
41
+ * @param key - The cache key.
42
+ * @returns The cached value, or `null` if not found or expired.
43
+ */
44
+ get<T = unknown>(key: string): Promise<T | null>;
45
+ /**
46
+ * Set a value in the default store.
47
+ *
48
+ * @param key - The cache key.
49
+ * @param value - The value to store.
50
+ * @param ttl - Time-to-live in seconds. Falls back to the default TTL.
51
+ */
52
+ set(key: string, value: unknown, ttl?: number): Promise<void>;
53
+ /**
54
+ * Check whether a key exists (and is not expired) in the default store.
55
+ *
56
+ * @param key - The cache key.
57
+ * @returns `true` if the key exists.
58
+ */
59
+ has(key: string): Promise<boolean>;
60
+ /**
61
+ * Delete a key from the default store.
62
+ *
63
+ * @param key - The cache key.
64
+ * @returns `true` if the key was deleted.
65
+ */
66
+ delete(key: string): Promise<boolean>;
67
+ /**
68
+ * Flush all entries from the default store.
69
+ */
70
+ flush(): Promise<void>;
71
+ /**
72
+ * Get a cached value or compute and store it if missing. Inspired by AdonisJS.
73
+ *
74
+ * @param key - The cache key.
75
+ * @param ttl - Time-to-live in seconds for the computed value.
76
+ * @param factory - An async function that produces the value when not cached.
77
+ * @returns The cached or freshly-computed value.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const users = await cache.getOrSet('users', 300, () => db.query('SELECT * FROM users'))
82
+ * ```
83
+ */
84
+ getOrSet<T>(key: string, ttl: number, factory: () => Promise<T>): Promise<T>;
85
+ /**
86
+ * Get a value and immediately delete it from the cache (atomic get-and-remove).
87
+ *
88
+ * @param key - The cache key.
89
+ * @returns The cached value, or `null` if not found.
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * const token = await cache.pull<string>('one-time-token')
94
+ * ```
95
+ */
96
+ pull<T = unknown>(key: string): Promise<T | null>;
97
+ }
98
+ /**
99
+ * Create a new {@link Cache} instance with the given configuration.
100
+ *
101
+ * @param config - Optional cache configuration.
102
+ * @returns A new Cache instance.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * const cache = createCache({ ttl: 120 })
107
+ * ```
108
+ */
109
+ export declare function createCache(config?: CacheConfig): Cache;
package/dist/cache.js ADDED
@@ -0,0 +1,133 @@
1
+ import { MemoryCacheStore } from './stores/memory';
2
+ /**
3
+ * Multi-store cache manager that delegates to configured {@link CacheStore}
4
+ * implementations. Supports named stores, a default TTL, and convenience
5
+ * methods like {@link getOrSet} and {@link pull}.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const cache = new Cache({ stores: { memory: new MemoryCacheStore() }, ttl: 60 })
10
+ * await cache.set('key', 'value')
11
+ * const val = await cache.get<string>('key')
12
+ * ```
13
+ */
14
+ export class Cache {
15
+ stores;
16
+ defaultStore;
17
+ defaultTtl;
18
+ /**
19
+ * Create a new Cache instance.
20
+ *
21
+ * @param config - Cache configuration including stores, default store name, and TTL.
22
+ */
23
+ constructor(config = {}) {
24
+ this.stores = config.stores || { memory: new MemoryCacheStore() };
25
+ this.defaultStore = config.default || Object.keys(this.stores)[0];
26
+ this.defaultTtl = config.ttl || 3600;
27
+ }
28
+ /**
29
+ * Retrieve a specific named store, or the default store if no name is given.
30
+ *
31
+ * @param name - The store name. Omit to use the default store.
32
+ * @returns The resolved {@link CacheStore} instance.
33
+ * @throws Error if the requested store is not configured.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const redis = cache.store('redis')
38
+ * await redis.get('key')
39
+ * ```
40
+ */
41
+ store(name) {
42
+ const storeName = name || this.defaultStore;
43
+ const s = this.stores[storeName];
44
+ if (!s)
45
+ throw new Error(`Cache store "${storeName}" not configured`);
46
+ return s;
47
+ }
48
+ /**
49
+ * Get a value from the default store.
50
+ *
51
+ * @param key - The cache key.
52
+ * @returns The cached value, or `null` if not found or expired.
53
+ */
54
+ async get(key) { return this.store().get(key); }
55
+ /**
56
+ * Set a value in the default store.
57
+ *
58
+ * @param key - The cache key.
59
+ * @param value - The value to store.
60
+ * @param ttl - Time-to-live in seconds. Falls back to the default TTL.
61
+ */
62
+ async set(key, value, ttl) { return this.store().set(key, value, ttl ?? this.defaultTtl); }
63
+ /**
64
+ * Check whether a key exists (and is not expired) in the default store.
65
+ *
66
+ * @param key - The cache key.
67
+ * @returns `true` if the key exists.
68
+ */
69
+ async has(key) { return this.store().has(key); }
70
+ /**
71
+ * Delete a key from the default store.
72
+ *
73
+ * @param key - The cache key.
74
+ * @returns `true` if the key was deleted.
75
+ */
76
+ async delete(key) { return this.store().delete(key); }
77
+ /**
78
+ * Flush all entries from the default store.
79
+ */
80
+ async flush() { return this.store().flush(); }
81
+ /**
82
+ * Get a cached value or compute and store it if missing. Inspired by AdonisJS.
83
+ *
84
+ * @param key - The cache key.
85
+ * @param ttl - Time-to-live in seconds for the computed value.
86
+ * @param factory - An async function that produces the value when not cached.
87
+ * @returns The cached or freshly-computed value.
88
+ *
89
+ * @example
90
+ * ```ts
91
+ * const users = await cache.getOrSet('users', 300, () => db.query('SELECT * FROM users'))
92
+ * ```
93
+ */
94
+ async getOrSet(key, ttl, factory) {
95
+ const cached = await this.get(key);
96
+ if (cached !== null)
97
+ return cached;
98
+ const value = await factory();
99
+ await this.set(key, value, ttl);
100
+ return value;
101
+ }
102
+ /**
103
+ * Get a value and immediately delete it from the cache (atomic get-and-remove).
104
+ *
105
+ * @param key - The cache key.
106
+ * @returns The cached value, or `null` if not found.
107
+ *
108
+ * @example
109
+ * ```ts
110
+ * const token = await cache.pull<string>('one-time-token')
111
+ * ```
112
+ */
113
+ async pull(key) {
114
+ const value = await this.get(key);
115
+ if (value !== null)
116
+ await this.delete(key);
117
+ return value;
118
+ }
119
+ }
120
+ /**
121
+ * Create a new {@link Cache} instance with the given configuration.
122
+ *
123
+ * @param config - Optional cache configuration.
124
+ * @returns A new Cache instance.
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * const cache = createCache({ ttl: 120 })
129
+ * ```
130
+ */
131
+ export function createCache(config) {
132
+ return new Cache(config);
133
+ }
@@ -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/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { MemoryCacheStore } from './stores/memory';
2
+ export { RedisCacheStore } from './stores/redis';
3
+ export { DatabaseCacheStore } from './stores/database';
4
+ export { Cache, createCache } from './cache';
5
+ export { CacheProvider } from './provider';
@@ -0,0 +1,21 @@
1
+ import type { App } from '@tekir/core';
2
+ /**
3
+ * Service provider that registers a {@link Cache} instance into the application
4
+ * container. Reads the `cache` configuration to create stores for each configured
5
+ * driver (`memory`, `redis`, or `database`).
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * // In your kernel:
10
+ * app.register(new CacheProvider())
11
+ * ```
12
+ */
13
+ export declare class CacheProvider {
14
+ /**
15
+ * Register the cache service with the application. Reads `cache.stores`,
16
+ * `cache.ttl`, and `cache.default` from the application config.
17
+ *
18
+ * @param app - The application instance.
19
+ */
20
+ register(app: App): Promise<void>;
21
+ }
@@ -0,0 +1,86 @@
1
+ import { Cache } from './cache';
2
+ import { MemoryCacheStore } from './stores/memory';
3
+ /**
4
+ * Service provider that registers a {@link Cache} instance into the application
5
+ * container. Reads the `cache` configuration to create stores for each configured
6
+ * driver (`memory`, `redis`, or `database`).
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * // In your kernel:
11
+ * app.register(new CacheProvider())
12
+ * ```
13
+ */
14
+ export class CacheProvider {
15
+ /**
16
+ * Register the cache service with the application. Reads `cache.stores`,
17
+ * `cache.ttl`, and `cache.default` from the application config.
18
+ *
19
+ * @param app - The application instance.
20
+ */
21
+ async register(app) {
22
+ const config = app.use('config');
23
+ if (!config('cache'))
24
+ return;
25
+ const storesConfig = config('cache.stores', {});
26
+ const stores = {};
27
+ for (const [name, storeConfig] of Object.entries(storesConfig)) {
28
+ // Already a CacheStore instance (backwards compat)
29
+ if (storeConfig && typeof storeConfig.get === 'function') {
30
+ stores[name] = storeConfig;
31
+ continue;
32
+ }
33
+ const driver = storeConfig?.driver || name;
34
+ if (driver === 'memory') {
35
+ stores[name] = new MemoryCacheStore();
36
+ }
37
+ else if (driver === 'redis') {
38
+ let redis;
39
+ try {
40
+ redis = app.use('redis');
41
+ }
42
+ catch { }
43
+ if (!redis) {
44
+ // Fallback: create instance from config
45
+ let Redis;
46
+ try {
47
+ Redis = (await import('@tekir/redis')).Redis;
48
+ }
49
+ catch {
50
+ throw new Error(`[@tekir/cache] Store "${name}" uses the redis driver but @tekir/redis is not installed. ` +
51
+ 'Run: bun add @tekir/redis and register RedisProvider before CacheProvider.');
52
+ }
53
+ redis = new Redis({ ...config('redis', {}), ...storeConfig });
54
+ }
55
+ const { RedisCacheStore } = await import('./stores/redis');
56
+ stores[name] = new RedisCacheStore(redis, storeConfig?.prefix);
57
+ }
58
+ else if (driver === 'database') {
59
+ let db;
60
+ try {
61
+ db = app.use('db');
62
+ }
63
+ catch { }
64
+ if (!db) {
65
+ throw new Error(`[@tekir/cache] Store "${name}" uses the database driver but no database service is registered. ` +
66
+ 'Add DatabaseProvider to your kernel before CacheProvider.');
67
+ }
68
+ const { DatabaseCacheStore } = await import('./stores/database');
69
+ stores[name] = new DatabaseCacheStore(db, storeConfig?.table);
70
+ }
71
+ else {
72
+ throw new Error(`[@tekir/cache] Unknown cache driver "${driver}" for store "${name}". ` +
73
+ 'Supported drivers: memory, redis, database');
74
+ }
75
+ }
76
+ // Fallback to memory if no stores configured
77
+ if (Object.keys(stores).length === 0) {
78
+ stores.memory = new MemoryCacheStore();
79
+ }
80
+ app.instance('cache', new Cache({
81
+ stores,
82
+ ttl: config('cache.ttl', 60),
83
+ default: config('cache.default', Object.keys(stores)[0]),
84
+ }));
85
+ }
86
+ }
@@ -0,0 +1,63 @@
1
+ import type { CacheStore } from '../types';
2
+ /**
3
+ * Database-backed cache store using a SQLite/SQL table with optional TTL.
4
+ * The table is created automatically on first use.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const store = new DatabaseCacheStore(db, 'cache')
9
+ * await store.set('key', { foo: 'bar' }, 300)
10
+ * ```
11
+ */
12
+ export declare class DatabaseCacheStore implements CacheStore {
13
+ private db;
14
+ private table;
15
+ private _ready;
16
+ /**
17
+ * Create a new DatabaseCacheStore.
18
+ *
19
+ * @param db - A database client with `exec`, `run`, and `queryOne` methods.
20
+ * @param table - The SQL table name for storing cache entries. Defaults to `'cache'`.
21
+ * @throws Error if the table name contains invalid characters.
22
+ */
23
+ constructor(db: any, table?: string);
24
+ private _ensureTable;
25
+ /**
26
+ * Retrieve a cached value by key. Expired entries are deleted and `null` is returned.
27
+ *
28
+ * @param key - The cache key.
29
+ * @returns The stored value parsed from JSON, or `null`.
30
+ */
31
+ get<T = unknown>(key: string): Promise<T | null>;
32
+ /**
33
+ * Store a value under the given key, reptekirg any existing entry.
34
+ *
35
+ * @param key - The cache key.
36
+ * @param value - The value to cache (serialized to JSON).
37
+ * @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
38
+ */
39
+ set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
40
+ /**
41
+ * Check whether a key exists and is not expired.
42
+ *
43
+ * @param key - The cache key.
44
+ * @returns `true` if the key exists and has not expired.
45
+ */
46
+ has(key: string): Promise<boolean>;
47
+ /**
48
+ * Delete a key from the database.
49
+ *
50
+ * @param key - The cache key to remove.
51
+ * @returns Always returns `true`.
52
+ */
53
+ delete(key: string): Promise<boolean>;
54
+ /**
55
+ * Remove all entries from the cache table.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * await store.flush()
60
+ * ```
61
+ */
62
+ flush(): Promise<void>;
63
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Database-backed cache store using a SQLite/SQL table with optional TTL.
3
+ * The table is created automatically on first use.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const store = new DatabaseCacheStore(db, 'cache')
8
+ * await store.set('key', { foo: 'bar' }, 300)
9
+ * ```
10
+ */
11
+ export class DatabaseCacheStore {
12
+ db;
13
+ table;
14
+ _ready = false;
15
+ /**
16
+ * Create a new DatabaseCacheStore.
17
+ *
18
+ * @param db - A database client with `exec`, `run`, and `queryOne` methods.
19
+ * @param table - The SQL table name for storing cache entries. Defaults to `'cache'`.
20
+ * @throws Error if the table name contains invalid characters.
21
+ */
22
+ constructor(db, table = 'cache') {
23
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table))
24
+ throw new Error(`Invalid table name: "${table}"`);
25
+ this.db = db;
26
+ this.table = table;
27
+ }
28
+ async _ensureTable() {
29
+ if (this._ready)
30
+ return;
31
+ try {
32
+ await this.db.exec(`CREATE TABLE IF NOT EXISTS "${this.table}" (key TEXT PRIMARY KEY, value TEXT, expires_at INTEGER)`);
33
+ this._ready = true;
34
+ }
35
+ catch { }
36
+ }
37
+ /**
38
+ * Retrieve a cached value by key. Expired entries are deleted and `null` is returned.
39
+ *
40
+ * @param key - The cache key.
41
+ * @returns The stored value parsed from JSON, or `null`.
42
+ */
43
+ async get(key) {
44
+ await this._ensureTable();
45
+ const row = await this.db.queryOne(`SELECT value, expires_at FROM "${this.table}" WHERE key = ?`, [key]);
46
+ if (!row)
47
+ return null;
48
+ if (row.expires_at && Date.now() > row.expires_at) {
49
+ await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key]);
50
+ return null;
51
+ }
52
+ try {
53
+ return JSON.parse(row.value);
54
+ }
55
+ catch {
56
+ return row.value;
57
+ }
58
+ }
59
+ /**
60
+ * Store a value under the given key, reptekirg any existing entry.
61
+ *
62
+ * @param key - The cache key.
63
+ * @param value - The value to cache (serialized to JSON).
64
+ * @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
65
+ */
66
+ async set(key, value, ttlSeconds) {
67
+ await this._ensureTable();
68
+ const val = JSON.stringify(value);
69
+ const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
70
+ await this.db.run(`INSERT OR REPLACE INTO "${this.table}" (key, value, expires_at) VALUES (?, ?, ?)`, [key, val, expiresAt]);
71
+ }
72
+ /**
73
+ * Check whether a key exists and is not expired.
74
+ *
75
+ * @param key - The cache key.
76
+ * @returns `true` if the key exists and has not expired.
77
+ */
78
+ async has(key) {
79
+ return (await this.get(key)) !== null;
80
+ }
81
+ /**
82
+ * Delete a key from the database.
83
+ *
84
+ * @param key - The cache key to remove.
85
+ * @returns Always returns `true`.
86
+ */
87
+ async delete(key) {
88
+ await this._ensureTable();
89
+ await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key]);
90
+ return true;
91
+ }
92
+ /**
93
+ * Remove all entries from the cache table.
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * await store.flush()
98
+ * ```
99
+ */
100
+ async flush() {
101
+ await this._ensureTable();
102
+ await this.db.run(`DELETE FROM "${this.table}"`);
103
+ }
104
+ }
@@ -0,0 +1,49 @@
1
+ import type { CacheStore } from '../types';
2
+ /**
3
+ * In-memory cache store with optional TTL expiration. Data is stored in a Map
4
+ * and lost when the process exits. Ideal for development and testing.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const store = new MemoryCacheStore()
9
+ * await store.set('key', 'value', 60)
10
+ * const val = await store.get<string>('key') // 'value'
11
+ * ```
12
+ */
13
+ export declare class MemoryCacheStore implements CacheStore {
14
+ private data;
15
+ /**
16
+ * Retrieve a cached value by key. Returns `null` if the key does not exist
17
+ * or has expired.
18
+ *
19
+ * @param key - The cache key.
20
+ * @returns The stored value cast to `T`, or `null`.
21
+ */
22
+ get<T = unknown>(key: string): Promise<T | null>;
23
+ /**
24
+ * Store a value under the given key with an optional TTL.
25
+ *
26
+ * @param key - The cache key.
27
+ * @param value - The value to cache.
28
+ * @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
29
+ */
30
+ set(key: string, value: unknown, ttlSeconds?: number): Promise<void>;
31
+ /**
32
+ * Check whether a key exists and is not expired.
33
+ *
34
+ * @param key - The cache key.
35
+ * @returns `true` if the key exists and has not expired.
36
+ */
37
+ has(key: string): Promise<boolean>;
38
+ /**
39
+ * Delete a key from the store.
40
+ *
41
+ * @param key - The cache key to remove.
42
+ * @returns `true` if the key was present and deleted.
43
+ */
44
+ delete(key: string): Promise<boolean>;
45
+ /**
46
+ * Remove all entries from the store.
47
+ */
48
+ flush(): Promise<void>;
49
+ }