Skip to content

Commit 957329b

Browse files
authored
Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)
* Fix room state being updated with old (now overwritten) state and emitting for those updates. * remove timestamp condition Add configuration for toStartOfTimeline * fix timeline tests * only skip event adding if event_id and replaces_state is set. * fix room tests * test skipping insertion * rename back to lastStateEvent * store if a state is at the start of a timeline in the RoomState class * make `isStartTimelineState` a `public readonly` and fix condition.
1 parent 1733ec7 commit 957329b

File tree

3 files changed

+87
-4
lines changed

3 files changed

+87
-4
lines changed

spec/unit/room-state.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1122,4 +1122,59 @@ describe("RoomState", function () {
11221122
).toBeFalsy();
11231123
});
11241124
});
1125+
describe("skipWrongOrderRoomStateInserts", () => {
1126+
const idNow = "now";
1127+
const idBefore = "before";
1128+
const endState = new RoomState(roomId);
1129+
const startState = new RoomState(roomId, undefined, true);
1130+
1131+
let onRoomStateEvent: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
1132+
const evNow = new MatrixEvent({
1133+
type: "m.call.member",
1134+
room_id: roomId,
1135+
state_key: "@user:example.org",
1136+
content: {},
1137+
});
1138+
evNow.event.unsigned = { replaces_state: idBefore };
1139+
evNow.event.event_id = idNow;
1140+
1141+
const evBefore = new MatrixEvent({
1142+
type: "m.call.member",
1143+
room_id: roomId,
1144+
state_key: "@user:example.org",
1145+
content: {},
1146+
});
1147+
1148+
const updatedStateWithBefore = jest.fn();
1149+
const updatedStateWithNow = jest.fn();
1150+
1151+
beforeEach(() => {
1152+
evBefore.event.event_id = idBefore;
1153+
onRoomStateEvent = (event, _state, _lastState) => {
1154+
if (event.event.event_id === idNow) {
1155+
updatedStateWithNow();
1156+
} else if (event.event.event_id === idBefore) {
1157+
updatedStateWithBefore();
1158+
}
1159+
};
1160+
endState.on(RoomStateEvent.Events, onRoomStateEvent);
1161+
startState.on(RoomStateEvent.Events, onRoomStateEvent);
1162+
});
1163+
afterEach(() => {
1164+
endState.off(RoomStateEvent.Events, onRoomStateEvent);
1165+
startState.off(RoomStateEvent.Events, onRoomStateEvent);
1166+
updatedStateWithNow.mockReset();
1167+
updatedStateWithBefore.mockReset();
1168+
});
1169+
it("should skip inserting state to the end of the timeline if the current endState events replaces_state id is the same as the inserted events id", () => {
1170+
endState.setStateEvents([evNow, evBefore]);
1171+
expect(updatedStateWithBefore).not.toHaveBeenCalled();
1172+
expect(updatedStateWithNow).toHaveBeenCalled();
1173+
});
1174+
it("should skip inserting state at the beginning of the timeline if the inserted events replaces_state is the event id of the current startState", () => {
1175+
startState.setStateEvents([evBefore, evNow]);
1176+
expect(updatedStateWithBefore).toHaveBeenCalled();
1177+
expect(updatedStateWithNow).not.toHaveBeenCalled();
1178+
});
1179+
});
11251180
});

src/models/event-timeline.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export class EventTimeline {
127127
public constructor(private readonly eventTimelineSet: EventTimelineSet) {
128128
this.roomId = eventTimelineSet.room?.roomId ?? null;
129129
if (this.roomId) {
130-
this.startState = new RoomState(this.roomId);
130+
this.startState = new RoomState(this.roomId, undefined, true);
131131
this.endState = new RoomState(this.roomId);
132132
}
133133

@@ -267,7 +267,7 @@ export class EventTimeline {
267267
/**
268268
* Get a pagination token
269269
*
270-
* @param direction - EventTimeline.BACKWARDS to get the pagination
270+
* @param direction - EventTimeline.BACKWARDS to get the pagination
271271
* token for going backwards in time; EventTimeline.FORWARDS to get the
272272
* pagination token for going forwards in time.
273273
*

src/models/room-state.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
194194
* As the timeline might get reset while they are loading, this state needs to be inherited
195195
* and shared when the room state is cloned for the new timeline.
196196
* This should only be passed from clone.
197+
* @param isStartTimelineState - Optional. This state is marked as a start state.
198+
* This is used to skip state insertions that are
199+
* in the wrong order. The order is determined by the `replaces_state` id.
200+
*
201+
* Example:
202+
* A current state events `replaces_state` value is `1`.
203+
* Trying to insert a state event with `event_id` `1` in its place would fail if isStartTimelineState = false.
204+
*
205+
* A current state events `event_id` is `2`.
206+
* Trying to insert a state event where its `replaces_state` value is `2` would fail if isStartTimelineState = true.
197207
*/
208+
198209
public constructor(
199210
public readonly roomId: string,
200211
private oobMemberFlags = { status: OobStatus.NotStarted },
212+
public readonly isStartTimelineState = false,
201213
) {
202214
super();
203215
this.updateModifiedTime();
@@ -408,7 +420,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
408420
* Fires {@link RoomStateEvent.Events}
409421
* Fires {@link RoomStateEvent.Marker}
410422
*/
411-
public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
423+
public setStateEvents(stateEvents: MatrixEvent[], options?: IMarkerFoundOptions): void {
412424
this.updateModifiedTime();
413425

414426
// update the core event dict
@@ -420,6 +432,22 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
420432
}
421433

422434
const lastStateEvent = this.getStateEventMatching(event);
435+
436+
// Safety measure to not update the room (and emit the update) with older state.
437+
// The sync loop really should not send old events but it does very regularly.
438+
// Logging on return in those two conditions results in a large amount of logging. (on startup and when running element)
439+
const lastReplaceId = lastStateEvent?.event.unsigned?.replaces_state;
440+
const lastId = lastStateEvent?.event.event_id;
441+
const newReplaceId = event.event.unsigned?.replaces_state;
442+
const newId = event.event.event_id;
443+
if (this.isStartTimelineState) {
444+
// Add an event to the start of the timeline. Its replace id should not be the same as the one of the current/last start state event.
445+
if (newReplaceId && lastId && newReplaceId === lastId) return;
446+
} else {
447+
// Add an event to the end of the timeline. It should not be the same as the one replaced by the current/last end state event.
448+
if (lastReplaceId && newId && lastReplaceId === newId) return;
449+
}
450+
423451
this.setStateEvent(event);
424452
if (event.getType() === EventType.RoomMember) {
425453
this.updateDisplayNameCache(event.getStateKey()!, event.getContent().displayname ?? "");
@@ -476,7 +504,7 @@ export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap>
476504
// assume all our sentinels are now out-of-date
477505
this.sentinels = {};
478506
} else if (UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
479-
this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
507+
this.emit(RoomStateEvent.Marker, event, options);
480508
}
481509
});
482510

0 commit comments

Comments
 (0)