npm package: @onlineapps/conn-base-cache
Description: Redis cache connector with TTL, invalidation, and namespace support for OA Drive microservices
Author: OA Drive Team
Latest version: 1.0.9

# @onlineapps/conn-base-cache

Redis cache connector with TTL, invalidation, and namespace support for OA Drive microservices.

## Features

- 🔑 **Automatic key prefixing** - All keys use `cache:` prefix
- ⏱️ **TTL support** - Automatic expiration for cached data
- 🗂️ **Namespace isolation** - Separate cache spaces per service
- 📊 **Built-in statistics** - Hit rate, miss tracking
- 🔄 **Atomic operations** - Increment, decrement support
- 🎯 **Pattern invalidation** - Delete by wildcard patterns
- 🔐 **Safe namespacing** - Never touches `obs:` monitoring keys
- 🧪 **Mock implementation** - For testing

## Installation

```bash
npm install @onlineapps/conn-base-cache
```

## Quick Start

```javascript
const CacheConnector = require('@onlineapps/conn-base-cache');

const cache = new CacheConnector({
  host: '127.0.0.1',
  port: 6379,
  defaultTTL: 3600, // 1 hour
  namespace: 'invoice-service'
});

await cache.connect();

// Set value with TTL
await cache.set('user:123', { name: 'John', role: 'admin' }, 600); // 10 min

// Get value
const user = await cache.get('user:123');

// Delete by pattern
await cache.deleteByPattern('user:*');

// Get statistics
console.log(cache.getStats());
// { hits: 1, misses: 0, sets: 1, deletes: 5, errors: 0, hitRate: '100%' }
```

## Configuration

| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
| `host` | `REDIS_HOST` | **(required)** | Redis server host |
| `port` | `REDIS_PORT` | **(required)** | Redis server port |
| `password` | `REDIS_PASSWORD` | - | Redis password |
| `db` | `REDIS_DB` | `0` | Redis database number |
| `defaultTTL` | - | `3600` | Default TTL in seconds |
| `namespace` | - | - | Key namespace |
| `maxRetries` | - | `3` | Max connection retries |
| `retryDelay` | - | `100` | Retry delay in ms |

## API Reference

### Connection Management

#### `connect()`
Establish Redis connection.

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

#### `disconnect()`
Close Redis connection.

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

### Basic Operations

#### `get(key)`
Get value from cache.

```javascript
const value = await cache.get('key');
// Returns parsed object or string
```

#### `set(key, value, ttl?)`
Set value in cache with optional TTL.

```javascript
await cache.set('key', { data: 'value' }, 3600);
// Returns true on success
```

#### `delete(key)`
Delete single key.

```javascript
await cache.delete('key');
// Returns true if key existed
```

#### `exists(key)`
Check if key exists.

```javascript
const exists = await cache.exists('key');
// Returns boolean
```

### Pattern Operations

#### `deleteByPattern(pattern)`
Delete all keys matching pattern.

```javascript
const deleted = await cache.deleteByPattern('session:*');
// Returns number of deleted keys
```

### TTL Management

#### `ttl(key)`
Get remaining TTL for key.

```javascript
const seconds = await cache.ttl('key');
// Returns: seconds (-1 if no TTL, -2 if not exists)
```

#### `expire(key, ttl)`
Update TTL for existing key.

```javascript
await cache.expire('key', 7200); // 2 hours
// Returns true on success
```

### Atomic Operations

#### `incr(key, increment?)`
Increment numeric value.

```javascript
const newValue = await cache.incr('counter', 5);
// Returns new value
```

#### `decr(key, decrement?)`
Decrement numeric value.

```javascript
const newValue = await cache.decr('counter', 1);
// Returns new value
```

### Batch Operations

#### `mget(keys)`
Get multiple values at once.

```javascript
const values = await cache.mget(['key1', 'key2', 'key3']);
// Returns: { key1: value1, key2: value2, key3: value3 }
```

#### `mset(keyValues, ttl?)`
Set multiple values at once.

```javascript
await cache.mset({
  'key1': 'value1',
  'key2': { data: 'value2' }
}, 3600);
// Returns true on success
```

### Utility Methods

#### `withNamespace(namespace)`
Create namespaced instance.

