dot

package module
Version: v0.0.0-...-917641f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 30, 2019 License: MIT Imports: 12 Imported by: 1

README

DOT

Status GoDoc codecov Go Report Card

The DOT project is a blend of operational transformation, CmRDT, persistent/immutable datastructures and reactive stream processing.

This is an implementation of distributed data synchronization of rich custom data structures with conflict-free merging.

Status

This is very close to v1 release. The ES6 version interoperates well right now but outstanding short-term issues have more to do with consistency of the API surface than features:

  • ~The ES6 version has a simpler polling-based Network API that seems worth adopting here.~ ** Adopted **
  • ~The ES6 branch/undo integration also feels a lot simpler.~ ** Adopted **
  • The ES6 version prefers replace() instead of update().
  • Nullable value types (i.e typed Nil values vs change.Nil vs nil) seems confusing.

Features

  1. Small, well tested mutations and immutable persistent values
  2. Support for rich user-defined types, not just collaborative text
  3. Streams and Git-like branching, merging support
  4. Simple network support (Gob serialization) and storage support
  5. Strong references support that are automatically updated with changes
  6. Rich builtin undo support for any type and mutation
  7. Folding (committed changes on top of uncommitted changes)
  8. Support for CmRDT types (see crdt)

An interoperable ES6 version is available on dotchain/dotjs with a TODO MVC demo of it here

Contents

  1. Status
  2. Features
  3. CRDTs
  4. TODO Example
    1. Server
    2. Types
    3. Type registration
    4. Code generation
    5. Toggling Complete
    6. Changing description
    7. Adding Todos
    8. Client connection
    9. Running the demo
    10. In browser demo
  5. How it all works
    1. Applying changes
    2. Applying changes with streams
    3. Composition of changes
    4. Convergence
    5. Convergence using streams
    6. Revert and undo
    7. Folding
    8. Branching of streams
    9. References
    10. Network synchronization and server
  6. Broad Issues
  7. Contributing

CRDTs

Much of the framework can support operation-based CRDT changes which simply appear as commutative operations (and so the merge operation is trivial). A set of types built this way is available in the crdt folder.

TODO Example

The standard TODO-MVC example demonstrates the features of collaborative (eventually consistent) distributed data structures.

Server

The DOT backend is essentially a simple log store. All mutations to the application state are represented as a sequence of operations and written in append-only fashion onto the log. The following snippet shows how to start a web server (though it does not include authentication or CORs for example).


func Server() {
	// import net/http
	// import github.com/dotchain/dot

        // uses a local-file backed bolt DB backend
	http.Handle("/dot/", dot.BoltServer("file.bolt"))
        http.ListenAndServe(":8080", nil)
}

The example above uses the Bolt backend for the actual storage of the operations. There is also a Postgres backend available.

Note that the server above has no real reference to any application logic: it simply accepts operations and writes them out in a guaranteed order broadcasting these to all the clients.

Types

A TODO MVC app consists of only two core types: Todo and TodoList:


// Todo tracks a single todo item
type Todo struct {
	Complete bool
        Description string
}

// TodoList tracks a collection of todo items
type TodoList []Todo

Type registration

To use the types across the network, they have to be registered with the codec (which will be sjson in this example)

// import github.com/dotchain/dot/ops/nw

func init() {
	nw.Register(Todo{})
        nw.Register(TodoList{})
}
Code generation

For use with DOT, these types need to be augmented with standard methods of the Value interface (or in the case of lists like TodoList, also implement the Collection interface).

These interfaces are essentially the ability to take changes of the form replace a sub field or replace items in the array and calculate the result of applying them. They are mostly boilerplate and so can be autogenerated easily via the dotc package. See code generation for augmenting the above type information.

The code generation not only implements these two interfaces, it also produces a new Stream type for Todo and TodoList. A stream type is like a linked list with the Value field being the underlying value and Next() returning the next entry in the stream (in case the value was modified). And Latest returns the last entry in the stream at that point. Also, each stream type implements mutation methods to easily modify the value associated with a stream.

What makes the streams interesting is that two different modifications from the same state cause the Latest of both to be the same with the effect of both merged. (This is done using the magic of operational transformations)

Toggling Complete

The code to toggle the Complete field of a particular todo item looks like the following:

func Toggle(t *TodoListStream, index int) {
	// TodoListStream.Item() is generated code. It returns
        // a stream of the n'th element of the slice so that
        // particular stream can be modified. When that stream is
        // modified, the effect is automatically merged into the
        // parent (and available via .Next of the parent stream)
	todoStream := t.Item(index) 

	// TodoStream.Complete is generated code. It returns a stream
        // for the Todo.Complete field so that it can be modified. As
        // with slices above, mutations on the field's stream are
        // reflected on the struct stream (via .Next or .Latest())
        completeStream := todoStream.Complete()

	// completeStream is of type streams.Bool. All streams
        // implement the simple Update(newValue) method that replaces
        // the current value with a new value.
        completeStream.Update(!completeStream.Value)
}

