ai-wpa/lib/cache.ts

142 lines
4.5 KiB
TypeScript

import { connectRedis } from './redis'
/**
* Cache management utilities for dashboard and topic data
*/
export class DashboardCache {
/**
* Invalidate dashboard cache for a specific user
*/
static async invalidateUserDashboard(userId: string): Promise<void> {
try {
const redisClient = await connectRedis()
const cacheKey = `dashboard:user:${userId}`
await redisClient.del(cacheKey)
console.log(`Dashboard cache invalidated for user ${userId}`)
} catch (error) {
console.warn('Failed to invalidate dashboard cache:', error)
}
}
/**
* Invalidate dashboard caches for multiple users
*/
static async invalidateMultipleUserDashboards(userIds: string[]): Promise<void> {
try {
const redisClient = await connectRedis()
const cacheKeys = userIds.map((userId) => `dashboard:user:${userId}`)
if (cacheKeys.length > 0) {
await redisClient.del(cacheKeys)
console.log(`Dashboard caches invalidated for ${userIds.length} users`)
}
} catch (error) {
console.warn('Failed to invalidate multiple dashboard caches:', error)
}
}
/**
* Get cached dashboard data for a user
*/
static async getUserDashboard(userId: string): Promise<any | null> {
try {
const redisClient = await connectRedis()
const cacheKey = `dashboard:user:${userId}`
const cachedData = await redisClient.get(cacheKey)
if (!cachedData) return null
// Handle both string and Buffer responses
const dataString = Buffer.isBuffer(cachedData)
? cachedData.toString('utf-8')
: String(cachedData)
return JSON.parse(dataString)
} catch (error) {
console.warn('Failed to get cached dashboard data:', error)
return null
}
}
/**
* Set dashboard cache for a user
*/
static async setUserDashboard(userId: string, data: any, ttlSeconds = 300): Promise<void> {
try {
const redisClient = await connectRedis()
const cacheKey = `dashboard:user:${userId}`
await redisClient.setEx(cacheKey, ttlSeconds, JSON.stringify(data))
console.log(`Dashboard data cached for user ${userId} (TTL: ${ttlSeconds}s)`)
} catch (error) {
console.warn('Failed to cache dashboard data:', error)
}
}
}
export class TopicCache {
/**
* Invalidate topic-specific caches when a topic is modified
*/
static async invalidateTopicCaches(topicSlug: string, authorId: string): Promise<void> {
try {
const redisClient = await connectRedis()
// Invalidate patterns that might be affected
const cachePatterns = [
`topic:${topicSlug}`, // Individual topic cache
`topics:public:*`, // Public topic listings
`topics:user:${authorId}:*`, // User's topic listings
`dashboard:user:${authorId}`, // User's dashboard
]
for (const pattern of cachePatterns) {
if (pattern.includes('*')) {
// For patterns with wildcards, we need to scan and delete
const keys = await redisClient.keys(pattern)
if (keys.length > 0) {
await redisClient.del(keys)
console.log(`Invalidated ${keys.length} cache keys matching pattern: ${pattern}`)
}
} else {
// Direct key deletion
await redisClient.del(pattern)
console.log(`Cache key deleted: ${pattern}`)
}
}
} catch (error) {
console.warn('Failed to invalidate topic caches:', error)
}
}
/**
* Invalidate public topic listing caches
*/
static async invalidatePublicTopicCaches(): Promise<void> {
try {
const redisClient = await connectRedis()
const keys = await redisClient.keys('topics:public:*')
if (keys.length > 0) {
await redisClient.del(keys)
console.log(`Invalidated ${keys.length} public topic cache keys`)
}
} catch (error) {
console.warn('Failed to invalidate public topic caches:', error)
}
}
}
/**
* Utility function to generate consistent cache keys
*/
export const CacheKeys = {
userDashboard: (userId: string) => `dashboard:user:${userId}`,
userTopics: (userId: string, page = 1, limit = 10) =>
`topics:user:${userId}:page:${page}:limit:${limit}`,
publicTopics: (page = 1, limit = 10, filters?: string) => {
const filterKey = filters ? `:filters:${Buffer.from(filters).toString('base64')}` : ''
return `topics:public:page:${page}:limit:${limit}${filterKey}`
},
individualTopic: (slug: string) => `topic:${slug}`,
relatedTopics: (topicId: string) => `topics:related:${topicId}`,
}