v2.26.0: fix SSE fanout crash and branded logo FOUC

- Fix SSE fanout thread AttributeError when source queue is None during
  interpreter shutdown by snapshotting to local variable with null guard
- Fix branded "i" logo rendering oversized on first page load (FOUC) by
  adding inline width/height to SVG elements across 10 templates
- Bump version to 2.26.0 in config.py, pyproject.toml, and CHANGELOG.md

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Smittix
2026-03-13 11:51:27 +00:00
parent 00362bcd57
commit e00fbfddc1
183 changed files with 2006 additions and 4243 deletions
+57 -51
View File
@@ -2,12 +2,14 @@
from __future__ import annotations
import contextlib
import json
import queue
import threading
import time
from collections.abc import Generator
from dataclasses import dataclass, field
from typing import Any, Callable, Generator
from typing import Any, Callable
@dataclass
@@ -29,6 +31,12 @@ def _run_fanout(channel: _QueueFanoutChannel) -> None:
idle_drain_batch = 512
while True:
src = channel.source_queue
if src is None:
# Source queue was cleared (e.g. during interpreter shutdown).
time.sleep(0.5)
continue
with channel.lock:
subscribers = tuple(channel.subscribers)
@@ -39,7 +47,7 @@ def _run_fanout(channel: _QueueFanoutChannel) -> None:
drained = 0
for _ in range(idle_drain_batch):
try:
channel.source_queue.get_nowait()
src.get_nowait()
drained += 1
except queue.Empty:
break
@@ -49,7 +57,7 @@ def _run_fanout(channel: _QueueFanoutChannel) -> None:
continue
try:
msg = channel.source_queue.get(timeout=channel.source_timeout)
msg = src.get(timeout=channel.source_timeout)
except queue.Empty:
continue
@@ -153,10 +161,8 @@ def sse_stream_fanout(
msg = subscriber.get(timeout=timeout)
last_keepalive = time.time()
if on_message and isinstance(msg, dict):
try:
with contextlib.suppress(Exception):
on_message(msg)
except Exception:
pass
yield format_sse(msg)
except queue.Empty:
now = time.time()
@@ -174,7 +180,7 @@ def sse_stream(
stop_check: Callable[[], bool] | None = None,
channel_key: str | None = None,
) -> Generator[str, None, None]:
"""
"""
Generate SSE stream from a queue.
Args:
@@ -195,47 +201,47 @@ def sse_stream(
keepalive_interval=keepalive_interval,
stop_check=stop_check,
)
def format_sse(data: dict[str, Any] | str, event: str | None = None) -> str:
"""
Format data as SSE message.
Args:
data: Data to send (will be JSON encoded if dict)
event: Optional event name
Returns:
SSE formatted string
"""
if isinstance(data, dict):
data = json.dumps(data)
lines = []
if event:
lines.append(f"event: {event}")
lines.append(f"data: {data}")
lines.append("")
lines.append("")
return '\n'.join(lines)
def clear_queue(q: queue.Queue) -> int:
"""
Clear all items from a queue.
Args:
q: Queue to clear
Returns:
Number of items cleared
"""
count = 0
while True:
try:
q.get_nowait()
count += 1
except queue.Empty:
break
return count
def format_sse(data: dict[str, Any] | str, event: str | None = None) -> str:
"""
Format data as SSE message.
Args:
data: Data to send (will be JSON encoded if dict)
event: Optional event name
Returns:
SSE formatted string
"""
if isinstance(data, dict):
data = json.dumps(data)
lines = []
if event:
lines.append(f"event: {event}")
lines.append(f"data: {data}")
lines.append("")
lines.append("")
return '\n'.join(lines)
def clear_queue(q: queue.Queue) -> int:
"""
Clear all items from a queue.
Args:
q: Queue to clear
Returns:
Number of items cleared
"""
count = 0
while True:
try:
q.get_nowait()
count += 1
except queue.Empty:
break
return count