Note that the function does not return any value here but the updates can be fetched by calling .Latest() on any of the corresponding streams. If a single stream instance has multiple edits, the Latest() value is the merged value of all those edits.

Changing description

The code for changing the Description field is similar. The string Description field in Todo maps to a streams.S16 stream. This implements an Update() method like all streams.

But to make things interesting, lets look at splicing rather than replacing the whole string. Splicing is taking a subsequence of the string at a particular position and replacing it with the provided value. It captures insert, delete and replace in one operation.

This probably better mimics what text editors do and a benefit of such high granularity edits is that when two users edit the same text, the edits will merge quite cleanly so long as they don't directly touch the same characters.

func SpliceDescription(t *TodoListStream, index, offset, count int, replacement string) {
	// TodoListStream.Item() is generated code. It returns
        // a stream of the n'th element of the slice so that
        // particular stream can be modified. When that stream is
        // modified, the effect is automatically merged into the
        // parent (and available via .Next of the parent stream)
	todoStream := t.Item(index) 

	// TodoStream.Description is generated code. It returns a
        // stream for the Todo.Description field so that it can be
        // modified. As with slices above, mutations on the field's
        // stream are reflected on the struct stream (via .Next or
        // .Latest()) 
	// TodoStream.Description() returns streams.S16 type
        descStream := todoStream.Description()

	// streams.S16 implements Splice(offset, removeCount, replacement)
        descStream.Splice(offset, count, replacement)
}
Adding Todos

Adding a Todo is relatively simple as well:

func AddTodo(t *TodoListStream, todo Todo) {
	// All slice streams implement Splice(offset, removeCount, replacement)
	t.Splice(len(t.Value), 0, todo)
}

The use of Splice in this example should hint that (just like strings) collections support insertion/deletion at arbitrary points within via the Splice method. In addition to supporting this, collections also support the Move(offset, count, distance) method to move some items around within the collection

Client connection

Setting up the client requires connecting to the URL where the server is hosted. In addition, the code below illustrates how sessions could be saved and restarted if needed.

// import time
// import sync
// import github.com/dotchain/dot

var Lock sync.Mutex
func Client(stop chan struct{}, render func(*TodoListStream)) {
	url := "http://localhost:8080/dot/"
        session, todos := SavedSession()
	s, store := session.NonBlockingStream(url, nil)
        defer store.Close()

	todosStream := &TodoListStream{Stream: s, Value: todos}

        ticker := time.NewTicker(500*time.Millisecond)
        changed := true
	for {
        	if changed {
			render(todosStream)
                }
        	select {
                case <- stop:
                	return
                case <- ticker.C:
                }

                Lock.Lock()
		s.Push()
                s.Pull()
                next := todosStream.Latest()
                changed = next != todosStream
                todosStream, s = next, next.Stream
                Lock.Unlock()
        }

       	SaveSession(session, todosStream.Value)
}


func SaveSession(s *dot.Session, todos TodoList) {
	// this is not yet implemented. if it were, then
        // this value should be persisted locally and returned
        // by the call to savedSession
}

func SavedSession() (s *dot.Session, todos TodoList) {
	// this is not yet implemented. return default values
        return dot.NewSession(), nil
}

Running the demo

The TODO MVC demo is in the example folder.

The snippets in this markdown file can be used to generate the todo.go file and then auto-generate the "generated.go" file:

$ go get github.com/tvastar/test/cmd/testmd
$ testmd -pkg example -o examples/todo.go README.md
$ testmd -pkg main codegen.md > examples/generated.go

The server can then be started by:

$ go run server.go

The client can then be started by:

$ go run client.go

The provide client.go stub file simply appends a task every 10 seconds.

In browser demo

The fuss project has demos of a TODO-MVC app built on top of this framework using gopherjs. In particular, the collab folder illustrates how simple the code is to make something work collaboratively (the rest of the code base is not even aware of whether things are collaborative).

How it all works

There are values, changes and streams.

  1. Values implement the Value interface. If the value represents a collection, it also implements the Collection interface.
  2. Changes represent mutations to values that can be merged. If two independent changes are made to the same value, they can be merged so that the A + merged(B) = B + merged(A). This is represented by the Change interface. The changes package implements the core changes with composition that allow richer changes to be implemented.
  3. Streams represent a sequence of changes to a value, except it is convergent -- if multiple writers modify a value, they each get a separate stream instance that only reflects their local change but following the Next chain will guarantee that all versions end up with the same final value.
Applying changes

The following example illustrates how to edit a string with values and changes

	// import fmt
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types

	// S8 is DOT-compatible string type with UTF8 string indices
	initial := types.S8("hello")

        append := changes.Splice{
        	Offset: len("hello"), // end of "hello"
                Before: types.S8(""), // nothing to remove
                After: types.S8(" world"), // insert " world"
        }

        // apply the change
        updated := initial.Apply(nil, append)

	fmt.Println(updated)
        // Output: hello world
