1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-07-31 15:24:23 +03:00

Timeline needs to refresh when we see a MSC2716 marker event (#2299)

Inform the client that historical messages were imported in the timeline and they should refresh the timeline in order to see the new events.

Companion `matrix-react-sdk` PR: https://github.com/matrix-org/matrix-react-sdk/pull/8354

The `marker` events are being used as state now because this way they can't be lost in a timeline gap. Regardless of when they were sent, we will still have the latest version of the state to compare against. Any time we see our latest state value change for marker events, prompt the user that the timeline needs to refresh.

> In a [sync meeting with @ara4n](https://docs.google.com/document/d/1KCEmpnGr4J-I8EeaVQ8QJZKBDu53ViI7V62y5BzfXr0/edit#bookmark=id.67nio1ka8znc), we came up with the idea to make the `marker` events as state events. When the client sees that the `m.room.marker` state changed to a different event ID, it can throw away all of the timeline and re-fetch as needed.
>
> For homeservers where the [same problem](https://github.com/matrix-org/matrix-doc/pull/2716#discussion_r782499674) can happen, we probably don't want to throw away the whole timeline but it can go up the `unsigned.replaces_state` chain of the `m.room.marker` state events to get them all.
>
> In terms of state performance, there could be thousands of `marker` events in a room but it's no different than room members joining and leaving over and over like an IRC room.
>
> *-- https://github.com/matrix-org/matrix-spec-proposals/pull/2716#discussion_r782629097*


### Why are we just setting `timlineNeedsRefresh` (and [prompting the user](https://github.com/matrix-org/matrix-react-sdk/pull/8354)) instead of automatically refreshing the timeline for the user?

If we refreshed the timeline automatically, someone could cause your Element client to constantly refresh the timeline by just sending marker events over and over. Granted, you probably want to leave a room like this 🤷. Perhaps also some sort of DOS vector since everyone will be refreshing and hitting the server at the exact same time.

In order to avoid the timeline maybe going blank during the refresh, we could re-fetch the new events first, then replace the timeline. But the points above still stand on why we shouldn't.
This commit is contained in:
Eric Eastwood
2022-06-01 16:31:20 -05:00
committed by GitHub
parent 2e27a4134c
commit b64dbdce74
16 changed files with 1635 additions and 125 deletions

View File

@ -540,6 +540,77 @@ describe("MatrixClient event timelines", function() {
});
});
describe("getLatestTimeline", function() {
it("should create a new timeline for new events", function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
const latestMessageId = 'event1:bar';
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.respond(200, function() {
return {
chunk: [{
event_id: latestMessageId,
}],
};
});
httpBackend.when("GET", `/rooms/!foo%3Abar/context/${encodeURIComponent(latestMessageId)}`)
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
ROOM_NAME_EVENT,
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
return Promise.all([
client.getLatestTimeline(timelineSet).then(function(tl) {
// Instead of this assertion logic, we could just add a spy
// for `getEventTimeline` and make sure it's called with the
// correct parameters. This doesn't feel too bad to make sure
// `getLatestTimeline` is doing the right thing though.
expect(tl.getEvents().length).toEqual(4);
for (let i = 0; i < 4; i++) {
expect(tl.getEvents()[i].event).toEqual(EVENTS[i]);
expect(tl.getEvents()[i].sender.name).toEqual(userName);
}
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
.toEqual("start_token");
expect(tl.getPaginationToken(EventTimeline.FORWARDS))
.toEqual("end_token");
}),
httpBackend.flushAllExpected(),
]);
});
it("should throw error when /messages does not return a message", () => {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
httpBackend.when("GET", "/rooms/!foo%3Abar/messages")
.respond(200, () => {
return {
chunk: [
// No messages to return
],
};
});
return Promise.all([
expect(client.getLatestTimeline(timelineSet)).rejects.toThrow(),
httpBackend.flushAllExpected(),
]);
});
});
describe("paginateEventTimeline", function() {
it("should allow you to paginate backwards", function() {
const room = client.getRoom(roomId);

View File

@ -1,5 +1,6 @@
import * as utils from "../test-utils/test-utils";
import { EventStatus } from "../../src/models/event";
import { RoomEvent } from "../../src";
import { TestClient } from "../TestClient";
describe("MatrixClient room timelines", function() {
@ -579,7 +580,7 @@ describe("MatrixClient room timelines", function() {
});
});
it("should emit a 'Room.timelineReset' event", function() {
it("should emit a `RoomEvent.TimelineReset` event when the sync response is `limited`", function() {
const eventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
@ -608,4 +609,271 @@ describe("MatrixClient room timelines", function() {
});
});
});
describe('Refresh live timeline', () => {
const initialSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
utils.mkMessage({ user: userId, room: roomId }),
];
const contextUrl = `/rooms/${encodeURIComponent(roomId)}/context/` +
`${encodeURIComponent(initialSyncEventData[2].event_id)}`;
const contextResponse = {
start: "start_token",
events_before: [initialSyncEventData[1], initialSyncEventData[0]],
event: initialSyncEventData[2],
events_after: [],
state: [
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
let room;
beforeEach(async () => {
setNextSyncData(initialSyncEventData);
// Create a room from the sync
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
]);
// Get the room after the first sync so the room is created
room = client.getRoom(roomId);
expect(room).toBeTruthy();
});
it('should clear and refresh messages in timeline', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return contextResponse;
});
// Refresh the timeline.
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
]);
// Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
initialSyncEventData[0].event_id,
initialSyncEventData[1].event_id,
initialSyncEventData[2].event_id,
]);
});
it('Perfectly merges timelines if a sync finishes while refreshing the timeline', async () => {
// `/context` request for `refreshLiveTimeline()` ->
// `getEventTimeline()` to construct a new timeline from.
//
// We only resolve this request after we detect that the timeline
// was reset(when it goes blank) and force a sync to happen in the
// middle of all of this refresh timeline logic. We want to make
// sure the sync pagination still works as expected after messing
// the refresh timline logic messes with the pagination tokens.
httpBackend.when("GET", contextUrl)
.respond(200, () => {
// Now finally return and make the `/context` request respond
return contextResponse;
});
// Wait for the timeline to reset(when it goes blank) which means
// it's in the middle of the refrsh logic right before the
// `getEventTimeline()` -> `/context`. Then simulate a racey `/sync`
// to happen in the middle of all of this refresh timeline logic. We
// want to make sure the sync pagination still works as expected
// after messing the refresh timline logic messes with the
// pagination tokens.
//
// We define this here so the event listener is in place before we
// call `room.refreshLiveTimeline()`.
const racingSyncEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
const waitForRaceySyncAfterResetPromise = new Promise((resolve, reject) => {
let eventFired = false;
// Throw a more descriptive error if this part of the test times out.
const failTimeout = setTimeout(() => {
if (eventFired) {
reject(new Error(
'TestError: `RoomEvent.TimelineReset` fired but we timed out trying to make' +
'a `/sync` happen in time.',
));
} else {
reject(new Error(
'TestError: Timed out while waiting for `RoomEvent.TimelineReset` to fire.',
));
}
}, 4000 /* FIXME: Is there a way to reference the current timeout of this test in Jest? */);
room.on(RoomEvent.TimelineReset, async () => {
try {
eventFired = true;
// The timeline should be cleared at this point in the refresh
expect(room.getUnfilteredTimelineSet().getLiveTimeline().getEvents().length).toEqual(0);
// Then make a `/sync` happen by sending a message and seeing that it
// shows up (simulate a /sync naturally racing with us).
setNextSyncData(racingSyncEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flush("/sync", 1),
utils.syncPromise(client, 1),
]);
// Make sure the timeline has the racey sync data
const afterRaceySyncTimelineEvents = room
.getUnfilteredTimelineSet()
.getLiveTimeline()
.getEvents();
const afterRaceySyncTimelineEventIds = afterRaceySyncTimelineEvents
.map((event) => event.getId());
expect(afterRaceySyncTimelineEventIds).toEqual([
racingSyncEventData[0].event_id,
]);
clearTimeout(failTimeout);
resolve();
} catch (err) {
reject(err);
}
});
});
// Refresh the timeline. Just start the function, we will wait for
// it to finish after the racey sync.
const refreshLiveTimelinePromise = room.refreshLiveTimeline();
await waitForRaceySyncAfterResetPromise;
await Promise.all([
refreshLiveTimelinePromise,
// Then flush the remaining `/context` to left the refresh logic complete
httpBackend.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline.
const afterRefreshEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
]);
// Make sure the timeline includes the the events from the `/sync`
// that raced and beat us in the middle of everything and the
// `/sync` after the refresh. Since the `/sync` beat us to create
// the timeline, `initialSyncEventData` won't be visible unless we
// paginate backwards with `/messages`.
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
racingSyncEventData[0].event_id,
afterRefreshEventData[0].event_id,
]);
});
it('Timeline recovers after `/context` request to generate new timeline fails', async () => {
// `/context` request for `refreshLiveTimeline()` -> `getEventTimeline()`
// to construct a new timeline from.
httpBackend.when("GET", contextUrl)
.respond(500, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return {
errcode: 'TEST_FAKE_ERROR',
error: 'We purposely intercepted this /context request to make it fail ' +
'in order to test whether the refresh timeline code is resilient',
};
});
// Refresh the timeline and expect it to fail
const settledFailedRefreshPromises = await Promise.allSettled([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
]);
// We only expect `TEST_FAKE_ERROR` here. Anything else is
// unexpected and should fail the test.
if (settledFailedRefreshPromises[0].status === 'fulfilled') {
throw new Error('Expected the /context request to fail with a 500');
} else if (settledFailedRefreshPromises[0].reason.errcode !== 'TEST_FAKE_ERROR') {
throw settledFailedRefreshPromises[0].reason;
}
// The timeline will be empty after we refresh the timeline and fail
// to construct a new timeline.
expect(room.timeline.length).toEqual(0);
// `/messages` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` to construct a new timeline from.
httpBackend.when("GET", `/rooms/${encodeURIComponent(roomId)}/messages`)
.respond(200, function() {
return {
chunk: [{
// The latest message in the room
event_id: initialSyncEventData[2].event_id,
}],
};
});
// `/context` request for `refreshLiveTimeline()` ->
// `getLatestTimeline()` -> `getEventTimeline()` to construct a new
// timeline from.
httpBackend.when("GET", contextUrl)
.respond(200, function() {
// The timeline should be cleared at this point in the refresh
expect(room.timeline.length).toEqual(0);
return contextResponse;
});
// Refresh the timeline again but this time it should pass
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
]);
// Make sure sync pagination still works by seeing a new message show up
// after refreshing the timeline.
const afterRefreshEventData = [
utils.mkMessage({ user: userId, room: roomId }),
];
setNextSyncData(afterRefreshEventData);
httpBackend.when("GET", "/sync").respond(200, function() {
return NEXT_SYNC_DATA;
});
await Promise.all([
httpBackend.flushAllExpected(),
utils.syncPromise(client, 1),
]);
// Make sure the message are visible
const resultantEventsInTimeline = room.getUnfilteredTimelineSet().getLiveTimeline().getEvents();
const resultantEventIdsInTimeline = resultantEventsInTimeline.map((event) => event.getId());
expect(resultantEventIdsInTimeline).toEqual([
initialSyncEventData[0].event_id,
initialSyncEventData[1].event_id,
initialSyncEventData[2].event_id,
afterRefreshEventData[0].event_id,
]);
});
});
});

