WebSocket and WSX
See WebSockets: WSX and raw hosting for handshake ownership, message encoding, raw hosting and buffering limits.
The websocket facade: one ASGI socket as an object, and nothing above it.
WebSocket wraps the three things an ASGI server hands a websocket
application — the scope, receive and send — and gives them a shape a
reader can follow: accept() consumes the connect and answers it,
receive_text() and receive_bytes() read one message, send_text()
and send_bytes() write one, close() ends the connection once, and
iterating the object yields the incoming texts until the client leaves.
It knows nothing of WSX. The protocol lives in wsx.py and the motor in
the server; this object is the transport, so the admitted raw seam — an
application that wants the socket itself — is served by the same class
(internals/10_server/055_websocket/decisions.md).
The state is one boolean. connected is true between the accept and the
end, and everything that depends on the state reads it: an accept happens once,
a close writes once, and a read or a write with nothing accepted raises. There
is no exported state type, because the three readers of the state are those
three rules and each is a question with a yes or a no (owner, 2026-09-06; the
precedent is WorkerConnector.connected).
A disconnect is an exception, never a value. Every read raises
WebSocketDisconnect when the client is gone, so a read loop never has to
check what it got back, and the iterator ends on it.
The handshake facts are read once, in the constructor. The path, the
headers, the cookies and the subprotocols the client offered come off the
scope — headers lowercased and TYTX-hydrated, cookies split out of the
Cookie header, exactly as Request does for HTTP.
WebSocketRegistry is the server’s picture of what is connected: the live
sockets, and the page_id → socket association a page writes when it opens
its channel, so the server can address one page later. It is NEUTRAL — it does
not know the SPA and validates nothing: whether a page belongs to the
connection asking for it is judged by the application that holds the pool.
- class genro_asgi.websocket.WebSocket(scope, receive, send)[source]
Bases:
objectOne ASGI websocket connection, as an object.
- Parameters:
scope (Scope)
receive (Receive)
send (Send)
- __init__(scope, receive, send)[source]
Args: scope: the ASGI websocket scope of the handshake. receive: the ASGI receive callable. send: the ASGI send callable.
- Parameters:
scope (MutableMapping[str, Any])
receive (Callable[[], Awaitable[MutableMapping[str, Any]]])
send (Callable[[MutableMapping[str, Any]], Awaitable[None]])
- Return type:
None
- read_handshake()[source]
Fill the header and cookie maps off the scope.
Keys are lowercased and values TYTX-hydrated;
cookiestays out of the map and becomescookies, the wayRequestreads an HTTP request. Acts on the instance; called by__init__.- Return type:
- property subprotocols: tuple[str, ...]
The subprotocols the client offered, in the order it offered them.
- async accept(subprotocol=None, headers=None)[source]
Consume the connect and accept the connection.
- Parameters:
- Raises:
RuntimeError – this socket was already accepted, or the first message on the wire was not
websocket.connect.- Return type:
Sets
connected.
- async refuse(code=1008, reason='')[source]
Turn the handshake away without accepting it.
- Parameters:
- Raises:
RuntimeError – this socket was accepted already — turning away what is already in is a
close, not a refusal.- Return type:
The connect is consumed first: a close written before it is read leaves that message on the wire. Sets
connectedto false, so nothing can be written afterwards.
- async close(code=1000, reason='')[source]
End the connection, once.
- Parameters:
- Raises:
RuntimeError – nothing was accepted yet — before the accept a handshake is turned away with
refuse, which is what a hostile Origin gets. Everything judged AFTER the accept — the home application’s cookie, an invalid credential, a server that is not running — is accepted first and closed here with its code, so the browser can read why.- Return type:
None
Sets
connectedto false. Calling it again writes nothing.
- async receive_text()[source]
The next message, as text.
- Return type:
- Returns:
The message’s text.
- Raises:
RuntimeError – this socket is not connected.
TypeError – the message carried bytes.
WebSocketDisconnect – the client is gone.
- async receive_bytes()[source]
The next message, as bytes.
- Return type:
- Returns:
The message’s bytes.
- Raises:
RuntimeError – this socket is not connected.
TypeError – the message carried text.
WebSocketDisconnect – the client is gone.
- async read_message()[source]
One raw ASGI message, with the disconnect turned into an exception.
- Return type:
- Returns:
The
websocket.receivemessage as it came.- Raises:
RuntimeError – this socket is not connected.
WebSocketDisconnect – the client is gone;
connectedis false from here on.
- async send_text(text)[source]
Write one text message.
- Raises:
RuntimeError – this socket is not connected.
- Return type:
- Parameters:
text (str)
- async send_bytes(data)[source]
Write one binary message.
- Raises:
RuntimeError – this socket is not connected.
- Return type:
- Parameters:
data (bytes)
- class genro_asgi.websocket.WebSocketRegistry[source]
Bases:
objectThe live sockets of one server, and which one each page speaks on.
- unregister(socket)[source]
Take one socket out, and every page that still speaks on IT.
A page whose association has moved to another socket — a reconnection that happened before this one closed — is left alone: the comparison is on the socket itself, never on the page. A socket that was never registered is no error: the
finallyof a handshake that failed before the accept comes through here too.
- bind_page(page_id, socket)[source]
Say that this page speaks on this socket.
- Parameters:
- Return type:
A page already bound is REBOUND, with no error: a browser that lost its socket and opened a new one says so again, and the association follows it. One socket carries as many pages as the browser has under that connection.
The WSX envelope: one message on a websocket, in either direction.
A WSX message is the text WSX:// followed by JSON. WsxEnvelope is that
message as an object, and it is built two ways — from the text a socket
delivered, or from the fields somebody is about to send:
envelope = WsxEnvelope(text) # read one
reply = WsxEnvelope(id=envelope.id, status=200, data=…) # write one
await socket.send_text(reply.encode())
The prefix is what tells a WSX message from any other text on the socket, and the routing fields parallel the internal channel’s info, so a message copies into a CALL one field at a time.
A request carries ``method`` and ``path``; an answer carries ``status``.
Both carry data and, when they belong to a page, page_id. id is
what correlates an answer with the message it answers, and its ABSENCE is
meaningful twice: a message with no id is an event nobody answers, and a
message the server sends by itself never has one. reply_path is where a
page asks to be called back when the work is done. A field nobody set does not
reach the wire — a null there would read as a value.
The application data stays serialized while routing. The outer JSON contains a TYTX string. WsxEnvelope parses only that JSON and keeps an explicit SerializedWsxPayload. Its data property is an opt-in consumer decoder; routing uses serialized_data. Value constructors and public send_message still serialize ordinary Python values. Application endpoints adapt XML/msgpack responses to browser JSON, while forwarding responses travel without application codecs.
A text that is not a WSX message raises. So does a body that is not JSON, and one that is not an object. All three are the same thing to a reader — this text is not a message of ours — and the read loop logs and moves on.
WsxConnection is one live connection speaking that protocol: serve() is
its whole life. It gates the handshake, accepts, reads messages until the
client leaves, and waits for what is still in flight. Every message with an
id becomes a synthetic HTTP request with the method WSK, handed to the
application its path names through the server’s own demux — so an
application learns no new method and a websocket message travels the road a
request travels. A message with no id is an event: served, answered by
nothing.
The gate answers in one shape: accept, then close with a readable code. A
handshake on a path no application serves, or one missing the cookie its home
application demands, closes 1008. The single exception is a hostile Origin,
refused BEFORE the accept — there is nobody to tell, because nobody was
admitted. The state of the server is judged higher up, in on_websocket,
above the demux and for every websocket alike: this gate never sees a
connection the machine had already refused.
- class genro_asgi.wsx.WsxConnection(server, scope, receive, send)[source]
Bases:
objectOne websocket connection speaking WSX, from the handshake to the end.
- Parameters:
server (Any)
scope (Scope)
receive (Receive)
send (Send)
- __init__(server, scope, receive, send)[source]
Initialize this instance.
- Parameters:
server (
Any) – the server this connection belongs to — it owns the demux, the identity, the request registry and the websocket registry.scope (
MutableMapping[str,Any]) – the ASGI websocket scope of the handshake.receive (
Callable[[],Awaitable[MutableMapping[str,Any]]]) – the ASGI receive callable.send (
Callable[[MutableMapping[str,Any]],Awaitable[None]]) – the ASGI send callable.
- Return type:
None
- class genro_asgi.wsx.WsxEnvelope(text=None, *, id=None, method=None, path=None, data=None, page_id=None, reply_path=None, status=None, serialized_data=None)[source]
Bases:
objectOne WSX message: read from its text, or built to be sent.
- Parameters:
- __init__(text=None, *, id=None, method=None, path=None, data=None, page_id=None, reply_path=None, status=None, serialized_data=None)[source]
Initialize this instance.
- Parameters:
text (
str|None) – a WSX message to read; the keywords are ignored when it is given.id (
str|None) – what correlates an answer with its message;Nonefor an event.method (
str|None) – the request’s method —WSKfor a page’s rpc.path (
str|None) – the request’s path, which names the application and the route.data (
Any) – the payload, as a Python value.page_id (
str|None) – the page the message belongs to, when it belongs to one.reply_path (
str|None) – where the page asks to be called back.status (
int|None) – the answer’s status;Noneon a request.serialized_data (SerializedWsxPayload | None)
- Raises:
ValueError –
textis not a WSX message, its body is not JSON, or that body is not an object.- Return type:
None
- read_text(text)[source]
The JSON body of one WSX message.
- Parameters:
text (
str) – the message as the socket delivered it.- Return type:
- Returns:
The body, its
datastill the TYTX string.- Raises:
ValueError – no
WSX://prefix, a body that is not JSON, or a body that is not an object.