```javascript
const userCache = cache.withNamespace('users');
await userCache.set('123', userData);
// Actual key: cache:invoice-service:users:123
```

#### `wrap(fn, options)`
Create cached function (memoization).

```javascript
const cachedFetch = cache.wrap(async (userId) => {
  return await fetchUserFromDB(userId);
}, {
  ttl: 600,
  keyPrefix: 'user'
});

const user = await cachedFetch(123);
// First call fetches from DB and caches
// Subsequent calls return from cache
```

#### `flush(confirm)`
Clear all cache keys (dangerous!).

```javascript
await cache.flush(true); // Must confirm with true
```

#### `getStats()`
Get cache statistics.

```javascript
const stats = cache.getStats();
// { hits: 50, misses: 10, sets: 30, deletes: 5, errors: 0, hitRate: '83.33%' }
```

#### `healthCheck()`
Check Redis connection health.

```javascript
const healthy = await cache.healthCheck();
// Returns boolean
```

## Usage Patterns

### Service-Level Caching

```javascript
class InvoiceService {
  constructor() {
    this.cache = new CacheConnector({
      namespace: 'invoice-service'
    });
  }

  async getInvoice(id) {
    // Check cache first
    const cached = await this.cache.get(`invoice:${id}`);
    if (cached) return cached;

    // Fetch from database
    const invoice = await db.getInvoice(id);

    // Cache for 1 hour
    await this.cache.set(`invoice:${id}`, invoice, 3600);

    return invoice;
  }

  async invalidateInvoice(id) {
    await this.cache.delete(`invoice:${id}`);
  }
}
```

### API Response Caching

```javascript
// In service wrapper
async function callApiWithCache(operation, params) {
  const cacheKey = `api:${operation}:${JSON.stringify(params)}`;

  // Try cache
  const cached = await cache.get(cacheKey);
  if (cached) {
    logger.debug('Cache hit for API call');
    return cached;
  }

  // Make API call
  const result = await apiCaller.call(operation, params);

  // Cache result
  await cache.set(cacheKey, result, 300); // 5 minutes

  return result;
}
```

### Session Management

```javascript
const sessionCache = cache.withNamespace('sessions');

// Store session
await sessionCache.set(sessionId, {
  userId: 123,
  roles: ['admin'],
  createdAt: Date.now()
}, 1800); // 30 minutes

// Refresh session TTL
await sessionCache.expire(sessionId, 1800);

// Clear all sessions for user
await sessionCache.deleteByPattern(`*:user:123`);
```

## Testing

Use the built-in mock for testing:

```javascript
const { MockCacheConnector } = require('@onlineapps/conn-base-cache');

describe('MyService', () => {
  let cache;

  beforeEach(() => {
    cache = new MockCacheConnector();
  });

  it('should cache results', async () => {
    await cache.set('test', 'value');
    const value = await cache.get('test');
    expect(value).toBe('value');
    expect(cache.getStats().hits).toBe(1);
  });
});
```

## Important Notes

1. **Key Prefixing**: All keys automatically get `cache:` prefix
2. **Namespace Isolation**: Use namespaces to avoid key collisions
3. **TTL Best Practices**: Always set appropriate TTL to prevent memory issues
4. **Pattern Deletion**: Use carefully as it can be expensive on large datasets
5. **Never Access**: This connector never touches `obs:` namespace (monitoring)

## Integration with Service Wrapper

```javascript
class ServiceWrapper {
  constructor(config) {
    this.cache = new CacheConnector({
      namespace: config.serviceName
    });
  }

  async processWorkflow(message) {
    // Cache workflow state
    await this.cache.set(
      `workflow:${message.workflowId}`,
      message,
      600 // 10 minutes
    );
  }
}
```

## Error Handling

All methods throw errors with descriptive messages:

```javascript
try {
  await cache.get('key');
} catch (error) {
  if (error.message.includes('Cache get failed')) {
    // Handle cache failure
  }
}
```

## Performance Tips

1. **Use mget/mset** for batch operations
2. **Enable pipelining** for multiple operations
3. **Set appropriate TTL** to balance freshness and performance
4. **Use namespaces** to organize keys
5. **Monitor hit rate** with getStats()

## License

MIT