feat: add centralized cache management system

- Create CacheManager class to unify all Redis cache operations
- Define cache types with prefixes and default TTLs
- Provide type-safe cache operations
- Add bulk clear operations for related caches
- Include cache statistics and monitoring capabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-07-10 21:33:25 -07:00
parent f17934dcb8
commit a8997382b7

View file

@ -0,0 +1,165 @@
import redis from '../../routes/api/redis-client'
import { logger } from './logger'
export interface CacheConfig {
prefix: string
defaultTTL: number
description: string
}
export class CacheManager {
private static cacheTypes: Map<string, CacheConfig> = new Map([
['lastfm-recent', { prefix: 'lastfm:recent:', defaultTTL: 30, description: 'Last.fm recent tracks' }],
['lastfm-album', { prefix: 'lastfm:albuminfo:', defaultTTL: 3600, description: 'Last.fm album info' }],
['apple-album', { prefix: 'apple:album:', defaultTTL: 86400, description: 'Apple Music album data' }],
['apple-notfound', { prefix: 'notfound:apple-music:', defaultTTL: 3600, description: 'Apple Music not found records' }],
['apple-failure', { prefix: 'failure:apple-music:', defaultTTL: 86400, description: 'Apple Music API failures' }],
['apple-ratelimit', { prefix: 'ratelimit:apple-music:', defaultTTL: 3600, description: 'Apple Music rate limit state' }]
])
/**
* Get a value from cache
*/
static async get(type: string, key: string): Promise<string | null> {
const config = this.cacheTypes.get(type)
if (!config) {
logger.error(`Unknown cache type: ${type}`)
return null
}
const fullKey = `${config.prefix}${key}`
return await redis.get(fullKey)
}
/**
* Set a value in cache
*/
static async set(type: string, key: string, value: string, ttl?: number): Promise<void> {
const config = this.cacheTypes.get(type)
if (!config) {
logger.error(`Unknown cache type: ${type}`)
return
}
const fullKey = `${config.prefix}${key}`
const expiry = ttl || config.defaultTTL
await redis.set(fullKey, value, 'EX', expiry)
logger.music('debug', `Cached ${type} for key: ${key} (TTL: ${expiry}s)`)
}
/**
* Delete a specific cache entry
*/
static async delete(type: string, key: string): Promise<boolean> {
const config = this.cacheTypes.get(type)
if (!config) {
logger.error(`Unknown cache type: ${type}`)
return false
}
const fullKey = `${config.prefix}${key}`
const deleted = await redis.del(fullKey)
return deleted > 0
}
/**
* Clear all entries for a specific cache type
*/
static async clearType(type: string): Promise<number> {
const config = this.cacheTypes.get(type)
if (!config) {
logger.error(`Unknown cache type: ${type}`)
return 0
}
const pattern = `${config.prefix}*`
const keys = await redis.keys(pattern)
if (keys.length === 0) return 0
const deleted = await redis.del(...keys)
logger.music('info', `Cleared ${deleted} entries from ${type} cache`)
return deleted
}
/**
* Clear all entries matching a pattern within a cache type
*/
static async clearPattern(type: string, pattern: string): Promise<number> {
const config = this.cacheTypes.get(type)
if (!config) {
logger.error(`Unknown cache type: ${type}`)
return 0
}
const searchPattern = `${config.prefix}*${pattern}*`
const keys = await redis.keys(searchPattern)
if (keys.length === 0) return 0
const deleted = await redis.del(...keys)
logger.music('info', `Cleared ${deleted} entries matching "${pattern}" from ${type} cache`)
return deleted
}
/**
* Clear all caches for a specific album
*/
static async clearAlbum(artist: string, album: string): Promise<number> {
const albumKey = `${artist}:${album}`
let totalDeleted = 0
// Clear all cache types that might contain this album
for (const [type, config] of this.cacheTypes) {
if (type.includes('album') || type.includes('notfound')) {
const deleted = await this.clearPattern(type, albumKey)
totalDeleted += deleted
}
}
logger.music('info', `Cleared ${totalDeleted} cache entries for album "${album}" by "${artist}"`)
return totalDeleted
}
/**
* Get all cache types and their info
*/
static getCacheTypes(): Array<{ type: string; config: CacheConfig }> {
return Array.from(this.cacheTypes.entries()).map(([type, config]) => ({ type, config }))
}
/**
* Get cache statistics
*/
static async getStats(): Promise<Array<{ type: string; count: number; description: string }>> {
const stats = []
for (const [type, config] of this.cacheTypes) {
const keys = await redis.keys(`${config.prefix}*`)
stats.push({
type,
count: keys.length,
description: config.description
})
}
return stats
}
}
// Export convenience functions for common operations
export const cache = {
lastfm: {
getRecent: (username: string) => CacheManager.get('lastfm-recent', username),
setRecent: (username: string, data: string) => CacheManager.set('lastfm-recent', username, data),
getAlbum: (artist: string, album: string) => CacheManager.get('lastfm-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string) => CacheManager.set('lastfm-album', `${artist}:${album}`, data)
},
apple: {
getAlbum: (artist: string, album: string) => CacheManager.get('apple-album', `${artist}:${album}`),
setAlbum: (artist: string, album: string, data: string, ttl?: number) => CacheManager.set('apple-album', `${artist}:${album}`, data, ttl),
isNotFound: (artist: string, album: string) => CacheManager.get('apple-notfound', `${artist}:${album}`),
markNotFound: (artist: string, album: string, ttl?: number) => CacheManager.set('apple-notfound', `${artist}:${album}`, '1', ttl)
}
}