"""Sync orchestration: read moodtracker's SQLite file -> idempotent upsert. Single stream ('moodtracker_entries'), cursor = highest source_id ingested so far. Each run re-checks a small overlap of already-synced ids (config.OVERLAP_ROWS) as a cheap safety net, then upserts anything with id > cursor - overlap. """ import time from . import config, store from .mood_source import connect_source, fetch_entries_since STREAM_KEY = "moodtracker_entries" def run_sync(conn, source_db_path=None): """One sync pass. Returns a counts dict. Never raises — errors are recorded in ingest_runs/sync_state and returned in totals['errors'].""" source_db_path = source_db_path or config.SOURCE_DB_PATH run_id = store.start_run(conn) totals = {"entries": 0, "errors": []} try: last = store.get_last_synced_id(conn, STREAM_KEY) since = max(0, last - config.OVERLAP_ROWS) source_conn = connect_source(source_db_path) try: rows = fetch_entries_since(source_conn, since) finally: source_conn.close() n = store.upsert_entries(conn, rows) totals["entries"] = n max_id = max((r["id"] for r in rows), default=last) store.set_sync_state(conn, STREAM_KEY, max(max_id, last)) store.finish_run(conn, run_id, "ok", entries=n) except Exception as e: # noqa: BLE001 - isolate failures, keep the loop alive totals["errors"].append(str(e)) store.set_sync_state( conn, STREAM_KEY, store.get_last_synced_id(conn, STREAM_KEY), status="error", error=str(e), ) store.finish_run(conn, run_id, "error", entries=0, error=str(e)) return totals def sync_loop(conn, source_db_path=None, interval=None, once=False): interval = interval or config.SYNC_INTERVAL_SECONDS while True: yield run_sync(conn, source_db_path) if once: return time.sleep(interval)