project_stream.py 1.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849
  1. import json
  2. from fastapi import APIRouter, Depends, Request
  3. from fastapi.responses import StreamingResponse
  4. from app.api import deps
  5. from app.core.events import broadcast
  6. from app.models import OrgRole, User
  7. router = APIRouter()
  8. @router.get("/project/{project_id}/stream", summary="Real-Time SSE Stream")
  9. async def project_stream(
  10. project_id: str,
  11. client_id: str,
  12. request: Request,
  13. current_user: User = Depends(
  14. deps.require_org_role(OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION)
  15. ),
  16. ):
  17. """
  18. Connect to this endpoint to receive real-time updates.
  19. **Required Query Parameters:**
  20. - `client_id`: A unique UUID generated by the frontend to prevent echoing.
  21. """
  22. async def event_generator():
  23. channel = f"project_{project_id}"
  24. # Subscribe to the Redis channel specific to this project
  25. async with broadcast.subscribe(channel=channel) as subscriber:
  26. async for event in subscriber:
  27. # If the user closes the tab or navigates away, cleanly close the connection
  28. if await request.is_disconnected():
  29. break
  30. # Parse the raw string payload from Redis
  31. message = json.loads(event.message)
  32. # Echo prevention: Do not send the event back to the client that caused it
  33. if message.get("client_id") == client_id:
  34. continue
  35. # Yield the properly formatted Server-Sent Event (SSE)
  36. yield f"event: {message['event']}\ndata: {json.dumps(message['item'])}\n\n"
  37. # Return as text/event-stream so the browser knows it's an ongoing connection
  38. return StreamingResponse(event_generator(), media_type="text/event-stream")