# app/api/endpoints/events.py — SSE stream for node status transitions # # Clients connect to GET /api/chains/{chain_id}/events and receive # server-sent events whenever a node in that chain changes status. # # Event format: # event: node-status # data: {"node_id": "...", "status": "generating"|"ready"|"error", ...} from __future__ import annotations import asyncio import json import logging from fastapi import APIRouter, Depends from sse_starlette.sse import EventSourceResponse from app.api.deps import get_conn from app.api.events_store import subscribe, unsubscribe logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/chains", tags=["events"]) _KEEPALIVE_S = 15 # send a comment ping every N seconds to keep the connection alive @router.get("/{chain_id}/events") async def chain_events(chain_id: str, conn=Depends(get_conn)) -> EventSourceResponse: """ SSE stream for node status transitions in a chain. Emits 'node-status' events. Closes when the client disconnects. """ q = subscribe(chain_id) async def generator(): try: while True: try: event = await asyncio.wait_for(q.get(), timeout=_KEEPALIVE_S) yield { "event": "node-status", "data": json.dumps(event), } except asyncio.TimeoutError: # Send keepalive comment to prevent proxy/browser timeout yield {"comment": "keepalive"} except asyncio.CancelledError: pass finally: unsubscribe(chain_id, q) logger.debug("SSE client disconnected from chain %s", chain_id) return EventSourceResponse(generator())