The shared document behind the Leaf collaboration testbed.
This exists because the first version of the testbed kept the operation log in each LiveView's own assigns, which quietly made the page unable to show the thing it was built to show. A LiveView is torn down whenever its socket drops — routine on this host, where the socket often falls back to longpoll and a backgrounded tab stops polling — and it remounts with empty assigns. So a tab left open while you typed in another one came back blank, having missed every broadcast it was not alive for.
Holding the document and the operation log in one process fixes that and is also honest about what real collaboration needs: a session that joins late, or rejoins after a drop, has to be able to catch up. A page that only ever works for sessions present since the first keystroke is not collaborating.
Still no CRDT, on purpose. Operations are applied in arrival order and
base_length mismatches are recorded rather than resolved — surfacing
divergence is the point, since that is what a real merge layer would have to
handle.
Summary
Functions
Replace the document with a session's own copy of it.
Apply one session's operation to the shared document.
Returns a specification to start this module under a supervisor.
Everyone's caret except except, ready to hand to Leaf.
Write the document out now, without waiting for the timers.
Where this room's sessions hear about each other.
Register a session so its caret can be shown to everyone else.
Move a session's caret and selection.
Rebase op so it means the same thing after applied has happened.
Record what a session says it is holding.
Every session's last report.
Clear the document and the log, for everyone.
The current document and recent operations, for a session that just mounted.
Start a room for one document.
Functions
Replace the document with a session's own copy of it.
Used when an operation could not be placed. The editor's serialization is the
canonical form — markdown round-tripped through HTML comes back normalized,
so a room holding hand-written markdown (- ] rather than - ]) can never
agree with an editor about lengths. Taking the editor's text ends that
disagreement in the only direction that converges; pushing the room's text
back at the editor does not, because the editor immediately re-normalizes it
and disagrees again.
Apply one session's operation to the shared document.
An operation built against a document of a different length than the room
holds is REJECTED rather than applied. Its offsets refer to text that is not
here, so applying it would put characters in the wrong places for everybody,
and no later edit would undo that. The reply says applied: false and
carries the room's document so the sender can be put back in step.
Returns a specification to start this module under a supervisor.
See Supervisor.
Everyone's caret except except, ready to hand to Leaf.
Write the document out now, without waiting for the timers.
Where this room's sessions hear about each other.
Asked of the room rather than repeated by the host: one place decides, and a host that gets it wrong would have sessions that never see each other with nothing obviously broken.
Register a session so its caret can be shown to everyone else.
identity is who the host says this is: %{name: "Sasha", color: "#e11d48"}.
Both are optional and both have fallbacks, because a host may have no idea
who is editing — a public page, a draft nobody has signed in for — and that
should still work. A host with real users passes their names, and everyone
else sees a name rather than a random identifier.
The room monitors pid, so a session that closes its tab — or drops its
socket, which happens routinely on longpoll — has its caret removed without
needing to say goodbye. A LiveView cannot be relied on to run cleanup on the
way out, so being told is not an option.
Move a session's caret and selection.
offset is the caret and anchor is where the selection began; equal means
nothing is selected. nil means the person is no longer in the editor.
Rebase op so it means the same thing after applied has happened.
Two people typing at once each describe their edit against the text as they saw it, which is not the text the other one's edit produced. Applying both verbatim puts the characters in different places for each of them — the documents come apart by exactly the length of what the other person typed.
The disjoint cases are exact. Overlapping edits — two people changing the same characters — have no answer that preserves both intentions, so what is kept is whatever each edit touched that the other did not, which at least leaves every session with the same text.
Record what a session says it is holding.
Kept so two sessions can be compared: holding the same document while disagreeing about how many characters are in it is exactly the state that misplaces a caret, and neither session can see it on its own.
Every session's last report.
Clear the document and the log, for everyone.
The current document and recent operations, for a session that just mounted.
Start a room for one document.
One per document, started and supervised by the host — a vault would start one per note, on demand, and stop it when everybody has left.
Options:
:name— how to reach it. Required in practice: every other function names the room it means.:pubsub— yourPhoenix.PubSub, how the sessions in this document hear about each other.:document_id— what to call this document when talking to the store.:initial_content— what an empty document contains. The store wins if it has anything, so this is the text for a document nobody has written yet.:store— where the document lives; seeLeaf.Collab.Store. Defaults toLeaf.Collab.Store.None, which keeps nothing.:flush_after— how long a pause in the writing counts as finished, in milliseconds. Defaults to 2 seconds.:flush_at_most_every— how long writing that never pauses may go unwritten. Defaults to 15 seconds.
Stop it rather than killing it: the last thing a room does is write down what it was holding.