Websocket¶
Version: 0.3 · Last Updated: 2026-09-08 · Status: 🔴 DA REVISIONARE
How a message on a socket becomes a method call: who holds the connection, what a message looks like, how it reaches an application in this process or a worker in another, and how the server addresses one page by itself.
The anatomy¶
| Part | What it is |
|---|---|
WebSocket |
the neutral facade over the ASGI websocket: accept with subprotocol and headers, an idempotent close, text and bytes both ways, and an iterator over incoming messages. It knows nothing of WSX, so the admitted raw mode uses the same object |
WsxEnvelope |
one message as a class: id?, method, path, data, page_id?, reply_path?, and the answer's id, status, data. The WSX:// prefix is its marker, and data is the TYTX string |
WsxConnection |
one per connection: it accepts, gates, resolves the identity once, then reads messages and serves each on a task of its own under a per-connection ceiling |
WebSocketRegistry |
every live connection of the server, and the page_id → socket association openchannel writes. Neutral: it knows no application |
BaseServer.on_websocket |
the entrance: it checks server state before demux, hands the raw socket to an application that defines serve_websocket, and otherwise builds a WsxConnection and drives it |
WsxControl |
the front's routing class under _wsx: openchannel, the command a page sends before anything of its own |
WsxCommands |
the worker's dispatcher branch for the same command, where the channel is written on the page's row |
server/websocket |
the config element: origins, max_concurrent |
1. The handshake, in order¶
The server checks its state before resolving an application or accepting the connection. For WSX, the application, Origin and cookie gates precede message dispatch.
- The state. A server that is not
RUNNINGrefuses before accept, for both WSX and raw applications. The browser sees a failed handshake rather than a readable 1013 close frame. The state gate precedes demux, as recorded in WebSocket decisions. - The Origin, BEFORE the accept. A rejected origin must not get an
accepted WSX socket. With
originsdeclared the header must be in the list; without it, same-origin. - The accept.
- The identity, once. The header first through the server's own authentication, then the session from the cookie. An invalid credential is accepted and closed 1008: the answer must be readable by the client.
- The home application. The handshake's path names it through the server's
demux, and its
handshake_cookiesays which cookie the socket must carry. Missing cookie → 1008 «connection cookie required». No application at that path → 1008 «no application at this path». A home application whosehandshake_cookieisNonehas no cookie gate. - The registration. The connection enters the registry, and leaves it in
the
finallyof the read loop, whatever ends it.
2. A message becomes a request¶
The envelope is read, and its path is the address. What happens next is the
core's ordinary dispatch, reached through a synthetic scope:
type: "http",method: "WSK", the envelope'spath, the handshake's headers and query string, and theauthandsessioncopied from the handshake's own scope — an identity is not re-derived per message;- the server's demux picks the application, and the application is called directly. NEVER the server itself: the middleware chain must not run once per message;
- the synthetic
receivehandsdataas the body; the syntheticsendcollects the status, the headers and the body, and the answer'sdatais read back from it. A response that begins as a stream is refused with an explicit error; - an
HTTPExceptionbecomes the answer'sstatus; anything else becomes 500 and a log line.
A message carrying an id is registered in the server's RequestRegistry,
exactly like an HTTP request: the shutdown waits for it, Request.db closes
what it opened, and the in-flight picture is complete. A message with no id
is an event: executed, unanswered, unregistered.
3. The SPA's road¶
For the SPA the synthetic request is packed and sent down the lane, and the worker serves it as it serves an HTTP request:
browser → WsxConnection → SpaApplication → SpaCommander.serve_request
→ CALL http → SpaWorker._serve_request → hosted_app_seam → the page
Three things ride along the way:
page_idandreply_path, added to thehttpdict beside the connection id when the envelope carries them, and written into the environ and the scope asgenro.page_idandgenro.reply_path. Absent in the envelope means absent in the environ — notNone.openchannel, the mandatory first message of a page. It is a route under the front's own_wsxroot, and it carries a payload form of its own — nohttpdict — down to the worker, through the same barrier and the same placement a request meets. At the worker it shares the prologue of a request, because the page's user may be frozen and the row must be in memory beforewsxcan be written on it.- The per-page queue. With
sequentialdeclared, the row's own lock is taken around the serving, in the CALL's own task, with the slot open and the pendings counted. Without it, messages of the same page are served in parallel, like the page's HTTP calls.
The cid validates, it does not choose. At openchannel, at every message
addressed to the SPA and at every push, the front checks that the commander's
page_connection_map names the same connection id the handshake carried. A
page that is not the caller's own is answered 403. The map is already up to
date when the browser sends openchannel, because the birth of a page rides
the REPLY of the HTTP request that created it.
4. The server speaks first¶
A page is addressed by SpaWorker.send_message(page_id, path, data). The
worker reads the connection off the page's row and places a CALL upward; the
front's websocket branch, mounted under the commander's operations, finds the
socket the page_id is associated with, validates it against
page_connection_map, and writes a message in the shape of a request — no
id. The reply says «written on the socket» or «no websocket for this page».
Reconnection and death need no protocol of their own. A new openchannel for
the same page_id on another socket replaces the association; closing a socket
removes only the associations still pointing at it; a page the fold already
dropped fails validation at the first touch and is discarded. A page_id is
never reused.
5. Order, honestly¶
At the server every message is a task created in the order the socket is read. The CALL goes down in the order the tasks reach the commander, and the per-page lock is FIFO among those waiting. But a message that meets the user's barrier during a transfer and one that arrives after it can swap places. So the core guarantees ordering in the absence of transfers, and mutual exclusion always. Whoever needs total ordering between its own writes waits for the answer or carries a revision of its own: a save barrier is the application's.
6. What stands beside this¶
- 025 routing system — the tree a message is resolved on is the same tree an HTTP request is resolved on, filters included.
- 030 middleware — the chain carries HTTP and says so; a websocket scope passes straight through it. What the chain would have done at the handshake is done once by the handshake itself.
- 050 authentication — the avatar of the handshake, and the 401-vs-403 rule the refusal follows.
- 040 sessions — the session read from the cookie at the handshake and kept for the connection.
- 20 spa — the front, the commander and the worker the SPA's messages travel through, and the ASGI seam of the worker.