Live editing, wired into a LiveView in one call.
Everything a shared document needs — placing edits that crossed on the wire, agreeing which version everyone is on, putting a session back in step after its socket drops, showing where other people's carets are — is here rather than in the host. It is not obvious code and it is not code worth writing twice; every part of it exists because something went visibly wrong without it.
Using it
def mount(%{"id" => id}, _session, socket) do
{:ok,
Leaf.Collab.join(socket,
room: MyApp.Notes.room_name(id),
editor_id: "note-editor",
identity: %{name: socket.assigns.current_user.name}
)}
endand in the template:
<.leaf_editor
id="note-editor"
content={@leaf_collab.content}
collaboration={@leaf_collab.collaboration}
/>That is the whole integration. join/2 attaches a handle_info hook, so the
host writes no message handling of its own. A LiveView that switches
documents without remounting calls leave/1 before joining the next room —
see leave/1.
What the hook consumes
The hook handles — and stops — {:leaf_operation, …}, {:leaf_awareness, …},
{:leaf_ready, …}, {:leaf_resync, …}, {:leaf_debug_state, …},
{:leaf_changed, …}, and the room's {:leaf_conflict, …} /
{:leaf_conflict_cleared, …}. A collaborating LiveView therefore does not receive
{:leaf_changed, …}: read the document from @leaf_collab.content, which is
kept current on every edit, local or remote. Anything the hook does not
recognise passes through to the host's own handle_info clauses untouched.
One collaborative editor per LiveView: join/2 owns the @leaf_collab
assign and the hook name.
What the host still owns
Starting a room per document and supervising it — Leaf has no opinion about
how many nodes you run or how you name processes. And saying where documents
live, which is Leaf.Collab.Store.
Turning it off
Not calling join/2 costs nothing. The editor does no collaboration work
unless it is asked to: no coordinates measured, no fingerprints taken, no
selection listener attached. Somebody using Leaf for a comment box pays for
none of this.
Summary
Functions
Join the document this room holds.
Options:
:room— the room process, started and supervised by the host:editor_id— theidgiven toleaf_editor:identity—%{name:, color:}, both optional. A host with signed-in users passes theirs so everyone sees a name rather than an identifier.:awareness— show other people's carets and selections. Defaults true.:debug— diagnostics. Defaults false; seeLeaf.Collab.Log.
Leave the document, undoing what join/2 did.
For a LiveView that outlives its stay in a room — one that switches
documents without remounting, the way an editor with a sidebar keeps the
sidebar alive. Closing the tab needs no call: the room monitors the
process. But a live process that merely moves on is invisible to the
monitor, so without this the person lingers as a ghost caret in the
document they left, and the next join/2 raises on the hook it cannot
attach twice.
Removes this session from the room so its caret disappears for everyone
else, unsubscribes, detaches the hook, and clears @leaf_collab — after
which join/2 works again with whatever room comes next:
def handle_params(%{"id" => id}, _uri, socket) do
{:noreply,
socket
|> Leaf.Collab.leave()
|> Leaf.Collab.join(room: room_for(id), editor_id: "note-editor")}
endIdempotent: leaving without having joined is a no-op, so the pipeline above needs no first-mount special case.
Everyone in the document, for a host that wants to list them.
Put the document back to its starting text, for everyone.
Handles the part a host would forget: telling the other sessions. A reset that only changed the room left every other tab holding the old text, and the next reconciliation adopted it straight back — the reset undone by the people it was for.