Первый коммит: модульный монолит ядра (.NET 10) и gRPC-сервисы ai/ml/telegram, фронтенд Vue 3/Vite/Tailwind, документация (ТЗ, инструкция пользователя, техдокументация, код-стайл), бэклог, скрипты развёртывания и архив прототипа LeadRadar.
52 lines
1.8 KiB
Python
52 lines
1.8 KiB
Python
"""SSE-брокер событий.
|
|
|
|
Сервер рассылает события подключённым браузерам:
|
|
new_lead, lead_updated, toast, reminder_due, project_updated, system_status.
|
|
Поток однонаправленный, по ТЗ — Server-Sent Events с автопереподключением.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
class Broker:
|
|
def __init__(self) -> None:
|
|
self._subscribers: set[asyncio.Queue] = set()
|
|
self._lock = asyncio.Lock()
|
|
|
|
async def subscribe(self) -> asyncio.Queue:
|
|
q: asyncio.Queue = asyncio.Queue(maxsize=200)
|
|
async with self._lock:
|
|
self._subscribers.add(q)
|
|
return q
|
|
|
|
async def unsubscribe(self, q: asyncio.Queue) -> None:
|
|
async with self._lock:
|
|
self._subscribers.discard(q)
|
|
|
|
async def publish(self, event_type: str, data: Any) -> None:
|
|
payload = f"event: {event_type}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n"
|
|
async with self._lock:
|
|
subs = list(self._subscribers)
|
|
for q in subs:
|
|
try:
|
|
q.put_nowait(payload)
|
|
except asyncio.QueueFull:
|
|
# при переполнении сбрасываем очередь подписчика — браузер переподключится
|
|
try:
|
|
q.get_nowait()
|
|
except Exception:
|
|
pass
|
|
try:
|
|
q.put_nowait(payload)
|
|
except Exception:
|
|
pass
|
|
|
|
async def publish_toast(self, text: str, icon: str = "check") -> None:
|
|
await self.publish("toast", {"text": text, "icon": icon})
|
|
|
|
|
|
broker = Broker()
|