Home Reference Source Test

dist/participant.js

import { EventEmitter } from 'eventemitter3';
import { stringify } from 'querystring';
import { RPC, RPCError } from './rpc';
import { ErrorCode } from './typings';
/**
 * Stringifies and appends the given query string to the URL.
 */
function appendQueryString(url, qs) {
    const delimiter = url.indexOf('?') > -1 ? '&' : '?';
    return `${url}${delimiter}${stringify(qs)}`;
}
/**
 * Participant is a bridge between the Interactive service and an iframe that
 * shows custom controls. It proxies calls between them and emits events
 * when states change.
 * @private (at least to most consumers!)
 */
export class Participant extends EventEmitter {
    constructor(frame, settings) {
        super();
        this.frame = frame;
        /**
         * Buffer of packets from to replay once the controls load.
         * As soon as we connect to interactive it'll send the initial state
         * messages, but there's a good chance we won't have loaded the controls
         * by that time, so buffer 'em until the controls say they're ready.
         */
        this.replayBuffer = [];
        /**
         * Controls state.
         */
        this.state = 0 /* Loading */;
        /**
         * onFrameLoad is called once the iframe loads.
         * @private
         */
        this.onFrameLoad = () => {
            if (this.state === 0 /* Loading */) {
                this.attachListeners();
            }
            this.frame.removeEventListener('load', this.onFrameLoad);
        };
        this.runOnRpc(rpc => {
            rpc.call('updateSettings', settings, false);
            const windowsAPI = window.Windows;
            if (windowsAPI) {
                const viewPane = windowsAPI.UI.ViewManagement.InputPane.getForCurrentView();
                viewPane.onshowing = () => {
                    rpc.call('keyboardShowing', {}, false);
                };
                viewPane.onhiding = () => {
                    rpc.call('keyboardHiding', {}, false);
                };
            }
        });
    }
    /**
     * Creates a connection to the given Interactive address.
     */
    connect(options) {
        const qs = {
            // cache bust the iframe to ensure that it reloads
            // whenever we get a new connection.
            bustCache: Date.now(),
            key: options.key,
            'x-protocol-version': Participant.protocolVersion,
            'x-auth-user': options.xAuthUser ? JSON.stringify(options.xAuthUser) : undefined,
        };
        const ws = (this.websocket = new WebSocket(appendQueryString(options.socketAddress, qs)));
        this.frame.src = options.contentAddress;
        this.frame.addEventListener('load', this.onFrameLoad);
        ws.addEventListener('message', data => {
            this.sendInteractive(data.data);
        });
        ws.addEventListener('close', ev => {
            this.emit('close', {
                code: ev.code,
                message: ev.reason,
                expected: this.state === 2 /* Closing */,
                ev,
            });
            this.state = 3 /* Closed */;
            this.destroy();
        });
        ws.addEventListener('error', ev => {
            this.handleWebsocketError(ev);
        });
        return this;
    }
    add(method, fn) {
        this.runOnRpc(rpc => {
            rpc.expose(method, fn);
        });
        return this;
    }
    /**
     * Updates the controls' settings.
     */
    updateSettings(settings) {
        this.runOnRpc(rpc => {
            rpc.call('updateSettings', settings, false);
        });
    }
    /**
     * Triggers a dump of state from the nested controls. Returns undefined if
     * the controls do not expose a dumpState method.
     */
    dumpState() {
        if (!this.rpc) {
            return Promise.resolve(undefined);
        }
        return this.rpc.call('dumpState', {}, true).catch(err => {
            if (err instanceof RPCError && err.code === ErrorCode.AppBadMethod) {
                return undefined; // controls don't expose dumpState, sad but we'll hide our sadness
            }
            throw new err();
        });
    }
    /**
     * Closes the participant connection and frees resources.
     */
    destroy() {
        if (this.state < 2 /* Closing */) {
            this.state = 2 /* Closing */;
        }
        if (this.rpc) {
            this.rpc.destroy();
        }
        try {
            if (this.websocket) {
                this.websocket.close();
            }
        }
        catch (_e) {
            // Ignored. Sockets can be fussy if they're closed at
            // the wrong time but it doesn't cause issues.
        }
    }
    on(event, handler) {
        super.on(event, handler);
        return this;
    }
    /**
     * Calls the function with the RPC instance once it's ready and attached.
     */
    runOnRpc(fn) {
        if (this.state !== 1 /* Ready */) {
            this.replayBuffer.push(fn);
        }
        else {
            fn(this.rpc);
        }
    }
    /**
     * sendInteractive broadcasts the interactive payload down to the controls,
     * and emits a `transmit` event.
     */
    sendInteractive(data) {
        const parsed = JSON.parse(data);
        this.runOnRpc(rpc => {
            rpc.call('recieveInteractivePacket', parsed, false);
        });
        this.emit('transmit', parsed);
    }
    /**
     * attachListeners is called once the frame contents load to boot up
     * the RPC system.
     */
    attachListeners() {
        this.rpc = new RPC(this.frame.contentWindow, '1.0');
        this.rpc.expose('sendInteractivePacket', data => {
            this.websocket.send(JSON.stringify(Object.assign({}, data, { type: 'method', discard: true })));
        });
        this.rpc.expose('controlsReady', () => {
            if (this.state !== 0 /* Loading */) {
                return;
            }
            this.state = 1 /* Ready */;
            this.replayBuffer.forEach(p => {
                p(this.rpc);
            });
            this.replayBuffer = [];
            this.emit('loaded');
        });
        this.rpc.expose('maximize', (params) => {
            this.emit('maximize', params.maximized, params.message);
        });
        this.rpc.expose('moveVideo', (options) => {
            this.emit('moveVideo', options);
        });
        this.rpc.expose('unloading', () => {
            this.emit('unload');
        });
        this.rpc.expose('log', params => {
            this.emit('log', params);
        });
        this.rpc.expose('focusOut', () => {
            this.emit('focusOut');
        });
        this.rpc.expose('handleExit', () => {
            this.emit('handleExit');
        });
        this.rpc.expose('navigate', () => {
            this.emit('navigate');
        });
        this.rpc.call('resendReady', {}, false);
    }
    /**
     * handleWebsocketError is called when the websocket emits an `error`. This
     * is generally called when the connection is terminated before a socket
     * connection is established. We want to go back and get the error code/body.
     */
    handleWebsocketError(ev) {
        // tslint:disable-next-line
        fetch(this.websocket.url.replace(/^ws/, 'http'))
            .then(res => {
            return res.text().then(message => {
                this.emit('close', {
                    message,
                    code: res.status,
                    expected: this.state === 2 /* Closing */,
                    ev,
                });
            });
        })
            .catch(err => {
            this.emit('close', {
                code: -1,
                message: err.message,
                expected: this.state === 2 /* Closing */,
                ev,
            });
        })
            .then(() => {
            this.state = 3 /* Closed */;
            this.destroy();
        });
    }
}
/**
 * Interactive protocol version this participant implements.
 */
Participant.protocolVersion = '2.0';