Simplified Sliding Sync (#28515)

* Experimental SSS

Working branch to get SSS functional on element-web.

Requires https://github.com/matrix-org/matrix-js-sdk/pull/4400

* Adjust tests to use new behaviour

* Remove well-known proxy URL lookup; always use native

This is actually required for SSS because otherwise it would use
the proxy over native support.

* Linting

* Debug logging

* Control the race condition when swapping between rooms

* Dont' filter by space as synapse doesn't support it

* Remove SS code related to registering lists and managing ranges

- Update the spidering code to spider all the relevant lists.
- Add canonical alias to the required_state to allow room name calcs to work.

Room sort order is busted because we don't yet look at `bump_stamp`.

* User bumpStamp if it is present

* Drop initial room load from 20 per list to 10

* Half the batch size to trickle more quickly

* Prettier

* prettier on tests too

* Remove proxy URL & unused import

* Hopefully fix tests to assert what the behaviour is supposed to be

* Move the singleton to the manager tyo fix import loop

* Very well, code, I will remove you

Why were you there in the first place?

* Strip out more unused stuff

* Fix playwright test

Seems like this lack of order updating unless a room is selected
was just always a bug with both regular and non-sliding sync. I
have no idea how the test passed on develop because it won't run.

* Fix test to do maybe what it was supposed to do... possibly?

* Remove test for old pre-simplified sliding sync behaviour

* Unused import

* Remove sliding sync proxy & test

I was wrong about what this test was asserting, it was suposed
to assert that notification dots aren't shown (because SS didn't
support them somehow I guess) but they are fine in SSS so the test
is just no longer relevant.

* Remove now pointless credentials

* Remove subscription removal as SSS doesn't do that

* Update tests

* add test

* Switch to new labs flag & break if old labs flag is enabled

* Remove unused import & fix test

* Fix other test

* Remove name & description from old labs flag

as they're not displayed anywhere so not useful

* Remove old sliding sync option

by making it not a feature

* Add back unread nindicator test but inverted

and minus the bit about disabling notification which surely would have
defeated the original point anyway?

* Reinstate test for room_subscriptions

...and also make tests actually use sliding sync

* Use UserFriendlyError

* Remove empty constructor

* Remove unrelated changes

* Unused import

* Fix import

* Avoid moving import

---------

