from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.model.memory import MemorySyncOutbox class ProjectionReconciliationService: """Finds projection events that still need delivery or replay.""" def __init__(self, session: AsyncSession) -> None: self.session = session async def pending(self, *, limit: int = 100) -> list[MemorySyncOutbox]: now = datetime.now(UTC).replace(tzinfo=None) result = await self.session.scalars( select(MemorySyncOutbox) .where( MemorySyncOutbox.status.in_({"pending", "failed"}), ( MemorySyncOutbox.next_retry_at.is_(None) | (MemorySyncOutbox.next_retry_at <= now) ), ) .order_by(MemorySyncOutbox.id) .limit(max(1, min(limit, 1000))) ) return list(result) async def mark_replay(self, event_uuid: str) -> bool: event = await self.session.scalar( select(MemorySyncOutbox).where(MemorySyncOutbox.event_uuid == event_uuid) ) if event is None or event.status == "processed": return False event.status = "pending" event.next_retry_at = None event.last_error = None await self.session.commit() return True