@hemia/cache-manager
v0.0.5
Published
Manejador de caché con soporte Redis y inversify para aplicaciones TypeScript.
Readme
@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
npm install @hemia/cache-managerUso Básico (sin inyección de dependencias)
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
npm install inversify reflect-metadata2. Crear módulo de cache en tu backend
Crea src/infrastructure/cache/CacheModule.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:
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
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
// 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
// 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
// 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
// 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)
// 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)
// 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
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
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
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
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
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
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
// 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
// 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
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
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:
import type {
ICacheClient,
ICacheService,
RedisConfig
} from '@hemia/cache-manager';Mejores Prácticas
- Usa TTL apropiados: Siempre establece tiempos de expiración para evitar datos obsoletos
- Evita KEYS en producción: El método
getKeysByPatternusa el comando KEYS que puede ser lento - Implementa Circuit Breaker: Maneja errores de Redis para evitar que afecten tu aplicación
- Monitorea el tamaño: Controla el tamaño de los objetos que guardas en cache
- Namespace tus claves: Usa prefijos descriptivos como
user:,session:, etc.
Licencia
MIT — © Hemia Technologies
Desarrollado por Hemia Technologies
