Skip to main content

@tenmol/client

The browser side of the transport: one WebSocket to the PyMOL bridge, request/response correlation, topic events, binary frame decoding, and a Proxy-based cmd facade. ~1,270 lines, one dependency (@tenmol/protocol).

What it exports

Subpath exports: . ./connection ./cmd ./events.

Five behaviours that are not obvious from the types

1. cmd is a Proxy, so any symbol is callable without a declaration. cmd.util.cbc() becomes fn: 'util.cbc'. A trailing plain object is sent as Python **kwargs; arrays, typed arrays, Date and class instances stay positional, which matters because cmd.set_view([...18 floats]) and cmd.load_cgo([...]) take list arguments. Use cmd.$call<T>(fn, args, kwargs) when you want to bypass the sniffing or type the result, and cmd.$any for the untyped surface. About 15 commands have hand-written signatures; the rest are any, because noUncheckedIndexedAccess would otherwise make every dynamic call “possibly undefined”. 2. Requests are queued while the socket is down; input frames are not. call/do/sub/unsub buffer into an outbox (cap 256, then reject) and flush on open. sendInput() returns false and drops the frame — replaying stale mouse events after a reconnect would make the camera jump. Anything that sends input has to re-send its state on reconnect; reshape is the example. 3. requestTimeoutMs defaults to 0, i.e. no timeout. PyMOL calls can be genuinely slow (cmd.ray, a large load). A hung request therefore hangs forever unless you opt in. 4. Re-subscription is automatic and de-duplicated. Live topics are re-subed after a reconnect. A sub issued while the socket was down is both remembered and queued, and the queued frame wins — because it carries the id the caller is awaiting. Without that check, four topics produced eight sub frames on first connect. 5. EventMessage.seq is dropped. handleText() emits message.payload and not message.seq, so the sequence-gap detection in @tenmol/stores’s bridgeBinding.ts is inert and reports seqAvailable: false. Forwarding seq here is a one-line change that turns that machinery on; nothing downstream needs to move.

Binary frames

Every binary frame is decoded once with decodeBinaryFrame and emitted three ways: binary:frame (all of them, in arrival order — what the viewport subscribes to), then pixels:frame or geometry:frame. Using decodeGeometryFrame here instead is a known trap: it throws on a Mode-P pixel frame by design, which turned every streamed frame into a connection error.

Tests

This package has no test files of its own. It is covered indirectly by apps/web/e2e (which drives a real socket against a real bridge) and by the app’s feature tests, which inject a fake connection. Only typecheck runs here:
If you change reconnect, correlation or kwargs behaviour, the cheapest real proof is node apps/web/e2e/run.mjs -t "the app loads and connects".