package telemirror import ( "encoding/json" "os" "path/filepath" "strings" "sync" "time" ) // Cache TTLs. FreshTTL is intentionally short so opening a channel a // second time triggers a background refresh — without it, a "live" // feed feels frozen for the first 10 minutes after every fetch. const ( FreshTTL = 1 * time.Minute StaleTTL = 24 * time.Hour ) // Cache is a per-channel disk cache backed by a small in-memory map. type Cache struct { dir string mu sync.Mutex mem map[string]*FetchResult } func NewCache(dir string) *Cache { return &Cache{dir: dir, mem: make(map[string]*FetchResult)} } func (c *Cache) path(username string) string { return filepath.Join(c.dir, strings.ToLower(SanitizeUsername(username))+".json") } // Get returns (entry, fresh). fresh=false means the entry is older than // FreshTTL but still within StaleTTL, so the caller can serve it while // refreshing in the background. func (c *Cache) Get(username string) (*FetchResult, bool) { username = strings.ToLower(SanitizeUsername(username)) if username == "" { return nil, false } c.mu.Lock() if r, ok := c.mem[username]; ok && r != nil { age := time.Since(r.FetchedAt) if age <= StaleTTL { c.mu.Unlock() return r, age < FreshTTL } delete(c.mem, username) } c.mu.Unlock() b, err := os.ReadFile(c.path(username)) if err != nil { return nil, false } var r FetchResult if err := json.Unmarshal(b, &r); err != nil { return nil, false } if time.Since(r.FetchedAt) > StaleTTL { return nil, false } c.mu.Lock() c.mem[username] = &r c.mu.Unlock() return &r, time.Since(r.FetchedAt) < FreshTTL } func (c *Cache) Put(username string, r *FetchResult) error { username = strings.ToLower(SanitizeUsername(username)) if username == "" || r == nil { return ErrEmptyUsername } if err := os.MkdirAll(c.dir, 0700); err != nil { return err } r.FetchedAt = time.Now() b, err := json.MarshalIndent(r, "", " ") if err != nil { return err } if err := os.WriteFile(c.path(username), b, 0600); err != nil { return err } c.mu.Lock() c.mem[username] = r c.mu.Unlock() return nil } // Clear drops all in-memory and on-disk entries. func (c *Cache) Clear() { c.mu.Lock() c.mem = make(map[string]*FetchResult) c.mu.Unlock() entries, err := os.ReadDir(c.dir) if err != nil { return } for _, e := range entries { if e.IsDir() { continue } _ = os.Remove(filepath.Join(c.dir, e.Name())) } }