npm package: @hemia/cache-manager
Description: Manejador de caché con soporte Redis y inversify para aplicaciones TypeScript.
Author: Hemia Technologies
Latest version: 0.0.5

# @hemia/cache-manager

Una librería sencilla y extensible para manejar caché utilizando **Redis** a través de `ioredis`. Totalmente independiente de frameworks de inyección de dependencias, pero compatible con **InversifyJS** y otros sistemas DI.

---

## Características

- ✨ **Independiente**: Sin dependencias de frameworks DI
- 🔧 **Flexible**: Úsalo con o sin inyección de dependencias
- 🚀 **TypeScript**: Completamente tipado
- 📦 **Ligero**: Solo depende de `ioredis`
- 🔌 **Compatible**: Funciona con InversifyJS, Awilix, TSyringe, etc.

---

## Instalación

```bash
npm install @hemia/cache-manager
```

---

## Uso Básico (sin inyección de dependencias)

```ts
import { CacheClient, CacheService } from '@hemia/cache-manager';

// 1. Crear el cliente de Redis
const cacheClient = new CacheClient({
  host: 'localhost',
  port: 6379,
  password: 'your-password',
  db: 0,
  tls: false,
});

// 2. Crear el servicio de cache
const cacheService = new CacheService(cacheClient);

// 3. Usar el servicio
async function example() {
  // Guardar un string
  await cacheService.setString('user:123', 'John Doe', 3600);
  
  // Obtener un string
  const name = await cacheService.getString('user:123');
  console.log(name); // 'John Doe'
  
  // Guardar un objeto
  await cacheService.setObject('user:data:123', { id: 123, name: 'John' }, 3600);
  
  // Obtener un objeto
  const user = await cacheService.getObject('user:data:123');
  console.log(user); // { id: 123, name: 'John' }
}
```

---

## Uso con InversifyJS

### 1. Instalación adicional

```bash
npm install inversify reflect-metadata
```

### 2. Crear módulo de cache en tu backend

Crea `src/infrastructure/cache/CacheModule.ts`:

```ts
import { ContainerModule, interfaces } from 'inversify';
import { CacheClient, CacheService } from '@hemia/cache-manager';
import type { ICacheClient, ICacheService, RedisConfig } from '@hemia/cache-manager';

export const CACHE_TYPES = {
  RedisConfig: Symbol.for('RedisConfig'),
  CacheClient: Symbol.for('CacheClient'),
  CacheService: Symbol.for('CacheService'),
};

export const createCacheModule = (config: RedisConfig): ContainerModule => {
  return new ContainerModule((bind: interfaces.Bind) => {
    bind<RedisConfig>(CACHE_TYPES.RedisConfig).toConstantValue(config);
    
    bind<ICacheClient>(CACHE_TYPES.CacheClient)
      .toDynamicValue(() => new CacheClient(config))
      .inSingletonScope();
    
    bind<ICacheService>(CACHE_TYPES.CacheService)
      .toDynamicValue((context) => {
        const cacheClient = context.container.get<ICacheClient>(CACHE_TYPES.CacheClient);
        return new CacheService(cacheClient);
      })
      .inSingletonScope();
  });
};
```

### 3. Configurar el contenedor

Crea `src/infrastructure/inversify.config.ts`:

```ts
import 'reflect-metadata';
import { Container } from 'inversify';
import { createCacheModule } from './cache/CacheModule';

const container = new Container();

// Cargar el módulo de cache
container.load(
  createCacheModule({
    host: process.env.REDIS_HOST || 'localhost',
    port: parseInt(process.env.REDIS_PORT || '6379'),
    password: process.env.REDIS_PASSWORD,
    db: 0,
    tls: process.env.REDIS_TLS === 'true',
  })
);

export { container };
```

### 4. Usar en tus servicios

```ts
import { injectable, inject } from 'inversify';
import type { ICacheService } from '@hemia/cache-manager';
import { CACHE_TYPES } from '../infrastructure/cache/CacheModule';

interface User {
  id: string;
  name: string;
  email: string;
}

@injectable()
export class UserService {
  constructor(
    @inject(CACHE_TYPES.CacheService) private cacheService: ICacheService
  ) {}

  async getUserById(id: string): Promise<User | null> {
    const cacheKey = `user:${id}`;
    
    // Intentar obtener del cache
    const cachedUser = await this.cacheService.getObject<User>(cacheKey);
    if (cachedUser) {
      return cachedUser;
    }

    // Si no está en cache, obtener de la base de datos
    const user = await this.fetchFromDatabase(id);
    
    if (user) {
      // Guardar en cache por 1 hora
      await this.cacheService.setObject(cacheKey, user, 3600);
    }
    
    return user;
  }

  async updateUser(id: string, data: Partial<User>): Promise<User> {
    const user = await this.database.update(id, data);
    
    // Invalidar el cache
    await this.cacheService.deleteKey(`user:${id}`);
    
    return user;
  }
}
```