View File

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventTimeline, MatrixEvent, RoomEvent } from "../../src";
import { EventTimeline, MatrixEvent, RoomEvent, RoomStateEvent, RoomMemberEvent } from "../../src";
import { UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
import * as utils from "../test-utils/test-utils";
import { TestClient } from "../TestClient";
@ -76,7 +77,7 @@ describe("MatrixClient syncing", function() {
});
});
it("should emit Room.myMembership for invite->leave->invite cycles", async () => {
it("should emit RoomEvent.MyMembership for invite->leave->invite cycles", async () => {
const roomId = "!cycles:example.org";
// First sync: an invite
@ -298,7 +299,7 @@ describe("MatrixClient syncing", function() {
httpBackend.when("GET", "/sync").respond(200, syncData);
let latestFiredName = null;
client.on("RoomMember.name", function(event, m) {
client.on(RoomMemberEvent.Name, function(event, m) {
if (m.userId === userC && m.roomId === roomOne) {
latestFiredName = m.name;
}
@ -582,6 +583,477 @@ describe("MatrixClient syncing", function() {
xit("should update the room topic", function() {
});
describe("onMarkerStateEvent", () => {
const normalMessageEvent = utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello",
});
it('new marker event *NOT* from the room creator in a subsequent syncs ' +
'should *NOT* mark the timeline as needing a refresh', async () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create", room: roomOne, user: otherUserId,
content: {
creator: otherUserId,
room_version: '9',
},
});
const normalFirstSync = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
normalFirstSync.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
};
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should normally trigger
// `timelineNeedsRefresh=true` but this marker isn't
// being sent by the room creator so it has no
// special meaning in existing room versions.
utils.mkEvent({
type: UNSTABLE_MSC2716_MARKER.name,
room: roomOne,
// The important part we're testing is here!
// `userC` is not the room creator.
user: userC,
skey: "",
content: {
"m.insertion_id": "$abc",
},
}),
],
prev_batch: "pagTok",
},
};
// Ensure the marker is being sent by someone who is not the room creator
// because this is the main thing we're testing in this spec.
const markerEvent = nextSyncData.rooms.join[roomOne].timeline.events[0];
expect(markerEvent.sender).toBeDefined();
expect(markerEvent.sender).not.toEqual(roomCreateEvent.sender);
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]);
const room = client.getRoom(roomOne);
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
[{
label: 'In existing room versions (when the room creator sends the MSC2716 events)',
roomVersion: '9',
}, {
label: 'In a MSC2716 supported room version',
roomVersion: 'org.matrix.msc2716v3',
}].forEach((testMeta) => {
describe(testMeta.label, () => {
const roomCreateEvent = utils.mkEvent({
type: "m.room.create", room: roomOne, user: otherUserId,
content: {
creator: otherUserId,
room_version: testMeta.roomVersion,
},
});
const markerEventFromRoomCreator = utils.mkEvent({
type: UNSTABLE_MSC2716_MARKER.name, room: roomOne, user: otherUserId,
skey: "",
content: {
"m.insertion_id": "$abc",
},
});
const normalFirstSync = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
normalFirstSync.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
};
it('no marker event in sync response '+
'should *NOT* mark the timeline as needing a refresh (check for a sane default)', async () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
const room = client.getRoom(roomOne);
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
it('marker event already sent within timeline range when you join ' +
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [markerEventFromRoomCreator],
prev_batch: "pagTok",
},
state: {
events: [roomCreateEvent],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
const room = client.getRoom(roomOne);
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
it('marker event already sent before joining (in state) ' +
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
const syncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
syncData.rooms.join[roomOne] = {
timeline: {
events: [normalMessageEvent],
prev_batch: "pagTok",
},
state: {
events: [
roomCreateEvent,
markerEventFromRoomCreator,
],
},
};
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
const room = client.getRoom(roomOne);
expect(room.getTimelineNeedsRefresh()).toEqual(false);
});
it('new marker event in a subsequent syncs timeline range ' +
'should mark the timeline as needing a refresh', async () => {
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
// In subsequent syncs, a marker event in timeline
// range should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
prev_batch: "pagTok",
},
};
const markerEventId = nextSyncData.rooms.join[roomOne].timeline.events[0].event_id;
// Only do the first sync
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
// Get the room after the first sync so the room is created
const room = client.getRoom(roomOne);
let emitCount = 0;
room.on(RoomEvent.HistoryImportedWithinTimeline, function(markerEvent, room) {
expect(markerEvent.getId()).toEqual(markerEventId);
expect(room.roomId).toEqual(roomOne);
emitCount += 1;
});
// Now do a subsequent sync with the marker event
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
expect(room.getTimelineNeedsRefresh()).toEqual(true);
// Make sure `RoomEvent.HistoryImportedWithinTimeline` was emitted
expect(emitCount).toEqual(1);
});
// Mimic a marker event being sent far back in the scroll back but since our last sync
it('new marker event in sync state should mark the timeline as needing a refresh', async () => {
const nextSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
nextSyncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "hello again",
}),
],
prev_batch: "pagTok",
},
state: {
events: [
// In subsequent syncs, a marker event in state
// should trigger `timelineNeedsRefresh=true`
markerEventFromRoomCreator,
],
},
};
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(2),
]);
const room = client.getRoom(roomOne);
expect(room.getTimelineNeedsRefresh()).toEqual(true);
});
});
});
});
// Make sure the state listeners work and events are re-emitted properly from
// the client regardless if we reset and refresh the timeline.
describe('state listeners and re-registered when RoomEvent.CurrentStateUpdated is fired', () => {
const EVENTS = [
utils.mkMessage({
room: roomOne, user: userA, msg: "we",
}),
utils.mkMessage({
room: roomOne, user: userA, msg: "could",
}),
utils.mkMessage({
room: roomOne, user: userA, msg: "be",
}),
utils.mkMessage({
room: roomOne, user: userA, msg: "heroes",
}),
];
const SOME_STATE_EVENT = utils.mkEvent({
event: true,
type: 'org.matrix.test_state',
room: roomOne,
user: userA,
skey: "",
content: {
"foo": "bar",
},
});
const USER_MEMBERSHIP_EVENT = utils.mkMembership({
room: roomOne, mship: "join", user: userA,
});
// This appears to work even if we comment out
// `RoomEvent.CurrentStateUpdated` part which triggers everything to
// re-listen after the `room.currentState` reference changes. I'm
// not sure how it's getting re-emitted.
it("should be able to listen to state events even after " +
"the timeline is reset during `limited` sync response", async () => {
// Create a room from the sync
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
// Get the room after the first sync so the room is created
const room = client.getRoom(roomOne);
expect(room).toBeTruthy();
let stateEventEmitCount = 0;
client.on(RoomStateEvent.Update, () => {
stateEventEmitCount += 1;
});
// Cause `RoomStateEvent.Update` to be fired
room.currentState.setStateEvents([SOME_STATE_EVENT]);
// Make sure we can listen to the room state events before the reset
expect(stateEventEmitCount).toEqual(1);
// Make a `limited` sync which will cause a `room.resetLiveTimeline`
const limitedSyncData = {
next_batch: "batch_token",
rooms: {
join: {},
},
};
limitedSyncData.rooms.join[roomOne] = {
timeline: {
events: [
utils.mkMessage({
room: roomOne, user: otherUserId, msg: "world",
}),
],
// The important part, make the sync `limited`
limited: true,
prev_batch: "newerTok",
},
};
httpBackend.when("GET", "/sync").respond(200, limitedSyncData);
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
// This got incremented again from processing the sync response
expect(stateEventEmitCount).toEqual(2);
// Cause `RoomStateEvent.Update` to be fired
room.currentState.setStateEvents([SOME_STATE_EVENT]);
// Make sure we can still listen to the room state events after the reset
expect(stateEventEmitCount).toEqual(3);
});
// Make sure it re-registers the state listeners after the
// `room.currentState` reference changes
it("should be able to listen to state events even after " +
"refreshing the timeline", async () => {
const testClientWithTimelineSupport = new TestClient(
selfUserId,
"DEVICE",
selfAccessToken,
undefined,
{ timelineSupport: true },
);
httpBackend = testClientWithTimelineSupport.httpBackend;
httpBackend.when("GET", "/versions").respond(200, {});
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
client = testClientWithTimelineSupport.client;
// Create a room from the sync
httpBackend.when("GET", "/sync").respond(200, syncData);
client.startClient();
await Promise.all([
httpBackend.flushAllExpected(),
awaitSyncEvent(),
]);
// Get the room after the first sync so the room is created
const room = client.getRoom(roomOne);
expect(room).toBeTruthy();
let stateEventEmitCount = 0;
client.on(RoomStateEvent.Update, () => {
stateEventEmitCount += 1;
});
// Cause `RoomStateEvent.Update` to be fired
room.currentState.setStateEvents([SOME_STATE_EVENT]);
// Make sure we can listen to the room state events before the reset
expect(stateEventEmitCount).toEqual(1);
const eventsInRoom = syncData.rooms.join[roomOne].timeline.events;
const contextUrl = `/rooms/${encodeURIComponent(roomOne)}/context/` +
`${encodeURIComponent(eventsInRoom[0].event_id)}`;
httpBackend.when("GET", contextUrl)
.respond(200, function() {
return {
start: "start_token",
events_before: [EVENTS[1], EVENTS[0]],
event: EVENTS[2],
events_after: [EVENTS[3]],
state: [
USER_MEMBERSHIP_EVENT,
],
end: "end_token",
};
});
// Refresh the timeline. This will cause the `room.currentState`
// reference to change
await Promise.all([
room.refreshLiveTimeline(),
httpBackend.flushAllExpected(),
]);
// Cause `RoomStateEvent.Update` to be fired
room.currentState.setStateEvents([SOME_STATE_EVENT]);
// Make sure we can still listen to the room state events after the reset
expect(stateEventEmitCount).toEqual(2);
});
});
});
describe("timeline", function() {
@ -637,7 +1109,7 @@ describe("MatrixClient syncing", function() {
awaitSyncEvent(),
]).then(function() {
const room = client.getRoom(roomTwo);
expect(room).toBeDefined();
expect(room).toBeTruthy();
const tok = room.getLiveTimeline()
.getPaginationToken(EventTimeline.BACKWARDS);
expect(tok).toEqual("roomtwotok");
@ -666,7 +1138,7 @@ describe("MatrixClient syncing", function() {
let resetCallCount = 0;
// the token should be set *before* timelineReset is emitted
client.on("Room.timelineReset", function(room) {
client.on(RoomEvent.TimelineReset, function(room) {
resetCallCount++;
const tl = room.getLiveTimeline();