Reconciliation
Typing should feel instant, and the server should stay the source of truth. Write-ahead reconciliation is how you get both.
The three states
Section titled “The three states”Each subscribed channel keeps:
- confirmed — derived purely from server-sequenced actions.
- pending — your actions, applied locally, not yet echoed back.
- optimistic — confirmed with pending replayed on top.
Only the last is ever rendered, and it is derived, never stored:
flowchart LR
C[(confirmed)] --> R{replay pending}
P[pending queue] --> R
R --> O[(optimistic)]
O --> UI[what you render]
What happens on dispatch
Section titled “What happens on dispatch”final handle = runtime.dispatch(channel, action);The action lands in pending and optimistic recomputes immediately — the UI
updates on that line, before anything reaches the socket.
Then the notification goes out.
When the server echoes it back, one of four things happens.
It confirms.
The entry pops off pending and applies to confirmed.
Visually nothing moves: the recomputed optimistic state is identical to what
was already on screen, so no rebuild is emitted.
It is rejected.
confirmed is left alone — a rejected action never applied server-side, so
dropping it from pending is the revert.
handle.settled resolves to DispatchRejected with the host’s reason.
A foreign action arrives first.
Another client’s action applies to confirmed, and your pending work rebases
on top of it.
There is no operational transform here; the reducers are last-write-wins, so
recomputing is the rebase.
The server skips it.
If the host echoes a later action of yours, the skipped ones are never
coming — one frame is one message over an ordered socket, so this is not
transient reordering.
They are dropped and surfaced rather than left pending forever, and their
handles settle as DispatchDiscarded.
Awaiting the verdict
Section titled “Awaiting the verdict”final handle = runtime.dispatch(channel, ChatMessageSentAction(text: text));controller.clear(); // optimistic: the bubble is already up
switch (await handle.settled) { case DispatchConfirmed(): break; case DispatchRejected(:final reason): controller.text = text; // restore the draft; the bubble vanished showSnackBar('Not sent: $reason'); case DispatchDiscarded(): controller.text = text;}Rejection is a value, not an exception.
A host declining an action is an ordinary product outcome, and awaiting one
should not require try/catch.
Reconnects clear the queue
Section titled “Reconnects clear the queue”clientSeq is per-connection, so the server has no memory of your pending
actions after a drop.
The spec says clear them, and clearing is correct.
But silently vaporising a message someone typed is a product decision, not a protocol one, so the engine clears and reports:
runtime.mirror.pendingDrains.listen((drain) { // Restore a draft, offer "resend", or just tell them. for (final action in drain.actions) { /* … */ }});Every discarded handle also settles as DispatchDiscarded, so nothing awaits
forever.
How this is verified
Section titled “How this is verified”With no upstream corpus, correctness rests on the property reconciliation exists to guarantee — convergence:
With the pending queue drained,
confirmedequals the naive fold of the server’s own accepted order, with no trace of client history.
The suite generates 2,000 random but legal interleavings of local dispatch, own echo, foreign action, rejection, duplicate, and out-of-order echo, and asserts that invariant after every one. It is not equivalent to a cross-language corpus, and the parity checklist says so plainly.
Sequence gaps
Section titled “Sequence gaps”serverSeq is global to the connection, not per-channel — reconnect resumes
from a single number covering every subscription.
So a client subscribed to several channels legitimately sees 10 then 47 on any
one of them, and a per-channel contiguity check would false-positive on every
healthy connection.
Gap detection therefore lives on the mirror, where the sequence really is
contiguous, and ships as observe: it counts and reports without acting.
Whether contiguity holds at the connection level for a client subscribed to a subset of channels is not stated anywhere in the spec, and no other client checks at all. The diagnostic exists so a real host can settle the question before anything acts on it.