import Redis from 'redis';
import { redisConfig, featureConfig } from '../config';
import { CalendarEvent, Calendar, TimeSlot, UserPreferences } from '../types';
export interface CacheManager {
// Calendar operations
getCalendars(userId: string): Promise<Calendar[] | null>;
setCalendars(userId: string, calendars: Calendar[]): Promise<void>;
// Event operations
getEvents(userId: string, calendarId: string, startDate: Date, endDate: Date): Promise<CalendarEvent[] | null>;
setEvents(userId: string, calendarId: string, startDate: Date, endDate: Date, events: CalendarEvent[]): Promise<void>;
invalidateEvents(userId: string, calendarId?: string): Promise<void>;
// Free time operations
getFreeTime(userId: string, key: string): Promise<TimeSlot[] | null>;
setFreeTime(userId: string, key: string, slots: TimeSlot[]): Promise<void>;
// User preferences
getUserPreferences(userId: string): Promise<UserPreferences | null>;
setUserPreferences(userId: string, preferences: UserPreferences): Promise<void>;
// Contact suggestions
getContactSuggestions(userId: string, query: string): Promise<string[] | null>;
setContactSuggestions(userId: string, query: string, contacts: string[]): Promise<void>;
// General cache operations
invalidateUser(userId: string): Promise<void>;
clear(): Promise<void>;
ping(): Promise<boolean>;
}
export class RedisCacheManager implements CacheManager {
private client: Redis.RedisClientType;
private connected: boolean = false;
constructor() {
this.client = Redis.createClient({
url: redisConfig.url,
retry_strategy: (options) => {
if (options.error && options.error.code === 'ECONNREFUSED') {
console.error('Redis connection refused');
return new Error('Redis connection refused');
}
if (options.total_retry_time > 1000 * 60 * 60) {
return new Error('Retry time exhausted');
}
if (options.attempt > 10) {
return undefined;
}
return Math.min(options.attempt * 100, 3000);
}
});
this.client.on('error', (err) => {
console.error('Redis Client Error:', err);
this.connected = false;
});
this.client.on('connect', () => {
console.log('Redis Client Connected');
this.connected = true;
});
this.client.on('disconnect', () => {
console.log('Redis Client Disconnected');
this.connected = false;
});
}
async connect(): Promise<void> {
if (!this.connected) {
await this.client.connect();
}
}
async disconnect(): Promise<void> {
if (this.connected) {
await this.client.disconnect();
}
}
private generateKey(userId: string, type: string, ...params: string[]): string {
return `cal:${userId}:${type}:${params.join(':')}`;
}
private async safeGet<T>(key: string): Promise<T | null> {
if (!featureConfig.enableCaching || !this.connected) {
return null;
}
try {
const value = await this.client.get(key);
return value ? JSON.parse(value) : null;
} catch (error) {
console.error('Cache get error:', error);
return null;
}
}
private async safeSet(key: string, value: any, ttlSeconds: number): Promise<void> {
if (!featureConfig.enableCaching || !this.connected) {
return;
}
try {
await this.client.setEx(key, ttlSeconds, JSON.stringify(value));
} catch (error) {
console.error('Cache set error:', error);
}
}
private async safeDelete(pattern: string): Promise<void> {
if (!featureConfig.enableCaching || !this.connected) {
return;
}
try {
const keys = await this.client.keys(pattern);
if (keys.length > 0) {
await this.client.del(keys);
}
} catch (error) {
console.error('Cache delete error:', error);
}
}
// Calendar operations
async getCalendars(userId: string): Promise<Calendar[] | null> {
const key = this.generateKey(userId, 'calendars');
return this.safeGet<Calendar[]>(key);
}
async setCalendars(userId: string, calendars: Calendar[]): Promise<void> {
const key = this.generateKey(userId, 'calendars');
await this.safeSet(key, calendars, redisConfig.ttl.userPreferences);
}
// Event operations
async getEvents(
userId: string,
calendarId: string,
startDate: Date,
endDate: Date
): Promise<CalendarEvent[] | null> {
const key = this.generateKey(
userId,
'events',
calendarId,
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
return this.safeGet<CalendarEvent[]>(key);
}
async setEvents(
userId: string,
calendarId: string,
startDate: Date,
endDate: Date,
events: CalendarEvent[]
): Promise<void> {
const key = this.generateKey(
userId,
'events',
calendarId,
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
await this.safeSet(key, events, redisConfig.ttl.events);
}
async invalidateEvents(userId: string, calendarId?: string): Promise<void> {
const pattern = calendarId
? this.generateKey(userId, 'events', calendarId, '*')
: this.generateKey(userId, 'events', '*');
await this.safeDelete(pattern);
}
// Free time operations
async getFreeTime(userId: string, key: string): Promise<TimeSlot[] | null> {
const cacheKey = this.generateKey(userId, 'freetime', key);
return this.safeGet<TimeSlot[]>(cacheKey);
}
async setFreeTime(userId: string, key: string, slots: TimeSlot[]): Promise<void> {
const cacheKey = this.generateKey(userId, 'freetime', key);
await this.safeSet(cacheKey, slots, redisConfig.ttl.freeTime);
}
// User preferences
async getUserPreferences(userId: string): Promise<UserPreferences | null> {
const key = this.generateKey(userId, 'preferences');
return this.safeGet<UserPreferences>(key);
}
async setUserPreferences(userId: string, preferences: UserPreferences): Promise<void> {
const key = this.generateKey(userId, 'preferences');
await this.safeSet(key, preferences, redisConfig.ttl.userPreferences);
}
// Contact suggestions
async getContactSuggestions(userId: string, query: string): Promise<string[] | null> {
const key = this.generateKey(userId, 'contacts', query.toLowerCase());
return this.safeGet<string[]>(key);
}
async setContactSuggestions(userId: string, query: string, contacts: string[]): Promise<void> {
const key = this.generateKey(userId, 'contacts', query.toLowerCase());
await this.safeSet(key, contacts, redisConfig.ttl.contacts);
}
// General operations
async invalidateUser(userId: string): Promise<void> {
const pattern = this.generateKey(userId, '*');
await this.safeDelete(pattern);
}
async clear(): Promise<void> {
if (!this.connected) return;
try {
await this.client.flushDb();
} catch (error) {
console.error('Cache clear error:', error);
}
}
async ping(): Promise<boolean> {
if (!this.connected) return false;
try {
const result = await this.client.ping();
return result === 'PONG';
} catch (error) {
console.error('Cache ping error:', error);
return false;
}
}
}
// In-memory cache fallback for development or when Redis is unavailable
export class MemoryCacheManager implements CacheManager {
private cache: Map<string, { value: any; expiry: number }> = new Map();
private isExpired(expiry: number): boolean {
return Date.now() > expiry;
}
private generateKey(userId: string, type: string, ...params: string[]): string {
return `cal:${userId}:${type}:${params.join(':')}`;
}
private get<T>(key: string): T | null {
const item = this.cache.get(key);
if (!item || this.isExpired(item.expiry)) {
this.cache.delete(key);
return null;
}
return item.value as T;
}
private set(key: string, value: any, ttlSeconds: number): void {
const expiry = Date.now() + (ttlSeconds * 1000);
this.cache.set(key, { value, expiry });
}
private deletePattern(pattern: string): void {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key);
}
}
}
async getCalendars(userId: string): Promise<Calendar[] | null> {
const key = this.generateKey(userId, 'calendars');
return this.get<Calendar[]>(key);
}
async setCalendars(userId: string, calendars: Calendar[]): Promise<void> {
const key = this.generateKey(userId, 'calendars');
this.set(key, calendars, redisConfig.ttl.userPreferences);
}
async getEvents(
userId: string,
calendarId: string,
startDate: Date,
endDate: Date
): Promise<CalendarEvent[] | null> {
const key = this.generateKey(
userId,
'events',
calendarId,
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
return this.get<CalendarEvent[]>(key);
}
async setEvents(
userId: string,
calendarId: string,
startDate: Date,
endDate: Date,
events: CalendarEvent[]
): Promise<void> {
const key = this.generateKey(
userId,
'events',
calendarId,
startDate.toISOString().split('T')[0],
endDate.toISOString().split('T')[0]
);
this.set(key, events, redisConfig.ttl.events);
}
async invalidateEvents(userId: string, calendarId?: string): Promise<void> {
const pattern = calendarId
? this.generateKey(userId, 'events', calendarId, '*')
: this.generateKey(userId, 'events', '*');
this.deletePattern(pattern);
}
async getFreeTime(userId: string, key: string): Promise<TimeSlot[] | null> {
const cacheKey = this.generateKey(userId, 'freetime', key);
return this.get<TimeSlot[]>(cacheKey);
}
async setFreeTime(userId: string, key: string, slots: TimeSlot[]): Promise<void> {
const cacheKey = this.generateKey(userId, 'freetime', key);
this.set(cacheKey, slots, redisConfig.ttl.freeTime);
}
async getUserPreferences(userId: string): Promise<UserPreferences | null> {
const key = this.generateKey(userId, 'preferences');
return this.get<UserPreferences>(key);
}
async setUserPreferences(userId: string, preferences: UserPreferences): Promise<void> {
const key = this.generateKey(userId, 'preferences');
this.set(key, preferences, redisConfig.ttl.userPreferences);
}
async getContactSuggestions(userId: string, query: string): Promise<string[] | null> {
const key = this.generateKey(userId, 'contacts', query.toLowerCase());
return this.get<string[]>(key);
}
async setContactSuggestions(userId: string, query: string, contacts: string[]): Promise<void> {
const key = this.generateKey(userId, 'contacts', query.toLowerCase());
this.set(key, contacts, redisConfig.ttl.contacts);
}
async invalidateUser(userId: string): Promise<void> {
const pattern = this.generateKey(userId, '*');
this.deletePattern(pattern);
}
async clear(): Promise<void> {
this.cache.clear();
}
async ping(): Promise<boolean> {
return true; // Memory cache is always available
}
}
// Factory function to create the appropriate cache manager
export function createCacheManager(): CacheManager {
if (redisConfig.url && featureConfig.enableCaching) {
return new RedisCacheManager();
} else {
console.warn('Redis not configured or caching disabled, using in-memory cache');
return new MemoryCacheManager();
}
}