---

## API Reference

### CacheService

#### Strings y Números

```ts
// Guardar string o número con expiración opcional (en segundos)
await cacheService.setString(key: string, value: string | number, expireTime?: number): Promise<void>

// Obtener string
await cacheService.getString(key: string): Promise<string | null>
```

#### Objetos

```ts
// Guardar objeto con expiración opcional
await cacheService.setObject<T>(key: string, value: T, expireTime?: number): Promise<void>

// Obtener objeto tipado
await cacheService.getObject<T>(key: string): Promise<T | null>
```

#### Listas

```ts
// Agregar elemento a una lista (LPUSH)
await cacheService.setList(key: string, value: string | number): Promise<void>

// Obtener elementos de una lista (LRANGE)
await cacheService.getList(key: string, start?: number, stop?: number): Promise<string[]>
```

#### Operaciones Generales

```ts
// Eliminar una clave
await cacheService.deleteKey(key: string): Promise<void>

// Establecer tiempo de expiración
await cacheService.expire(key: string, seconds: number): Promise<void>

// Verificar si una clave existe
await cacheService.exists(key: string): Promise<boolean>

// Incrementar un contador
await cacheService.increment(key: string): Promise<number>

// Decrementar un contador
await cacheService.decrement(key: string): Promise<number>

// Buscar claves por patrón (usar con precaución en producción)
await cacheService.getKeysByPattern(pattern: string): Promise<string[]>

// Obtener tiempo de vida restante (TTL)
// Retorna: -2 si no existe, -1 si existe sin expiración, segundos restantes
await cacheService.getTTL(key: string): Promise<number>
```

#### Conjuntos (Sets)

```ts
// Agregar uno o más elementos a un conjunto
await cacheService.addToSet(key: string, ...members: (string | number)[]): Promise<number>

// Eliminar uno o más elementos de un conjunto
await cacheService.removeFromSet(key: string, ...members: (string | number)[]): Promise<number>

// Obtener todos los miembros de un conjunto
await cacheService.getSetMembers(key: string): Promise<string[]>

// Verificar si un elemento pertenece a un conjunto
await cacheService.isSetMember(key: string, member: string | number): Promise<boolean>

// Obtener el número de elementos en un conjunto
await cacheService.getSetSize(key: string): Promise<number>
```

#### Transacciones (Multi)

```ts
// Ejecutar múltiples comandos de forma atómica
cacheService.multi()
  .set(key1, value1, 'EX', ttl)
  .sadd(key2, member)
  .del(key3)
  .exec();
```

---

## Ejemplos de Uso

### Contador de visitas

```ts
async function trackPageView(pageId: string) {
  const key = `page:views:${pageId}`;
  const views = await cacheService.increment(key);
  await cacheService.expire(key, 86400); // Expira en 24 horas
  return views;
}
```

### Cache de sesiones

```ts
interface Session {
  userId: string;
  token: string;
  expiresAt: Date;
}

async function saveSession(sessionId: string, session: Session) {
  await cacheService.setObject(`session:${sessionId}`, session, 3600); // 1 hora
}

async function getSession(sessionId: string): Promise<Session | null> {
  return await cacheService.getObject<Session>(`session:${sessionId}`);
}
```

### Cache con patrón Cache-Aside

```ts
async function getUser(userId: string) {
  const cacheKey = `user:${userId}`;
  
  // 1. Intentar obtener del cache
  const cached = await cacheService.getObject<User>(cacheKey);
  if (cached) {
    return cached;
  }
  
  // 2. Si no está, obtener de la base de datos
  const user = await database.users.findById(userId);
  
  // 3. Guardar en cache para futuras consultas
  if (user) {
    await cacheService.setObject(cacheKey, user, 1800); // 30 minutos
  }
  
  return user;
}
```

### Invalidación de cache

```ts
async function updateUser(userId: string, data: Partial<User>) {
  // Actualizar en la base de datos
  const user = await database.users.update(userId, data);
  
  // Invalidar cache
  await cacheService.deleteKey(`user:${userId}`);
  
  // Invalidar caches relacionados
  await cacheService.deleteKey(`user:profile:${userId}`);
  
  return user;
}
```

### Rate Limiting

```ts
async function checkRateLimit(userId: string, maxRequests: number = 100): Promise<boolean> {
  const key = `rate:${userId}`;
  const current = await cacheService.increment(key);
  
  if (current === 1) {
    // Primera request, establecer expiración de 1 minuto
    await cacheService.expire(key, 60);
  }
  
  return current <= maxRequests;
}
```

### Gestión de Sesiones con Sets

