npm package: @pesalink/cache-package
Description: Shared cache package that allows microservices to set, get and delete cache data
Author: Nicholas Kute
Latest version: 1.0.25

# CacheService

A comprehensive Redis caching service with support for multiple data structures, circuit breaker pattern, and connection pooling. This service provides a singleton pattern implementation with built-in error handling and monitoring capabilities.

## Features

- **Singleton Pattern**: Ensures single Redis connection instance
- **Circuit Breaker**: Protects against Redis failures with automatic recovery
- **Multiple Data Structures**: Supports strings, hashes, JSON, lists, and sets
- **Connection Pooling**: Configurable concurrency limits and retry strategies
- **Environment Isolation**: Automatic key prefixing based on environment
- **Comprehensive Error Handling**: Timeout protection and graceful degradation
- **Health Monitoring**: Built-in health checks and connection monitoring


# Installation

To use the CacheService in your microservice, follow these steps:

```
npm install @pesalink/cache-package
```
```

## Environment Variables

```env
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_USER=username              # Optional
REDIS_PASSWORD=password          # Optional
REDIS_TLS=true                   # Optional, enables TLS
REDIS_CLUSTER=true               # Optional, enables cluster mode
REDIS_MAX_CONCURRENT_OPS=100     # Optional, default: 100
REDIS_CACHE_TIMEOUT=10000        # Optional, default: 10000ms
NODE_ENV=production              # Optional, used for key prefixing
ENVIRONMENT=production           # Alternative to NODE_ENV
```

## Usage

### Basic Setup

```javascript
const CacheService = require('./CacheService');

// Get singleton instance
const cache = CacheService.getInstance();

// Health check
const isHealthy = await cache.healthCheck();
console.log('Redis is healthy:', isHealthy);
```

### Key Building

The service automatically builds environment-prefixed keys from objects:

```javascript
const keyObj = { userId: 123, type: 'profile' };
// Generates key: "production|type:profile|userId:123"
```

## API Reference

### String Methods

#### `getCache(keyObj)`
Retrieves cached data stored as JSON string.

```javascript
const keyObj = { userId: 123, type: 'profile' };
const userData = await cache.getCache(keyObj);
console.log(userData); // null or parsed JSON object
```

#### `setCache(keyObj, value, expiryInSeconds = 3600)`
Stores data as JSON string with optional expiry.

```javascript
const keyObj = { userId: 123, type: 'profile' };
const userData = { name: 'John', email: 'john@example.com' };
await cache.setCache(keyObj, userData, 7200); // 2 hours expiry
```

### Hash Methods

#### `setHashCache(keyObj, hashData, expiryInSeconds?)`
Stores data as Redis hash fields.

```javascript
const keyObj = { userId: 123, type: 'session' };
const sessionData = {
  token: 'abc123',
  lastActive: '2023-01-01T00:00:00Z',
  ipAddress: '192.168.1.1'
};
await cache.setHashCache(keyObj, sessionData, 3600);
```

#### `getHashCache(keyObj)`
Retrieves all fields from a Redis hash.

```javascript
const keyObj = { userId: 123, type: 'session' };
const sessionData = await cache.getHashCache(keyObj);
console.log(sessionData); // { token: 'abc123', lastActive: '...', ipAddress: '...' }
```

#### `getHashFields(keyObj, fields)`
Retrieves specific fields from a Redis hash.

```javascript
const keyObj = { userId: 123, type: 'session' };
const fields = ['token', 'lastActive'];
const partialData = await cache.getHashFields(keyObj, fields);
console.log(partialData); // { token: 'abc123', lastActive: '...' }
```

### JSON Methods (Requires RedisJSON Module)

#### `setJsonCache(keyObj, jsonData, expiryInSeconds?)`
Stores data using Redis JSON module.

```javascript
const keyObj = { userId: 123, type: 'preferences' };
const preferences = {
  theme: 'dark',
  notifications: { email: true, sms: false },
  settings: { language: 'en', timezone: 'UTC' }
};
await cache.setJsonCache(keyObj, preferences, 86400); // 24 hours
```

#### `getJsonCache(keyObj)`
Retrieves JSON data from Redis.

```javascript
const keyObj = { userId: 123, type: 'preferences' };
const preferences = await cache.getJsonCache(keyObj);
console.log(preferences); // Full JSON object
```

#### `getJsonPaths(keyObj, paths)`
Retrieves specific JSON paths.

```javascript
const keyObj = { userId: 123, type: 'preferences' };
const paths = ['$.theme', '$.notifications.email'];
const partialPrefs = await cache.getJsonPaths(keyObj, paths);
```

### List Methods

#### `pushToList(keyObj, items, expiryInSeconds?, left = false)`
Adds items to a Redis list.

```javascript
const keyObj = { userId: 123, type: 'activity_log' };
const activities = [
  { action: 'login', timestamp: new Date() },
  { action: 'view_profile', timestamp: new Date() }
];

// Push to right (RPUSH)
await cache.pushToList(keyObj, activities, 604800); // 7 days

