Merge pull request #9934 from matrix-org/kegan/lists-as-keys

refactor: sliding sync: convert to lists-as-keys rather than indexes
This commit is contained in:
Andy Balaam 2023-01-23 15:26:42 +00:00 committed by GitHub
commit 51b4555106
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 329 additions and 178 deletions

View File

@ -30,8 +30,8 @@ export default defineConfig({
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
},
env: {
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
SLIDING_SYNC_PROXY_TAG: "v0.6.0",
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
SLIDING_SYNC_PROXY_TAG: "v0.99.0-rc1",
HOMESERVER: "synapse",
},
retries: {

View File

@ -21,8 +21,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Interception } from "cypress/types/net-stubbing";
import { HomeserverInstance } from "../../plugins/utils/homeserver";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { Layout } from "../../../src/settings/enums/Layout";
import { ProxyInstance } from "../../plugins/sliding-sync";
describe("Sliding Sync", () => {
@ -102,21 +100,6 @@ describe("Sliding Sync", () => {
});
};
// sanity check everything works
it("should correctly render expected messages", () => {
cy.get<string>("@roomId").then((roomId) => cy.visit("/#/room/" + roomId));
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
// Wait until configuration is finished
cy.contains(
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
"created and configured the room.",
);
// Click "expand" link button
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
});
it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
// create rooms and check room names are correct
cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple"));

View File

@ -23,7 +23,7 @@ import { getFreePort } from "../utils/port";
import { HomeserverInstance } from "../utils/homeserver";
// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
export interface ProxyInstance {
containerId: string;
@ -72,7 +72,7 @@ async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Pr
const port = await getFreePort();
console.log(new Date(), "starting proxy container...", dockerTag);
const containerId = await dockerRun({
image: "ghcr.io/matrix-org/sliding-sync-proxy:" + dockerTag,
image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag,
containerName: "react-sdk-cypress-sliding-sync-proxy",
params: [
"--rm",

View File

@ -119,12 +119,10 @@ export class SlidingSyncManager {
public slidingSync: SlidingSync;
private client: MatrixClient;
private listIdToIndex: Record<string, number>;
private configureDefer: IDeferred<void>;
public constructor() {
this.listIdToIndex = {};
this.configureDefer = defer<void>();
}
@ -134,13 +132,18 @@ export class SlidingSyncManager {
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
this.client = client;
this.listIdToIndex = {};
// by default use the encrypted subscription as that gets everything, which is a safer
// default than potentially missing member events.
this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
this.slidingSync = new SlidingSync(
proxyUrl,
new Map(),
ENCRYPTED_SUBSCRIPTION,
client,
SLIDING_SYNC_TIMEOUT_MS,
);
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
// set the space list
this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), {
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
ranges: [[0, 20]],
sort: ["by_name"],
slow_get_all_rooms: true,
@ -173,47 +176,16 @@ export class SlidingSyncManager {
return this.slidingSync;
}
public listIdForIndex(index: number): string | null {
for (const listId in this.listIdToIndex) {
if (this.listIdToIndex[listId] === index) {
return listId;
}
}
return null;
}
/**
* Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces
* @param listId A string which represents the list.
* @returns The index to use when registering lists or listening for callbacks.
*/
public getOrAllocateListIndex(listId: string): number {
let index = this.listIdToIndex[listId];
if (index === undefined) {
// assign next highest index
index = -1;
for (const id in this.listIdToIndex) {
const listIndex = this.listIdToIndex[id];
if (listIndex > index) {
index = listIndex;
}
}
index++;
this.listIdToIndex[listId] = index;
}
return index;
}
/**
* Ensure that this list is registered.
* @param listIndex The list index to register
* @param listKey The list key to register
* @param updateArgs The fields to update on the list.
* @returns The complete list request params
*/
public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
logger.debug("ensureListRegistered:::", listIndex, updateArgs);
public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
logger.debug("ensureListRegistered:::", listKey, updateArgs);
await this.configureDefer.promise;
let list = this.slidingSync.getList(listIndex);
let list = this.slidingSync.getListParams(listKey);
if (!list) {
list = {
ranges: [[0, 20]],
@ -252,14 +224,14 @@ export class SlidingSyncManager {
try {
// if we only have range changes then call a different function so we don't nuke the list from before
if (updateArgs.ranges && Object.keys(updateArgs).length === 1) {
await this.slidingSync.setListRanges(listIndex, updateArgs.ranges);
await this.slidingSync.setListRanges(listKey, updateArgs.ranges);
} else {
await this.slidingSync.setList(listIndex, list);
await this.slidingSync.setList(listKey, list);
}
} catch (err) {
logger.debug("ensureListRegistered: update failed txn_id=", err);
}
return this.slidingSync.getList(listIndex);
return this.slidingSync.getListParams(listKey)!;
}
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
@ -304,7 +276,6 @@ export class SlidingSyncManager {
*/
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
let startIndex = batchSize;
let hasMore = true;
let firstTime = true;
@ -316,7 +287,7 @@ export class SlidingSyncManager {
[startIndex, endIndex],
];
if (firstTime) {
await this.slidingSync.setList(listIndex, {
await this.slidingSync.setList(SlidingSyncManager.ListSearch, {
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
// any changes to the list whilst spidering are caught.
ranges: ranges,
@ -342,15 +313,17 @@ export class SlidingSyncManager {
},
});
} else {
await this.slidingSync.setListRanges(listIndex, ranges);
await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges);
}
// gradually request more over time
await sleep(gapBetweenRequestsMs);
} catch (err) {
// do nothing, as we reject only when we get interrupted but that's fine as the next
// request will include our data
} finally {
// gradually request more over time, even on errors.
await sleep(gapBetweenRequestsMs);
}
hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount;
const listData = this.slidingSync.getListData(SlidingSyncManager.ListSearch)!;
hasMore = endIndex + 1 < listData.joinedCount;
startIndex += batchSize;
firstTime = false;
}

View File

@ -340,9 +340,8 @@ export default class RoomSublist extends React.Component<IProps, IState> {
private onShowAllClick = async (): Promise<void> => {
if (this.slidingSyncMode) {
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
const count = RoomListStore.instance.getCount(this.props.tagId);
await SlidingSyncManager.instance.ensureListRegistered(slidingSyncIndex, {
await SlidingSyncManager.instance.ensureListRegistered(this.props.tagId, {
ranges: [[0, count]],
});
}
@ -566,10 +565,9 @@ export default class RoomSublist extends React.Component<IProps, IState> {
let isAlphabetical = RoomListStore.instance.getTagSorting(this.props.tagId) === SortAlgorithm.Alphabetic;
let isUnreadFirst = RoomListStore.instance.getListOrder(this.props.tagId) === ListAlgorithm.Importance;
if (this.slidingSyncMode) {
const slidingSyncIndex = SlidingSyncManager.instance.getOrAllocateListIndex(this.props.tagId);
const slidingList = SlidingSyncManager.instance.slidingSync.getList(slidingSyncIndex);
isAlphabetical = slidingList.sort[0] === "by_name";
isUnreadFirst = slidingList.sort[0] === "by_notification_level";
const slidingList = SlidingSyncManager.instance.slidingSync.getListParams(this.props.tagId);
isAlphabetical = (slidingList?.sort || [])[0] === "by_name";
isUnreadFirst = (slidingList?.sort || [])[0] === "by_notification_level";
}
// Invites don't get some nonsense options, so only add them if we have to.

View File

@ -34,7 +34,6 @@ export const useSlidingSyncRoomSearch = (): {
const [rooms, setRooms] = useState<Room[]>([]);
const [loading, setLoading] = useState(false);
const listIndex = SlidingSyncManager.instance.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
const [updateQuery, updateResult] = useLatestResult<{ term: string; limit?: number }, Room[]>(setRooms);
@ -50,14 +49,16 @@ export const useSlidingSyncRoomSearch = (): {
try {
setLoading(true);
await SlidingSyncManager.instance.ensureListRegistered(listIndex, {
await SlidingSyncManager.instance.ensureListRegistered(SlidingSyncManager.ListSearch, {
ranges: [[0, limit]],
filters: {
room_name_like: term,
},
});
const rooms = [];
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(listIndex);
const { roomIndexToRoomId } = SlidingSyncManager.instance.slidingSync.getListData(
SlidingSyncManager.ListSearch,
)!;
let i = 0;
while (roomIndexToRoomId[i]) {
const roomId = roomIndexToRoomId[i];
@ -78,7 +79,7 @@ export const useSlidingSyncRoomSearch = (): {
// TODO: delete the list?
}
},
[updateQuery, updateResult, listIndex],
[updateQuery, updateResult],
);
return {

View File

@ -84,20 +84,20 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
public constructor(dis: MatrixDispatcher, private readonly context: SdkContextClass) {
super(dis);
this.setMaxListeners(20); // RoomList + LeftPanel + 8xRoomSubList + spares
this.stickyRoomId = null;
}
public async setTagSorting(tagId: TagID, sort: SortAlgorithm): Promise<void> {
logger.info("SlidingRoomListStore.setTagSorting ", tagId, sort);
this.tagIdToSortAlgo[tagId] = sort;
const slidingSyncIndex = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
switch (sort) {
case SortAlgorithm.Alphabetic:
await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, {
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
});
break;
case SortAlgorithm.Recent:
await this.context.slidingSyncManager.ensureListRegistered(slidingSyncIndex, {
await this.context.slidingSyncManager.ensureListRegistered(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
});
break;
@ -164,8 +164,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
// check all lists for each tag we know about and see if the room is there
const tags: TagID[] = [];
for (const tagId in this.tagIdToSortAlgo) {
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId);
if (!listData) {
continue;
}
@ -251,19 +250,19 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
}
// now set the rooms
const rooms = orderedRoomIds.map((roomId) => {
return this.matrixClient.getRoom(roomId);
const rooms: Room[] = [];
orderedRoomIds.forEach((roomId) => {
const room = this.matrixClient.getRoom(roomId);
if (!room) {
return;
}
rooms.push(room);
});
tagMap[tagId] = rooms;
this.tagMap = tagMap;
}
private onSlidingSyncListUpdate(
listIndex: number,
joinCount: number,
roomIndexToRoomId: Record<number, string>,
): void {
const tagId = this.context.slidingSyncManager.listIdForIndex(listIndex);
private onSlidingSyncListUpdate(tagId: string, joinCount: number, roomIndexToRoomId: Record<number, string>): void {
this.counts[tagId] = joinCount;
this.refreshOrderedLists(tagId, roomIndexToRoomId);
// let the UI update
@ -295,8 +294,7 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
if (room) {
// resort it based on the slidingSync view of the list. This may cause this old sticky
// room to cease to exist.
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
const listData = this.context.slidingSyncManager.slidingSync.getListData(index);
const listData = this.context.slidingSyncManager.slidingSync.getListData(tagId);
if (!listData) {
continue;
}
@ -334,9 +332,8 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
const sort = SortAlgorithm.Recent; // default to recency sort, TODO: read from config
this.tagIdToSortAlgo[tagId] = sort;
this.emit(LISTS_LOADING_EVENT, tagId, true);
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
this.context.slidingSyncManager
.ensureListRegistered(index, {
.ensureListRegistered(tagId, {
filters: filter,
sort: SlidingSyncSortToFilter[sort],
})
@ -361,15 +358,17 @@ export class SlidingRoomListStoreClass extends AsyncStoreWithClient<IState> impl
if (roomId === activeSpace) {
return;
}
if (!filters.spaces) {
filters.spaces = [];
}
filters.spaces.push(roomId); // add subspace
},
false,
);
this.emit(LISTS_LOADING_EVENT, tagId, true);
const index = this.context.slidingSyncManager.getOrAllocateListIndex(tagId);
this.context.slidingSyncManager
.ensureListRegistered(index, {
.ensureListRegistered(tagId, {
filters: filters,
})
.then(() => {

View File

@ -78,13 +78,76 @@ describe("SlidingSyncManager", () => {
});
});
describe("ensureListRegistered", () => {
it("creates a new list based on the key", async () => {
const listKey = "key";
mocked(slidingSync.getListParams).mockReturnValue(null);
mocked(slidingSync.setList).mockResolvedValue("yep");
await manager.ensureListRegistered(listKey, {
sort: ["by_recency"],
});
expect(slidingSync.setList).toBeCalledWith(
listKey,
expect.objectContaining({
sort: ["by_recency"],
}),
);
});
it("updates an existing list based on the key", async () => {
const listKey = "key";
mocked(slidingSync.getListParams).mockReturnValue({
ranges: [[0, 42]],
});
mocked(slidingSync.setList).mockResolvedValue("yep");
await manager.ensureListRegistered(listKey, {
sort: ["by_recency"],
});
expect(slidingSync.setList).toBeCalledWith(
listKey,
expect.objectContaining({
sort: ["by_recency"],
ranges: [[0, 42]],
}),
);
});
it("updates ranges on an existing list based on the key if there's no other changes", async () => {
const listKey = "key";
mocked(slidingSync.getListParams).mockReturnValue({
ranges: [[0, 42]],
});
mocked(slidingSync.setList).mockResolvedValue("yep");
await manager.ensureListRegistered(listKey, {
ranges: [[0, 52]],
});
expect(slidingSync.setList).not.toBeCalled();
expect(slidingSync.setListRanges).toBeCalledWith(listKey, [[0, 52]]);
});
it("no-ops for idential changes", async () => {
const listKey = "key";
mocked(slidingSync.getListParams).mockReturnValue({
ranges: [[0, 42]],
sort: ["by_recency"],
});
mocked(slidingSync.setList).mockResolvedValue("yep");
await manager.ensureListRegistered(listKey, {
ranges: [[0, 42]],
sort: ["by_recency"],
});
expect(slidingSync.setList).not.toBeCalled();
expect(slidingSync.setListRanges).not.toBeCalled();
});
});
describe("startSpidering", () => {
it("requests in batchSizes", async () => {
const gapMs = 1;
const batchSize = 10;
mocked(slidingSync.setList).mockResolvedValue("yep");
mocked(slidingSync.setListRanges).mockResolvedValue("yep");
mocked(slidingSync.getListData).mockImplementation((i) => {
mocked(slidingSync.getListData).mockImplementation((key) => {
return {
joinedCount: 64,
roomIndexToRoomId: {},
@ -106,24 +169,24 @@ describe("SlidingSyncManager", () => {
wantWindows.forEach((range, i) => {
if (i === 0) {
expect(slidingSync.setList).toBeCalledWith(
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
SlidingSyncManager.ListSearch,
expect.objectContaining({
ranges: [[0, batchSize - 1], range],
}),
);
return;
}
expect(slidingSync.setListRanges).toBeCalledWith(
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
[[0, batchSize - 1], range],
);
expect(slidingSync.setListRanges).toBeCalledWith(SlidingSyncManager.ListSearch, [
[0, batchSize - 1],
range,
]);
});
});
it("handles accounts with zero rooms", async () => {
const gapMs = 1;
const batchSize = 10;
mocked(slidingSync.setList).mockResolvedValue("yep");
mocked(slidingSync.getListData).mockImplementation((i) => {
mocked(slidingSync.getListData).mockImplementation((key) => {
return {
joinedCount: 0,
roomIndexToRoomId: {},
@ -133,7 +196,7 @@ describe("SlidingSyncManager", () => {
expect(slidingSync.getListData).toBeCalledTimes(1);
expect(slidingSync.setList).toBeCalledTimes(1);
expect(slidingSync.setList).toBeCalledWith(
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
SlidingSyncManager.ListSearch,
expect.objectContaining({
ranges: [
[0, batchSize - 1],
@ -146,7 +209,7 @@ describe("SlidingSyncManager", () => {
const gapMs = 1;
const batchSize = 10;
mocked(slidingSync.setList).mockRejectedValue("narp");
mocked(slidingSync.getListData).mockImplementation((i) => {
mocked(slidingSync.getListData).mockImplementation((key) => {
return {
joinedCount: 0,
roomIndexToRoomId: {},
@ -156,7 +219,7 @@ describe("SlidingSyncManager", () => {
expect(slidingSync.getListData).toBeCalledTimes(1);
expect(slidingSync.setList).toBeCalledTimes(1);
expect(slidingSync.setList).toBeCalledWith(
manager.getOrAllocateListIndex(SlidingSyncManager.ListSearch),
SlidingSyncManager.ListSearch,
expect.objectContaining({
ranges: [
[0, batchSize - 1],

View File

@ -0,0 +1,111 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line deprecate/import
import { mount } from "enzyme";
import { sleep } from "matrix-js-sdk/src/utils";
import React from "react";
import { act } from "react-dom/test-utils";
import { mocked } from "jest-mock";
import { SlidingSync } from "matrix-js-sdk/src/sliding-sync";
import { Room } from "matrix-js-sdk/src/matrix";
import { SlidingSyncRoomSearchOpts, useSlidingSyncRoomSearch } from "../../src/hooks/useSlidingSyncRoomSearch";
import { MockEventEmitter, stubClient } from "../test-utils";
import { SlidingSyncManager } from "../../src/SlidingSyncManager";
type RoomSearchHook = {
loading: boolean;
rooms: Room[];
search(opts: SlidingSyncRoomSearchOpts): Promise<boolean>;
};
// hooks must be inside a React component else you get:
// "Invalid hook call. Hooks can only be called inside of the body of a function component."
function RoomSearchComponent(props: { onClick: (h: RoomSearchHook) => void }) {
const roomSearch = useSlidingSyncRoomSearch();
return <div onClick={() => props.onClick(roomSearch)} />;
}
describe("useSlidingSyncRoomSearch", () => {
it("should display rooms when searching", async () => {
const client = stubClient();
const roomA = new Room("!a:localhost", client, client.getUserId()!);
const roomB = new Room("!b:localhost", client, client.getUserId()!);
const slidingSync = mocked(
new MockEventEmitter({
getListData: jest.fn(),
}) as unknown as SlidingSync,
);
jest.spyOn(SlidingSyncManager.instance, "ensureListRegistered").mockResolvedValue({
ranges: [[0, 9]],
});
SlidingSyncManager.instance.slidingSync = slidingSync;
mocked(slidingSync.getListData).mockReturnValue({
joinedCount: 2,
roomIndexToRoomId: {
0: roomA.roomId,
1: roomB.roomId,
},
});
mocked(client.getRoom).mockImplementation((roomId) => {
switch (roomId) {
case roomA.roomId:
return roomA;
case roomB.roomId:
return roomB;
default:
return null;
}
});
// first check that everything is empty and then do the search
let executeHook = (roomSearch: RoomSearchHook) => {
expect(roomSearch.loading).toBe(false);
expect(roomSearch.rooms).toEqual([]);
roomSearch.search({
limit: 10,
query: "foo",
});
};
const wrapper = mount(
<RoomSearchComponent
onClick={(roomSearch: RoomSearchHook) => {
executeHook(roomSearch);
}}
/>,
);
// run the query
await act(async () => {
await sleep(1);
wrapper.simulate("click");
return act(() => sleep(1));
});
// now we expect there to be rooms
executeHook = (roomSearch) => {
expect(roomSearch.loading).toBe(false);
expect(roomSearch.rooms).toEqual([roomA, roomB]);
};
// run the query
await act(async () => {
await sleep(1);
wrapper.simulate("click");
return act(() => sleep(1));
});
});
});

View File

@ -30,7 +30,7 @@ import { RoomViewStore } from "../../../src/stores/RoomViewStore";
import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher";
import { SortAlgorithm } from "../../../src/stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../src/stores/room-list/models";
import { UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces";
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";
@ -42,7 +42,6 @@ describe("SlidingRoomListStore", () => {
let context: TestSdkContext;
let dis: MatrixDispatcher;
let activeSpace: string;
let tagIdToIndex = {};
beforeEach(async () => {
context = new TestSdkContext();
@ -64,27 +63,6 @@ describe("SlidingRoomListStore", () => {
getRoomId: jest.fn(),
}) as unknown as RoomViewStore,
);
// mock implementations to allow the store to map tag IDs to sliding sync list indexes and vice versa
let index = 0;
tagIdToIndex = {};
mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockImplementation((listId: string): number => {
if (tagIdToIndex[listId] != null) {
return tagIdToIndex[listId];
}
tagIdToIndex[listId] = index;
index++;
return index;
});
mocked(context.slidingSyncManager.listIdForIndex).mockImplementation((i) => {
for (const tagId in tagIdToIndex) {
const j = tagIdToIndex[tagId];
if (i === j) {
return tagId;
}
}
return null;
});
mocked(context._SlidingSyncManager.ensureListRegistered).mockResolvedValue({
ranges: [[0, 10]],
});
@ -104,17 +82,31 @@ describe("SlidingRoomListStore", () => {
// change the active space
activeSpace = spaceRoomId;
context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
await p;
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
tagIdToIndex[DefaultTagID.Untagged],
{
filters: expect.objectContaining({
spaces: [spaceRoomId],
}),
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 () => {
@ -126,8 +118,8 @@ describe("SlidingRoomListStore", () => {
});
await store.start(); // call onReady
await p;
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
tagIdToIndex[DefaultTagID.Untagged],
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(
DefaultTagID.Untagged,
expect.objectContaining({
filters: expect.objectContaining({
spaces: [spaceRoomId],
@ -146,7 +138,7 @@ describe("SlidingRoomListStore", () => {
return listName === DefaultTagID.Untagged && !isLoading;
});
mocked(context._SpaceStore.traverseSpace).mockImplementation(
mocked(context._SpaceStore!.traverseSpace).mockImplementation(
(spaceId: string, fn: (roomId: string) => void) => {
if (spaceId === spaceRoomId) {
fn(subSpace1);
@ -157,31 +149,27 @@ describe("SlidingRoomListStore", () => {
// change the active space
activeSpace = spaceRoomId;
context._SpaceStore.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
context._SpaceStore!.emit(UPDATE_SELECTED_SPACE, spaceRoomId, false);
await p;
expect(context._SlidingSyncManager.ensureListRegistered).toHaveBeenCalledWith(
tagIdToIndex[DefaultTagID.Untagged],
{
filters: expect.objectContaining({
spaces: [spaceRoomId, subSpace1, subSpace2],
}),
},
);
expect(context._SlidingSyncManager!.ensureListRegistered).toHaveBeenCalledWith(DefaultTagID.Untagged, {
filters: expect.objectContaining({
spaces: [spaceRoomId, subSpace1, subSpace2],
}),
});
});
});
it("setTagSorting alters the 'sort' option in the list", async () => {
mocked(context._SlidingSyncManager.getOrAllocateListIndex).mockReturnValue(0);
const tagId: TagID = "foo";
await store.setTagSorting(tagId, SortAlgorithm.Alphabetic);
expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, {
expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Alphabetic],
});
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Alphabetic);
await store.setTagSorting(tagId, SortAlgorithm.Recent);
expect(context._SlidingSyncManager.ensureListRegistered).toBeCalledWith(0, {
expect(context._SlidingSyncManager!.ensureListRegistered).toBeCalledWith(tagId, {
sort: SlidingSyncSortToFilter[SortAlgorithm.Recent],
});
expect(store.getTagSorting(tagId)).toEqual(SortAlgorithm.Recent);
@ -189,33 +177,31 @@ describe("SlidingRoomListStore", () => {
it("getTagsForRoom gets the tags for the room", async () => {
await store.start();
const untaggedIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Untagged);
const favIndex = context._SlidingSyncManager.getOrAllocateListIndex(DefaultTagID.Favourite);
const roomA = "!a:localhost";
const roomB = "!b:localhost";
const indexToListData = {
[untaggedIndex]: {
const keyToListData: Record<string, { joinedCount: number; roomIndexToRoomId: Record<number, string> }> = {
[DefaultTagID.Untagged]: {
joinedCount: 10,
roomIndexToRoomId: {
0: roomA,
1: roomB,
},
},
[favIndex]: {
[DefaultTagID.Favourite]: {
joinedCount: 2,
roomIndexToRoomId: {
0: roomB,
},
},
};
mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => {
return indexToListData[i] || null;
mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => {
return keyToListData[key] || null;
});
expect(store.getTagsForRoom(new Room(roomA, context.client, context.client.getUserId()))).toEqual([
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([
expect(store.getTagsForRoom(new Room(roomB, context.client!, context.client!.getUserId()))).toEqual([
DefaultTagID.Favourite,
DefaultTagID.Untagged,
]);
@ -227,7 +213,6 @@ describe("SlidingRoomListStore", () => {
const roomB = "!b:localhost";
const roomC = "!c:localhost";
const tagId = DefaultTagID.Favourite;
const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId);
const joinCount = 10;
const roomIndexToRoomId = {
// mixed to ensure we sort
@ -236,11 +221,11 @@ describe("SlidingRoomListStore", () => {
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()),
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) => {
mocked(context.client!.getRoom).mockImplementation((roomId: string) => {
switch (roomId) {
case roomA:
return rooms[0];
@ -252,7 +237,7 @@ describe("SlidingRoomListStore", () => {
return null;
});
const p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
expect(store.getCount(tagId)).toEqual(joinCount);
expect(store.orderedLists[tagId]).toEqual(rooms);
@ -265,7 +250,6 @@ describe("SlidingRoomListStore", () => {
const roomIdB = "!b:localhost";
const roomIdC = "!c:localhost";
const tagId = DefaultTagID.Favourite;
const listIndex = context.slidingSyncManager.getOrAllocateListIndex(tagId);
const joinCount = 10;
const roomIndexToRoomId = {
// mixed to ensure we sort
@ -273,10 +257,10 @@ describe("SlidingRoomListStore", () => {
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) => {
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;
@ -287,8 +271,8 @@ describe("SlidingRoomListStore", () => {
}
return null;
});
mocked(context._SlidingSyncManager.slidingSync.getListData).mockImplementation((i: number) => {
if (i !== listIndex) {
mocked(context._SlidingSyncManager!.slidingSync.getListData).mockImplementation((key: string) => {
if (key !== tagId) {
return null;
}
return {
@ -297,7 +281,7 @@ describe("SlidingRoomListStore", () => {
};
});
let p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
expect(store.orderedLists[tagId]).toEqual([roomA, roomB, roomC]);
@ -310,7 +294,7 @@ describe("SlidingRoomListStore", () => {
roomIndexToRoomId[1] = roomIdA;
roomIndexToRoomId[2] = roomIdB;
p = untilEmission(store, LISTS_UPDATE_EVENT);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, listIndex, joinCount, roomIndexToRoomId);
context.slidingSyncManager.slidingSync.emit(SlidingSyncEvent.List, tagId, joinCount, roomIndexToRoomId);
await p;
// check that B didn't move and that A was put below B
@ -323,4 +307,43 @@ describe("SlidingRoomListStore", () => {
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]);
});
});