```ts
interface Session {
  userId: string;
  token: string;
  createdAt: Date;
}

// Crear sesión y agregar al set del usuario
async function createSession(userId: string, sessionId: string, sessionData: Session) {
  const sessionKey = `session:${sessionId}`;
  const userSessionsKey = `user:${userId}:sessions`;
  
  // Usar transacción atómica para garantizar consistencia
  await cacheService.multi()
    .set(sessionKey, JSON.stringify(sessionData), 'EX', 60 * 60 * 24 * 14) // 14 días
    .sadd(userSessionsKey, sessionId)
    .exec();
}

// Eliminar sesión y actualizar el set del usuario
async function deleteSession(sessionId: string) {
  const rawSession = await cacheService.getString(`session:${sessionId}`);
  
  if (rawSession) {
    const { userId } = JSON.parse(rawSession);
    await cacheService.deleteKey(`session:${sessionId}`);
    await cacheService.removeFromSet(`user:${userId}:sessions`, sessionId);
  }
}

// Obtener todas las sesiones activas de un usuario
async function getUserSessions(userId: string): Promise<string[]> {
  return await cacheService.getSetMembers(`user:${userId}:sessions`);
}

// Cerrar todas las sesiones de un usuario
async function revokeAllUserSessions(userId: string) {
  const sessions = await cacheService.getSetMembers(`user:${userId}:sessions`);
  
  for (const sessionId of sessions) {
    await cacheService.deleteKey(`session:${sessionId}`);
  }
  
  await cacheService.deleteKey(`user:${userId}:sessions`);
}
```

### Seguimiento de Usuarios Activos

```ts
// Agregar usuario activo
async function markUserActive(userId: string) {
  await cacheService.addToSet('users:active', userId);
  await cacheService.expire('users:active', 3600); // Resetear TTL a 1 hora
}

// Obtener número de usuarios activos
async function getActiveUsersCount(): Promise<number> {
  return await cacheService.getSetSize('users:active');
}

// Verificar si un usuario está activo
async function isUserActive(userId: string): Promise<boolean> {
  return await cacheService.isSetMember('users:active', userId);
}

// Obtener lista de usuarios activos
async function getActiveUsers(): Promise<string[]> {
  return await cacheService.getSetMembers('users:active');
}
```

### Operaciones Atómicas con Multi

```ts
// Transferencia de puntos entre usuarios (atómico)
async function transferPoints(fromUserId: string, toUserId: string, points: number) {
  const fromKey = `user:${fromUserId}:points`;
  const toKey = `user:${toUserId}:points`;
  
  await cacheService.multi()
    .decrby(fromKey, points)
    .incrby(toKey, points)
    .exec();
}

// Registrar evento con múltiples operaciones
async function logUserEvent(userId: string, eventType: string) {
  const eventKey = `events:${eventType}`;
  const userEventsKey = `user:${userId}:events`;
  const statsKey = `stats:events:${eventType}`;
  
  await cacheService.multi()
    .lpush(eventKey, userId)
    .sadd(userEventsKey, eventType)
    .incr(statsKey)
    .expire(eventKey, 86400) // 24 horas
    .exec();
}
```

---

## Configuración de Redis

```ts
interface RedisConfig {
  host: string;          // Dirección del servidor Redis
  port: number;          // Puerto (por defecto 6379)
  username?: string;     // Usuario (Redis 6+)
  password?: string;     // Contraseña
  db: number;            // Número de base de datos (0-15)
  tls?: boolean;         // Usar TLS/SSL
}
```

### Ejemplo de configuración completa

```ts
const cacheClient = new CacheClient({
  host: process.env.REDIS_HOST || 'localhost',
  port: parseInt(process.env.REDIS_PORT || '6379'),
  username: process.env.REDIS_USERNAME,
  password: process.env.REDIS_PASSWORD,
  db: parseInt(process.env.REDIS_DB || '0'),
  tls: process.env.NODE_ENV === 'production',
});
```

---

## TypeScript

La librería está completamente tipada. Puedes usar los tipos e interfaces exportados:

```ts
import type { 
  ICacheClient, 
  ICacheService, 
  RedisConfig 
} from '@hemia/cache-manager';
```

---

## Mejores Prácticas

1. **Usa TTL apropiados**: Siempre establece tiempos de expiración para evitar datos obsoletos
2. **Evita KEYS en producción**: El método `getKeysByPattern` usa el comando KEYS que puede ser lento
3. **Implementa Circuit Breaker**: Maneja errores de Redis para evitar que afecten tu aplicación
4. **Monitorea el tamaño**: Controla el tamaño de los objetos que guardas en cache
5. **Namespace tus claves**: Usa prefijos descriptivos como `user:`, `session:`, etc.

---

## Licencia

MIT — © Hemia Technologies

Desarrollado por [Hemia Technologies](https://hemia.mx)