Implement widget API for signaling the widget to gracefully terminate

In theory, widgets could use iframe unload/beforeunload events for
cleanup, but in practice browsers have restrictions on what can be done
in those events which may not give sufficient time for clean
termination.

Signed-off-by: Pauli Virtanen <pav@iki.fi>
pull/21833/head
Pauli Virtanen 2020-04-18 14:52:41 +03:00
parent 355539feb5
commit 1f2bf0485e
2 changed files with 27 additions and 0 deletions

View File

@ -87,6 +87,19 @@ export default class WidgetMessaging {
}); });
} }
/**
* Tells the widget that it should terminate now.
* It is not necessarily called in all instances before the widget is removed,
* and the client may force termination with a timeout.
* @returns {Promise<*>} Resolves when widget has acknowledged the message.
*/
terminate() {
return this.messageToWidget({
api: OUTBOUND_API_NAME,
action: KnownWidgetActions.Terminate,
});
}
/** /**
* Request a screenshot from a widget * Request a screenshot from a widget
* @return {Promise} To be resolved with screenshot data when it has been generated * @return {Promise} To be resolved with screenshot data when it has been generated

View File

@ -34,6 +34,7 @@ export enum KnownWidgetActions {
ReceiveOpenIDCredentials = "openid_credentials", ReceiveOpenIDCredentials = "openid_credentials",
SetAlwaysOnScreen = "set_always_on_screen", SetAlwaysOnScreen = "set_always_on_screen",
ClientReady = "im.vector.ready", ClientReady = "im.vector.ready",
Terminate = "im.vector.terminate",
} }
export type WidgetAction = KnownWidgetActions | string; export type WidgetAction = KnownWidgetActions | string;
@ -68,6 +69,8 @@ export class WidgetApi {
private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {}; private inFlightRequests: { [requestId: string]: (reply: FromWidgetRequest) => void } = {};
private readyPromise: Promise<any>; private readyPromise: Promise<any>;
private readyPromiseResolve: () => void; private readyPromiseResolve: () => void;
private terminatePromise: Promise<any>;
private terminatePromiseResolve: () => void;
/** /**
* Set this to true if your widget is expecting a ready message from the client. False otherwise (default). * Set this to true if your widget is expecting a ready message from the client. False otherwise (default).
@ -78,6 +81,7 @@ export class WidgetApi {
this.origin = new URL(currentUrl).origin; this.origin = new URL(currentUrl).origin;
this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve); this.readyPromise = new Promise<any>(resolve => this.readyPromiseResolve = resolve);
this.terminatePromise = new Promise<any>(resolve => this.terminatePromiseResolve = resolve);
window.addEventListener("message", event => { window.addEventListener("message", event => {
if (event.origin !== this.origin) return; // ignore: invalid origin if (event.origin !== this.origin) return; // ignore: invalid origin
@ -98,6 +102,12 @@ export class WidgetApi {
// Automatically acknowledge so we can move on // Automatically acknowledge so we can move on
this.replyToRequest(<ToWidgetRequest>payload, {}); this.replyToRequest(<ToWidgetRequest>payload, {});
} else if (payload.action === KnownWidgetActions.Terminate) {
// Reply after resolving
this.terminatePromise.then(() => {
this.replyToRequest(<ToWidgetRequest>payload, {});
});
this.terminatePromiseResolve();
} else { } else {
console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`); console.warn(`[WidgetAPI] Got unexpected action: ${payload.action}`);
} }
@ -116,6 +126,10 @@ export class WidgetApi {
return this.readyPromise; return this.readyPromise;
} }
public addTerminateCallback(action) {
this.terminatePromise = this.terminatePromise.then(action);
}
private replyToRequest(payload: ToWidgetRequest, reply: any) { private replyToRequest(payload: ToWidgetRequest, reply: any) {
if (!window.parent) return; if (!window.parent) return;