Add tree updater to ACL tree
This commit is contained in:
parent
cc802ebc69
commit
5b9d403b7d
@ -23,11 +23,17 @@ type AddResult struct {
|
|||||||
Summary AddResultSummary
|
Summary AddResultSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TreeUpdateListener interface {
|
||||||
|
Update(tree ACLTree)
|
||||||
|
Rebuild(tree ACLTree)
|
||||||
|
}
|
||||||
|
|
||||||
type ACLTree interface {
|
type ACLTree interface {
|
||||||
ACLState() *ACLState
|
ACLState() *ACLState
|
||||||
AddContent(f func(builder ChangeBuilder)) (*Change, error)
|
AddContent(f func(builder ChangeBuilder)) (*Change, error)
|
||||||
AddChanges(changes ...*Change) (AddResult, error)
|
AddChanges(changes ...*Change) (AddResult, error)
|
||||||
Heads() []string
|
Heads() []string
|
||||||
|
Root() *Change
|
||||||
Iterate(func(change *Change) bool)
|
Iterate(func(change *Change) bool)
|
||||||
IterateFrom(string, func(change *Change) bool)
|
IterateFrom(string, func(change *Change) bool)
|
||||||
HasChange(string) bool
|
HasChange(string) bool
|
||||||
@ -36,6 +42,7 @@ type ACLTree interface {
|
|||||||
type aclTree struct {
|
type aclTree struct {
|
||||||
thread thread.Thread
|
thread thread.Thread
|
||||||
accountData *account.AccountData
|
accountData *account.AccountData
|
||||||
|
updateListener TreeUpdateListener
|
||||||
|
|
||||||
fullTree *Tree
|
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
|
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
|
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()
|
decoder := keys.NewEd25519Decoder()
|
||||||
aclTreeBuilder := newACLTreeBuilder(t, decoder)
|
aclTreeBuilder := newACLTreeBuilder(t, decoder)
|
||||||
treeBuilder := newTreeBuilder(t, decoder)
|
treeBuilder := newTreeBuilder(t, decoder)
|
||||||
@ -68,6 +78,7 @@ func BuildACLTree(t thread.Thread, acc *account.AccountData) (ACLTree, error) {
|
|||||||
aclStateBuilder: aclStateBuilder,
|
aclStateBuilder: aclStateBuilder,
|
||||||
snapshotValidator: snapshotValidator,
|
snapshotValidator: snapshotValidator,
|
||||||
changeBuilder: changeBuilder,
|
changeBuilder: changeBuilder,
|
||||||
|
updateListener: listener,
|
||||||
}
|
}
|
||||||
err := aclTree.rebuildFromThread(false)
|
err := aclTree.rebuildFromThread(false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -183,6 +194,7 @@ func (a *aclTree) AddContent(build func(builder ChangeBuilder)) (*Change, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.thread.SetHeads([]string{ch.Id})
|
a.thread.SetHeads([]string{ch.Id})
|
||||||
|
a.updateListener.Update(a)
|
||||||
return ch, nil
|
return ch, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +202,8 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) {
|
|||||||
a.Lock()
|
a.Lock()
|
||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
// TODO: make proper error handling, because there are a lot of corner cases where this will break
|
// TODO: make proper error handling, because there are a lot of corner cases where this will break
|
||||||
var aclChanges []*Change
|
|
||||||
var err error
|
var err error
|
||||||
|
var mode Mode
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -209,13 +221,17 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
a.thread.RemoveOrphans(toRemove...)
|
a.thread.RemoveOrphans(toRemove...)
|
||||||
|
switch mode {
|
||||||
|
case Append:
|
||||||
|
a.updateListener.Update(a)
|
||||||
|
case Rebuild:
|
||||||
|
a.updateListener.Rebuild(a)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, ch := range changes {
|
for _, ch := range changes {
|
||||||
if ch.IsACLChange() {
|
|
||||||
aclChanges = append(aclChanges, ch)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
err = a.thread.AddChange(ch)
|
err = a.thread.AddChange(ch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return AddResult{}, err
|
return AddResult{}, err
|
||||||
@ -224,7 +240,7 @@ func (a *aclTree) AddChanges(changes ...*Change) (AddResult, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
prevHeads := a.fullTree.Heads()
|
prevHeads := a.fullTree.Heads()
|
||||||
mode := a.fullTree.Add(changes...)
|
mode = a.fullTree.Add(changes...)
|
||||||
switch mode {
|
switch mode {
|
||||||
case Nothing:
|
case Nothing:
|
||||||
return AddResult{
|
return AddResult{
|
||||||
@ -284,3 +300,9 @@ func (a *aclTree) Heads() []string {
|
|||||||
defer a.Unlock()
|
defer a.Unlock()
|
||||||
return a.fullTree.Heads()
|
return a.fullTree.Heads()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *aclTree) Root() *Change {
|
||||||
|
a.Lock()
|
||||||
|
defer a.Unlock()
|
||||||
|
return a.fullTree.Root()
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
378
plaintextdocument/document.go
Normal file
378
plaintextdocument/document.go
Normal file
@ -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
|
||||||
|
//}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package exampledocument
|
package plaintextdocument
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder"
|
"github.com/anytypeio/go-anytype-infrastructure-experiments/testutils/threadbuilder"
|
||||||
@ -22,7 +22,7 @@ func TestDocument_Build(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
st := res.(*PlainTextDocumentState)
|
st := res.(*DocumentState)
|
||||||
assert.Equal(t, st.Text, "some text|first")
|
assert.Equal(t, st.Text, "some text|first")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,11 +42,11 @@ func TestDocument_Update(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
st := res.(*PlainTextDocumentState)
|
st := res.(*DocumentState)
|
||||||
assert.Equal(t, st.Text, "some text|first")
|
assert.Equal(t, st.Text, "some text|first")
|
||||||
|
|
||||||
rawChs := thread.GetUpdatedChanges()
|
rawChs := thread.GetUpdatedChanges()
|
||||||
res, updateResult, err := doc.Update(rawChs...)
|
res, updateResult, err := doc.Update(rawChs...)
|
||||||
assert.Equal(t, updateResult, UpdateResultAppend)
|
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")
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package exampledocument
|
package plaintextdocument
|
||||||
|
|
||||||
import "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree"
|
import "github.com/anytypeio/go-anytype-infrastructure-experiments/acltree"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package exampledocument
|
package plaintextdocument
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package exampledocument
|
package plaintextdocument
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anytypeio/go-anytype-infrastructure-experiments/acltree"
|
"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)
|
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")
|
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)
|
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")
|
assert.Equal(t, st.Text, "some text|first")
|
||||||
}
|
}
|
||||||
59
plaintextdocument/plaintextdocstate.go
Normal file
59
plaintextdocument/plaintextdocstate.go
Normal file
@ -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
|
||||||
|
}
|
||||||
1
plaintextdocument/plaintextdocument.go
Normal file
1
plaintextdocument/plaintextdocument.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package plaintextdocument
|
||||||
Loading…
x
Reference in New Issue
Block a user