Spaces:
Running
Running
| import asyncio | |
| import json | |
| import logging | |
| from fastapi import APIRouter | |
| from sse_starlette.sse import EventSourceResponse | |
| from app.services.note_store import get_note | |
| from app.models.enums import NoteStatus | |
| logger = logging.getLogger(__name__) | |
| router = APIRouter() | |
| async def note_events(note_id: str): | |
| """ | |
| Server-Sent Events endpoint: | |
| - Push note.status whenever it changes | |
| - Close stream when status == ready or error | |
| """ | |
| async def event_generator(): | |
| last_status = None | |
| try: | |
| while True: | |
| note = get_note(note_id) | |
| if not note: | |
| # note chưa được tạo, chờ | |
| await asyncio.sleep(0.5) | |
| continue | |
| status = note.get("status") | |
| # Push only when status changes | |
| if status != last_status: | |
| yield { | |
| "event": "status", | |
| "data": json.dumps({ | |
| "note_id": note_id, | |
| "status": status, | |
| }), | |
| } | |
| last_status = status | |
| # Terminal states → close SSE | |
| if status in ( | |
| NoteStatus.ready, | |
| NoteStatus.error, | |
| ): | |
| break | |
| # Avoid hammering Firestore | |
| await asyncio.sleep(1.0) | |
| except asyncio.CancelledError: | |
| # Client disconnected (normal) | |
| logger.info( | |
| "[notes_events] SSE client disconnected note_id=%s", | |
| note_id, | |
| ) | |
| except Exception as e: | |
| logger.exception( | |
| "[notes_events] SSE error note_id=%s: %s", | |
| note_id, | |
| e, | |
| ) | |
| yield { | |
| "event": "error", | |
| "data": json.dumps({ | |
| "note_id": note_id, | |
| "status": "internal_error", | |
| }), | |
| } | |
| return EventSourceResponse( | |
| event_generator(), | |
| ping=15, # keep-alive, tránh proxy/nginx kill connection | |
| ) | |