Rxova
Skip to content

BusWire

type BusWire =
| {
clientId: string;
key: string;
kind: PeerKind;
scope: "state";
type: "patch";
v: 1;
value: unknown;
version: Version;
}
| {
clientId: string;
kind: PeerKind;
scope: "state";
type: "hello";
v: 1;
}
| {
clientId: string;
kind: PeerKind;
scope: "state";
state: Record<string, unknown>;
type: "snapshot";
v: 1;
versions: Record<string, Version>;
}
| {
clientId: string;
kind: PeerKind;
metadata?: unknown;
scope: "presence";
type: "hello" | "ping" | "bye";
v: 1;
}
| {
clientId: string;
kind: PeerKind;
scope: "leader";
type: "hello";
v: 1;
}
| {
clientId: string;
kind: PeerKind;
scope: "leader";
term: Version;
type: "claim" | "heartbeat" | "resign";
v: 1;
}
| {
clientId: string;
key: string;
kind: PeerKind;
scope: "op";
type: "hello";
v: 1;
}
| {
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;
scope: "op";
type: "propose";
v: 1;
}
| {
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;
scope: "op";
seq: number;
type: "commit";
v: 1;
}
| {
clientId: string;
key: string;
kind: PeerKind;
scope: "op";
seq: number;
state: unknown;
type: "snapshot";
v: 1;
}
| {
clientId: string;
kind: PeerKind;
msgId: string;
payload: unknown;
replyTo?: string;
scope: "event";
type: string;
v: 1;
};

Everything on the same-origin bus, multiplexed by scope over one BroadcastChannel per name.

v is the wire protocol version, and changing it is a decision with rules — see wire.ts for what may be added within a version and what must bump it.

{
clientId: string;
key: string;
kind: PeerKind;
scope: "state";
type: "patch";
v: 1;
value: unknown;
version: Version;
}

{
clientId: string;
kind: PeerKind;
scope: "state";
type: "hello";
v: 1;
}

{
clientId: string;
kind: PeerKind;
scope: "state";
state: Record<string, unknown>;
type: "snapshot";
v: 1;
versions: Record<string, Version>;
}

{
clientId: string;
kind: PeerKind;
metadata?: unknown;
scope: "presence";
type: "hello" | "ping" | "bye";
v: 1;
}
clientId: string;
kind: PeerKind;
optional metadata?: unknown;

Whatever this client wants peers to know about it — a display name, a tab title, a cursor.

Carried on hello only, never on a ping. A ping is a heartbeat and arrives constantly; attaching metadata to it would re-announce unchanged data forever and churn every subscriber’s roster. Additive within wire v1: a build that predates it neither sets nor reads it.

scope: "presence";
type: "hello" | "ping" | "bye";
v: 1;

{
clientId: string;
kind: PeerKind;
scope: "leader";
type: "hello";
v: 1;
}

{
clientId: string;
kind: PeerKind;
scope: "leader";
term: Version;
type: "claim" | "heartbeat" | "resign";
v: 1;
}
clientId: string;
kind: PeerKind;
scope: "leader";
term: Version;

The claimant’s term. Arbitrated with newer() — the same clock the store uses.

type: "claim" | "heartbeat" | "resign";
v: 1;

{
clientId: string;
key: string;
kind: PeerKind;
scope: "op";
type: "hello";
v: 1;
}

{
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;
scope: "op";
type: "propose";
v: 1;
}
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;

Identifies this dispatch across its proposal and its commit, so a commit can be recognised as one’s own and applied once.

scope: "op";
type: "propose";
v: 1;

{
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;
scope: "op";
seq: number;
type: "commit";
v: 1;
}
action: unknown;
clientId: string;
key: string;
kind: PeerKind;
opId: string;
scope: "op";
seq: number;

The leader’s ordering decision: a gapless counter every client replays in the same order.

type: "commit";
v: 1;

{
clientId: string;
key: string;
kind: PeerKind;
scope: "op";
seq: number;
state: unknown;
type: "snapshot";
v: 1;
}

{
clientId: string;
kind: PeerKind;
msgId: string;
payload: unknown;
replyTo?: string;
scope: "event";
type: string;
v: 1;
}
clientId: string;
kind: PeerKind;
msgId: string;
payload: unknown;
optional replyTo?: string;

The msgId this is an answer to, when it is one.

Additive within wire v1: a build that predates ask never sets it and never reads it, so the field is simply absent both ways — which is the rule for new optional fields (see wire.ts).

scope: "event";
type: string;
v: 1;