@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.
- package/dist/http-cache.d.ts +2 -1
- package/dist/index.js +538 -6
- package/dist/stores/database.d.ts +3 -0
- package/package.json +4 -4
- package/src/http-cache.ts +52 -14
- package/src/stores/database.ts +21 -12
- package/dist/cache.js +0 -151
- package/dist/http-cache.js +0 -198
- package/dist/provider.js +0 -94
- package/dist/stores/database.js +0 -129
- package/dist/stores/memory.js +0 -108
- package/dist/stores/redis.js +0 -143
- package/dist/types.js +0 -1
package/dist/stores/database.js
DELETED
|
@@ -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
|
-
}
|
package/dist/stores/memory.js
DELETED
|
@@ -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
|
-
}
|
package/dist/stores/redis.js
DELETED
|
@@ -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 {};
|