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:
parent
f17934dcb8
commit
a8997382b7
1 changed files with 165 additions and 0 deletions
165
src/lib/server/cache-manager.ts
Normal file
165
src/lib/server/cache-manager.ts
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue