cleaned up cache lib

This commit is contained in:
Boki 2025-06-10 13:38:47 -04:00
parent ee57b66391
commit e98b1d8ae2
9 changed files with 637 additions and 1209 deletions

240
libs/cache/src/redis-cache.ts vendored Normal file
View file

@ -0,0 +1,240 @@
import Redis from 'ioredis';
import { getLogger } from '@stock-bot/logger';
import { CacheProvider, CacheOptions, CacheStats } from './types';
import { RedisConnectionManager } from './connection-manager';
/**
* Simplified Redis-based cache provider using connection manager
*/
export class RedisCache implements CacheProvider {
private redis: Redis;
private logger = getLogger('redis-cache');
private defaultTTL: number;
private keyPrefix: string;
private enableMetrics: boolean;
private isConnected = false;
private startTime = Date.now();
private connectionManager: RedisConnectionManager;
private stats: CacheStats = {
hits: 0,
misses: 0,
errors: 0,
hitRate: 0,
total: 0,
uptime: 0
};
constructor(options: CacheOptions = {}) {
this.defaultTTL = options.ttl ?? 3600; // 1 hour default
this.keyPrefix = options.keyPrefix ?? 'cache:';
this.enableMetrics = options.enableMetrics ?? true;
// Get connection manager instance
this.connectionManager = RedisConnectionManager.getInstance();
// Generate connection name based on cache type
const baseName = options.name || this.keyPrefix.replace(':', '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase() || 'CACHE';
// Get Redis connection (shared by default for cache)
this.redis = this.connectionManager.getConnection({
name: `${baseName}-SERVICE`,
singleton: options.shared ?? true, // Default to shared connection for cache
});
this.setupEventHandlers();
}
private setupEventHandlers(): void {
this.redis.on('connect', () => {
this.logger.info('Redis cache connected');
});
this.redis.on('ready', () => {
this.isConnected = true;
this.logger.info('Redis cache ready');
});
this.redis.on('error', (error: any) => {
this.isConnected = false;
this.logger.error('Redis cache connection error', { error: error.message });
});
this.redis.on('close', () => {
this.isConnected = false;
this.logger.warn('Redis cache connection closed');
});
this.redis.on('reconnecting', () => {
this.logger.info('Redis cache reconnecting...');
});
}
private getKey(key: string): string {
return `${this.keyPrefix}${key}`;
}
private updateStats(hit: boolean, error = false): void {
if (!this.enableMetrics) return;
if (error) {
this.stats.errors++;
} else if (hit) {
this.stats.hits++;
} else {
this.stats.misses++;
}
this.stats.total = this.stats.hits + this.stats.misses;
this.stats.hitRate = this.stats.total > 0 ? this.stats.hits / this.stats.total : 0;
this.stats.uptime = Date.now() - this.startTime;
}
private async safeExecute<T>(
operation: () => Promise<T>,
fallback: T,
operationName: string
): Promise<T> {
try {
if (!this.isConnected) {
this.logger.warn(`Redis not connected for ${operationName}, using fallback`);
this.updateStats(false, true);
return fallback;
}
return await operation();
} catch (error) {
this.logger.error(`Redis ${operationName} failed`, {
error: error instanceof Error ? error.message : String(error)
});
this.updateStats(false, true);
return fallback;
}
}
async get<T>(key: string): Promise<T | null> {
return this.safeExecute(
async () => {
const fullKey = this.getKey(key);
const value = await this.redis.get(fullKey);
if (value === null) {
this.updateStats(false);
this.logger.debug('Cache miss', { key });
return null;
}
this.updateStats(true);
this.logger.debug('Cache hit', { key });
try {
return JSON.parse(value) as T;
} catch {
// If parsing fails, return as string
return value as unknown as T;
}
},
null,
'get'
);
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
await this.safeExecute(
async () => {
const fullKey = this.getKey(key);
const expiry = ttl ?? this.defaultTTL;
const serialized = typeof value === 'string' ? value : JSON.stringify(value);
await this.redis.setex(fullKey, expiry, serialized);
this.logger.debug('Cache set', { key, ttl: expiry });
},
undefined,
'set'
);
}
async del(key: string): Promise<void> {
await this.safeExecute(
async () => {
const fullKey = this.getKey(key);
await this.redis.del(fullKey);
this.logger.debug('Cache delete', { key });
},
undefined,
'del'
);
}
async exists(key: string): Promise<boolean> {
return this.safeExecute(
async () => {
const fullKey = this.getKey(key);
const result = await this.redis.exists(fullKey);
return result === 1;
},
false,
'exists'
);
}
async clear(): Promise<void> {
await this.safeExecute(
async () => {
const pattern = `${this.keyPrefix}*`;
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
this.logger.info('Cache cleared', { keysDeleted: keys.length });
}
},
undefined,
'clear'
);
}
async health(): Promise<boolean> {
try {
const pong = await this.redis.ping();
return pong === 'PONG';
} catch (error) {
this.logger.error('Redis health check failed', {
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
getStats(): CacheStats {
return {
...this.stats,
uptime: Date.now() - this.startTime
};
}
async waitForReady(timeout = 5000): Promise<void> {
return new Promise((resolve, reject) => {
if (this.redis.status === 'ready') {
resolve();
return;
}
const timeoutId = setTimeout(() => {
reject(new Error(`Redis connection timeout after ${timeout}ms`));
}, timeout);
this.redis.once('ready', () => {
clearTimeout(timeoutId);
resolve();
});
this.redis.once('error', (error) => {
clearTimeout(timeoutId);
reject(error);
});
});
}
isReady(): boolean {
return this.isConnected && this.redis.status === 'ready';
}
}