Applying changes with streams

A less verbose stream based version (preferred) would look like so:

	// import fmt
        // import github.com/dotchain/dot/streams

        initial := &streams.S8{Stream: streams.New(), Value: "hello"}
        updated := initial.Splice(5, 0, " world")

	fmt.Println(updated.Value)
        // Output: hello world

The changes package implements the core changes: Splice, Move and Replace. The logical model for these changes is to treat all values as either being like arrays or like maps. The actual underlying datatype can be different as long as the array/map semantics is implemented.

Composition of changes

Changes can be composed together. A simple form of composition is just a set of changes:

	// import fmt
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types

	initial := types.S8("hello")

        // append " world" => "hello world"
        append1 := changes.Splice{
        	Offset: len("hello"),
                Before: types.S8(""),
                After: types.S8(" world"),
        }

        // append "." => "hello world."
        append2 := changes.Splice{
        	Offset: len("hello world"),
                Before: types.S8(""),
                After: types.S8("."),
        }
        
        // now combine the two appends and apply
        both := changes.ChangeSet{append1, append2}
        updated := initial.Apply(nil, both)
        fmt.Println(updated)

	// Output: hello world.

Another form of composition is modifying a sub-element such as an array element or a dictionary path:

	// import fmt
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types

        // types.A is a generic array type and types.M is a map type
        initial := types.A{types.M{"hello": types.S8("world")}}

        // replace "world" with "world!"
        replace := changes.Replace{Before: types.S8("world"), After: types.S8("world!")}

        // replace "world" with "world!" of initial[0]["hello"]
        path := []interface{}{0, "hello"}
        c := changes.PathChange{Path: path, Change: replace}
        updated := initial.Apply(nil, c)
        fmt.Println(updated)

	// Output: [map[hello:world!]]        
Convergence

The core property of all changes is the ability to guarantee convergence when two mutations are attempted on the same state:

	// import fmt
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types

	initial := types.S8("hello")

	// two changes: append " world" and delete "lo"
	insert := changes.Splice{Offset: 5, Before: types.S8(""), After: types.S8(" world")}
	remove := changes.Splice{Offset: 3, Before: types.S8("lo"), After: types.S8("")}

	// two versions derived from initial
        inserted := initial.Apply(nil, insert)
        removed := initial.Apply(nil, remove)

        // merge the changes
        removex, insertx := insert.Merge(remove)

        // converge by applying the above
        final1 := inserted.Apply(nil, removex)
        final2 := removed.Apply(nil, insertx)

        fmt.Println(final1, final1 == final2)
        // Output: hel world true
Convergence using streams

The same convergence example is a lot easier to read with streams:

	// import fmt
        // import github.com/dotchain/dot/streams

	initial := streams.S8{Stream:  streams.New(), Value: "hello"}

	// two changes: append " world" and delete "lo"
        s1 := initial.Splice(5, 0, " world")
	s2 := initial.Splice(3, len("lo"), "")

	// streams automatically merge because they are both
        // based on initial
        s1 = s1.Latest()
        s2 = s2.Latest()

        fmt.Println(s1.Value, s1.Value == s2.Value)
        // Output: hel world true

The ability to merge two independent changes done to the same initial state is the basis for the eventual convergence of the data structures. The changes package has fairly intensive tests to cover the change types defined there, both individually and in composition.

Revert and undo

All the predefined types of changes in DOT (see changes) are carefully designed so that every change can be inverted easily without reference to the underlying value. For example, changes.Replace has both the Before and After fields instead of just keeping the After. This allows the reverse to be computed quite easily by swapping the two fields. This does generally incur additional storage expenses but the tradeoff is that code gets much simpler to work with.

In particular, it is possible to build generic undo support quite easily and naturally. The following example shows both Undo and Redo being invoked from an undo stack.

	// import fmt
        // import github.com/dotchain/dot/streams
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types
        // import github.com/dotchain/dot/streams/undo

	// create master, undoable child and the undo stack itself
	master := &streams.S16{Stream: streams.New(), Value: "hello"}
        s := undo.New(master.Stream)
        undoableChild := &streams.S16{Stream: s, Value: master.Value}

	// change hello => Hello
	undoableChild = undoableChild.Splice(0, len("h"), "H")
	fmt.Println(undoableChild.Value)

	// for kicks, update master hello => hello$ as if it came
        // from the server
        master.Splice(len("hello"), 0, "$")

	// now undo this via the stack
        s.Undo()

	// now undoableChild should be hello$
        undoableChild = undoableChild.Latest()
        fmt.Println(undoableChild.Value)

	// now redo the last operation to get Hello$
        s.Redo()
        undoableChild = undoableChild.Latest()
        fmt.Println(undoableChild.Value)
        
	// Output:
        // Hello
        // hello$
        // Hello$
