corvix.web.sse ============== .. py:module:: corvix.web.sse .. autoapi-nested-parse:: Server-Sent Events: push snapshot updates instead of client-side polling. The server polls storage on a short interval and pushes a ``snapshot`` event only when the serialized payload actually changes, so idle connections cost a periodic comparison (plus an occasional keep-alive) rather than a full response on every tick. Browsers reconnect automatically via EventSource, and the frontend falls back to interval polling when SSE is unavailable. A short-lived, process-wide cache of the serialized body (keyed by dashboard) collapses the storage reads of concurrent connections watching the same dashboard into roughly one read per poll interval, instead of one read per connection per tick. Attributes ---------- .. autoapisummary:: corvix.web.sse.logger corvix.web.sse._SSE_DEFAULT_POLL_INTERVAL_SECONDS corvix.web.sse._SSE_KEEPALIVE_SECONDS corvix.web.sse._snapshot_body_cache corvix.web.sse._snapshot_body_cache_lock Functions --------- .. autoapisummary:: corvix.web.sse._sse_poll_interval corvix.web.sse._snapshot_event_body corvix.web.sse._cached_snapshot_event_body corvix.web.sse._snapshot_error_payload corvix.web.sse._snapshot_event_generator corvix.web.sse.events Module Contents --------------- .. py:data:: logger .. py:data:: _SSE_DEFAULT_POLL_INTERVAL_SECONDS :value: 3.0 .. py:data:: _SSE_KEEPALIVE_SECONDS :value: 20.0 .. py:function:: _sse_poll_interval() -> float Return the server-side SSE poll interval in seconds. Read from ``CORVIX_SSE_POLL_INTERVAL_SECONDS``; falls back to the default when unset, non-numeric, or non-positive. .. py:function:: _snapshot_event_body(dashboard: str | None) -> str Build the snapshot payload and serialize it to a compact JSON string. Uses msgspec (the encoder Litestar uses for the equivalent HTTP route) so the SSE body and the ``GET /api/v1/snapshot`` response share one serialization path and stay byte-for-byte consistent. .. py:data:: _snapshot_body_cache :type: dict[str | None, tuple[float, str]] .. py:data:: _snapshot_body_cache_lock .. py:function:: _cached_snapshot_event_body(dashboard: str | None, ttl: float) -> str Return the snapshot body for *dashboard*, reusing a build newer than *ttl*. Within a ``ttl``-second window (one poll interval) concurrent SSE connections watching the same dashboard share a single storage read and serialization. A strict ``age < ttl`` comparison means a lone connection, whose ticks are spaced one interval apart, still rebuilds every tick — so the cache adds no latency in the common single-client case. .. py:function:: _snapshot_error_payload(error: Exception) -> str Serialize an SSE ``snapshot-error`` payload for *error*. ``HTTPException`` carries a client-safe detail and status code; any other exception is reported generically (its message is not leaked to the client). .. py:function:: _snapshot_event_generator(dashboard: str | None) -> collections.abc.AsyncIterator[litestar.response.ServerSentEventMessage] :async: Yield SSE messages for *dashboard*, pushing only on change. Emits a ``snapshot`` event whenever the serialized payload differs from the last one sent, a ``snapshot-error`` event when the payload cannot be produced, and a comment-only keep-alive when nothing has changed for a while (so proxies do not drop an idle connection). The blocking storage read runs in a worker thread to avoid stalling the event loop. Any error building the snapshot is reported to the client and the stream keeps running, recovering on a later tick; this avoids tearing down the connection (and triggering a client reconnection storm) on a transient storage or serialization failure. .. py:function:: events(dashboard: str | None = None) -> litestar.response.ServerSentEvent :async: Stream dashboard snapshots as Server-Sent Events. Replaces fixed-interval client polling: the connection stays open and the server pushes a ``snapshot`` event only when the data changes, cutting both latency and per-cycle overhead when nothing has happened.