State lives on the server. The client is told about it through the WebSocket, and
what arrives is distributed inside the page over a broadcast bus. These are two
separate mechanisms and confusing them is the source of a lot of trouble.
Packets: what the server sends
A packet is a named message from the server —
MAP, VESSEL-WAYPOINTS, ALERT,
DAMAGE-TEAMS, ACTIVE-CONSOLES, and dozens more. Packets are
upper‑case and hyphenated by convention.
The server does not send everything to everyone. A console asks for
the packets it needs:
this.AcceptPacket("DAMAGE-TEAMS");
Several things follow:
- Accepting is refcounted. Several widgets can want the same packet.
The socket tracks owners and only sends a rejection to the server when the last one
releases it. This is why widgets must claim through
this.AcceptPacket rather than the global — see Widgets.
- Accepting delivers immediately. The server answers a new claim by
sending the current value straight away, rather than waiting for the next change. This
is convenient and it is also a race: a widget that mounts while the page is loading
something over REST can have the socket's answer arrive first and win.
- A second claim is answered by the page. When something on the page
already holds the packet, the server is not asked again. Instead the topics that packet
raised last time are broadcast again, at once, inside the claim itself, to everyone
subscribed. A widget that claims a packet before it has subscribed to the topic misses
that replay, so subscribe first.
- Claims outlive the connection. A claim made before the socket has
opened is kept and sent when it connects, and every claim is sent again after a
reconnect. You do not have to wait for the socket to claim a packet.
A screen can also declare packets it always wants, in its screen registration, which
is how admin stations get message and debug traffic from the outset.
Topics: what the page distributes
When a packet arrives, the client updates its state and then broadcasts one or more
topics — lower‑case, hyphenated. Widgets subscribe to topics,
not to packets:
this.Subscribe("vessel-shields", (m) => this.Render());
The mapping is not one to one. One packet can raise several topics
— the MAP packet updates the current map and raises
map-info, and related packets raise location-current and
planetary-system-detail. And one topic can be raised by several packets, or
by page code with no packet involved at all. The Game Master's radar, for one, reports a
right-click as gm-radar-context and leaves the menu to the page.
This is exactly why releasing a topic subscription says nothing about whether a
packet is still needed, and why the two are tracked separately.
State and events are treated differently. Most topics carry state,
which is why a second claim can replay them. A topic that reports an event instead
(a removal, a hit, a sound, a log line) is broadcast with transient: true
and is never replayed, because replaying it would make it happen twice. If your own
code raises an event topic, mark it the same way:
game.Broadcast({ topic: "my-widget-fired", value: id, transient: true });
Without the mark, an event raised in the course of handling a packet can be kept with
that packet's topics and replayed to the next widget that claims it.
The topics you will actually use
There are over a hundred. These are the ones nearly every widget wants:
| Topic | Raised when |
ready | The page has loaded and the client runtime has started. The socket is still opening at this point. Raised once per page, but a widget built later that subscribes to it is still called, just after the code that subscribed has finished. |
socket-connected, offline | The socket opened, or closed. |
reset | The session reset. Re‑render, and remember this is not startup. |
resize | The console changed size. |
vessel | The bound vessel's general state changed. |
vessel-id | This console has been given a vessel. |
vessel-initialized | The vessel is fully populated. Prefer this to vessel-id — it is a superset, and a widget that renders on vessel-id often finds half the data missing. |
vessel-components | The component set changed. |
vessel-shields, vessel-damage, vessel-alert | Combat state. |
contacts, contact-updated, contact-removed | The sensor picture. |
target-flight, target-tactical, target-science | The three separate target selections. |
map-info, location-current | Where the ship is. |
mission, game-start, session | Session lifecycle. |
Traps worth knowing in advance
Contact topics arrive in storms. In a busy mission
contact-updated can fire many times per second. A widget that re‑renders
its whole list on every one will dominate the frame. Coalesce, or render on a timer and
let the topic only mark you dirty.
The session state, not the mission state, is authoritative for
whether a mission is actually running. A mission can be loaded without being started,
and code that checks the wrong one acts too early.
Editor pages must guard against live packets. An editor holds one
specific record it loaded over REST. A packet describing whatever the server
currently has loaded is a different thing, and letting it land replaces the record
being edited. The map editor sets a flag for exactly this reason. Packets are also cut
down to what the consoles draw: the MAP packet carries each spectator
camera as only the fields the Spectator screen reads. An editor reads the full record
over REST.
The catalogs are refilled, not kept. game.VesselClasses,
game.Components, game.Models and the other content tables are
emptied every time the console identifies itself, which is on every connect and
reconnect, and again when the server's set of modules changes (the page raises
module-data-reset). They are then refilled by the packets the page has
claimed. Two consequences: a position in one of these tables is not a stable identity,
so key anything you keep by record id; and a table you filled yourself over REST, with
no matching packet claimed, is emptied by the next identify (which can come moments
after the page loads) and stays empty. Keep a list you fetched in your own variable and
render from that.
Read the payload, not the arrival.
planetary-system-detail is also raised, with null, when the
ship leaves a planetary system. A handler that treats the topic itself as "we are at a
body now" gets that wrong.
The fast lane
Most state reaches a console with the game frame. A station that is flown by hand can
ask for its own vessel's position more often than that:
FastVessel(33); // this console's vessel, about every 33 ms
FastVessel(33, 500); // ...and the motion of contacts within 500 m
FastVessel(0); // off
With a radius, what is close enough to race, ram or shoot at is drawn at the same
moment as your own hull rather than a game frame or more behind it. The server clamps
the interval to between 16 and 200 ms, carries at most twelve nearby contacts, and sends
nothing for a hull that is not moving. The request is remembered and repeated after
every reconnect. The nearby contacts' records move them and do nothing else: they never
add or remove a contact and they raise no topic, so a widget that lists contacts is not
woken by them. The stock Pilot station asks for FastVessel(33, 500).
Sending state the other way
See Server Commands.