From de5ea155a0cf3b3445b0f3e5bb3c4f6f8b202021 Mon Sep 17 00:00:00 2001 From: GitHub Copilot Date: Sat, 24 Jan 2026 12:20:49 +0000 Subject: [PATCH] Throttle SSE notifications and debounce client refreshes Server-side: - Limit SSE notifications to max once per second per route - Track last notification time per route Client-side: - Debounce screenshot refreshes with 2s minimum interval per tile - Pending refreshes are scheduled if events arrive during debounce window --- src/textual_webterm/local_server.py | 34 +++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/textual_webterm/local_server.py b/src/textual_webterm/local_server.py index f14cf41..c4c8951 100644 --- a/src/textual_webterm/local_server.py +++ b/src/textual_webterm/local_server.py @@ -147,9 +147,13 @@ def _rewrite_svg_fonts(svg: str) -> str: class LocalServer: def mark_route_activity(self, route_key: str) -> None: - self._route_last_activity[route_key] = asyncio.get_event_loop().time() - # Notify SSE subscribers of activity - self._notify_activity(route_key) + now = asyncio.get_event_loop().time() + self._route_last_activity[route_key] = now + # Throttle SSE notifications - max once per second per route + last_notified = self._route_last_sse_notification.get(route_key, 0.0) + if now - last_notified >= 1.0: + self._route_last_sse_notification[route_key] = now + self._notify_activity(route_key) def _notify_activity(self, route_key: str) -> None: """Notify SSE subscribers that a route has activity.""" @@ -222,6 +226,7 @@ class LocalServer: self._screenshot_cache_etag: dict[str, str] = {} self._screenshot_locks: dict[str, asyncio.Lock] = {} self._route_last_activity: dict[str, float] = {} + self._route_last_sse_notification: dict[str, float] = {} # SSE subscribers for activity notifications self._sse_subscribers: list[asyncio.Queue[str]] = [] @@ -874,12 +879,33 @@ class LocalServer: // SSE connection for real-time screenshot updates let eventSource = null; let sparklineTimer = null; + // Debounce tracking per tile + const pendingRefresh = {{}}; + const lastRefresh = {{}}; + const REFRESH_DEBOUNCE_MS = 2000; // Min 2s between refreshes per tile + + function scheduleRefreshTile(slug) {{ + const now = Date.now(); + const last = lastRefresh[slug] || 0; + // If we refreshed recently, schedule for later + if (now - last < REFRESH_DEBOUNCE_MS) {{ + if (!pendingRefresh[slug]) {{ + pendingRefresh[slug] = setTimeout(() => {{ + pendingRefresh[slug] = null; + refreshTile(slug); + }}, REFRESH_DEBOUNCE_MS - (now - last)); + }} + return; + }} + refreshTile(slug); + lastRefresh[slug] = now; + }} function startSSE() {{ if (eventSource) return; eventSource = new EventSource('/events'); eventSource.addEventListener('activity', (e) => {{ - refreshTile(e.data); + scheduleRefreshTile(e.data); }}); eventSource.onerror = () => {{ // Reconnect on error