Folding

In the case of editors, folding refers to a piece of text that has been hidden away. The difficulty with implementing this in a collaborative setting is that as external edits come in, the fold has to be maintained.

The design of DOT allows for an elegant way to achieve this: consider the "folding" as a local change (replacing the folded region with say "..."). This local change is never meant to be sent out. All changes to the unfolded and folded versions can be proxied quite nicely without much app involvement:

	// import fmt
        // import github.com/dotchain/dot/streams
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types
        // import github.com/dotchain/dot/x/fold

	// create master, folded child and the folding itself
	master := &streams.S16{Stream: streams.New(), Value: "hello world!"}
        foldChange := changes.Splice{
        	Offset: len("hello"),
                Before: types.S16(" world"),
                After: types.S16("..."),
        }
        foldedStream := fold.New(foldChange, master.Stream)
        folded := &streams.S16{Stream: foldedStream, Value :"hello...!"}

        // folded:  hello...! => Hello...!!!
	folded = folded.Splice(0, len("h"), "H")
        folded = folded.Splice(len("Hello...!"), 0, "!!")
        fmt.Println(folded.Value)

	// master: hello world => hullo world
	master = master.Splice(len("h"), len("e"), "u")
        fmt.Println(master.Value)

        // now folded = Hullo...!!!
        fmt.Println(folded.Latest().Value)

        // master = Hullo world!!!
        fmt.Println(master.Latest().Value)

	// Output:
        // Hello...!!!
        // hullo world!
        // Hullo...!!!
        // Hullo world!!!
Branching of streams

Streams in DOT can also be branched a la Git. Changes made in branches do not affect the master or vice-versa -- until one of Pull or Push are called.

	// import fmt
        // import github.com/dotchain/dot/streams
        // import github.com/dotchain/dot/changes
        // import github.com/dotchain/dot/changes/types        
        
        // local is a branch of master
        master := &streams.S16{Stream: streams.New(), Value: "hello"}
        local := &streams.S16{Stream: streams.Branch(master.Stream), Value: master.Value}

	// edit locally: hello => hallo
	local.Splice(len("h"), len("e"), "a")

	// changes will not be reflected on master yet
        fmt.Println(master.Latest().Value)

	// push local changes up to master now
        local.Stream.Push()

	// now master = hallo
	fmt.Println(master.Latest().Value)

        // Output:
        // hello
        // hallo

There are other neat benefits to the branching model: it provides a fine grained control for pulling changes from the network on demand and suspending it as well as providing a way for making local changes.

References

There are two broad cases where a JSON-like structure is not quite enough.

  1. Editors often need to track the cursor or selection which can be thought of as offsets in the editor text. When changes happen to the text, for example, the offset would need to be updated.
  2. Objects often need to refer to other parts of the JSON-tree. For example, one can represent a graph using the array, map primitives with the addition of references. When changes happen, these too would need to be updated.

The refs package implements a set of types that help work with these. In particular, it defines a Container value that allows elements within to refer to other elements.

Network synchronization and server

DOT uses a fairly simple backend Store interface: an append-only dumb log. The Bolt and Postgres implementations are quite simple and other data backends can be easily added.

See Server and Client connection for sample server and client applications. Note that the journal approach used implies that the journal size only increases and so clients will eventually take a while to rebuild their state from the journal. The client API allows snapshotting state to make the rebuilds faster. There is no server support for snapshots though it is possible to build one rather easily

Broad Issues

  1. changes.Context/changes.Meta are not fully integrated
  2. ~gob-encoding makes it harder to deal with other languages but JSON encodindg wont work with interfaces.~
    • Added sjson encoding as a portable (if verbose) format.
    • The ES6 dotjs package uses this as the native format.
  3. Cross-object merging and persisted branches need more platform support
    • Snapshots are somewhat related to this as well.
  4. Full rich-text support with collaborative cursors still needs work with references and reference containers.
  5. Code generation can infer types from regular go declarations
  6. Snapshots and transient states need some sugar.

Contributing

Please see CONTRIBUTING.md.

Documentation

Overview

Package dot implements data synchronization of user defined types using operational transformation/OT.

Please see https://github.com/dotchain/dot for a tutorial on how to use DOT.

The core functionality is spread out between dot/changes, dot/streams, dot/refs and dot/ops but this package exposes simple client and server implementations for common use cases:

Server example

import "encoding/gob"
import "net/http"
import "github.com/dotchain/dot"
...
gob.Register(..) // register any user-standard OT types used
http.Handle("/api/", dot.BoltServer("file.bolt"))
http.ListenAndServe(":8080", nil)

Client example

import "encoding/gob"
import "net/http"
import "github.com/dotchain/dot"
...
gob.Register(..) // register any user-standard OT types used
session, stream := dot.Connect("http://localhost:8080/api/")

Immutable values

DOT uses immutable values. Every Value must implement the change.Value interface which is a single Apply method that returns the result of applying a mutation (while leaving the original value effectively unchanged).

