diff --git a/commonspace/object/tree/objecttree/mock_objecttree/mock_objecttree.go b/commonspace/object/tree/objecttree/mock_objecttree/mock_objecttree.go index fb5ea6b1..e2ebe8e0 100644 --- a/commonspace/object/tree/objecttree/mock_objecttree/mock_objecttree.go +++ b/commonspace/object/tree/objecttree/mock_objecttree/mock_objecttree.go @@ -258,6 +258,20 @@ func (mr *MockObjectTreeMockRecorder) IterateRoot(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateRoot", reflect.TypeOf((*MockObjectTree)(nil).IterateRoot), arg0, arg1) } +// Len mocks base method. +func (m *MockObjectTree) Len() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Len") + ret0, _ := ret[0].(int) + return ret0 +} + +// Len indicates an expected call of Len. +func (mr *MockObjectTreeMockRecorder) Len() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockObjectTree)(nil).Len)) +} + // Lock mocks base method. func (m *MockObjectTree) Lock() { m.ctrl.T.Helper() diff --git a/commonspace/object/tree/objecttree/objecttree.go b/commonspace/object/tree/objecttree/objecttree.go index f455b150..430329c2 100644 --- a/commonspace/object/tree/objecttree/objecttree.go +++ b/commonspace/object/tree/objecttree/objecttree.go @@ -57,6 +57,7 @@ type ReadableObjectTree interface { ChangeInfo() *treechangeproto.TreeChangeInfo Heads() []string Root() *Change + Len() int AclList() list.AclList @@ -135,6 +136,10 @@ func (ot *objectTree) Id() string { return ot.id } +func (ot *objectTree) Len() int { + return ot.tree.Len() +} + func (ot *objectTree) AclList() list.AclList { return ot.aclList } diff --git a/commonspace/object/tree/objecttree/util.go b/commonspace/object/tree/objecttree/util.go index 2e960233..70911364 100644 --- a/commonspace/object/tree/objecttree/util.go +++ b/commonspace/object/tree/objecttree/util.go @@ -3,6 +3,7 @@ package objecttree import ( "fmt" "github.com/anytypeio/any-sync/util/crypto" + "math/rand" ) func commonSnapshotForTwoPaths(ourPath []string, theirPath []string) (string, error) { @@ -40,3 +41,16 @@ func deriveTreeKey(key crypto.SymKey, cid string) (crypto.SymKey, error) { } return crypto.DeriveSymmetricKey(raw, fmt.Sprintf(crypto.AnysyncTreePath, cid)) } + +func DoSnapshot(treeLen int) bool { + if treeLen <= 100 { + return false + } + + var ( + delta = treeLen/50 + 1 + midPoint = 1000 + val = rand.Intn(midPoint * 2) + ) + return midPoint-delta <= val && val <= midPoint+delta +} diff --git a/commonspace/object/tree/synctree/mock_synctree/mock_synctree.go b/commonspace/object/tree/synctree/mock_synctree/mock_synctree.go index 1dee0ce3..3ab6cf1b 100644 --- a/commonspace/object/tree/synctree/mock_synctree/mock_synctree.go +++ b/commonspace/object/tree/synctree/mock_synctree/mock_synctree.go @@ -383,6 +383,20 @@ func (mr *MockSyncTreeMockRecorder) IterateRoot(arg0, arg1 interface{}) *gomock. return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IterateRoot", reflect.TypeOf((*MockSyncTree)(nil).IterateRoot), arg0, arg1) } +// Len mocks base method. +func (m *MockSyncTree) Len() int { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Len") + ret0, _ := ret[0].(int) + return ret0 +} + +// Len indicates an expected call of Len. +func (mr *MockSyncTreeMockRecorder) Len() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Len", reflect.TypeOf((*MockSyncTree)(nil).Len)) +} + // Lock mocks base method. func (m *MockSyncTree) Lock() { m.ctrl.T.Helper() diff --git a/commonspace/settings/settings.go b/commonspace/settings/settings.go index 42bd88ed..1455faff 100644 --- a/commonspace/settings/settings.go +++ b/commonspace/settings/settings.go @@ -40,6 +40,8 @@ var ( ErrCantDeleteSpace = errors.New("not able to delete space") ) +var doSnapshot = objecttree.DoSnapshot + type BuildTreeFunc func(ctx context.Context, id string, listener updatelistener.UpdateListener) (t synctree.SyncTree, err error) type Deps struct { @@ -228,14 +230,13 @@ func (s *settingsObject) DeleteObject(id string) (err error) { err = ErrObjDoesNotExist return } - - // TODO: add snapshot logic - res, err := s.changeFactory.CreateObjectDeleteChange(id, s.state, false) + isSnapshot := doSnapshot(s.Len()) + res, err := s.changeFactory.CreateObjectDeleteChange(id, s.state, isSnapshot) if err != nil { return } - return s.addContent(res) + return s.addContent(res, isSnapshot) } func (s *settingsObject) verifyDeleteSpace(raw *treechangeproto.RawTreeChangeWithId) (err error) { @@ -246,12 +247,12 @@ func (s *settingsObject) verifyDeleteSpace(raw *treechangeproto.RawTreeChangeWit return verifyDeleteContent(data, "") } -func (s *settingsObject) addContent(data []byte) (err error) { +func (s *settingsObject) addContent(data []byte, isSnapshot bool) (err error) { accountData := s.account.Account() _, err = s.AddContent(context.Background(), objecttree.SignableChangeContent{ Data: data, Key: accountData.SignKey, - IsSnapshot: false, + IsSnapshot: isSnapshot, IsEncrypted: false, }) if err != nil { diff --git a/commonspace/settings/settings_test.go b/commonspace/settings/settings_test.go index 1526b1f4..166cf284 100644 --- a/commonspace/settings/settings_test.go +++ b/commonspace/settings/settings_test.go @@ -125,7 +125,7 @@ func TestSettingsObject_Init(t *testing.T) { require.NoError(t, err) } -func TestSettingsObject_DeleteObject(t *testing.T) { +func TestSettingsObject_DeleteObject_NoSnapshot(t *testing.T) { fx := newSettingsFixture(t) defer fx.stop() @@ -134,11 +134,14 @@ func TestSettingsObject_DeleteObject(t *testing.T) { err := fx.doc.Init(context.Background()) require.NoError(t, err) - time.Sleep(100 * time.Millisecond) delId := "delId" + doSnapshot = func(len int) bool { + return false + } fx.syncTree.EXPECT().Id().Return("syncId") + fx.syncTree.EXPECT().Len().Return(10) fx.delState.EXPECT().Exists(delId).Return(false) fx.spaceStorage.EXPECT().TreeStorage(delId).Return(nil, nil) res := []byte("settingsData") @@ -165,6 +168,49 @@ func TestSettingsObject_DeleteObject(t *testing.T) { require.NoError(t, err) } +func TestSettingsObject_DeleteObject_WithSnapshot(t *testing.T) { + fx := newSettingsFixture(t) + defer fx.stop() + + fx.spaceStorage.EXPECT().SpaceSettingsId().Return(fx.docId) + fx.deleter.EXPECT().Delete() + + err := fx.doc.Init(context.Background()) + require.NoError(t, err) + + delId := "delId" + doSnapshot = func(len int) bool { + return true + } + + fx.syncTree.EXPECT().Id().Return("syncId") + fx.syncTree.EXPECT().Len().Return(10) + fx.delState.EXPECT().Exists(delId).Return(false) + fx.spaceStorage.EXPECT().TreeStorage(delId).Return(nil, nil) + res := []byte("settingsData") + fx.doc.state = &settingsstate.State{LastIteratedId: "someId"} + fx.changeFactory.EXPECT().CreateObjectDeleteChange(delId, fx.doc.state, true).Return(res, nil) + + accountData, err := accountdata.NewRandom() + require.NoError(t, err) + fx.account.EXPECT().Account().Return(accountData) + fx.syncTree.EXPECT().AddContent(gomock.Any(), objecttree.SignableChangeContent{ + Data: res, + Key: accountData.SignKey, + IsSnapshot: true, + IsEncrypted: false, + }).Return(objecttree.AddResult{}, nil) + + fx.stateBuilder.EXPECT().Build(fx.doc, fx.doc.state).Return(fx.doc.state, nil) + fx.deletionManager.EXPECT().UpdateState(gomock.Any(), fx.doc.state).Return(nil) + err = fx.doc.DeleteObject(delId) + require.NoError(t, err) + + fx.syncTree.EXPECT().Close().Return(nil) + err = fx.doc.Close() + require.NoError(t, err) +} + func TestSettingsObject_Rebuild(t *testing.T) { fx := newSettingsFixture(t) defer fx.stop() diff --git a/commonspace/settings/settingsstate/changefactory.go b/commonspace/settings/settingsstate/changefactory.go index df3da76b..a5a70dc5 100644 --- a/commonspace/settings/settingsstate/changefactory.go +++ b/commonspace/settings/settingsstate/changefactory.go @@ -24,9 +24,10 @@ func (c *changeFactory) CreateObjectDeleteChange(id string, state *State, isSnap Content: []*spacesyncproto.SpaceSettingsContent{ {Value: content}, }, - Snapshot: nil, } - // TODO: add snapshot logic + if isSnapshot { + change.Snapshot = c.makeSnapshot(state, id, "") + } res, err = change.Marshal() return } @@ -39,9 +40,27 @@ func (c *changeFactory) CreateSpaceDeleteChange(peerId string, state *State, isS Content: []*spacesyncproto.SpaceSettingsContent{ {Value: content}, }, - Snapshot: nil, } - // TODO: add snapshot logic + if isSnapshot { + change.Snapshot = c.makeSnapshot(state, "", peerId) + } res, err = change.Marshal() return } + +func (c *changeFactory) makeSnapshot(state *State, objectId, deleterPeer string) *spacesyncproto.SpaceSettingsSnapshot { + var ( + deletedIds = state.DeletedIds + deleterId = state.DeleterId + ) + if objectId != "" { + deletedIds = append(deletedIds, objectId) + } + if deleterPeer != "" { + deleterId = deleterPeer + } + return &spacesyncproto.SpaceSettingsSnapshot{ + DeletedIds: deletedIds, + DeleterPeerId: deleterId, + } +} diff --git a/commonspace/settings/settingsstate/changefactory_test.go b/commonspace/settings/settingsstate/changefactory_test.go new file mode 100644 index 00000000..41135b36 --- /dev/null +++ b/commonspace/settings/settingsstate/changefactory_test.go @@ -0,0 +1,59 @@ +package settingsstate + +import ( + "github.com/anytypeio/any-sync/commonspace/spacesyncproto" + "github.com/gogo/protobuf/proto" + "github.com/stretchr/testify/require" + "testing" +) + +func TestChangeFactory_CreateObjectDeleteChange(t *testing.T) { + factory := NewChangeFactory() + state := &State{ + DeletedIds: []string{"1", "2"}, + DeleterId: "del", + } + marshalled, err := factory.CreateObjectDeleteChange("3", state, false) + require.NoError(t, err) + data := &spacesyncproto.SettingsData{} + err = proto.Unmarshal(marshalled, data) + require.NoError(t, err) + require.Nil(t, data.Snapshot) + require.Equal(t, "3", data.Content[0].Value.(*spacesyncproto.SpaceSettingsContent_ObjectDelete).ObjectDelete.Id) + + marshalled, err = factory.CreateObjectDeleteChange("3", state, true) + require.NoError(t, err) + data = &spacesyncproto.SettingsData{} + err = proto.Unmarshal(marshalled, data) + require.NoError(t, err) + require.Equal(t, &spacesyncproto.SpaceSettingsSnapshot{ + DeletedIds: []string{"1", "2", "3"}, + DeleterPeerId: "del", + }, data.Snapshot) + require.Equal(t, "3", data.Content[0].Value.(*spacesyncproto.SpaceSettingsContent_ObjectDelete).ObjectDelete.Id) +} + +func TestChangeFactory_CreateSpaceDeleteChange(t *testing.T) { + factory := NewChangeFactory() + state := &State{ + DeletedIds: []string{"1", "2"}, + } + marshalled, err := factory.CreateSpaceDeleteChange("del", state, false) + require.NoError(t, err) + data := &spacesyncproto.SettingsData{} + err = proto.Unmarshal(marshalled, data) + require.NoError(t, err) + require.Nil(t, data.Snapshot) + require.Equal(t, "del", data.Content[0].Value.(*spacesyncproto.SpaceSettingsContent_SpaceDelete).SpaceDelete.DeleterPeerId) + + marshalled, err = factory.CreateSpaceDeleteChange("del", state, true) + require.NoError(t, err) + data = &spacesyncproto.SettingsData{} + err = proto.Unmarshal(marshalled, data) + require.NoError(t, err) + require.Equal(t, &spacesyncproto.SpaceSettingsSnapshot{ + DeletedIds: []string{"1", "2"}, + DeleterPeerId: "del", + }, data.Snapshot) + require.Equal(t, "del", data.Content[0].Value.(*spacesyncproto.SpaceSettingsContent_SpaceDelete).SpaceDelete.DeleterPeerId) +}