diff --git a/core/domain/syncstatus.go b/core/domain/syncstatus.go index 239ed98d4..178cece1a 100644 --- a/core/domain/syncstatus.go +++ b/core/domain/syncstatus.go @@ -14,6 +14,7 @@ const ( Syncing SpaceSyncStatus = 1 Error SpaceSyncStatus = 2 Offline SpaceSyncStatus = 3 + Unknown SpaceSyncStatus = 4 ) type ObjectSyncStatus int32 diff --git a/core/syncstatus/detailsupdater/updater.go b/core/syncstatus/detailsupdater/updater.go index 5d5b2c08e..cf8155283 100644 --- a/core/syncstatus/detailsupdater/updater.go +++ b/core/syncstatus/detailsupdater/updater.go @@ -15,7 +15,6 @@ import ( "github.com/anyproto/anytype-heart/core/block/editor/basic" "github.com/anyproto/anytype-heart/core/block/editor/smartblock" "github.com/anyproto/anytype-heart/core/domain" - "github.com/anyproto/anytype-heart/core/syncstatus/detailsupdater/helper" "github.com/anyproto/anytype-heart/core/syncstatus/filesyncstatus" "github.com/anyproto/anytype-heart/pkg/lib/bundle" "github.com/anyproto/anytype-heart/pkg/lib/database" @@ -107,7 +106,7 @@ func (u *syncStatusUpdater) UpdateDetails(objectId []string, status domain.Objec } u.mx.Unlock() } - err := u.batcher.TryAdd(&syncStatusDetails{ + err := u.batcher.Add(u.ctx, &syncStatusDetails{ objectIds: objectId, status: status, syncError: syncError, @@ -168,6 +167,9 @@ func (u *syncStatusUpdater) setObjectDetails(syncStatusDetails *syncStatusDetail if !changed { return nil } + if !u.isLayoutSuitableForSyncRelations(record) { + return nil + } spc, err := u.spaceService.Get(u.ctx, syncStatusDetails.spaceId) if err != nil { return err @@ -196,6 +198,18 @@ func (u *syncStatusUpdater) setObjectDetails(syncStatusDetails *syncStatusDetail }) } +func (u *syncStatusUpdater) isLayoutSuitableForSyncRelations(details *types.Struct) bool { + layoutsWithoutSyncRelations := []float64{ + float64(model.ObjectType_participant), + float64(model.ObjectType_dashboard), + float64(model.ObjectType_spaceView), + float64(model.ObjectType_space), + float64(model.ObjectType_date), + } + layout := details.Fields[bundle.RelationKeyLayout.String()].GetNumberValue() + return !slices.Contains(layoutsWithoutSyncRelations, layout) +} + func mapObjectSyncToSpaceSyncStatus(status domain.ObjectSyncStatus, syncError domain.SyncError) domain.SpaceSyncStatus { switch status { case domain.ObjectSynced: @@ -236,9 +250,6 @@ func mapFileStatus(status filesyncstatus.Status) (domain.ObjectSyncStatus, domai } func (u *syncStatusUpdater) setSyncDetails(sb smartblock.SmartBlock, status domain.ObjectSyncStatus, syncError domain.SyncError) error { - if !slices.Contains(helper.SyncRelationsSmartblockTypes(), sb.Type()) { - return nil - } if d, ok := sb.(basic.DetailsSettable); ok { syncStatusDetails := []*model.Detail{ { @@ -280,24 +291,29 @@ func (u *syncStatusUpdater) hasRelationsChange(record *types.Struct, status doma func (u *syncStatusUpdater) processEvents() { defer close(u.finish) for { - status, err := u.batcher.WaitOne(u.ctx) - if err != nil { + statuses, err := u.batcher.Wait(u.ctx) + if len(statuses) == 0 { return } - for _, id := range status.objectIds { - u.mx.Lock() - objectStatus := u.entries[id] - delete(u.entries, id) - u.mx.Unlock() - if objectStatus != nil { - err := u.updateObjectDetails(objectStatus, id) - if err != nil { - log.Errorf("failed to update details %s", err) + for _, status := range statuses { + if err != nil { + return + } + for _, id := range status.objectIds { + u.mx.Lock() + objectStatus := u.entries[id] + delete(u.entries, id) + u.mx.Unlock() + if objectStatus != nil { + err := u.updateObjectDetails(objectStatus, id) + if err != nil { + log.Errorf("failed to update details %s", err) + } } } - } - if len(status.objectIds) == 0 { - u.updateDetails(status) + if len(status.objectIds) == 0 { + u.updateDetails(status) + } } } } diff --git a/core/syncstatus/detailsupdater/updater_test.go b/core/syncstatus/detailsupdater/updater_test.go index 61687b5f3..9eb31813c 100644 --- a/core/syncstatus/detailsupdater/updater_test.go +++ b/core/syncstatus/detailsupdater/updater_test.go @@ -7,6 +7,7 @@ import ( "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/ocache" "github.com/cheggaaa/mb/v3" + "github.com/gogo/protobuf/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -18,6 +19,7 @@ import ( "github.com/anyproto/anytype-heart/pkg/lib/bundle" coresb "github.com/anyproto/anytype-heart/pkg/lib/core/smartblock" "github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore" + "github.com/anyproto/anytype-heart/pkg/lib/pb/model" "github.com/anyproto/anytype-heart/space/clientspace/mock_clientspace" "github.com/anyproto/anytype-heart/space/mock_space" "github.com/anyproto/anytype-heart/tests/testutil" @@ -256,23 +258,33 @@ func TestSyncStatusUpdater_setSyncDetails(t *testing.T) { }) } -func TestSyncStatusUpdater_updateDetails(t *testing.T) { - t.Run("update sync status and date - no changes", func(t *testing.T) { +func TestSyncStatusUpdater_isLayoutSuitableForSyncRelations(t *testing.T) { + t.Run("isLayoutSuitableForSyncRelations - participant details", func(t *testing.T) { // given fixture := newFixture(t) - space := mock_clientspace.NewMockSpace(t) - fixture.service.EXPECT().Get(fixture.updater.ctx, "spaceId").Return(space, nil) - fixture.storeFixture.AddObjects(t, []objectstore.TestObject{ - { - bundle.RelationKeyId: pbtypes.String("id"), - bundle.RelationKeySpaceId: pbtypes.String("spaceId"), - }, - }) - space.EXPECT().DoLockedIfNotExists("id", mock.Anything).Return(nil) // when - fixture.statusUpdater.EXPECT().SendUpdate(domain.MakeSyncStatus("spaceId", domain.Synced, domain.Null, domain.Objects)) - fixture.updater.updateDetails(&syncStatusDetails{nil, domain.ObjectSynced, domain.Null, "spaceId"}) + details := &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_participant)), + }} + isSuitable := fixture.updater.isLayoutSuitableForSyncRelations(details) + + // then + assert.False(t, isSuitable) + }) + + t.Run("isLayoutSuitableForSyncRelations - basic details", func(t *testing.T) { + // given + fixture := newFixture(t) + + // when + details := &types.Struct{Fields: map[string]*types.Value{ + bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_basic)), + }} + isSuitable := fixture.updater.isLayoutSuitableForSyncRelations(details) + + // then + assert.True(t, isSuitable) }) } diff --git a/core/syncstatus/spacesyncstatus/filestate.go b/core/syncstatus/spacesyncstatus/filestate.go index 65bbb86fc..d972e0502 100644 --- a/core/syncstatus/spacesyncstatus/filestate.go +++ b/core/syncstatus/spacesyncstatus/filestate.go @@ -79,7 +79,10 @@ func (f *FileState) SetSyncStatusAndErr(status domain.SpaceSyncStatus, syncErr d func (f *FileState) GetSyncStatus(spaceId string) domain.SpaceSyncStatus { f.Lock() defer f.Unlock() - return f.fileSyncStatusBySpace[spaceId] + if status, ok := f.fileSyncStatusBySpace[spaceId]; ok { + return status + } + return domain.Unknown } func (f *FileState) GetSyncObjectCount(spaceId string) int { diff --git a/core/syncstatus/spacesyncstatus/filestate_test.go b/core/syncstatus/spacesyncstatus/filestate_test.go index 5d1cb4278..249d38afb 100644 --- a/core/syncstatus/spacesyncstatus/filestate_test.go +++ b/core/syncstatus/spacesyncstatus/filestate_test.go @@ -56,7 +56,7 @@ func TestFileState_GetSyncStatus(t *testing.T) { syncStatus := fileState.GetSyncStatus("spaceId") // then - assert.Equal(t, domain.Synced, syncStatus) + assert.Equal(t, domain.Unknown, syncStatus) }) } diff --git a/core/syncstatus/spacesyncstatus/mock_spacesyncstatus/mock_SpaceIdGetter.go b/core/syncstatus/spacesyncstatus/mock_spacesyncstatus/mock_SpaceIdGetter.go index ff2ea22f6..1c4b99a1f 100644 --- a/core/syncstatus/spacesyncstatus/mock_spacesyncstatus/mock_SpaceIdGetter.go +++ b/core/syncstatus/spacesyncstatus/mock_spacesyncstatus/mock_SpaceIdGetter.go @@ -158,51 +158,6 @@ func (_c *MockSpaceIdGetter_Name_Call) RunAndReturn(run func() string) *MockSpac return _c } -// PersonalSpaceId provides a mock function with given fields: -func (_m *MockSpaceIdGetter) PersonalSpaceId() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for PersonalSpaceId") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// MockSpaceIdGetter_PersonalSpaceId_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PersonalSpaceId' -type MockSpaceIdGetter_PersonalSpaceId_Call struct { - *mock.Call -} - -// PersonalSpaceId is a helper method to define mock.On call -func (_e *MockSpaceIdGetter_Expecter) PersonalSpaceId() *MockSpaceIdGetter_PersonalSpaceId_Call { - return &MockSpaceIdGetter_PersonalSpaceId_Call{Call: _e.mock.On("PersonalSpaceId")} -} - -func (_c *MockSpaceIdGetter_PersonalSpaceId_Call) Run(run func()) *MockSpaceIdGetter_PersonalSpaceId_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *MockSpaceIdGetter_PersonalSpaceId_Call) Return(_a0 string) *MockSpaceIdGetter_PersonalSpaceId_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *MockSpaceIdGetter_PersonalSpaceId_Call) RunAndReturn(run func() string) *MockSpaceIdGetter_PersonalSpaceId_Call { - _c.Call.Return(run) - return _c -} - // TechSpaceId provides a mock function with given fields: func (_m *MockSpaceIdGetter) TechSpaceId() string { ret := _m.Called() diff --git a/core/syncstatus/spacesyncstatus/objectstate.go b/core/syncstatus/spacesyncstatus/objectstate.go index 7964d0e25..1b482db40 100644 --- a/core/syncstatus/spacesyncstatus/objectstate.go +++ b/core/syncstatus/spacesyncstatus/objectstate.go @@ -87,7 +87,10 @@ func (o *ObjectState) SetSyncStatusAndErr(status domain.SpaceSyncStatus, syncErr func (o *ObjectState) GetSyncStatus(spaceId string) domain.SpaceSyncStatus { o.Lock() defer o.Unlock() - return o.objectSyncStatusBySpace[spaceId] + if status, ok := o.objectSyncStatusBySpace[spaceId]; ok { + return status + } + return domain.Unknown } func (o *ObjectState) GetSyncObjectCount(spaceId string) int { diff --git a/core/syncstatus/spacesyncstatus/objectstate_test.go b/core/syncstatus/spacesyncstatus/objectstate_test.go index b6b442370..cfb2bfc46 100644 --- a/core/syncstatus/spacesyncstatus/objectstate_test.go +++ b/core/syncstatus/spacesyncstatus/objectstate_test.go @@ -55,7 +55,7 @@ func TestObjectState_GetSyncStatus(t *testing.T) { syncStatus := objectState.GetSyncStatus("spaceId") // then - assert.Equal(t, domain.Synced, syncStatus) + assert.Equal(t, domain.Unknown, syncStatus) }) } diff --git a/core/syncstatus/spacesyncstatus/spacestatus.go b/core/syncstatus/spacesyncstatus/spacestatus.go index dcd62a4bb..42d866945 100644 --- a/core/syncstatus/spacesyncstatus/spacestatus.go +++ b/core/syncstatus/spacesyncstatus/spacestatus.go @@ -26,7 +26,6 @@ type Updater interface { type SpaceIdGetter interface { app.Component TechSpaceId() string - PersonalSpaceId() string AllSpaceIds() []string } @@ -89,7 +88,7 @@ func (s *spaceSyncStatus) Run(ctx context.Context) (err error) { close(s.finish) return } else { - s.sendStartEvent(s.spaceIdGetter.PersonalSpaceId()) + s.sendStartEvent(s.spaceIdGetter.AllSpaceIds()) } s.ctx, s.ctxCancel = context.WithCancel(context.Background()) go s.processEvents() @@ -106,14 +105,16 @@ func (s *spaceSyncStatus) sendEventToSession(spaceId, token string) { }) } -func (s *spaceSyncStatus) sendStartEvent(spaceId string) { - s.eventSender.Broadcast(&pb.Event{ - Messages: []*pb.EventMessage{{ - Value: &pb.EventMessageValueOfSpaceSyncStatusUpdate{ - SpaceSyncStatusUpdate: s.makeSpaceSyncEvent(spaceId), - }, - }}, - }) +func (s *spaceSyncStatus) sendStartEvent(spaceIds []string) { + for _, id := range spaceIds { + s.eventSender.Broadcast(&pb.Event{ + Messages: []*pb.EventMessage{{ + Value: &pb.EventMessageValueOfSpaceSyncStatusUpdate{ + SpaceSyncStatusUpdate: s.makeSpaceSyncEvent(id), + }, + }}, + }) + } } func (s *spaceSyncStatus) sendLocalOnlyEvent() { @@ -183,7 +184,11 @@ func (s *spaceSyncStatus) isStatusNotChanged(status *domain.SpaceSync) bool { return false } syncErrNotChanged := s.getError(status.SpaceId) == mapError(status.SyncError) - statusNotChanged := s.getSpaceSyncStatus(status.SpaceId) == status.Status + syncStatus := s.getSpaceSyncStatus(status.SpaceId) + if syncStatus == domain.Unknown { + return false + } + statusNotChanged := syncStatus == status.Status if syncErrNotChanged && statusNotChanged { return true } @@ -221,6 +226,9 @@ func (s *spaceSyncStatus) getSpaceSyncStatus(spaceId string) domain.SpaceSyncSta filesStatus := s.filesState.GetSyncStatus(spaceId) objectsStatus := s.objectsState.GetSyncStatus(spaceId) + if s.isUnknown(filesStatus, objectsStatus) { + return domain.Unknown + } if s.isOfflineStatus(filesStatus, objectsStatus) { return domain.Offline } @@ -276,6 +284,10 @@ func (s *spaceSyncStatus) getError(spaceId string) pb.EventSpaceSyncError { return pb.EventSpace_Null } +func (s *spaceSyncStatus) isUnknown(filesStatus domain.SpaceSyncStatus, objectsStatus domain.SpaceSyncStatus) bool { + return filesStatus == domain.Unknown && objectsStatus == domain.Unknown +} + func mapNetworkMode(mode pb.RpcAccountNetworkMode) pb.EventSpaceNetwork { switch mode { case pb.RpcAccount_LocalOnly: diff --git a/core/syncstatus/spacesyncstatus/spacestatus_test.go b/core/syncstatus/spacesyncstatus/spacestatus_test.go index 9c723824f..4d25a5e9f 100644 --- a/core/syncstatus/spacesyncstatus/spacestatus_test.go +++ b/core/syncstatus/spacesyncstatus/spacestatus_test.go @@ -42,7 +42,7 @@ func TestSpaceSyncStatus_Init(t *testing.T) { // then assert.Nil(t, err) - space.EXPECT().PersonalSpaceId().Return("personalId") + space.EXPECT().AllSpaceIds().Return([]string{"personalId"}) eventSender.EXPECT().Broadcast(&pb.Event{ Messages: []*pb.EventMessage{{ Value: &pb.EventMessageValueOfSpaceSyncStatusUpdate{ @@ -380,10 +380,38 @@ func TestSpaceSyncStatus_updateSpaceSyncStatus(t *testing.T) { // when assert.Equal(t, domain.Synced, status.objectsState.GetSyncStatus("spaceId")) assert.Equal(t, 0, status.objectsState.GetSyncObjectCount("spaceId")) - assert.Equal(t, domain.Synced, status.filesState.GetSyncStatus("spaceId")) + assert.Equal(t, domain.Unknown, status.filesState.GetSyncStatus("spaceId")) assert.Equal(t, 0, status.filesState.GetSyncObjectCount("spaceId")) assert.Equal(t, domain.Synced, status.getSpaceSyncStatus(syncStatus.SpaceId)) }) + t.Run("send initial synced event", func(t *testing.T) { + // given + eventSender := mock_event.NewMockSender(t) + status := spaceSyncStatus{ + eventSender: eventSender, + networkConfig: &config.Config{NetworkMode: pb.RpcAccount_CustomConfig}, + batcher: mb.New[*domain.SpaceSync](0), + filesState: NewFileState(objectstore.NewStoreFixture(t)), + objectsState: NewObjectState(objectstore.NewStoreFixture(t)), + } + eventSender.EXPECT().Broadcast(&pb.Event{ + Messages: []*pb.EventMessage{{ + Value: &pb.EventMessageValueOfSpaceSyncStatusUpdate{ + SpaceSyncStatusUpdate: &pb.EventSpaceSyncStatusUpdate{ + Id: "spaceId", + Status: pb.EventSpace_Synced, + Network: pb.EventSpace_SelfHost, + Error: pb.EventSpace_Null, + SyncingObjectsCounter: 0, + }, + }, + }}, + }) + // then + syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, domain.Null, domain.Objects) + status.updateSpaceSyncStatus(syncStatus) + }) + t.Run("not send not needed synced event", func(t *testing.T) { // given eventSender := mock_event.NewMockSender(t) @@ -394,8 +422,10 @@ func TestSpaceSyncStatus_updateSpaceSyncStatus(t *testing.T) { filesState: NewFileState(objectstore.NewStoreFixture(t)), objectsState: NewObjectState(objectstore.NewStoreFixture(t)), } - // then + status.objectsState.SetSyncStatusAndErr(domain.Synced, domain.Null, "spaceId") + status.filesState.SetSyncStatusAndErr(domain.Synced, domain.Null, "spaceId") + // then syncStatus := domain.MakeSyncStatus("spaceId", domain.Synced, domain.Null, domain.Objects) status.updateSpaceSyncStatus(syncStatus) diff --git a/pkg/lib/core/smartblock/smartblock.go b/pkg/lib/core/smartblock/smartblock.go index f8a7e4bd8..9c2a71292 100644 --- a/pkg/lib/core/smartblock/smartblock.go +++ b/pkg/lib/core/smartblock/smartblock.go @@ -68,7 +68,7 @@ func (sbt SmartBlockType) IsOneOf(sbts ...SmartBlockType) bool { // Indexable determines if the object of specific type need to be proceeded by the indexer in order to appear in sets func (sbt SmartBlockType) Indexable() (details, outgoingLinks bool) { switch sbt { - case SmartBlockTypeDate, SmartBlockTypeAccountOld, SmartBlockTypeArchive, SmartBlockTypeHome: + case SmartBlockTypeDate, SmartBlockTypeAccountOld, SmartBlockTypeArchive, SmartBlockTypeHome, SmartBlockTypeNotificationObject: return false, false case SmartBlockTypeWidget: return true, false