@tekir/cache 0.1.8 → 0.1.10

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.
@@ -1,129 +0,0 @@
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 (e) {
36
- // Don't silently swallow: an unset _ready means every later get/set blows
37
- // up with a confusing SQL error. Surface the real cause and rethrow so the
38
- // misconfiguration is visible at the point of failure.
39
- console.error(`[@tekir/cache] Failed to create cache table "${this.table}": ${e.message}`);
40
- throw e;
41
- }
42
- }
43
- /**
44
- * Retrieve a cached value by key. Expired entries are deleted and `null` is returned.
45
- *
46
- * @param key - The cache key.
47
- * @returns The stored value parsed from JSON, or `null`.
48
- */
49
- async get(key) {
50
- await this._ensureTable();
51
- const row = await this.db.queryOne(`SELECT value, expires_at FROM "${this.table}" WHERE key = ?`, [key]);
52
- if (!row)
53
- return null;
54
- if (row.expires_at && Date.now() > row.expires_at) {
55
- await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key]);
56
- return null;
57
- }
58
- try {
59
- return JSON.parse(row.value);
60
- }
61
- catch {
62
- return row.value;
63
- }
64
- }
65
- /**
66
- * Store a value under the given key, reptekirg any existing entry.
67
- *
68
- * @param key - The cache key.
69
- * @param value - The value to cache (serialized to JSON).
70
- * @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
71
- */
72
- async set(key, value, ttlSeconds) {
73
- await this._ensureTable();
74
- const val = JSON.stringify(value);
75
- const expiresAt = ttlSeconds ? Date.now() + ttlSeconds * 1000 : null;
76
- await this.db.run(`INSERT OR REPLACE INTO "${this.table}" (key, value, expires_at) VALUES (?, ?, ?)`, [key, val, expiresAt]);
77
- }
78
- /**
79
- * Check whether a key exists and is not expired.
80
- *
81
- * @param key - The cache key.
82
- * @returns `true` if the key exists and has not expired.
83
- */
84
- async has(key) {
85
- await this._ensureTable();
86
- const row = await this.db.queryOne(`SELECT expires_at FROM "${this.table}" WHERE key = ?`, [key]);
87
- if (!row)
88
- return false;
89
- if (row.expires_at && Date.now() > row.expires_at) {
90
- await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key]);
91
- return false;
92
- }
93
- // Present even if the stored value is `null` (negative caching).
94
- return true;
95
- }
96
- /**
97
- * Delete a key from the database.
98
- *
99
- * @param key - The cache key to remove.
100
- * @returns Always returns `true`.
101
- */
102
- async delete(key) {
103
- await this._ensureTable();
104
- await this.db.run(`DELETE FROM "${this.table}" WHERE key = ?`, [key]);
105
- return true;
106
- }
107
- /**
108
- * Delete all expired entries. Entries are otherwise only removed when read,
109
- * so call this periodically to stop never-read expired rows from accumulating.
110
- *
111
- * @returns A promise that resolves once expired rows have been removed.
112
- */
113
- async prune() {
114
- await this._ensureTable();
115
- await this.db.run(`DELETE FROM "${this.table}" WHERE expires_at IS NOT NULL AND expires_at < ?`, [Date.now()]);
116
- }
117
- /**
118
- * Remove all entries from the cache table.
119
- *
120
- * @example
121
- * ```ts
122
- * await store.flush()
123
- * ```
124
- */
125
- async flush() {
126
- await this._ensureTable();
127
- await this.db.run(`DELETE FROM "${this.table}"`);
128
- }
129
- }
@@ -1,108 +0,0 @@
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
- maxEntries;
15
- writes = 0;
16
- /**
17
- * @param options.maxEntries - Hard cap on stored entries. When exceeded, the
18
- * oldest insertion-order entry is evicted (after pruning expired ones).
19
- * Defaults to 10000 to bound memory growth from never-read keys. Set to 0
20
- * to disable the cap.
21
- */
22
- constructor(options = {}) {
23
- this.maxEntries = options.maxEntries ?? 10000;
24
- }
25
- /** Remove every entry whose TTL has elapsed. */
26
- prune() {
27
- const now = Date.now();
28
- for (const [k, entry] of this.data) {
29
- if (entry.expiresAt && now > entry.expiresAt)
30
- this.data.delete(k);
31
- }
32
- }
33
- /**
34
- * Retrieve a cached value by key. Returns `null` if the key does not exist
35
- * or has expired.
36
- *
37
- * @param key - The cache key.
38
- * @returns The stored value cast to `T`, or `null`.
39
- */
40
- async get(key) {
41
- const entry = this.data.get(key);
42
- if (!entry)
43
- return null;
44
- if (entry.expiresAt && Date.now() > entry.expiresAt) {
45
- this.data.delete(key);
46
- return null;
47
- }
48
- return entry.value;
49
- }
50
- /**
51
- * Store a value under the given key with an optional TTL.
52
- *
53
- * @param key - The cache key.
54
- * @param value - The value to cache.
55
- * @param ttlSeconds - Time-to-live in seconds. Omit for no expiration.
56
- */
57
- async set(key, value, ttlSeconds) {
58
- this.data.set(key, {
59
- value,
60
- expiresAt: ttlSeconds ? Date.now() + ttlSeconds * 1000 : null,
61
- });
62
- // Periodically sweep expired entries so keys that are never read again don't
63
- // accumulate unbounded, then enforce the size cap.
64
- if (this.maxEntries > 0) {
65
- if (++this.writes % 256 === 0)
66
- this.prune();
67
- while (this.data.size > this.maxEntries) {
68
- const oldest = this.data.keys().next().value;
69
- if (oldest === undefined)
70
- break;
71
- this.data.delete(oldest);
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) {
82
- // Check the map directly so an explicitly stored `null` value still counts
83
- // as present (get() alone can't distinguish stored-null from absent).
84
- const entry = this.data.get(key);
85
- if (!entry)
86
- return false;
87
- if (entry.expiresAt && Date.now() > entry.expiresAt) {
88
- this.data.delete(key);
89
- return false;
90
- }
91
- return true;
92
- }
93
- /**
94
- * Delete a key from the store.
95
- *
96
- * @param key - The cache key to remove.
97
- * @returns `true` if the key was present and deleted.
98
- */
99
- async delete(key) {
100
- return this.data.delete(key);
101
- }
102
- /**
103
- * Remove all entries from the store.
104
- */
105
- async flush() {
106
- this.data.clear();
107
- }
108
- }
@@ -1,143 +0,0 @@
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
- const fullKey = this.prefix + key;
64
- if (ttlSeconds) {
65
- const sendable = this.redis;
66
- if (typeof sendable.send === "function") {
67
- // Atomic SET ... EX so a crash between SET and EXPIRE can never leave a
68
- // permanent (TTL-less) key behind.
69
- await sendable.send("SET", [fullKey, val, "EX", String(Math.floor(ttlSeconds))]);
70
- }
71
- else {
72
- // Fallback for clients without a raw `send`: best-effort two-step.
73
- await this.redis.set(fullKey, val);
74
- await this.redis.expire(fullKey, ttlSeconds);
75
- }
76
- }
77
- else {
78
- await this.redis.set(fullKey, val);
79
- }
80
- }
81
- /**
82
- * Check whether a key exists in Redis.
83
- *
84
- * @param key - The cache key (without prefix).
85
- * @returns `true` if the key exists.
86
- *
87
- * @example
88
- * ```ts
89
- * if (await store.has('session:abc')) { ... }
90
- * ```
91
- */
92
- async has(key) {
93
- // Coerces both shapes: @tekir/redis returns boolean, node-redis/ioredis
94
- // return number. `!!0` → false, `!!1` → true, `!!true` → true.
95
- return !!(await this.redis.exists(this.prefix + key));
96
- }
97
- /**
98
- * Delete a key from Redis.
99
- *
100
- * @param key - The cache key (without prefix) to remove.
101
- * @returns Always returns `true`.
102
- *
103
- * @example
104
- * ```ts
105
- * await store.delete('expired-token')
106
- * ```
107
- */
108
- async delete(key) {
109
- await this.redis.del(this.prefix + key);
110
- return true;
111
- }
112
- /**
113
- * Remove only this store's keys (those under its prefix) using a non-blocking
114
- * SCAN + DEL. This no longer flushes the entire Redis database, so data owned
115
- * by other stores sharing the same database (sessions, queues, ...) is left
116
- * intact.
117
- *
118
- * If the prefix is empty (which would match every key) this throws rather than
119
- * risk wiping unrelated data; configure a non-empty prefix to use flush.
120
- *
121
- * @example
122
- * ```ts
123
- * await store.flush()
124
- * ```
125
- */
126
- async flush() {
127
- if (!this.prefix) {
128
- throw new Error('[@tekir/cache] RedisCacheStore.flush() refused: an empty prefix would delete every key in the database. Configure a non-empty prefix.');
129
- }
130
- // Structural cast: callers may pass clients with varying `send` arg
131
- // types. We only need it to accept a string command and an array.
132
- const client = this.redis;
133
- const pattern = `${this.prefix}*`;
134
- let cursor = '0';
135
- do {
136
- const reply = (await client.send('SCAN', [cursor, 'MATCH', pattern, 'COUNT', '100']));
137
- const [next, batch] = reply;
138
- cursor = next;
139
- if (batch && batch.length)
140
- await client.send('DEL', batch);
141
- } while (cursor !== '0');
142
- }
143
- }
package/dist/types.js DELETED
@@ -1 +0,0 @@
1
- export {};