LiangrunDa / AutomergeWithMove

Implementation for my paper "Extending JSON CRDT with Move Operations"
5 stars 0 forks source link

Real-world example #1

Open nkev opened 2 weeks ago

nkev commented 2 weeks ago

Thanks for sharing this!

I wanted to try a real-world example of this CRDT. For example, merging a remote user record with a local one.

I tried a new test file automerge_struct_test.go below but the two user structs seem to just swap their data. Could you please point me in the right direction?

package automergeproto

import (
    "testing"

    "github.com/LiangrunDa/AutomergeWithMove/internal/opset"
    "github.com/google/uuid"
    "github.com/stretchr/testify/assert"
)

type User struct {
    ID      uuid.UUID
    Name    string
    Score   int64
    Emails  []string //List test
    Address struct { //Map test
        Number   int64
        Street   string
        Postcode string
    }
}

func (u *User) ToCRDT() *Automerge {
    doc := NewAutomerge(u.ID)
    tx := doc.StartTransaction()
    tx.Put(ExRootOpId, "name", u.Name)
    tx.Put(ExRootOpId, "score", u.Score)

    //add email LIST
    emails, _ := tx.PutObject(ExRootOpId, "emails", opset.LIST)
    for _, e := range u.Emails {
        tx.Insert(emails, 0, e)
    }

    //add address MAP
    address, _ := tx.PutObject(ExRootOpId, "address", opset.MAP)
    tx.Put(address, "number", u.Address.Number)
    tx.Put(address, "street", u.Address.Street)
    tx.Put(address, "postcode", u.Address.Postcode)

    doc.CommitTransaction()
    return doc
}

func CreateTestUsers() (localUser, remoteUser User) {
    userID, _ := uuid.NewRandom() //we are merging the same user record between remote and local versions

    localUser = User{ //user record from local server
        ID:     userID,
        Name:   "Jon",
        Score:  100,
        Emails: []string{"lu1@test.com", "lu2@test.com"},
        Address: struct {
            Number   int64
            Street   string
            Postcode string
        }{
            Number:   11,
            Street:   "Local St",
            Postcode: "Local Postcode",
        },
    }
    remoteUser = User{ //same user record with mutations from a remote server
        ID:     userID,
        Name:   "Jonathan",
        Score:  120,
        Emails: []string{"lu1@test.com", "ru1@test.com", "ru2@test.com"},
        Address: struct {
            Number   int64
            Street   string
            Postcode string
        }{
            Number:   111,
            Street:   "Remote St",
            Postcode: "Remote Postcode",
        },
    }
    return
}

func TestStructMerge1(t *testing.T) {
    localUser, remoteUser := CreateTestUsers()
    cLocalUser := localUser.ToCRDT()
    cRemoteUser := remoteUser.ToCRDT()

    // merge localUser and remoteUser
    cLocalUser.Merge(cRemoteUser)
    cRemoteUser.Merge(cLocalUser)

    // ensure localUser and remoteUser are the same
    txLoc := cLocalUser.StartTransaction()
    txRem := cRemoteUser.StartTransaction()

    nameLoc, _ := txLoc.Get(ExRootOpId, "name")
    nameRem, _ := txRem.Get(ExRootOpId, "name")
    assert.Equal(t, nameLoc, nameRem)

    scoreLoc, _ := txLoc.Get(ExRootOpId, "score")
    scoreRem, _ := txRem.Get(ExRootOpId, "score")
    assert.Equal(t, scoreLoc, scoreRem)

    emailsLoc, _ := txLoc.Get(ExRootOpId, "emails")
    emailsLocId := emailsLoc.(ExOpId)
    em, _ := txLoc.Get(emailsLocId, 2)
    assert.Equal(t, "ru2@test.com", em)

    addressMapLoc, _ := txLoc.Get(ExRootOpId, "address")
    addressMapLocId := addressMapLoc.(ExOpId)
    num, _ := txLoc.Get(addressMapLocId, "number")
    assert.Equal(t, 111, num)

    // cLocalUser.CommitTransaction()
    // cRemoteUser.CommitTransaction()
}
LiangrunDa commented 5 days ago

Sorry for the late reply! Here is a modified version that works:

