Presence is the "who's here and what are they doing" layer of a collaborative app — each person's name and colour, whether they are online, where their cursor is — as opposed to the document they are editing together.
It is kept deliberately separate from your Crdt.Doc, because it behaves differently:
presence is throwaway. You want the newest value and nothing else — when someone
moves their cursor you only care where it is now, and when they close the tab their
marker should just disappear. There is no history to preserve and nothing to reconcile
character by character, so presence has its own tiny, self-contained mechanism rather
than going through the document's CRDT machinery.
Each peer owns one slot in a shared table, keyed by its Crdt.Id.ReplicaId. When two
tables merge, the newer value wins for each peer — simple last-writer-wins, per peer.
You first describe the shape of one peer's presence with a small Codec, so it can be
sent as JSON. It reads like a compact record builder:
type alias Peer =
{ name : String
, color : String
, caret : Maybe Cursor
}
peerCodec : Presence.Codec Peer
peerCodec =
Presence.codec Peer
|> Presence.field "name" .name Presence.string
|> Presence.field "color" .color Presence.string
|> Presence.optional "caret" .caret Presence.cursor
|> Presence.buildCodecKeep one Presence Peer in your model alongside your document. Set your own state as it
changes, broadcast the table to peers exactly as you broadcast document updates, merge
what comes back, and render everyone with peers:
model.presence
|> Presence.setLocal { name = "Alice", color = "#e11", caret = Just cursor }
|> Presence.encode
-- send this JSON to peers
-- on receiving a peer's table:
case Presence.decode peerCodec json of
Ok theirs ->
{ model | presence = Presence.merge model.presence theirs }
Err _ ->
modelDescribes how one peer's presence value is turned into JSON and back. Build one with
codec/field/buildCodec. Opaque.
A fresh, empty presence table for a replica. Pass the same ReplicaId you gave
Crdt.init and the Codec describing your presence value.
Set this replica's own presence state (replacing any previous value). Call it whenever your local state changes — the user renamed themselves, moved their cursor — and then broadcast the table.
Update this replica's own state with a function, if it has been set. Handy for changing one part — moving the cursor while keeping name and colour:
Presence.updateLocal (\me -> { me | caret = Just cursor }) model.presenceEvery known peer and its state, including yourself, ordered by replica id. This is what you fold over to render collaborators (filter out your own id if you don't want to draw yourself).
Merge a table received from a peer into this one, keeping the newer value for each
replica. Like document merge, the result is the same no matter what order tables
arrive in.
Drop a peer from the table, e.g. when they disconnect. Because presence is throwaway there is nothing left behind — the peer simply vanishes, and reappears if it broadcasts again later.
Decode a presence table received from a peer. Merge the result into your own with
merge.
Start describing a presence value, given the constructor of your record (just like
Crdt.record). Follow it with a field/optional per record field and finish with
buildCodec.
Add a required field: its JSON name, a getter, and its field type.
Add an optional field, read as a Maybe and sent as null when absent — good for
things not always present, like a cursor only shown while editing.
Finish building a presence codec.
A string field.
A boolean field.
An integer field.
A Crdt.Cursor field — a peer's caret position. The common case for sharing where
everyone is typing, so it is built in:
Presence.codec Peer
|> Presence.optional "caret" .caret Presence.cursor
|> Presence.buildCodecA Crdt.Cursor.Range field — a peer's text selection (its two ends). Like
cursor, but for a highlighted range rather than a single caret.
A field with your own JSON encoder and decoder — for anything the built-in field types don't cover:
Presence.custom encodeColor colorDecoder
A table of everyone's presence state: this replica's own id and codec, plus the latest known value for every peer (including itself). Opaque — build one with
init.