Skip to content

Commit

Permalink
Fixes remote operation Apply
Browse files Browse the repository at this point in the history
  • Loading branch information
gpestana committed Jan 23, 2021
1 parent 0c23c16 commit a150709
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 13 deletions.
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# rdoc

[![Build Status](https://travis-ci.org/gpestana/rdoc.svg?branch=master)](https://travis-ci.org/gpestana/rdoc) [![Go Reference](https://pkg.go.dev/badge/github.com/gpestana/rdoc.svg)](https://pkg.go.dev/github.com/gpestana/rdoc) [![Package Version](https://img.shields.io/github/v/tag/gpestana/rdoc)](https://img.shields.io/github/v/tag/gpestana/rdoc)
### Build better decentralized and offline-first application in Go

**rdoc - Build better decentralized and offline-first application in Go**
[![Build Status](https://travis-ci.org/gpestana/rdoc.svg?branch=master)](https://travis-ci.org/gpestana/rdoc) [![Package Version](https://img.shields.io/github/v/tag/gpestana/rdoc)](https://img.shields.io/github/v/tag/gpestana/rdoc)

[![Go Reference](https://pkg.go.dev/badge/github.com/gpestana/rdoc.svg)](https://pkg.go.dev/github.com/gpestana/rdoc)

rdoc is a native go implementation of a conflict-free replicated JSON data structure, as introduced by Martin Kleppmann and Alastair R. Beresford in their seminal work [1]. A JSON CRDT is "[...] an algorithm and formal semantics for a JSON data structure that automatically resolves concurrent modifications such that no updates are lost, and such that all replicas converge towards the same state (a conflict-free replicated datatype or CRDT)." [1];

Expand Down Expand Up @@ -41,6 +43,15 @@ if err != nil {
panic(err)
}

// Get Doc operations to sent over the wire to merge into other replicas
doc1Operations := doc.Operations()

// ... apply state from doc1 into doc2, in order for replicas to converge

doc2.Apply(doc1Operations)

// ...

// Native Go marshaling/unmarshaling supported
buffer, err := json.Marshal(*doc)
if err != nil {
Expand Down
50 changes: 42 additions & 8 deletions rdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,12 @@ func (doc *Doc) applyOperation(operation Operation) {
}

func (doc *Doc) tryBufferedOperations() {
buffer, err := doc.MarshalFullJSON()
bufferedOperations, err := doc.marshalBufferedOperations()
if err != nil {
panic(fmt.Sprintf("Buffered operations are not valid -- this should never happen: %v\n", err))
panic(fmt.Sprintf("Error marshalling buffered operations -- this should never happen: %v\n", err))
}

err = doc.Apply(buffer)
err = doc.Apply(bufferedOperations)
if err != nil {
panic(fmt.Sprintf("Error applying buffered operations -- this should never happen: %v\n", err))
}
Expand Down Expand Up @@ -154,17 +154,51 @@ func (doc Doc) MarshalJSON() ([]byte, error) {
// MarshalFullJSON marshals a Doc into a buffer, including the dependencies
// field of each operation.
func (doc Doc) MarshalFullJSON() ([]byte, error) {
type operationNoDeps struct {
type operation struct {
ID string `json:"id"`
Op string `json:"op"`
Path string `json:"path"`
Deps []string `json:"deps"`
Value interface{} `json:"value"`
}

buffer := []operationNoDeps{}
buffer := []operation{}

for _, operation := range doc.operations {
for _, op := range doc.operations {
path, err := op.raw.Path()
if err != nil {
return nil, err
}
value, err := op.raw.ValueInterface()
if err != nil {
return nil, err
}

structOp := operation{
ID: op.id,
Deps: op.deps,
Op: op.raw.Kind(),
Path: path,
Value: value,
}

buffer = append(buffer, structOp)
}
return json.Marshal(buffer)
}

func (doc Doc) marshalBufferedOperations() ([]byte, error) {
type bufferedOperation struct {
ID string `json:"id"`
Op string `json:"op"`
Path string `json:"path"`
Deps []string `json:"deps"`
Value interface{} `json:"value"`
}

buffer := []bufferedOperation{}

for _, operation := range doc.bufferedOperations {
path, err := operation.raw.Path()
if err != nil {
return nil, err
Expand All @@ -174,15 +208,15 @@ func (doc Doc) MarshalFullJSON() ([]byte, error) {
return nil, err
}

opNoDeps := operationNoDeps{
op := bufferedOperation{
ID: operation.id,
Deps: operation.deps,
Op: operation.raw.Kind(),
Path: path,
Value: value,
}

buffer = append(buffer, opNoDeps)
buffer = append(buffer, op)
}
return json.Marshal(buffer)
}
Expand Down
26 changes: 23 additions & 3 deletions rdoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import (
// 1. end-to-end tests mapping to the examples of the JSON CRDT paper

// 1.A: different value assignment of a register on multiple replicas
func _Test_E2E_One(t *testing.T) {
func Test_E2E_One(t *testing.T) {
expectedFinalDoc1 := `[{"id":"2.64487784","op":"add","path":"/key","value":"A"},{"id":"3.64487784","op":"add","path":"/key","value":"B"},{"id":"2.64553321","op":"add","path":"/key","value":"A"},{"id":"3.64553321","op":"add","path":"/key","value":"C"}]`
expectedFinalDoc2 := `[{"id":"2.64553321","op":"add","path":"/key","value":"A"},{"id":"3.64553321","op":"add","path":"/key","value":"C"},{"id":"2.64487784","op":"add","path":"/key","value":"A"},{"id":"3.64487784","op":"add","path":"/key","value":"B"}]`

doc1 := rdoc.Init("doc1")
doc2 := rdoc.Init("doc2")

Expand Down Expand Up @@ -56,13 +59,30 @@ func _Test_E2E_One(t *testing.T) {
// merge doc2 state into doc1
err = doc1.Apply(doc2PatchToApplyRemotely)
if err != nil {
t.Error(t)
t.Error(err)
}

// merge doc1 state into doc2
err = doc2.Apply(doc1PatchToApplyRemotely)
if err != nil {
t.Error(t)
t.Error(err)
}

// check results
bufferDoc1, err := json.Marshal(*doc1)
if err != nil {
t.Error(err)
}
if string(bufferDoc1) != expectedFinalDoc1 {
t.Error("Doc1 not correct after Apply")
}

bufferDoc2, err := json.Marshal(*doc2)
if err != nil {
t.Error(err)
}
if string(bufferDoc2) != expectedFinalDoc2 {
t.Error("Doc2 not correct after Apply")
}
}

Expand Down

0 comments on commit a150709

Please sign in to comment.