143 lines
5.6 KiB
Go
Raw Permalink Normal View History

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.
bugfix: fix a bug with list ops when sorting with unread counts; fix a bug which could cause typing/receipts to not be live streamed Previously, we would not send unread count INCREASES to the client, as we would expect the actual event update to wake up the client conn. This was great because it meant the event+unread count arrived atomically on the client. This was implemented as "parse unread counts first, then events". However, this introduced a bug when there were >1 user in the same room. In this scenario, one poller may get the event first, which would go through to the client. The subsequent unread count update would then be dropped and not sent to the client. This would just be an unfortunate UI bug if it weren't for sorting by_notification_count and sorting by_notification_level. Both of these sort operations use the unread counts to determine room list ordering. This list would be updated on the server, but no list operation would be sent to the client, causing the room lists to de-sync, and resulting in incorrect DELETE/INSERT ops. This would manifest as duplicate rooms on the room list. In the process of fixing this, also fix a bug where typing notifications would not always be sent to the client - it would only do so when piggybacked due to incorrect type switches. Also fix another bug which prevented receipts from always being sent to the client. This was caused by the extensions handler not checking if the receipt extension had data to determine if it should return. This the interacted with an as-yet unfixed bug which cleared the extension on subequent updates, causing the receipt to be lost entirely. A fix for this will be inbound soon.
2023-02-07 13:34:26 +00:00
//
// A,B,C,D,E,F,G,H,I <-- List
// `----` `----` <-- RequestList.Ranges
// DEL E | ADD J | CHANGE C <-- ListOp RoomID
//
// returns:
bugfix: fix a bug with list ops when sorting with unread counts; fix a bug which could cause typing/receipts to not be live streamed Previously, we would not send unread count INCREASES to the client, as we would expect the actual event update to wake up the client conn. This was great because it meant the event+unread count arrived atomically on the client. This was implemented as "parse unread counts first, then events". However, this introduced a bug when there were >1 user in the same room. In this scenario, one poller may get the event first, which would go through to the client. The subsequent unread count update would then be dropped and not sent to the client. This would just be an unfortunate UI bug if it weren't for sorting by_notification_count and sorting by_notification_level. Both of these sort operations use the unread counts to determine room list ordering. This list would be updated on the server, but no list operation would be sent to the client, causing the room lists to de-sync, and resulting in incorrect DELETE/INSERT ops. This would manifest as duplicate rooms on the room list. In the process of fixing this, also fix a bug where typing notifications would not always be sent to the client - it would only do so when piggybacked due to incorrect type switches. Also fix another bug which prevented receipts from always being sent to the client. This was caused by the extensions handler not checking if the receipt extension had data to determine if it should return. This the interacted with an as-yet unfixed bug which cleared the extension on subequent updates, causing the receipt to be lost entirely. A fix for this will be inbound soon.
2023-02-07 13:34:26 +00:00
//
// [ {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
2023-04-05 15:36:05 +01:00
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
}