@db-bridge/redis 1.0.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 +21 -0
- package/README.md +468 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +1676 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Berke Erdoğan
|
|
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,468 @@
|
|
|
1
|
+
# @db-bridge/redis
|
|
2
|
+
|
|
3
|
+
Redis adapter and caching layer for DB Bridge - A comprehensive database management library.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @db-bridge/redis @db-bridge/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- Full Redis command support via ioredis
|
|
14
|
+
- Caching layer for database adapters
|
|
15
|
+
- Automatic cache invalidation
|
|
16
|
+
- Pub/Sub support
|
|
17
|
+
- Batch operations
|
|
18
|
+
- TTL management
|
|
19
|
+
- Key pattern matching
|
|
20
|
+
- TypeScript support
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### As a Cache Adapter
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { RedisAdapter } from '@db-bridge/redis';
|
|
28
|
+
|
|
29
|
+
const cache = new RedisAdapter({
|
|
30
|
+
keyPrefix: 'myapp:',
|
|
31
|
+
ttl: 3600, // Default TTL: 1 hour
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Connect
|
|
35
|
+
await cache.connect({
|
|
36
|
+
host: 'localhost',
|
|
37
|
+
port: 6379,
|
|
38
|
+
password: 'your-password',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Basic operations
|
|
42
|
+
await cache.set('user:123', { id: 123, name: 'John Doe' });
|
|
43
|
+
const user = await cache.get('user:123');
|
|
44
|
+
|
|
45
|
+
// Disconnect
|
|
46
|
+
await cache.disconnect();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### As a Database Cache Layer
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { MySQLAdapter } from '@db-bridge/mysql';
|
|
53
|
+
import { RedisAdapter, CachedAdapter } from '@db-bridge/redis';
|
|
54
|
+
|
|
55
|
+
// Setup adapters
|
|
56
|
+
const mysql = new MySQLAdapter();
|
|
57
|
+
const redis = new RedisAdapter({ keyPrefix: 'cache:' });
|
|
58
|
+
|
|
59
|
+
// Create cached adapter
|
|
60
|
+
const db = new CachedAdapter({
|
|
61
|
+
adapter: mysql,
|
|
62
|
+
cache: redis,
|
|
63
|
+
defaultTTL: 300, // 5 minutes
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Queries are automatically cached
|
|
67
|
+
const users = await db.query('SELECT * FROM users WHERE role = ?', ['admin'], {
|
|
68
|
+
cache: { key: 'admin-users', ttl: 600 },
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
|
+
|
|
74
|
+
### Redis Connection Options
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
interface RedisConnectionConfig {
|
|
78
|
+
host?: string;
|
|
79
|
+
port?: number;
|
|
80
|
+
password?: string;
|
|
81
|
+
db?: number;
|
|
82
|
+
username?: string;
|
|
83
|
+
sentinels?: Array<{ host: string; port: number }>;
|
|
84
|
+
name?: string; // Sentinel master name
|
|
85
|
+
family?: 4 | 6;
|
|
86
|
+
path?: string; // Unix socket
|
|
87
|
+
keepAlive?: number;
|
|
88
|
+
connectionName?: string;
|
|
89
|
+
enableReadyCheck?: boolean;
|
|
90
|
+
enableOfflineQueue?: boolean;
|
|
91
|
+
connectTimeout?: number;
|
|
92
|
+
autoResubscribe?: boolean;
|
|
93
|
+
autoResendUnfulfilledCommands?: boolean;
|
|
94
|
+
lazyConnect?: boolean;
|
|
95
|
+
tls?: ConnectionOptions;
|
|
96
|
+
keyPrefix?: string;
|
|
97
|
+
retryStrategy?: (times: number) => number | void | null;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Adapter Options
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
const redis = new RedisAdapter({
|
|
105
|
+
// Key prefix for all operations
|
|
106
|
+
keyPrefix: 'myapp:cache:',
|
|
107
|
+
|
|
108
|
+
// Default TTL in seconds
|
|
109
|
+
ttl: 3600,
|
|
110
|
+
|
|
111
|
+
// Logger instance
|
|
112
|
+
logger: console,
|
|
113
|
+
|
|
114
|
+
// Connection timeout
|
|
115
|
+
connectionTimeout: 5000,
|
|
116
|
+
|
|
117
|
+
// Command timeout
|
|
118
|
+
commandTimeout: 2500,
|
|
119
|
+
|
|
120
|
+
// Enable compression for large values
|
|
121
|
+
enableCompression: true,
|
|
122
|
+
|
|
123
|
+
// ioredis specific options
|
|
124
|
+
redis: {
|
|
125
|
+
maxRetriesPerRequest: 3,
|
|
126
|
+
enableAutoPipelining: true,
|
|
127
|
+
retryStrategy: (times) => Math.min(times * 50, 2000),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Usage
|
|
133
|
+
|
|
134
|
+
### Basic Cache Operations
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Set with TTL
|
|
138
|
+
await redis.set('session:abc123', { userId: 123, role: 'admin' }, 1800); // 30 minutes
|
|
139
|
+
|
|
140
|
+
// Get
|
|
141
|
+
const session = await redis.get('session:abc123');
|
|
142
|
+
|
|
143
|
+
// Check existence
|
|
144
|
+
const exists = await redis.exists('session:abc123');
|
|
145
|
+
|
|
146
|
+
// Delete
|
|
147
|
+
const deleted = await redis.delete('session:abc123');
|
|
148
|
+
|
|
149
|
+
// Get TTL
|
|
150
|
+
const ttl = await redis.ttl('session:abc123');
|
|
151
|
+
|
|
152
|
+
// Extend TTL
|
|
153
|
+
await redis.expire('session:abc123', 3600);
|
|
154
|
+
|
|
155
|
+
// Clear all with pattern
|
|
156
|
+
const keys = await redis.keys('session:*');
|
|
157
|
+
for (const key of keys) {
|
|
158
|
+
await redis.delete(key);
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Batch Operations
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
// Batch set
|
|
166
|
+
await redis.mset([
|
|
167
|
+
{ key: 'user:1', value: { name: 'Alice' }, ttl: 3600 },
|
|
168
|
+
{ key: 'user:2', value: { name: 'Bob' }, ttl: 3600 },
|
|
169
|
+
{ key: 'user:3', value: { name: 'Charlie' }, ttl: 3600 },
|
|
170
|
+
]);
|
|
171
|
+
|
|
172
|
+
// Batch get
|
|
173
|
+
const users = await redis.mget(['user:1', 'user:2', 'user:3']);
|
|
174
|
+
|
|
175
|
+
// Batch delete
|
|
176
|
+
await redis.mdel(['user:1', 'user:2', 'user:3']);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Atomic Operations
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// Increment/Decrement
|
|
183
|
+
const count = await redis.increment('page:views', 1);
|
|
184
|
+
const remaining = await redis.decrement('api:quota', 1);
|
|
185
|
+
|
|
186
|
+
// Atomic compare and swap
|
|
187
|
+
const key = 'resource:lock';
|
|
188
|
+
const token = 'unique-token';
|
|
189
|
+
const acquired = await redis.setnx(key, token, 30); // Lock for 30 seconds
|
|
190
|
+
|
|
191
|
+
if (acquired) {
|
|
192
|
+
try {
|
|
193
|
+
// Do work
|
|
194
|
+
} finally {
|
|
195
|
+
// Release lock only if we own it
|
|
196
|
+
const current = await redis.get(key);
|
|
197
|
+
if (current === token) {
|
|
198
|
+
await redis.delete(key);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Database Caching
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Create cached database adapter
|
|
208
|
+
const cachedDb = new CachedAdapter({
|
|
209
|
+
adapter: mysql,
|
|
210
|
+
cache: redis,
|
|
211
|
+
defaultTTL: 300,
|
|
212
|
+
strategy: 'lazy', // 'lazy' | 'eager' | 'refresh'
|
|
213
|
+
cacheableCommands: ['SELECT'], // Only cache SELECT queries
|
|
214
|
+
logger: console,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Automatic caching with custom key
|
|
218
|
+
const products = await cachedDb.query(
|
|
219
|
+
'SELECT * FROM products WHERE category = ?',
|
|
220
|
+
['electronics'],
|
|
221
|
+
{
|
|
222
|
+
cache: {
|
|
223
|
+
key: 'products:electronics',
|
|
224
|
+
ttl: 600,
|
|
225
|
+
tags: ['products', 'electronics'],
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
// Disable cache for specific query
|
|
231
|
+
const realtimeData = await cachedDb.query('SELECT * FROM active_sessions', [], { cache: false });
|
|
232
|
+
|
|
233
|
+
// Manual cache invalidation
|
|
234
|
+
const cacheManager = cachedDb.getCacheManager();
|
|
235
|
+
await cacheManager.invalidate(['products:electronics']);
|
|
236
|
+
await cacheManager.invalidateByTags(['products']);
|
|
237
|
+
await cacheManager.invalidateAll();
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Redis Commands
|
|
241
|
+
|
|
242
|
+
Access all Redis commands through the `commands` property:
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
const redis = new RedisAdapter();
|
|
246
|
+
const commands = redis.commands;
|
|
247
|
+
|
|
248
|
+
// Strings
|
|
249
|
+
await commands.set('key', 'value');
|
|
250
|
+
await commands.get('key');
|
|
251
|
+
await commands.mget('key1', 'key2');
|
|
252
|
+
await commands.incr('counter');
|
|
253
|
+
await commands.incrby('counter', 5);
|
|
254
|
+
|
|
255
|
+
// Hashes
|
|
256
|
+
await commands.hset('user:123', 'name', 'John');
|
|
257
|
+
await commands.hget('user:123', 'name');
|
|
258
|
+
await commands.hgetall('user:123');
|
|
259
|
+
await commands.hmset('user:123', { name: 'John', age: 30 });
|
|
260
|
+
|
|
261
|
+
// Lists
|
|
262
|
+
await commands.lpush('queue', 'task1', 'task2');
|
|
263
|
+
await commands.rpop('queue');
|
|
264
|
+
await commands.lrange('queue', 0, -1);
|
|
265
|
+
await commands.llen('queue');
|
|
266
|
+
|
|
267
|
+
// Sets
|
|
268
|
+
await commands.sadd('tags', 'nodejs', 'redis');
|
|
269
|
+
await commands.srem('tags', 'redis');
|
|
270
|
+
await commands.smembers('tags');
|
|
271
|
+
await commands.sismember('tags', 'nodejs');
|
|
272
|
+
await commands.scard('tags');
|
|
273
|
+
|
|
274
|
+
// Sorted Sets
|
|
275
|
+
await commands.zadd('leaderboard', 100, 'player1', 200, 'player2');
|
|
276
|
+
await commands.zrange('leaderboard', 0, -1);
|
|
277
|
+
await commands.zrevrange('leaderboard', 0, 9, 'WITHSCORES');
|
|
278
|
+
await commands.zscore('leaderboard', 'player1');
|
|
279
|
+
await commands.zrank('leaderboard', 'player1');
|
|
280
|
+
|
|
281
|
+
// Pub/Sub
|
|
282
|
+
await commands.subscribe('channel1', 'channel2');
|
|
283
|
+
await commands.publish('channel1', 'Hello World');
|
|
284
|
+
commands.on('message', (channel, message) => {
|
|
285
|
+
console.log(`Received ${message} from ${channel}`);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// Transactions
|
|
289
|
+
const multi = commands.multi();
|
|
290
|
+
multi.set('key1', 'value1');
|
|
291
|
+
multi.set('key2', 'value2');
|
|
292
|
+
multi.get('key1');
|
|
293
|
+
const results = await multi.exec();
|
|
294
|
+
|
|
295
|
+
// Lua Scripts
|
|
296
|
+
const script = `
|
|
297
|
+
local current = redis.call('get', KEYS[1])
|
|
298
|
+
if current == ARGV[1] then
|
|
299
|
+
redis.call('set', KEYS[1], ARGV[2])
|
|
300
|
+
return 1
|
|
301
|
+
else
|
|
302
|
+
return 0
|
|
303
|
+
end
|
|
304
|
+
`;
|
|
305
|
+
const result = await commands.eval(script, 1, 'key', 'oldValue', 'newValue');
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Advanced Features
|
|
309
|
+
|
|
310
|
+
### Cache Invalidation Strategies
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// Tag-based invalidation
|
|
314
|
+
await cachedDb.query('SELECT * FROM products WHERE category = ?', ['electronics'], {
|
|
315
|
+
cache: { tags: ['products', 'electronics'] },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Invalidate by tag
|
|
319
|
+
await cacheManager.invalidateByTags(['electronics']);
|
|
320
|
+
|
|
321
|
+
// Pattern-based invalidation
|
|
322
|
+
await cacheManager.invalidatePattern('products:*');
|
|
323
|
+
|
|
324
|
+
// Automatic invalidation on mutations
|
|
325
|
+
const result = await cachedDb.execute('UPDATE products SET price = ? WHERE id = ?', [99.99, 123]); // Automatically invalidates related cache
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Cache Warming
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
// Pre-warm cache on startup
|
|
332
|
+
const warmupQueries = [
|
|
333
|
+
{ sql: 'SELECT * FROM config', key: 'app:config', ttl: 86400 },
|
|
334
|
+
{ sql: 'SELECT * FROM categories', key: 'categories:all', ttl: 3600 },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
for (const query of warmupQueries) {
|
|
338
|
+
const result = await mysql.query(query.sql);
|
|
339
|
+
await redis.set(query.key, result.rows, query.ttl);
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### Monitoring and Statistics
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// Cache statistics
|
|
347
|
+
const stats = cacheManager.getStatistics();
|
|
348
|
+
console.log('Cache hit rate:', stats.hitRate);
|
|
349
|
+
console.log('Total hits:', stats.hits);
|
|
350
|
+
console.log('Total misses:', stats.misses);
|
|
351
|
+
console.log('Total cached:', stats.totalCached);
|
|
352
|
+
|
|
353
|
+
// Redis info
|
|
354
|
+
const info = await redis.info();
|
|
355
|
+
console.log('Memory usage:', info.memory.used_memory_human);
|
|
356
|
+
console.log('Connected clients:', info.clients.connected_clients);
|
|
357
|
+
|
|
358
|
+
// Monitor slow queries
|
|
359
|
+
redis.on('slowlog', (log) => {
|
|
360
|
+
console.warn('Slow Redis command:', log);
|
|
361
|
+
});
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Clustering Support
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
// Redis Cluster
|
|
368
|
+
const redis = new RedisAdapter({
|
|
369
|
+
cluster: [
|
|
370
|
+
{ host: 'redis1', port: 6379 },
|
|
371
|
+
{ host: 'redis2', port: 6379 },
|
|
372
|
+
{ host: 'redis3', port: 6379 },
|
|
373
|
+
],
|
|
374
|
+
clusterOptions: {
|
|
375
|
+
enableReadyCheck: true,
|
|
376
|
+
maxRedirections: 16,
|
|
377
|
+
retryDelayOnFailover: 100,
|
|
378
|
+
retryDelayOnClusterDown: 300,
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Sentinel
|
|
383
|
+
const redis = new RedisAdapter({
|
|
384
|
+
sentinels: [
|
|
385
|
+
{ host: 'sentinel1', port: 26379 },
|
|
386
|
+
{ host: 'sentinel2', port: 26379 },
|
|
387
|
+
{ host: 'sentinel3', port: 26379 },
|
|
388
|
+
],
|
|
389
|
+
name: 'mymaster',
|
|
390
|
+
sentinelPassword: 'sentinel-password',
|
|
391
|
+
});
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
## Error Handling
|
|
395
|
+
|
|
396
|
+
```typescript
|
|
397
|
+
import { ConnectionError, CacheError } from '@db-bridge/redis';
|
|
398
|
+
|
|
399
|
+
try {
|
|
400
|
+
await redis.connect(config);
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof ConnectionError) {
|
|
403
|
+
console.error('Redis connection failed:', error.message);
|
|
404
|
+
// Fallback to database-only mode
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Handle cache misses gracefully
|
|
409
|
+
try {
|
|
410
|
+
const data = await redis.get('key');
|
|
411
|
+
if (!data) {
|
|
412
|
+
// Cache miss - fetch from database
|
|
413
|
+
const dbData = await fetchFromDatabase();
|
|
414
|
+
await redis.set('key', dbData, 300);
|
|
415
|
+
return dbData;
|
|
416
|
+
}
|
|
417
|
+
return data;
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (error instanceof CacheError) {
|
|
420
|
+
// Log error but continue with database
|
|
421
|
+
console.error('Cache error:', error);
|
|
422
|
+
return fetchFromDatabase();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Best Practices
|
|
428
|
+
|
|
429
|
+
1. **Use Key Prefixes**: Namespace your keys to avoid collisions
|
|
430
|
+
2. **Set TTLs**: Always set appropriate TTLs to prevent memory bloat
|
|
431
|
+
3. **Handle Cache Misses**: Implement fallback to database
|
|
432
|
+
4. **Monitor Memory**: Keep track of Redis memory usage
|
|
433
|
+
5. **Use Pipelining**: Batch operations when possible
|
|
434
|
+
6. **Implement Circuit Breaker**: Protect against Redis failures
|
|
435
|
+
|
|
436
|
+
## TypeScript Support
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
interface CachedUser {
|
|
440
|
+
id: number;
|
|
441
|
+
name: string;
|
|
442
|
+
email: string;
|
|
443
|
+
preferences: {
|
|
444
|
+
theme: 'light' | 'dark';
|
|
445
|
+
notifications: boolean;
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Type-safe cache operations
|
|
450
|
+
await redis.set<CachedUser>('user:123', {
|
|
451
|
+
id: 123,
|
|
452
|
+
name: 'John Doe',
|
|
453
|
+
email: 'john@example.com',
|
|
454
|
+
preferences: {
|
|
455
|
+
theme: 'dark',
|
|
456
|
+
notifications: true,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const user = await redis.get<CachedUser>('user:123');
|
|
461
|
+
if (user) {
|
|
462
|
+
console.log(user.preferences.theme); // TypeScript knows the type
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
## License
|
|
467
|
+
|
|
468
|
+
MIT © Berke Erdoğan
|