If the underlying type behaves like a collection (such as with Slices), the type must also implement some collection specific methods specified in the changes.Collection interface.

Most actual types are likely to be structs or slices with boilerplate implementaations of the interfaces. The x/dotc package has a code generator which can emit such boilerplate implementations simplifying this task.

Changes

The changes package implements a set of simple changes (Replace, Splice and Move). Richer changes are expected to be built up by composition via changes.ChangeSet (which is a sequence of changes) and changes.PathChange (which modifies a value at a path).

Changes are immutable too and generally are meant to not maintain any reference to the value they apply on. While custom changes are possible (they have to implement the changes.Custom interface), they are expected to be rare as the default set of chnange types cover a vast variety of scenarios.

The core logic of DOT is in the Merge methods of changes: they guaranteee that if two independent changes are done to a value, the deviation in the values can be converged. The basic property of any two changes (on the same value) is that:

leftx, rightx := left.Merge(right)
initial.Apply(nil, left).Apply(nil, leftx) ==
initial.Apply(nil, right).Apply(nil, rightx)

Care must be taken with custom changes to ensure that this property is preserved.

Streams

Streams represent the sequence of changes associated with a single value. Stream instances behave like they are immutable: when a change happens, a new stream instance captures the change. Streams also support multiple-writers: it is possible for two independent changes to the same stream instance. In this case, the newly-created stream instances only capture the respective changes but these both have a "Next" value that converges to the same value. That is, the two separate streams implicitly have the changes from each other (but after transforming through the Merge) method.

This allows streams to perform quite nicely as convergent data structures without much syntax overhead:

initial := streams.S8{Stream:  streams.New(), Value: "hello"}

// two changes: append " world" and delete "lo"
s1 := initial.Splice(5, 0, " world")
s2 := initial.Splice(3, len("lo"), "")

// streams automatically merge because they are both
// based on initial
s1 = s1.Latest()
s2 = s2.Latest()

fmt.Println(s1.Value, s1.Value == s2.Value)
// Output: hel world true

Strongly typed streams

The streams package provides a generic Stream implementation (via the New function) which implements the idea of a sequence of convergent changes. But much of the power of streams is in having strongly type streams where the stream is associated with a strongly typed value. The streams package provides simple text streamss (S8 and S16) as well as Bool and Counter types. Richer types like structs and slices can be converted to their stream equivalent rather mechanically and this is done by the x/dotc package -- using code generation.

Some day, Golang would support generics and then the code
generation ugliness of x/dotc will no longer be needed.

Substreams

Substreams are streams that refer into a particular field of a parent stream. For example, if the parent value is a struct with a "Done" field, it is possible to treat the "Done stream" as the changes scoped to this field. This allows code to be written much more cleanly. See the https://github.com/dotchain/dot#toggling-complete section of the documentation for an example.

Other features

Streams support branching (a la Git) and folding. See the examples!

Streams also support references. A typical use case is maintaining the user cursor within a region of text. When remote changes happen to the text, the cursor needs to be updated. In fact, when one takes a substream of an element of an array, the array index needs to be automatically managed (i.e. insertions into the array before the index should automatically update the index etc). This is managed within streams using references.

Server implementations

A particular value can be reconstituted from the sequence of changes to that value. In DOT, only these changes are stored and that too in an append-only log. This make the backend rather simple and generally agnostic of application types to a large extent.

See https://github.com/dotchain/dot#server for example code.

Example (ApplyingChanges)
package main

import (
	"fmt"

	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types

	// S8 is DOT-compatible string type with UTF8 string indices
	initial := types.S8("hello")

	append := changes.Splice{
		Offset: len("hello"),       // end of "hello"
		Before: types.S8(""),       // nothing to remove
		After:  types.S8(" world"), // insert " world"
	}

	// apply the change
	updated := initial.Apply(nil, append)

	fmt.Println(updated)
}
Output:

hello world
Example (ApplyingChangesUsingStreams)
package main

import (
	"fmt"

	"github.com/dotchain/dot/streams"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/streams

	initial := &streams.S8{Stream: streams.New(), Value: "hello"}
	updated := initial.Splice(5, 0, " world")

	fmt.Println(updated.Value)
}
Output:

hello world
Example (Branching)
package main

import (
	"fmt"

	"github.com/dotchain/dot/streams"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/streams
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types

	// local is a branch of master
	master := &streams.S16{Stream: streams.New(), Value: "hello"}
	local := &streams.S16{Stream: streams.Branch(master.Stream), Value: master.Value}

	// edit locally: hello => hallo
	local.Splice(len("h"), len("e"), "a")

	// changes will not be reflected on master yet
	fmt.Println(master.Latest().Value)

	// push local changes up to master now
	local.Stream.Push()

	// now master = hallo
	fmt.Println(master.Latest().Value)

}
Output:

hello
hallo
Example (ChangesetComposition)
package main

