Skip to content

Commit 3146a71

Browse files
committed
fix(sliding-sync): reset live timeline on reconnect to prevent event ordering corruption
When the sliding sync pos token expires (app backgrounded) and the server re-sends initial:true for an already-open room, the SDK's processRoomData treats any post-gap 'stranded' event as a known anchor and prepends gap-fill events ahead of earlier history, producing out-of-order timelines (Today -> Yesterday -> Today). Register onInitialRoomData in the constructor and call slidingSync.on() in attach() before mx.startClient() (i.e. before SlidingSyncSdk.onRoomData is registered). When initial:true arrives for a room that already has events, reset the live timeline so _eventIdToTimeline is cleared, all received events go through the newEvents path, and chronological order is preserved.
1 parent 9f45a73 commit 3146a71

1 file changed

Lines changed: 23 additions & 0 deletions

File tree

src/client/slidingSync.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,13 @@ export class SlidingSyncManager {
277277
(roomId: string, data: MSC3575RoomData) => void
278278
>();
279279

280+
/**
281+
* Listener registered in attach() that resets the live timeline before the SDK
282+
* processes initial:true room data. Must fire before SlidingSyncSdk.onRoomData,
283+
* which is registered in mx.startClient() (called after attach()).
284+
*/
285+
private readonly onInitialRoomData: (roomId: string, roomData: MSC3575RoomData) => void;
286+
280287
/** Wall-clock time recorded in attach() — used to compute true initial-sync latency. */
281288
private attachTime: number | null = null;
282289

@@ -425,6 +432,17 @@ export class SlidingSyncManager {
425432
}
426433
};
427434

435+
// Reset the live timeline when the server re-sends initial:true (reconnect after
436+
// pos token expiry). Must be assigned in the constructor so the reference is stable
437+
// for removeListener(); registered in attach() before mx.startClient() so it fires
438+
// ahead of SlidingSyncSdk.onRoomData.
439+
this.onInitialRoomData = (roomId: string, roomData: MSC3575RoomData) => {
440+
if (!roomData.initial) return;
441+
const room = this.mx.getRoom(roomId);
442+
if (!room || room.getLiveTimeline().getEvents().length === 0) return;
443+
room.getUnfilteredTimelineSet().resetLiveTimeline();
444+
};
445+
428446
this.onMembershipLeave = (_event, member) => {
429447
if (member.userId !== this.mx.getUserId()) return;
430448
if (member.membership !== KnownMembership.Leave && member.membership !== KnownMembership.Ban)
@@ -474,6 +492,10 @@ export class SlidingSyncManager {
474492
attributes: { 'sync.transport': 'sliding', 'sync.proxy': this.proxyBaseUrl },
475493
});
476494

495+
// Register BEFORE mx.startClient() so this fires ahead of SlidingSyncSdk.onRoomData
496+
// (see constructor comment on onInitialRoomData for the full explanation).
497+
this.slidingSync.on(SlidingSyncEvent.RoomData, this.onInitialRoomData);
498+
477499
this.slidingSync.on(SlidingSyncEvent.Lifecycle, this.onLifecycle);
478500
this.mx.on(RoomMemberEvent.Membership, this.onMembershipLeave);
479501
const connection = (
@@ -513,6 +535,7 @@ export class SlidingSyncManager {
513535
this.disposed = true;
514536
// Stop the SDK's internal polling loop and abort any in-flight requests.
515537
this.slidingSync.stop();
538+
this.slidingSync.removeListener(SlidingSyncEvent.RoomData, this.onInitialRoomData);
516539
this.slidingSync.removeListener(SlidingSyncEvent.Lifecycle, this.onLifecycle);
517540
this.mx.removeListener(RoomMemberEvent.Membership, this.onMembershipLeave);
518541
const connection = (

0 commit comments

Comments
 (0)