/**
* In-memory cache with TTL support
*/
interface CacheEntry<T> {
value: T;
expiry: number;
hits: number;
}
export interface CacheStats {
size: number;
hits: number;
misses: number;
hitRate: number;
}
// TTL values in milliseconds
export const CacheTTL = {
SEARCH_RESULTS: 5 * 60 * 1000, // 5 minutes
RESTAURANT_DETAILS: 24 * 60 * 60 * 1000, // 24 hours
AVAILABILITY: 60 * 1000, // 1 minute (real-time critical)
PLATFORM_HEALTH: 30 * 1000, // 30 seconds
} as const;
export class CacheService {
private cache = new Map<string, CacheEntry<unknown>>();
private hits = 0;
private misses = 0;
private cleanupInterval: NodeJS.Timeout | null = null;
constructor() {
// Cleanup expired entries every minute
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
}
/**
* Get a value from the cache
*/
get<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) {
this.misses++;
return null;
}
if (Date.now() > entry.expiry) {
this.cache.delete(key);
this.misses++;
return null;
}
entry.hits++;
this.hits++;
return entry.value as T;
}
/**
* Set a value in the cache with TTL
*/
set<T>(key: string, value: T, ttl: number): void {
this.cache.set(key, {
value,
expiry: Date.now() + ttl,
hits: 0,
});
}
/**
* Check if a key exists and is not expired
*/
has(key: string): boolean {
const entry = this.cache.get(key);
if (!entry) return false;
if (Date.now() > entry.expiry) {
this.cache.delete(key);
return false;
}
return true;
}
/**
* Delete a specific key
*/
delete(key: string): boolean {
return this.cache.delete(key);
}
/**
* Invalidate cache entries matching a pattern
* Pattern supports * as wildcard
*/
invalidate(pattern: string): number {
let count = 0;
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
for (const key of this.cache.keys()) {
if (regex.test(key)) {
this.cache.delete(key);
count++;
}
}
return count;
}
/**
* Clear all cache entries
*/
clear(): void {
this.cache.clear();
this.hits = 0;
this.misses = 0;
}
/**
* Get cache statistics
*/
stats(): CacheStats {
const total = this.hits + this.misses;
return {
size: this.cache.size,
hits: this.hits,
misses: this.misses,
hitRate: total > 0 ? this.hits / total : 0,
};
}
/**
* Clean up expired entries
*/
private cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiry) {
this.cache.delete(key);
}
}
}
/**
* Stop the cleanup interval (for graceful shutdown)
*/
destroy(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
}
}
// Cache key generators
export const CacheKeys = {
search(queryHash: string): string {
return `search:${queryHash}`;
},
details(platform: string, id: string | number): string {
return `details:${platform}:${id}`;
},
availability(platform: string, id: string | number, date: string, partySize: number): string {
return `availability:${platform}:${id}:${date}:${partySize}`;
},
health(platform: string): string {
return `health:${platform}`;
},
};
/**
* Generate a hash for search queries
*/
export function hashSearchQuery(query: string, location: string, cuisine?: string): string {
const normalized = `${query.toLowerCase().trim()}|${location.toLowerCase().trim()}|${cuisine?.toLowerCase().trim() || ''}`;
// Simple hash function
let hash = 0;
for (let i = 0; i < normalized.length; i++) {
const char = normalized.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(36);
}
// Singleton instance
export const cache = new CacheService();