Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

bugfix: fix a sliding sync bug which could cause rooms to loop #9268

Merged
merged 4 commits into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/stores/RoomViewStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ const INITIAL_STATE = {
joinError: null as Error,
// The room ID of the room currently being viewed
roomId: null as string,
// The room ID being subscribed to (in Sliding Sync)
subscribingRoomId: null as string,

// The event to scroll to when the room is first viewed
initialEventId: null as string,
Expand Down Expand Up @@ -286,6 +288,7 @@ export class RoomViewStore extends Store<ActionPayload> {
SlidingSyncManager.instance.setRoomVisible(this.state.roomId, false);
}
this.setState({
subscribingRoomId: payload.room_id,
roomId: payload.room_id,
initialEventId: null,
initialEventPixelOffset: null,
Expand All @@ -300,6 +303,12 @@ export class RoomViewStore extends Store<ActionPayload> {
// set this room as the room subscription. We need to await for it as this will fetch
// all room state for this room, which is required before we get the state below.
await SlidingSyncManager.instance.setRoomVisible(payload.room_id, true);
// Whilst we were subscribing another room was viewed, so stop what we're doing and
// unsubscribe
if (this.state.subscribingRoomId !== payload.room_id) {
SlidingSyncManager.instance.setRoomVisible(this.state.roomId, false);
return;
}
// Re-fire the payload: we won't re-process it because the prev room ID == payload room ID now
dis.dispatch({
...payload,
Expand Down
50 changes: 47 additions & 3 deletions test/stores/RoomViewStore-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { RoomViewStore } from '../../src/stores/RoomViewStore';
import { Action } from '../../src/dispatcher/actions';
import * as testUtils from '../test-utils';
import { flushPromises, getMockClientWithEventEmitter } from '../test-utils';
import SettingsStore from '../../src/settings/SettingsStore';
import { SlidingSyncManager } from '../../src/SlidingSyncManager';

const dispatch = testUtils.getDispatchForStore(RoomViewStore.instance);

Expand Down Expand Up @@ -47,7 +49,7 @@ describe('RoomViewStore', function() {

beforeEach(function() {
jest.clearAllMocks();
mockClient.credentials = { userId: "@test:example.com" };
mockClient.credentials = { userId: userId };
mockClient.joinRoom.mockResolvedValue(room);
mockClient.getRoom.mockReturnValue(room);
mockClient.isGuest.mockReturnValue(false);
Expand All @@ -58,7 +60,7 @@ describe('RoomViewStore', function() {

it('can be used to view a room by ID and join', async () => {
dispatch({ action: Action.ViewRoom, room_id: '!randomcharacters:aser.ver' });
dispatch({ action: 'join_room' });
dispatch({ action: Action.JoinRoom });
await flushPromises();
expect(mockClient.joinRoom).toHaveBeenCalledWith('!randomcharacters:aser.ver', { viaServers: [] });
expect(RoomViewStore.instance.isJoining()).toBe(true);
Expand All @@ -78,11 +80,53 @@ describe('RoomViewStore', function() {
expect(RoomViewStore.instance.getRoomId()).toBe(roomId);

// join the room
dispatch({ action: 'join_room' });
dispatch({ action: Action.JoinRoom });

expect(RoomViewStore.instance.isJoining()).toBeTruthy();
await flushPromises();

expect(mockClient.joinRoom).toHaveBeenCalledWith(alias, { viaServers: [] });
});

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.
});
RoomViewStore.instance.reset();
});

it("subscribes to the room", async () => {
const setRoomVisible = jest.spyOn(SlidingSyncManager.instance, "setRoomVisible").mockReturnValue(
Promise.resolve(""),
);
const subscribedRoomId = "!sub1:localhost";
dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId });
await flushPromises();
await flushPromises();
expect(RoomViewStore.instance.getRoomId()).toBe(subscribedRoomId);
expect(setRoomVisible).toHaveBeenCalledWith(subscribedRoomId, true);
});

// Regression test for an in-the-wild bug where rooms would rapidly switch forever in sliding sync mode
it("doesn't get stuck in a loop if you view rooms quickly", async () => {
const setRoomVisible = jest.spyOn(SlidingSyncManager.instance, "setRoomVisible").mockReturnValue(
Promise.resolve(""),
);
const subscribedRoomId = "!sub2:localhost";
const subscribedRoomId2 = "!sub3:localhost";
dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId });
dispatch({ action: Action.ViewRoom, room_id: subscribedRoomId2 });
// sub(1) then unsub(1) sub(2)
expect(setRoomVisible).toHaveBeenCalledTimes(3);
await flushPromises();
await flushPromises();
// this should not churn, extra call to allow unsub(1)
expect(setRoomVisible).toHaveBeenCalledTimes(4);
// flush a bit more to ensure this doesn't change
await flushPromises();
await flushPromises();
expect(setRoomVisible).toHaveBeenCalledTimes(4);
});
});
});