diff --git a/src/stores/VideoChannelStore.ts b/src/stores/VideoChannelStore.ts index 0e249ac02f..8d7b32a94a 100644 --- a/src/stores/VideoChannelStore.ts +++ b/src/stores/VideoChannelStore.ts @@ -136,14 +136,40 @@ export default class VideoChannelStore extends AsyncStoreWithClient { } } + // Now that we got the messaging, we need a way to ensure that it doesn't get stopped + const dontStopMessaging = new Promise((resolve, reject) => { + const listener = (uid: string) => { + if (uid === jitsiUid) { + cleanup(); + reject(new Error("Messaging stopped")); + } + }; + const done = () => { + cleanup(); + resolve(); + }; + const cleanup = () => { + messagingStore.off(WidgetMessagingStoreEvent.StopMessaging, listener); + this.off(VideoChannelEvent.Connect, done); + this.off(VideoChannelEvent.Disconnect, done); + }; + + messagingStore.on(WidgetMessagingStoreEvent.StopMessaging, listener); + this.on(VideoChannelEvent.Connect, done); + this.on(VideoChannelEvent.Disconnect, done); + }); + if (!messagingStore.isWidgetReady(jitsiUid)) { // Wait for the widget to be ready to receive our join event try { - await waitForEvent( - messagingStore, - WidgetMessagingStoreEvent.WidgetReady, - (uid: string) => uid === jitsiUid, - ); + await Promise.race([ + waitForEvent( + messagingStore, + WidgetMessagingStoreEvent.WidgetReady, + (uid: string) => uid === jitsiUid, + ), + dontStopMessaging, + ]); } catch (e) { throw new Error(`Video channel in room ${roomId} never became ready: ${e}`); } @@ -178,11 +204,12 @@ export default class VideoChannelStore extends AsyncStoreWithClient { videoDevice: videoDevice?.label, }); try { - await waitForJoin; + await Promise.race([waitForJoin, dontStopMessaging]); } catch (e) { // If it timed out, clean up our advance preparations this.activeChannel = null; this.roomId = null; + messaging.off(`action:${ElementWidgetActions.CallParticipants}`, this.onParticipants); messaging.off(`action:${ElementWidgetActions.MuteAudio}`, this.onMuteAudio); messaging.off(`action:${ElementWidgetActions.UnmuteAudio}`, this.onUnmuteAudio); diff --git a/src/stores/widgets/WidgetMessagingStore.ts b/src/stores/widgets/WidgetMessagingStore.ts index bcc46c0e43..686f222a1f 100644 --- a/src/stores/widgets/WidgetMessagingStore.ts +++ b/src/stores/widgets/WidgetMessagingStore.ts @@ -25,6 +25,7 @@ import WidgetUtils from "../../utils/WidgetUtils"; export enum WidgetMessagingStoreEvent { StoreMessaging = "store_messaging", + StopMessaging = "stop_messaging", WidgetReady = "widget_ready", } @@ -71,9 +72,7 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { } public stopMessaging(widget: Widget, roomId: string) { - const uid = WidgetUtils.calcWidgetUid(widget.id, roomId); - this.widgetMap.remove(uid)?.stop(); - this.readyWidgets.delete(uid); + this.stopMessagingByUid(WidgetUtils.calcWidgetUid(widget.id, roomId)); } public getMessaging(widget: Widget, roomId: string): ClientWidgetApi { @@ -86,6 +85,8 @@ export class WidgetMessagingStore extends AsyncStoreWithClient { */ public stopMessagingByUid(widgetUid: string) { this.widgetMap.remove(widgetUid)?.stop(); + this.readyWidgets.delete(widgetUid); + this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid); } /** diff --git a/test/stores/VideoChannelStore-test.ts b/test/stores/VideoChannelStore-test.ts index 09508d477a..cf5def3334 100644 --- a/test/stores/VideoChannelStore-test.ts +++ b/test/stores/VideoChannelStore-test.ts @@ -114,23 +114,26 @@ describe("VideoChannelStore", () => { expect(store.roomId).toBeFalsy(); expect(store.connected).toEqual(false); - store.connect("!1:example.org", null, null); + const connectPromise = store.connect("!1:example.org", null, null); await confirmConnect(); + await expect(connectPromise).resolves.toBeUndefined(); expect(store.roomId).toEqual("!1:example.org"); expect(store.connected).toEqual(true); - store.disconnect(); + const disconnectPromise = store.disconnect(); await confirmDisconnect(); + await expect(disconnectPromise).resolves.toBeUndefined(); expect(store.roomId).toBeFalsy(); expect(store.connected).toEqual(false); WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org"); }); it("waits for messaging when connecting", async () => { - store.connect("!1:example.org", null, null); + const connectPromise = store.connect("!1:example.org", null, null); WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging); widgetReady(); await confirmConnect(); + await expect(connectPromise).resolves.toBeUndefined(); expect(store.roomId).toEqual("!1:example.org"); expect(store.connected).toEqual(true); @@ -138,4 +141,19 @@ describe("VideoChannelStore", () => { await confirmDisconnect(); WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org"); }); + + it("rejects if the widget's messaging gets stopped mid-connect", async () => { + WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging); + widgetReady(); + expect(store.roomId).toBeFalsy(); + expect(store.connected).toEqual(false); + + const connectPromise = store.connect("!1:example.org", null, null); + // Wait for the store to contact the widget API, then stop the messaging + await messageSent; + WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org"); + await expect(connectPromise).rejects.toBeDefined(); + expect(store.roomId).toBeFalsy(); + expect(store.connected).toEqual(false); + }); });