package handlers import ( "sync" "time" "veola/internal/apify" ) // previewKey caches the *raw* apify result set (post-decode, post-merge, // pre-filter). Filters like min_price and exclude_keywords are applied after // the cache lookup so the operator can iterate on them without burning credits. // // Condition and Region are part of the key, not post-filters: they are // server-side eBay Browse API filters that change the result set the API // returns, so a different condition/region must miss the cache. type previewKey struct { Queries, URL, Marketplace, ListingType, ActorIDs string Condition, Region string MaxResults int } type previewEntry struct { results []apify.UnifiedResult source string stored time.Time } type PreviewCache struct { ttl time.Duration mu sync.Mutex entries map[previewKey]previewEntry } func NewPreviewCache(ttl time.Duration) *PreviewCache { return &PreviewCache{ ttl: ttl, entries: make(map[previewKey]previewEntry), } } func (c *PreviewCache) Get(k previewKey) ([]apify.UnifiedResult, string, bool) { c.mu.Lock() defer c.mu.Unlock() e, ok := c.entries[k] if !ok { return nil, "", false } if time.Since(e.stored) > c.ttl { delete(c.entries, k) return nil, "", false } return e.results, e.source, true } func (c *PreviewCache) Put(k previewKey, results []apify.UnifiedResult, source string) { c.mu.Lock() defer c.mu.Unlock() c.entries[k] = previewEntry{results: results, source: source, stored: time.Now()} if len(c.entries) > 64 { c.evictExpired() } } func (c *PreviewCache) evictExpired() { for k, e := range c.entries { if time.Since(e.stored) > c.ttl { delete(c.entries, k) } } }