"""Bounded background refresh orchestration for registered knowledge sources.""" from __future__ import annotations import asyncio from collections.abc import Awaitable, Callable, Iterable from uuid import UUID from packages.connector_kit.knowledge import ( IndexReport, InMemoryKnowledgeIndex, ) from packages.connector_kit.knowledge import ( KnowledgeRefreshJob as SingleRefreshJob, ) class KnowledgeRefreshScheduler: """Cooperative scheduler; it never accepts a caller-supplied physical path.""" def __init__( self, index: InMemoryKnowledgeIndex, source_ids: Iterable[UUID], *, interval_seconds: float = 300.0, on_refresh: Callable[[tuple[IndexReport, ...]], Awaitable[None]] | None = None, ) -> None: if not 1 <= interval_seconds <= 86_400: raise ValueError("refresh interval is outside bounds") self.index = index self.source_ids = tuple(source_ids) self.interval_seconds = interval_seconds self.on_refresh = on_refresh self._task: asyncio.Task[None] | None = None self._stop = asyncio.Event() async def run_once(self) -> tuple[IndexReport, ...]: reports = SingleRefreshJob(self.index, self.source_ids).run_once() if self.on_refresh is not None: await self.on_refresh(reports) return reports async def start(self) -> None: if self._task is not None and not self._task.done(): return self._stop.clear() self._task = asyncio.create_task(self._loop()) async def stop(self) -> None: self._stop.set() task = self._task self._task = None if task is not None: await task async def _loop(self) -> None: while not self._stop.is_set(): await self.run_once() try: await asyncio.wait_for(self._stop.wait(), timeout=self.interval_seconds) except TimeoutError: continue