@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 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 (v2 planned) | Yes | N/A |
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 = new MemoryCache({ adapter: 'memory' });
620
+ const devCache = CacheFactory.create({ adapter: 'memory' });
574
621
 
575
622
  // Testing: mock external services
576
- const testCache = new MemoryCache({ adapter: 'memory' });
623
+ const testCache = CacheFactory.create({ adapter: 'memory' });
577
624
 
578
- // Production: single server, high performance
579
- const prodCache = new RedisCache({
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
- // High-traffic distributed system
586
- const distributedCache = new RedisCache({
633
+ // Production cluster: distributed Redis cluster
634
+ const clusterCache = CacheFactory.create({
587
635
  adapter: 'redis',
588
- host: process.env.REDIS_CLUSTER_ENDPOINT
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
- // Legacy system with Memcache
592
- const legacyCache = new MemcacheCache({
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 options = {
23
- host: this.redisConfig.host ?? 'localhost',
24
- port: this.redisConfig.port ?? 6379,
25
- db: this.redisConfig.db ?? 0
26
- };
27
- if (this.redisConfig.username) {
28
- options.username = this.redisConfig.username;
29
- }
30
- if (this.redisConfig.password) {
31
- options.password = this.redisConfig.password;
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
- if (this.redisConfig.tls) {
34
- options.tls = true;
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 = (0, redis_1.createClient)(options);
37
- this.client.on('error', (err) => {
38
- this.isConnected = false;
39
- console.error('Redis connection error:', err);
40
- });
41
- this.client.on('connect', () => {
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
- // Clear only keys with the current namespace
136
- const pattern = `${this.namespace}*`;
137
- const keys = await this.client.keys(pattern);
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
- await this.client.flushDb();
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
- await this.client.ping();
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',
@@ -15,7 +15,16 @@ class CacheFactory {
15
15
  static create(config) {
16
16
  switch (config.adapter) {
17
17
  case 'redis':
18
- return new redis_1.RedisCache(config);
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':
@@ -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 options = {
20
- host: this.redisConfig.host ?? 'localhost',
21
- port: this.redisConfig.port ?? 6379,
22
- db: this.redisConfig.db ?? 0
23
- };
24
- if (this.redisConfig.username) {
25
- options.username = this.redisConfig.username;
26
- }
27
- if (this.redisConfig.password) {
28
- options.password = this.redisConfig.password;
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
- if (this.redisConfig.tls) {
31
- options.tls = true;
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 = createClient(options);
34
- this.client.on('error', (err) => {
35
- this.isConnected = false;
36
- console.error('Redis connection error:', err);
37
- });
38
- this.client.on('connect', () => {
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
- // Clear only keys with the current namespace
133
- const pattern = `${this.namespace}*`;
134
- const keys = await this.client.keys(pattern);
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
- await this.client.flushDb();
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
- await this.client.ping();
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',
@@ -12,7 +12,16 @@ export class CacheFactory {
12
12
  static create(config) {
13
13
  switch (config.adapter) {
14
14
  case 'redis':
15
- return new RedisCache(config);
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':
@@ -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;
@@ -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.1.0",
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",