Skip to content

Commit 36a574a

Browse files
authored
Use server side relations for voice broadcasts (matrix-org#9534)
1 parent 3747464 commit 36a574a

11 files changed

+385
-181
lines changed

src/events/RelationsHelper.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export class RelationsHelper
3838
extends TypedEventEmitter<RelationsHelperEvent, EventMap>
3939
implements IDestroyable {
4040
private relations?: Relations;
41+
private eventId: string;
42+
private roomId: string;
4143

4244
public constructor(
4345
private event: MatrixEvent,
@@ -46,6 +48,21 @@ export class RelationsHelper
4648
private client: MatrixClient,
4749
) {
4850
super();
51+
52+
const eventId = event.getId();
53+
54+
if (!eventId) {
55+
throw new Error("unable to create RelationsHelper: missing event ID");
56+
}
57+
58+
const roomId = event.getRoomId();
59+
60+
if (!roomId) {
61+
throw new Error("unable to create RelationsHelper: missing room ID");
62+
}
63+
64+
this.eventId = eventId;
65+
this.roomId = roomId;
4966
this.setUpRelations();
5067
}
5168

@@ -73,7 +90,7 @@ export class RelationsHelper
7390
private setRelations(): void {
7491
const room = this.client.getRoom(this.event.getRoomId());
7592
this.relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
76-
this.event.getId(),
93+
this.eventId,
7794
this.relationType,
7895
this.relationEventType,
7996
);
@@ -87,6 +104,32 @@ export class RelationsHelper
87104
this.relations?.getRelations()?.forEach(e => this.emit(RelationsHelperEvent.Add, e));
88105
}
89106

107+
public getCurrent(): MatrixEvent[] {
108+
return this.relations?.getRelations() || [];
109+
}
110+
111+
/**
112+
* Fetches all related events from the server and emits them.
113+
*/
114+
public async emitFetchCurrent(): Promise<void> {
115+
let nextBatch: string | undefined = undefined;
116+
117+
do {
118+
const response = await this.client.relations(
119+
this.roomId,
120+
this.eventId,
121+
this.relationType,
122+
this.relationEventType,
123+
{
124+
from: nextBatch,
125+
limit: 50,
126+
},
127+
);
128+
nextBatch = response?.nextBatch;
129+
response?.events.forEach(e => this.emit(RelationsHelperEvent.Add, e));
130+
} while (nextBatch);
131+
}
132+
90133
public destroy(): void {
91134
this.removeAllListeners();
92135
this.event.off(MatrixEventEvent.RelationsCreated, this.onRelationsCreated);

src/voice-broadcast/models/VoiceBroadcastPlayback.ts

+34-23
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ import { MediaEventHelper } from "../../utils/MediaEventHelper";
3232
import { IDestroyable } from "../../utils/IDestroyable";
3333
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
3434
import { RelationsHelper, RelationsHelperEvent } from "../../events/RelationsHelper";
35-
import { getReferenceRelationsForEvent } from "../../events";
3635
import { VoiceBroadcastChunkEvents } from "../utils/VoiceBroadcastChunkEvents";
3736

3837
export enum VoiceBroadcastPlaybackState {
@@ -89,15 +88,27 @@ export class VoiceBroadcastPlayback
8988
this.setUpRelationsHelper();
9089
}
9190

92-
private setUpRelationsHelper(): void {
91+
private async setUpRelationsHelper(): Promise<void> {
9392
this.infoRelationHelper = new RelationsHelper(
9493
this.infoEvent,
9594
RelationType.Reference,
9695
VoiceBroadcastInfoEventType,
9796
this.client,
9897
);
99-
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
100-
this.infoRelationHelper.emitCurrent();
98+
this.infoRelationHelper.getCurrent().forEach(this.addInfoEvent);
99+
100+
if (this.infoState !== VoiceBroadcastInfoState.Stopped) {
101+
// Only required if not stopped. Stopped is the final state.
102+
this.infoRelationHelper.on(RelationsHelperEvent.Add, this.addInfoEvent);
103+
104+
try {
105+
await this.infoRelationHelper.emitFetchCurrent();
106+
} catch (err) {
107+
logger.warn("error fetching server side relation for voice broadcast info", err);
108+
// fall back to local events
109+
this.infoRelationHelper.emitCurrent();
110+
}
111+
}
101112

102113
this.chunkRelationHelper = new RelationsHelper(
103114
this.infoEvent,
@@ -106,7 +117,15 @@ export class VoiceBroadcastPlayback
106117
this.client,
107118
);
108119
this.chunkRelationHelper.on(RelationsHelperEvent.Add, this.addChunkEvent);
109-
this.chunkRelationHelper.emitCurrent();
120+
121+
try {
122+
// TODO Michael W: only fetch events if needed, blocked by PSF-1708
123+
await this.chunkRelationHelper.emitFetchCurrent();
124+
} catch (err) {
125+
logger.warn("error fetching server side relation for voice broadcast chunks", err);
126+
// fall back to local events
127+
this.chunkRelationHelper.emitCurrent();
128+
}
110129
}
111130

112131
private addChunkEvent = async (event: MatrixEvent): Promise<boolean> => {
@@ -150,23 +169,18 @@ export class VoiceBroadcastPlayback
150169
this.setInfoState(state);
151170
};
152171

153-
private async loadChunks(): Promise<void> {
154-
const relations = getReferenceRelationsForEvent(this.infoEvent, EventType.RoomMessage, this.client);
155-
const chunkEvents = relations?.getRelations();
156-
157-
if (!chunkEvents) {
158-
return;
159-
}
172+
private async enqueueChunks(): Promise<void> {
173+
const promises = this.chunkEvents.getEvents().reduce((promises, event: MatrixEvent) => {
174+
if (!this.playbacks.has(event.getId() || "")) {
175+
promises.push(this.enqueueChunk(event));
176+
}
177+
return promises;
178+
}, [] as Promise<void>[]);
160179

161-
this.chunkEvents.addEvents(chunkEvents);
162-
this.setDuration(this.chunkEvents.getLength());
163-
164-
for (const chunkEvent of chunkEvents) {
165-
await this.enqueueChunk(chunkEvent);
166-
}
180+
await Promise.all(promises);
167181
}
168182

169-
private async enqueueChunk(chunkEvent: MatrixEvent) {
183+
private async enqueueChunk(chunkEvent: MatrixEvent): Promise<void> {
170184
const eventId = chunkEvent.getId();
171185

172186
if (!eventId) {
@@ -317,10 +331,7 @@ export class VoiceBroadcastPlayback
317331
}
318332

319333
public async start(): Promise<void> {
320-
if (this.playbacks.size === 0) {
321-
await this.loadChunks();
322-
}
323-
334+
await this.enqueueChunks();
324335
const chunkEvents = this.chunkEvents.getEvents();
325336

326337
const toPlay = this.getInfoState() === VoiceBroadcastInfoState.Stopped

test/@types/common.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
export type PublicInterface<T> = {
18+
[P in keyof T]: T[P];
19+
};

test/events/RelationsHelper-test.ts

+74-10
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,15 @@ import { Relations } from "matrix-js-sdk/src/models/relations";
2828
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
2929

3030
import { RelationsHelper, RelationsHelperEvent } from "../../src/events/RelationsHelper";
31-
import { mkEvent, mkStubRoom, stubClient } from "../test-utils";
31+
import { mkEvent, mkRelationsContainer, mkStubRoom, stubClient } from "../test-utils";
3232

3333
describe("RelationsHelper", () => {
3434
const roomId = "!room:example.com";
35+
let userId: string;
3536
let event: MatrixEvent;
3637
let relatedEvent1: MatrixEvent;
3738
let relatedEvent2: MatrixEvent;
39+
let relatedEvent3: MatrixEvent;
3840
let room: Room;
3941
let client: MatrixClient;
4042
let relationsHelper: RelationsHelper;
@@ -46,47 +48,81 @@ describe("RelationsHelper", () => {
4648

4749
beforeEach(() => {
4850
client = stubClient();
51+
userId = client.getUserId() || "";
52+
mocked(client.relations).mockClear();
4953
room = mkStubRoom(roomId, "test room", client);
50-
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
54+
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
5155
if (getRoomId === roomId) {
5256
return room;
5357
}
58+
59+
return null;
5460
});
5561
event = mkEvent({
5662
event: true,
5763
type: EventType.RoomMessage,
5864
room: roomId,
59-
user: client.getUserId(),
65+
user: userId,
6066
content: {},
6167
});
6268
relatedEvent1 = mkEvent({
6369
event: true,
6470
type: EventType.RoomMessage,
6571
room: roomId,
66-
user: client.getUserId(),
67-
content: {},
72+
user: userId,
73+
content: { relatedEvent: 1 },
6874
});
6975
relatedEvent2 = mkEvent({
7076
event: true,
7177
type: EventType.RoomMessage,
7278
room: roomId,
73-
user: client.getUserId(),
74-
content: {},
79+
user: userId,
80+
content: { relatedEvent: 2 },
81+
});
82+
relatedEvent3 = mkEvent({
83+
event: true,
84+
type: EventType.RoomMessage,
85+
room: roomId,
86+
user: userId,
87+
content: { relatedEvent: 3 },
7588
});
7689
onAdd = jest.fn();
90+
relationsContainer = mkRelationsContainer();
7791
// TODO Michael W: create test utils, remove casts
78-
relationsContainer = {
79-
getChildEventsForEvent: jest.fn(),
80-
} as unknown as RelationsContainer;
8192
relations = {
8293
getRelations: jest.fn(),
8394
on: jest.fn().mockImplementation((type, l) => relationsOnAdd = l),
95+
off: jest.fn(),
8496
} as unknown as Relations;
8597
timelineSet = {
8698
relations: relationsContainer,
8799
} as unknown as EventTimelineSet;
88100
});
89101

102+
afterEach(() => {
103+
relationsHelper?.destroy();
104+
});
105+
106+
describe("when there is an event without ID", () => {
107+
it("should raise an error", () => {
108+
jest.spyOn(event, "getId").mockReturnValue(undefined);
109+
110+
expect(() => {
111+
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
112+
}).toThrowError("unable to create RelationsHelper: missing event ID");
113+
});
114+
});
115+
116+
describe("when there is an event without room ID", () => {
117+
it("should raise an error", () => {
118+
jest.spyOn(event, "getRoomId").mockReturnValue(undefined);
119+
120+
expect(() => {
121+
new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
122+
}).toThrowError("unable to create RelationsHelper: missing room ID");
123+
});
124+
});
125+
90126
describe("when there is an event without relations", () => {
91127
beforeEach(() => {
92128
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
@@ -118,6 +154,34 @@ describe("RelationsHelper", () => {
118154
});
119155
});
120156

157+
describe("when there is an event with two pages server side relations", () => {
158+
beforeEach(() => {
159+
mocked(client.relations)
160+
.mockResolvedValueOnce({
161+
events: [relatedEvent1, relatedEvent2],
162+
nextBatch: "next",
163+
})
164+
.mockResolvedValueOnce({
165+
events: [relatedEvent3],
166+
nextBatch: null,
167+
});
168+
relationsHelper = new RelationsHelper(event, RelationType.Reference, EventType.RoomMessage, client);
169+
relationsHelper.on(RelationsHelperEvent.Add, onAdd);
170+
});
171+
172+
describe("emitFetchCurrent", () => {
173+
beforeEach(async () => {
174+
await relationsHelper.emitFetchCurrent();
175+
});
176+
177+
it("should emit the server side events", () => {
178+
expect(onAdd).toHaveBeenCalledWith(relatedEvent1);
179+
expect(onAdd).toHaveBeenCalledWith(relatedEvent2);
180+
expect(onAdd).toHaveBeenCalledWith(relatedEvent3);
181+
});
182+
});
183+
});
184+
121185
describe("when there is an event with relations", () => {
122186
beforeEach(() => {
123187
mocked(room.getUnfilteredTimelineSet).mockReturnValue(timelineSet);

test/test-utils/audio.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@ import { SimpleObservable } from "matrix-widget-api";
2020
import { Playback, PlaybackState } from "../../src/audio/Playback";
2121
import { PlaybackClock } from "../../src/audio/PlaybackClock";
2222
import { UPDATE_EVENT } from "../../src/stores/AsyncStore";
23-
24-
type PublicInterface<T> = {
25-
[P in keyof T]: T[P];
26-
};
23+
import { PublicInterface } from "../@types/common";
2724

2825
export const createTestPlayback = (): Playback => {
2926
const eventEmitter = new EventEmitter();

test/test-utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from './call';
2525
export * from './wrappers';
2626
export * from './utilities';
2727
export * from './date';
28+
export * from './relations';

test/test-utils/relations.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { Relations } from "matrix-js-sdk/src/models/relations";
18+
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
19+
20+
import { PublicInterface } from "../@types/common";
21+
22+
export const mkRelations = (): Relations => {
23+
return {
24+
25+
} as PublicInterface<Relations> as Relations;
26+
};
27+
28+
export const mkRelationsContainer = (): RelationsContainer => {
29+
return {
30+
aggregateChildEvent: jest.fn(),
31+
aggregateParentEvent: jest.fn(),
32+
getAllChildEventsForEvent: jest.fn(),
33+
getChildEventsForEvent: jest.fn(),
34+
} as PublicInterface<RelationsContainer> as RelationsContainer;
35+
};

0 commit comments

Comments
 (0)