@naman_deep_singh/cache 1.1.0 → 1.3.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/README.md +68 -10
- package/dist/cjs/adapters/memcache/MemcacheCache.js +42 -12
- package/dist/cjs/adapters/redis/RedisCache.js +60 -30
- package/dist/cjs/core/factory.js +10 -1
- package/dist/cjs/middleware/express/cacheMiddleware.js +17 -4
- package/dist/cjs/session/SessionStore.d.ts +6 -0
- package/dist/cjs/session/SessionStore.js +8 -0
- package/dist/cjs/types.d.ts +19 -0
- package/dist/esm/adapters/memcache/MemcacheCache.js +42 -12
- package/dist/esm/adapters/redis/RedisCache.js +61 -31
- package/dist/esm/core/factory.js +10 -1
- package/dist/esm/middleware/express/cacheMiddleware.js +17 -4
- package/dist/esm/session/SessionStore.d.ts +6 -0
- package/dist/esm/session/SessionStore.js +8 -0
- package/dist/esm/types.d.ts +19 -0
- package/dist/types/session/SessionStore.d.ts +6 -0
- package/dist/types/types.d.ts +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @naman_deep_singh/cache
|
|
2
2
|
|
|
3
|
+
**Version:** 1.3.0 (with Redis Clustering support)
|
|
4
|
+
|
|
3
5
|
A flexible, extensible caching layer with support for Redis, Memcache, and in-memory caches. Includes session management, health checks, and Express middleware.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -208,6 +210,51 @@ await cache.delete('key');
|
|
|
208
210
|
// ... and all other methods
|
|
209
211
|
```
|
|
210
212
|
|
|
213
|
+
### Redis Cluster Support
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
import { CacheFactory } from '@naman_deep_singh/cache';
|
|
217
|
+
|
|
218
|
+
// Single cluster configuration with array notation
|
|
219
|
+
const clusterCache = CacheFactory.create({
|
|
220
|
+
adapter: 'redis',
|
|
221
|
+
cluster: [
|
|
222
|
+
{ host: 'redis-node-1.example.com', port: 6379 },
|
|
223
|
+
{ host: 'redis-node-2.example.com', port: 6379 },
|
|
224
|
+
{ host: 'redis-node-3.example.com', port: 6379 }
|
|
225
|
+
],
|
|
226
|
+
namespace: 'myapp',
|
|
227
|
+
ttl: 3600
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Or with detailed cluster config
|
|
231
|
+
const clusterCacheAlt = CacheFactory.create({
|
|
232
|
+
adapter: 'redis',
|
|
233
|
+
cluster: {
|
|
234
|
+
nodes: [
|
|
235
|
+
{ host: 'redis-node-1.example.com', port: 6379 },
|
|
236
|
+
{ host: 'redis-node-2.example.com', port: 6379 }
|
|
237
|
+
],
|
|
238
|
+
options: {
|
|
239
|
+
enableReadyCheck: true,
|
|
240
|
+
maxRedirections: 3,
|
|
241
|
+
retryDelayOnFailover: 100,
|
|
242
|
+
retryDelayOnClusterDown: 300
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Use exactly like single instance - same ICache interface
|
|
248
|
+
await clusterCache.set('key', value);
|
|
249
|
+
const data = await clusterCache.get('key');
|
|
250
|
+
await clusterCache.delete('key');
|
|
251
|
+
|
|
252
|
+
// Cluster automatically handles key distribution across nodes
|
|
253
|
+
// No changes needed to your application logic
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Note:** Cannot mix single-instance (`host`/`port`) and cluster (`cluster`) config. Choose one or the other.
|
|
257
|
+
|
|
211
258
|
### Memcache Adapter
|
|
212
259
|
|
|
213
260
|
```typescript
|
|
@@ -563,33 +610,38 @@ All adapters implement the same `ICache<T>` interface and work identically. Choo
|
|
|
563
610
|
| **Best For** | Production (distributed systems) | High-traffic scenarios | Development & testing |
|
|
564
611
|
| **Cost** | Free (open source) | Free (open source) | Free |
|
|
565
612
|
| **Performance** | Fast | Very Fast | Fastest (in-memory) |
|
|
566
|
-
| **Cluster Support** | Yes (
|
|
613
|
+
| **Cluster Support** | Yes (v1.2+) | Yes | N/A |
|
|
567
614
|
| **Authentication** | Username/Password/TLS | Optional | N/A |
|
|
568
615
|
|
|
569
616
|
### When to Use Each
|
|
570
617
|
|
|
571
618
|
```typescript
|
|
572
619
|
// Development: quick setup, no dependencies
|
|
573
|
-
const devCache =
|
|
620
|
+
const devCache = CacheFactory.create({ adapter: 'memory' });
|
|
574
621
|
|
|
575
622
|
// Testing: mock external services
|
|
576
|
-
const testCache =
|
|
623
|
+
const testCache = CacheFactory.create({ adapter: 'memory' });
|
|
577
624
|
|
|
578
|
-
// Production: single server, high performance
|
|
579
|
-
const prodCache =
|
|
625
|
+
// Production single instance: single server, high performance
|
|
626
|
+
const prodCache = CacheFactory.create({
|
|
580
627
|
adapter: 'redis',
|
|
581
628
|
host: process.env.REDIS_HOST,
|
|
629
|
+
port: process.env.REDIS_PORT ? parseInt(process.env.REDIS_PORT) : 6379,
|
|
582
630
|
password: process.env.REDIS_PASSWORD
|
|
583
631
|
});
|
|
584
632
|
|
|
585
|
-
//
|
|
586
|
-
const
|
|
633
|
+
// Production cluster: distributed Redis cluster
|
|
634
|
+
const clusterCache = CacheFactory.create({
|
|
587
635
|
adapter: 'redis',
|
|
588
|
-
|
|
636
|
+
cluster: [
|
|
637
|
+
{ host: 'redis-node-1', port: 6379 },
|
|
638
|
+
{ host: 'redis-node-2', port: 6379 },
|
|
639
|
+
{ host: 'redis-node-3', port: 6379 }
|
|
640
|
+
]
|
|
589
641
|
});
|
|
590
642
|
|
|
591
|
-
//
|
|
592
|
-
const
|
|
643
|
+
// High-traffic with Memcache multi-server
|
|
644
|
+
const memcacheCache = CacheFactory.create({
|
|
593
645
|
adapter: 'memcache',
|
|
594
646
|
servers: ['memcache1:11211', 'memcache2:11211']
|
|
595
647
|
});
|
|
@@ -635,3 +687,9 @@ ISC
|
|
|
635
687
|
## Author
|
|
636
688
|
|
|
637
689
|
Naman Deep Singh
|
|
690
|
+
|
|
691
|
+
## TypeScript Notes
|
|
692
|
+
|
|
693
|
+
- The package provides strong generics for `ICache<T>` and `SessionStore` so you can store typed values safely.
|
|
694
|
+
- Middleware in this package may attach runtime properties (for example session data) to `Request` objects in Express. If you're using TypeScript, import the package into your project so the included type augmentations are picked up automatically — no need to cast `req` to `any` in most cases.
|
|
695
|
+
|
|
@@ -86,19 +86,28 @@ class MemcacheCache extends BaseCache_1.BaseCache {
|
|
|
86
86
|
reject(err);
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
if (data === undefined) {
|
|
89
|
+
if (data === undefined || data === null) {
|
|
90
90
|
this.recordMiss();
|
|
91
91
|
resolve(null);
|
|
92
|
+
return;
|
|
92
93
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
this.recordHit();
|
|
95
|
+
try {
|
|
96
|
+
if (typeof data === 'string') {
|
|
96
97
|
resolve(this.deserialize(data));
|
|
97
98
|
}
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
else if (Buffer.isBuffer(data)) {
|
|
100
|
+
resolve(this.deserialize(data.toString()));
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
// Unknown shape from memcached client - treat as miss
|
|
104
|
+
this.recordMiss();
|
|
105
|
+
resolve(null);
|
|
100
106
|
}
|
|
101
107
|
}
|
|
108
|
+
catch (parseErr) {
|
|
109
|
+
reject(parseErr);
|
|
110
|
+
}
|
|
102
111
|
});
|
|
103
112
|
});
|
|
104
113
|
}
|
|
@@ -201,21 +210,42 @@ class MemcacheCache extends BaseCache_1.BaseCache {
|
|
|
201
210
|
return;
|
|
202
211
|
}
|
|
203
212
|
const result = {};
|
|
213
|
+
if (!data || typeof data !== 'object') {
|
|
214
|
+
// Treat as all misses
|
|
215
|
+
for (const key of keys) {
|
|
216
|
+
this.recordMiss();
|
|
217
|
+
result[key] = null;
|
|
218
|
+
}
|
|
219
|
+
resolve(result);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const map = data;
|
|
204
223
|
keys.forEach((key) => {
|
|
205
224
|
const fullKey = this.buildKey(key);
|
|
206
|
-
|
|
225
|
+
const value = map[fullKey];
|
|
226
|
+
if (value === undefined || value === null) {
|
|
227
|
+
this.recordMiss();
|
|
228
|
+
result[key] = null;
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
207
231
|
this.recordHit();
|
|
208
232
|
try {
|
|
209
|
-
|
|
233
|
+
if (typeof value === 'string') {
|
|
234
|
+
result[key] = this.deserialize(value);
|
|
235
|
+
}
|
|
236
|
+
else if (Buffer.isBuffer(value)) {
|
|
237
|
+
result[key] = this.deserialize(value.toString());
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
// Unknown, treat as miss
|
|
241
|
+
this.recordMiss();
|
|
242
|
+
result[key] = null;
|
|
243
|
+
}
|
|
210
244
|
}
|
|
211
245
|
catch (parseErr) {
|
|
212
246
|
reject(parseErr);
|
|
213
247
|
}
|
|
214
248
|
}
|
|
215
|
-
else {
|
|
216
|
-
this.recordMiss();
|
|
217
|
-
result[key] = null;
|
|
218
|
-
}
|
|
219
249
|
});
|
|
220
250
|
resolve(result);
|
|
221
251
|
});
|
|
@@ -19,30 +19,50 @@ class RedisCache extends BaseCache_1.BaseCache {
|
|
|
19
19
|
*/
|
|
20
20
|
async connect() {
|
|
21
21
|
try {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
const cluster = this.redisConfig.cluster;
|
|
23
|
+
const hasCluster = cluster && (Array.isArray(cluster) ? cluster.length > 0 : cluster.nodes?.length > 0);
|
|
24
|
+
if (hasCluster && cluster) {
|
|
25
|
+
// Cluster mode
|
|
26
|
+
let nodes = [];
|
|
27
|
+
if (Array.isArray(cluster)) {
|
|
28
|
+
nodes = cluster;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
nodes = cluster.nodes;
|
|
32
|
+
}
|
|
33
|
+
this.client = (0, redis_1.createCluster)({
|
|
34
|
+
rootNodes: nodes.map(node => ({ url: `redis://${node.host}:${node.port}` }))
|
|
35
|
+
});
|
|
32
36
|
}
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
else {
|
|
38
|
+
// Single instance mode
|
|
39
|
+
const options = {
|
|
40
|
+
host: this.redisConfig.host ?? 'localhost',
|
|
41
|
+
port: this.redisConfig.port ?? 6379,
|
|
42
|
+
db: this.redisConfig.db ?? 0
|
|
43
|
+
};
|
|
44
|
+
if (this.redisConfig.username) {
|
|
45
|
+
options.username = this.redisConfig.username;
|
|
46
|
+
}
|
|
47
|
+
if (this.redisConfig.password) {
|
|
48
|
+
options.password = this.redisConfig.password;
|
|
49
|
+
}
|
|
50
|
+
if (this.redisConfig.tls) {
|
|
51
|
+
options.tls = true;
|
|
52
|
+
}
|
|
53
|
+
this.client = (0, redis_1.createClient)(options);
|
|
35
54
|
}
|
|
36
|
-
this.client
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
55
|
+
if (this.client) {
|
|
56
|
+
this.client.on('error', (err) => {
|
|
57
|
+
this.isConnected = false;
|
|
58
|
+
console.error('Redis connection error:', err);
|
|
59
|
+
});
|
|
60
|
+
this.client.on('connect', () => {
|
|
61
|
+
this.isConnected = true;
|
|
62
|
+
});
|
|
63
|
+
await this.client.connect();
|
|
42
64
|
this.isConnected = true;
|
|
43
|
-
}
|
|
44
|
-
await this.client.connect();
|
|
45
|
-
this.isConnected = true;
|
|
65
|
+
}
|
|
46
66
|
}
|
|
47
67
|
catch (err) {
|
|
48
68
|
throw new errors_1.CacheError('Failed to connect to Redis', 'REDIS_CONNECTION_ERROR', 'redis', err);
|
|
@@ -132,16 +152,19 @@ class RedisCache extends BaseCache_1.BaseCache {
|
|
|
132
152
|
try {
|
|
133
153
|
await this.ensureConnected();
|
|
134
154
|
if (this.namespace) {
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (keys.length > 0) {
|
|
139
|
-
await this.client.del(keys);
|
|
140
|
-
}
|
|
155
|
+
// For cluster mode, we can't use FLUSHDB, so we skip clearing in cluster
|
|
156
|
+
// In production, use explicit key tracking or Redis ACL scoping
|
|
157
|
+
console.warn('Cluster mode: namespace clear requires explicit key tracking');
|
|
141
158
|
}
|
|
142
159
|
else {
|
|
143
|
-
// Clear all keys
|
|
144
|
-
|
|
160
|
+
// Clear all keys only in single-instance mode
|
|
161
|
+
const client = this.client;
|
|
162
|
+
if (client && typeof client.flushDb === 'function') {
|
|
163
|
+
await client.flushDb();
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
console.warn('Clear operation not supported in cluster mode');
|
|
167
|
+
}
|
|
145
168
|
}
|
|
146
169
|
}
|
|
147
170
|
catch (err) {
|
|
@@ -253,7 +276,14 @@ class RedisCache extends BaseCache_1.BaseCache {
|
|
|
253
276
|
async isAlive() {
|
|
254
277
|
try {
|
|
255
278
|
await this.ensureConnected();
|
|
256
|
-
|
|
279
|
+
// Use sendCommand which works for both single and cluster
|
|
280
|
+
// `sendCommand` exists on both single and cluster clients in runtime; cast narrowly for the call
|
|
281
|
+
if (this.client && typeof this.client.sendCommand === 'function') {
|
|
282
|
+
await this.client.sendCommand(['PING']);
|
|
283
|
+
}
|
|
284
|
+
else if (this.client && typeof this.client.ping === 'function') {
|
|
285
|
+
await this.client.ping();
|
|
286
|
+
}
|
|
257
287
|
return {
|
|
258
288
|
isAlive: true,
|
|
259
289
|
adapter: 'redis',
|
package/dist/cjs/core/factory.js
CHANGED
|
@@ -15,7 +15,16 @@ class CacheFactory {
|
|
|
15
15
|
static create(config) {
|
|
16
16
|
switch (config.adapter) {
|
|
17
17
|
case 'redis':
|
|
18
|
-
|
|
18
|
+
const redisConfig = config;
|
|
19
|
+
// Validate: can't use both single + cluster
|
|
20
|
+
if (redisConfig.host && redisConfig.cluster) {
|
|
21
|
+
throw new errors_1.CacheError('Cannot specify both host and cluster config', 'INVALID_CONFIG');
|
|
22
|
+
}
|
|
23
|
+
// Require either single or cluster
|
|
24
|
+
if (!redisConfig.host && !redisConfig.cluster) {
|
|
25
|
+
throw new errors_1.CacheError('Redis requires either host or cluster config', 'INVALID_CONFIG');
|
|
26
|
+
}
|
|
27
|
+
return new redis_1.RedisCache(redisConfig);
|
|
19
28
|
case 'memcache':
|
|
20
29
|
return new memcache_1.MemcacheCache(config);
|
|
21
30
|
case 'memory':
|
|
@@ -82,10 +82,23 @@ function cacheResponseMiddleware(cache, options) {
|
|
|
82
82
|
if (res.statusCode >= 200 &&
|
|
83
83
|
res.statusCode < 300 &&
|
|
84
84
|
!excludeStatusCodes.includes(res.statusCode)) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
85
|
+
let responseData = null;
|
|
86
|
+
if (typeof data === 'string') {
|
|
87
|
+
responseData = data;
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
try {
|
|
91
|
+
responseData = JSON.stringify(data);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
responseData = null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (responseData !== null) {
|
|
98
|
+
cache.set(cacheKey, responseData, ttl).catch((err) => {
|
|
99
|
+
console.error('Failed to cache response:', err);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
89
102
|
}
|
|
90
103
|
res.set('X-Cache', 'MISS');
|
|
91
104
|
return originalSend.call(this, data);
|
|
@@ -48,4 +48,10 @@ export declare class SessionStore {
|
|
|
48
48
|
* Get session data and extend expiry in one operation
|
|
49
49
|
*/
|
|
50
50
|
getAndExtend(sessionId: string, ttl?: number): Promise<SessionData | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Optional close hook for graceful shutdowns.
|
|
53
|
+
* Present to allow callers to call `close()` during shutdown without
|
|
54
|
+
* requiring every store implementation to provide one.
|
|
55
|
+
*/
|
|
56
|
+
close(): Promise<void>;
|
|
51
57
|
}
|
|
@@ -149,5 +149,13 @@ class SessionStore {
|
|
|
149
149
|
throw new errors_1.CacheError(`Failed to get and extend session "${sessionId}"`, 'SESSION_GET_EXTEND_ERROR', 'session', err);
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Optional close hook for graceful shutdowns.
|
|
154
|
+
* Present to allow callers to call `close()` during shutdown without
|
|
155
|
+
* requiring every store implementation to provide one.
|
|
156
|
+
*/
|
|
157
|
+
async close() {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
152
160
|
}
|
|
153
161
|
exports.SessionStore = SessionStore;
|
package/dist/cjs/types.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface CacheConfig {
|
|
|
7
7
|
ttl?: number;
|
|
8
8
|
fallback?: boolean;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Redis-cluster configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface RedisClusterConfig {
|
|
14
|
+
nodes: Array<{
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
}>;
|
|
18
|
+
options?: {
|
|
19
|
+
enableReadyCheck?: boolean;
|
|
20
|
+
maxRedirections?: number;
|
|
21
|
+
retryDelayOnFailover?: number;
|
|
22
|
+
retryDelayOnClusterDown?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Redis-specific configuration
|
|
12
27
|
*/
|
|
@@ -14,6 +29,10 @@ export interface RedisCacheConfig extends CacheConfig {
|
|
|
14
29
|
adapter: 'redis';
|
|
15
30
|
host?: string;
|
|
16
31
|
port?: number;
|
|
32
|
+
cluster?: RedisClusterConfig | Array<{
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
}>;
|
|
17
36
|
username?: string;
|
|
18
37
|
password?: string;
|
|
19
38
|
db?: number;
|
|
@@ -80,19 +80,28 @@ export class MemcacheCache extends BaseCache {
|
|
|
80
80
|
reject(err);
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
83
|
-
if (data === undefined) {
|
|
83
|
+
if (data === undefined || data === null) {
|
|
84
84
|
this.recordMiss();
|
|
85
85
|
resolve(null);
|
|
86
|
+
return;
|
|
86
87
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
this.recordHit();
|
|
89
|
+
try {
|
|
90
|
+
if (typeof data === 'string') {
|
|
90
91
|
resolve(this.deserialize(data));
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
else if (Buffer.isBuffer(data)) {
|
|
94
|
+
resolve(this.deserialize(data.toString()));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
// Unknown shape from memcached client - treat as miss
|
|
98
|
+
this.recordMiss();
|
|
99
|
+
resolve(null);
|
|
94
100
|
}
|
|
95
101
|
}
|
|
102
|
+
catch (parseErr) {
|
|
103
|
+
reject(parseErr);
|
|
104
|
+
}
|
|
96
105
|
});
|
|
97
106
|
});
|
|
98
107
|
}
|
|
@@ -195,21 +204,42 @@ export class MemcacheCache extends BaseCache {
|
|
|
195
204
|
return;
|
|
196
205
|
}
|
|
197
206
|
const result = {};
|
|
207
|
+
if (!data || typeof data !== 'object') {
|
|
208
|
+
// Treat as all misses
|
|
209
|
+
for (const key of keys) {
|
|
210
|
+
this.recordMiss();
|
|
211
|
+
result[key] = null;
|
|
212
|
+
}
|
|
213
|
+
resolve(result);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const map = data;
|
|
198
217
|
keys.forEach((key) => {
|
|
199
218
|
const fullKey = this.buildKey(key);
|
|
200
|
-
|
|
219
|
+
const value = map[fullKey];
|
|
220
|
+
if (value === undefined || value === null) {
|
|
221
|
+
this.recordMiss();
|
|
222
|
+
result[key] = null;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
201
225
|
this.recordHit();
|
|
202
226
|
try {
|
|
203
|
-
|
|
227
|
+
if (typeof value === 'string') {
|
|
228
|
+
result[key] = this.deserialize(value);
|
|
229
|
+
}
|
|
230
|
+
else if (Buffer.isBuffer(value)) {
|
|
231
|
+
result[key] = this.deserialize(value.toString());
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
// Unknown, treat as miss
|
|
235
|
+
this.recordMiss();
|
|
236
|
+
result[key] = null;
|
|
237
|
+
}
|
|
204
238
|
}
|
|
205
239
|
catch (parseErr) {
|
|
206
240
|
reject(parseErr);
|
|
207
241
|
}
|
|
208
242
|
}
|
|
209
|
-
else {
|
|
210
|
-
this.recordMiss();
|
|
211
|
-
result[key] = null;
|
|
212
|
-
}
|
|
213
243
|
});
|
|
214
244
|
resolve(result);
|
|
215
245
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createClient } from 'redis';
|
|
1
|
+
import { createClient, createCluster } from 'redis';
|
|
2
2
|
import { BaseCache } from '../../core/BaseCache';
|
|
3
3
|
import { CacheError } from '../../errors';
|
|
4
4
|
/**
|
|
@@ -16,30 +16,50 @@ export class RedisCache extends BaseCache {
|
|
|
16
16
|
*/
|
|
17
17
|
async connect() {
|
|
18
18
|
try {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
19
|
+
const cluster = this.redisConfig.cluster;
|
|
20
|
+
const hasCluster = cluster && (Array.isArray(cluster) ? cluster.length > 0 : cluster.nodes?.length > 0);
|
|
21
|
+
if (hasCluster && cluster) {
|
|
22
|
+
// Cluster mode
|
|
23
|
+
let nodes = [];
|
|
24
|
+
if (Array.isArray(cluster)) {
|
|
25
|
+
nodes = cluster;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
nodes = cluster.nodes;
|
|
29
|
+
}
|
|
30
|
+
this.client = createCluster({
|
|
31
|
+
rootNodes: nodes.map(node => ({ url: `redis://${node.host}:${node.port}` }))
|
|
32
|
+
});
|
|
29
33
|
}
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
else {
|
|
35
|
+
// Single instance mode
|
|
36
|
+
const options = {
|
|
37
|
+
host: this.redisConfig.host ?? 'localhost',
|
|
38
|
+
port: this.redisConfig.port ?? 6379,
|
|
39
|
+
db: this.redisConfig.db ?? 0
|
|
40
|
+
};
|
|
41
|
+
if (this.redisConfig.username) {
|
|
42
|
+
options.username = this.redisConfig.username;
|
|
43
|
+
}
|
|
44
|
+
if (this.redisConfig.password) {
|
|
45
|
+
options.password = this.redisConfig.password;
|
|
46
|
+
}
|
|
47
|
+
if (this.redisConfig.tls) {
|
|
48
|
+
options.tls = true;
|
|
49
|
+
}
|
|
50
|
+
this.client = createClient(options);
|
|
32
51
|
}
|
|
33
|
-
this.client
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
52
|
+
if (this.client) {
|
|
53
|
+
this.client.on('error', (err) => {
|
|
54
|
+
this.isConnected = false;
|
|
55
|
+
console.error('Redis connection error:', err);
|
|
56
|
+
});
|
|
57
|
+
this.client.on('connect', () => {
|
|
58
|
+
this.isConnected = true;
|
|
59
|
+
});
|
|
60
|
+
await this.client.connect();
|
|
39
61
|
this.isConnected = true;
|
|
40
|
-
}
|
|
41
|
-
await this.client.connect();
|
|
42
|
-
this.isConnected = true;
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
64
|
catch (err) {
|
|
45
65
|
throw new CacheError('Failed to connect to Redis', 'REDIS_CONNECTION_ERROR', 'redis', err);
|
|
@@ -129,16 +149,19 @@ export class RedisCache extends BaseCache {
|
|
|
129
149
|
try {
|
|
130
150
|
await this.ensureConnected();
|
|
131
151
|
if (this.namespace) {
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (keys.length > 0) {
|
|
136
|
-
await this.client.del(keys);
|
|
137
|
-
}
|
|
152
|
+
// For cluster mode, we can't use FLUSHDB, so we skip clearing in cluster
|
|
153
|
+
// In production, use explicit key tracking or Redis ACL scoping
|
|
154
|
+
console.warn('Cluster mode: namespace clear requires explicit key tracking');
|
|
138
155
|
}
|
|
139
156
|
else {
|
|
140
|
-
// Clear all keys
|
|
141
|
-
|
|
157
|
+
// Clear all keys only in single-instance mode
|
|
158
|
+
const client = this.client;
|
|
159
|
+
if (client && typeof client.flushDb === 'function') {
|
|
160
|
+
await client.flushDb();
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
console.warn('Clear operation not supported in cluster mode');
|
|
164
|
+
}
|
|
142
165
|
}
|
|
143
166
|
}
|
|
144
167
|
catch (err) {
|
|
@@ -250,7 +273,14 @@ export class RedisCache extends BaseCache {
|
|
|
250
273
|
async isAlive() {
|
|
251
274
|
try {
|
|
252
275
|
await this.ensureConnected();
|
|
253
|
-
|
|
276
|
+
// Use sendCommand which works for both single and cluster
|
|
277
|
+
// `sendCommand` exists on both single and cluster clients in runtime; cast narrowly for the call
|
|
278
|
+
if (this.client && typeof this.client.sendCommand === 'function') {
|
|
279
|
+
await this.client.sendCommand(['PING']);
|
|
280
|
+
}
|
|
281
|
+
else if (this.client && typeof this.client.ping === 'function') {
|
|
282
|
+
await this.client.ping();
|
|
283
|
+
}
|
|
254
284
|
return {
|
|
255
285
|
isAlive: true,
|
|
256
286
|
adapter: 'redis',
|
package/dist/esm/core/factory.js
CHANGED
|
@@ -12,7 +12,16 @@ export class CacheFactory {
|
|
|
12
12
|
static create(config) {
|
|
13
13
|
switch (config.adapter) {
|
|
14
14
|
case 'redis':
|
|
15
|
-
|
|
15
|
+
const redisConfig = config;
|
|
16
|
+
// Validate: can't use both single + cluster
|
|
17
|
+
if (redisConfig.host && redisConfig.cluster) {
|
|
18
|
+
throw new CacheError('Cannot specify both host and cluster config', 'INVALID_CONFIG');
|
|
19
|
+
}
|
|
20
|
+
// Require either single or cluster
|
|
21
|
+
if (!redisConfig.host && !redisConfig.cluster) {
|
|
22
|
+
throw new CacheError('Redis requires either host or cluster config', 'INVALID_CONFIG');
|
|
23
|
+
}
|
|
24
|
+
return new RedisCache(redisConfig);
|
|
16
25
|
case 'memcache':
|
|
17
26
|
return new MemcacheCache(config);
|
|
18
27
|
case 'memory':
|
|
@@ -77,10 +77,23 @@ export function cacheResponseMiddleware(cache, options) {
|
|
|
77
77
|
if (res.statusCode >= 200 &&
|
|
78
78
|
res.statusCode < 300 &&
|
|
79
79
|
!excludeStatusCodes.includes(res.statusCode)) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
80
|
+
let responseData = null;
|
|
81
|
+
if (typeof data === 'string') {
|
|
82
|
+
responseData = data;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
try {
|
|
86
|
+
responseData = JSON.stringify(data);
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
responseData = null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (responseData !== null) {
|
|
93
|
+
cache.set(cacheKey, responseData, ttl).catch((err) => {
|
|
94
|
+
console.error('Failed to cache response:', err);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
84
97
|
}
|
|
85
98
|
res.set('X-Cache', 'MISS');
|
|
86
99
|
return originalSend.call(this, data);
|
|
@@ -48,4 +48,10 @@ export declare class SessionStore {
|
|
|
48
48
|
* Get session data and extend expiry in one operation
|
|
49
49
|
*/
|
|
50
50
|
getAndExtend(sessionId: string, ttl?: number): Promise<SessionData | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Optional close hook for graceful shutdowns.
|
|
53
|
+
* Present to allow callers to call `close()` during shutdown without
|
|
54
|
+
* requiring every store implementation to provide one.
|
|
55
|
+
*/
|
|
56
|
+
close(): Promise<void>;
|
|
51
57
|
}
|
|
@@ -146,4 +146,12 @@ export class SessionStore {
|
|
|
146
146
|
throw new CacheError(`Failed to get and extend session "${sessionId}"`, 'SESSION_GET_EXTEND_ERROR', 'session', err);
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Optional close hook for graceful shutdowns.
|
|
151
|
+
* Present to allow callers to call `close()` during shutdown without
|
|
152
|
+
* requiring every store implementation to provide one.
|
|
153
|
+
*/
|
|
154
|
+
async close() {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
149
157
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface CacheConfig {
|
|
|
7
7
|
ttl?: number;
|
|
8
8
|
fallback?: boolean;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Redis-cluster configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface RedisClusterConfig {
|
|
14
|
+
nodes: Array<{
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
}>;
|
|
18
|
+
options?: {
|
|
19
|
+
enableReadyCheck?: boolean;
|
|
20
|
+
maxRedirections?: number;
|
|
21
|
+
retryDelayOnFailover?: number;
|
|
22
|
+
retryDelayOnClusterDown?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Redis-specific configuration
|
|
12
27
|
*/
|
|
@@ -14,6 +29,10 @@ export interface RedisCacheConfig extends CacheConfig {
|
|
|
14
29
|
adapter: 'redis';
|
|
15
30
|
host?: string;
|
|
16
31
|
port?: number;
|
|
32
|
+
cluster?: RedisClusterConfig | Array<{
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
}>;
|
|
17
36
|
username?: string;
|
|
18
37
|
password?: string;
|
|
19
38
|
db?: number;
|
|
@@ -48,4 +48,10 @@ export declare class SessionStore {
|
|
|
48
48
|
* Get session data and extend expiry in one operation
|
|
49
49
|
*/
|
|
50
50
|
getAndExtend(sessionId: string, ttl?: number): Promise<SessionData | null>;
|
|
51
|
+
/**
|
|
52
|
+
* Optional close hook for graceful shutdowns.
|
|
53
|
+
* Present to allow callers to call `close()` during shutdown without
|
|
54
|
+
* requiring every store implementation to provide one.
|
|
55
|
+
*/
|
|
56
|
+
close(): Promise<void>;
|
|
51
57
|
}
|
package/dist/types/types.d.ts
CHANGED
|
@@ -7,6 +7,21 @@ export interface CacheConfig {
|
|
|
7
7
|
ttl?: number;
|
|
8
8
|
fallback?: boolean;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Redis-cluster configuration
|
|
12
|
+
*/
|
|
13
|
+
export interface RedisClusterConfig {
|
|
14
|
+
nodes: Array<{
|
|
15
|
+
host: string;
|
|
16
|
+
port: number;
|
|
17
|
+
}>;
|
|
18
|
+
options?: {
|
|
19
|
+
enableReadyCheck?: boolean;
|
|
20
|
+
maxRedirections?: number;
|
|
21
|
+
retryDelayOnFailover?: number;
|
|
22
|
+
retryDelayOnClusterDown?: number;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
10
25
|
/**
|
|
11
26
|
* Redis-specific configuration
|
|
12
27
|
*/
|
|
@@ -14,6 +29,10 @@ export interface RedisCacheConfig extends CacheConfig {
|
|
|
14
29
|
adapter: 'redis';
|
|
15
30
|
host?: string;
|
|
16
31
|
port?: number;
|
|
32
|
+
cluster?: RedisClusterConfig | Array<{
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
}>;
|
|
17
36
|
username?: string;
|
|
18
37
|
password?: string;
|
|
19
38
|
db?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@naman_deep_singh/cache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Extensible caching layer supporting Redis, Memcache, and in-memory caches with automatic fallback, namespacing, session management, and Express middleware.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|