Skip to content

Commit d37fa81

Browse files
committed
Timeline needs to refresh when we see a MSC2716 marker event
> 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](matrix-org/matrix-spec-proposals#2716 (comment)) 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. > > *-- matrix-org/matrix-spec-proposals#2716 (comment)
1 parent 9aab917 commit d37fa81

File tree

9 files changed

+650
-65
lines changed

9 files changed

+650
-65
lines changed

spec/integ/matrix-client-syncing.spec.js

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { MatrixEvent } from "../../src/models/event";
22
import { EventTimeline } from "../../src/models/event-timeline";
3+
import { EventType } from "../../src/@types/event";
34
import * as utils from "../test-utils/test-utils";
45
import { TestClient } from "../TestClient";
56

@@ -461,6 +462,304 @@ describe("MatrixClient syncing", function() {
461462
xit("should update the room topic", function() {
462463

463464
});
465+
466+
describe("onMarkerStateEvent", () => {
467+
const normalMessageEvent = utils.mkMessage({
468+
room: roomOne, user: otherUserId, msg: "hello",
469+
});
470+
471+
it('new marker event *NOT* from the room creator in a subsequent syncs ' +
472+
'should *NOT* mark the timeline as needing a refresh', async () => {
473+
const roomCreateEvent = utils.mkEvent({
474+
type: "m.room.create", room: roomOne, user: otherUserId,
475+
content: {
476+
creator: otherUserId,
477+
room_version: '9',
478+
},
479+
});
480+
const normalFirstSync = {
481+
next_batch: "batch_token",
482+
rooms: {
483+
join: {},
484+
},
485+
};
486+
normalFirstSync.rooms.join[roomOne] = {
487+
timeline: {
488+
events: [normalMessageEvent],
489+
prev_batch: "pagTok",
490+
},
491+
state: {
492+
events: [roomCreateEvent],
493+
},
494+
};
495+
496+
const nextSyncData = {
497+
next_batch: "batch_token",
498+
rooms: {
499+
join: {},
500+
},
501+
};
502+
nextSyncData.rooms.join[roomOne] = {
503+
timeline: {
504+
events: [
505+
// In subsequent syncs, a marker event in timeline
506+
// range should normally trigger
507+
// `timelineNeedsRefresh=true` but this marker isn't
508+
// being sent by the room creator so it has no
509+
// special meaning in existing room versions.
510+
utils.mkEvent({
511+
type: EventType.Marker,
512+
room: roomOne,
513+
// The important part we're testing is here!
514+
// `userC` is not the room creator.
515+
user: userC,
516+
skey: "",
517+
content: {
518+
"m.insertion_id": "$abc",
519+
},
520+
}),
521+
],
522+
prev_batch: "pagTok",
523+
},
524+
};
525+
526+
// Ensure the marker is being sent by someone who is not the room creator
527+
// because this is the main thing we're testing in this spec.
528+
const markerEvent = nextSyncData.rooms.join[roomOne].timeline.events[0];
529+
expect(markerEvent.sender).toBeDefined();
530+
expect(markerEvent.sender).not.toEqual(roomCreateEvent.sender);
531+
532+
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
533+
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
534+
535+
client.startClient();
536+
await Promise.all([
537+
httpBackend.flushAllExpected(),
538+
awaitSyncEvent(2),
539+
]);
540+
541+
const room = client.getRoom(roomOne);
542+
expect(room.getTimelineNeedsRefresh()).toEqual(false);
543+
});
544+
545+
[{
546+
label: 'In existing room versions (when the room creator sends the MSC2716 events)',
547+
roomVersion: '9',
548+
}, {
549+
label: 'In a MSC2716 supported room version',
550+
roomVersion: 'org.matrix.msc2716v3',
551+
}].forEach((testMeta) => {
552+
describe(testMeta.label, () => {
553+
const roomCreateEvent = utils.mkEvent({
554+
type: "m.room.create", room: roomOne, user: otherUserId,
555+
content: {
556+
creator: otherUserId,
557+
room_version: testMeta.roomVersion,
558+
},
559+
});
560+
561+
const markerEventFromRoomCreator = utils.mkEvent({
562+
type: EventType.Marker, room: roomOne, user: otherUserId,
563+
skey: "",
564+
content: {
565+
"m.insertion_id": "$abc",
566+
},
567+
});
568+
569+
const normalFirstSync = {
570+
next_batch: "batch_token",
571+
rooms: {
572+
join: {},
573+
},
574+
};
575+
normalFirstSync.rooms.join[roomOne] = {
576+
timeline: {
577+
events: [normalMessageEvent],
578+
prev_batch: "pagTok",
579+
},
580+
state: {
581+
events: [roomCreateEvent],
582+
},
583+
};
584+
585+
it('no marker event in sync response '+
586+
'should *NOT* mark the timeline as needing a refresh (check for a sane default)', async () => {
587+
const syncData = {
588+
next_batch: "batch_token",
589+
rooms: {
590+
join: {},
591+
},
592+
};
593+
syncData.rooms.join[roomOne] = {
594+
timeline: {
595+
events: [normalMessageEvent],
596+
prev_batch: "pagTok",
597+
},
598+
state: {
599+
events: [roomCreateEvent],
600+
},
601+
};
602+
603+
httpBackend.when("GET", "/sync").respond(200, syncData);
604+
605+
client.startClient();
606+
await Promise.all([
607+
httpBackend.flushAllExpected(),
608+
awaitSyncEvent(),
609+
]);
610+
611+
const room = client.getRoom(roomOne);
612+
expect(room.getTimelineNeedsRefresh()).toEqual(false);
613+
});
614+
615+
it('marker event already sent within timeline range when you join ' +
616+
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
617+
const syncData = {
618+
next_batch: "batch_token",
619+
rooms: {
620+
join: {},
621+
},
622+
};
623+
syncData.rooms.join[roomOne] = {
624+
timeline: {
625+
events: [markerEventFromRoomCreator],
626+
prev_batch: "pagTok",
627+
},
628+
state: {
629+
events: [roomCreateEvent],
630+
},
631+
};
632+
633+
httpBackend.when("GET", "/sync").respond(200, syncData);
634+
635+
client.startClient();
636+
await Promise.all([
637+
httpBackend.flushAllExpected(),
638+
awaitSyncEvent(),
639+
]);
640+
641+
const room = client.getRoom(roomOne);
642+
expect(room.getTimelineNeedsRefresh()).toEqual(false);
643+
});
644+
645+
it('marker event already sent before joining (in state) ' +
646+
'should *NOT* mark the timeline as needing a refresh (timelineWasEmpty)', async () => {
647+
const syncData = {
648+
next_batch: "batch_token",
649+
rooms: {
650+
join: {},
651+
},
652+
};
653+
syncData.rooms.join[roomOne] = {
654+
timeline: {
655+
events: [normalMessageEvent],
656+
prev_batch: "pagTok",
657+
},
658+
state: {
659+
events: [
660+
roomCreateEvent,
661+
markerEventFromRoomCreator,
662+
],
663+
},
664+
};
665+
666+
httpBackend.when("GET", "/sync").respond(200, syncData);
667+
668+
client.startClient();
669+
await Promise.all([
670+
httpBackend.flushAllExpected(),
671+
awaitSyncEvent(),
672+
]);
673+
674+
const room = client.getRoom(roomOne);
675+
expect(room.getTimelineNeedsRefresh()).toEqual(false);
676+
});
677+
678+
it('new marker event in a subsequent syncs timeline range ' +
679+
'should mark the timeline as needing a refresh', async () => {
680+
const nextSyncData = {
681+
next_batch: "batch_token",
682+
rooms: {
683+
join: {},
684+
},
685+
};
686+
nextSyncData.rooms.join[roomOne] = {
687+
timeline: {
688+
events: [
689+
// In subsequent syncs, a marker event in timeline
690+
// range should trigger `timelineNeedsRefresh=true`
691+
markerEventFromRoomCreator,
692+
],
693+
prev_batch: "pagTok",
694+
},
695+
};
696+
697+
const markerEventId = nextSyncData.rooms.join[roomOne].timeline.events[0].event_id;
698+
699+
let emitCount = 0;
700+
client.on("Room.historyImportedWithinTimeline", function(markerEvent, room) {
701+
expect(markerEvent.getId()).toEqual(markerEventId);
702+
expect(room.roomId).toEqual(roomOne);
703+
emitCount += 1;
704+
});
705+
706+
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
707+
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
708+
709+
client.startClient();
710+
await Promise.all([
711+
httpBackend.flushAllExpected(),
712+
awaitSyncEvent(2),
713+
]);
714+
715+
const room = client.getRoom(roomOne);
716+
expect(room.getTimelineNeedsRefresh()).toEqual(true);
717+
// Make sure "Room.historyImportedWithinTimeline" was emitted
718+
expect(emitCount).toEqual(1);
719+
expect(room.getLastMarkerEventIdProcessed()).toEqual(markerEventId);
720+
});
721+
722+
// Mimic a marker event being sent far back in the scroll back but since our last sync
723+
it('new marker event in sync state should mark the timeline as needing a refresh', async () => {
724+
const nextSyncData = {
725+
next_batch: "batch_token",
726+
rooms: {
727+
join: {},
728+
},
729+
};
730+
nextSyncData.rooms.join[roomOne] = {
731+
timeline: {
732+
events: [
733+
utils.mkMessage({
734+
room: roomOne, user: otherUserId, msg: "hello again",
735+
}),
736+
],
737+
prev_batch: "pagTok",
738+
},
739+
state: {
740+
events: [
741+
// In subsequent syncs, a marker event in state
742+
// should trigger `timelineNeedsRefresh=true`
743+
markerEventFromRoomCreator,
744+
],
745+
},
746+
};
747+
748+
httpBackend.when("GET", "/sync").respond(200, normalFirstSync);
749+
httpBackend.when("GET", "/sync").respond(200, nextSyncData);
750+
751+
client.startClient();
752+
await Promise.all([
753+
httpBackend.flushAllExpected(),
754+
awaitSyncEvent(2),
755+
]);
756+
757+
const room = client.getRoom(roomOne);
758+
expect(room.getTimelineNeedsRefresh()).toEqual(true);
759+
});
760+
});
761+
});
762+
});
464763
});
465764

