The protocol¶
The server and the browser talk over a binary protocol: typed-array buffers
for topology and positions, JSON for metadata. Topology and positions are what
there are a lot of, and they are exactly what a Float32Array carries with no
parsing at all; names, types and bounds are what there are few of, and they are
exactly what JSON is good at.
It lives in kglite-visual-core and it is transport-agnostic by rule:
nothing in that crate may know it is talking to a WebSocket. The encoder is a
seam, and the WebSocket, the JSON twin and the
headless renderer are three consumers of it.
The version number¶
Every response carries protocol_version. It currently reads 4.
{"protocol_version":4,"core_version":"0.1.1","tier":"compact","slot_count":98,…}
Version 4 added the layout message and request: the server computes a static
arrangement for the live view and broadcasts it, and every attached client
stops its simulation and holds the picture still. That is what
set_layout and POST /api/layout drive.
There is no version skew¶
This is the short section, and it is short because of a packaging decision.
The frontend bundle is compiled into the binary. rust-embed bakes
frontend/dist into the executable, and the wheel carries that same extension
module. So the server and the client it serves are always the same build —
there is no separately-deployed frontend, no CDN, no cached bundle from last
month, and no matrix of “which client version works with which server”.
protocol_version is therefore not a negotiation. It is a tripwire: a
client that ever sees a number it does not expect is looking at a build that
should not exist, and says so rather than guessing.
The corollary is a build rule rather than a runtime one. A stale
frontend/dist inside a fresh binary looks exactly like a backend bug, so the
build refuses it: check_bundle.py fails on a bundle older than
frontend/src, and its --resolve-binary mode refuses a binary older than the
bundle it should embed.
Slots and tombstones¶
The view is a slot space: an integer index per node currently in the view.
The entry screen occupies slots 0..n — one per node type — and every
expansion appends.
Collapsing does not reissue slot numbers, so anything holding a slot stays
valid. What it leaves behind is a tombstone, and slot_count includes
them while pointCount does not. When tombstones accumulate the server
compacts, which renumbers everything — and the response says so, because a
compaction is the one event that invalidates a slot a caller was holding.
Joining a session in progress¶
A client connecting to a view that has already been drilled into is greeted with the whole current view — every live node named, holes marked, positions from slot zero, and the static arrangement in force if there is one.
That is not the obvious implementation, and the obvious one was wrong. Every client used to be greeted with the entry screen, whatever the shared view had been drilled into since; the next change then arrived indexing slots that browser had never been told about, and the points appeared with no label, no id and nothing to click.
window.__kglv reports both halves of that as an honest pair: slotCount is
how many slots the client holds a position for, and namedSlots is how many
it holds an identity for. Unequal means a client is mid-resync.