← back to Wine Finder Next
lib/redis.ts
139 lines
import Redis from 'ioredis';
// Redis client singleton
let redis: Redis | null = null;
export function getRedisClient(): Redis {
if (!redis) {
redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: parseInt(process.env.REDIS_PORT || '6379'),
password: process.env.REDIS_PASSWORD,
db: parseInt(process.env.REDIS_DB || '0'),
retryStrategy: (times: number) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
maxRetriesPerRequest: 3,
enableReadyCheck: true,
lazyConnect: true,
});
redis.on('error', (err) => {
console.error('Redis Client Error:', err);
});
redis.on('connect', () => {
console.log('Redis Client Connected');
});
}
return redis;
}
// Cache wrapper with TTL
export class CacheManager {
private redis: Redis;
private defaultTTL: number;
constructor(defaultTTL = 300) {
this.redis = getRedisClient();
this.defaultTTL = defaultTTL;
}
async get<T>(key: string): Promise<T | null> {
try {
const data = await this.redis.get(key);
if (data) {
return JSON.parse(data);
}
return null;
} catch (error) {
console.error(`Cache get error for key ${key}:`, error);
return null;
}
}
async set(key: string, value: any, ttl?: number): Promise<void> {
try {
const ttlSeconds = ttl || this.defaultTTL;
await this.redis.setex(key, ttlSeconds, JSON.stringify(value));
} catch (error) {
console.error(`Cache set error for key ${key}:`, error);
}
}
async del(key: string): Promise<void> {
try {
await this.redis.del(key);
} catch (error) {
console.error(`Cache delete error for key ${key}:`, error);
}
}
async flush(): Promise<void> {
try {
await this.redis.flushdb();
} catch (error) {
console.error('Cache flush error:', error);
}
}
// Pattern-based cache invalidation
async invalidatePattern(pattern: string): Promise<void> {
try {
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
} catch (error) {
console.error(`Cache invalidation error for pattern ${pattern}:`, error);
}
}
// Cache wrapper with automatic fallback
async getOrSet<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
// Try to get from cache first
const cached = await this.get<T>(key);
if (cached !== null) {
return cached;
}
// Fetch fresh data
const freshData = await fetcher();
// Store in cache
await this.set(key, freshData, ttl);
return freshData;
}
}
// Export singleton instance
export const cache = new CacheManager();
// Cache key generators
export const cacheKeys = {
wine: (id: string) => `wine:${id}`,
wineSearch: (query: string) => `search:wine:${query}`,
featured: () => 'featured:wines',
membershipBottles: () => 'membership:bottles',
membershipVotes: (active: boolean) => `membership:votes:${active}`,
membershipMarket: (active: boolean) => `membership:market:${active}`,
priceHistory: (wineName: string) => `price:history:${wineName}`,
userSession: (userId: string) => `session:${userId}`,
apiResponse: (endpoint: string, params: string) => `api:${endpoint}:${params}`,
};
// Cache TTL values (in seconds)
export const cacheTTL = {
short: 60, // 1 minute
medium: 300, // 5 minutes
long: 1800, // 30 minutes
veryLong: 3600, // 1 hour
day: 86400, // 24 hours
};