@naman_deep_singh/cache 1.1.0 → 1.2.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 +62 -10
- package/dist/cjs/adapters/redis/RedisCache.js +54 -30
- package/dist/cjs/core/factory.js +10 -1
- package/dist/cjs/types.d.ts +19 -0
- package/dist/esm/adapters/redis/RedisCache.js +55 -31
- package/dist/esm/core/factory.js +10 -1
- package/dist/esm/types.d.ts +19 -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.2.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
|
});
|
|
@@ -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.flushDb) {
|
|
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,8 @@ 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
|
+
await this.client.sendCommand(['PING']);
|
|
257
281
|
return {
|
|
258
282
|
isAlive: true,
|
|
259
283
|
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':
|
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;
|
|
@@ -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.flushDb) {
|
|
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,8 @@ 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
|
+
await this.client.sendCommand(['PING']);
|
|
254
278
|
return {
|
|
255
279
|
isAlive: true,
|
|
256
280
|
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':
|
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;
|
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.2.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",
|