2023-04-05 18:24:02 +01:00

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
}