watershed lets people edit shared data without losing concurrent changes.
Clients apply edits at once; the server sequences them; each data structure
resolves conflicts under its stated policy. The same Gleam core runs on the
BEAM and in the browser, on a distributed-data-structure (DDS) runtime and
sequencer drawn from Fluid Framework’s design.
On a photorevised survey sheet, magenta marks updates not yet
field-checked. This demo borrows that color code: edits that aren’t
confirmed yet are drawn in magenta,
confirmed state in ink. All three
clients run watershed’s compiled map_kernel Gleam code
and share one ordered list of changes. That is a shared map, the
structure most collaborative apps start with.
Nudge a gauge, have two clients write at once, stretch the network
delay. The copies agree once queued writes arrive; the most recent
write to a key wins.
Try cutting Client B’s link mid-edit: its writes park locally, the
others keep converging, and restoring the link catches B up and
resubmits what it wrote offline.
1 · local edit prints in magenta
2 · the sequencer stamps it with an SN
3 · every client lands the same ink state
Merge rule: last write wins. Race two writes to the
same key and the op the sequencer stamps later overwrites the earlier
one, identically on every replica.
Merge rule: increments commute. Race two increments
and both apply. The counter converges on the sum; nothing is
overwritten.
Merge rule: grow-only max per replica. Each client owns
one monotonically increasing tally. Race two inspections or deliver a
delta twice: replicas converge by keeping the maximum count seen for
each author.
Merge rule: deltas join a lattice. Every update
travels as a CRDT delta; merging is commutative and idempotent. Race
it, resend it, deliver it twice: it counts exactly once, and the
balance converges on fill − cut.
Merge rule: first writer wins. A claim commits only
if the slot is still unclaimed when the sequencer stamps it (or its
reference SN matches the holder’s exactly). Nothing prints
optimistically: a filed claim is invisible, even to you, until it
round-trips as won or lost.
Merge rule: add-wins, observed-remove. A strike only
removes stockpile entries the striker had seen. Race a strike against a
concurrent delivery and the row survives, with every logged yard.
Re-opening a struck pile brings its ledger back.
Merge rule: add-wins, observed-remove. Clearing a marker
removes only the add tags this replica had observed. Race a clear against
a concurrent re-mark and the marker remains present because the fresh tag
was unseen by the clear.
Merge rule: grow-only union. Marking a benchmark is a
permanent fact. Race two marks or deliver one twice: the registry only
grows, and duplicate deltas are absorbed.
Merge rule: tombstone wins. A retired marker can never
become active again. Race a re-place against a retire: both replicas keep
the tombstone, and duplicate retire deltas are absorbed.
Merge rule: atomic plus retained versions. A write that
knew the current atomic sequence wins the linearizable slot. Concurrent
losers still append a version, so LWW reads can intentionally diverge from
atomic reads.
Merge rule: FIFO by sequence. Adds enter the queue in
server order. Acquires remove the front item when their op sequences; a
racing acquire may come back empty because the earlier SN already took it.
Merge rule: one assignee, FIFO waiters. Volunteers join
a per-task queue in sequenced order. The first connected volunteer owns the
task; abandon promotes the next waiter, and complete clears the queue.
Merge rule: quorum acceptance. A set first prints as
pending with a frozen signoff list. It becomes accepted only after every
connected client signs off, or leaves drain the list.
Client A
0 pending
Shared map replica on Client A
mill-race
24
kettle-run
61
low-ford
42
sandbags-placed
earthwork-balance · yd³
fill Σ
74
cut Σ
30
cutfill
inspection-count
A Σ
0
B Σ
0
C Σ
0
Claims replica on Client A
north-levee
spillway-gate
pump-house
OR-map stockpile ledger replica on Client A
spoil-north
borrow-pit-7
wash-fill
OR-set field marker roster replica on Client A
north-stake
sluice-tag
borrow-flag
G-set permanent benchmark registry replica on Client A
BM-17
BM-22
BM-31
2P-set retired marker ledger replica on Client A
stake-3
gate-pin
silt-flag
RegisterCollection replica on Client A
register
atomic
LWW
revise
north-bench
gate-setpoint
pump-mode
OrderedCollection replica on Client A
sheet
state
op
queue
held jobs
PactMap replica on Client A
pact
accepted
pending
op
datum-grid
gate-policy
inspection-window
TaskManager replica on Client A
task
assigned
waiters
op
sluice-inspection
pump-watch
crest-walk
Client B
0 pending
link cut — ops park locally until the link is restored
Shared map replica on Client B
mill-race
24
kettle-run
61
low-ford
42
sandbags-placed
earthwork-balance · yd³
fill Σ
74
cut Σ
30
cutfill
inspection-count
A Σ
0
B Σ
0
C Σ
0
Claims replica on Client B
north-levee
spillway-gate
pump-house
OR-map stockpile ledger replica on Client B
spoil-north
borrow-pit-7
wash-fill
OR-set field marker roster replica on Client B
north-stake
sluice-tag
borrow-flag
G-set permanent benchmark registry replica on Client B
BM-17
BM-22
BM-31
2P-set retired marker ledger replica on Client B
stake-3
gate-pin
silt-flag
RegisterCollection replica on Client B
register
atomic
LWW
revise
north-bench
gate-setpoint
pump-mode
OrderedCollection replica on Client B
sheet
state
op
queue
held jobs
PactMap replica on Client B
pact
accepted
pending
op
datum-grid
gate-policy
inspection-window
TaskManager replica on Client B
task
assigned
waiters
op
sluice-inspection
pump-watch
crest-walk
Client C
0 pending
Shared map replica on Client C
mill-race
24
kettle-run
61
low-ford
42
sandbags-placed
earthwork-balance · yd³
fill Σ
74
cut Σ
30
cutfill
inspection-count
A Σ
0
B Σ
0
C Σ
0
Claims replica on Client C
north-levee
spillway-gate
pump-house
OR-map stockpile ledger replica on Client C
spoil-north
borrow-pit-7
wash-fill
OR-set field marker roster replica on Client C
north-stake
sluice-tag
borrow-flag
G-set permanent benchmark registry replica on Client C
BM-17
BM-22
BM-31
2P-set retired marker ledger replica on Client C
stake-3
gate-pin
silt-flag
RegisterCollection replica on Client C
register
atomic
LWW
revise
north-bench
gate-setpoint
pump-mode
OrderedCollection replica on Client C
sheet
state
op
queue
held jobs
PactMap replica on Client C
pact
accepted
pending
op
datum-grid
gate-policy
inspection-window
TaskManager replica on Client C
task
assigned
waiters
op
sluice-inspection
pump-watch
crest-walk
Sequencer
Loading
booting watershed kernels…
Latency and jitter affect simulated arrival order; animation speed
changes playback only. “Ops in flight” counts every hop still
travelling: one client → sequencer leg, then one
sequencer → replica leg per client.
Every other structure runs the same way on its own page. Start with the field atlas, the full catalog →
A246142
B246142
C246142
The live demo couldn’t start: it runs watershed’s runtime counter channel
and kernels as compiled JavaScript modules, and this browser didn’t load
them. The rest of the page works fine without it. Reload the page to retry.
One ordered stream, one clear rule per structure
watershed doesn’t paper over conflicts with one catch-all merge. Each
structure states a small, clear rule: what can show up instantly, what
has to wait for the server to confirm it, and what gets saved so a client
can reload later.
Only a few of watershed’s structures are shown here. Find the rest,
grouped by family, in the field atlas. The
full catalog includes a live demo for each structure.
SharedMap
map_kernel
Merge rule
for each key, the most recent write wins, decided by server order
Before it’s confirmed
your writes show instantly, then lock in once the server confirms them
What gets saved
confirmed entries reload with their keys and insertion order intact
Every structure above rides the same ordered stream of changes, the very
one the live SharedMap demo runs on. Open any family to switch which
structure is on screen; they all stay hosted together as channels sharing
one sequenced document.
01
Submit
A client makes a change and updates just its own local copy for now.
02
Sequence
A server like floodgate puts everyone’s changes in one order and sends that order to every client.
03
Confirm / apply
The client that made the change locks it in; everyone else applies it through the exact same code.
04
Summarize
A saved snapshot lets any client reload the current state, the same path used after a reconnect.
Every structure, mapped by family.
The demo above runs one kernel. The atlas is the whole field guide:
every structure watershed ships, grouped by how it converges, each with
its own live demo and merge rule.
watershed is written once and runs in two places. runtime_core,
channel, wire, and the kernels compile for both
targets. Erlang and JavaScript keep separate facades, runtimes, and socket
bindings only where the platform forces them to differ.
JavaScript-only API: immediate document handle, on_ready
callback, promises, and the same DDS verbs.
Runtime: runtime_beam
OTP actor, receiver process, process calls, heartbeat timer, and
reconnect orchestration.
Runtime: runtime
Callback-driven shell over a mutable cell; same bootstrap,
ordering, catch-up, and resubmit discipline.
Shared pure core
runtime_core owns CSN/RSN, ack matching, gap detection,
detached attach, and resubmit. channel dispatches the
kernels hosted by the document runtime. wire and
wire/* encode the same operation and sequencing payloads
on both targets.
Phoenix channel client over Gun, with Roost decoding; gated to
Erlang with @target(erlang).
Transport: transport_js
Phoenix.js FFI, socket callbacks, mutable cell helpers, and browser
Web Crypto dev-token signing.
The boundary is enforced by target gates: watershed_beam and
runtime_beam are @target(erlang);
watershed, runtime, and
transport_js are @target(javascript). The
ungated core underneath is the compiled code running in the browser demo.
A map over one sequenced stream
SharedMap exposes familiar operations: set,
get, subscribe. There is no merge callback to
install: local writes apply optimistically, the server assigns their
sequence numbers, and the highest-sequenced write wins per key.
The same core runs on the Erlang target as an OTP actor and on the
JavaScript target inside a Lustre single-page app. The facades differ
at the edge: watershed_beam blocks and returns a Subject, while watershed uses callbacks. See the examples for both, verified converging against a live floodgate sequencer.
import gleam/erlang/processimport gleam/ioimport gleam/jsonimport watershed_beamimport watershed/map_kernel.{type MapEvent, ValueChanged}type Msg { MapChanged(MapEvent)}pub fn main() { // Connect, blocking until the op history has replayed locally. `connect` // returns a Result — asserted here for brevity; a supervised app matches // the Error case and lets OTP restart the link. let assert Ok(doc) = watershed_beam.connect( host: "127.0.0.1", port: 4000, tenant: "dev-tenant", document: "river-gauges", token: token, user_id: "gauge-1", ) let gauges = watershed_beam.root(doc) let selector = process.new_selector() |> process.select_map(watershed_beam.subscribe(gauges), MapChanged) // Optimistic write: applies now and emits locally, then goes to the server. watershed_beam.set(gauges, "mill-race", json.int(24)) let MapChanged(event) = process.selector_receive_forever(selector) case event { ValueChanged(key:, ..) -> io.println("revised: " <> key) _ -> Nil }}
examples · erlang + javascript targets
What is implemented and tested
The runtime is tested three ways: against reference models, against a
live floodgate server, and against deliberately nasty orderings of
operations — 1,392 test functions across 105 test files at last count.
The ledger is what those checks cover: everything watershed ships.
Property-tested convergence
Eleven reference models — map, directory, counters, claims,
registers, ordered collection, pact, sequence, text — each replay
randomized multi-client command traces, 200 per property by default
(FUZZ_ITERATIONS raises it), including partial
sequencing and delivery, reconnect, rollback, stashed ops, and
summary-loaded joins. Invariants are checked after each command and
convergence after a final sync; qcheck shrinks failures and the
harness dumps a replayable JSON trace.
Read the harness →
Reference-checked map ops
SharedMap’s inner set, delete, and
clear payloads are checked against 26 oracle scenarios
generated from @fluidframework/map itself, including
state, events, and convergence. Container, attach, and summary
formats remain watershed’s own.
Read the corpus →
Reconnect safety
Buffered out-of-order delivery, in-band op requests, and client-id
remapping on reconnect. Pending edits survive the round trip — the
fuzz harness disconnects clients mid-trace to prove it.
Read the disconnect suite →
What ships today
01
MapsSharedMap, OR-map, and SharedDirectory: Fluid-derived op semantics, summaries, nested handles, and folder hierarchies
02
Counters and setsSharedCounter, G-counter, PN counter, G-set, 2P-set, and OR-set: delta merge, duplicate-safe delivery, add-wins and permanent removal
03
Sequences and textSharedSequence and SharedText: grapheme-indexed edits, convergent move, and anchors that survive concurrent edits
04
Transformsjson_ot and SharedRichText: one client-transform protocol over the ottypes json0 and quill-delta algebras
05
CoordinationClaims, TaskManager, ordered collection, register collection, and pact map: ownership, work queues with failover, and quorum agreement
06
RuntimeServer-sequenced delivery, requestOps catch-up, reconnect with client-id remap, nack policy, heartbeat, resubmit, and automatic summaries
07
App layerTyped schema decoding, ensure_* bootstrap, presence ripples, Lustre effect bindings, and the in-memory sluice for tests
Not yet a Hex release — installs are
git-pinned — and container, attach, and summary formats are
watershed’s own rather than Fluid’s. Wire compatibility is at the
operation layer.
Magenta indicates revisions not yet field-checkedConvergence by server sequencing — assumptions on the models sheettylerbutler.com