@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 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 (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
  });
@@ -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
- else {
94
- this.recordHit();
95
- try {
94
+ this.recordHit();
95
+ try {
96
+ if (typeof data === 'string') {
96
97
  resolve(this.deserialize(data));
97
98
  }
98
- catch (parseErr) {
99
- reject(parseErr);
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
- if (fullKey in data) {
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
- result[key] = this.deserialize(data[fullKey]);
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 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 && 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
- await this.client.ping();
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',
@@ -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':
@@ -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
- const responseData = typeof data === 'string' ? data : JSON.stringify(data);
86
- cache.set(cacheKey, responseData, ttl).catch((err) => {
87
- console.error('Failed to cache response:', err);
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;
@@ -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
- else {
88
- this.recordHit();
89
- try {
88
+ this.recordHit();
89
+ try {
90
+ if (typeof data === 'string') {
90
91
  resolve(this.deserialize(data));
91
92
  }
92
- catch (parseErr) {
93
- reject(parseErr);
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
- if (fullKey in data) {
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
- result[key] = this.deserialize(data[fullKey]);
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 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 && 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
- await this.client.ping();
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',
@@ -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':
@@ -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
- const responseData = typeof data === 'string' ? data : JSON.stringify(data);
81
- cache.set(cacheKey, responseData, ttl).catch((err) => {
82
- console.error('Failed to cache response:', err);
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
  }
@@ -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
  }
@@ -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.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",