watershed Collaborative data structures for Gleam

← watershed · Runtime / Peer-to-peer over WebRTC

Peer-to-peer over WebRTC

A document that runs on a WebRTC mesh with no sequencer at all. Eligible structures merge instead of ordering, and a relay can attach later for durability without changing the document's state or handles.

Traces CrdtDocument · Auto / SequencedOnly / P2pOnly · crdt_relay_v1

A document that doesn't need a sequencer

Every other sheet on this page assumes a server assigns the order that resolves concurrent writes. A CRDT document drops that assumption: browsers discover one another over WebRTC, edit while disconnected, reconnect in any order, and converge once they exchange state. The structures involved define their own merge rule instead of depending on somebody stamping a sequence number. The clap counter in the examples index is this running with the sequencer never in the picture at all: two tabs holding the clap button down both land, and nothing acknowledges either one.

Signaling introduces peers; it never sequences

A signaling service exchanges offers, answers, and ICE candidates so two browsers can open a direct connection (and admits a room, if it chooses to authenticate). It never assigns operation order, never stores document state, and cannot see a delta once the data channel is open: by protocol shape, an opaque envelope is the only thing that ever crosses it. A signaling outage blocks new peers from finding each other; it does not touch a document already converged.

The first topology is a small full mesh: every peer opens one data channel to every other peer, capped at eight, and the ninth arrival is refused with RoomFull rather than degrading the mesh underneath everyone already in it. Applications supply their own STUN and TURN servers (watershed ships neither); host candidates alone only connect peers with no NAT between them.

Not every structure is eligible

A merge rule is what makes order-independence possible, so only the channels built as CRDTs may run this way: PN counter, OR-set, the OR-map, G-set, 2P-set, SharedSequence, and SharedText. A p2p document's root is one of these, chosen explicitly at config time, never the ordinary last-write-wins SharedMap.

Everything that depends on server order or acknowledgement stays out: SharedMap, SharedCounter, and SharedDirectory resolve by sequence number; claims and registers persist one; TaskManager, pact maps, and ordered collections depend on ordered membership; and the OT-transformed json_ot and rich-text channels transform against sequenced history. None of these have a merge function to fall back on, so none of them are safe with no sequencer in the room.

One document, three policies

A CrdtDocument chooses its transports once, at config time:

import watershed/crdt_js
import watershed/p2p

// Auto starts on WebRTC and attempts the relay in parallel — it never
// waits for one, and prefers it while healthy.
let config =
  crdt_js.config(
    room_id: "retro-board-9c2",
    replica_label: "ada",
    compatibility_tag: "retro-board/v1",
    root: p2p.pn_counter_root(),
    signaling: signaling,
  )
  |> crdt_js.with_transport_policy(crdt_js.Auto)
  |> crdt_js.with_sequencer(crdt_js.sequencer("wss://relay.example/room"))

Auto is the default: the mesh and any configured relay each come up on their own schedule, readiness waits only for the mesh, and the relay becomes the durable delta path once it proves itself. P2pOnly opens no relay socket and ignores a configured one entirely. SequencedOnly is the inverse: no signaling, no RTCPeerConnection, and readiness waits on the relay alone, under a bounded deadline, because there is nothing else to wait on. All three run the identical document over the identical wire envelopes, so a snapshot exported under one policy imports under either of the others.

Losing the relay doesn't lose the document. When a primary relay connection drops, the document reports the failover, pushes the digest it owed the mesh, asks its peers for state, and keeps running on WebRTC: same public handles, same converged state, one transport quietly retired. Readiness under Auto never waited on the relay in the first place.

An optional durable relay

A relay speaking crdt_relay_v1 is a durable fan-out point, not a sequencer transplanted into this mode: it stamps a diagnostic order for its own log, broadcasts what it accepts, and answers a request for the state it holds. It never merges, never decodes a kernel payload, and never decides which of two replicas is right. That stays the mesh's job. A room with no relay simply keeps its state in whichever replicas are connected; one that attaches a relay keeps running exactly the same after it disconnects again, minus the durability.

In short

A CRDT document trades the server's order for a merge rule, so it can run on a WebRTC mesh with no sequencer at all. Signaling only introduces peers; eligible structures converge by merge; and an optional relay attaches later purely for durability and reach, never changing what the document means.