1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-06 12:02:40 +03:00

Support MSC4222 state_after (#4487)

* WIP support for state_after

* Fix sliding sync sdk / embedded tests

* Allow both state & state_after to be undefined

Since it must have allowed state to be undefined previously: the test
had it as such.

* Fix limited sync handling

* Need to use state_after being undefined

if state can be undefined anyway

* Make sliding sync sdk tests pass

* Remove deprecated interfaces & backwards-compat code

* Remove useless assignment

* Use updates unstable prefix

* Clarify docs

* Remove additional semi-backwards compatible overload

* Update unstable prefixes

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix test

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test for MSC4222 behaviour

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Improve coverage

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Fix tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Tidy

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add comments to explain why things work as they are.

* Fix sync accumulator for state_after sync handling

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Revert "Fix room state being updated with old (now overwritten) state and emitting for those updates. (#4242)"

This reverts commit 957329b218.

* Fix Sync Accumulator toJSON putting start timeline state in state_after field

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Update tests

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Add test case

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

* Iterate

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>

---------

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
Co-authored-by: Michael Telatynski <7t3chguy@gmail.com>
Co-authored-by: Timo <toger5@hotmail.de>
This commit is contained in:
David Baker
2024-11-27 11:40:41 +00:00
committed by GitHub
parent 66f099b2e7
commit 5bcd26e506
32 changed files with 1343 additions and 735 deletions

View File

@@ -6136,7 +6136,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
room.partitionThreadedEvents(matrixEvents);
this.processAggregatedTimelineEvents(room, timelineEvents);
room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
room.addEventsToTimeline(timelineEvents, true, true, room.getLiveTimeline());
this.processThreadEvents(room, threadedEvents, true);
unknownRelations.forEach((event) => room.relations.aggregateChildEvent(event));
@@ -6248,7 +6248,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
}
const [timelineEvents, threadedEvents, unknownRelations] = timelineSet.room.partitionThreadedEvents(events);
timelineSet.addEventsToTimeline(timelineEvents, true, timeline, res.start);
timelineSet.addEventsToTimeline(timelineEvents, true, false, timeline, res.start);
// The target event is not in a thread but process the contextual events, so we can show any threads around it.
this.processThreadEvents(timelineSet.room, threadedEvents, true);
this.processAggregatedTimelineEvents(timelineSet.room, timelineEvents);
@@ -6342,10 +6342,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
timeline.initialiseState(res.state.map(mapper));
}
timelineSet.addEventsToTimeline(events, true, timeline, resNewer.next_batch);
timelineSet.addEventsToTimeline(events, true, false, timeline, resNewer.next_batch);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(resNewer.next_batch ?? null, Direction.Forward);
@@ -6399,10 +6399,10 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timeline = timelineSet.getLiveTimeline();
timeline.getState(EventTimeline.BACKWARDS)!.setUnknownStateEvents(res.state.map(mapper));
timelineSet.addEventsToTimeline(events, true, timeline, null);
timelineSet.addEventsToTimeline(events, true, false, timeline, null);
if (!resOlder.next_batch) {
const originalEvent = await this.fetchRoomEvent(timelineSet.room.roomId, thread.id);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, timeline, null);
timelineSet.addEventsToTimeline([mapper(originalEvent)], true, false, timeline, null);
}
timeline.setPaginationToken(resOlder.next_batch ?? null, Direction.Backward);
timeline.setPaginationToken(null, Direction.Forward);
@@ -6665,7 +6665,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
// No need to partition events for threads here, everything lives
// in the notification timeline set
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
// if we've hit the end of the timeline, we need to stop trying to
@@ -6708,7 +6708,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const matrixEvents = res.chunk.filter(noUnsafeEventProps).map(this.getEventMapper());
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, matrixEvents);
this.processThreadRoots(room, matrixEvents, backwards);
@@ -6756,12 +6756,12 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const newToken = res.next_batch;
const timelineSet = eventTimeline.getTimelineSet();
timelineSet.addEventsToTimeline(matrixEvents, backwards, eventTimeline, newToken ?? null);
timelineSet.addEventsToTimeline(matrixEvents, backwards, false, eventTimeline, newToken ?? null);
if (!newToken && backwards) {
const originalEvent =
thread.rootEvent ??
mapper(await this.fetchRoomEvent(eventTimeline.getRoomId() ?? "", thread.id));
timelineSet.addEventsToTimeline([originalEvent], true, eventTimeline, null);
timelineSet.addEventsToTimeline([originalEvent], true, false, eventTimeline, null);
}
this.processAggregatedTimelineEvents(timelineSet.room, matrixEvents);
@@ -6800,7 +6800,7 @@ export class MatrixClient extends TypedEventEmitter<EmittedEvents, ClientEventHa
const timelineSet = eventTimeline.getTimelineSet();
const [timelineEvents, , unknownRelations] = room.partitionThreadedEvents(matrixEvents);
timelineSet.addEventsToTimeline(timelineEvents, backwards, eventTimeline, token);
timelineSet.addEventsToTimeline(timelineEvents, backwards, false, eventTimeline, token);
this.processAggregatedTimelineEvents(room, timelineEvents);
this.processThreadRoots(
room,

View File

@@ -284,7 +284,13 @@ export class RoomWidgetClient extends MatrixClient {
const rawEvents = await this.widgetApi.readStateEvents(eventType, undefined, stateKey, [this.roomId]);
const events = rawEvents.map((rawEvent) => new MatrixEvent(rawEvent as Partial<IEvent>));
await this.syncApi!.injectRoomEvents(this.room!, [], events);
if (this.syncApi instanceof SyncApi) {
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, undefined, events);
} else {
await this.syncApi!.injectRoomEvents(this.room!, events); // Sliding Sync
}
events.forEach((event) => {
this.emit(ClientEvent.Event, event);
logger.info(`Backfilled event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);
@@ -567,7 +573,34 @@ export class RoomWidgetClient extends MatrixClient {
// Only inject once we have update the txId
await this.updateTxId(event);
await this.syncApi!.injectRoomEvents(this.room!, [], [event]);
// The widget API does not tell us whether a state event came from `state_after` or not so we assume legacy behaviour for now.
if (this.syncApi instanceof SyncApi) {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// // Passing undefined for `stateAfterEventList` makes `injectRoomEvents` run in "legacy mode"
// // -> state events part of the `timelineEventList` parameter will update the state.
// this.injectRoomEvents(this.room!, [], undefined, [event]);
// } else {
// this.injectRoomEvents(this.room!, undefined, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
// Passing undefined for `stateAfterEventList` allows will make `injectRoomEvents` run in legacy mode
// -> state events in `timelineEventList` will update the state.
await this.syncApi.injectRoomEvents(this.room!, [], undefined, [event]);
} else {
// The code will want to be something like:
// ```
// if (!params.addToTimeline && !params.addToState) {
// this.injectRoomEvents(this.room!, [], [event]);
// } else {
// this.injectRoomEvents(this.room!, params.addToState ? [event] : [], params.addToTimeline ? [event] : []);
// }
// ```
await this.syncApi!.injectRoomEvents(this.room!, [], [event]); // Sliding Sync
}
this.emit(ClientEvent.Event, event);
this.setSyncState(SyncState.Syncing);
logger.info(`Received event ${event.getId()} ${event.getType()} ${event.getStateKey()}`);

View File

@@ -58,13 +58,13 @@ export interface IRoomTimelineData {
}
export interface IAddEventToTimelineOptions
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> {
extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Whether the sync response came from cache */
fromCache?: boolean;
}
export interface IAddLiveEventOptions
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> {
extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty" | "addToState"> {
/** Applies to events in the timeline only. If this is 'replace' then if a
* duplicate is encountered, the event passed to this function will replace
* the existing event in the timeline. If this is not specified, or is
@@ -391,6 +391,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventsToTimeline(
events: MatrixEvent[],
toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline,
paginationToken?: string | null,
): void {
@@ -495,6 +496,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// we don't know about this event yet. Just add it to the timeline.
this.addEventToTimeline(event, timeline, {
toStartOfTimeline,
addToState,
});
lastEventWasNew = true;
didUpdate = true;
@@ -592,7 +594,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
*/
public addLiveEvent(
event: MatrixEvent,
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions = {},
{ duplicateStrategy, fromCache, roomState, timelineWasEmpty, addToState }: IAddLiveEventOptions,
): void {
if (this.filter) {
const events = this.filter.filterRoomTimeline([event]);
@@ -630,6 +632,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache,
roomState,
timelineWasEmpty,
addToState,
});
}
@@ -649,40 +652,8 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
{ toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions,
): void;
/**
* @deprecated In favor of the overload with `IAddEventToTimelineOptions`
*/
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimeline: boolean,
fromCache?: boolean,
roomState?: RoomState,
): void;
public addEventToTimeline(
event: MatrixEvent,
timeline: EventTimeline,
toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
fromCache = false,
roomState?: RoomState,
{ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty, addToState }: IAddEventToTimelineOptions,
): void {
let toStartOfTimeline = !!toStartOfTimelineOrOpts;
let timelineWasEmpty: boolean | undefined;
if (typeof toStartOfTimelineOrOpts === "object") {
({ toStartOfTimeline, fromCache = false, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
} else if (toStartOfTimelineOrOpts !== undefined) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
logger.warn(
"Overload deprecated: " +
"`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " +
"is deprecated in favor of the overload with " +
"`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`",
);
}
if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
@@ -713,6 +684,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
toStartOfTimeline,
roomState,
timelineWasEmpty,
addToState,
});
this._eventIdToTimeline.set(eventId, timeline);
@@ -741,7 +713,12 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
* @remarks
* Fires {@link RoomEvent.Timeline}
*/
public insertEventIntoTimeline(event: MatrixEvent, timeline: EventTimeline, roomState: RoomState): void {
public insertEventIntoTimeline(
event: MatrixEvent,
timeline: EventTimeline,
roomState: RoomState,
addToState: boolean,
): void {
if (timeline.getTimelineSet() !== this) {
throw new Error(`EventTimelineSet.insertEventIntoTimeline: Timeline=${timeline.toString()} does not belong " +
"in timelineSet(threadId=${this.thread?.id})`);
@@ -777,6 +754,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
fromCache: false,
timelineWasEmpty: false,
roomState,
addToState,
});
return;
}
@@ -799,7 +777,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
// If we got to the end of the loop, insertIndex points at the end of
// the list.
timeline.insertEvent(event, insertIndex, roomState);
timeline.insertEvent(event, insertIndex, roomState, addToState);
this._eventIdToTimeline.set(eventId, timeline);
const data: IRoomTimelineData = {
@@ -832,6 +810,7 @@ export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTime
} else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
this.addEventToTimeline(localEvent, this.liveTimeline, {
toStartOfTimeline: false,
addToState: false,
});
}
}

