Client A
0 graphemes 0 pendingno anchor pinned
← Sequences · SharedText
A character offset only means something against one version of a
string. The instant two people type in the same word, “index 6” names
different characters on different screens.
SharedText gives every grapheme a stable identity
beneath its index: you say insert at 6 or
replace 0..5, but the delta that ships names the graphemes, not
the offsets. Two people can type into the same word at once:
concurrent inserts both land, overlapping edits merge, and every
replica converges on the same text. Indexing is by
grapheme, never UTF-16 code unit, so an emoji or a
combining accent is one indivisible character. watershed’s
text_kernel models that identity and converges.
Three watershed documents share one
SharedText, a collaboratively-typed string. One client
creates it and shares the handle; the others resolve it. They talk to one
sluice, the in-memory server that ships in the library. It runs the production client runtime without a
network backend. Type in any editor:
your keystrokes show in magenta while
pending, then settle to ink once the sluice
sequences them and every replica lands the same text.
Every keystroke is diffed into one minimal grapheme edit: an insert, delete, or replace against grapheme indexes, never raw UTF-16 offsets. Stage a race: Crowd an insert has B and C type different words at the same spot; Overlapping edit has B replace a word while C deletes across it. Both converge, because each grapheme keeps a stable identity: the delta names graphemes, not character positions.
The seed text is grapheme-rich on purpose. 👨👩👧 is a ZWJ sequence, several code points joined into one grapheme, one index; a cursor can never split it. The í in río is a letter plus a combining accent, again one grapheme. And 🏞️ includes a variation selector. SharedText indexes by grapheme, so none of these can be bisected by an edit.
no anchor pinned
no anchor pinned
no anchor pinned
Converged all replicas identical · nothing pending
Latency and jitter affect simulated arrival order; animation speed changes playback only.
The live demo couldn’t start: it runs the production client runtime against
the in-memory sluice as compiled JavaScript, and this browser
didn’t load it. The rest of the page works fine without it.
Everything the rig above does by hand ships as a
component: <watershed-textarea>, the watershed_lustre editor
compiled from Gleam and registered as a custom element. These two panes
are that element: two documents, one shared SharedText,
the same in-memory sluice, and this page contributes no editor code at all.
The channel goes in as one property (el.channel = text);
edits, errors, and this user’s cursor come back out as DOM events; the
other pane’s cursor goes back in as data.
The component owns what the naïve bridge gets wrong: each keystroke is diffed into one minimal grapheme op, a peer’s edit can’t teleport your caret, and an IME composition survives remote keystrokes. Select a few words in one pane and a named highlight appears in the other, then type before them and watch it stay glued to those words. It travels as content-bound anchors, not offsets, which is the same machinery that keeps your own caret in place.
Cursors here hop panes through a property assignment; in an app
the cursor event’s payload rides your presence channel
unchanged.
The live demo couldn’t start: it runs the compiled
<watershed-textarea> component against the in-memory
sluice, and this browser didn’t load it. The rest of the page
works fine without it.