466765
describe("timeline", function() {

spec/unit/room-state.spec.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
33
import { filterEmitCallsByEventType } from "../test-utils/emitter";
44
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
55
import { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
6+
import { EventType } from "../../src/@types/event";
67

78
describe("RoomState", function() {
89
const roomId = "!foo:bar";
@@ -252,6 +253,29 @@ describe("RoomState", function() {
252253
);
253254
});
254255

256+
it("should emit 'RoomStateEvent.Marker' for each marker event", function() {
257+
const events = [
258+
utils.mkEvent({
259+
event: true,
260+
type: EventType.Marker,
261+
room: roomId,
262+
user: userA,
263+
skey: "",
264+
content: {
265+
"m.insertion_id": "$abc",
266+
},
267+
}),
268+
];
269+
let emitCount = 0;
270+
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
271+
expect(markerEvent).toEqual(events[emitCount]);
272+
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
273+
emitCount += 1;
274+
});
275+
state.setStateEvents(events, { timelineWasEmpty: true });
276+
expect(emitCount).toEqual(1);
277+
});
278+
255279
describe('beacon events', () => {
256280
it('adds new beacon info events to state and emits', () => {
257281
const beaconEvent = makeBeaconInfoEvent(userA, roomId);

src/@types/event.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export enum EventType {
3333
RoomGuestAccess = "m.room.guest_access",
3434
RoomServerAcl = "m.room.server_acl",
3535
RoomTombstone = "m.room.tombstone",
36+
Marker = "org.matrix.msc2716.marker", // MSC2716
3637
/**
3738
* @deprecated Should not be used.
3839
*/

src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,7 @@ type RoomEvents = RoomEvent.Name
790790
| RoomEvent.Receipt
791791
| RoomEvent.Tags
792792
| RoomEvent.LocalEchoUpdated
793+
| RoomEvent.historyImportedWithinTimeline
793794
| RoomEvent.AccountData
794795
| RoomEvent.MyMembership
795796
| RoomEvent.Timeline
@@ -799,6 +800,7 @@ type RoomStateEvents = RoomStateEvent.Events
799800
| RoomStateEvent.Members
800801
| RoomStateEvent.NewMember
801802
| RoomStateEvent.Update
803+
| RoomStateEvent.Marker
802804
;
803805

804806
type CryptoEvents = CryptoEvent.KeySignatureUploadFailure

0 commit comments

Comments
 (0)