1
0
Fork 0
mirror of https://github.com/anyproto/anytype-heart.git synced 2025-06-11 10:18:28 +09:00

GO-2421 Merge branch 'main' of github.com:anyproto/anytype-heart into go-2421-add-details-to-response-of-blocklinkcreatewithobject

This commit is contained in:
AnastasiaShemyakinskaya 2024-02-18 20:34:03 +01:00
commit 8266bc64ef
No known key found for this signature in database
GPG key ID: CCD60ED83B103281
44 changed files with 1283 additions and 949 deletions

View file

@ -75,6 +75,7 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/localstore/objectstore"
"github.com/anyproto/anytype-heart/pkg/lib/logging"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/space/deletioncontroller"
"github.com/anyproto/anytype-heart/space/spacecore"
"github.com/anyproto/anytype-heart/space/spacecore/clientserver"
"github.com/anyproto/anytype-heart/space/spacecore/credentialprovider"
@ -228,6 +229,7 @@ func Bootstrap(a *app.App, components ...app.Component) {
Register(source.New()).
Register(spacefactory.New()).
Register(space.New()).
Register(deletioncontroller.New()).
Register(invitestore.New()).
Register(fileobject.New()).
Register(acl.New()).

View file

@ -4,11 +4,15 @@ import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
"github.com/go-chi/chi/v5"
"github.com/gogo/protobuf/jsonpb"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
"github.com/anyproto/anytype-heart/core/block/source"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/database"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
@ -19,10 +23,22 @@ import (
func (s *Service) DebugRouter(r chi.Router) {
r.Get("/objects", debug.JSONHandler(s.debugListObjects))
r.Get("/tree/{id}", debug.JSONHandler(s.debugTree))
r.Get("/objects_per_space/{spaceId}", debug.JSONHandler(s.debugListObjectsPerSpace))
r.Get("/objects/{id}", debug.JSONHandler(s.debugGetObject))
}
type debugTree struct {
Id string
Changes []debugChange
}
type debugChange struct {
Change json.RawMessage
Identity string
Timestamp string
}
type debugObject struct {
ID string
Details json.RawMessage
@ -84,6 +100,36 @@ func (s *Service) debugGetObject(req *http.Request) (debugObject, error) {
return s.getDebugObject(id)
}
func (s *Service) debugTree(req *http.Request) (debugTree, error) {
id := chi.URLParam(req, "id")
result := debugTree{
Id: id,
}
err := Do(s, id, func(sb smartblock.SmartBlock) error {
ot := sb.Tree()
return ot.IterateRoot(source.UnmarshalChange, func(change *objecttree.Change) bool {
change.Next = nil
raw, err := json.Marshal(change)
if err != nil {
log.Error("debug tree: marshal change", zap.Error(err))
return false
}
ts := time.Unix(change.Timestamp, 0)
ch := debugChange{
Change: raw,
Timestamp: ts.Format(time.RFC3339),
}
if change.Identity != nil {
ch.Identity = change.Identity.Account()
}
result.Changes = append(result.Changes, ch)
return true
})
})
return result, err
}
func (s *Service) getDebugObject(id string) (debugObject, error) {
var obj debugObject
err := Do(s, id, func(sb smartblock.SmartBlock) error {

View file

@ -36,6 +36,7 @@ func (p *participant) Init(ctx *smartblock.InitContext) (err error) {
template.InitTemplate(ctx.State,
template.WithEmpty,
template.WithTitle,
template.WithDescription,
template.WithFeaturedRelations,
template.WithAddedFeaturedRelation(bundle.RelationKeyType),
template.WithAddedFeaturedRelation(bundle.RelationKeyBacklinks),

View file

@ -604,7 +604,6 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
if lastModifiedFromState > 0 {
lastModified = time.Unix(lastModifiedFromState, 0)
}
s.SetLocalDetail(bundle.RelationKeyLastModifiedDate.String(), pbtypes.Int64(lastModified.Unix()))
if existingCreatedDate := pbtypes.GetInt64(s.LocalDetails(), bundle.RelationKeyCreatedDate.String()); existingCreatedDate == 0 || existingCreatedDate > lastModified.Unix() {
// this can happen if we don't have creation date in the root change
@ -612,9 +611,6 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
}
}
s.SetLocalDetail(bundle.RelationKeyLastModifiedBy.String(), pbtypes.String(sb.currentParticipantId))
s.SetLocalDetail(bundle.RelationKeyLastModifiedDate.String(), pbtypes.Int64(lastModified.Unix()))
sb.beforeStateApply(s)
if !keepInternalFlags {
@ -653,6 +649,11 @@ func (sb *smartBlock) Apply(s *state.State, flags ...ApplyFlag) (err error) {
return nil
}
pushChange := func() error {
if !sb.source.ReadOnly() {
// We can set details directly in object's state, they'll be indexed correctly
st.SetLocalDetail(bundle.RelationKeyLastModifiedBy.String(), pbtypes.String(sb.currentParticipantId))
st.SetLocalDetail(bundle.RelationKeyLastModifiedDate.String(), pbtypes.Int64(lastModified.Unix()))
}
fileDetailsKeys := st.FileRelationKeys()
var fileDetailsKeysFiltered []string
for _, ch := range changes {
@ -928,12 +929,21 @@ func (sb *smartBlock) injectCreationInfo(s *state.State) error {
if creatorIdentityObjectId != "" {
s.SetDetailAndBundledRelation(bundle.RelationKeyCreator, pbtypes.String(creatorIdentityObjectId))
} else {
// For derived objects we set current identity
s.SetDetailAndBundledRelation(bundle.RelationKeyCreator, pbtypes.String(sb.currentParticipantId))
}
if originalCreated := s.OriginalCreatedTimestamp(); originalCreated > 0 {
// means we have imported object, so we need to set original created date
s.SetDetailAndBundledRelation(bundle.RelationKeyCreatedDate, pbtypes.Float64(float64(originalCreated)))
s.SetDetailAndBundledRelation(bundle.RelationKeyAddedDate, pbtypes.Float64(float64(treeCreatedDate)))
// Only set AddedDate once because we have a side effect with treeCreatedDate:
// - When we import object, treeCreateDate is set to time.Now()
// - But after push it is changed to original modified date
// - So after account recovery we will get treeCreateDate = original modified date, which is not equal to AddedDate
if pbtypes.GetInt64(s.Details(), bundle.RelationKeyAddedDate.String()) == 0 {
s.SetDetailAndBundledRelation(bundle.RelationKeyAddedDate, pbtypes.Float64(float64(treeCreatedDate)))
}
} else {
s.SetDetailAndBundledRelation(bundle.RelationKeyCreatedDate, pbtypes.Float64(float64(treeCreatedDate)))
}

View file

@ -144,6 +144,7 @@ func (s *State) Merge(s2 *State) *State {
}
func (s *State) ApplyChange(changes ...*pb.ChangeContent) (err error) {
defer s.resetParentIdsCache()
for _, ch := range changes {
if err = s.applyChange(ch); err != nil {
return
@ -174,6 +175,7 @@ func (s *State) GetAndUnsetFileKeys() (keys []pb.ChangeFileKeys) {
// ApplyChangeIgnoreErr should be called with changes from the single pb.Change
func (s *State) ApplyChangeIgnoreErr(changes ...*pb.ChangeContent) {
defer s.resetParentIdsCache()
for _, ch := range changes {
if err := s.applyChange(ch); err != nil {
log.With("objectID", s.RootId()).Warnf("error while applying change %T: %v; ignore", ch.Value, err)
@ -389,15 +391,10 @@ func (s *State) changeBlockUpdate(update *pb.ChangeBlockUpdate) error {
}
func (s *State) changeBlockMove(move *pb.ChangeBlockMove) error {
ns := s.NewState()
for _, id := range move.Ids {
ns.Unlink(id)
s.Unlink(id)
}
if err := ns.InsertTo(move.TargetId, move.Position, move.Ids...); err != nil {
return err
}
_, _, err := ApplyStateFastOne(ns)
return err
return s.InsertTo(move.TargetId, move.Position, move.Ids...)
}
func (s *State) changeStoreKeySet(set *pb.ChangeStoreKeySet) error {

View file

@ -104,7 +104,7 @@ func (s *State) normalizeChildren(b simple.Block) {
m := b.Model()
for _, cid := range m.ChildrenIds {
if !s.Exists(cid) {
m.ChildrenIds = slice.RemoveMut(m.ChildrenIds, cid)
s.setChildrenIds(m, slice.RemoveMut(m.ChildrenIds, cid))
s.normalizeChildren(b)
return
}
@ -166,7 +166,7 @@ func (s *State) normalizeTree() (err error) {
if s.Pick(headerId) != nil && slice.FindPos(s.Pick(s.RootId()).Model().ChildrenIds, headerId) != 0 {
s.Unlink(headerId)
root := s.Get(s.RootId()).Model()
root.ChildrenIds = append([]string{headerId}, root.ChildrenIds...)
s.setChildrenIds(root, append([]string{headerId}, root.ChildrenIds...))
}
return nil
}
@ -229,9 +229,9 @@ func (s *State) wrapChildrenToDiv(id string) (nextId string) {
}
div := s.newDiv()
div.Model().ChildrenIds = parent.ChildrenIds
s.setChildrenIds(div.Model(), parent.ChildrenIds)
s.Add(div)
parent.ChildrenIds = []string{div.Model().Id}
s.setChildrenIds(parent, []string{div.Model().Id})
return parent.Id
}
@ -245,9 +245,8 @@ func (s *State) divBalance(d1, d2 *model.Block) (overflow bool) {
d1ChildrenIds := make([]string, div)
copy(d1ChildrenIds, d1.ChildrenIds[:div])
d2.ChildrenIds = d1.ChildrenIds[div:]
d1.ChildrenIds = d1ChildrenIds
s.setChildrenIds(d2, d1.ChildrenIds[div:])
s.setChildrenIds(d1, d1ChildrenIds)
return len(d2.ChildrenIds) > maxChildrenThreshold
}
@ -305,7 +304,7 @@ func (s *State) removeDuplicates() {
idx = idx - i
chIds = append(chIds[:idx], chIds[idx+1:]...)
}
b.Model().ChildrenIds = chIds
s.setChildrenIds(b.Model(), chIds)
}
handledBlocks[b.Model().Id] = struct{}{}
return true
@ -384,7 +383,7 @@ func CleanupLayouts(s *State) (removedCount int) {
}
}
}
b.Model().ChildrenIds = result
s.setChildrenIds(b.Model(), result)
return
}
cleanup(s.RootId())

View file

@ -13,7 +13,6 @@ import (
"golang.org/x/exp/slices"
"github.com/anyproto/anytype-heart/core/block/simple"
"github.com/anyproto/anytype-heart/core/block/simple/base"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/util/pbtypes"
@ -57,37 +56,6 @@ func TestState_Normalize(t *testing.T) {
assert.Empty(t, hist)
})
t.Run("lastmodifieddate should not change", func(t *testing.T) {
r := NewDoc("1", map[string]simple.Block{
"1": base.NewBase(&model.Block{Id: "1"}),
})
r.(*State).SetLastModified(1, "abc")
s := r.NewState()
s.Add(simple.New(&model.Block{Id: "2"}))
s.InsertTo("1", model.Block_Inner, "2")
s.SetLastModified(2, "abc")
msgs, hist, err := ApplyState(s, true)
require.NoError(t, err)
assert.Len(t, msgs, 3) // BlockSetChildrenIds, BlockAdd, ObjectDetailsAmend(lastmodifieddate)
assert.Len(t, s.changes, 1) // BlockCreate
assert.Len(t, hist.Add, 1)
assert.Len(t, hist.Change, 1)
assert.Nil(t, hist.Details)
s = s.NewState()
s.SetLastModified(3, "abc")
msgs, hist, err = ApplyState(s, true)
require.NoError(t, err)
assert.Len(t, msgs, 0) // last modified should be reverted and not msg should be produced
assert.Len(t, s.changes, 0)
assert.Len(t, hist.Add, 0)
assert.Len(t, hist.Change, 0)
assert.Nil(t, hist.Details)
})
t.Run("clean missing children", func(t *testing.T) {
r := NewDoc("root", map[string]simple.Block{
"root": simple.New(&model.Block{Id: "root", ChildrenIds: []string{"one"}}),
@ -279,7 +247,7 @@ func TestState_Normalize(t *testing.T) {
}
_, _, err = ApplyState(r.NewState(), true)
require.NoError(t, err)
//t.Log(r.String())
// t.Log(r.String())
root := r.Pick(ev.RootId).Model()
for _, childId := range root.ChildrenIds {
@ -361,7 +329,7 @@ func TestState_Normalize(t *testing.T) {
assert.Equal(t, len(expectedRemovedIDs), expectedRemovedIDsCount)
})
//t.Run("normalize size - big details", func(t *testing.T) {
// t.Run("normalize size - big details", func(t *testing.T) {
// //given
// blocks := map[string]simple.Block{
// "root": simple.New(&model.Block{
@ -379,9 +347,9 @@ func TestState_Normalize(t *testing.T) {
// //then
// assert.Less(t, blockSizeLimit, s.blocks["root"].Model().Size())
// assert.Error(t, err)
//})
// })
//
//t.Run("normalize size - big content", func(t *testing.T) {
// t.Run("normalize size - big content", func(t *testing.T) {
// //given
// blocks := map[string]simple.Block{
// "root": simple.New(&model.Block{
@ -399,9 +367,9 @@ func TestState_Normalize(t *testing.T) {
// //then
// assert.Less(t, blockSizeLimit, s.blocks["root"].Model().Size())
// assert.Error(t, err)
//})
// })
//
//t.Run("normalize size - no error", func(t *testing.T) {
// t.Run("normalize size - no error", func(t *testing.T) {
// //given
// blocks := map[string]simple.Block{
// "root": simple.New(&model.Block{
@ -422,7 +390,7 @@ func TestState_Normalize(t *testing.T) {
// //then
// assert.Less(t, s.blocks["root"].Model().Size(), blockSizeLimit)
// assert.NoError(t, err)
//})
// })
}
func TestCleanupLayouts(t *testing.T) {
@ -484,17 +452,17 @@ func BenchmarkNormalize(b *testing.B) {
func TestShortenDetailsToLimit(t *testing.T) {
t.Run("shorten description", func(t *testing.T) {
//given
// given
details := map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String("my page"),
bundle.RelationKeyDescription.String(): pbtypes.String(strings.Repeat("a", detailSizeLimit+10)),
bundle.RelationKeyWidthInPixels.String(): pbtypes.Int64(20),
}
//when
// when
shortenDetailsToLimit("", details)
//then
// then
assert.Len(t, details[bundle.RelationKeyName.String()].GetStringValue(), 7)
assert.Less(t, len(details[bundle.RelationKeyDescription.String()].GetStringValue()), detailSizeLimit)
})
@ -502,25 +470,25 @@ func TestShortenDetailsToLimit(t *testing.T) {
func TestShortenValueOnN(t *testing.T) {
t.Run("string value", func(t *testing.T) {
//given
// given
value := pbtypes.String("abrakadabra")
//when
// when
value, left := shortenValueByN(value, 7)
//then
// then
assert.Equal(t, 0, left)
assert.Equal(t, "abra", value.GetStringValue())
})
t.Run("string list", func(t *testing.T) {
//given
// given
value := pbtypes.StringList([]string{"Liberté", "Égalité", "Fraternité"})
//when
// when
value, left := shortenValueByN(value, 15)
//then
// then
expected := pbtypes.StringList([]string{"", "É", "Fraternité"})
assert.Equal(t, 0, left)
@ -528,13 +496,13 @@ func TestShortenValueOnN(t *testing.T) {
})
t.Run("cut off all strings", func(t *testing.T) {
//given
// given
value := pbtypes.StringList([]string{"😂", "😄", "🥰", "😔", "😰", "😥", "🥕", "🍅", "🌶"})
//when
// when
value, left := shortenValueByN(value, 100)
//then
// then
assert.Equal(t, 100-(9*4), left)
assert.Equal(t, 0, countStringsLength(value))
})

View file

@ -55,19 +55,20 @@ func (s *State) InsertTo(targetId string, reqPos model.BlockPosition, ids ...str
switch reqPos {
case model.Block_Bottom:
pos = targetPos + 1
targetParentM.ChildrenIds = slice.Insert(targetParentM.ChildrenIds, pos, ids...)
s.insertChildrenIds(targetParentM, pos, ids...)
case model.Block_Top:
pos = targetPos
targetParentM.ChildrenIds = slice.Insert(targetParentM.ChildrenIds, pos, ids...)
s.insertChildrenIds(targetParentM, pos, ids...)
case model.Block_Left, model.Block_Right:
if err = s.moveFromSide(target, s.Get(targetParentM.Id), reqPos, ids...); err != nil {
return
}
case model.Block_Inner:
target.Model().ChildrenIds = append(target.Model().ChildrenIds, ids...)
s.prependChildrenIds(target.Model(), ids...)
case model.Block_Replace:
pos = targetPos + 1
if len(ids) > 0 && len(s.Get(ids[0]).Model().ChildrenIds) == 0 {
id0Block := s.Get(ids[0]).Model()
if len(ids) > 0 && len(id0Block.ChildrenIds) == 0 {
var idsIsChild bool
if targetChild := target.Model().ChildrenIds; len(targetChild) > 0 {
for _, id := range ids {
@ -78,13 +79,13 @@ func (s *State) InsertTo(targetId string, reqPos model.BlockPosition, ids ...str
}
}
if !idsIsChild {
s.Get(ids[0]).Model().ChildrenIds = target.Model().ChildrenIds
s.setChildrenIds(id0Block, target.Model().ChildrenIds)
}
}
targetParentM.ChildrenIds = slice.Insert(targetParentM.ChildrenIds, pos, ids...)
s.insertChildrenIds(targetParentM, pos, ids...)
s.Unlink(target.Model().Id)
case model.Block_InnerFirst:
target.Model().ChildrenIds = append(ids, target.Model().ChildrenIds...)
s.appendChildrenIds(target.Model(), ids...)
default:
return fmt.Errorf("unexpected position")
}
@ -160,16 +161,7 @@ func (s *State) moveFromSide(target, parent simple.Block, pos model.BlockPositio
}
target = s.Get(row.Model().ChildrenIds[0])
}
column := simple.New(&model.Block{
Id: "cd-" + opId,
ChildrenIds: ids,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Column,
},
},
})
s.Add(column)
column := s.addNewColumn(opId, ids)
targetPos := slice.FindPos(row.Model().ChildrenIds, target.Model().Id)
if targetPos == -1 {
@ -180,12 +172,73 @@ func (s *State) moveFromSide(target, parent simple.Block, pos model.BlockPositio
if pos == model.Block_Right {
columnPos += 1
}
row.Model().ChildrenIds = slice.Insert(row.Model().ChildrenIds, columnPos, column.Model().Id)
s.insertChildrenIds(row.Model(), columnPos, column.Model().Id)
s.changesStructureIgnoreIds = append(s.changesStructureIgnoreIds, "cd-"+opId, "ct-"+opId, "r-"+opId, row.Model().Id)
return
}
func (s *State) wrapToRow(opId string, parent, b simple.Block) (row simple.Block, err error) {
column := s.addNewBlockAndWrapToColumn(opId, b)
row = s.addNewColumnToRow(opId, column)
pos := slice.FindPos(parent.Model().ChildrenIds, b.Model().Id)
if pos == -1 {
return nil, fmt.Errorf("creating row: can't find child[%s] in given parent[%s]", b.Model().Id, parent.Model().Id)
}
parent.Model().ChildrenIds[pos] = row.Model().Id
return
}
func (s *State) setChildrenIds(parent *model.Block, childrenIds []string) {
parent.ChildrenIds = childrenIds
if s.isParentIdsCacheEnabled {
cache := s.getParentIdsCache()
for _, childId := range childrenIds {
cache[childId] = parent.Id
}
}
}
func (s *State) prependChildrenIds(block *model.Block, ids ...string) {
s.setChildrenIds(block, append(block.ChildrenIds, ids...))
}
func (s *State) appendChildrenIds(block *model.Block, ids ...string) {
s.setChildrenIds(block, append(ids, block.ChildrenIds...))
}
func (s *State) insertChildrenIds(block *model.Block, pos int, ids ...string) {
s.setChildrenIds(block, slice.Insert(block.ChildrenIds, pos, ids...))
}
func (s *State) addNewColumn(opId string, ids []string) simple.Block {
column := simple.New(&model.Block{
Id: "cd-" + opId,
ChildrenIds: ids,
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Column,
},
},
})
s.Add(column)
return column
}
func (s *State) addNewColumnToRow(opId string, column simple.Block) simple.Block {
row := simple.New(&model.Block{
Id: "r-" + opId,
ChildrenIds: []string{column.Model().Id},
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Row,
},
},
})
s.Add(row)
return row
}
func (s *State) addNewBlockAndWrapToColumn(opId string, b simple.Block) simple.Block {
column := simple.New(&model.Block{
Id: "ct-" + opId,
ChildrenIds: []string{b.Model().Id},
@ -196,20 +249,5 @@ func (s *State) wrapToRow(opId string, parent, b simple.Block) (row simple.Block
},
})
s.Add(column)
row = simple.New(&model.Block{
Id: "r-" + opId,
ChildrenIds: []string{column.Model().Id},
Content: &model.BlockContentOfLayout{
Layout: &model.BlockContentLayout{
Style: model.BlockContentLayout_Row,
},
},
})
s.Add(row)
pos := slice.FindPos(parent.Model().ChildrenIds, b.Model().Id)
if pos == -1 {
return nil, fmt.Errorf("creating row: can't find child[%s] in given parent[%s]", b.Model().Id, parent.Model().Id)
}
parent.Model().ChildrenIds[pos] = row.Model().Id
return
return column
}

View file

@ -133,6 +133,8 @@ type State struct {
groupId string
noObjectType bool
originalCreatedTimestamp int64 // pass here from snapshots when importing objects
parentIdsCache map[string]string
isParentIdsCacheEnabled bool
}
func (s *State) MigrationVersion() uint32 {
@ -201,6 +203,7 @@ func (s *State) Add(b simple.Block) (ok bool) {
if s.Pick(id) == nil {
s.blocks[id] = b
s.blockInit(b)
s.setChildrenIds(b.Model(), b.Model().ChildrenIds)
return true
}
return false
@ -210,11 +213,21 @@ func (s *State) Set(b simple.Block) {
if !s.Exists(b.Model().Id) {
s.Add(b)
} else {
s.removeFromCache(s.Pick(b.Model().Id).Model().ChildrenIds...)
s.setChildrenIds(b.Model(), b.Model().ChildrenIds)
s.blocks[b.Model().Id] = b
s.blockInit(b)
}
}
func (s *State) removeFromCache(ids ...string) {
if s.isParentIdsCacheEnabled {
for _, id := range ids {
delete(s.parentIdsCache, id)
}
}
}
func (s *State) Get(id string) (b simple.Block) {
if b = s.blocks[id]; b != nil {
return
@ -269,6 +282,7 @@ func (s *State) Unlink(id string) (ok bool) {
if parent := s.GetParentOf(id); parent != nil {
parentM := parent.Model()
parentM.ChildrenIds = slice.RemoveMut(parentM.ChildrenIds, id)
s.removeFromCache(id)
return true
}
return
@ -308,6 +322,13 @@ func (s *State) HasParent(id, parentId string) bool {
}
func (s *State) PickParentOf(id string) (res simple.Block) {
if s.isParentIdsCacheEnabled {
if parentId, ok := s.getParentIdsCache()[id]; ok {
return s.Pick(parentId)
}
return
}
s.Iterate(func(b simple.Block) bool {
if slice.FindPos(b.Model().ChildrenIds, id) != -1 {
res = b
@ -318,6 +339,24 @@ func (s *State) PickParentOf(id string) (res simple.Block) {
return
}
func (s *State) resetParentIdsCache() {
s.parentIdsCache = nil
s.isParentIdsCacheEnabled = false
}
func (s *State) getParentIdsCache() map[string]string {
if s.parentIdsCache == nil {
s.parentIdsCache = make(map[string]string)
s.Iterate(func(block simple.Block) bool {
for _, id := range block.Model().ChildrenIds {
s.parentIdsCache[id] = block.Model().Id
}
return true
})
}
return s.parentIdsCache
}
func (s *State) IsChild(parentId, childId string) bool {
for {
parent := s.PickParentOf(childId)
@ -422,6 +461,7 @@ func ApplyStateFastOne(s *State) (msgs []simple.EventMessage, action undo.Action
}
func (s *State) apply(fast, one, withLayouts bool) (msgs []simple.EventMessage, action undo.Action, err error) {
defer s.resetParentIdsCache()
if s.parent != nil && (s.parent.parent != nil || fast) {
s.intermediateApply()
if one {
@ -648,24 +688,6 @@ func (s *State) apply(fast, one, withLayouts bool) (msgs []simple.EventMessage,
s.parent.relationLinks = s.relationLinks
}
if len(msgs) == 0 && action.IsEmpty() && s.parent != nil {
// revert lastModified update if we don't have any actual changes being made
prevModifiedDate := pbtypes.Get(s.parent.LocalDetails(), bundle.RelationKeyLastModifiedDate.String())
prevModifiedBy := pbtypes.Get(s.parent.LocalDetails(), bundle.RelationKeyLastModifiedBy.String())
if s.localDetails != nil {
if _, isNull := prevModifiedDate.GetKind().(*types.Value_NullValue); prevModifiedDate == nil || isNull {
log.With("objectID", s.rootId).Debugf("failed to revert prev modifed date: prev date is nil")
} else {
s.localDetails.Fields[bundle.RelationKeyLastModifiedDate.String()] = prevModifiedDate
}
if _, isNull := prevModifiedBy.GetKind().(*types.Value_NullValue); prevModifiedBy == nil || isNull {
log.With("objectID", s.rootId).Debugf("failed to revert prev modifed by: prev value is nil")
} else {
s.localDetails.Fields[bundle.RelationKeyLastModifiedBy.String()] = prevModifiedBy
}
}
}
if s.parent != nil && s.localDetails != nil {
prev := s.parent.LocalDetails()
if diff := pbtypes.StructDiff(prev, s.localDetails); diff != nil {
@ -693,7 +715,6 @@ func (s *State) apply(fast, one, withLayouts bool) (msgs []simple.EventMessage,
}
msgs = s.processTrailingDuplicatedEvents(msgs)
log.Debugf("middle: state apply: %d affected; %d for remove; %d copied; %d changes; for a %v", len(affectedIds), len(toRemove), len(s.blocks), len(s.changes), time.Since(st))
return
}

View file

@ -27,6 +27,9 @@ func (s *service) InstallBundledObjects(
sourceObjectIds []string,
isNewSpace bool,
) (ids []string, objects []*types.Struct, err error) {
if space.IsReadOnly() {
return
}
marketplaceSpace, err := s.spaceService.Get(ctx, addr.AnytypeMarketplaceWorkspace)
if err != nil {

View file

@ -64,6 +64,7 @@ type treeSyncer struct {
headPools map[string]*executor
treeManager treemanager.TreeManager
isRunning bool
isSyncing bool
}
func NewTreeSyncer(spaceId string) treesyncer.TreeSyncer {
@ -80,6 +81,7 @@ func NewTreeSyncer(spaceId string) treesyncer.TreeSyncer {
}
func (t *treeSyncer) Init(a *app.App) (err error) {
t.isSyncing = true
t.treeManager = app.MustComponent[treemanager.TreeManager](a)
return nil
}
@ -119,6 +121,19 @@ func (t *treeSyncer) StartSync() {
}
}
func (t *treeSyncer) StopSync() {
t.Lock()
defer t.Unlock()
t.isRunning = false
t.isSyncing = false
}
func (t *treeSyncer) ShouldSync(peerId string) bool {
t.Lock()
defer t.Unlock()
return t.isSyncing
}
func (t *treeSyncer) SyncAll(ctx context.Context, peerId string, existing, missing []string) error {
t.Lock()
defer t.Unlock()

View file

@ -1,97 +0,0 @@
package source
import (
"context"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/block/editor/state"
"github.com/anyproto/anytype-heart/pb"
"github.com/anyproto/anytype-heart/pkg/lib/bundle"
"github.com/anyproto/anytype-heart/pkg/lib/core/smartblock"
)
type identityService interface {
SpaceId() string
GetDetails(ctx context.Context, identity string) (details *types.Struct, err error)
}
func NewIdentity(identityService identityService, id string) (s Source) {
ctx, cancel := context.WithCancel(context.Background())
return &identity{
identityService: identityService,
id: id,
closingCtx: ctx,
closingCtxFunc: cancel,
}
}
type identity struct {
identityService identityService
closingCtx context.Context
closingCtxFunc context.CancelFunc
id string
}
func (v *identity) ListIds() ([]string, error) {
// todo: later
return []string{}, nil
}
func (v *identity) ReadOnly() bool {
return true
}
func (v *identity) Id() string {
return v.id
}
func (v *identity) SpaceID() string {
return v.identityService.SpaceId()
}
func (v *identity) Type() smartblock.SmartBlockType {
return smartblock.SmartBlockTypeIdentity
}
func (v *identity) detailsToState(details *types.Struct) (doc state.Doc) {
t := state.NewDoc(v.id, nil).(*state.State)
t.SetObjectTypeKey(bundle.TypeKeyProfile)
t.SetDetails(details)
return t
}
func (v *identity) ReadDoc(ctx context.Context, receiver ChangeReceiver, empty bool) (doc state.Doc, err error) {
details, err := v.identityService.GetDetails(ctx, v.id)
if err != nil {
return nil, err
}
return v.detailsToState(details), nil
}
func (v *identity) ReadMeta(ctx context.Context, r ChangeReceiver) (doc state.Doc, err error) {
return v.ReadDoc(ctx, r, false)
}
func (v *identity) Close() (err error) {
v.closingCtxFunc()
return
}
func (v *identity) Heads() []string {
return []string{"todo"}
}
func (s *identity) GetFileKeysSnapshot() []*pb.ChangeFileKeys {
return nil
}
func (s *identity) PushChange(params PushChangeParams) (id string, err error) {
return
}
func (s *identity) GetCreationInfo() (creatorObjectId string, createdDate int64, err error) {
return s.id, 0, nil
}

View file

@ -6,7 +6,9 @@ import (
"strings"
"sync"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
"github.com/gogo/protobuf/types"
"github.com/anyproto/any-sync/accountservice"
@ -65,7 +67,6 @@ type service struct {
spaceCoreService spacecore.SpaceCoreService
storageService storage.ClientStorage
fileService files.Service
identityService identityService
objectStore objectstore.ObjectStore
fileObjectMigrator fileObjectMigrator
@ -82,7 +83,6 @@ func (s *service) Init(a *app.App) (err error) {
s.fileStore = app.MustComponent[filestore.FileStore](a)
s.spaceCoreService = app.MustComponent[spacecore.SpaceCoreService](a)
s.storageService = a.MustComponent(spacestorage.CName).(storage.ClientStorage)
s.identityService = app.MustComponent[identityService](a)
s.fileService = app.MustComponent[files.Service](a)
s.objectStore = app.MustComponent[objectstore.ObjectStore](a)
@ -102,7 +102,10 @@ type BuildOptions struct {
func (b *BuildOptions) BuildTreeOpts() objecttreebuilder.BuildTreeOpts {
return objecttreebuilder.BuildTreeOpts{
Listener: b.Listener,
TreeBuilder: objecttree.BuildKeyVerifiableObjectTree,
TreeBuilder: objecttree.BuildKeyFilterableObjectTree,
TreeValidator: func(payload treestorage.TreeStorageCreatePayload, buildFunc objecttree.BuildObjectTreeFunc, aclList list.AclList) (retPayload treestorage.TreeStorageCreatePayload, err error) {
return objecttree.ValidateFilterRawTree(payload, aclList)
},
}
}
@ -134,8 +137,6 @@ func (s *service) newSource(ctx context.Context, space Space, id string, buildOp
return NewBundledObjectType(id), nil
case smartblock.SmartBlockTypeBundledRelation:
return NewBundledRelation(id), nil
case smartblock.SmartBlockTypeIdentity:
return NewIdentity(s.identityService, id), nil
case smartblock.SmartBlockTypeParticipant:
participantState := state.NewDoc(id, nil).(*state.State)
// Set object type here in order to derive value of Type relation in smartblock.Init

View file

@ -599,7 +599,6 @@ func BuildState(spaceId string, initState *state.State, ot objecttree.ReadableOb
}
if lastChange != nil && !st.IsTheHeaderChange() {
// todo: why do we don't need to set last modified for the header change?
st.SetLastModified(lastChange.Timestamp, domain.NewParticipantId(spaceId, lastChange.Identity.Account()))
}
st.SetMigrationVersion(lastMigrationVersion)

View file

@ -10,31 +10,28 @@ import (
)
type StaticSourceParams struct {
Id domain.FullID
SbType smartblock.SmartBlockType
State *state.State
CreatorId string
PushChange func(p PushChangeParams) (string, error)
Id domain.FullID
SbType smartblock.SmartBlockType
State *state.State
CreatorId string
}
func (s *service) NewStaticSource(params StaticSourceParams) SourceWithType {
return &static{
id: params.Id,
sbType: params.SbType,
doc: params.State,
s: s,
creatorId: params.CreatorId,
pushChange: params.PushChange,
id: params.Id,
sbType: params.SbType,
doc: params.State,
s: s,
creatorId: params.CreatorId,
}
}
type static struct {
id domain.FullID
sbType smartblock.SmartBlockType
doc *state.State
creatorId string
pushChange func(p PushChangeParams) (string, error)
s *service
id domain.FullID
sbType smartblock.SmartBlockType
doc *state.State
creatorId string
s *service
}
func (s *static) Id() string {
@ -50,7 +47,7 @@ func (s *static) Type() smartblock.SmartBlockType {
}
func (s *static) ReadOnly() bool {
return s.pushChange == nil
return true
}
func (s *static) ReadDoc(ctx context.Context, receiver ChangeReceiver, empty bool) (doc state.Doc, err error) {
@ -58,9 +55,6 @@ func (s *static) ReadDoc(ctx context.Context, receiver ChangeReceiver, empty boo
}
func (s *static) PushChange(params PushChangeParams) (id string, err error) {
if s.pushChange != nil {
return s.pushChange(params)
}
return
}

View file

@ -27,7 +27,6 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/pb/model"
"github.com/anyproto/anytype-heart/space"
"github.com/anyproto/anytype-heart/util/pbtypes"
"github.com/anyproto/anytype-heart/util/slice"
)
const CName = "history"
@ -75,25 +74,25 @@ func (h *history) Show(id domain.FullID, versionID string) (bs *model.ObjectView
if err != nil {
return
}
s.SetDetailAndBundledRelation(bundle.RelationKeyId, pbtypes.String(id.ObjectID))
s.SetDetailAndBundledRelation(bundle.RelationKeySpaceId, pbtypes.String(id.SpaceID))
typeId, err := space.GetTypeIdByKey(context.Background(), s.ObjectTypeKey())
if err != nil {
return nil, nil, fmt.Errorf("get type id by key: %w", err)
}
s.SetDetailAndBundledRelation(bundle.RelationKeyType, pbtypes.String(typeId))
dependentObjectIDs := objectlink.DependentObjectIDs(s, space, true, true, false, true, false)
// nolint:errcheck
metaD, _ := h.objectStore.QueryByID(dependentObjectIDs)
details := make([]*model.ObjectViewDetailsSet, 0, len(metaD))
metaD = append(metaD, database.Record{Details: s.CombinedDetails()})
uniqueObjTypes := s.ObjectTypeKeys()
for _, m := range metaD {
details = append(details, &model.ObjectViewDetailsSet{
Id: pbtypes.GetString(m.Details, bundle.RelationKeyId.String()),
Details: m.Details,
})
if typeKey := domain.TypeKey(pbtypes.GetString(m.Details, bundle.RelationKeyType.String())); typeKey != "" {
if slice.FindPos(uniqueObjTypes, typeKey) == -1 {
// todo: what is happening here?
uniqueObjTypes = append(uniqueObjTypes, typeKey)
}
}
}
relations, err := h.objectStore.FetchRelationByLinks(id.SpaceID, s.PickRelationLinks())
@ -113,10 +112,6 @@ func (h *history) Versions(id domain.FullID, lastVersionId string, limit int) (r
if limit <= 0 {
limit = 100
}
profileId, profileName, err := h.getProfileInfo(id.SpaceID)
if err != nil {
return
}
var includeLastId = true
reverse := func(vers []*pb.RpcHistoryVersion) []*pb.RpcHistoryVersion {
@ -134,11 +129,11 @@ func (h *history) Versions(id domain.FullID, lastVersionId string, limit int) (r
var data []*pb.RpcHistoryVersion
e = tree.IterateFrom(tree.Root().Id, source.UnmarshalChange, func(c *objecttree.Change) (isContinue bool) {
participantId := domain.NewParticipantId(id.SpaceID, c.Identity.Account())
data = append(data, &pb.RpcHistoryVersion{
Id: c.Id,
PreviousIds: c.PreviousIds,
AuthorId: profileId,
AuthorName: profileName,
AuthorId: participantId,
Time: c.Timestamp,
})
return true
@ -228,28 +223,13 @@ func (h *history) buildState(id domain.FullID, versionId string) (st *state.Stat
st.BlocksInit(st)
if ch, e := tree.GetChange(versionId); e == nil {
profileId, profileName, e := h.getProfileInfo(id.SpaceID)
if e != nil {
err = e
return
}
participantId := domain.NewParticipantId(id.SpaceID, ch.Identity.Account())
ver = &pb.RpcHistoryVersion{
Id: ch.Id,
PreviousIds: ch.PreviousIds,
AuthorId: profileId,
AuthorName: profileName,
AuthorId: participantId,
Time: ch.Timestamp,
}
}
return
}
func (h *history) getProfileInfo(spaceId string) (profileId, profileName string, err error) {
profileId = h.accountService.MyParticipantId(spaceId)
lp, err := h.accountService.ProfileInfo()
if err != nil {
return
}
profileName = lp.Name
return
}

View file

@ -7,7 +7,6 @@ import (
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/identityrepo/identityrepoproto"
"github.com/anyproto/any-sync/util/crypto"
"github.com/dgraph-io/badger/v4"
@ -47,10 +46,6 @@ type Service interface {
// UnregisterIdentitiesInSpace removes all identity observers in the space
UnregisterIdentitiesInSpace(spaceId string)
// GetDetails returns the last store details of the identity and provides a way to receive updates via updateHook
GetDetails(ctx context.Context, identity string) (details *types.Struct, err error)
// SpaceId returns the spaceId used to store the identities in the objectStore
SpaceId() string
app.ComponentRunnable
}
@ -63,19 +58,25 @@ type observer struct {
initialized bool
}
type identityRepoClient interface {
app.Component
IdentityRepoPut(ctx context.Context, identity string, data []*identityrepoproto.Data) (err error)
IdentityRepoGet(ctx context.Context, identities []string, kinds []string) (res []*identityrepoproto.DataWithIdentity, err error)
}
type service struct {
dbProvider datastore.Datastore
db *badger.DB
spaceService space.Service
objectStore objectstore.ObjectStore
accountService account.Service
spaceIdDeriver spaceIdDeriver
coordinatorClient coordinatorclient.CoordinatorClient
fileAclService fileacl.Service
closing chan struct{}
startedCh chan struct{}
techSpaceId string
personalSpaceId string
dbProvider datastore.Datastore
db *badger.DB
spaceService space.Service
objectStore objectstore.ObjectStore
accountService account.Service
spaceIdDeriver spaceIdDeriver
identityRepoClient identityRepoClient
fileAclService fileacl.Service
closing chan struct{}
startedCh chan struct{}
techSpaceId string
personalSpaceId string
myIdentity string
currentProfileDetailsLock sync.RWMutex
@ -110,7 +111,7 @@ func (s *service) Init(a *app.App) (err error) {
s.accountService = app.MustComponent[account.Service](a)
s.spaceIdDeriver = app.MustComponent[spaceIdDeriver](a)
s.spaceService = app.MustComponent[space.Service](a)
s.coordinatorClient = app.MustComponent[coordinatorclient.CoordinatorClient](a)
s.identityRepoClient = app.MustComponent[identityRepoClient](a)
s.fileAclService = app.MustComponent[fileacl.Service](a)
s.dbProvider = app.MustComponent[datastore.Datastore](a)
return
@ -152,51 +153,6 @@ func (s *service) Close(ctx context.Context) (err error) {
return nil
}
func (s *service) SpaceId() string {
return s.techSpaceId
}
func (s *service) GetDetails(ctx context.Context, profileId string) (details *types.Struct, err error) {
rec, err := s.objectStore.GetDetails(profileId)
if err != nil {
return nil, err
}
return rec.Details, nil
}
func getDetailsFromProfile(id, spaceId string, details *types.Struct) *types.Struct {
name := pbtypes.GetString(details, bundle.RelationKeyName.String())
description := pbtypes.GetString(details, bundle.RelationKeyDescription.String())
image := pbtypes.GetString(details, bundle.RelationKeyIconImage.String())
profileId := pbtypes.GetString(details, bundle.RelationKeyId.String())
d := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(name),
bundle.RelationKeyDescription.String(): pbtypes.String(description),
bundle.RelationKeyId.String(): pbtypes.String(id),
bundle.RelationKeyIsReadonly.String(): pbtypes.Bool(true),
bundle.RelationKeyIsArchived.String(): pbtypes.Bool(false),
bundle.RelationKeyIsHidden.String(): pbtypes.Bool(false),
bundle.RelationKeySpaceId.String(): pbtypes.String(spaceId),
bundle.RelationKeyType.String(): pbtypes.String(bundle.TypeKeyProfile.BundledURL()),
bundle.RelationKeyIdentityProfileLink.String(): pbtypes.String(profileId),
bundle.RelationKeyLayout.String(): pbtypes.Float64(float64(model.ObjectType_profile)),
bundle.RelationKeyLastModifiedBy.String(): pbtypes.String(id),
}}
if image != "" {
d.Fields[bundle.RelationKeyIconImage.String()] = pbtypes.String(image)
}
// deprecated, but we have existing profiles which use this, so let's it be up for clients to decide either to render it or not
iconOption := pbtypes.Get(details, bundle.RelationKeyIconOption.String())
if iconOption != nil {
d.Fields[bundle.RelationKeyIconOption.String()] = iconOption
}
return d
}
func (s *service) runLocalProfileSubscriptions(ctx context.Context) (err error) {
uniqueKey, err := domain.NewUniqueKey(coresb.SmartBlockTypeProfilePage, "")
if err != nil {
@ -290,9 +246,10 @@ func (s *service) cacheProfileDetails(details *types.Struct) {
s.currentProfileDetailsLock.Unlock()
identityProfile := &model.IdentityProfile{
Identity: s.myIdentity,
Name: pbtypes.GetString(details, bundle.RelationKeyName.String()),
IconCid: pbtypes.GetString(details, bundle.RelationKeyIconImage.String()),
Identity: s.myIdentity,
Name: pbtypes.GetString(details, bundle.RelationKeyName.String()),
Description: pbtypes.GetString(details, bundle.RelationKeyDescription.String()),
IconCid: pbtypes.GetString(details, bundle.RelationKeyIconImage.String()),
}
observers, ok := s.identityObservers[s.myIdentity]
if ok {
@ -323,6 +280,7 @@ func (s *service) pushProfileToIdentityRegistry(ctx context.Context) error {
identityProfile := &model.IdentityProfile{
Identity: identity,
Name: pbtypes.GetString(s.currentProfileDetails, bundle.RelationKeyName.String()),
Description: pbtypes.GetString(s.currentProfileDetails, bundle.RelationKeyDescription.String()),
IconCid: iconCid,
IconEncryptionKeys: iconEncryptionKeys,
}
@ -341,7 +299,7 @@ func (s *service) pushProfileToIdentityRegistry(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to sign profile data: %w", err)
}
err = s.coordinatorClient.IdentityRepoPut(ctx, identity, []*identityrepoproto.Data{
err = s.identityRepoClient.IdentityRepoPut(ctx, identity, []*identityrepoproto.Data{
{
Kind: "profile",
Data: data,
@ -409,7 +367,7 @@ func (s *service) observeIdentities(ctx context.Context) error {
}
func (s *service) getIdentitiesDataFromRepo(ctx context.Context, identities []string) ([]*identityrepoproto.DataWithIdentity, error) {
res, err := s.coordinatorClient.IdentityRepoGet(ctx, identities, []string{identityRepoDataKind})
res, err := s.identityRepoClient.IdentityRepoGet(ctx, identities, []string{identityRepoDataKind})
if err == nil {
return res, nil
}

View file

@ -8,13 +8,11 @@ import (
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/coordinator/coordinatorclient/mock_coordinatorclient"
"github.com/anyproto/any-sync/identityrepo/identityrepoproto"
"github.com/anyproto/any-sync/util/crypto"
"github.com/gogo/protobuf/proto"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"github.com/anyproto/anytype-heart/core/anytype/account/mock_account"
"github.com/anyproto/anytype-heart/core/files/fileacl/mock_fileacl"
@ -28,16 +26,15 @@ import (
type fixture struct {
*service
coordinatorClient *mock_coordinatorclient.MockCoordinatorClient
coordinatorClient *inMemoryIdentityRepo
}
const testObserverPeriod = 5 * time.Millisecond
func newFixture(t *testing.T) *fixture {
ctrl := gomock.NewController(t)
ctx := context.Background()
coordinatorClient := mock_coordinatorclient.NewMockCoordinatorClient(ctrl)
identityRepoClient := newInMemoryIdentityRepo()
objectStore := objectstore.NewStoreFixture(t)
accountService := mock_account.NewMockService(t)
spaceService := mock_space.NewMockService(t)
@ -50,7 +47,7 @@ func newFixture(t *testing.T) *fixture {
a.Register(&spaceIdDeriverStub{})
a.Register(dataStore)
a.Register(objectStore)
a.Register(testutil.PrepareMock(ctx, a, coordinatorClient))
a.Register(identityRepoClient)
a.Register(testutil.PrepareMock(ctx, a, accountService))
a.Register(testutil.PrepareMock(ctx, a, spaceService))
a.Register(testutil.PrepareMock(ctx, a, fileAclService))
@ -68,7 +65,7 @@ func newFixture(t *testing.T) *fixture {
svcRef.db = db
fx := &fixture{
service: svcRef,
coordinatorClient: coordinatorClient,
coordinatorClient: identityRepoClient,
}
go fx.observeIdentitiesLoop()
@ -83,13 +80,67 @@ func marshalProfile(t *testing.T, profile *model.IdentityProfile, key crypto.Sym
return data
}
type inMemoryIdentityRepo struct {
lock sync.Mutex
isUnavailable bool
identitiesData map[string]*identityrepoproto.DataWithIdentity
}
func newInMemoryIdentityRepo() *inMemoryIdentityRepo {
return &inMemoryIdentityRepo{
identitiesData: make(map[string]*identityrepoproto.DataWithIdentity),
}
}
func (d *inMemoryIdentityRepo) Init(a *app.App) (err error) {
return nil
}
func (d *inMemoryIdentityRepo) Name() (name string) {
return "inMemoryIdentityRepo"
}
func (d *inMemoryIdentityRepo) setUnavailable() {
d.lock.Lock()
defer d.lock.Unlock()
d.isUnavailable = true
}
func (d *inMemoryIdentityRepo) IdentityRepoPut(ctx context.Context, identity string, data []*identityrepoproto.Data) (err error) {
d.lock.Lock()
defer d.lock.Unlock()
d.identitiesData[identity] = &identityrepoproto.DataWithIdentity{
Identity: identity,
Data: data,
}
return nil
}
func (d *inMemoryIdentityRepo) IdentityRepoGet(ctx context.Context, identities []string, kinds []string) (res []*identityrepoproto.DataWithIdentity, err error) {
d.lock.Lock()
defer d.lock.Unlock()
if d.isUnavailable {
return nil, fmt.Errorf("network problem")
}
res = make([]*identityrepoproto.DataWithIdentity, 0, len(identities))
for _, identity := range identities {
if data, ok := d.identitiesData[identity]; ok {
res = append(res, data)
}
}
return
}
func TestIdentityProfileCache(t *testing.T) {
fx := newFixture(t)
spaceId := "space1"
identity := "identity1"
fx.coordinatorClient.EXPECT().IdentityRepoGet(gomock.Any(), []string{identity}, []string{identityRepoDataKind}).Return(nil, fmt.Errorf("network problem")).AnyTimes()
fx.coordinatorClient.setUnavailable()
profileSymKey, err := crypto.NewRandomAES()
require.NoError(t, err)
@ -137,43 +188,32 @@ func TestObservers(t *testing.T) {
})
require.NoError(t, err)
var identitiesFromRepo []*identityrepoproto.DataWithIdentity
fx.coordinatorClient.EXPECT().IdentityRepoGet(gomock.Any(), []string{identity}, []string{identityRepoDataKind}).DoAndReturn(func(_ context.Context, _ []string, _ []string) ([]*identityrepoproto.DataWithIdentity, error) {
return identitiesFromRepo, nil
}).AnyTimes()
time.Sleep(testObserverPeriod)
identitiesFromRepo = []*identityrepoproto.DataWithIdentity{
err = fx.identityRepoClient.IdentityRepoPut(context.Background(), identity, []*identityrepoproto.Data{
{
Identity: identity,
Data: []*identityrepoproto.Data{
{
Kind: identityRepoDataKind,
Data: wantData,
},
},
Kind: identityRepoDataKind,
Data: wantData,
},
}
})
require.NoError(t, err)
t.Run("change profile's name", func(t *testing.T) {
wantProfile2 := &model.IdentityProfile{
Identity: identity,
Name: "name1 edited",
Identity: identity,
Name: "name1 edited",
Description: "my description",
}
wantData2 := marshalProfile(t, wantProfile2, profileSymKey)
time.Sleep(testObserverPeriod)
identitiesFromRepo = []*identityrepoproto.DataWithIdentity{
err = fx.identityRepoClient.IdentityRepoPut(context.Background(), identity, []*identityrepoproto.Data{
{
Identity: identity,
Data: []*identityrepoproto.Data{
{
Kind: identityRepoDataKind,
Data: wantData2,
},
},
Kind: identityRepoDataKind,
Data: wantData2,
},
}
})
require.NoError(t, err)
})
wg.Wait()
@ -184,8 +224,9 @@ func TestObservers(t *testing.T) {
Name: "name1",
},
{
Identity: identity,
Name: "name1 edited",
Identity: identity,
Name: "name1 edited",
Description: "my description",
},
}
assert.Equal(t, wantCalls, callbackCalls)

View file

@ -26,7 +26,7 @@ import (
const (
// ForceObjectsReindexCounter reindex thread-based objects
ForceObjectsReindexCounter int32 = 14
ForceObjectsReindexCounter int32 = 15
// ForceFilesReindexCounter reindex ipfs-file-based objects
ForceFilesReindexCounter int32 = 11 //

View file

@ -2,8 +2,10 @@ package core
import (
"context"
"errors"
"fmt"
"github.com/anyproto/any-sync/commonspace/object/acl/list"
"github.com/anyproto/any-sync/util/crypto"
"github.com/ipfs/go-cid"
@ -14,8 +16,16 @@ import (
)
func (mw *Middleware) SpaceDelete(cctx context.Context, req *pb.RpcSpaceDeleteRequest) *pb.RpcSpaceDeleteResponse {
spaceService := mw.applicationService.GetApp().MustComponent(space.CName).(space.Service)
err := spaceService.Delete(cctx, req.SpaceId)
spaceService := getService[space.Service](mw)
aclService := getService[acl.AclService](mw)
err := aclService.Leave(cctx, req.SpaceId)
// we check for possible error cases:
// 1. user is an owner
// 2. user already left a request to delete
// 3. user is not a member of the space anymore
if err == nil || errors.Is(err, list.ErrIsOwner) || errors.Is(err, list.ErrPendingRequest) || errors.Is(err, list.ErrNoSuchAccount) {
err = spaceService.Delete(cctx, req.SpaceId)
}
code := mapErrorCode(err,
errToCode(space.ErrSpaceDeleted, pb.RpcSpaceDeleteResponseError_SPACE_IS_DELETED),
errToCode(space.ErrSpaceNotExists, pb.RpcSpaceDeleteResponseError_NO_SUCH_SPACE),

View file

@ -24270,6 +24270,7 @@ Used to decode block meta only, without the content itself
| name | [string](#string) | | |
| iconCid | [string](#string) | | |
| iconEncryptionKeys | [FileEncryptionKey](#anytype-model-FileEncryptionKey) | repeated | |
| description | [string](#string) | | |
@ -25672,6 +25673,7 @@ RelationFormat describes how the underlying data is stored in the google.protobu
| SpaceDeleted | 7 | SpaceDeleted - the space should be deleted in the network |
| SpaceActive | 8 | SpaceActive - the space is active in the network |
| SpaceJoining | 9 | SpaceJoining - the account is joining the space |
| SpaceRemoving | 10 | SpaceRemoving - the account is removing from space or the space is removed from network |

2
go.mod
View file

@ -7,7 +7,7 @@ require (
github.com/PuerkitoBio/goquery v1.8.1
github.com/VividCortex/ewma v1.2.0
github.com/adrium/goheif v0.0.0-20230113233934-ca402e77a786
github.com/anyproto/any-sync v0.3.19-alpha.0.20240201153711-141ab46e389c
github.com/anyproto/any-sync v0.3.21
github.com/anyproto/go-naturaldate/v2 v2.0.2-0.20230524105841-9829cfd13438
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
github.com/blevesearch/bleve/v2 v2.3.10

4
go.sum
View file

@ -107,8 +107,8 @@ github.com/andybalholm/cascadia v1.2.0/go.mod h1:YCyR8vOZT9aZ1CHEd8ap0gMVm2aFgxB
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
github.com/anyproto/any-sync v0.3.19-alpha.0.20240201153711-141ab46e389c h1:uuOCkIINAzzyG1+ten2rhwawwyZ13DeLjt2WUebJSFI=
github.com/anyproto/any-sync v0.3.19-alpha.0.20240201153711-141ab46e389c/go.mod h1:exw/7+W3jfkuaVWsStQD8s8aLFT9L2NBiW/DVwTNi/E=
github.com/anyproto/any-sync v0.3.21 h1:3U/EZRvOHzSQ9ukqVqNXqFKkqLbtIOjskSBMjUgQ5h4=
github.com/anyproto/any-sync v0.3.21/go.mod h1:ViWsWYe+jDuYip9gdp89RqvTfgHzorGypZSn+Ae0WUU=
github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580 h1:Ba80IlCCxkZ9H1GF+7vFu/TSpPvbpDCxXJ5ogc4euYc=
github.com/anyproto/badger/v4 v4.2.1-0.20240110160636-80743fa3d580/go.mod h1:T/uWAYxrXdaXw64ihI++9RMbKTCpKd/yE9+saARew7k=
github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY=

View file

@ -64,6 +64,12 @@ func New(eventEndpoint string, key string, isCompressed bool) Service {
}
func (c *Client) SendEvents(amplEvents []Event, info AppInfoProvider) error {
if c.key == "" {
return nil
}
if len(amplEvents) == 0 {
return nil
}
arena := c.arenaPool.Get()
appVersion := arena.NewString(info.GetAppVersion())
deviceId := arena.NewString(info.GetDeviceId())

View file

@ -28,14 +28,14 @@ import (
type Format string
func init() {
image.RegisterFormat("ico", string([]byte{0x00, 0x00, 0x01, 0x00}), ico.Decode, ico.DecodeConfig)
image.RegisterFormat("vnd.microsoft.icon", string([]byte{0x00, 0x00, 0x01, 0x00}), ico.Decode, ico.DecodeConfig)
}
const (
JPEG Format = "jpeg"
PNG Format = "png"
GIF Format = "gif"
ICO Format = "ico"
ICO Format = "vnd.microsoft.icon"
WEBP Format = "webp"
HEIC Format = "heic"
)

File diff suppressed because it is too large Load diff

View file

@ -911,6 +911,8 @@ enum SpaceStatus {
SpaceActive = 8;
// SpaceJoining - the account is joining the space
SpaceJoining = 9;
// SpaceRemoving - the account is removing from space or the space is removed from network
SpaceRemoving = 10;
}
message ParticipantPermissionChange {
@ -1063,6 +1065,7 @@ message IdentityProfile {
string name = 2;
string iconCid = 3;
repeated FileEncryptionKey iconEncryptionKeys = 4;
string description = 5;
}
message FileInfo {

View file

@ -1289,6 +1289,51 @@ func (_c *MockSpace_IsPersonal_Call) RunAndReturn(run func() bool) *MockSpace_Is
return _c
}
// IsReadOnly provides a mock function with given fields:
func (_m *MockSpace) IsReadOnly() bool {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for IsReadOnly")
}
var r0 bool
if rf, ok := ret.Get(0).(func() bool); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(bool)
}
return r0
}
// MockSpace_IsReadOnly_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'IsReadOnly'
type MockSpace_IsReadOnly_Call struct {
*mock.Call
}
// IsReadOnly is a helper method to define mock.On call
func (_e *MockSpace_Expecter) IsReadOnly() *MockSpace_IsReadOnly_Call {
return &MockSpace_IsReadOnly_Call{Call: _e.mock.On("IsReadOnly")}
}
func (_c *MockSpace_IsReadOnly_Call) Run(run func()) *MockSpace_IsReadOnly_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockSpace_IsReadOnly_Call) Return(_a0 bool) *MockSpace_IsReadOnly_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockSpace_IsReadOnly_Call) RunAndReturn(run func() bool) *MockSpace_IsReadOnly_Call {
_c.Call.Return(run)
return _c
}
// LoadObjects provides a mock function with given fields: ctx, ids
func (_m *MockSpace) LoadObjects(ctx context.Context, ids []string) error {
ret := _m.Called(ctx, ids)

View file

@ -10,6 +10,7 @@ import (
"github.com/anyproto/any-sync/commonspace/headsync"
"github.com/anyproto/any-sync/commonspace/objecttreebuilder"
"github.com/anyproto/any-sync/commonspace/spacestorage"
"github.com/anyproto/any-sync/util/crypto"
"github.com/gogo/protobuf/types"
"github.com/anyproto/anytype-heart/core/block/editor/smartblock"
@ -43,6 +44,7 @@ type Space interface {
GetRelationIdByKey(ctx context.Context, key domain.RelationKey) (id string, err error)
GetTypeIdByKey(ctx context.Context, key domain.TypeKey) (id string, err error)
IsReadOnly() bool
IsPersonal() bool
Close(ctx context.Context) error
@ -70,7 +72,8 @@ type space struct {
spaceCore spacecore.SpaceCoreService
personalSpaceId string
common commonspace.Space
myIdentity crypto.PubKey
common commonspace.Space
loadMandatoryObjectsCh chan struct{}
loadMandatoryObjectsErr error
@ -95,6 +98,7 @@ func BuildSpace(ctx context.Context, deps SpaceDeps) (Space, error) {
common: deps.CommonSpace,
personalSpaceId: deps.PersonalSpaceId,
spaceCore: deps.SpaceCore,
myIdentity: deps.AccountService.Account().SignKey.GetPublic(),
loadMandatoryObjectsCh: make(chan struct{}),
}
sp.Cache = objectcache.New(deps.AccountService, deps.ObjectFactory, deps.PersonalSpaceId, sp)
@ -235,3 +239,7 @@ func (s *space) InstallBundledObjects(ctx context.Context) error {
}
return nil
}
func (s *space) IsReadOnly() bool {
return !s.CommonSpace().Acl().AclState().Permissions(s.myIdentity).CanWrite()
}

View file

@ -38,6 +38,7 @@ func NewTechSpace(deps TechSpaceDeps) *TechSpace {
common: deps.CommonSpace,
loadMandatoryObjectsCh: make(chan struct{}),
personalSpaceId: deps.PersonalSpaceId,
myIdentity: deps.AccountService.Account().SignKey.GetPublic(),
},
TechSpace: deps.TechSpace,
}

View file

@ -143,3 +143,7 @@ func (c *virtualCommonSpace) TryClose(objectTTL time.Duration) (close bool, err
func (c *virtualCommonSpace) Close() error {
return nil
}
func (c *virtualCommonSpace) IsReadOnly() bool {
return false
}

View file

@ -1,81 +0,0 @@
package space
import (
"context"
"time"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"github.com/anyproto/any-sync/util/periodicsync"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/space/spaceinfo"
)
const (
loopPeriodSecs = 60
loopTimeout = time.Second * 120
)
type localDeleter interface {
updateRemoteStatus(ctx context.Context, spaceId string, status spaceinfo.RemoteStatus) error
allIDs() (ids []string)
}
type deletionController struct {
deleter localDeleter
client coordinatorclient.CoordinatorClient
periodicCall periodicsync.PeriodicSync
}
func newDeletionController(
localDeleter localDeleter,
client coordinatorclient.CoordinatorClient) *deletionController {
d := &deletionController{
deleter: localDeleter,
client: client,
}
d.periodicCall = periodicsync.NewPeriodicSync(loopPeriodSecs, loopTimeout, d.loopIterate, log)
return d
}
func (d *deletionController) Run() {
d.periodicCall.Run()
}
func (d *deletionController) Close() {
d.periodicCall.Close()
}
func (d *deletionController) loopIterate(ctx context.Context) error {
d.updateStatuses(ctx)
return nil
}
func (d *deletionController) updateStatuses(ctx context.Context) {
ids := d.deleter.allIDs()
remoteStatuses, err := d.client.StatusCheckMany(ctx, ids)
if err != nil {
log.Warn("remote status check error", zap.Error(err))
return
}
convStatus := func(status coordinatorproto.SpaceStatus) spaceinfo.RemoteStatus {
switch status {
case coordinatorproto.SpaceStatus_SpaceStatusCreated:
return spaceinfo.RemoteStatusOk
case coordinatorproto.SpaceStatus_SpaceStatusPendingDeletion:
return spaceinfo.RemoteStatusWaitingDeletion
default:
return spaceinfo.RemoteStatusDeleted
}
}
for idx, nodeStatus := range remoteStatuses {
remoteStatus := convStatus(nodeStatus.Status)
err := d.deleter.updateRemoteStatus(ctx, ids[idx], remoteStatus)
if err != nil {
log.Warn("remote status update error", zap.Error(err), zap.String("spaceId", ids[idx]))
return
}
}
}

View file

@ -0,0 +1,148 @@
package deletioncontroller
import (
"context"
"sync"
"time"
"github.com/anyproto/any-sync/accountservice"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/anyproto/any-sync/commonspace/acl/aclclient"
"github.com/anyproto/any-sync/commonspace/object/accountdata"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/coordinator/coordinatorproto"
"github.com/anyproto/any-sync/util/periodicsync"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/space/spacecore"
"github.com/anyproto/anytype-heart/space/spaceinfo"
)
const CName = "client.space.deletioncontroller"
var log = logger.NewNamed(CName)
const (
loopPeriodSecs = 60
loopTimeout = time.Second * 120
)
type DeletionController interface {
app.ComponentRunnable
AddSpace(spaceId string)
}
func New() DeletionController {
return &deletionController{}
}
type spaceManager interface {
UpdateRemoteStatus(ctx context.Context, spaceId string, status spaceinfo.RemoteStatus, isOwned bool) error
AllSpaceIds() (ids []string)
}
type deletionController struct {
spaceManager spaceManager
client coordinatorclient.CoordinatorClient
spaceCore spacecore.SpaceCoreService
joiningClient aclclient.AclJoiningClient
keys *accountdata.AccountKeys
periodicCall periodicsync.PeriodicSync
mx sync.Mutex
toDelete map[string]struct{}
}
func (d *deletionController) Init(a *app.App) (err error) {
d.client = a.MustComponent(coordinatorclient.CName).(coordinatorclient.CoordinatorClient)
d.spaceCore = a.MustComponent(spacecore.CName).(spacecore.SpaceCoreService)
d.joiningClient = a.MustComponent(aclclient.CName).(aclclient.AclJoiningClient)
d.spaceManager = app.MustComponent[spaceManager](a)
d.keys = a.MustComponent(accountservice.CName).(accountservice.Service).Account()
d.periodicCall = periodicsync.NewPeriodicSync(loopPeriodSecs, loopTimeout, d.loopIterate, log)
d.toDelete = make(map[string]struct{})
return
}
func (d *deletionController) Name() (name string) {
return CName
}
func (d *deletionController) AddSpace(spaceId string) {
d.mx.Lock()
defer d.mx.Unlock()
d.toDelete[spaceId] = struct{}{}
}
func (d *deletionController) Run(ctx context.Context) error {
d.periodicCall.Run()
return nil
}
func (d *deletionController) Close(ctx context.Context) error {
d.periodicCall.Close()
return nil
}
func (d *deletionController) loopIterate(ctx context.Context) error {
ownedIds := d.updateStatuses(ctx)
d.mx.Lock()
var toDeleteOwnedIds []string
for _, id := range ownedIds {
if _, exists := d.toDelete[id]; exists {
toDeleteOwnedIds = append(toDeleteOwnedIds, id)
}
}
d.mx.Unlock()
d.deleteOwnedSpaces(ctx, toDeleteOwnedIds)
return nil
}
func (d *deletionController) updateStatuses(ctx context.Context) (ownedIds []string) {
ids := d.spaceManager.AllSpaceIds()
remoteStatuses, err := d.client.StatusCheckMany(ctx, ids)
if err != nil {
log.Warn("remote status check error", zap.Error(err))
return
}
convStatus := func(status coordinatorproto.SpaceStatus) spaceinfo.RemoteStatus {
switch status {
case coordinatorproto.SpaceStatus_SpaceStatusCreated:
return spaceinfo.RemoteStatusOk
case coordinatorproto.SpaceStatus_SpaceStatusPendingDeletion:
return spaceinfo.RemoteStatusWaitingDeletion
default:
return spaceinfo.RemoteStatusDeleted
}
}
for idx, nodeStatus := range remoteStatuses {
if nodeStatus.Status == coordinatorproto.SpaceStatus_SpaceStatusNotExists {
continue
}
isOwned := false
if nodeStatus.Status == coordinatorproto.SpaceStatus_SpaceStatusCreated && nodeStatus.Permissions == coordinatorproto.SpacePermissions_SpacePermissionsOwner {
isOwned = true
ownedIds = append(ownedIds, ids[idx])
}
remoteStatus := convStatus(nodeStatus.Status)
err := d.spaceManager.UpdateRemoteStatus(ctx, ids[idx], remoteStatus, isOwned)
if err != nil {
log.Warn("remote status update error", zap.Error(err), zap.String("spaceId", ids[idx]))
return
}
}
return
}
func (d *deletionController) deleteOwnedSpaces(ctx context.Context, spaceIds []string) {
for _, spaceId := range spaceIds {
if err := d.spaceCore.Delete(ctx, spaceId); err != nil {
log.Warn("space deletion error", zap.Error(err), zap.String("spaceId", spaceId))
continue
}
d.mx.Lock()
delete(d.toDelete, spaceId)
d.mx.Unlock()
}
}

View file

@ -147,6 +147,7 @@ func (a *aclObjectManager) initAndRegisterMyIdentity(ctx context.Context) error
}
details := buildParticipantDetails(id, a.sp.Id(), myIdentity, model.ParticipantPermissions_Owner, model.ParticipantStatus_Active)
details.Fields[bundle.RelationKeyName.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyName.String()))
details.Fields[bundle.RelationKeyDescription.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyDescription.String()))
details.Fields[bundle.RelationKeyIconImage.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyIconImage.String()))
details.Fields[bundle.RelationKeyIdentityProfileLink.String()] = pbtypes.String(pbtypes.GetString(profileDetails, bundle.RelationKeyId.String()))
err = a.modifier.ModifyDetails(id, func(current *types.Struct) (*types.Struct, error) {
@ -190,7 +191,7 @@ func (a *aclObjectManager) processAcl() (err error) {
}
return common.Acl().AclState().GetMetadata(key, true)
}
states := common.Acl().AclState().CurrentStates()
states := common.Acl().AclState().CurrentAccounts()
// decrypt all metadata
states, err = decryptAll(states, decrypt)
if err != nil {
@ -198,7 +199,7 @@ func (a *aclObjectManager) processAcl() (err error) {
}
a.mx.Lock()
defer a.mx.Unlock()
err = a.processStates(states)
err = a.processStates(states, common.Acl().AclState().Identity())
if err != nil {
return
}
@ -206,9 +207,19 @@ func (a *aclObjectManager) processAcl() (err error) {
return
}
func (a *aclObjectManager) processStates(states []list.AccountState) (err error) {
func (a *aclObjectManager) processStates(states []list.AccountState, myIdentity crypto.PubKey) (err error) {
var numActiveUsers int
for _, state := range states {
if state.Permissions.NoPermissions() && state.PubKey.Equals(myIdentity) {
a.status.Lock()
err := a.status.SetPersistentStatus(a.ctx, spaceinfo.AccountStatusRemoving)
if err != nil {
a.status.Unlock()
return err
}
a.status.Unlock()
return nil
}
if !(state.Permissions.IsOwner() || state.Permissions.NoPermissions()) {
numActiveUsers++
}
@ -275,8 +286,9 @@ func (a *aclObjectManager) updateParticipantFromIdentity(ctx context.Context, id
return err
}
details := &types.Struct{Fields: map[string]*types.Value{
bundle.RelationKeyName.String(): pbtypes.String(profile.Name),
bundle.RelationKeyIconImage.String(): pbtypes.String(profile.IconCid),
bundle.RelationKeyName.String(): pbtypes.String(profile.Name),
bundle.RelationKeyDescription.String(): pbtypes.String(profile.Description),
bundle.RelationKeyIconImage.String(): pbtypes.String(profile.IconCid),
}}
return a.modifier.ModifyDetails(id, func(current *types.Struct) (*types.Struct, error) {
status := pbtypes.GetInt64(current, bundle.RelationKeyParticipantStatus.String())

View file

@ -9,6 +9,7 @@ import (
"github.com/anyproto/any-sync/app/logger"
"go.uber.org/zap"
"github.com/anyproto/anytype-heart/space/deletioncontroller"
dependencies2 "github.com/anyproto/anytype-heart/space/internal/components/dependencies"
"github.com/anyproto/anytype-heart/space/internal/components/spacestatus"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/mode"
@ -39,6 +40,7 @@ type spaceOffloader struct {
storageService storage.ClientStorage
indexer dependencies2.SpaceIndexer
spaceCore spacecore.SpaceCoreService
delController deletioncontroller.DeletionController
ctx context.Context
cancel context.CancelFunc
offloaded atomic.Bool
@ -50,6 +52,7 @@ func (o *spaceOffloader) Init(a *app.App) (err error) {
o.storageService = app.MustComponent[storage.ClientStorage](a)
o.indexer = app.MustComponent[dependencies2.SpaceIndexer](a)
o.spaceCore = app.MustComponent[spacecore.SpaceCoreService](a)
o.delController = app.MustComponent[deletioncontroller.DeletionController](a)
o.ctx, o.cancel = context.WithCancel(context.Background())
return nil
}
@ -70,14 +73,8 @@ func (o *spaceOffloader) Run(ctx context.Context) (err error) {
}
}
localStatus := o.status.GetLocalStatus()
remoteStatus := o.status.GetRemoteStatus()
o.status.Unlock()
if !remoteStatus.IsDeleted() {
err := o.spaceCore.Delete(ctx, o.status.SpaceId())
if err != nil {
log.Debug("network delete error", zap.Error(err), zap.String("spaceId", o.status.SpaceId()))
}
}
o.delController.AddSpace(o.status.SpaceId())
if localStatus == spaceinfo.LocalStatusMissing {
o.offloaded.Store(true)
return nil

View file

@ -0,0 +1,68 @@
package syncstopper
import (
"context"
"time"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/any-sync/app/logger"
"github.com/anyproto/any-sync/util/periodicsync"
"github.com/anyproto/anytype-heart/space/internal/components/spacestatus"
space "github.com/anyproto/anytype-heart/space/spacecore"
)
const CName = "common.components.syncstopper"
const (
timeout = 30 * time.Second
loopIntervalSecs = 30
stopSyncTimeout = 10 * time.Minute
)
var log = logger.NewNamed(CName)
type SyncStopper struct {
spaceCore space.SpaceCoreService
spaceStatus spacestatus.SpaceStatus
periodicCall periodicsync.PeriodicSync
startTime time.Time
}
func New() *SyncStopper {
return &SyncStopper{}
}
func (s *SyncStopper) Init(a *app.App) (err error) {
s.spaceCore = a.MustComponent(space.CName).(space.SpaceCoreService)
s.spaceStatus = a.MustComponent(spacestatus.CName).(spacestatus.SpaceStatus)
return
}
func (s *SyncStopper) Name() (name string) {
return CName
}
func (s *SyncStopper) Run(ctx context.Context) (err error) {
s.startTime = time.Now()
s.periodicCall = periodicsync.NewPeriodicSync(loopIntervalSecs, timeout, s.spaceCheck, log)
return nil
}
func (s *SyncStopper) spaceCheck(ctx context.Context) (err error) {
sp, err := s.spaceCore.Pick(ctx, s.spaceStatus.SpaceId())
if err != nil {
return
}
if time.Since(s.startTime) > stopSyncTimeout {
sp.TreeSyncer().StopSync()
}
return nil
}
func (s *SyncStopper) Close(ctx context.Context) (err error) {
if s.periodicCall != nil {
s.periodicCall.Close()
}
return nil
}

View file

@ -14,6 +14,7 @@ import (
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/loader"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/mode"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/offloader"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/remover"
"github.com/anyproto/anytype-heart/space/spaceinfo"
)
@ -58,6 +59,9 @@ func (s *spaceController) Start(ctx context.Context) error {
case spaceinfo.AccountStatusJoining:
_, err := s.sm.ChangeMode(mode.ModeJoining)
return err
case spaceinfo.AccountStatusRemoving:
_, err := s.sm.ChangeMode(mode.ModeRemoving)
return err
default:
_, err := s.sm.ChangeMode(mode.ModeLoading)
return err
@ -85,7 +89,7 @@ func (s *spaceController) SetStatus(ctx context.Context, status spaceinfo.Accoun
func (s *spaceController) UpdateStatus(ctx context.Context, status spaceinfo.AccountStatus) error {
s.status.Lock()
if s.lastUpdatedStatus == status {
if s.lastUpdatedStatus == status || (s.lastUpdatedStatus == spaceinfo.AccountStatusDeleted && status == spaceinfo.AccountStatusRemoving) {
s.status.Unlock()
return nil
}
@ -103,6 +107,8 @@ func (s *spaceController) UpdateStatus(ctx context.Context, status spaceinfo.Acc
return updateStatus(mode.ModeOffloading)
case spaceinfo.AccountStatusJoining:
return updateStatus(mode.ModeJoining)
case spaceinfo.AccountStatusRemoving:
return updateStatus(mode.ModeRemoving)
default:
return updateStatus(mode.ModeLoading)
}
@ -136,6 +142,11 @@ func (s *spaceController) Process(md mode.Mode) mode.Process {
return offloader.New(s.app, offloader.Params{
Status: s.status,
})
case mode.ModeRemoving:
return remover.New(s.app, remover.Params{
SpaceId: s.spaceId,
Status: s.status,
})
case mode.ModeJoining:
return joiner.New(s.app, joiner.Params{
SpaceId: s.spaceId,

View file

@ -17,6 +17,7 @@ const (
ModeLoading
ModeOffloading
ModeJoining
ModeRemoving
)
type WaitResult struct {

View file

@ -44,7 +44,7 @@ func (o *offloader) Start(ctx context.Context) error {
}
func (o *offloader) CanTransition(next mode.Mode) bool {
return false
return true
}
func (o *offloader) WaitOffload(ctx context.Context) error {

View file

@ -0,0 +1,61 @@
package remover
import (
"context"
"github.com/anyproto/any-sync/app"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/internal/components/aclobjectmanager"
"github.com/anyproto/anytype-heart/space/internal/components/builder"
"github.com/anyproto/anytype-heart/space/internal/components/spaceloader"
"github.com/anyproto/anytype-heart/space/internal/components/spacestatus"
"github.com/anyproto/anytype-heart/space/internal/components/syncstopper"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/loader"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/mode"
)
type remover struct {
app *app.App
}
type Remover interface {
mode.Process
loader.LoadWaiter
}
type Params struct {
SpaceId string
Status spacestatus.SpaceStatus
StopIfMandatoryFail bool
OwnerMetadata []byte
}
func New(app *app.App, params Params) Remover {
child := app.ChildApp()
child.Register(params.Status).
Register(builder.New()).
Register(spaceloader.New(params.StopIfMandatoryFail)).
Register(syncstopper.New()).
Register(aclobjectmanager.New(params.OwnerMetadata))
return &remover{
app: child,
}
}
func (r *remover) Start(ctx context.Context) error {
return r.app.Start(ctx)
}
func (r *remover) Close(ctx context.Context) error {
return r.app.Close(ctx)
}
func (r *remover) CanTransition(next mode.Mode) bool {
return true
}
func (r *remover) WaitLoad(ctx context.Context) (sp clientspace.Space, err error) {
spaceLoader := app.MustComponent[spaceloader.SpaceLoader](r.app)
return spaceLoader.WaitLoad(ctx)
}

View file

@ -63,7 +63,7 @@ func (s *service) waitLoad(ctx context.Context, ctrl spacecontroller.SpaceContro
if ld, ok := ctrl.Current().(loader.LoadWaiter); ok {
return ld.WaitLoad(ctx)
}
return nil, fmt.Errorf("failed to load space, mode is %d", ctrl.Mode())
return nil, fmt.Errorf("failed to load space, mode is %d: %w", ctrl.Mode(), ErrFailedToLoad)
}
func (s *service) loadPersonalSpace(ctx context.Context) (err error) {

View file

@ -12,7 +12,6 @@ import (
"github.com/anyproto/any-sync/app/logger"
"github.com/anyproto/any-sync/commonspace/object/tree/treechangeproto"
"github.com/anyproto/any-sync/commonspace/spacesyncproto"
"github.com/anyproto/any-sync/coordinator/coordinatorclient"
"github.com/anyproto/any-sync/util/crypto"
"github.com/gogo/protobuf/types"
"go.uber.org/zap"
@ -21,7 +20,6 @@ import (
"github.com/anyproto/anytype-heart/pkg/lib/localstore/addr"
"github.com/anyproto/anytype-heart/space/clientspace"
"github.com/anyproto/anytype-heart/space/internal/spacecontroller"
"github.com/anyproto/anytype-heart/space/internal/spaceprocess/mode"
"github.com/anyproto/anytype-heart/space/spacecore"
"github.com/anyproto/anytype-heart/space/spacefactory"
"github.com/anyproto/anytype-heart/space/spaceinfo"
@ -36,6 +34,7 @@ var (
ErrSpaceNotExists = errors.New("space not exists")
ErrSpaceDeleted = errors.New("space is deleted")
ErrSpaceIsClosing = errors.New("space is closing")
ErrFailedToLoad = errors.New("failed to load space")
)
func New() Service {
@ -68,7 +67,6 @@ type service struct {
spaceCore spacecore.SpaceCoreService
accountService accountservice.Service
config *config.Config
delController *deletionController
personalSpaceId string
newAccount bool
@ -104,8 +102,6 @@ func (s *service) Delete(ctx context.Context, id string) (err error) {
func (s *service) Init(a *app.App) (err error) {
s.newAccount = app.MustComponent[isNewAccount](a).IsNewAccount()
coordClient := app.MustComponent[coordinatorclient.CoordinatorClient](a)
s.delController = newDeletionController(s, coordClient)
s.factory = app.MustComponent[spacefactory.SpaceFactory](a)
s.spaceCore = app.MustComponent[spacecore.SpaceCoreService](a)
s.accountService = app.MustComponent[accountservice.Service](a)
@ -156,7 +152,6 @@ func (s *service) Run(ctx context.Context) (err error) {
}
return fmt.Errorf("init personal space: %w", err)
}
s.delController.Run()
// only persist networkId after successful space init
err = s.config.PersistAccountNetworkId()
if err != nil {
@ -222,7 +217,7 @@ func (s *service) AccountMetadataPayload() []byte {
return s.accountMetadataPayload
}
func (s *service) updateRemoteStatus(ctx context.Context, spaceId string, status spaceinfo.RemoteStatus) error {
func (s *service) UpdateRemoteStatus(ctx context.Context, spaceId string, status spaceinfo.RemoteStatus, isOwned bool) error {
s.mu.Lock()
ctrl := s.spaceControllers[spaceId]
s.mu.Unlock()
@ -233,6 +228,9 @@ func (s *service) updateRemoteStatus(ctx context.Context, spaceId string, status
if err != nil {
return fmt.Errorf("updateRemoteStatus: %w", err)
}
if !isOwned && status == spaceinfo.RemoteStatusDeleted {
return ctrl.SetStatus(ctx, spaceinfo.AccountStatusRemoving)
}
return nil
}
@ -262,15 +260,14 @@ func (s *service) Close(ctx context.Context) error {
if err != nil {
log.Error("close tech space", zap.Error(err))
}
s.delController.Close()
return nil
}
func (s *service) allIDs() (ids []string) {
func (s *service) AllSpaceIds() (ids []string) {
s.mu.Lock()
defer s.mu.Unlock()
for id, sc := range s.spaceControllers {
if id == addr.AnytypeMarketplaceWorkspace || sc.Mode() != mode.ModeLoading {
for id := range s.spaceControllers {
if id == addr.AnytypeMarketplaceWorkspace {
continue
}
ids = append(ids, id)

View file

@ -61,6 +61,7 @@ type SpaceCoreService interface {
DeriveID(ctx context.Context, spaceType string) (id string, err error)
Delete(ctx context.Context, spaceID string) (err error)
Get(ctx context.Context, id string) (*AnySpace, error)
Pick(ctx context.Context, id string) (*AnySpace, error)
CloseSpace(ctx context.Context, id string) error
StreamPool() streampool.StreamPool
@ -203,6 +204,14 @@ func (s *service) Get(ctx context.Context, id string) (space *AnySpace, err erro
return v.(*AnySpace), nil
}
func (s *service) Pick(ctx context.Context, id string) (space *AnySpace, err error) {
v, err := s.spaceCache.Pick(ctx, id)
if err != nil {
return
}
return v.(*AnySpace), nil
}
func (s *service) HandleMessage(ctx context.Context, senderId string, req *spacesyncproto.ObjectSyncMessage) (err error) {
var msg = &spacesyncproto.SpaceSubscription{}
if err = msg.Unmarshal(req.Payload); err != nil {

View file

@ -28,10 +28,11 @@ func (r RemoteStatus) IsDeleted() bool {
type AccountStatus int
const (
AccountStatusUnknown = AccountStatus(model.SpaceStatus_Unknown)
AccountStatusDeleted = AccountStatus(model.SpaceStatus_SpaceDeleted)
AccountStatusJoining = AccountStatus(model.SpaceStatus_SpaceJoining)
AccountStatusActive = AccountStatus(model.SpaceStatus_SpaceActive)
AccountStatusUnknown = AccountStatus(model.SpaceStatus_Unknown)
AccountStatusDeleted = AccountStatus(model.SpaceStatus_SpaceDeleted)
AccountStatusJoining = AccountStatus(model.SpaceStatus_SpaceJoining)
AccountStatusActive = AccountStatus(model.SpaceStatus_SpaceActive)
AccountStatusRemoving = AccountStatus(model.SpaceStatus_SpaceRemoving)
)
type SpaceLocalInfo struct {