# backend/cache_manager.py """Caché global en memoria con refresco en segundo plano (igual que el dashboard de ventas).""" import time import threading from typing import Any, Callable, Dict, Tuple _CACHE: Dict[str, Tuple[float, Any]] = {} _LOCK = threading.Lock() _DEFAULT_TTL = 900 # 15 min def _make_key(prefix: str, args: tuple) -> str: return prefix + ":" + "|".join(str(a) for a in args) def cache_get_or_set(prefix: str, args: tuple, loader: Callable[[], Any], ttl: int = _DEFAULT_TTL) -> Any: key = _make_key(prefix, args) now = time.time() with _LOCK: hit = _CACHE.get(key) if hit and (now - hit[0] < ttl): return hit[1] value = loader() with _LOCK: _CACHE[key] = (now, value) return value def cache_keys(): with _LOCK: return list(_CACHE.keys()) def cache_refresh_existing(loader_for_key): for key in cache_keys(): try: nuevo = loader_for_key(key) if nuevo is not None: with _LOCK: _CACHE[key] = (time.time(), nuevo) except Exception as e: print(f"[cache refresh] {key}: {e}") def cache_invalidate(prefix: str = None) -> None: with _LOCK: if prefix is None: _CACHE.clear() else: for k in list(_CACHE.keys()): if k.startswith(prefix + ":"): del _CACHE[k] def cache_stats() -> dict: with _LOCK: return {"entradas": len(_CACHE), "claves": list(_CACHE.keys())} _background_started = False def start_background_refresh(refresh_fn: Callable[[], None], interval: int = 900): global _background_started if _background_started: return _background_started = True def _loop(): while True: time.sleep(interval) try: refresh_fn() except Exception as e: print(f"[background refresh] error: {e}") t = threading.Thread(target=_loop, daemon=True) t.start() print(f"[cache] refresco en segundo plano cada {interval}s iniciado")