diff --git a/acltree/acltree.go b/acltree/acltree.go index 59268b34..276a4054 100644 --- a/acltree/acltree.go +++ b/acltree/acltree.go @@ -23,19 +23,26 @@ type AddResult struct { Summary AddResultSummary } +type TreeUpdateListener interface { + Update(tree ACLTree) + Rebuild(tree ACLTree) +} + type ACLTree interface { ACLState() *ACLState AddContent(f func(builder ChangeBuilder)) (*Change, error) AddChanges(changes ...*Change) (AddResult, error) Heads() []string + Root() *Change Iterate(func(change *Change) bool) IterateFrom(string, func(change *Change) bool) HasChange(string) bool } type aclTree struct { - thread thread.Thread - accountData *account.AccountData + thread thread.Thread + accountData *account.AccountData + updateListener TreeUpdateListener fullTree *Tree aclTreeFromStart *Tree // TODO: right now we don't use it, we can probably have only local var for now. This tree is built from start of the document @@ -50,7 +57,10 @@ type aclTree struct { sync.Mutex } -func BuildACLTree(t thread.Thread, acc *account.AccountData) (ACLTree, error) { +func BuildACLTree( + t thread.Thread, + acc *account.AccountData, + listener TreeUpdateListener) (ACLTree, error) { decoder := keys.NewEd25519Decoder() aclTreeBuilder := newACLTreeBuilder(t, decoder) treeBuilder := newTreeBuilder(t, decoder) @@ -68,6 +78,7 @@ func BuildACLTree(t thread.Thread, acc *account.AccountData) (ACLTree, error) { aclStateBuilder: aclStateBuilder, snapshotValidator: snapshotValidator, changeBuilder: changeBuilder, + updateListener: listener, } err := aclTree.rebuildFromThread(false) if err != nil { @@ -183,6 +194,7 @@ func (a *aclTree) AddContent(build func(builder ChangeBuilder)) (*Change, error) } a.thread.SetHeads([]string{ch.Id}) + a.updateListener.Update(a) return ch, nil } @@ -190,8 +202,8 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) { a.Lock() defer a.Unlock() // TODO: make proper error handling, because there are a lot of corner cases where this will break - var aclChanges []*Change var err error + var mode Mode defer func() { if err != nil { @@ -209,13 +221,17 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) { } } a.thread.RemoveOrphans(toRemove...) + switch mode { + case Append: + a.updateListener.Update(a) + case Rebuild: + a.updateListener.Rebuild(a) + default: + break + } }() for _, ch := range changes { - if ch.IsACLChange() { - aclChanges = append(aclChanges, ch) - break - } err = a.thread.AddChange(ch) if err != nil { return AddResult{}, err @@ -224,7 +240,7 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) { } prevHeads := a.fullTree.Heads() - mode := a.fullTree.Add(changes...) + mode = a.fullTree.Add(changes...) switch mode { case Nothing: return AddResult{ @@ -284,3 +300,9 @@ func (a *aclTree) Heads() []string { defer a.Unlock() return a.fullTree.Heads() } + +func (a *aclTree) Root() *Change { + a.Lock() + defer a.Unlock() + return a.fullTree.Root() +} diff --git a/exampledocument/docstate.go b/exampledocument/docstate.go deleted file mode 100644 index 95bc41b6..00000000 --- a/exampledocument/docstate.go +++ /dev/null @@ -1,9 +0,0 @@ -package exampledocument - -type DocumentState interface { - ApplyChange(change []byte, id string) (DocumentState, error) -} - -type InitialStateProvider interface { - ProvideFromInitialChange(change []byte, id string) (DocumentState, error) -} diff --git a/exampledocument/document.go b/exampledocument/document.go deleted file mode 100644 index d196b111..00000000 --- a/exampledocument/document.go +++ /dev/null @@ -1,303 +0,0 @@ -package exampledocument - -import ( - "fmt" - "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" - "github.com/anytypeio/go-anytype-infrastructure-experiments/data/pb" - "github.com/anytypeio/go-anytype-infrastructure-experiments/thread" - "github.com/anytypeio/go-anytype-infrastructure-experiments/util/slice" - "github.com/gogo/protobuf/proto" -) - -type AccountData struct { - Identity string - SignKey threadmodels.SigningPrivKey - EncKey threadmodels.EncryptionPrivKey -} - -type Document struct { - // TODO: ensure that every operation on Document is synchronized - thread threadmodels.Thread - stateProvider InitialStateProvider - accountData *AccountData - decoder threadmodels.SigningPubKeyDecoder - - treeBuilder *acltree.treeBuilder - aclTreeBuilder *acltree.aclTreeBuilder - aclStateBuilder *acltree.aclStateBuilder - snapshotValidator *acltree.snapshotValidator - docStateBuilder *acltree.documentStateBuilder - - docContext *acltree.documentContext -} - -type UpdateResult int - -const ( - UpdateResultNoAction UpdateResult = iota - UpdateResultAppend - UpdateResultRebuild -) - -type CreateChangePayload struct { - ChangesData proto.Marshaler - ACLData *pb.ACLChangeACLData - Id string // TODO: this is just for testing, because id should be created automatically from content -} - -func NewDocument( - thread threadmodels.Thread, - stateProvider InitialStateProvider, - accountData *AccountData) *Document { - - decoder := threadmodels.NewEd25519Decoder() - return &Document{ - thread: thread, - stateProvider: stateProvider, - accountData: accountData, - decoder: decoder, - aclTreeBuilder: acltree.newACLTreeBuilder(thread, decoder), - treeBuilder: acltree.newTreeBuilder(thread, decoder), - snapshotValidator: acltree.newSnapshotValidator(decoder, accountData), - aclStateBuilder: acltree.newACLStateBuilder(decoder, accountData), - docStateBuilder: acltree.newDocumentStateBuilder(stateProvider), - docContext: &acltree.documentContext{}, - } -} - -//sync layer -> Object cache -> document -> Update (..raw changes) -//client layer -> Object cache -> document -> CreateChange(...) -// - -// smartblock -> CreateChange(payload) -// SmartTree iterate etc -func (d *Document) CreateChange(payload *CreateChangePayload) error { - // TODO: add snapshot creation logic - marshalled, err := payload.ChangesData.Marshal() - if err != nil { - return err - } - - encrypted, err := d.docContext.aclState.userReadKeys[d.docContext.aclState.currentReadKeyHash]. - Encrypt(marshalled) - if err != nil { - return err - } - - aclChange := &pb.ACLChange{ - TreeHeadIds: d.docContext.fullTree.Heads(), - AclHeadIds: d.getACLHeads(), - SnapshotBaseId: d.docContext.fullTree.RootId(), - AclData: payload.ACLData, - ChangesData: encrypted, - CurrentReadKeyHash: d.docContext.aclState.currentReadKeyHash, - Timestamp: 0, - Identity: d.accountData.Identity, - } - - // TODO: add CID creation logic based on content - ch := acltree.NewChange(payload.Id, aclChange) - ch.DecryptedDocumentChange = marshalled - - fullMarshalledChange, err := proto.Marshal(aclChange) - if err != nil { - return err - } - signature, err := d.accountData.SignKey.Sign(fullMarshalledChange) - if err != nil { - return err - } - - if aclChange.AclData != nil { - // we can apply change right away without going through builder, because - err = d.docContext.aclState.ApplyChange(payload.Id, aclChange) - if err != nil { - return err - } - } - d.docContext.fullTree.AddFast(ch) - - err = d.thread.AddChange(&thread.RawChange{ - Payload: marshalled, - Signature: signature, - Id: payload.Id, - }) - if err != nil { - return err - } - - d.thread.SetHeads([]string{ch.Id}) - d.thread.SetMaybeHeads([]string{ch.Id}) - - return nil -} - -func (d *Document) Update(changes ...*thread.RawChange) (DocumentState, UpdateResult, error) { - var treeChanges []*acltree.Change - - var foundACLChange bool - for _, ch := range changes { - aclChange, err := d.treeBuilder.makeVerifiedACLChange(ch) - if err != nil { - return nil, UpdateResultNoAction, fmt.Errorf("change with id %s is incorrect: %w", ch.Id, err) - } - - treeChange := d.treeBuilder.changeCreator(ch.Id, aclChange) - treeChanges = append(treeChanges, treeChange) - - // this already sets PossibleHeads to include new changes - // TODO: change this behaviour as non-obvious, because it is not evident from the interface - err = d.thread.AddChange(ch) - if err != nil { - return nil, UpdateResultNoAction, fmt.Errorf("change with id %s cannot be added: %w", ch.Id, err) - } - if treeChange.IsACLChange() { - foundACLChange = true - } - } - - if foundACLChange { - res, err := d.Build() - return res, UpdateResultRebuild, err - } - - prevHeads := d.docContext.fullTree.Heads() - mode := d.docContext.fullTree.Add(treeChanges...) - switch mode { - case acltree.Nothing: - return d.docContext.docState, UpdateResultNoAction, nil - case acltree.Rebuild: - res, err := d.Build() - return res, UpdateResultRebuild, err - default: - break - } - - // TODO: we should still check if the user making those changes are able to write using "aclState" - // decrypting everything, because we have no new keys - for _, ch := range treeChanges { - if ch.Content.GetChangesData() != nil { - key, exists := d.docContext.aclState.userReadKeys[ch.Content.CurrentReadKeyHash] - if !exists { - err := fmt.Errorf("failed to find key with hash: %d", ch.Content.CurrentReadKeyHash) - return nil, UpdateResultNoAction, err - } - - err := ch.DecryptContents(key) - if err != nil { - err = fmt.Errorf("failed to decrypt contents for hash: %d", ch.Content.CurrentReadKeyHash) - return nil, UpdateResultNoAction, err - } - } - } - - // because for every new change we know it was after any of the previous heads - // each of previous heads must have same "Next" nodes - // so it doesn't matter which one we choose - // so we choose first one - newState, err := d.docStateBuilder.appendFrom(prevHeads[0], d.docContext.docState) - if err != nil { - res, _ := d.Build() - return res, UpdateResultRebuild, fmt.Errorf("could not add changes to state, rebuilded") - } - - // setting all heads - d.thread.SetHeads(d.docContext.fullTree.Heads()) - // this should be the entrypoint when we build the document - d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) - - return newState, UpdateResultAppend, nil -} - -func (d *Document) Build() (DocumentState, error) { - return d.build(false) -} - -// TODO: this should not be the responsibility of Document, move it somewhere else after testing -func (d *Document) getACLHeads() []string { - var aclTreeHeads []string - for _, head := range d.docContext.fullTree.Heads() { - if slice.FindPos(aclTreeHeads, head) != -1 { // do not scan known heads - continue - } - precedingHeads := d.getPrecedingACLHeads(head) - - for _, aclHead := range precedingHeads { - if slice.FindPos(aclTreeHeads, aclHead) != -1 { - continue - } - aclTreeHeads = append(aclTreeHeads, aclHead) - } - } - return aclTreeHeads -} - -func (d *Document) getPrecedingACLHeads(head string) []string { - headChange := d.docContext.fullTree.attached[head] - - if headChange.Content.GetAclData() != nil { - return []string{head} - } else { - return headChange.Content.AclHeadIds - } -} - -func (d *Document) build(fromStart bool) (DocumentState, error) { - d.treeBuilder.init() - d.aclTreeBuilder.init() - - var err error - d.docContext.fullTree, err = d.treeBuilder.build(fromStart) - if err != nil { - return nil, err - } - - // TODO: remove this from context as this is used only to validate snapshot - d.docContext.aclTree, err = d.aclTreeBuilder.build() - if err != nil { - return nil, err - } - - if !fromStart { - err = d.snapshotValidator.init(d.docContext.aclTree) - if err != nil { - return nil, err - } - - valid, err := d.snapshotValidator.validateSnapshot(d.docContext.fullTree.root) - if err != nil { - return nil, err - } - if !valid { - return d.build(true) - } - } - err = d.aclStateBuilder.init(d.docContext.fullTree) - if err != nil { - return nil, err - } - - d.docContext.aclState, err = d.aclStateBuilder.build() - if err != nil { - return nil, err - } - - // tree should be exposed - - d.docStateBuilder.init(d.docContext.aclState, d.docContext.fullTree) - d.docContext.docState, err = d.docStateBuilder.build() - if err != nil { - return nil, err - } - - // setting all heads - d.thread.SetHeads(d.docContext.fullTree.Heads()) - // this should be the entrypoint when we build the document - d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) - - return d.docContext.docState, nil -} - -func (d *Document) State() DocumentState { - return nil -} diff --git a/exampledocument/plaintextdocstate.go b/exampledocument/plaintextdocstate.go deleted file mode 100644 index e5296e42..00000000 --- a/exampledocument/plaintextdocstate.go +++ /dev/null @@ -1,68 +0,0 @@ -package exampledocument - -import ( - "fmt" - "github.com/anytypeio/go-anytype-infrastructure-experiments/data/pb" - "github.com/gogo/protobuf/proto" -) - -// TestDocumentState -> testutils -// ThreadBuilder -> testutils -// move protos to test utils - -type PlainTextDocumentState struct { - LastChangeId string - Text string -} - -func NewPlainTextDocumentState(text string, id string) *PlainTextDocumentState { - return &PlainTextDocumentState{ - LastChangeId: id, - Text: text, - } -} - -func (p *PlainTextDocumentState) ApplyChange(change []byte, id string) (DocumentState, error) { - var changesData pb.PlainTextChangeData - err := proto.Unmarshal(change, &changesData) - if err != nil { - return nil, err - } - - for _, content := range changesData.GetContent() { - err = p.applyChange(content) - if err != nil { - return nil, err - } - } - p.LastChangeId = id - return p, nil -} - -func (p *PlainTextDocumentState) applyChange(ch *pb.PlainTextChangeContent) error { - switch { - case ch.GetTextAppend() != nil: - text := ch.GetTextAppend().GetText() - p.Text += "|" + text - } - return nil -} - -type PlainTextDocumentStateProvider struct{} - -func NewPlainTextDocumentStateProvider() *PlainTextDocumentStateProvider { - return &PlainTextDocumentStateProvider{} -} - -func (p *PlainTextDocumentStateProvider) ProvideFromInitialChange(change []byte, id string) (DocumentState, error) { - var changesData pb.PlainTextChangeData - err := proto.Unmarshal(change, &changesData) - if err != nil { - return nil, err - } - - if changesData.GetSnapshot() == nil { - return nil, fmt.Errorf("could not create state from empty snapshot") - } - return NewPlainTextDocumentState(changesData.GetSnapshot().GetText(), id), nil -} diff --git a/plaintextdocument/document.go b/plaintextdocument/document.go new file mode 100644 index 00000000..3908cb29 --- /dev/null +++ b/plaintextdocument/document.go @@ -0,0 +1,378 @@ +package plaintextdocument + +import ( + "fmt" + "github.com/anytypeio/go-anytype-infrastructure-experiments/account" + "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" + "github.com/anytypeio/go-anytype-infrastructure-experiments/thread" +) + +type PlainTextDocument struct { + heads []string + aclTree acltree.ACLTree + state *DocumentState +} + +func (p *PlainTextDocument) Update(tree acltree.ACLTree) { + var err error + defer func() { + if err != nil { + fmt.Println("rebuild has returned error:", err) + } + }() + + prevHeads := p.heads + p.heads = tree.Heads() + startId := prevHeads[0] + tree.IterateFrom(startId, func(change *acltree.Change) (isContinue bool) { + if change.Id == startId { + return true + } + if change.DecryptedDocumentChange != nil { + p.state, err = p.state.ApplyChange(change.DecryptedDocumentChange, change.Id) + if err != nil { + return false + } + } + return true + }) +} + +func (p *PlainTextDocument) Rebuild(tree acltree.ACLTree) { + p.heads = tree.Heads() + var startId string + var err error + defer func() { + if err != nil { + fmt.Println("rebuild has returned error:", err) + } + }() + + rootChange := tree.Root() + + if rootChange.DecryptedDocumentChange == nil { + err = fmt.Errorf("root doesn't have decrypted change") + return + } + + state, err := BuildDocumentStateFromChange(rootChange.DecryptedDocumentChange, rootChange.Id) + if err != nil { + return + } + + startId = rootChange.Id + tree.Iterate(func(change *acltree.Change) (isContinue bool) { + if startId == change.Id { + return true + } + if change.DecryptedDocumentChange != nil { + state, err = state.ApplyChange(change.DecryptedDocumentChange, change.Id) + if err != nil { + return false + } + } + return true + }) + if err != nil { + return + } + p.state = state +} + +func NewPlainTextDocument(t thread.Thread, acc *account.AccountData) (*PlainTextDocument, error) { + tree, e +} + +// +//type AccountData struct { +// Identity string +// SignKey threadmodels.SigningPrivKey +// EncKey threadmodels.EncryptionPrivKey +//} +// +//type Document struct { +// // TODO: ensure that every operation on Document is synchronized +// thread threadmodels.Thread +// stateProvider InitialStateProvider +// accountData *AccountData +// decoder threadmodels.SigningPubKeyDecoder +// +// treeBuilder *acltree.treeBuilder +// aclTreeBuilder *acltree.aclTreeBuilder +// aclStateBuilder *acltree.aclStateBuilder +// snapshotValidator *acltree.snapshotValidator +// docStateBuilder *acltree.documentStateBuilder +// +// docContext *acltree.documentContext +//} +// +//type UpdateResult int +// +//const ( +// UpdateResultNoAction UpdateResult = iota +// UpdateResultAppend +// UpdateResultRebuild +//) +// +//type CreateChangePayload struct { +// ChangesData proto.Marshaler +// ACLData *pb.ACLChangeACLData +// Id string // TODO: this is just for testing, because id should be created automatically from content +//} +// +//func NewDocument( +// thread threadmodels.Thread, +// stateProvider InitialStateProvider, +// accountData *AccountData) *Document { +// +// decoder := threadmodels.NewEd25519Decoder() +// return &Document{ +// thread: thread, +// stateProvider: stateProvider, +// accountData: accountData, +// decoder: decoder, +// aclTreeBuilder: acltree.newACLTreeBuilder(thread, decoder), +// treeBuilder: acltree.newTreeBuilder(thread, decoder), +// snapshotValidator: acltree.newSnapshotValidator(decoder, accountData), +// aclStateBuilder: acltree.newACLStateBuilder(decoder, accountData), +// docStateBuilder: acltree.newDocumentStateBuilder(stateProvider), +// docContext: &acltree.documentContext{}, +// } +//} +// +////sync layer -> Object cache -> document -> Update (..raw changes) +////client layer -> Object cache -> document -> CreateChange(...) +//// +// +//// smartblock -> CreateChange(payload) +//// SmartTree iterate etc +//func (d *Document) CreateChange(payload *CreateChangePayload) error { +// // TODO: add snapshot creation logic +// marshalled, err := payload.ChangesData.Marshal() +// if err != nil { +// return err +// } +// +// encrypted, err := d.docContext.aclState.userReadKeys[d.docContext.aclState.currentReadKeyHash]. +// Encrypt(marshalled) +// if err != nil { +// return err +// } +// +// aclChange := &pb.ACLChange{ +// TreeHeadIds: d.docContext.fullTree.Heads(), +// AclHeadIds: d.getACLHeads(), +// SnapshotBaseId: d.docContext.fullTree.RootId(), +// AclData: payload.ACLData, +// ChangesData: encrypted, +// CurrentReadKeyHash: d.docContext.aclState.currentReadKeyHash, +// Timestamp: 0, +// Identity: d.accountData.Identity, +// } +// +// // TODO: add CID creation logic based on content +// ch := acltree.NewChange(payload.Id, aclChange) +// ch.DecryptedDocumentChange = marshalled +// +// fullMarshalledChange, err := proto.Marshal(aclChange) +// if err != nil { +// return err +// } +// signature, err := d.accountData.SignKey.Sign(fullMarshalledChange) +// if err != nil { +// return err +// } +// +// if aclChange.AclData != nil { +// // we can apply change right away without going through builder, because +// err = d.docContext.aclState.ApplyChange(payload.Id, aclChange) +// if err != nil { +// return err +// } +// } +// d.docContext.fullTree.AddFast(ch) +// +// err = d.thread.AddChange(&thread.RawChange{ +// Payload: marshalled, +// Signature: signature, +// Id: payload.Id, +// }) +// if err != nil { +// return err +// } +// +// d.thread.SetHeads([]string{ch.Id}) +// d.thread.SetMaybeHeads([]string{ch.Id}) +// +// return nil +//} +// +//func (d *Document) Update(changes ...*thread.RawChange) (DocumentState, UpdateResult, error) { +// var treeChanges []*acltree.Change +// +// var foundACLChange bool +// for _, ch := range changes { +// aclChange, err := d.treeBuilder.makeVerifiedACLChange(ch) +// if err != nil { +// return nil, UpdateResultNoAction, fmt.Errorf("change with id %s is incorrect: %w", ch.Id, err) +// } +// +// treeChange := d.treeBuilder.changeCreator(ch.Id, aclChange) +// treeChanges = append(treeChanges, treeChange) +// +// // this already sets PossibleHeads to include new changes +// // TODO: change this behaviour as non-obvious, because it is not evident from the interface +// err = d.thread.AddChange(ch) +// if err != nil { +// return nil, UpdateResultNoAction, fmt.Errorf("change with id %s cannot be added: %w", ch.Id, err) +// } +// if treeChange.IsACLChange() { +// foundACLChange = true +// } +// } +// +// if foundACLChange { +// res, err := d.Build() +// return res, UpdateResultRebuild, err +// } +// +// prevHeads := d.docContext.fullTree.Heads() +// mode := d.docContext.fullTree.Add(treeChanges...) +// switch mode { +// case acltree.Nothing: +// return d.docContext.docState, UpdateResultNoAction, nil +// case acltree.Rebuild: +// res, err := d.Build() +// return res, UpdateResultRebuild, err +// default: +// break +// } +// +// // TODO: we should still check if the user making those changes are able to write using "aclState" +// // decrypting everything, because we have no new keys +// for _, ch := range treeChanges { +// if ch.Content.GetChangesData() != nil { +// key, exists := d.docContext.aclState.userReadKeys[ch.Content.CurrentReadKeyHash] +// if !exists { +// err := fmt.Errorf("failed to find key with hash: %d", ch.Content.CurrentReadKeyHash) +// return nil, UpdateResultNoAction, err +// } +// +// err := ch.DecryptContents(key) +// if err != nil { +// err = fmt.Errorf("failed to decrypt contents for hash: %d", ch.Content.CurrentReadKeyHash) +// return nil, UpdateResultNoAction, err +// } +// } +// } +// +// // because for every new change we know it was after any of the previous heads +// // each of previous heads must have same "Next" nodes +// // so it doesn't matter which one we choose +// // so we choose first one +// newState, err := d.docStateBuilder.appendFrom(prevHeads[0], d.docContext.docState) +// if err != nil { +// res, _ := d.Build() +// return res, UpdateResultRebuild, fmt.Errorf("could not add changes to state, rebuilded") +// } +// +// // setting all heads +// d.thread.SetHeads(d.docContext.fullTree.Heads()) +// // this should be the entrypoint when we build the document +// d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) +// +// return newState, UpdateResultAppend, nil +//} +// +//func (d *Document) Build() (DocumentState, error) { +// return d.build(false) +//} +// +//// TODO: this should not be the responsibility of Document, move it somewhere else after testing +//func (d *Document) getACLHeads() []string { +// var aclTreeHeads []string +// for _, head := range d.docContext.fullTree.Heads() { +// if slice.FindPos(aclTreeHeads, head) != -1 { // do not scan known heads +// continue +// } +// precedingHeads := d.getPrecedingACLHeads(head) +// +// for _, aclHead := range precedingHeads { +// if slice.FindPos(aclTreeHeads, aclHead) != -1 { +// continue +// } +// aclTreeHeads = append(aclTreeHeads, aclHead) +// } +// } +// return aclTreeHeads +//} +// +//func (d *Document) getPrecedingACLHeads(head string) []string { +// headChange := d.docContext.fullTree.attached[head] +// +// if headChange.Content.GetAclData() != nil { +// return []string{head} +// } else { +// return headChange.Content.AclHeadIds +// } +//} +// +//func (d *Document) build(fromStart bool) (DocumentState, error) { +// d.treeBuilder.init() +// d.aclTreeBuilder.init() +// +// var err error +// d.docContext.fullTree, err = d.treeBuilder.build(fromStart) +// if err != nil { +// return nil, err +// } +// +// // TODO: remove this from context as this is used only to validate snapshot +// d.docContext.aclTree, err = d.aclTreeBuilder.build() +// if err != nil { +// return nil, err +// } +// +// if !fromStart { +// err = d.snapshotValidator.init(d.docContext.aclTree) +// if err != nil { +// return nil, err +// } +// +// valid, err := d.snapshotValidator.validateSnapshot(d.docContext.fullTree.root) +// if err != nil { +// return nil, err +// } +// if !valid { +// return d.build(true) +// } +// } +// err = d.aclStateBuilder.init(d.docContext.fullTree) +// if err != nil { +// return nil, err +// } +// +// d.docContext.aclState, err = d.aclStateBuilder.build() +// if err != nil { +// return nil, err +// } +// +// // tree should be exposed +// +// d.docStateBuilder.init(d.docContext.aclState, d.docContext.fullTree) +// d.docContext.docState, err = d.docStateBuilder.build() +// if err != nil { +// return nil, err +// } +// +// // setting all heads +// d.thread.SetHeads(d.docContext.fullTree.Heads()) +// // this should be the entrypoint when we build the document +// d.thread.SetMaybeHeads(d.docContext.fullTree.Heads()) +// +// return d.docContext.docState, nil +//} +// +//func (d *Document) State() DocumentState { +// return nil +//} diff --git a/exampledocument/document_test.go b/plaintextdocument/document_test.go similarity index 87% rename from exampledocument/document_test.go rename to plaintextdocument/document_test.go index 99fea827..a2b6691f 100644 --- a/exampledocument/document_test.go +++ b/plaintextdocument/document_test.go @@ -1,4 +1,4 @@ -package exampledocument +package plaintextdocument import ( "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder" @@ -22,7 +22,7 @@ func TestDocument_Build(t *testing.T) { t.Fatal(err) } - st := res.(*PlainTextDocumentState) + st := res.(*DocumentState) assert.Equal(t, st.Text, "some text|first") } @@ -42,11 +42,11 @@ func TestDocument_Update(t *testing.T) { t.Fatal(err) } - st := res.(*PlainTextDocumentState) + st := res.(*DocumentState) assert.Equal(t, st.Text, "some text|first") rawChs := thread.GetUpdatedChanges() res, updateResult, err := doc.Update(rawChs...) assert.Equal(t, updateResult, UpdateResultAppend) - assert.Equal(t, res.(*PlainTextDocumentState).Text, "some text|first|second") + assert.Equal(t, res.(*DocumentState).Text, "some text|first|second") } diff --git a/exampledocument/documentcontext.go b/plaintextdocument/documentcontext.go similarity index 90% rename from exampledocument/documentcontext.go rename to plaintextdocument/documentcontext.go index bb165ab8..2aae6a97 100644 --- a/exampledocument/documentcontext.go +++ b/plaintextdocument/documentcontext.go @@ -1,4 +1,4 @@ -package exampledocument +package plaintextdocument import "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" diff --git a/exampledocument/documentstatebuilder.go b/plaintextdocument/documentstatebuilder.go similarity index 98% rename from exampledocument/documentstatebuilder.go rename to plaintextdocument/documentstatebuilder.go index 4a6ac604..ab23274e 100644 --- a/exampledocument/documentstatebuilder.go +++ b/plaintextdocument/documentstatebuilder.go @@ -1,4 +1,4 @@ -package exampledocument +package plaintextdocument import ( "fmt" diff --git a/exampledocument/documentstatebuilder_test.go b/plaintextdocument/documentstatebuilder_test.go similarity index 91% rename from exampledocument/documentstatebuilder_test.go rename to plaintextdocument/documentstatebuilder_test.go index 3f1d7232..b293d212 100644 --- a/exampledocument/documentstatebuilder_test.go +++ b/plaintextdocument/documentstatebuilder_test.go @@ -1,4 +1,4 @@ -package exampledocument +package plaintextdocument import ( "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree" @@ -23,7 +23,7 @@ func TestDocumentStateBuilder_UserJoinBuild(t *testing.T) { t.Fatalf("should build acl aclState without err: %v", err) } - st := ctx.DocState.(*PlainTextDocumentState) + st := ctx.DocState.(*DocumentState) assert.Equal(t, st.Text, "some text|first") } @@ -43,6 +43,6 @@ func TestDocumentStateBuilder_UserRemoveBuild(t *testing.T) { t.Fatalf("should build acl aclState without err: %v", err) } - st := ctx.DocState.(*PlainTextDocumentState) + st := ctx.DocState.(*DocumentState) assert.Equal(t, st.Text, "some text|first") } diff --git a/plaintextdocument/plaintextdocstate.go b/plaintextdocument/plaintextdocstate.go new file mode 100644 index 00000000..d7447b03 --- /dev/null +++ b/plaintextdocument/plaintextdocstate.go @@ -0,0 +1,59 @@ +package plaintextdocument + +import ( + "fmt" + + "github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/testchanges/pb" + "github.com/gogo/protobuf/proto" +) + +type DocumentState struct { + LastChangeId string + Text string +} + +func NewDocumentState(text string, id string) *DocumentState { + return &DocumentState{ + LastChangeId: id, + Text: text, + } +} + +func BuildDocumentStateFromChange(change []byte, id string) (*DocumentState, error) { + var changesData pb.PlainTextChangeData + err := proto.Unmarshal(change, &changesData) + if err != nil { + return nil, err + } + + if changesData.GetSnapshot() == nil { + return nil, fmt.Errorf("could not create state from empty snapshot") + } + return NewDocumentState(changesData.GetSnapshot().GetText(), id), nil +} + +func (p *DocumentState) ApplyChange(change []byte, id string) (*DocumentState, error) { + var changesData pb.PlainTextChangeData + err := proto.Unmarshal(change, &changesData) + if err != nil { + return nil, err + } + + for _, content := range changesData.GetContent() { + err = p.applyChange(content) + if err != nil { + return nil, err + } + } + p.LastChangeId = id + return p, nil +} + +func (p *DocumentState) applyChange(ch *pb.PlainTextChangeContent) error { + switch { + case ch.GetTextAppend() != nil: + text := ch.GetTextAppend().GetText() + p.Text += "|" + text + } + return nil +} diff --git a/plaintextdocument/plaintextdocument.go b/plaintextdocument/plaintextdocument.go new file mode 100644 index 00000000..d033fb38 --- /dev/null +++ b/plaintextdocument/plaintextdocument.go @@ -0,0 +1 @@ +package plaintextdocument