mirror of
https://github.com/matrix-org/sliding-sync.git
synced 2025-03-10 13:37:11 +00:00
143 lines
5.6 KiB
Go
143 lines
5.6 KiB
Go
package sync3
|
|
|
|
import (
|
|
"context"
|
|
"github.com/matrix-org/sliding-sync/internal"
|
|
)
|
|
|
|
type List interface {
|
|
IndexOf(roomID string) (int, bool)
|
|
Len() int64
|
|
Sort(sortBy []string) error
|
|
Add(roomID string) bool
|
|
Remove(roomID string) int
|
|
Get(index int) string
|
|
}
|
|
|
|
// CalculateListOps contains the core list moving algorithm. It accepts the client's list of ranges,
|
|
// the underlying list on which to perform operations on, and the room which was modified and in what
|
|
// way. It returns a list of INSERT/DELETE operations, which may be zero length, as well as which rooms
|
|
// are newly added into the window.
|
|
//
|
|
// A,B,C,D,E,F,G,H,I <-- List
|
|
// `----` `----` <-- RequestList.Ranges
|
|
// DEL E | ADD J | CHANGE C <-- ListOp RoomID
|
|
//
|
|
// returns:
|
|
//
|
|
// [ {op:DELETE, index:2}, {op:INSERT, index:0, room_id:A} ] <--- []ResponseOp
|
|
// [ "A" ] <--- []string, new room subscriptions, if it wasn't in the window before
|
|
//
|
|
// This function will modify List to Add/Delete/Sort appropriately.
|
|
func CalculateListOps(ctx context.Context, reqList *RequestList, list List, roomID string, listOp ListOp) (ops []ResponseOp, subs []string) {
|
|
fromIndex, ok := list.IndexOf(roomID)
|
|
if !ok {
|
|
if listOp == ListOpAdd {
|
|
fromIndex = int(list.Len())
|
|
} else {
|
|
// room doesn't exist and we aren't adding it, nothing to do.
|
|
return
|
|
}
|
|
}
|
|
_, wasInsideRange := reqList.Ranges.Inside(int64(fromIndex))
|
|
|
|
var toIndex int
|
|
// modify the list
|
|
switch listOp {
|
|
case ListOpAdd:
|
|
wasInsideRange = false // can't be inside the range if this is a new room
|
|
list.Add(roomID)
|
|
// this should only move exactly 1 room at most as this is called for every single update
|
|
if err := list.Sort(reqList.Sort); err != nil {
|
|
logger.Err(err).Msg("cannot sort list")
|
|
internal.GetSentryHubFromContextOrDefault(ctx).CaptureException(err)
|
|
}
|
|
// find the new position of this room
|
|
toIndex, _ = list.IndexOf(roomID)
|
|
case ListOpDel:
|
|
list.Remove(roomID)
|
|
// no need to resort here, everything is in the right order already and just needs a shift
|
|
// there will be no toIndex, set it to the end of the array
|
|
toIndex = int(list.Len()) - 1
|
|
if toIndex == -1 {
|
|
// we are removing the last element of the list
|
|
ops = append(ops, reqList.WriteDeleteOp(0))
|
|
return
|
|
}
|
|
case ListOpChange:
|
|
// this should only move exactly 1 room at most as this is called for every single update
|
|
if err := list.Sort(reqList.Sort); err != nil {
|
|
logger.Err(err).Msg("cannot sort list")
|
|
internal.GetSentryHubFromContextOrDefault(ctx).CaptureException(err)
|
|
}
|
|
// find the new position of this room
|
|
toIndex, _ = list.IndexOf(roomID)
|
|
}
|
|
|
|
listFromTos := reqList.CalculateMoveIndexes(fromIndex, toIndex)
|
|
if len(listFromTos) == 0 {
|
|
return
|
|
}
|
|
|
|
for _, listFromTo := range listFromTos {
|
|
listFromIndex := listFromTo[0]
|
|
listToIndex := listFromTo[1]
|
|
wasUpdatedRoomInserted := listToIndex == toIndex
|
|
toRoomID := list.Get(listToIndex)
|
|
if toRoomID == roomID && listFromIndex == listToIndex && listOp == ListOpChange && wasInsideRange && len(listFromTos) == 1 {
|
|
// DELETE/INSERT have the same index, we're INSERTing the room that was updated, it was a Change not Add/Delete, it
|
|
// was previously inside the window AND there's just 1 move operation = it's moving to and from the same index so
|
|
// don't send an update.
|
|
continue // no-op move
|
|
}
|
|
|
|
if wasUpdatedRoomInserted && listOp == ListOpDel {
|
|
// ignore this insert, as deletions algorithmically just jump to the end of the array.
|
|
// we do this so we can calculate jumps over ranges using the same codepaths as moves.
|
|
isInsertingDeletedRoom := toRoomID == roomID
|
|
// The algorithm needs to know if it should issue an INSERT for the `to` room. The whole
|
|
// "treat deletes as inserts at the end of the array" thing makes it hard to know if it
|
|
// should or should not. This check ensures that we only skip the INSERT if the to-be shifted
|
|
// room was not already sent to the client.
|
|
_, isShiftedRoomAlreadySent := reqList.Ranges.Inside(list.Len())
|
|
if isInsertingDeletedRoom || (listToIndex == int(list.Len()-1) && isShiftedRoomAlreadySent) {
|
|
ops = append(ops, reqList.WriteDeleteOp(listFromIndex))
|
|
continue
|
|
}
|
|
}
|
|
|
|
swapOp := reqList.WriteSwapOp(toRoomID, listFromIndex, listToIndex)
|
|
ops = append(ops, swapOp...)
|
|
|
|
addedSub := false
|
|
// if a different room is being inserted or the room wasn't previously inside a range, send
|
|
// the entire room data
|
|
if !wasUpdatedRoomInserted || !wasInsideRange {
|
|
subs = append(subs, toRoomID)
|
|
addedSub = true
|
|
}
|
|
|
|
if wasUpdatedRoomInserted && listOp == ListOpAdd {
|
|
if !addedSub {
|
|
subs = append(subs, toRoomID)
|
|
}
|
|
// The client needs to know which item in the list to delete to know which direction to
|
|
// shift items (left or right). This means the vast majority of BRAND NEW rooms will result
|
|
// in a swap op even though you could argue that an INSERT[0] or something is sufficient
|
|
// (though bear in mind that we don't always insert to [0]). The one edge case is when
|
|
// there are no items in the list at all. In this case, there is no swap op as there is
|
|
// nothing to swap, but we still need to tell clients about the operation, hence a lone
|
|
// INSERT op.
|
|
// This can be intuitively explained by saying that "if there is a room at this toIndex
|
|
// then we need a DELETE to tell the client whether the room being replaced should shift
|
|
// left or shift right". In absence of a room being there, we just INSERT, which plays
|
|
// nicely with the logic of "if there is nothing at this index position, insert, else expect
|
|
// a delete op to have happened before".
|
|
if swapOp == nil {
|
|
ops = append(ops, reqList.WriteInsertOp(toIndex, toRoomID))
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|