Co-authored-by: Kegan Dougal <7190048+kegsay@users.noreply.github.com>
This commit is contained in:
David Baker
2025-03-18 17:54:32 +00:00
committed by GitHub
co-authored by Kegan Dougal
parent 4fa540962a
commit f59af3786e
21 changed files with 367 additions and 1418 deletions
@@ -161,7 +161,7 @@ describe("MemberListStore", () => {
describe("sliding sync", () => {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {
return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled.
return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled.
});
client.members = jest.fn();
});
+9 -17
View File
@@ -383,43 +383,35 @@ describe("RoomViewStore", function () {
describe("Sliding Sync", function () {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {
return settingName === "feature_sliding_sync"; // this is enabled, everything else is disabled.
return settingName === "feature_simplified_sliding_sync"; // this is enabled, everything else is disabled.
});
});
it("subscribes to the room", async () => {
const setRoomVisible = jest
.spyOn(slidingSyncManager, "setRoomVisible")
.mockReturnValue(Promise.resolve(""));
const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve());
const subscribedRoomId = "!sub1:localhost";
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId });
await untilDispatch(Action.ActiveRoomChanged, dis);
expect(roomViewStore.getRoomId()).toBe(subscribedRoomId);
expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true);
expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId);
});
// Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode
// Previously a regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode
// although that was before the complexity was removed with similified mode. I've removed the complexity but kept the
// test anyway.
it("doesn't get stuck in a loop if you view rooms quickly", async () => {
const setRoomVisible = jest
.spyOn(slidingSyncManager, "setRoomVisible")
.mockReturnValue(Promise.resolve(""));
const setRoomVisible = jest.spyOn(slidingSyncManager, "setRoomVisible").mockReturnValue(Promise.resolve());
const subscribedRoomId = "!sub1:localhost";
const subscribedRoomId2 = "!sub2:localhost";
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId }, true);
dis.dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 }, true);
await untilDispatch(Action.ActiveRoomChanged, dis);
// sub(1) then unsub(1) sub(2), unsub(1)
const wantCalls = [
[subscribedRoomId, true],
[subscribedRoomId, false],
[subscribedRoomId2, true],
[subscribedRoomId, false],
];
// should view 1, then 2
const wantCalls = [[subscribedRoomId], [subscribedRoomId2]];
expect(setRoomVisible).toHaveBeenCalledTimes(wantCalls.length);
wantCalls.forEach((v, i) => {
try {
expect(setRoomVisible.mock.calls[i][0]).toEqual(v[0]);
expect(setRoomVisible.mock.calls[i][1]).toEqual(v[1]);
} catch {
throw new Error(`i=${i} got ${setRoomVisible.mock.calls[i]} want ${v}`);
}
@@ -1,341 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import { type SlidingSync, SlidingSyncEvent } from "matrix-js-sdk/src/sliding-sync";
import { Room } from "matrix-js-sdk/src/matrix";
import {
LISTS_UPDATE_EVENT,
SlidingRoomListStoreClass,
SlidingSyncSortToFilter,
} from "../../../../src/stores/room-list/SlidingRoomListStore";
import { type SpaceStoreClass } from "../../../../src/stores/spaces/SpaceStore";
import { MockEventEmitter, stubClient, untilEmission } from "../../../test-utils";
import { TestSdkContext } from "../../TestSdkContext";
import { SlidingSyncManager } from "../../../../src/SlidingSyncManager";
import { type RoomViewStore } from "../../../../src/stores/RoomViewStore";
import { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher";
import { SortAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
import { DefaultTagID, type TagID } from "../../../../src/stores/room-list/models";
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
import { LISTS_LOADING_EVENT } from "../../../../src/stores/room-list/RoomListStore";
import { UPDATE_EVENT } from "../../../../src/stores/AsyncStore";
jest.mock("../../../../src/SlidingSyncManager");
const MockSlidingSyncManager = <jest.Mock<SlidingSyncManager>>(<unknown>SlidingSyncManager);
describe("SlidingRoomListStore", () => {
let store: SlidingRoomListStoreClass;
let context: TestSdkContext;
let dis: MatrixDispatcher;
let activeSpace: string;
beforeEach(async () => {
context = new TestSdkContext();
context.client = stubClient();
context._SpaceStore = new MockEventEmitter<SpaceStoreClass>({
traverseSpace: jest.fn(),
get activeSpace() {
return activeSpace;
},
}) as SpaceStoreClass;
context._SlidingSyncManager = new MockSlidingSyncManager();
context._SlidingSyncManager.slidingSync = mocked(
new MockEventEmitter({
getListData: jest.fn(),
}) as unknown as SlidingSync,
);
context._RoomViewStore = mocked(
new MockEventEmitter({
getRoomId: jest.fn(),
}) as unknown as RoomViewStore,
);
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
ranges: [[0, 10]],
});
dis = new MatrixDispatcher();
store = new SlidingRoomListStoreClass(dis, context);
});
describe("spaces", () => {
it("alters 'filters.spaces' on the DefaultTagID.Untagged list when the selected space changes", async () => {
await store.start(); // call onReady
const spaceRoomId = "!foo:bar";
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
return listName === DefaultTagID.Untagged && !isLoading;
});
// change the active space
activeSpace = spaceRoomId;
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
await p;
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
filters: expect.objectContaining({
spaces: [spaceRoomId],
}),
});
});
it("gracefully handles subspaces in the home metaspace", async () => {
const subspace = "!sub:space";
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
(spaceId: string, fn: (roomId: string) => void) => {
fn(subspace);
},
);
activeSpace = MetaSpace.Home;
await store.start(); // call onReady
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
filters: expect.objectContaining({
spaces: [subspace],
}),
});
});
it("alters 'filters.spaces' on the DefaultTagID.Untagged list if it loads with an active space", async () => {
// change the active space before we are ready
const spaceRoomId = "!foo2:bar";
activeSpace = spaceRoomId;
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
return listName === DefaultTagID.Untagged && !isLoading;
});
await store.start(); // call onReady
await p;
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
DefaultTagID.Untagged,
expect.objectContaining({
filters: expect.objectContaining({
spaces: [spaceRoomId],
}),
}),
);
});
it("includes subspaces in 'filters.spaces' when the selected space has subspaces", async () => {
await store.start(); // call onReady
const spaceRoomId = "!foo:bar";
const subSpace1 = "!ss1:bar";
const subSpace2 = "!ss2:bar";
const p = untilEmission(store, LISTS_LOADING_EVENT, (listName, isLoading) => {
return listName === DefaultTagID.Untagged && !isLoading;
});
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
(spaceId: string, fn: (roomId: string) => void) => {
if (spaceId === spaceRoomId) {
fn(subSpace1);
fn(subSpace2);
}
},
);
// change the active space
activeSpace = spaceRoomId;
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
await p;
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
filters: expect.objectContaining({
spaces: [spaceRoomId, subSpace1, subSpace2],
}),
});
});
});
it("setTagSorting alters the 'sort' option in the list", async () => {
const tagId: TagID = "foo";
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
});
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
await store.setTagSorting(tagId, SortAlgorithm.Recent);
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
});
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
});
it("getTagsForRoom gets the tags for the room", async () => {
await store.start();
const roomA = "!a:localhost";
const roomB = "!b:localhost";
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
[DefaultTagID.Untagged]: {
joinedCount: 10,
roomIndexToRoomId: {
0: roomA,
1: roomB,
},
},
[DefaultTagID.Favourite]: {
joinedCount: 2,
roomIndexToRoomId: {
0: roomB,
},
},
};
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
return keyToListData[key] || null;
});
expect(store.getTagsForRoom(new Room(roomA, context.client!, context.client!.getUserId()!))).toEqual([
DefaultTagID.Untagged,
]);
expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()!))).toEqual([
DefaultTagID.Favourite,
DefaultTagID.Untagged,
]);
});
it("emits LISTS_UPDATE_EVENT when slidingSync lists update", async () => {
await store.start();
const roomA = "!a:localhost";
const roomB = "!b:localhost";
const roomC = "!c:localhost";
const tagId = DefaultTagID.Favourite;
const joinCount = 10;
const roomIndexToRoomId = {
// mixed to ensure we sort
1: roomB,
2: roomC,
0: roomA,
};
const rooms = [
new Room(roomA, context.client!, context.client!.getUserId()!),
new Room(roomB, context.client!, context.client!.getUserId()!),
new Room(roomC, context.client!, context.client!.getUserId()!),
];
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
switch (roomId) {
case roomA:
return rooms[0];
case roomB:
return rooms[1];
case roomC:
return rooms[2];
}
return null;
});
const p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
expect(store.getCount(tagId)).toEqual(joinCount);
expect(store.orderedLists[tagId]).toEqual(rooms);
});
it("sets the sticky room on the basis of the viewed room in RoomViewStore", async () => {
await store.start();
// seed the store with 3 rooms
const roomIdA = "!a:localhost";
const roomIdB = "!b:localhost";
const roomIdC = "!c:localhost";
const tagId = DefaultTagID.Favourite;
const joinCount = 10;
const roomIndexToRoomId = {
// mixed to ensure we sort
1: roomIdB,
2: roomIdC,
0: roomIdA,
};
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
const roomB = new Room(roomIdB, context.client!, context.client!.getUserId()!);
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
switch (roomId) {
case roomIdA:
return roomA;
case roomIdB:
return roomB;
case roomIdC:
return roomC;
}
return null;
});
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
if (key !== tagId) {
return null;
}
return {
roomIndexToRoomId: roomIndexToRoomId,
joinedCount: joinCount,
};
});
let p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
// make roomB sticky and inform the store
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdB);
context.roomViewStore.emit(UPDATE_EVENT);
// bump room C to the top, room B should not move from i=1 despite the list update saying to
roomIndexToRoomId[0] = roomIdC;
roomIndexToRoomId[1] = roomIdA;
roomIndexToRoomId[2] = roomIdB;
p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
// check that B didn't move and that A was put below B
expect(store.orderedLists[tagId]).toEqual([roomC, roomB, roomA]);
// make room C sticky: rooms should move as a result, without needing an additional list update
mocked(context.roomViewStore.getRoomId).mockReturnValue(roomIdC);
p = untilEmission(store, LISTS_UPDATE_EVENT);
context.roomViewStore.emit(UPDATE_EVENT);
await p;
expect(store.orderedLists[tagId].map((r) => r.roomId)).toEqual([roomC, roomA, roomB].map((r) => r.roomId));
});
it("gracefully handles unknown room IDs", async () => {
await store.start();
const roomIdA = "!a:localhost";
const roomIdB = "!b:localhost"; // does not exist
const roomIdC = "!c:localhost";
const roomIndexToRoomId = {
0: roomIdA,
1: roomIdB, // does not exist
2: roomIdC,
};
const tagId = DefaultTagID.Favourite;
const joinCount = 10;
// seed the store with 2 rooms
const roomA = new Room(roomIdA, context.client!, context.client!.getUserId()!);
const roomC = new Room(roomIdC, context.client!, context.client!.getUserId()!);
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
switch (roomId) {
case roomIdA:
return roomA;
case roomIdC:
return roomC;
}
return null;
});
mocked(context._SlidingSyncManager!.slidingSync!.getListData).mockImplementation((key: string) => {
if (key !== tagId) {
return null;
}
return {
roomIndexToRoomId: roomIndexToRoomId,
joinedCount: joinCount,
};
});
const p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync!.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
expect(store.orderedLists[tagId]).toEqual([roomA, roomC]);
});
});