// Push to left (LPUSH)
await cache.pushToList(keyObj, activities, 604800, true);
```

#### `getListRange(keyObj, start = 0, stop = -1)`
Retrieves items from a Redis list.

```javascript
const keyObj = { userId: 123, type: 'activity_log' };

// Get all items
const allActivities = await cache.getListRange(keyObj);

// Get first 10 items
const recentActivities = await cache.getListRange(keyObj, 0, 9);

// Get last 5 items
const lastActivities = await cache.getListRange(keyObj, -5, -1);
```

### Set Methods

#### `addToSet(keyObj, items, expiryInSeconds?)`
Adds items to a Redis set.

```javascript
const keyObj = { userId: 123, type: 'viewed_products' };
const productIds = [101, 102, 103, 104];
await cache.addToSet(keyObj, productIds, 2592000); // 30 days
```

#### `getSetMembers(keyObj)`
Retrieves all members from a Redis set.

```javascript
const keyObj = { userId: 123, type: 'viewed_products' };
const viewedProducts = await cache.getSetMembers(keyObj);
console.log(viewedProducts); // [101, 102, 103, 104]
```

### Utility Methods

#### `getCachedData(cacheKey, dbFetchMethod, expiryInSeconds = 3600)`
Implements cache-aside pattern with automatic fallback.

```javascript
const keyObj = { userId: 123, type: 'profile' };

const userData = await cache.getCachedData(
  keyObj,
  async () => {
    // This function runs only if cache miss
    return await database.getUserProfile(123);
  },
  7200 // 2 hours cache
);
```

#### `deleteCache(keyObj)`
Deletes cached data.

```javascript
const keyObj = { userId: 123, type: 'profile' };
await cache.deleteCache(keyObj);
```

#### `invalidateCache(keyObj)`
Alias for `deleteCache()`.

```javascript
const keyObj = { userId: 123, type: 'profile' };
await cache.invalidateCache(keyObj);
```

#### `healthCheck()`
Performs Redis health check.

```javascript
const isHealthy = await cache.healthCheck();
if (!isHealthy) {
  console.error('Redis is not responding');
}
```

#### `disconnect()`
Gracefully closes Redis connection.

```javascript
await cache.disconnect();
```

## Error Handling

The service includes comprehensive error handling:

- **Circuit Breaker**: Automatically opens when Redis is unreachable
- **Operation Timeouts**: All operations have configurable timeouts
- **Retry Logic**: Built-in retry strategy for connection failures
- **Graceful Degradation**: Operations fail fast when circuit is open

```javascript
try {
  const data = await cache.getCache(keyObj);
} catch (error) {
  if (error.message === 'Redis circuit breaker is open') {
    // Handle circuit breaker state
    console.log('Redis is temporarily unavailable');
  } else if (error.message === 'Redis operation limit reached') {
    // Handle concurrency limit
    console.log('Too many concurrent operations');
  } else {
    // Handle other Redis errors
    console.error('Redis operation failed:', error);
  }
}
```

## Best Practices

### 1. Key Design
```javascript
// Good: Hierarchical and descriptive
const keyObj = { 
  service: 'user-service',
  userId: 123, 
  type: 'profile',
  version: 'v1'
};

// Avoid: Generic or unclear keys
const keyObj = { id: 123, data: 'stuff' };
```

### 2. Expiration Strategy
```javascript
// Different TTLs for different data types
await cache.setCache(userProfile, userData, 3600);      // 1 hour
await cache.setCache(sessionData, session, 1800);       // 30 minutes  
await cache.setCache(staticConfig, config, 86400);      // 24 hours
```

### 3. Error Handling
```javascript
// Always handle cache failures gracefully
async function getUserData(userId) {
  try {
    const cached = await cache.getCache({ userId, type: 'profile' });
    if (cached) return cached;
  } catch (error) {
    logger.warn('Cache read failed, falling back to database', error);
  }
  
  // Always have a fallback
  return await database.getUser(userId);
}
```

### 4. Monitoring
```javascript
// Regular health checks
setInterval(async () => {
  const healthy = await cache.healthCheck();
  if (!healthy) {
    // Alert or take corrective action
    logger.error('Redis health check failed');
  }
}, 30000); // Every 30 seconds
```

## Data Structure Selection Guide

| Use Case | Data Structure | Method |
|----------|----------------|---------|
| Simple key-value | String | `getCache()`, `setCache()` |
| Object with multiple fields | Hash | `getHashCache()`, `setHashCache()` |
| Complex nested objects | JSON | `getJsonCache()`, `setJsonCache()` |
| Ordered collections | List | `pushToList()`, `getListRange()` |
| Unique collections | Set | `addToSet()`, `getSetMembers()` |

## Performance Considerations

- **Connection Pooling**: Singleton pattern ensures efficient connection usage
- **Concurrent Operations**: Configurable limits prevent Redis overload
- **Timeouts**: All operations have timeout protection
- **JSON vs Hash vs String**: Choose based on access patterns:
  - Use **Hash** for partial field access
  - Use **JSON** for complex queries and updates
  - Use **String** for simple objects

## License

This service is part of the PesaLink package ecosystem.