introduce error boundary in call member

This commit is contained in:
Bruno Windels 2023-01-09 13:50:27 +01:00
parent f0d2c19184
commit b1687d7115

View File

@ -19,6 +19,7 @@ import {makeTxnId, makeId} from "../../common";
import {EventType, CallErrorCode} from "../callEventTypes"; import {EventType, CallErrorCode} from "../callEventTypes";
import {formatToDeviceMessagesPayload} from "../../common"; import {formatToDeviceMessagesPayload} from "../../common";
import {sortedIndex} from "../../../utils/sortedIndex"; import {sortedIndex} from "../../../utils/sortedIndex";
import { ErrorBoundary } from "../../../utils/ErrorBoundary";
import type {MuteSettings} from "../common"; import type {MuteSettings} from "../common";
import type {Options as PeerCallOptions, RemoteMedia} from "../PeerCall"; import type {Options as PeerCallOptions, RemoteMedia} from "../PeerCall";
@ -94,6 +95,9 @@ class MemberConnection {
export class Member { export class Member {
private connection?: MemberConnection; private connection?: MemberConnection;
private expireTimeout?: Timeout; private expireTimeout?: Timeout;
private errorBoundary = new ErrorBoundary(err => {
this.options.emitUpdate(this, "error");
});
constructor( constructor(
public member: RoomMember, public member: RoomMember,
@ -104,6 +108,10 @@ export class Member {
this._renewExpireTimeout(updateMemberLog); this._renewExpireTimeout(updateMemberLog);
} }
get error(): Error | undefined {
return this.errorBoundary.error;
}
private _renewExpireTimeout(log: ILogItem) { private _renewExpireTimeout(log: ILogItem) {
this.expireTimeout?.dispose(); this.expireTimeout?.dispose();
this.expireTimeout = undefined; this.expireTimeout = undefined;
@ -166,23 +174,26 @@ export class Member {
/** @internal */ /** @internal */
connect(localMedia: LocalMedia, localMuteSettings: MuteSettings, turnServer: BaseObservableValue<RTCIceServer>, memberLogItem: ILogItem): ILogItem | undefined { connect(localMedia: LocalMedia, localMuteSettings: MuteSettings, turnServer: BaseObservableValue<RTCIceServer>, memberLogItem: ILogItem): ILogItem | undefined {
if (this.connection) { return this.errorBoundary.try(() => {
return; if (this.connection) {
} return;
// Safari can't send a MediaStream to multiple sources, so clone it }
const connection = new MemberConnection( // Safari can't send a MediaStream to multiple sources, so clone it
localMedia.clone(), const connection = new MemberConnection(
localMuteSettings, localMedia.clone(),
turnServer, localMuteSettings,
memberLogItem turnServer,
); memberLogItem
this.connection = connection; );
let connectLogItem; this.connection = connection;
connection.logItem.wrap("connect", async log => { let connectLogItem: ILogItem | undefined;
connectLogItem = log; connection.logItem.wrap("connect", async log => {
await this.callIfNeeded(log); connectLogItem = log;
await this.callIfNeeded(log);
});
throw new Error("connect failed!");
return connectLogItem;
}); });
return connectLogItem;
} }
private callIfNeeded(log: ILogItem): Promise<void> { private callIfNeeded(log: ILogItem): Promise<void> {
@ -211,30 +222,34 @@ export class Member {
/** @internal */ /** @internal */
disconnect(hangup: boolean): ILogItem | undefined { disconnect(hangup: boolean): ILogItem | undefined {
const {connection} = this; return this.errorBoundary.try(() => {
if (!connection) { const {connection} = this;
return; if (!connection) {
} return;
let disconnectLogItem;
// if if not sending the hangup, still log disconnect
connection.logItem.wrap("disconnect", async log => {
disconnectLogItem = log;
if (hangup && connection.peerCall) {
await connection.peerCall.hangup(CallErrorCode.UserHangup, log);
} }
let disconnectLogItem;
// if if not sending the hangup, still log disconnect
connection.logItem.wrap("disconnect", async log => {
disconnectLogItem = log;
if (hangup && connection.peerCall) {
await connection.peerCall.hangup(CallErrorCode.UserHangup, log);
}
});
connection.dispose();
this.connection = undefined;
return disconnectLogItem;
}); });
connection.dispose();
this.connection = undefined;
return disconnectLogItem;
} }
/** @internal */ /** @internal */
updateCallInfo(callDeviceMembership: CallDeviceMembership, causeItem: ILogItem) { updateCallInfo(callDeviceMembership: CallDeviceMembership, causeItem: ILogItem) {
this.callDeviceMembership = callDeviceMembership; this.errorBoundary.try(() => {
this._renewExpireTimeout(causeItem); this.callDeviceMembership = callDeviceMembership;
if (this.connection) { this._renewExpireTimeout(causeItem);
this.connection.logItem.refDetached(causeItem); if (this.connection) {
} this.connection.logItem.refDetached(causeItem);
}
});
} }
/** @internal */ /** @internal */
@ -308,49 +323,51 @@ export class Member {
/** @internal */ /** @internal */
handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, syncLog: ILogItem): void { handleDeviceMessage(message: SignallingMessage<MGroupCallBase>, syncLog: ILogItem): void {
const {connection} = this; this.errorBoundary.try(() => {
if (connection) { const {connection} = this;
const destSessionId = message.content.dest_session_id; if (connection) {
if (destSessionId !== this.options.sessionId) { const destSessionId = message.content.dest_session_id;
const logItem = connection.logItem.log({l: "ignoring to_device event with wrong session_id", destSessionId, type: message.type}); if (destSessionId !== this.options.sessionId) {
syncLog.refDetached(logItem); const logItem = connection.logItem.log({l: "ignoring to_device event with wrong session_id", destSessionId, type: message.type});
return; syncLog.refDetached(logItem);
} return;
// if there is no peerCall, we either create it with an invite and Handle is implied or we'll ignore it
let action = IncomingMessageAction.Handle;
if (connection.peerCall) {
action = connection.peerCall.getMessageAction(message);
// deal with glare and replacing the call before creating new calls
if (action === IncomingMessageAction.InviteGlare) {
const {shouldReplace, log} = connection.peerCall.handleInviteGlare(message, this.deviceId, connection.logItem);
if (log) {
syncLog.refDetached(log);
}
if (shouldReplace) {
connection.peerCall = undefined;
action = IncomingMessageAction.Handle;
}
} }
} // if there is no peerCall, we either create it with an invite and Handle is implied or we'll ignore it
if (message.type === EventType.Invite && !connection.peerCall) { let action = IncomingMessageAction.Handle;
connection.peerCall = this._createPeerCall(message.content.call_id);
}
if (action === IncomingMessageAction.Handle) {
const idx = sortedIndex(connection.queuedSignallingMessages, message, (a, b) => a.content.seq - b.content.seq);
connection.queuedSignallingMessages.splice(idx, 0, message);
if (connection.peerCall) { if (connection.peerCall) {
const hasNewMessageBeenDequeued = this.dequeueSignallingMessages(connection, connection.peerCall, message, syncLog); action = connection.peerCall.getMessageAction(message);
if (!hasNewMessageBeenDequeued) { // deal with glare and replacing the call before creating new calls
syncLog.refDetached(connection.logItem.log({l: "queued signalling message", type: message.type, seq: message.content.seq})); if (action === IncomingMessageAction.InviteGlare) {
const {shouldReplace, log} = connection.peerCall.handleInviteGlare(message, this.deviceId, connection.logItem);
if (log) {
syncLog.refDetached(log);
}
if (shouldReplace) {
connection.peerCall = undefined;
action = IncomingMessageAction.Handle;
}
} }
} }
} else if (action === IncomingMessageAction.Ignore && connection.peerCall) { if (message.type === EventType.Invite && !connection.peerCall) {
const logItem = connection.logItem.log({l: "ignoring to_device event with wrong call_id", callId: message.content.call_id, type: message.type}); connection.peerCall = this._createPeerCall(message.content.call_id);
syncLog.refDetached(logItem); }
if (action === IncomingMessageAction.Handle) {
const idx = sortedIndex(connection.queuedSignallingMessages, message, (a, b) => a.content.seq - b.content.seq);
connection.queuedSignallingMessages.splice(idx, 0, message);
if (connection.peerCall) {
const hasNewMessageBeenDequeued = this.dequeueSignallingMessages(connection, connection.peerCall, message, syncLog);
if (!hasNewMessageBeenDequeued) {
syncLog.refDetached(connection.logItem.log({l: "queued signalling message", type: message.type, seq: message.content.seq}));
}
}
} else if (action === IncomingMessageAction.Ignore && connection.peerCall) {
const logItem = connection.logItem.log({l: "ignoring to_device event with wrong call_id", callId: message.content.call_id, type: message.type});
syncLog.refDetached(logItem);
}
} else {
syncLog.log({l: "member not connected", userId: this.userId, deviceId: this.deviceId});
} }
} else { });
syncLog.log({l: "member not connected", userId: this.userId, deviceId: this.deviceId});
}
} }
private dequeueSignallingMessages(connection: MemberConnection, peerCall: PeerCall, newMessage: SignallingMessage<MGroupCallBase>, syncLog: ILogItem): boolean { private dequeueSignallingMessages(connection: MemberConnection, peerCall: PeerCall, newMessage: SignallingMessage<MGroupCallBase>, syncLog: ILogItem): boolean {
@ -373,19 +390,23 @@ export class Member {
/** @internal */ /** @internal */
async setMedia(localMedia: LocalMedia, previousMedia: LocalMedia): Promise<void> { async setMedia(localMedia: LocalMedia, previousMedia: LocalMedia): Promise<void> {
const {connection} = this; return this.errorBoundary.try(async () => {
if (connection) { const {connection} = this;
connection.localMedia = localMedia.replaceClone(connection.localMedia, previousMedia); if (connection) {
await connection.peerCall?.setMedia(connection.localMedia, connection.logItem); connection.localMedia = localMedia.replaceClone(connection.localMedia, previousMedia);
} await connection.peerCall?.setMedia(connection.localMedia, connection.logItem);
}
});
} }
async setMuted(muteSettings: MuteSettings): Promise<void> { async setMuted(muteSettings: MuteSettings): Promise<void> {
const {connection} = this; return this.errorBoundary.try(async () => {
if (connection) { const {connection} = this;
connection.localMuteSettings = muteSettings; if (connection) {
await connection.peerCall?.setMuted(muteSettings, connection.logItem); connection.localMuteSettings = muteSettings;
} await connection.peerCall?.setMuted(muteSettings, connection.logItem);
}
});
} }
private _createPeerCall(callId: string): PeerCall { private _createPeerCall(callId: string): PeerCall {