import (
	"fmt"

	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types

	initial := types.S8("hello")

	// append " world" => "hello world"
	append1 := changes.Splice{
		Offset: len("hello"),
		Before: types.S8(""),
		After:  types.S8(" world"),
	}

	// append "." => "hello world."
	append2 := changes.Splice{
		Offset: len("hello world"),
		Before: types.S8(""),
		After:  types.S8("."),
	}

	// now combine the two appends and apply
	both := changes.ChangeSet{append1, append2}
	updated := initial.Apply(nil, both)
	fmt.Println(updated)

}
Output:

hello world.
Example (ClientServerUsingBoltDB)
package main

import (
	"fmt"
	"log"
	"net/http/httptest"
	"os"

	"github.com/dotchain/dot"
	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
)

func main() {
	defer remove("file.bolt")()

	logger := log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)
	srv := dot.WithLogger(dot.BoltServer("file.bolt"), logger)
	defer dot.CloseServer(srv)
	httpSrv := httptest.NewServer(srv)
	defer httpSrv.Close()

	stream1, store1 := dot.NewSession().NonBlockingStream(httpSrv.URL, nil)
	stream2, store2 := dot.NewSession().Stream(httpSrv.URL, nil)

	defer store1.Close()
	defer store2.Close()

	stream1.Append(changes.Replace{Before: changes.Nil, After: types.S8("hello")})
	fmt.Println("push", stream1.Push())
	fmt.Println("pull", stream2.Pull())

}

func remove(fname string) func() {
	if err := os.Remove(fname); err != nil {
		log.Println("Couldnt remove file", fname)
	}
	return func() {
		if err := os.Remove(fname); err != nil {
			log.Println("Couldnt remove file", fname)
		}
	}
}
Output:

push <nil>
pull <nil>
Example (ClientServerUsingPostgresDB)
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http/httptest"
	"os"
	"time"

	"github.com/dotchain/dot"
	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
	"github.com/dotchain/dot/ops/pg"
)

func main() {
	sourceName := "user=postgres dbname=dot_test sslmode=disable"
	maxPoll := pg.MaxPoll
	defer func() {
		pg.MaxPoll = maxPoll
		db, err := sql.Open("postgres", sourceName)
		must(err)
		_, err = db.Exec("DROP TABLE operations")
		must(err)
		must(db.Close())
	}()

	pg.MaxPoll = time.Second
	logger := log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile)
	srv := dot.WithLogger(dot.PostgresServer(sourceName), logger)
	defer dot.CloseServer(srv)
	httpSrv := httptest.NewServer(srv)
	defer httpSrv.Close()

	stream1, store1 := dot.NewSession().Stream(httpSrv.URL, logger)
	stream2, store2 := dot.NewSession().Stream(httpSrv.URL, logger)

	defer store1.Close()
	defer store2.Close()

	stream1.Append(changes.Replace{Before: changes.Nil, After: types.S8("hello")})
	fmt.Println("push", stream1.Push())
	fmt.Println("pull", stream2.Pull())

}

func must(err error) {
	if err != nil {
		panic(err)
	}
}
Output:

push <nil>
pull <nil>
Example (Convergence)
package main

import (
	"fmt"

	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types

	initial := types.S8("hello")

	// two changes: append " world" and delete "lo"
	insert := changes.Splice{Offset: 5, Before: types.S8(""), After: types.S8(" world")}
	remove := changes.Splice{Offset: 3, Before: types.S8("lo"), After: types.S8("")}

	// two versions derived from initial
	inserted := initial.Apply(nil, insert)
	removed := initial.Apply(nil, remove)

	// merge the changes
	removex, insertx := insert.Merge(remove)

	// converge by applying the above
	final1 := inserted.Apply(nil, removex)
	final2 := removed.Apply(nil, insertx)

	fmt.Println(final1, final1 == final2)
}
Output:

hel world true
Example (ConvergenceUsingStreams)
package main

import (
	"fmt"

	"github.com/dotchain/dot/streams"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/streams

	initial := streams.S8{Stream: streams.New(), Value: "hello"}

	// two changes: append " world" and delete "lo"
	s1 := initial.Splice(5, 0, " world")
	s2 := initial.Splice(3, len("lo"), "")

	// streams automatically merge because they are both
	// based on initial
	s1 = s1.Latest()
	s2 = s2.Latest()

	fmt.Println(s1.Value, s1.Value == s2.Value)
}
Output:

hel world true
Example (Folding)
package main

