Pure-Elm Conflict-free Replicated Data Types: these are data structures that have the wonderful property that if you edit them concurrently and then combine the edits in any order, they will always converge to the same result.
Furthermore, they have this property under decentralization, meaning that they don't need to rely on a central server to coordinate merging (but you can still use them if you have a central server!).
This library allows you to relatively easily build applications supporting (some of) these awesome features:
Compared to some other libraries we also have some useful technical features:
By document we usually mean the part of the model that will be persisted or synced.
type alias Board =
{ title : String
, todos : List String
}
Refs are pointers into a datastructure that allow us to record edit intent that merges nicely in a type safe way. They come bundled with the schema in one flat record — a Ref per field, plus a reserved schema field.
import Crdt exposing (Ref)
type alias BoardDoc =
{ title : Ref Board Crdt.Settable String
, todos : Ref Board (Crdt.ListK Crdt.Fixed Crdt.Settable String) (List String)
, schema : Crdt.Schema Crdt.Nested Board
}
This teaches elm-crdt how to serialize, deserialize and merge your document.
You'll need to pay attention to use elements with appropriate merge semantics:
board : BoardDoc
board =
Crdt.record Board BoardDoc
|> Crdt.field "title" .title Crdt.text
|> Crdt.field "todos" .todos (Crdt.list Crdt.text).schema
|> Crdt.build
build fills in the schema field; the rest are your edit handles.
import Crdt.Edit as Edit
import Crdt.Id
alice : Doc Board
alice =
Crdt.init (Crdt.Id.replica "alice") board.schema
|> Edit.set board.title "Hello "
|> Result.withDefault (Crdt.init (Crdt.Id.replica "alice") board.schema)
and on another computer:
bob : Doc Board
bob =
Crdt.init (Crdt.Id.replica "bob") board.schema
|> Edit.set board.title "world"
|> Result.withDefault (Crdt.init (Crdt.Id.replica "bob") board.schema)
it doesn't matter on which computer or in what order
import Crdt.Doc as Doc
converged : Result Doc.ReadError Board
converged =
Doc.read (Doc.merge alice bob)
-- i.e. (Doc.merge bob alice) produces the same result
converged
|> Result.map .title
--> Ok "Hello world"
Of course in a more realistic example you would serialize these and send them over some transport, as well as show UI presence, etc. To see this all in action, checkout our demo.
All replicated state lives in one uniform recursive type (internal Crdt.Node)
derived from an operation log, with a single monomorphic merge. A separate typed
combinator layer (the Crdt module) ties that state to your own Elm records and hands
back typed refs — schemas describe reads, refs drive writes, so convergence
correctness never depends on the codec and edits are compile-checked. Sequences and
text are backed by an RGA; map keys carry last-write-wins presence cells so concurrent
set-vs-remove is well-defined.
Crdt: describe your document and point into it.
text, richText, counter, int, float,
string, bool) and containers (list, movableList, dict, tree); assemble
records and custom types.Crdt.Edit: type-safe writes through those refs.
The ref's kind makes a nonsensical edit a compile error.Crdt.Doc: work with a live document once you have one:
read/readAt its typed value (current, or at a past version)merge two documents togetherundo/redoCrdt.Id: the two identity types the library hands back:
ReplicaId (who a replica is)OpId (stable handles to things inside a document).Crdt.Cursor: stable caret/selection positions anchored to element identity,
so they survive concurrent reordering and deletion; cursorAt/cursorOffset create
and resolve them.Crdt.Presence: ephemeral "who's here and what are they doing" state (names,
colours, cursors, etc.) kept on a separate channel from the document.Crdt.Tree: the value a tree schema reads as: a Forest of Items.Crdt.RichText: the values a richText field reads as: formatted Spans and
Blocks.The demo/ directory is
a real collaborative todo + notes board: each browser tab is a replica, syncing
over a WebSocket through a tiny broadcast relay, with live presence and version
history. Try it live!
npm ci # install dev dependencies
npm test # compile, format check, elm-review, tests
npm run fix # auto-format and apply elm-review fixes
npm run docs # preview the package docs locally
npm start # run the demo
Inspired by Loro and Automerge.
Speaking of those libraries, we have some performance comparisons.
MIT.
29 commits
Elm
97.3%
JavaScript
2.7%
Pure-Elm Conflict-free Replicated Data Types: these are data structures that have the wonderful property that if you edit them concurrently and then combine the edits in any order, they will always converge to the same result.
Furthermore, they have this property under decentralization, meaning that they don't need to rely on a central server to coordinate merging (but you can still use them if you have a central server!).
This library allows you to relatively easily build applications supporting (some of) these awesome features:
Compared to some other libraries we also have some useful technical features:
By document we usually mean the part of the model that will be persisted or synced.
type alias Board =
{ title : String
, todos : List String
}
Refs are pointers into a datastructure that allow us to record edit intent that merges nicely in a type safe way. They come bundled with the schema in one flat record — a Ref per field, plus a reserved schema field.
import Crdt exposing (Ref)
type alias BoardDoc =
{ title : Ref Board Crdt.Settable String
, todos : Ref Board (Crdt.ListK Crdt.Fixed Crdt.Settable String) (List String)
, schema : Crdt.Schema Crdt.Nested Board
}
This teaches elm-crdt how to serialize, deserialize and merge your document.
You'll need to pay attention to use elements with appropriate merge semantics:
board : BoardDoc
board =
Crdt.record Board BoardDoc
|> Crdt.field "title" .title Crdt.text
|> Crdt.field "todos" .todos (Crdt.list Crdt.text).schema
|> Crdt.build
build fills in the schema field; the rest are your edit handles.
import Crdt.Edit as Edit
import Crdt.Id
alice : Doc Board
alice =
Crdt.init (Crdt.Id.replica "alice") board.schema
|> Edit.set board.title "Hello "
|> Result.withDefault (Crdt.init (Crdt.Id.replica "alice") board.schema)
and on another computer:
bob : Doc Board
bob =
Crdt.init (Crdt.Id.replica "bob") board.schema
|> Edit.set board.title "world"
|> Result.withDefault (Crdt.init (Crdt.Id.replica "bob") board.schema)
it doesn't matter on which computer or in what order
import Crdt.Doc as Doc
converged : Result Doc.ReadError Board
converged =
Doc.read (Doc.merge alice bob)
-- i.e. (Doc.merge bob alice) produces the same result
converged
|> Result.map .title
--> Ok "Hello world"
Of course in a more realistic example you would serialize these and send them over some transport, as well as show UI presence, etc. To see this all in action, checkout our demo.
All replicated state lives in one uniform recursive type (internal Crdt.Node)
derived from an operation log, with a single monomorphic merge. A separate typed
combinator layer (the Crdt module) ties that state to your own Elm records and hands
back typed refs — schemas describe reads, refs drive writes, so convergence
correctness never depends on the codec and edits are compile-checked. Sequences and
text are backed by an RGA; map keys carry last-write-wins presence cells so concurrent
set-vs-remove is well-defined.
Crdt: describe your document and point into it.
text, richText, counter, int, float,
string, bool) and containers (list, movableList, dict, tree); assemble
records and custom types.Crdt.Edit: type-safe writes through those refs.
The ref's kind makes a nonsensical edit a compile error.Crdt.Doc: work with a live document once you have one:
read/readAt its typed value (current, or at a past version)merge two documents togetherundo/redoCrdt.Id: the two identity types the library hands back:
ReplicaId (who a replica is)OpId (stable handles to things inside a document).Crdt.Cursor: stable caret/selection positions anchored to element identity,
so they survive concurrent reordering and deletion; cursorAt/cursorOffset create
and resolve them.Crdt.Presence: ephemeral "who's here and what are they doing" state (names,
colours, cursors, etc.) kept on a separate channel from the document.Crdt.Tree: the value a tree schema reads as: a Forest of Items.Crdt.RichText: the values a richText field reads as: formatted Spans and
Blocks.The demo/ directory is
a real collaborative todo + notes board: each browser tab is a replica, syncing
over a WebSocket through a tiny broadcast relay, with live presence and version
history. Try it live!
npm ci # install dev dependencies
npm test # compile, format check, elm-review, tests
npm run fix # auto-format and apply elm-review fixes
npm run docs # preview the package docs locally
npm start # run the demo
Inspired by Loro and Automerge.
Speaking of those libraries, we have some performance comparisons.
MIT.
29 commits
Elm
97.3%
JavaScript
2.7%