Skip to content

client server dialog

Greg Wilkins edited this page Oct 6, 2026 · 2 revisions

The client–server dialog

What a boat and the server say to each other, and the rules that keep them able to talk after they have drifted apart in version. Code cites this page by section (§8.2), so the section numbers are stable.

The conversation is the optional half of the system. Nothing in it is on the path from a GPS fix to a crossing — the boat detects and times its own crossings with no network (crossing-detection) — so every part of it may be absent and the boat sails regardless. Where a club runs a race, it adds the committee's side: starts, flags, course changes, a channel and a fleet feed.

dialog.js is the boat's half, Dialog.java and DialogServlet.java the server's, and client/www/schemas/*.v1.json is shared by both.

1. What is trusted, and what does not move

1.1 THE BOAT IS TRUSTED ENTIRELY

Every position and every instant comes from the boat, and nothing checks whether it told the truth. The server has no independent position for any boat; a crossing's instant is the boat's own, interpolated from its own fixes and stamped by its own clock. The quality-control gates defend against a bad receiver reporting in good faith, never against a dishonest sailor. This system produces records, not verdicts; a club that needs more than a boat's word has the ordinary remedies — a mark boat, a finish line, witnesses.

A clock offset cancels in an elapsed time, which is why this costs less than it looks. Elapsed time is the difference between two instants from the same clock, so a boat whose clock is a minute fast starts and finishes a minute late and its elapsed time is exact. What does not survive is comparison in absolute time — against a gun, or between two boats at one mark — which needs clocks that agree, and this system neither provides nor checks that. Drift within one race is far too small to matter on a device with GPS.

The server will not call OCS, or infer any outcome. It could compare a published start with a reported crossing, and it does not: a boat that says it started fairly is recorded as having started fairly. Judging a competitor belongs to the club's people.

1.2 The server collates and redistributes; it does not measure

  • The boat decides whether a line was crossed, which way, and when. The server is told; it never sends a crossing to a boat.
  • The server keeps no racing clock. It publishes an absolute start time; every countdown, elapsed time and crossing instant is run by the boat.
  • A boat that has its course can sail all of it with the server switched off.
  • The server does not score. Places, OCS, penalties and series results belong to the club's software.

2. The server as a part-time race committee

For the formats that need one, the server acts in part as the race committee: start sequences, postponement and abandonment, divisions, and a live fleet feed. Many formats need none of it. What it never takes over is the recording: a committee that fires a gun still does not know when you crossed. A start is published as an absolute instant (2026-09-17T13:05:00Z); the countdown is the boat's own, and the boat's elapsed time runs from its own crossing.

3. Transport

One conversation, whatever the pipe. The design is a WebSocket, falling back to polling where a socket cannot be had. Polling is what is built, carrying exactly the envelopes and ordering a socket would:

Polling
Before a session POST /api/dialog with hello and join
Client → server POST /api/dialog/{session} with an array of envelopes
Server → client that POST's response carries whatever is queued
Idle the client polls at the interval hello.ok gave

Dialog.exchange(session, envelopes) is the whole contract, written so a socket could use it unchanged.

3.1 When the channel is gone

Crossing detection, timing, the approach screen, the course overview and the race clock all carry on. The fleet feed, messages, timers, flags and course updates stop; the Place screen ages and says so. The boat queues what it was going to say — crossings, its record, its messages — and sends it oldest first on reconnection. Fixes are the exception: only the latest is kept, because a fix is about now. The screen says when the channel is down.

4. The envelope

{ "v": 1, "type": "crossing", "id": "b7f3c1a2", "at": "2026-09-17T08:31:12.250Z",
  "tags": ["division:1"], "body": { } }
Field Meaning
v the protocol major version this message is framed in
type what the message is; decides how body is read
id unique to the sender; what an acknowledgement names, and what makes a resend idempotent
at when the sender says it sent it — used for ordering a late queue and de-duplicating, never for timing
tags labels for addressing; see §6
body the message's own fields, schema'd per type

Order comes from the transport. A socket delivers in order and a polled batch is a list. State is the messages applied as they arrive — a start is published, an AP voids it, a new start replaces the AP — and every party (server, race screen, boat) runs the same small state machine over the same sequence. There is no sequence number: reconnection re-states rather than replays (§4.1), and id already makes a resend recognisable.

4.1 Reconnection RE-STATES, it does not replay

A boat that was away wants what is true now, not what it missed. So on join the server re-states the current standing of every tag the boat belongs to as ordinary course, timer and flag messages. The channel is the exception, because a channel is a history: the boat sends channel.since naming the last entry it holds and gets the original envelopes back, ids and all. A re-stated course or flag writes an ordinary channel entry. There is no "clear the AP" message: publishing a start clears it. Fixes are never replayed.

5. Versioning, for clients that are installed rather than downloaded

A phone in a bracket is not refreshed between races, so the boat and server will be different versions on the same afternoon. The rules:

  1. Fields are only ever added — never removed, renamed or repurposed.
  2. Unknown fields are ignored on both sides.
  3. Unknown message types are ignored, and counted.
  4. A breaking change is a new type or a new v. The server speaks every v it has shipped until nothing in the fleet uses the old one.
  5. additionalProperties is never written in a schema, so it is always true — rule 2 where a validator can enforce it.

5.1 The handshake

The client opens with hello, naming every v it can speak; the server answers hello.ok with the one it chose and the polling interval, or rejected. The client proposes, the server disposes.

6. Tags

Tags are namespace:value strings. A division is a tag, division:<id>, where <id> is the key of the race's division map. A message with no tags is for everybody; one with tags is for boats carrying at least one of them. The server groups by tags without knowing what they mean.

7. Identity and sessions

A boat is identified by sail number and name, and by a session issued when it joins, which later messages carry so a reconnection is recognised. The boatId is the sail number, or the session id where none was given.

7.1 Officers are authenticated. Boats are not, and never will be

Authenticate authority, trust data. Officers sign in with OpenID Connect, because publishing a course or abandoning a race is a decision imposed on a fleet and who did this matters. Boats are never authenticated: their data is trusted by design, so a login would only put a name to a claim nothing checks. The whole of /api/dialog stays open (deployment).

That leaves two risks §1.1 does not cover — it trusts a boat about itself, not a third party about a boat: impersonation (any device can claim any sail number) and the open channel (anyone who can reach the server can broadcast). Both are deferred on purpose (§13).

8. The message catalogue

Direction is C→S or S→C. The schemas in client/www/schemas/ are the exact bodies.

8.1 Session and data

type Dir Body
hello C→S versions[], client
hello.ok S→C v, poll (ms), features[] — no time: the server is not a clock to set yours by
rejected S→C code, text

rejected carries a code a client can branch on (version, course, variant, unpublished, handicap, start, schema, record, internal) and a sentence a sailor can read; an unknown code falls back to the sentence. Queries for clubs, series, races and courses stay on the cacheable REST endpoints (/api/public, /api/races/…, /api/courses/{revision}).

8.2 Joining

type Dir Body
join C→S sailNo, name, club, series, course, variant, race, division, tcf, lengthM, allocatedStart, revision
joined S→C session, boatId, race, raceName, tags[], fixSeconds, revision, course or waiting
leave / left C→S / S→C session
  • A boat joins a race, and the course follows from its division: a boat does not choose the geometry it was entered for. A join naming no race is matched to a race today whose division sails that course.
  • joined carries the whole published snapshot, never a reference to it, so a boat on a flaky connection never has to fetch before it can sail. It is always what was published, never what the editor holds.
  • A public race can be joined before its course is published. Then joined carries waiting — a sentence and the start line as the programme has it — and when the club publishes, every waiting boat in that division is sent a course message with the snapshot and starts on it.
  • A distance-handicapped course needs a TCF inside the snapshot's tcfMin–tcfMax, or the join is rejected with code handicap giving the range.
  • An allocated start needs the boat's own start time, between the division's first and last, or the join is rejected with code start.
  • joined also arrives unasked, when a boat is entered for the next race of the day (§12.6).
  • With no conversation possible, the boat sails anyway, falling back to POST /api/join/{club}/{series}/{course}, which hands over the published snapshot and nothing else.

A JOIN WITH NO RACE BEHIND IT GETS NO CHANNEL, AND NOTHING ELSE LIVE

Practice and record attempts have nobody to talk to, so they get a session for crossings and the record, but no channel, no fleet feed, no timers or flags, and no fixes — a phone in a bracket for four hours reporting to nobody is battery and data spent on nobody. The Chat and Place screens are not offered.

8.3 What the boat reports

type Dir Body Notes
fix C→S session, position, cogDeg, sogKn, revision, at every fixSeconds (2 s), accepted fixes only
crossing C→S session, line, step, lap, letter, instant, revision, confirmedFixes, finish one per latch, with the interpolated instant
record C→S session, record the whole CourseRecord, sent when the course completes
retire C→S session, reason §8.6

A live crossing is advisory; the record is definitive. The server stamps the record with the session's race and division before filing it: everything else in a record is the boat's account of itself, but which race it is in is an entry the committee accepted.

8.4 What the server sends

type Dir Body Acknowledged?
fleet S→C race, boats[] — position, COG, SOG, revision, step, lap, start, finish, elapsed, TCF, outcome no
course S→C revision, course (the whole snapshot), reason, text yes
timer S→C kind, startAt, closesAt, openSeconds, warningSeconds, startSeconds, timeLimitSeconds, text no
window S→C opensAt, closesAt, text — superseded by a timer of kind open, still read no
flag S→C flag (postponed, abandoned, …), reason, text yes
say both from/session, text committee messages, by being read
outcome S→C boatId, outcome, reason, text yes
ack C→S session, ackOf, what
channel.since C→S session, after answered with the original envelopes

fleet goes out every five seconds, not at the fix rate: on a screen showing a whole course, the difference is invisible, and one message carries every boat.

timer is a division's start. Every start line is open for a period, and a crossing counts only while it is; a boat over early sees nothing register and comes back.

kind The line is open Elapsed runs from The boat shows
scratch from startAt for openSeconds (default 600) startAt a countdown to the start
open from startAt to closesAt the boat's own crossing when the line opens, then when it closes
allocated from the boat's own start time for openSeconds that time a countdown to its own start

warningSeconds and startSeconds put the warning and preparatory signals before the line opens; timeLimitSeconds closes the finish to a boat that long after its elapsed time began. Every countdown is run by the boat against its own clock; the server sends a timer once and does not tick.

One start per division, and no rolling sequence. Postponing one division does not move another. AP suspends, it does not reschedule: there is no "now" on the server to count from. Publishing a start is what clears an AP, so the cycle is publish → AP → publish, and the last message for a tag is its state.

The channel is open and relayed to everybody — a radio, with no private conversations. A joining boat is sent the backlog for its race. A message over 400 characters is truncated rather than refused. There is no automatic rate limit, because there is no identity to throttle; the committee muting a sail number is the intended defence (not built).

Every state message carries text, which is what the channel shows and what an older client can still display. Acknowledgements exist where "did they see it?" could decide a protest: a course change, a flag, an outcome and a committee message.

8.5 The state a division's start is in

From On To
— timer scheduled
scheduled flag: postponed postponed
postponed timer scheduled
scheduled the start passing, on each reader's own clock racing
racing flag: abandoned abandoned
racing every boat finished, retired or DNF finished

AP is the pre-start signal and abandonment the post-start one. The race screen enforces two rules against its own clock: no AP once a start has passed, and after an AP the next start is at least six minutes ahead (a minute, then a full sequence).

8.6 Outcomes: retiring, and DNF

A boat retires because a sailor pressed retire (retire, C→S); a boat is DNF because the committee decided so (outcome, S→C). The software never infers an outcome from a boat that stopped reporting. Whether a boat completed the course is collation; what place it got is scoring, which is the club's.

9. The screens a boat needs

The boat's own screens are described in boat-client and can be tried on the test client. The conversation adds three things, built in screens.js; the rules they follow are these.

9.1 Race progress — the Place screen

A ladder of every boat that joined: how far round, elapsed time, and corrected time where a TCF is known. It ages rather than blanks, saying how old it is, and it is a view, not a ranking anybody is held to.

9.2 The channel — one inbox for everything the boat was told

Committee messages, chat, and receipts for state changes, newest last, with an unread count. Common replies are one tap — Retiring, Protesting, OK — and the safety three — Need assistance, Standing by to assist, Man overboard — are set apart in warning colour, because they are not racing messages.

9.3 Alerts, and the one thing that must never be covered

A course change, flag or outcome opens an alert, and dismissing it is the acknowledgement. But nothing interrupts an approach: while the approach screen has the display, an alert shows as a banner and the modal waits until the approach ends. A plot covered at the moment of a crossing costs the crossing; an abandonment seen twenty seconds late costs nothing.

9.4 Course changes and flags as channel entries: YES to one inbox, NO to one message type

They appear in the channel, so a late joiner reads the afternoon as one story. But on the wire they are state, not events: the client holds the current course and flag explicitly, and the channel entry is a receipt. "Scroll up to find out whether you are racing" is not something to ask of somebody on the water.

9.5 Where they sit in the view selector

The views are Course, Line, Chat, Place and Auto, on every screen; Chat and Place appear only where there is a channel. Auto shows, in priority: the approach screen while approaching a line; then a new channel entry; then the approach screen by distance or time, else the course. Place is never chosen automatically. The start countdown sits above every screen.

10. Schemas

JSON Schema, one file per message type and major version, in client/www/schemas/, read by the server off the classpath and by the boat over HTTP — one copy for both. Each side validates what it receives: the server answers a bad message with rejected code schema; the boat drops and counts it.

The schemas are held to a subset a small hand-written validator covers — type, properties, required, enum, const, items, minimum/maximum, pattern, format: date-time — because the client has no build step and a full JSON Schema library would be paid for on every boat. A message that needs more than the subset should be simpler.

11. Course changes and the record

The record names the course the boat was sailing when it finished. Whether that record still means anything after a mid-race change is for the club to judge.

Withdrawing a course does not reach the water. Un-publishing stops new boats joining; boats already sailing finish on the snapshot they were handed. Stopping a race is flag: abandoned, and only that — tidying next week's courses on race morning must never stop a fleet.

12. The server screens

The committee's screens are for a club's desk or a committee boat, with a keyboard, and are behind the officers' login. See them on the live site: the course editor and the race screen.

12.1 Defining a race is EDITING; running one is not

A race's definition — its date, how it starts, each division's course, variant, start and time limit, and the race that follows — is configuration, edited on the editor's Races tab and saved in the series file. While a race is public the server hands each division the start its definition works out to. Running a race is a separate page, because the editor saves as you go and holds one undo, which is wrong for telling a fleet to stop: on the race screen an irreversible act asks twice.

12.2 The chart: colour is DIVISION here, and texture is progress

On the race screen colour means division; in the editor it means a leg's role. Progress is texture: legs some boats have sailed are solid, legs no boat has sailed are dashed, and legs all have sailed are faint. The solid band is the fleet's spread, which is what a committee deciding whether to shorten needs to see.

12.3 The panel

The race screen shows every public race running today and acts on one. It can delay a start, AP it before the start or abandon after, republish a division's course, mark a boat DNF, and write in the channel. There is no GO button: the server keeps no clock, so a start is scheduled, never triggered.

12.4 The fleet table, and why the acknowledgements exist

One row per boat: last fix age, mark and lap, elapsed time, outcome, and whether it has acknowledged the latest course and flag. That column is why acknowledgements are in the protocol: have all boats seen the new course? is the question a committee actually has.

12.5 Where the data lives

Where
Definition — date, divisions, starts, next race the series YAML, under races:
Conduct — who joined, the channel, flags, standings, acknowledgements data/store/conduct/{club}/{series}/{race}.json

The channel is kept with its race, so a day of several races is several separate conversations. After a server restart the channel and standings come back from the conduct file; sessions do not, so boats join again and are re-stated.

12.6 Several races in a day: a race knows its NEXT race

A race names the race that follows it (next), and a boat that retires from one is entered for the next, provided it is the same day, with a joined sent unasked. Its division carries over where the next race has a division of that name; where it does not, the chain stops and the boat joins by hand. A boat declines with leave.

13. What is deferred

Two defences against bad actors are deferred on purpose, both costs of not authenticating boats (§7.1):

  1. A rate cap on the channel. There is no identity to throttle; the intended instrument is the committee muting a sail number.
  2. Impersonation. A join token from the notice of race would raise the cost without being authentication, and is the obvious first move.

Not open, and not to be reopened without a reason: the trust model (§1.1), the absence of a server clock (§1.2), the absence of a sequence number (§4), unauthenticated boats (§7.1), and that a withdrawal never reaches the water (§11).

14. Where it lives, how it is tested, and what is not built

client/www/dialog.js, screens.js, schema.js the boat's half: joining, the queue, the held state, the channel, the validator
dialog/Dialog.java, Envelope.java, Schemas.java sessions, one room per race, standings, the chain, the validator
server/DialogServlet.java the polling transport
dialog-test.js, DialogTest.java the state machine, the channel's idempotence, the alert rule, that both validators agree
tools/editor-drive/drive-race.mjs, drive-client.mjs, drive-alert.mjs a race end to end over the wire; a whole course sailed with the network removed; no alert over an approach

Not built: the WebSocket; muting a sail number; the defences in §13; the next-race chain on finishing (only retiring fires it); the boat switching to a course change — it is received, shown and acknowledged, but the boat keeps sailing the geometry it joined with; a session for the race screen; stamping records posted over REST with their race; caching a joined race across a page reload; and re-posting the record with its full track.

Clone this wiki locally