roglag / app /realtime.py
deploy
deploy ROGLAG threat console
c4f5819
Raw
History Blame Contribute Delete
2.8 kB
"""Realtime fan-out over WebSocket.
A single in-process connection manager broadcasts events/alerts to every
connected browser. Run the API with a single worker so all sockets share this
manager; for multi-worker scale put Redis pub/sub in front (see docs).
"""
from __future__ import annotations
import asyncio
from typing import Any
from fastapi import WebSocket
class ConnectionManager:
def __init__(self) -> None:
self._connections: set[WebSocket] = set()
self._lock = asyncio.Lock()
async def connect(self, ws: WebSocket) -> None:
await ws.accept()
async with self._lock:
self._connections.add(ws)
async def disconnect(self, ws: WebSocket) -> None:
async with self._lock:
self._connections.discard(ws)
@property
def count(self) -> int:
return len(self._connections)
async def broadcast(self, message: dict[str, Any]) -> None:
if not self._connections:
return
async with self._lock:
targets = list(self._connections)
dead: list[WebSocket] = []
for ws in targets:
try:
await ws.send_json(message)
except Exception:
dead.append(ws)
if dead:
async with self._lock:
for ws in dead:
self._connections.discard(ws)
manager = ConnectionManager()
class StreamController:
"""Background task that emits mock logs at an interval until stopped."""
def __init__(self) -> None:
self._task: asyncio.Task | None = None
self.running = False
self.rate = 3 # events per tick
self.interval = 1.0 # seconds per tick
self.profile = "mixed"
def is_running(self) -> bool:
return self.running and self._task is not None and not self._task.done()
async def start(self, emit_coro, rate: int = 3, interval: float = 1.0, profile: str = "mixed") -> None:
if self.is_running():
return
self.rate, self.interval, self.profile = rate, interval, profile
self.running = True
self._task = asyncio.create_task(self._run(emit_coro))
async def _run(self, emit_coro) -> None:
try:
while self.running:
await emit_coro(self.rate, self.profile)
await asyncio.sleep(self.interval)
except asyncio.CancelledError:
pass
finally:
self.running = False
async def stop(self) -> None:
self.running = False
if self._task:
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
stream = StreamController()