from __future__ import annotations import asyncio import time from typing import Any DEFAULT_MAX_ENTRIES = 1000 DEFAULT_TTL_S = 600 class FeishuSentCache: def __init__(self, max_entries: int = DEFAULT_MAX_ENTRIES, ttl_s: int = DEFAULT_TTL_S): self._cache: dict[str, dict[str, Any]] = {} self._timestamps: dict[str, float] = {} self._max_entries = max_entries self._ttl_s = ttl_s self._lock = asyncio.Lock() async def cache_sent(self, msg_id: str, chat_id: str, metadata: dict[str, Any] | None = None) -> None: async with self._lock: self._evict_expired() key = self._make_key(msg_id, chat_id) self._cache[key] = {"message_id": msg_id, "chat_id": chat_id, "metadata": metadata or {}} self._timestamps[key] = time.monotonic() if len(self._cache) > self._max_entries: oldest = min(self._timestamps, key=self._timestamps.get) self._cache.pop(oldest, None) self._timestamps.pop(oldest, None) async def get_sent(self, msg_id: str, chat_id: str) -> dict[str, Any] | None: async with self._lock: key = self._make_key(msg_id, chat_id) entry = self._cache.get(key) if entry is None: return None ts = self._timestamps.get(key, 0) if time.monotonic() - ts > self._ttl_s: self._cache.pop(key, None) self._timestamps.pop(key, None) return None return entry async def invalidate(self, msg_id: str, chat_id: str) -> None: async with self._lock: key = self._make_key(msg_id, chat_id) self._cache.pop(key, None) self._timestamps.pop(key, None) @staticmethod def _make_key(msg_id: str, chat_id: str) -> str: return f"{chat_id}:{msg_id}" def _evict_expired(self) -> None: now = time.monotonic() expired = [k for k, ts in self._timestamps.items() if now - ts > self._ttl_s] for k in expired: self._cache.pop(k, None) self._timestamps.pop(k, None) async def clear(self) -> None: async with self._lock: self._cache.clear() self._timestamps.clear()