cache.ts•5.27 kB
/**
* Cache utility for storing and retrieving API responses
*/
import NodeCache from 'node-cache';
import { logger } from './logger';
// Default cache TTL settings (in seconds)
// Extended TTL values for a personal project with limited API quota (100 calls/day)
const DEFAULT_TTL = 12 * 60 * 60; // 12 hours
const CACHE_SETTINGS = {
// Top news - refresh once per day at most
top: 24 * 60 * 60, // 24 hours
// Sources almost never change
sources: 7 * 24 * 60 * 60, // 7 days
// Similar articles and article by UUID are permanent until cache clear
similar: 30 * 24 * 60 * 60, // 30 days
uuid: 30 * 24 * 60 * 60, // 30 days
// General search results
all: 24 * 60 * 60 // 24 hours
};
/**
* Cache service for API responses
*/
export class CacheService {
private static instance: CacheService;
private cache: NodeCache;
private statsTimer: NodeJS.Timeout | null = null;
private constructor() {
this.cache = new NodeCache({
stdTTL: DEFAULT_TTL,
checkperiod: 120, // Check for expired keys every 2 minutes
useClones: false // For better performance, don't clone objects
});
// Only start the stats timer if not in test environment
if (process.env.NODE_ENV !== 'test') {
this.startStatsTimer();
}
}
/**
* Start the statistics logging timer
*/
private startStatsTimer(): void {
// Clear any existing timer first
this.stopStatsTimer();
// Log cache statistics periodically
this.statsTimer = setInterval(() => {
const stats = this.cache.getStats();
logger.debug('Cache stats:', stats);
}, 15 * 60 * 1000); // Log every 15 minutes
}
/**
* Stop the statistics logging timer
*/
private stopStatsTimer(): void {
if (this.statsTimer) {
clearInterval(this.statsTimer);
this.statsTimer = null;
}
}
/**
* Get singleton instance of cache service
* @returns Cache service instance
*/
public static getInstance(): CacheService {
if (!CacheService.instance) {
CacheService.instance = new CacheService();
}
return CacheService.instance;
}
/**
* Get value from cache
* @param key Cache key
* @returns Cached value or undefined if not found
*/
public get<T>(key: string): T | undefined {
const value = this.cache.get<T>(key);
if (value) {
logger.debug(`Cache hit for key: ${key}`);
return value;
}
logger.debug(`Cache miss for key: ${key}`);
return undefined;
}
/**
* Set value in cache
* @param key Cache key
* @param value Value to cache
* @param ttl Time to live in seconds (optional)
* @returns Success status
*/
public set<T>(key: string, value: T, ttl?: number): boolean {
const ttlValue = ttl !== undefined ? ttl : DEFAULT_TTL;
logger.debug(`Setting cache for key: ${key}, TTL: ${ttlValue}s`);
return this.cache.set(key, value, ttlValue);
}
/**
* Check if key exists in cache
* @param key Cache key
* @returns True if key exists
*/
public has(key: string): boolean {
return this.cache.has(key);
}
/**
* Delete key from cache
* @param key Cache key
* @returns True if key was deleted
*/
public delete(key: string): boolean {
return this.cache.del(key) > 0;
}
/**
* Delete keys with a specific prefix
* @param prefix Key prefix to match
* @returns Number of keys deleted
*/
public deleteByPrefix(prefix: string): number {
const keys = this.cache.keys().filter(key => key.startsWith(prefix));
return this.cache.del(keys);
}
/**
* Clear the entire cache
* @returns Success status
*/
public clear(): boolean {
return this.cache.flushAll();
}
/**
* Shutdown the cache service, cleaning up resources
* Useful for tests to ensure clean teardown
*/
public shutdown(): void {
this.stopStatsTimer();
this.clear();
// Close the cache if needed
if (typeof this.cache.close === 'function') {
this.cache.close();
}
}
/**
* Get the appropriate TTL based on endpoint type
* @param type Type of endpoint ('top', 'all', 'similar', 'uuid', 'sources')
* @returns TTL in seconds
*/
public getTTL(type: keyof typeof CACHE_SETTINGS): number {
return CACHE_SETTINGS[type] || DEFAULT_TTL;
}
/**
* Get cache statistics
* @returns Object with cache stats
*/
public getStats(): object {
const stats = this.cache.getStats();
const keys = this.cache.keys();
// Count keys by type
const keysByType = {
top: keys.filter(k => k.startsWith('top_')).length,
all: keys.filter(k => k.startsWith('all_')).length,
similar: keys.filter(k => k.startsWith('similar_')).length,
uuid: keys.filter(k => k.startsWith('news_article_')).length,
sources: keys.filter(k => k.startsWith('news_sources_')).length,
other: keys.filter(k => !(
k.startsWith('top_') ||
k.startsWith('all_') ||
k.startsWith('similar_') ||
k.startsWith('news_article_') ||
k.startsWith('news_sources_')
)).length
};
return {
...stats,
keys: keys.length,
keysByType
};
}
}
// Export singleton instance
export const cacheService = CacheService.getInstance();