View File

@@ -35,6 +35,11 @@ export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWas
toStartOfTimeline: boolean;
/** The state events to reconcile metadata from */
roomState?: RoomState;
/** Whether to add timeline events to the state as was done in legacy sync v2.
* If true then timeline events will be added to the state.
* In sync v2 with org.matrix.msc4222.use_state_after and simplified sliding sync,
* all state arrives explicitly and timeline events should not be added. */
addToState: boolean;
}
export enum Direction {
@@ -362,7 +367,7 @@ export class EventTimeline {
*/
public addEvent(
event: MatrixEvent,
{ toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions = { toStartOfTimeline: false },
{ toStartOfTimeline, roomState, timelineWasEmpty, addToState }: IAddEventOptions,
): void {
if (!roomState) {
roomState = toStartOfTimeline ? this.startState : this.endState;
@@ -374,7 +379,7 @@ export class EventTimeline {
EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
// modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState?.setStateEvents([event], { timelineWasEmpty });
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try
@@ -417,14 +422,14 @@ export class EventTimeline {
*
* @internal
*/
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState): void {
public insertEvent(event: MatrixEvent, insertIndex: number, roomState: RoomState, addToState: boolean): void {
const timelineSet = this.getTimelineSet();
if (timelineSet.room) {
EventTimeline.setEventMetadata(event, roomState, false);
// modify state but only on unfiltered timelineSets
if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
if (addToState && event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
roomState.setStateEvents([event], {});
// it is possible that the act of setting the state event means we
// can set more metadata (specifically sender/target props), so try

View File

@@ -1250,7 +1250,9 @@ export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, Mat
const timeline = room.getLiveTimeline();
// We use insertEventIntoTimeline to insert it in timestamp order,
// because we don't know where it should go (until we have MSC4033).
timeline.getTimelineSet().insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!);
timeline
.getTimelineSet()
.insertEventIntoTimeline(this, timeline, timeline.getState(EventTimeline.FORWARDS)!, false);
}
/**

View File

@@ -1739,10 +1739,11 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
public addEventsToTimeline(
events: MatrixEvent[],
toStartOfTimeline: boolean,
addToState: boolean,
timeline: EventTimeline,
paginationToken?: string,
): void {
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, addToState, timeline, paginationToken);
}
/**
@@ -1907,7 +1908,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
// see https://github.com/vector-im/vector-web/issues/2109
unfilteredLiveTimeline.getEvents().forEach(function (event) {
timelineSet.addLiveEvent(event);
timelineSet.addLiveEvent(event, { addToState: false }); // Filtered timeline sets should not track state
});
// find the earliest unfiltered timeline
@@ -1994,6 +1995,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (filterType !== ThreadFilterType.My || currentUserParticipated) {
timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
toStartOfTimeline: false,
addToState: false,
});
}
});
@@ -2068,6 +2070,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
const opts = {
duplicateStrategy: DuplicateStrategy.Ignore,
fromCache: false,
addToState: false,
roomState,
};
this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
@@ -2190,6 +2193,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false,
roomState,
addToState: false,
});
}
}
@@ -2381,9 +2385,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy: DuplicateStrategy.Replace,
fromCache: false,
roomState: this.currentState,
addToState: false,
});
} else {
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline });
timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), {
toStartOfTimeline,
addToState: false,
});
}
}
};
@@ -2540,7 +2548,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* Fires {@link RoomEvent.Timeline}
*/
private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions;
const { duplicateStrategy, timelineWasEmpty, fromCache, addToState } = addLiveEventOptions;
// add to our timeline sets
for (const timelineSet of this.timelineSets) {
@@ -2548,6 +2556,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy,
fromCache,
timelineWasEmpty,
addToState,
});
}
@@ -2631,11 +2640,13 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
if (timelineSet.getFilter()!.filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
});
}
} else {
timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false,
addToState: false, // We don't support localEcho of state events yet
});
}
}
@@ -2886,8 +2897,8 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
* @param addLiveEventOptions - addLiveEvent options
* @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
*/
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false } = addLiveEventOptions ?? {};
public async addLiveEvents(events: MatrixEvent[], addLiveEventOptions: IAddLiveEventOptions): Promise<void> {
const { duplicateStrategy, fromCache, timelineWasEmpty = false, addToState } = addLiveEventOptions;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
}
@@ -2902,6 +2913,7 @@ export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
duplicateStrategy,
fromCache,
timelineWasEmpty,
addToState,
};
// List of extra events to check for being parents of any relations encountered

