WebSocket
WebSockets give you a bidirectional, full-duplex text channel between the browser and a server. WebComPy exposes them through the use_websocket composable, which returns a connection handle that is itself an async iterator of text messages.
Like SSE messages, WebSocket messages have occurrence semantics: every frame matters, duplicates included, and nothing is suppressed.
Basic usage
from webcompy.realtime import use_websocket
@define_component("live-chat")
def Chat(context):
ws = use_websocket("/api/chat")
@on_mounted
async def consume():
async for message in ws:
print(message) # each received text frame
ws.send(data) sends one text frame. Combined, the handle gives you a full round trip:
ws.send("hello") # send a text frame
async for message in ws: # receive text frames
...
The protocols parameter passes subprotocols to the server (see Connection sharing):
ws = use_websocket("/api/chat", protocols=("chat.v2",))
Binary frames sent by the server are ignored with a warning; only text frames are delivered.
Connection handle
The handle exposes four surfaces:
- Iteration —
async for message in ws:yields every received text message in arrival order. .state— aSignal[ConnectionState](CONNECTING/OPEN/RECONNECTING/CLOSED) reflecting the underlying connection..last_close— aSignal[CloseInfo | None]holding a frozenCloseInfofor the most recent close event, orNoneif the connection has never closed..send(data)— sends one text frame (see Sending while disconnected)..close()— detaches only your own subscription; it never tears down a connection that other subscribers still use.
CloseInfo field |
Meaning |
|---|---|
code |
The WebSocket close code (e.g., 1000 normal, 1006 abnormal, 1011 server error) |
reason |
The close reason string sent by the server |
was_clean |
Whether the close handshake completed cleanly |
.last_close is updated on every close of the underlying connection — including closures that reconnection later recovers — and is not reset when the connection reopens. It is the reliable way to inspect why the socket dropped:
effect(lambda: handle_last_close(ws.last_close.value))
Connection sharing
Within one app, use_websocket calls with the same URL and the same protocols share a single underlying socket. The first subscriber opens the connection; the last subscriber's close detaches it. Each subscriber receives its own FIFO queue, so a slow consumer never blocks another:
# These two calls share ONE underlying socket to /api/chat
chat_a = use_websocket("/api/chat")
chat_b = use_websocket("/api/chat")
Different protocols values select different application protocols, so they never share:
one = use_websocket("/api/chat", protocols=("chat.v2",))
two = use_websocket("/api/chat") # a separate socket
Reconnection settings (reconnect, the backoff delays, reconnect_max_attempts, buffer_while_disconnected) are properties of the shared connection, not of individual subscribers: the first call's settings win, and later calls with the same URL and protocols reuse that connection's settings. If a later call specifies different reconnection settings, a UserWarning is emitted and the existing connection's settings remain in effect.
Reconnection
Unlike the native EventSource, a native WebSocket does not reconnect automatically — the framework owns the reconnect loop. When the underlying socket closes abnormally (any close other than a clean 1000 or a user .close()), the shared connection schedules a reconnect with exponential backoff and jitter:
ws = use_websocket("/api/chat", reconnect_base_delay=1.0, reconnect_max_delay=30.0)
The delay before attempt n is min(max_delay, base_delay * 2 ** (n-1)) seconds multiplied by a random jitter factor in [0.5, 1.0] (so the first retry lands in [0.5, 1.0] s, the second in [1.0, 2.0] s, then 2–4, 4–8, capped at the max). During a backoff wait or in-flight attempt, .state is RECONNECTING; on success it returns to OPEN and iteration continues transparently.
| Parameter | Default | Meaning |
|---|---|---|
reconnect |
True |
Set False to fail once and stop |
reconnect_base_delay |
1.0 |
Base backoff in seconds |
reconnect_max_delay |
30.0 |
Backoff cap in seconds |
reconnect_max_attempts |
None |
None = unlimited; an int stops after that many failed attempts with .state == CLOSED |
No reconnect happens after a user-initiated .close(), after a clean 1000 close, or when reconnect=False (a single failure transitions to CLOSED). If you want a shared connection to reconnect on every drop, have the server close with a non-1000 code.
Sending while disconnected
ws.send(data) while the connection is not OPEN warns and discards the message by default — the framework refuses to silently queue data during an outage:
ws.send("hello") # while RECONNECTING → warns and drops, unless buffering is enabled
With buffer_while_disconnected=True, disconnected sends are buffered FIFO (unbounded — a long outage queues as much memory as you send) and flushed in order on the next transition to OPEN. The buffer is discarded if the connection reaches a terminal CLOSED. If ordering matters, wait for .state == OPEN before sending.
The gap/refetch recipe
Reconnection does not replay messages missed while disconnected (server-side replay is opt-in and out of scope). When .state returns to OPEN, re-pull the authoritative state yourself — exactly as with SSE:
from webcompy.realtime import ConnectionState
from webcompy.signal import effect
def refetch():
... # re-pull the authoritative state, e.g. a fresh HTTP request
effect(lambda: refetch() if ws.state.value == ConnectionState.OPEN else None)
Bridging to signals
Because use_websocket yields an async iterable, the signal-stream utilities integrate directly. Accumulate messages into a ReactiveList, or collapse to a single latest value with to_signal:
feed = to_reactive_list(use_websocket("/api/chat"), maxlen=50)
# feed.items: ReactiveList[str]
See Signals and Streams.
Server-side rendering
During SSR/SSG there is no browser connection: use_websocket returns an immediately-finished empty handle whose .state is CLOSED, whose .last_close is None, and whose .send() warns and discards, and emits a warning. No connection state, close info, or messages are transferred into the hydration payload.