brightdb/sequence

A CRDT for sequential data based on LSEQ


License
BSD-3-Clause
Install
elm-package install brightdb/sequence 1.2.0

Documentation

Sequence

This is a operation-based conflict-free replicated data type (CRDT) for sequential data written in Elm. Another name for it is "ElmSEQ".

Its implementation is derived from Nedelec et al. "LSEQ: an adaptive structure for sequences in distributed collaborative editing" (2013).

CRDTs

Imagine Alice and Bob editing the same text document at the same time from different devices -- but both being offline. How could their changes be merged eventually so that both ends yield the same result and the intention of both probably conflicting editions is preserved?

Conflict-free replicated data types (CRDTs) to the rescue! In a mobile world with unreliable network connectivity, CRDTs allow for distributed offline-first applications integrating concurrent changes in a lightweight way. Data replicas are guaranteed to be eventually consistent among peers while keeping the context and order of merged operations.

Resources

Installation

This library is for Elm programs. So I assume you have Elm installed. Then install this lib in your project through:

elm install brightdb/sequence

Example

This example demonstrates an example use case of this library. First, we create concurrent operations by hand. Then we apply them to the data structure. In a real distributed environment operations would be created by peers and propagated to each other.

Here we are dealing with text as a sequence of characters. However, the CRDT can be applied to sequences (ordered lists) of any other type (eg. todos).

module Main exposing (main)

import Sequence exposing (..)
import String
import Tuple
import Html exposing (div, li, text)


main =
    let
        -- Allocate paths (= positions) in the sequence
        -- given a preceding and a following path.
        -- In the beginning we use the minimum and maximum possible path.
        path1 =
            alloc minPath maxPath

        path2 =
            alloc path1 maxPath

        path3 =
            alloc path2 maxPath

        -- This path is concurrent to path2 because it's allocated between the 
        -- same pair of preceding and following paths.
        concurrentPath =
            alloc path1 maxPath

        -- insert operations
        ops =
            [ createInsert "me" path1 'x'
            , createInsert "me" path2 'y'
            , createInsert "me" path3 'z'
            -- a concurrent insert operation (would come in from outside)
            , createInsert "you" concurrentPath 'a'
            ]

        getChar value =
            case value of
                Value a ->
                    String.fromChar a
                        |> Just

                Tomb x ->
                    Nothing

        fold path entry lis =
            let
                item =
                    case entry of
                        Single origin (Value char) ->
                            String.fromChar char
                                |> text
                                |> (\x -> [ x ])
                                |> li []

                        -- concurrent values are concatenating
                        -- with commas
                        Concurrent mvr ->
                            mvrToList mvr
                                |> List.filterMap (Tuple.second >> getChar)
                                |> List.intersperse ","
                                |> String.concat
                                |> text
                                |> (\x -> [ x ])
                                |> li []

                        _ ->
                            text ""
            in
                item :: lis
    in
        apply ops empty
            |> Tuple.first
            |> foldr fold []
            |> div []

Yields:

  • x
  • y,a
  • z

So x, y and z have been inserted by "me" while a has been inserted by "you" concurrently to y.