View File

@@ -208,6 +208,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
public static setServerSideSupport(status: FeatureSupport): void {
Thread.hasServerSideSupport = status;
// XXX: This global latching behaviour is really unexpected and means that you can't undo when moving to a server without support
if (status !== FeatureSupport.Stable) {
FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
@@ -317,6 +318,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
toStartOfTimeline,
fromCache: false,
roomState: this.roomState,
addToState: false,
});
}
}
@@ -343,7 +345,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
if (this.findEventById(eventId)) {
return;
}
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState, false);
}
public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
@@ -618,7 +620,7 @@ export class Thread extends ReadReceipt<ThreadEmittedEvents, ThreadEventHandlerM
// if the thread has regular events, this will just load the last reply.
// if the thread is newly created, this will load the root event.
if (this.replyCount === 0 && this.rootEvent) {
this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
this.timelineSet.addEventsToTimeline([this.rootEvent], true, false, this.liveTimeline, null);
this.liveTimeline.setPaginationToken(null, Direction.Backward);
} else {
this.initalEventFetchProm = this.client.paginateEventTimeline(this.liveTimeline, {

View File

@@ -612,7 +612,7 @@ export class SlidingSyncSdk {
timelineEvents = newEvents;
if (oldEvents.length > 0) {
// old events are scrollback, insert them now
room.addEventsToTimeline(oldEvents, true, room.getLiveTimeline(), roomData.prev_batch);
room.addEventsToTimeline(oldEvents, true, false, room.getLiveTimeline(), roomData.prev_batch);
}
}
@@ -754,7 +754,7 @@ export class SlidingSyncSdk {
/**
* Injects events into a room's model.
* @param stateEventList - A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* at the *END* of the timeline list if it is supplied.
* @param timelineEventList - A list of timeline events. Lower index
* is earlier in time. Higher index is later.
* @param numLive - the number of events in timelineEventList which just happened,
@@ -763,13 +763,9 @@ export class SlidingSyncSdk {
public async injectRoomEvents(
room: Room,
stateEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[],
numLive?: number,
timelineEventList: MatrixEvent[] = [],
numLive: number = 0,
): Promise<void> {
timelineEventList = timelineEventList || [];
stateEventList = stateEventList || [];
numLive = numLive || 0;
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -820,16 +816,17 @@ export class SlidingSyncSdk {
timelineEventList = timelineEventList.slice(0, -1 * liveTimelineEvents.length);
}
// execute the timeline events. This will continue to diverge the current state
// if the timeline has any state events in it.
// Execute the timeline events.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
await room.addLiveEvents(timelineEventList, {
fromCache: true,
addToState: false,
});
if (liveTimelineEvents.length > 0) {
await room.addLiveEvents(liveTimelineEvents, {
fromCache: false,
addToState: false,
});
}
@@ -966,7 +963,7 @@ export class SlidingSyncSdk {
return a.getTs() - b.getTs();
});
this.notifEvents.forEach((event) => {
this.client.getNotifTimelineSet()?.addLiveEvent(event);
this.client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: false });
});
this.notifEvents = [];
}

View File

@@ -77,7 +77,9 @@ export interface ITimeline {
export interface IJoinedRoom {
"summary": IRoomSummary;
"state": IState;
// One of `state` or `state_after` is required.
"state"?: IState;
"org.matrix.msc4222.state_after"?: IState; // https://github.com/matrix-org/matrix-spec-proposals/pull/4222
"timeline": ITimeline;
"ephemeral": IEphemeral;
"account_data": IAccountData;
@@ -106,9 +108,11 @@ export interface IInvitedRoom {
}
export interface ILeftRoom {
state: IState;
timeline: ITimeline;
account_data: IAccountData;
// One of `state` or `state_after` is required.
"state"?: IState;
"org.matrix.msc4222.state_after"?: IState;
"timeline": ITimeline;
"account_data": IAccountData;
}
export interface IKnockedRoom {
@@ -481,13 +485,18 @@ export class SyncAccumulator {
// Work out the current state. The deltas need to be applied in the order:
// - existing state which didn't come down /sync.
// - State events under the 'state' key.
// - State events in the 'timeline'.
// - State events under the 'state_after' key OR state events in the 'timeline' if 'state_after' is not present.
data.state?.events?.forEach((e) => {
setState(currentData._currentState, e);
});
data.timeline?.events?.forEach((e, index) => {
// this nops if 'e' isn't a state event
data["org.matrix.msc4222.state_after"]?.events?.forEach((e) => {
setState(currentData._currentState, e);
});
data.timeline?.events?.forEach((e, index) => {
if (!data["org.matrix.msc4222.state_after"]) {
// this nops if 'e' isn't a state event
setState(currentData._currentState, e);
}
// append the event to the timeline. The back-pagination token
// corresponds to the first event in the timeline
let transformedEvent: TaggedEvent;
@@ -563,17 +572,22 @@ export class SyncAccumulator {
});
Object.keys(this.joinRooms).forEach((roomId) => {
const roomData = this.joinRooms[roomId];
const roomJson: IJoinedRoom = {
ephemeral: { events: [] },
account_data: { events: [] },
state: { events: [] },
timeline: {
const roomJson: IJoinedRoom & {
// We track both `state` and `state_after` for downgrade compatibility
"state": IState;
"org.matrix.msc4222.state_after": IState;
} = {
"ephemeral": { events: [] },
"account_data": { events: [] },
"state": { events: [] },
"org.matrix.msc4222.state_after": { events: [] },
"timeline": {
events: [],
prev_batch: null,
},
unread_notifications: roomData._unreadNotifications,
unread_thread_notifications: roomData._unreadThreadNotifications,
summary: roomData._summary as IRoomSummary,
"unread_notifications": roomData._unreadNotifications,
"unread_thread_notifications": roomData._unreadThreadNotifications,
"summary": roomData._summary as IRoomSummary,
};
// Add account data
Object.keys(roomData._accountData).forEach((evType) => {
@@ -650,8 +664,11 @@ export class SyncAccumulator {
Object.keys(roomData._currentState).forEach((evType) => {
Object.keys(roomData._currentState[evType]).forEach((stateKey) => {
let ev = roomData._currentState[evType][stateKey];
// Push to both fields to provide downgrade compatibility in the sync accumulator db
// the code will prefer `state_after` if it is present
roomJson["org.matrix.msc4222.state_after"].events.push(ev);
// Roll the state back to the value at the start of the timeline if it was changed
if (rollBackState[evType] && rollBackState[evType][stateKey]) {
// use the reverse clobbered event instead.
ev = rollBackState[evType][stateKey];
}
roomJson.state.events.push(ev);

View File

@@ -175,14 +175,15 @@ export enum SetPresence {
}
interface ISyncParams {
filter?: string;
timeout: number;
since?: string;
"filter"?: string;
"timeout": number;
"since"?: string;
// eslint-disable-next-line camelcase
full_state?: boolean;
"full_state"?: boolean;
// eslint-disable-next-line camelcase
set_presence?: SetPresence;
_cacheBuster?: string | number; // not part of the API itself
"set_presence"?: SetPresence;
"_cacheBuster"?: string | number; // not part of the API itself
"org.matrix.msc4222.use_state_after"?: boolean; // https://github.com/matrix-org/matrix-spec-proposals/pull/4222
}
type WrappedRoom<T> = T & {
@@ -344,8 +345,9 @@ export class SyncApi {
);
const qps: ISyncParams = {
timeout: 0, // don't want to block since this is a single isolated req
filter: filterId,
"timeout": 0, // don't want to block since this is a single isolated req
"filter": filterId,
"org.matrix.msc4222.use_state_after": true,
};
const data = await client.http.authedRequest<ISyncResponse>(Method.Get, "/sync", qps as any, undefined, {
@@ -375,21 +377,18 @@ export class SyncApi {
prev_batch: null,
events: [],
};
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
// set the back-pagination token. Do this *before* adding any
// events so that clients can start back-paginating.
room.getLiveTimeline().setPaginationToken(leaveObj.timeline.prev_batch, EventTimeline.BACKWARDS);
await this.injectRoomEvents(room, stateEvents, events);
const { timelineEvents } = await this.mapAndInjectRoomEvents(leaveObj);
room.recalculate();
client.store.storeRoom(room);
client.emit(ClientEvent.Room, room);
this.processEventsForNotifs(room, events);
this.processEventsForNotifs(room, timelineEvents);
return room;
}),
);
@@ -464,6 +463,7 @@ export class SyncApi {
this._peekRoom.addEventsToTimeline(
messages.reverse(),
true,
true,
this._peekRoom.getLiveTimeline(),
response.messages.start,
);
@@ -551,7 +551,7 @@ export class SyncApi {
})
.map(this.client.getEventMapper());
await peekRoom.addLiveEvents(events);
await peekRoom.addLiveEvents(events, { addToState: true });
this.peekPoll(peekRoom, res.end);
},
(err) => {
@@ -976,7 +976,11 @@ export class SyncApi {
filter = this.getGuestFilter();
}
const qps: ISyncParams = { filter, timeout };
const qps: ISyncParams = {
filter,
timeout,
"org.matrix.msc4222.use_state_after": true,
};
if (this.opts.disablePresence) {
qps.set_presence = SetPresence.Offline;
@@ -1242,7 +1246,7 @@ export class SyncApi {
const room = inviteObj.room;
const stateEvents = this.mapSyncEventsFormat(inviteObj.invite_state, room);
await this.injectRoomEvents(room, stateEvents);
await this.injectRoomEvents(room, stateEvents, undefined);
const inviter = room.currentState.getStateEvents(EventType.RoomMember, client.getUserId()!)?.getSender();
@@ -1282,15 +1286,24 @@ export class SyncApi {
await promiseMapSeries(joinRooms, async (joinObj) => {
const room = joinObj.room;
const stateEvents = this.mapSyncEventsFormat(joinObj.state, room);
const stateAfterEvents = this.mapSyncEventsFormat(joinObj["org.matrix.msc4222.state_after"], room);
// Prevent events from being decrypted ahead of time
// this helps large account to speed up faster
// room::decryptCriticalEvent is in charge of decrypting all the events
// required for a client to function properly
const events = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const timelineEvents = this.mapSyncEventsFormat(joinObj.timeline, room, false);
const ephemeralEvents = this.mapSyncEventsFormat(joinObj.ephemeral);
const accountDataEvents = this.mapSyncEventsFormat(joinObj.account_data);
const encrypted = this.isRoomEncrypted(room, stateEvents, events);
// If state_after is present, this is the events that form the state at the end of the timeline block and
// regular timeline events do *not* count towards state. If it's not present, then the state is formed by
// the state events plus the timeline events. Note mapSyncEventsFormat returns an empty array if the field
// is absent so we explicitly check the field on the original object.
const eventsFormingFinalState = joinObj["org.matrix.msc4222.state_after"]
? stateAfterEvents
: stateEvents.concat(timelineEvents);
const encrypted = this.isRoomEncrypted(room, eventsFormingFinalState);
// We store the server-provided value first so it's correct when any of the events fire.
if (joinObj.unread_notifications) {
/**
@@ -1378,8 +1391,8 @@ export class SyncApi {
// which we'll try to paginate but not get any new events (which
// will stop us linking the empty timeline into the chain).
//
for (let i = events.length - 1; i >= 0; i--) {
const eventId = events[i].getId()!;
for (let i = timelineEvents.length - 1; i >= 0; i--) {
const eventId = timelineEvents[i].getId()!;
if (room.getTimelineForEvent(eventId)) {
debuglog(`Already have event ${eventId} in limited sync - not resetting`);
limited = false;
@@ -1387,7 +1400,7 @@ export class SyncApi {
// we might still be missing some of the events before i;
// we don't want to be adding them to the end of the
// timeline because that would put them out of order.
events.splice(0, i);
timelineEvents.splice(0, i);
// XXX: there's a problem here if the skipped part of the
// timeline modifies the state set in stateEvents, because
@@ -1419,8 +1432,9 @@ export class SyncApi {
// avoids a race condition if the application tries to send a message after the
// state event is processed, but before crypto is enabled, which then causes the
// crypto layer to complain.
if (this.syncOpts.cryptoCallbacks) {
for (const e of stateEvents.concat(events)) {
for (const e of eventsFormingFinalState) {
if (e.isState() && e.getType() === EventType.RoomEncryption && e.getStateKey() === "") {
await this.syncOpts.cryptoCallbacks.onCryptoEvent(room, e);
}
@@ -1428,7 +1442,17 @@ export class SyncApi {
}
try {
await this.injectRoomEvents(room, stateEvents, events, syncEventData.fromCache);
if ("org.matrix.msc4222.state_after" in joinObj) {
await this.injectRoomEvents(
room,
undefined,
stateAfterEvents,
timelineEvents,
syncEventData.fromCache,
);
} else {
await this.injectRoomEvents(room, stateEvents, undefined, timelineEvents, syncEventData.fromCache);
}
} catch (e) {
logger.error(`Failed to process events on room ${room.roomId}:`, e);
}
@@ -1452,11 +1476,11 @@ export class SyncApi {
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, events);
this.processEventsForNotifs(room, timelineEvents);
const emitEvent = (e: MatrixEvent): boolean => client.emit(ClientEvent.Event, e);
stateEvents.forEach(emitEvent);
events.forEach(emitEvent);
timelineEvents.forEach(emitEvent);
ephemeralEvents.forEach(emitEvent);
accountDataEvents.forEach(emitEvent);
@@ -1469,11 +1493,9 @@ export class SyncApi {
// Handle leaves (e.g. kicked rooms)
await promiseMapSeries(leaveRooms, async (leaveObj) => {
const room = leaveObj.room;
const stateEvents = this.mapSyncEventsFormat(leaveObj.state, room);
const events = this.mapSyncEventsFormat(leaveObj.timeline, room);
const { timelineEvents, stateEvents, stateAfterEvents } = await this.mapAndInjectRoomEvents(leaveObj);
const accountDataEvents = this.mapSyncEventsFormat(leaveObj.account_data);
await this.injectRoomEvents(room, stateEvents, events);
room.addAccountData(accountDataEvents);
room.recalculate();
@@ -1482,12 +1504,15 @@ export class SyncApi {
client.emit(ClientEvent.Room, room);
}
this.processEventsForNotifs(room, events);
this.processEventsForNotifs(room, timelineEvents);
stateEvents.forEach(function (e) {
stateEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
events.forEach(function (e) {
stateAfterEvents?.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
timelineEvents.forEach(function (e) {
client.emit(ClientEvent.Event, e);
});
accountDataEvents.forEach(function (e) {
@@ -1500,7 +1525,7 @@ export class SyncApi {
const room = knockObj.room;
const stateEvents = this.mapSyncEventsFormat(knockObj.knock_state, room);
await this.injectRoomEvents(room, stateEvents);
await this.injectRoomEvents(room, stateEvents, undefined);
if (knockObj.isBrandNewRoom) {
room.recalculate();
@@ -1525,7 +1550,7 @@ export class SyncApi {
return a.getTs() - b.getTs();
});
this.notifEvents.forEach(function (event) {
client.getNotifTimelineSet()?.addLiveEvent(event);
client.getNotifTimelineSet()?.addLiveEvent(event, { addToState: true });
});
}
@@ -1669,7 +1694,7 @@ export class SyncApi {
}
private mapSyncEventsFormat(
obj: IInviteState | ITimeline | IEphemeral,
obj: IInviteState | ITimeline | IEphemeral | undefined,
room?: Room,
decrypt = true,
): MatrixEvent[] {
@@ -1737,28 +1762,69 @@ export class SyncApi {
// When processing the sync response we cannot rely on Room.hasEncryptionStateEvent we actually
// inject the events into the room object, so we have to inspect the events themselves.
private isRoomEncrypted(room: Room, stateEventList: MatrixEvent[], timelineEventList?: MatrixEvent[]): boolean {
return (
room.hasEncryptionStateEvent() ||
!!this.findEncryptionEvent(stateEventList) ||
!!this.findEncryptionEvent(timelineEventList)
private isRoomEncrypted(room: Room, eventsFormingFinalState: MatrixEvent[]): boolean {
return room.hasEncryptionStateEvent() || !!this.findEncryptionEvent(eventsFormingFinalState);
}
private async mapAndInjectRoomEvents(wrappedRoom: WrappedRoom<ILeftRoom>): Promise<{
timelineEvents: MatrixEvent[];
stateEvents?: MatrixEvent[];
stateAfterEvents?: MatrixEvent[];
}> {
const stateEvents = this.mapSyncEventsFormat(wrappedRoom.state, wrappedRoom.room);
const stateAfterEvents = this.mapSyncEventsFormat(
wrappedRoom["org.matrix.msc4222.state_after"],
wrappedRoom.room,
);
const timelineEvents = this.mapSyncEventsFormat(wrappedRoom.timeline, wrappedRoom.room);
if ("org.matrix.msc4222.state_after" in wrappedRoom) {
await this.injectRoomEvents(wrappedRoom.room, undefined, stateAfterEvents, timelineEvents);
} else {
await this.injectRoomEvents(wrappedRoom.room, stateEvents, undefined, timelineEvents);
}
return { timelineEvents, stateEvents, stateAfterEvents };
}
/**
* Injects events into a room's model.
* @param stateEventList - A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param stateAfterEventList - A list of state events. This is the state
* at the *END* of the timeline list if it is supplied.
* @param timelineEventList - A list of timeline events, including threaded. Lower index
* is earlier in time. Higher index is later.
* @param fromCache - whether the sync response came from cache
*
* No more than one of stateEventList and stateAfterEventList must be supplied. If
* stateEventList is supplied, the events in timelineEventList are added to the state
* after stateEventList. If stateAfterEventList is supplied, the events in timelineEventList
* are not added to the state.
*/
public async injectRoomEvents(
room: Room,
stateEventList: MatrixEvent[],
stateAfterEventList: undefined,
timelineEventList?: MatrixEvent[],
fromCache?: boolean,
): Promise<void>;
public async injectRoomEvents(
room: Room,
stateEventList: undefined,
stateAfterEventList: MatrixEvent[],
timelineEventList?: MatrixEvent[],
fromCache?: boolean,
): Promise<void>;
public async injectRoomEvents(
room: Room,
stateEventList: MatrixEvent[] | undefined,
stateAfterEventList: MatrixEvent[] | undefined,
timelineEventList?: MatrixEvent[],
fromCache = false,
): Promise<void> {
const eitherStateEventList = stateAfterEventList ?? stateEventList!;
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1772,10 +1838,11 @@ export class SyncApi {
// push actions cache elsewhere so we can freeze MatrixEvents, or otherwise
// find some solution where MatrixEvents are immutable but allow for a cache
// field.
for (const ev of stateEventList) {
for (const ev of eitherStateEventList) {
this.client.getPushActionsForEvent(ev);
}
liveTimeline.initialiseState(stateEventList, {
liveTimeline.initialiseState(eitherStateEventList, {
timelineWasEmpty,
});
}
@@ -1807,17 +1874,18 @@ export class SyncApi {
// XXX: As above, don't do this...
//room.addLiveEvents(stateEventList || []);
// Do this instead...
room.oldState.setStateEvents(stateEventList || []);
room.currentState.setStateEvents(stateEventList || []);
room.oldState.setStateEvents(eitherStateEventList);
room.currentState.setStateEvents(eitherStateEventList);
}
// Execute the timeline events. This will continue to diverge the current state
// if the timeline has any state events in it.
// Execute the timeline events. If addToState is true the timeline has any state
// events in it, this will continue to diverge the current state.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
await room.addLiveEvents(timelineEventList || [], {
fromCache,
timelineWasEmpty,
addToState: stateAfterEventList === undefined,
});
this.client.processBeaconEvents(room, timelineEventList);
}