Websocket request correlation¶
The viewer and the Python websocket server exchange one flatbuffer envelope,
wsock.Message (src/flatbuffers/schemas/message.fbs). Until the field
below was added, a reply could only be matched to the command that caused it by
verb (ServerReply.reply_to): nothing could be awaited, and two commands of
the same verb in flight were indistinguishable.
The field¶
Message.request_id: string (optional; appended last, so older readers and
writers stay wire-compatible). A client that wants to await a reply assigns an
id and sets it on the command. An empty or absent value means “not correlated”.
The echo rule¶
Every reply the server sends in answer to a command echoes that command’s
request_id. This includes SERVER_REPLY, MESH_INFO_REPLY,
LIST_WEB_CLIENTS and ERROR replies (an unknown command, or a handler
that raised after the command parsed), so an awaiting caller always settles
rather than timing out. Unsolicited pushes — a scene view pushed after a
finished procedure, a ping, a forwarded message — carry no request_id
(forwarded bytes pass through unchanged, so an id set by the original sender
survives forwarding).
On the Python side all such replies are built through
ada.comms.msg_handling.reply_to(message, **fields); both the websocket
handlers and the REST /rpc builders use it. A handler must never set
request_id itself. Verbs that send no reply (UPDATE_SCENE,
UPDATE_SERVER, DELETE_FILE_OBJECT, SHUTDOWN_SERVER, …) are
unchanged.
The client API¶
comms.request(build, opts?) on the transport interface
(src/frontend/src/utils/comms/types.ts), implemented by both WSComms
and RESTComms on top of utils/comms/wsRequests.ts:
const reply: Message = await comms.request((requestId) => {
const builder = new flatbuffers.Builder(1024);
const idOffset = builder.createString(requestId);
Message.startMessage(builder);
Message.addCommandType(builder, CommandType.LIST_PROCEDURES);
Message.addTargetGroup(builder, TargetType.SERVER);
Message.addClientType(builder, TargetType.WEB);
Message.addRequestId(builder, idOffset);
builder.finish(Message.endMessage(builder));
return builder.asUint8Array();
}, { timeoutMs: 30_000 });
build receives the id and must bake it into the serialized message. The
promise resolves with the reply that echoes the id, and rejects on timeout,
when the transport closes, when sending fails, or when the reply is a server
ERROR (ServerReplyError, which carries the decoded reply).
Incoming buffers are routed to the pending-request map first. A correlated
reply resolves its promise and is not passed on; only uncorrelated messages
fall through to the verb-keyed switch(replyTo) dispatcher in
utils/fb_handling/handle_incoming_buffers.ts, which continues to serve
unsolicited pushes and the verbs that are still fire-and-forget. A verb
converted to the awaited form therefore updates any store itself from the
resolved reply — request_list_of_nodes is the reference conversion.
Building on it¶
The capabilities seam (src/frontend/src/services/capabilities/) can now
implement websocket verbs as ordinary async functions returning the typed
reply, instead of sending bytes and watching a store for a side effect.