2022-08-31 13:43:09 +01:00
|
|
|
package sync3
|
|
|
|
|
2023-04-05 14:11:05 +01:00
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"github.com/matrix-org/sliding-sync/internal"
|
|
|
|
)
|
|
|
|
|
2022-08-31 13:43:09 +01:00
|
|
|
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
|
|
|
|
//
|
2022-08-31 13:43:09 +01:00
|
|
|
// 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
|
2022-08-31 13:43:09 +01:00
|
|
|
//
|
|
|
|
// This function will modify List to Add/Delete/Sort appropriately.
|
2023-04-05 14:11:05 +01:00
|
|
|
func CalculateListOps(ctx context.Context, reqList *RequestList, list List, roomID string, listOp ListOp) (ops []ResponseOp, subs []string) {
|
2022-08-31 13:43:09 +01:00
|
|
|
fromIndex, ok := list.IndexOf(roomID)
|
|
|
|
if !ok {
|
|
|
|
if listOp == ListOpAdd {
|
|
|
|
fromIndex = int(list.Len())
|
|
|
|
} else {
|
2022-08-31 17:54:07 +01:00
|
|
|
// room doesn't exist and we aren't adding it, nothing to do.
|
2022-08-31 13:43:09 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-08-31 17:54:07 +01:00
|
|
|
_, wasInsideRange := reqList.Ranges.Inside(int64(fromIndex))
|
2022-08-31 13:43:09 +01:00
|
|
|
|
2022-08-31 17:54:07 +01:00
|
|
|
var toIndex int
|
|
|
|
// modify the list
|
|
|
|
switch listOp {
|
|
|
|
case ListOpAdd:
|
|
|
|
wasInsideRange = false // can't be inside the range if this is a new room
|
2022-08-31 13:43:09 +01:00
|
|
|
list.Add(roomID)
|
2022-08-31 17:54:07 +01:00
|
|
|
// 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")
|
2023-04-05 14:11:05 +01:00
|
|
|
internal.GetSentryHubFromContextOrDefault(ctx).CaptureException(err)
|
2022-08-31 17:54:07 +01:00
|
|
|
}
|
|
|
|
// 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))
|
2022-08-31 13:43:09 +01:00
|
|
|
return
|
|
|
|
}
|
2022-08-31 17:54:07 +01:00
|
|
|
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")
|
2023-04-05 14:11:05 +01:00
|
|
|
internal.GetSentryHubFromContextOrDefault(ctx).CaptureException(err)
|
2022-08-31 17:54:07 +01:00
|
|
|
}
|
|
|
|
// find the new position of this room
|
|
|
|
toIndex, _ = list.IndexOf(roomID)
|
2022-08-31 13:43:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
bugfix: fix a bug where move updates could go missing between windows
If a room moved from one window range to another window range, and the
index of the destination was the leading edge of a different window,
this would trip up the code into thinking it was a no-op move and hence
not issue a DELETE/INSERT for the 2nd window, even though it was in fact
needed. For example:
```
w1 w2
[0,1,2] 3,4,5 [6,7,8]
Move 1 to 6 turns the list into:
[0,2,3] 4,5,6 [1,7,8]
which should be the operations:
DELETE 1, INSERT 2 (val=3)
DELETE 6, INSERT 6 (val=1)
but because DELETE/INSERT both have the same index value, and the target
room is the updated room, we thought this was the same as when you have:
[0,1,2] 3,4,5
Move 0 to 0
which should no-op.
```
Fixed by ensuring that we also check that there is only 1 move operation.
If there are >1 move operations then we are moving between lists and should
include the DELETE/INSERT operation with the same index. This could manifest
itself in updated rooms spontaneously disappearing and/or neighbouring rooms
being duplicated.
2022-09-05 16:47:05 +01:00
|
|
|
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.
|
2022-08-31 17:54:07 +01:00
|
|
|
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.
|
bugfix: ensure we always INSERT a shifted room when handling a deletion
Previously, this would fail:
```
// 0 1 2 3 4 5 6 7 8
before: []string{"a", "b", "c", "d", "e", "f", "g", "h", "i"},
after: []string{"b", "c", "d", "e", "f", "g", "h", "i"},
ranges: SliceRanges{{1, 3}, {5, 7}},
```
because the 2nd window range perfectly matched the list size, it would
ignore the `INSERT,7,i`.
2022-08-31 18:45:22 +01:00
|
|
|
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
|
|
|
|
}
|
2022-08-31 17:54:07 +01:00
|
|
|
}
|
2022-08-31 13:43:09 +01:00
|
|
|
|
2022-08-31 17:54:07 +01:00
|
|
|
swapOp := reqList.WriteSwapOp(toRoomID, listFromIndex, listToIndex)
|
2022-08-31 13:43:09 +01:00
|
|
|
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 {
|
2022-08-31 17:54:07 +01:00
|
|
|
subs = append(subs, toRoomID)
|
2022-08-31 13:43:09 +01:00
|
|
|
addedSub = true
|
|
|
|
}
|
|
|
|
|
2022-08-31 17:54:07 +01:00
|
|
|
if wasUpdatedRoomInserted && listOp == ListOpAdd {
|
2022-08-31 13:43:09 +01:00
|
|
|
if !addedSub {
|
2022-08-31 17:54:07 +01:00
|
|
|
subs = append(subs, toRoomID)
|
2022-08-31 13:43:09 +01:00
|
|
|
}
|
|
|
|
// 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 {
|
2022-08-31 17:54:07 +01:00
|
|
|
ops = append(ops, reqList.WriteInsertOp(toIndex, toRoomID))
|
2022-08-31 13:43:09 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|