type User struct {
    ID      uuid.UUID
    Name    string
    Score   int64
    Emails  []string //List test
    Address struct { //Map test
        Number   int64
        Street   string
        Postcode string
    }
}

func (u *User) ToCRDT(doc *Automerge) *Automerge {
    tx := doc.StartTransaction()
    tx.Put(ExRootOpId, "name", u.Name)
    tx.Put(ExRootOpId, "score", u.Score)

    //add email LIST
    emails, _ := tx.PutObject(ExRootOpId, "emails", opset.LIST)
    for _, e := range u.Emails {
        tx.Insert(emails, 0, e)
    }

    //add address MAP
    address, _ := tx.PutObject(ExRootOpId, "address", opset.MAP)
    tx.Put(address, "number", u.Address.Number)
    tx.Put(address, "street", u.Address.Street)
    tx.Put(address, "postcode", u.Address.Postcode)

    doc.CommitTransaction()
    return doc
}

func CreateTestUsers() (localUser, remoteUser User) {
    userID, _ := uuid.NewRandom() //we are merging the same user record between remote and local versions

    localUser = User{ //user record from local server
        ID:     userID,
        Name:   "Jon",
        Score:  100,
        Emails: []string{"lu1@test.com", "lu2@test.com"},
        Address: struct {
            Number   int64
            Street   string
            Postcode string
        }{
            Number:   11,
            Street:   "Local St",
            Postcode: "Local Postcode",
        },
    }
    remoteUser = User{ //same user record with mutations from a remote server
        ID:     userID,
        Name:   "Jonathan",
        Score:  120,
        Emails: []string{"lu1@test.com", "ru1@test.com", "ru2@test.com"},
        Address: struct {
            Number   int64
            Street   string
            Postcode string
        }{
            Number:   111,
            Street:   "Remote St",
            Postcode: "Remote Postcode",
        },
    }
    return
}

func TestStructMerge1(t *testing.T) {
    doc1 := NewAutomerge(uuid.New())
    doc2 := doc1.Fork()
    localUser, remoteUser := CreateTestUsers()
    cLocalUser := localUser.ToCRDT(doc1)
    cRemoteUser := remoteUser.ToCRDT(doc2)

    // merge localUser and remoteUser
    cLocalUser.Merge(cRemoteUser)
    cRemoteUser.Merge(cLocalUser)

    // ensure localUser and remoteUser are the same
    txLoc := cLocalUser.StartTransaction()
    txRem := cRemoteUser.StartTransaction()

    nameLoc, _ := txLoc.Get(ExRootOpId, "name")
    nameRem, _ := txRem.Get(ExRootOpId, "name")
    assert.Equal(t, nameLoc, nameRem)

    scoreLoc, _ := txLoc.Get(ExRootOpId, "score")
    scoreRem, _ := txRem.Get(ExRootOpId, "score")
    assert.Equal(t, scoreLoc, scoreRem)

    emailsLoc, _ := txLoc.Get(ExRootOpId, "emails")
    emailsLocId := emailsLoc.(ExOpId)
    emailsRem, _ := txRem.Get(ExRootOpId, "emails")
    emailsRemId := emailsRem.(ExOpId)

    for i := 0; i < 2; i++ {
        emLoc, _ := txLoc.Get(emailsLocId, i)
        emRem, _ := txRem.Get(emailsRemId, i)
        assert.Equal(t, emLoc, emRem)
    }

    addressMapLoc, _ := txLoc.Get(ExRootOpId, "address")
    addressMapLocId := addressMapLoc.(ExOpId)
    num, _ := txLoc.Get(addressMapLocId, "number")
    addressMapRem, _ := txRem.Get(ExRootOpId, "address")
    addressMapRemId := addressMapRem.(ExOpId)
    numRem, _ := txRem.Get(addressMapRemId, "number")
    assert.Equal(t, num, numRem)

    // cLocalUser.CommitTransaction()
    // cRemoteUser.CommitTransaction()
}

However, I noticed that you didn't use Move operation in your example. If you don't need a move operation at all, I would suggest using the real Automerge library instead: https://github.com/automerge/automerge. AutomergeWIthMove is an experimental codebase that tries to enhance the real Automerge library with move operations.