Skip to the content
W02NotesEngines from nothing

Weft, a text that stays one text when nobody is online

Weft is a CRDT for text writ­ten from an empty file. Several copies of one text are edited on their own, of­fline if need be, and every copy ends up as the same text once it has seen every change, what­ever order the changes arrive in. Below are three copies joined by a net­work you can cut. Type in all three, take one of­fline, keep typing, and bring it back.

PostedKindNotesSeriesEngines from nothing

All three copies read the same.

Who wrote what

Each copy keeps its own text, sends every change to the other two over the net­work, and ap­plies theirs as they arrive. An of­fline copy keeps what it sends until it is back. The dots are changes on their way. The line under the copies is the laptop’s text, each char­ac­ter marked by the copy that wrote it.

Every character has a name

A po­si­tion like “the fifth char­ac­ter” means dif­fer­ent things on two copies that have both been edited. So Weft never sends po­si­tions. Every char­ac­ter gets an id when it is typed: a counter and the name of the copy that typed it. No two char­ac­ters any­where share one. Each change is one of two kinds, “put this char­ac­ter behind that id” or “remove that id”, and an id points to the same char­ac­ter on every copy.

The counter is a Lamport clock. A copy’s next counter is one more than the high­est it has seen from anyone, so a char­ac­ter typed after an­other one was seen always has the higher counter.

The order comes from one rule

When two copies type behind the same char­ac­ter at the same time, both char­ac­ters are typed into the same spot. Weft puts the one with the higher id first and skips over every­thing typed behind it. Every copy com­pares the same ids, so every copy picks the same order, and a word typed on one copy is never split by a word typed on an­other.

The rule is the whole dif­fer­ence be­tween a CRDT and two texts that drift apart. Press “Leave out the rule” above and every copy in­serts right behind the char­ac­ter it was typed behind, in the order the net­work hap­pens to de­liver. The copies stop agree­ing within a few words.

Deleted characters stay, unseen

A copy that is of­fline may still type behind a char­ac­ter an­other copy has deleted. When that change ar­rives, the char­ac­ter it names has to be there. So a delete only marks a char­ac­ter as gone: it stays in the order as a tomb­stone, and the text you read skips it. “Show deleted” above draws them, struck through.

A change can also arrive before the change it de­pends on, when the net­work de­liv­ers out of order. Weft keeps it aside and tries again after each ar­rival. A change that ar­rives twice is rec­og­nized by its id and ap­plied once.

Held against chance

A hand­ful of hand-writ­ten cases cannot show that every order of ar­rival ends in the same text. The fuzzer can come closer. Each ses­sion starts two to four copies, types and deletes at random, de­liv­ers every change to every other copy in a shuf­fled order, some of them twice, and then checks that every copy reads the same. A seeded gen­er­a­tor makes each ses­sion re­peat­able, so a fail­ure can be re­played.

Sessions·
Operations·
Copies that differ·

Each dot is a ses­sion: violet when every copy agrees, red when they do not.

The same fuzzer runs from the com­mand line, with the rule and with­out it:

$ node web/notes/weft/fuzz.mjs
20,000 sessions, 2,276,527 operations, with the ordering rule, in 9.2 s
every session ended with every copy the same text

$ node web/notes/weft/fuzz.mjs --without-rule
20,000 sessions, 2,276,854 operations, without the ordering rule, in 9.1 s
19,871 sessions ended with copies that differ; the first is session 1

Without the rule nearly every ses­sion fails. A fuzzer that cannot catch a miss­ing rule would prove noth­ing when it passes.

What it does not have

Weft is writ­ten to un­der­stand what a pro­duc­tion CRDT does. Zibaldone keeps its doc­u­ments in Yjs, which also han­dles rich text, stores a run of typing as one record, and drops the con­tent of deleted text. Weft sends one op­er­a­tion per char­ac­ter, keeps every tomb­stone and knows only plain text. It does carry the prop­erty that mat­ters, and the fuzzer above checks it.

The source is in the port­fo­lio’s repos­i­tory under web/notes/weft/: weft.js for the CRDT and the fuzzer, fuzz.mjs for the com­mand, weft.test.mjs for the tests.