package cache import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "time" "github.com/redis/go-redis/v9" ) type RedisCache struct { client *redis.Client prefix string } func NewRedisCache(redisURL, prefix string) (*RedisCache, error) { opts, err := redis.ParseURL(redisURL) if err != nil { return nil, err } client := redis.NewClient(opts) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := client.Ping(ctx).Err(); err != nil { return nil, err } return &RedisCache{ client: client, prefix: prefix, }, nil } func (c *RedisCache) Close() error { return c.client.Close() } func (c *RedisCache) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error { data, err := json.Marshal(value) if err != nil { return err } return c.client.Set(ctx, c.prefix+":"+key, data, ttl).Err() } func (c *RedisCache) Get(ctx context.Context, key string, dest interface{}) error { data, err := c.client.Get(ctx, c.prefix+":"+key).Bytes() if err != nil { return err } return json.Unmarshal(data, dest) } func (c *RedisCache) Exists(ctx context.Context, key string) (bool, error) { n, err := c.client.Exists(ctx, c.prefix+":"+key).Result() if err != nil { return false, err } return n > 0, nil } func (c *RedisCache) Delete(ctx context.Context, key string) error { return c.client.Del(ctx, c.prefix+":"+key).Err() } func (c *RedisCache) SetJSON(ctx context.Context, key string, value interface{}, ttl time.Duration) error { return c.Set(ctx, key, value, ttl) } func (c *RedisCache) GetJSON(ctx context.Context, key string, dest interface{}) error { return c.Get(ctx, key, dest) } type CacheKey string const ( KeySearchResults CacheKey = "search" KeyArticleSummary CacheKey = "summary" KeyDigest CacheKey = "digest" KeyChatResponse CacheKey = "chat" ) func HashKey(parts ...string) string { combined := "" for _, p := range parts { combined += p + ":" } hash := sha256.Sum256([]byte(combined)) return hex.EncodeToString(hash[:16]) } func (c *RedisCache) CacheSearch(ctx context.Context, query string, results interface{}, ttl time.Duration) error { key := string(KeySearchResults) + ":" + HashKey(query) return c.Set(ctx, key, results, ttl) } func (c *RedisCache) GetCachedSearch(ctx context.Context, query string, dest interface{}) error { key := string(KeySearchResults) + ":" + HashKey(query) return c.Get(ctx, key, dest) } func (c *RedisCache) CacheArticleSummary(ctx context.Context, url string, events []string, ttl time.Duration) error { key := string(KeyArticleSummary) + ":" + HashKey(url) return c.Set(ctx, key, events, ttl) } func (c *RedisCache) GetCachedArticleSummary(ctx context.Context, url string) ([]string, error) { key := string(KeyArticleSummary) + ":" + HashKey(url) var events []string if err := c.Get(ctx, key, &events); err != nil { return nil, err } return events, nil } func (c *RedisCache) CacheDigest(ctx context.Context, topic, region, title string, digest interface{}, ttl time.Duration) error { key := string(KeyDigest) + ":" + HashKey(topic, region, title) return c.Set(ctx, key, digest, ttl) } func (c *RedisCache) GetCachedDigest(ctx context.Context, topic, region, title string, dest interface{}) error { key := string(KeyDigest) + ":" + HashKey(topic, region, title) return c.Get(ctx, key, dest) } type MemoryCache struct { data map[string]cacheEntry } type cacheEntry struct { value interface{} expiresAt time.Time } func NewMemoryCache() *MemoryCache { return &MemoryCache{ data: make(map[string]cacheEntry), } } func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) { c.data[key] = cacheEntry{ value: value, expiresAt: time.Now().Add(ttl), } } func (c *MemoryCache) Get(key string) (interface{}, bool) { entry, ok := c.data[key] if !ok { return nil, false } if time.Now().After(entry.expiresAt) { delete(c.data, key) return nil, false } return entry.value, true } func (c *MemoryCache) Delete(key string) { delete(c.data, key) } func (c *MemoryCache) Clear() { c.data = make(map[string]cacheEntry) } func (c *MemoryCache) Cleanup() int { count := 0 now := time.Now() for k, v := range c.data { if now.After(v.expiresAt) { delete(c.data, k) count++ } } return count }