Skip to main content

@tenmol/protocol

The wire contract between the browser and the Python bridge. Pure types, constants and pure functions: zero runtime dependencies, no I/O, importable from the browser, from Node, from a worker and from a test. packages/bridge/tenmol_bridge/session.py is the Python mirror of this package. The strings must match exactly on both sides.

What it exports

import { ... } from '@tenmol/protocol' re-exports errors, envelope, codec, geometry and topics. Everything else is reached by subpath.

src/topics/ holds two different things

19 wire topics — the TOPICS tuple in _registry.ts, re-exported through the barrel: feedback progress redisplay pixels view selection objects menu settings wizard editor dialog frame scenes movie_panel seqview colors plugin geometry. These are the only strings a client may sub to. 7 shared payload modules that are not topics and are deliberately absent from the barrel: builder compute console dialogs files menus movie. They type the request/reply shapes of panels that answer over ordinary call frames, and are imported by subpath only — import { ... } from '@tenmol/protocol/topics/files'. Adding one of those is not a protocol change; adding a wire topic is.

Three rules that are not negotiable

1. topics/index.ts and topics/_registry.ts are FROZEN. Every would-be shared file in this repo is a directory of one-file-per-owner modules behind a barrel that was written once. A package fills in its own topics/<name>.ts and edits nothing shared. The contract each owner honours is one line: keep exporting a type named <Name>Payload. topics/index.ts carries a compile-time assertion that TOPICS and TopicPayloads still agree, so drift is a build failure rather than a runtime surprise. 2. The binary frame header is 4-byte padded.
A WebSocket binary frame reaches the browser as an ArrayBuffer at byteOffset 0, so every 4-aligned BufferRef.byteOffset is absolutely 4-aligned and viewOf() returns a view, not a copy. Drop the padding and viewOf() falls back to payload.slice() — a memcpy of every buffer, ~93 MB per pull on a 1AON cartoon (docs/spikes/geometry.md §8). geometryFrameProblems() reports any producer that violates it, and decodeBinaryFrame() rejects an unaligned header length outright. The round-trip test asserts zeroCopyPos === true and prints the offsets it measured. 3. Spheres and cylinders are INSTANCE buffers, never tessellated. Tessellating is exactly how PyMOL’s own exporters destroy mesh/dots/lines: 1UBQ mesh became 31,710 cylinders + 63,420 spheres = 31.9 MB .wrl / 133.7 MB .dae. INSTANCE_ITEM_SIZE fixes the wire layout and geometryFrameProblems() fails an INSTANCED_ONLY_REPS frame that arrives with triangles and no instances. Two related constraints ride along: frames are keyed per object / per rep / per state and carry atom indices (RepSurface::AT, CGO_PICK_COLOR), which is what makes a per-rep or recolour-only update possible at all; and CGO_DRAW_ARRAYS blocks travel verbatim in CGOCombineBeginEnd’s consecutive (not interleaved) sub-array order, so a block becomes a three.js BufferGeometry with zero conversion.

Mode P and the mode split

PixelFrameHeader types the server-rendered bitmap stream: jpeg during motion, png on settle, flipY because glReadPixels is bottom-left origin, frameId for the at-most-one-unacked-frame flow control, and reps — which reps are IN this bitmap. reps is authoritative and pixelFrameDrawsRep() is how it is read: the client draws a rep client-side only if the last pixel frame says the server did not. undefined means the whole scene. MODE_G_CAPABLE_REPS lists 18 of the 21 reps. labels (text — needs an atlas overlay, and every exporter emits 0 bytes for it), volume (a 3-D scalar field, served as a blob) and callback (needs a real GL context to construct) are excluded, and MODE_G_FALLBACK_REASONS names the seven reasons a capable rep can still degrade back to Mode P.

Commands

The round-trip test shells out to python3 (override with TENMOL_PYTHON) to run test/make_fixtures.py, which encodes five frames with python/tenmol_wire.py. No fixtures are checked in, precisely so the Python producer and the TypeScript decoder cannot silently drift.

Before you change anything here

The envelope is closed: adding or removing a message type, an input kind or a wire topic is a PROTOCOL_VERSION bump, and session.py has to move in the same commit. Note that the bridge already accepts two frames this package does not type — {t:'confirm'} (the one-time cmd.system confirmation) and {t:'ping'} — and five input kinds where the TypeScript union has three (button, drag, reshape). If the browser ever needs those, type them here first instead of sending an untyped frame.