"""Hub Server-Sent Events: um único fluxo /eventos substitui os pollings do painel.""" from collections import deque import json import threading import time from urllib.parse import parse_qs, urlsplit PING = 15 class Eventos: def __init__(self, buffer=200): self.cond = threading.Condition() self.seq = 0 self.buffer = deque(maxlen=buffer) self.fechado = False def publicar(self, tipo, dados): corpo = json.dumps(dados, ensure_ascii=False) with self.cond: self.seq += 1 self.buffer.append((self.seq, tipo, corpo)) self.cond.notify_all() return self.seq def desde(self, seq): with self.cond: return [e for e in self.buffer if e[0] > seq] def fechar(self): with self.cond: self.fechado = True self.cond.notify_all() def atender(self, h): url = urlsplit(h.path) try: ultimo = int(h.headers.get('Last-Event-ID') or parse_qs(url.query).get('desde', ['0'])[0]) except ValueError: ultimo = 0 with self.cond: # Cliente novo (ou que perdeu demais): começa do agora e lê /estado. if ultimo <= 0 or (self.buffer and ultimo < self.buffer[0][0] - 1) or ultimo > self.seq: ultimo = self.seq h.send_response(200) h.send_header('Content-Type', 'text/event-stream; charset=utf-8') h.send_header('Cache-Control', 'no-store') h.send_header('X-Accel-Buffering', 'no') h.end_headers() try: h.wfile.write(f'retry: 3000\nevent: pronto\nid: {ultimo}\ndata: {{"seq": {ultimo}}}\n\n'.encode()) while True: limite = time.monotonic() + PING with self.cond: while not self.fechado and (not self.buffer or self.buffer[-1][0] <= ultimo): resta = limite - time.monotonic() if resta <= 0: break self.cond.wait(resta) if self.fechado: return novos = [e for e in self.buffer if e[0] > ultimo] if not novos: h.wfile.write(b': ping\n\n') continue for seq, tipo, corpo in novos: h.wfile.write(f'id: {seq}\nevent: {tipo}\ndata: {corpo}\n\n'.encode()) ultimo = seq except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError): return