import (
	"fmt"

	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
	"github.com/dotchain/dot/streams"
	"github.com/dotchain/dot/x/fold"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/streams
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types
	// import github.com/dotchain/dot/x/fold

	// create master, folded child and the folding itself
	master := &streams.S16{Stream: streams.New(), Value: "hello world!"}
	foldChange := changes.Splice{
		Offset: len("hello"),
		Before: types.S16(" world"),
		After:  types.S16("..."),
	}
	foldedStream := fold.New(foldChange, master.Stream)
	folded := &streams.S16{Stream: foldedStream, Value: "hello...!"}

	// folded:  hello...! => Hello...!!!
	folded = folded.Splice(0, len("h"), "H")
	folded = folded.Splice(len("Hello...!"), 0, "!!")
	fmt.Println(folded.Value)

	// master: hello world => hullo world
	master = master.Splice(len("h"), len("e"), "u")
	fmt.Println(master.Value)

	// now folded = Hullo...!!!
	fmt.Println(folded.Latest().Value)

	// master = Hullo world!!!
	fmt.Println(master.Latest().Value)

}
Output:

Hello...!!!
hullo world!
Hullo...!!!
Hullo world!!!
Example (PathComposition)
package main

import (
	"fmt"

	"github.com/dotchain/dot/changes"
	"github.com/dotchain/dot/changes/types"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types

	// types.A is a generic array type and types.M is a map type
	initial := types.A{types.M{"hello": types.S8("world")}}

	// replace "world" with "world!"
	replace := changes.Replace{Before: types.S8("world"), After: types.S8("world!")}

	// replace "world" with "world!" of initial[0]["hello"]
	path := []interface{}{0, "hello"}
	c := changes.PathChange{Path: path, Change: replace}
	updated := initial.Apply(nil, c)
	fmt.Println(updated)

}
Output:

[map[hello:world!]]
Example (UndoStreams)
package main

import (
	"fmt"

	"github.com/dotchain/dot/streams"
	"github.com/dotchain/dot/streams/undo"
)

func main() {
	// import fmt
	// import github.com/dotchain/dot/streams
	// import github.com/dotchain/dot/changes
	// import github.com/dotchain/dot/changes/types
	// import github.com/dotchain/dot/streams/undo

	// create master, undoable child and the undo stack itself
	master := &streams.S16{Stream: streams.New(), Value: "hello"}
	s := undo.New(master.Stream)
	undoableChild := &streams.S16{Stream: s, Value: master.Value}

	// change hello => Hello
	undoableChild = undoableChild.Splice(0, len("h"), "H")
	fmt.Println(undoableChild.Value)

	// for kicks, update master hello => hello$ as if it came
	// from the server
	master.Splice(len("hello"), 0, "$")

	// now undo this via the stack
	s.Undo()

	// now undoableChild should be hello$
	undoableChild = undoableChild.Latest()
	fmt.Println(undoableChild.Value)

	// now redo the last operation to get Hello$
	s.Redo()
	undoableChild = undoableChild.Latest()
	fmt.Println(undoableChild.Value)

}
Output:

Hello
hello$
Hello$

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func BoltServer

func BoltServer(fileName string) http.Handler

BoltServer returns a http.Handler serving DOT requests backed by the db

func CloseServer

func CloseServer(h http.Handler)

CloseServer closes the http.Handler returned by this package

func PostgresServer

func PostgresServer(sourceName string) http.Handler

PostgresServer returns a http.Handler serving DOT requests backed by the db

func WithLogger

func WithLogger(h http.Handler, l log.Log) http.Handler

WithLogger updates the logger for server

Types

type Session

type Session struct {
	Version        int
	Pending, Merge []ops.Op

	OpCache    map[int]ops.Op
	MergeCache map[int][]ops.Op
}

Session represents a client session

func NewSession

func NewSession() *Session

NewSession creates an empty session

func (*Session) Load

func (s *Session) Load(ver int) (ops.Op, []ops.Op)

Load implements the ops.Cache load interface

func (*Session) NonBlockingStream

func (s *Session) NonBlockingStream(url string, logger dotlog.Log) (streams.Stream, ops.Store)

NonBlockingStream returns the stream of changes for this session

The returned store can be used to *close* the stream when needed

Actual syncing of messages happens when Push and Pull are called on the stream. Pull() does the server-fetch asynchronously, returning immediately if there is no server data available.

func (*Session) Store

func (s *Session) Store(ver int, op ops.Op, merge []ops.Op)

Store implements the ops.Cache store interface

func (*Session) Stream

func (s *Session) Stream(url string, logger dotlog.Log) (streams.Stream, ops.Store)

Stream returns the stream of changes for this session

The returned store can be used to *close* the stream when needed

Actual syncing of messages happens when Push and Pull are called on the stream

func (*Session) UpdateVersion

func (s *Session) UpdateVersion(version int, pending, merge []ops.Op)

UpdateVersion updates the version/pending info

Directories

