50 lines
1.7 KiB
Python
50 lines
1.7 KiB
Python
"""Server-Sent Event bus for pushing updates to displays.
|
|
|
|
Events are scoped by user_id so that each user's display only receives
|
|
updates about their own data (alarms, calendar, pomodoro, ...).
|
|
"""
|
|
import json
|
|
import queue
|
|
import threading
|
|
|
|
|
|
class EventBus:
|
|
"""Simple pub/sub event system with SSE support.
|
|
|
|
publish(event_type, data, user_id=None): when user_id is given, only
|
|
subscribers registered for that user receive the event.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self._subscribers: list[tuple[int | None, queue.Queue]] = []
|
|
self._lock = threading.Lock()
|
|
|
|
def subscribe(self, user_id: int | None = None) -> queue.Queue:
|
|
"""Subscribe to events. user_id=None receives ALL events (broadcast)."""
|
|
q: queue.Queue = queue.Queue()
|
|
with self._lock:
|
|
self._subscribers.append((user_id, q))
|
|
return q
|
|
|
|
def unsubscribe(self, q: queue.Queue):
|
|
with self._lock:
|
|
self._subscribers = [(uid, sq) for uid, sq in self._subscribers if sq is not q]
|
|
|
|
def publish(self, event_type: str, data: dict, user_id: int | None = None):
|
|
msg = json.dumps({"type": event_type, "data": data})
|
|
with self._lock:
|
|
dead = []
|
|
for sub_uid, q in self._subscribers:
|
|
if sub_uid is not None and user_id is not None and sub_uid != user_id:
|
|
continue
|
|
try:
|
|
q.put_nowait(msg)
|
|
except queue.Full:
|
|
dead.append((sub_uid, q))
|
|
for item in dead:
|
|
self._subscribers.remove(item)
|
|
|
|
def broadcast(self):
|
|
"""Send heartbeat to all subscribers."""
|
|
self.publish("heartbeat", {"ts": __import__("time").time()})
|