diff --git a/commonspace/object/tree/objecttree/historytree.go b/commonspace/object/tree/objecttree/historytree.go new file mode 100644 index 00000000..3a59230b --- /dev/null +++ b/commonspace/object/tree/objecttree/historytree.go @@ -0,0 +1,43 @@ +package objecttree + +import "errors" + +var ErrLoadBeforeRoot = errors.New("can't load before root") + +type HistoryTree interface { + RWLocker + + Id() string + Root() *Change + Heads() []string + IterateFrom(id string, convert ChangeConvertFunc, iterate ChangeIterateFunc) error +} + +type historyTree struct { + *objectTree +} + +func (h *historyTree) rebuildFromStorage(beforeId string, include bool) (err error) { + ot := h.objectTree + ot.treeBuilder.Reset() + if beforeId == ot.Id() && !include { + return ErrLoadBeforeRoot + } + + heads := []string{beforeId} + if beforeId == "" { + heads, err = ot.treeStorage.Heads() + if err != nil { + return + } + } else if !include { + beforeChange, err := ot.treeBuilder.loadChange(beforeId) + if err != nil { + return err + } + heads = beforeChange.PreviousIds + } + + ot.tree, err = ot.treeBuilder.build(heads, nil, nil) + return +} diff --git a/commonspace/object/tree/objecttree/objecttree.go b/commonspace/object/tree/objecttree/objecttree.go index 176d7192..c08003b6 100644 --- a/commonspace/object/tree/objecttree/objecttree.go +++ b/commonspace/object/tree/objecttree/objecttree.go @@ -5,7 +5,7 @@ import ( "context" "errors" "github.com/anytypeio/any-sync/commonspace/object/acl/aclrecordproto" - list2 "github.com/anytypeio/any-sync/commonspace/object/acl/list" + list "github.com/anytypeio/any-sync/commonspace/object/acl/list" "github.com/anytypeio/any-sync/commonspace/object/keychain" "github.com/anytypeio/any-sync/commonspace/object/tree/treechangeproto" "github.com/anytypeio/any-sync/commonspace/object/tree/treestorage" @@ -75,7 +75,7 @@ type objectTree struct { validator ObjectTreeValidator rawChangeLoader *rawChangeLoader treeBuilder *treeBuilder - aclList list2.AclList + aclList list.AclList id string rawRoot *treechangeproto.RawTreeChangeWithId @@ -101,13 +101,13 @@ type objectTreeDeps struct { treeStorage treestorage.TreeStorage validator ObjectTreeValidator rawChangeLoader *rawChangeLoader - aclList list2.AclList + aclList list.AclList } func defaultObjectTreeDeps( rootChange *treechangeproto.RawTreeChangeWithId, treeStorage treestorage.TreeStorage, - aclList list2.AclList) objectTreeDeps { + aclList list.AclList) objectTreeDeps { keychain := keychain.NewKeychain() changeBuilder := NewChangeBuilder(keychain, rootChange) @@ -208,7 +208,7 @@ func (ot *objectTree) prepareBuilderContent(content SignableChangeContent) (cnt canWrite := state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Writer) || state.HasPermission(content.Identity, aclrecordproto.AclUserPermissions_Admin) if !canWrite { - err = list2.ErrInsufficientPermissions + err = list.ErrInsufficientPermissions return } @@ -471,7 +471,7 @@ func (ot *objectTree) IterateFrom(id string, convert ChangeConvertFunc, iterate } readKey, exists := ot.keys[c.ReadKeyHash] if !exists { - err = list2.ErrNoReadKey + err = list.ErrNoReadKey return } diff --git a/commonspace/object/tree/objecttree/objecttree_test.go b/commonspace/object/tree/objecttree/objecttree_test.go index 03f4322b..24506bf0 100644 --- a/commonspace/object/tree/objecttree/objecttree_test.go +++ b/commonspace/object/tree/objecttree/objecttree_test.go @@ -111,6 +111,24 @@ func prepareAclList(t *testing.T) list.AclList { return aclList } +func prepareTreeDeps(aclList list.AclList) (*mockChangeCreator, objectTreeDeps) { + changeCreator := &mockChangeCreator{} + treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id) + root, _ := treeStorage.Root() + changeBuilder := &mockChangeBuilder{ + originalBuilder: NewChangeBuilder(nil, root), + } + deps := objectTreeDeps{ + changeBuilder: changeBuilder, + treeBuilder: newTreeBuilder(treeStorage, changeBuilder), + treeStorage: treeStorage, + rawChangeLoader: newRawChangeLoader(treeStorage, changeBuilder), + validator: &mockChangeValidator{}, + aclList: aclList, + } + return changeCreator, deps +} + func prepareTreeContext(t *testing.T, aclList list.AclList) testTreeContext { changeCreator := &mockChangeCreator{} treeStorage := changeCreator.createNewTreeStorage("0", aclList.Head().Id) @@ -542,4 +560,87 @@ func TestObjectTree(t *testing.T) { assert.Equal(t, ch, raw, "the changes in the storage should be the same") } }) + + t.Run("test history tree not include", func(t *testing.T) { + changeCreator, deps := prepareTreeDeps(aclList) + + rawChanges := []*treechangeproto.RawTreeChangeWithId{ + changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"), + changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"), + changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"), + changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"), + changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"), + changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"), + } + deps.treeStorage.TransactionAdd(rawChanges, []string{"6"}) + hTree, err := buildHistoryTree(deps, HistoryTreeParams{ + BeforeId: "6", + IncludeBeforeId: false, + }) + require.NoError(t, err) + // check tree heads + assert.Equal(t, []string{"3", "4", "5"}, hTree.Heads()) + + // check tree iterate + var iterChangesId []string + err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool { + iterChangesId = append(iterChangesId, change.Id) + return true + }) + require.NoError(t, err, "iterate should be without error") + assert.Equal(t, []string{"0", "1", "2", "3", "4", "5"}, iterChangesId) + assert.Equal(t, "0", hTree.Root().Id) + }) + + t.Run("test history tree include", func(t *testing.T) { + changeCreator, deps := prepareTreeDeps(aclList) + + rawChanges := []*treechangeproto.RawTreeChangeWithId{ + changeCreator.createRaw("1", aclList.Head().Id, "0", false, "0"), + changeCreator.createRaw("2", aclList.Head().Id, "0", false, "1"), + changeCreator.createRaw("3", aclList.Head().Id, "0", true, "2"), + changeCreator.createRaw("4", aclList.Head().Id, "0", false, "2"), + changeCreator.createRaw("5", aclList.Head().Id, "0", false, "1"), + changeCreator.createRaw("6", aclList.Head().Id, "0", false, "3", "4", "5"), + } + deps.treeStorage.TransactionAdd(rawChanges, []string{"6"}) + hTree, err := buildHistoryTree(deps, HistoryTreeParams{ + BeforeId: "6", + IncludeBeforeId: true, + }) + require.NoError(t, err) + // check tree heads + assert.Equal(t, []string{"6"}, hTree.Heads()) + + // check tree iterate + var iterChangesId []string + err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool { + iterChangesId = append(iterChangesId, change.Id) + return true + }) + require.NoError(t, err, "iterate should be without error") + assert.Equal(t, []string{"0", "1", "2", "3", "4", "5", "6"}, iterChangesId) + assert.Equal(t, "0", hTree.Root().Id) + }) + + t.Run("test history tree root", func(t *testing.T) { + _, deps := prepareTreeDeps(aclList) + hTree, err := buildHistoryTree(deps, HistoryTreeParams{ + BeforeId: "0", + IncludeBeforeId: true, + }) + require.NoError(t, err) + // check tree heads + assert.Equal(t, []string{"0"}, hTree.Heads()) + + // check tree iterate + var iterChangesId []string + err = hTree.IterateFrom(hTree.Root().Id, nil, func(change *Change) bool { + iterChangesId = append(iterChangesId, change.Id) + return true + }) + require.NoError(t, err, "iterate should be without error") + assert.Equal(t, []string{"0"}, iterChangesId) + assert.Equal(t, "0", hTree.Root().Id) + }) } diff --git a/commonspace/object/tree/objecttree/treebuilder.go b/commonspace/object/tree/objecttree/treebuilder.go index e82952b0..ab5592c7 100644 --- a/commonspace/object/tree/objecttree/treebuilder.go +++ b/commonspace/object/tree/objecttree/treebuilder.go @@ -41,16 +41,20 @@ func (tb *treeBuilder) Reset() { } func (tb *treeBuilder) Build(theirHeads []string, newChanges []*Change) (*Tree, error) { - var proposedHeads []string - tb.cache = make(map[string]*Change) heads, err := tb.treeStorage.Heads() if err != nil { return nil, err } + return tb.build(heads, theirHeads, newChanges) +} + +func (tb *treeBuilder) build(heads []string, theirHeads []string, newChanges []*Change) (*Tree, error) { + var proposedHeads []string + tb.cache = make(map[string]*Change) // TODO: we can actually get this from tree (though not sure, that there would always be // an invariant where the tree has the closest common snapshot of heads) - // so if optimization is critical we can change this to inject from tree directly + // so if optimization is critical we can change this to inject from tree directly, // but then we have to be sure that invariant stays true oldBreakpoint, err := tb.findBreakpoint(heads, true) if err != nil { diff --git a/commonspace/object/tree/objecttree/objecttreefactory.go b/commonspace/object/tree/objecttree/treefactory.go similarity index 75% rename from commonspace/object/tree/objecttree/objecttreefactory.go rename to commonspace/object/tree/objecttree/treefactory.go index 1bd5a885..7cb2c302 100644 --- a/commonspace/object/tree/objecttree/objecttreefactory.go +++ b/commonspace/object/tree/objecttree/treefactory.go @@ -41,6 +41,15 @@ func BuildObjectTree(treeStorage treestorage.TreeStorage, aclList list.AclList) return buildObjectTree(deps) } +func BuildHistoryTree(params HistoryTreeParams) (HistoryTree, error) { + rootChange, err := params.TreeStorage.Root() + if err != nil { + return nil, err + } + deps := defaultObjectTreeDeps(rootChange, params.TreeStorage, params.AclList) + return buildHistoryTree(deps, params) +} + func CreateDerivedObjectTree( payload ObjectTreeCreatePayload, aclList list.AclList, @@ -118,7 +127,6 @@ func buildObjectTree(deps objectTreeDeps) (ObjectTree, error) { aclList: deps.aclList, changeBuilder: deps.changeBuilder, rawChangeLoader: deps.rawChangeLoader, - tree: nil, keys: make(map[uint64]*symmetric.Key), newChangesBuf: make([]*Change, 0, 10), difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10), @@ -146,3 +154,38 @@ func buildObjectTree(deps objectTreeDeps) (ObjectTree, error) { return objTree, nil } + +type HistoryTreeParams struct { + TreeStorage treestorage.TreeStorage + AclList list.AclList + BeforeId string + IncludeBeforeId bool +} + +func buildHistoryTree(deps objectTreeDeps, params HistoryTreeParams) (ht HistoryTree, err error) { + objTree := &objectTree{ + treeStorage: deps.treeStorage, + treeBuilder: deps.treeBuilder, + validator: deps.validator, + aclList: deps.aclList, + changeBuilder: deps.changeBuilder, + rawChangeLoader: deps.rawChangeLoader, + keys: make(map[uint64]*symmetric.Key), + newChangesBuf: make([]*Change, 0, 10), + difSnapshotBuf: make([]*treechangeproto.RawTreeChangeWithId, 0, 10), + notSeenIdxBuf: make([]int, 0, 10), + newSnapshotsBuf: make([]*Change, 0, 10), + } + + hTree := historyTree{objectTree: objTree} + err = hTree.rebuildFromStorage(params.BeforeId, params.IncludeBeforeId) + if err != nil { + return nil, err + } + objTree.id = objTree.treeStorage.Id() + objTree.rawRoot, err = objTree.treeStorage.Root() + if err != nil { + return nil, err + } + return hTree, nil +} diff --git a/commonspace/space.go b/commonspace/space.go index 2d299b31..742ece52 100644 --- a/commonspace/space.go +++ b/commonspace/space.go @@ -87,6 +87,7 @@ type Space interface { PutTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, listener updatelistener.UpdateListener) (t objecttree.ObjectTree, err error) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error) DeleteTree(ctx context.Context, id string) (err error) + BuildHistoryTree(ctx context.Context, id string, opts HistoryTreeOpts) (t objecttree.HistoryTree, err error) HeadSync() headsync.HeadSync SyncStatus() syncstatus.StatusUpdater @@ -296,6 +297,11 @@ type BuildTreeOpts struct { WaitTreeRemoteSync bool } +type HistoryTreeOpts struct { + BeforeId string + Include bool +} + func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t objecttree.ObjectTree, err error) { if s.isClosed.Load() { err = ErrSpaceClosed @@ -317,6 +323,24 @@ func (s *space) BuildTree(ctx context.Context, id string, opts BuildTreeOpts) (t return synctree.BuildSyncTreeOrGetRemote(ctx, id, deps) } +func (s *space) BuildHistoryTree(ctx context.Context, id string, opts HistoryTreeOpts) (t objecttree.HistoryTree, err error) { + if s.isClosed.Load() { + err = ErrSpaceClosed + return + } + + params := objecttree.HistoryTreeParams{ + AclList: s.aclList, + BeforeId: opts.BeforeId, + IncludeBeforeId: opts.Include, + } + params.TreeStorage, err = s.storage.TreeStorage(id) + if err == nil { + return + } + return objecttree.BuildHistoryTree(params) +} + func (s *space) DeleteTree(ctx context.Context, id string) (err error) { return s.settingsObject.DeleteObject(id) }