Path Synopsis
Package changes implements the core mutation types for OT.
Package changes implements the core mutation types for OT.
crdt
Package crdt implements CRDT types and associated changes The main CRDT types are Dict and Seq which implement map-like and list-like container types.
Package crdt implements CRDT types and associated changes The main CRDT types are Dict and Seq which implement map-like and list-like container types.
diff
Package diff compares two values and returns the changes
Package diff compares two values and returns the changes
run
Package run implements a custom change that applies to a sequence of array elements.
Package run implements a custom change that applies to a sequence of array elements.
table
Package table implements a loose 2d collection of values
Package table implements a loose 2d collection of values
types
Package types implements OT-compatible immutable values.
Package types implements OT-compatible immutable values.
Generated.
Generated.
Package log defines the interface for loging within the DOT project.
Package log defines the interface for loging within the DOT project.
ops
Package ops implements network and storage for DOT This builds on top of the https://godoc.org/github.com/dotchain/dot/changes package
Package ops implements network and storage for DOT This builds on top of the https://godoc.org/github.com/dotchain/dot/changes package
bolt
Package bolt implements the dot storage for files using boltdb A http server can be implemented like so: import "github.com/dotchain/dot/ops/bolt" import "github.com/dotchain/dot/ops/nw" store, _ := bolt.New("file.bolt", "instance", nil) defer store.Close() handler := &nw.Handler{Store: store} h := func(w http.ResponseWriter, req *http.Request) { // Enable CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if req.Method == "OPTIONS" { return } handler.ServeHTTP(w, req) } http.HandleFunc("/api/", h) http.ListenAndServe() Concurrency A single store instance is safe for concurrent access but the provided file is locked until the store is closed.
Package bolt implements the dot storage for files using boltdb A http server can be implemented like so: import "github.com/dotchain/dot/ops/bolt" import "github.com/dotchain/dot/ops/nw" store, _ := bolt.New("file.bolt", "instance", nil) defer store.Close() handler := &nw.Handler{Store: store} h := func(w http.ResponseWriter, req *http.Request) { // Enable CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if req.Method == "OPTIONS" { return } handler.ServeHTTP(w, req) } http.HandleFunc("/api/", h) http.ListenAndServe() Concurrency A single store instance is safe for concurrent access but the provided file is locked until the store is closed.
nw
pg
Package pg implements the dot storage for postgres 9.5+ A http server can be implemented like so: import "github.com/dotchain/dot/ops/pg" import "github.com/dotchain/dot/ops/nw" dataSource := "dbname=mydb user=xyz" store, _ := sql.New(dataSource, "instance", nil) defer store.Close() handler := &nw.Handler{Store: store} h := func(w http.ResponseWriter, req *http.Request) { // Enable CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if req.Method == "OPTIONS" { return } handler.ServeHTTP(w, req) } http.HandleFunc("/api/", h) http.ListenAndServe()
Package pg implements the dot storage for postgres 9.5+ A http server can be implemented like so: import "github.com/dotchain/dot/ops/pg" import "github.com/dotchain/dot/ops/nw" dataSource := "dbname=mydb user=xyz" store, _ := sql.New(dataSource, "instance", nil) defer store.Close() handler := &nw.Handler{Store: store} h := func(w http.ResponseWriter, req *http.Request) { // Enable CORS w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if req.Method == "OPTIONS" { return } handler.ServeHTTP(w, req) } http.HandleFunc("/api/", h) http.ListenAndServe()
sjson
Package sjson implements a portable strongly-typed json-like codec.
Package sjson implements a portable strongly-typed json-like codec.
Package refs implements reference paths, carets and selections.
Package refs implements reference paths, carets and selections.
Package streams defines convergent streams of changes A stream is like an event emitter or source: it tracks a sequence of changes on a value.
Package streams defines convergent streams of changes A stream is like an event emitter or source: it tracks a sequence of changes on a value.
test
seqtest
Package seqtest implements a standard suite of validations.
Package seqtest implements a standard suite of validations.
x
cmd/dotls
Command dotls lists the operations The argument can be a file name or a url
Command dotls lists the operations The argument can be a file name or a url
dotc
Package dotc implements code-generation tools for dot.changes
Package dotc implements code-generation tools for dot.changes
fold
Package fold implements a simple scheme for folding.
Package fold implements a simple scheme for folding.
heap
Package heap implements a heap value type
Package heap implements a heap value type
rich
Package rich implements rich text data types Package rich implements rich text data types
Package rich implements rich text data types Package rich implements rich text data types
rich/data
Package data impleements data structures for use with rich text
Package data impleements data structures for use with rich text
rich/eval
Package eval implements evaluated objects Package eval implements evaluated objects Package eval implements expression values that can be evaluated Expression syntax The language used by eval is a very simple infix expression.
Package eval implements evaluated objects Package eval implements evaluated objects Package eval implements expression values that can be evaluated Expression syntax The language used by eval is a very simple infix expression.
rich/html
Package html implements rich text to HTML conversion
Package html implements rich text to HTML conversion
snapshot
Package snapshot manages session storage Snapshots are session meta data (version, pending), the actual app value and the transformed/merged ops cache.
Package snapshot manages session storage Snapshots are session meta data (version, pending), the actual app value and the transformed/merged ops cache.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL