Home Reference Source Test

dist/rpc.js

import { EventEmitter } from 'eventemitter3';
/**
 * Checks whether the message duck-types into an Interactive message.
 * This is needed to distinguish between postmessages that we get,
 * and postmessages from other sources.
 */
function isRPCMessage(data) {
    return (data.type === 'method' || data.type === 'reply') && typeof data.counter === 'number';
}
/**
 * An RPCError can be thrown in socket.call() if bad input is
 * passed to the service. See the Interactive protocol doc for an enumaration
 * of codes and messages: https://dev.mixer.com/reference/interactive/protocol/protocol.pdf
 */
export class RPCError extends Error {
    constructor(code, message, path) {
        super(`Error #${code}: ${message}`);
        this.code = code;
        this.message = message;
        this.path = path;
    }
}
export function objToError(obj) {
    return new RPCError(obj.code, obj.message, obj.path);
}
/**
 * Primitive postMessage based RPC for the controls to interact with the
 * parent frame.
 */
export class RPC extends EventEmitter {
    /**
     * Creates a new RPC instance. Note: you should use the `rpc` singleton,
     * rather than creating this class directly, in your controls.
     *
     * @param {window} target The window instance to make calls to or from.
     * @param {string} protocolVersion The protocol version to communicate
     * to the remote.
     * @param {string} [origin='*'] Optionally, allow communication with the
     * target if its origin matches this.
     */
    constructor(target, protocolVersion, origin = '*') {
        super();
        this.target = target;
        this.origin = origin;
        this.idCounter = 0;
        this.calls = Object.create(null);
        this.callCounter = 0;
        this.remoteCallQueue = [];
        this.lastSequentialCall = -1;
        this.listener = (ev) => {
            const packet = ev.data;
            if (!isRPCMessage(packet) || packet.serviceID !== RPC.serviceID) {
                return;
            }
            // postMessage does not guarantee message order, reorder messages as needed.
            // Reset the call counter when we get a "ready" so that the other end sees
            // calls starting from 0.
            if (packet.type === 'method' && packet.method === 'ready') {
                this.lastSequentialCall = packet.counter - 1;
                this.remoteProtocolVersion = packet.params.protocolVersion;
                this.callCounter = 0;
            }
            if (packet.counter <= this.lastSequentialCall + 1) {
                this.dispatchIncoming(packet);
                this.replayQueue();
                return;
            }
            for (let i = 0; i < this.remoteCallQueue.length; i++) {
                if (this.remoteCallQueue[i].counter > packet.counter) {
                    this.remoteCallQueue.splice(i, 0, packet);
                    return;
                }
            }
            this.remoteCallQueue.push(packet);
        };
        window.addEventListener('message', this.listener);
        this.call('ready', { protocolVersion }, false);
    }
    /**
     * Attaches a method callable by the other window, to this one. The handler
     * function will be invoked with whatever the other window gives us. Can
     * return a Promise, or the results directly.
     *
     * @param {string} method
     * @param {function(params: any): Promise|*} handler
     */
    expose(method, handler) {
        this.on(method, (data) => {
            if (data.discard) {
                handler(data.params);
                return;
            }
            // tslint:disable-next-line
            Promise.resolve(handler(data.params)).then(result => {
                const packet = {
                    type: 'reply',
                    serviceID: RPC.serviceID,
                    id: data.id,
                    result,
                };
                this.emit('sendReply', packet);
                this.post(packet);
            });
        });
    }
    /**
     * Makes an RPC call out to the target window.
     *
     * @param {string} method
     * @param {*} params
     * @param {boolean} [waitForReply=true]
     * @return {Promise.<object> | undefined} If waitForReply is true, a
     * promise is returned that resolves once the server responds.
     */
    call(method, params, waitForReply = true) {
        const id = this.idCounter++;
        const packet = {
            type: 'method',
            serviceID: RPC.serviceID,
            id,
            params,
            method,
            discard: !waitForReply,
        };
        this.emit('sendMethod', packet);
        this.post(packet);
        if (!waitForReply) {
            return;
        }
        return new Promise((resolve, reject) => {
            this.calls[id] = (err, res) => {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(res);
                }
            };
        });
    }
    /**
     * Tears down resources associated with the RPC client.
     */
    destroy() {
        this.emit('destroy');
        window.removeEventListener('message', this.listener);
    }
    /**
     * Returns the protocol version that the remote client implements. This
     * will return `undefined` until we get a `ready` event.
     * @return {string | undefined}
     */
    remoteVersion() {
        return this.remoteProtocolVersion;
    }
    handleReply(packet) {
        const handler = this.calls[packet.id];
        if (!handler) {
            return;
        }
        if (packet.error) {
            handler(objToError(packet.error), null);
        }
        else {
            handler(null, packet.result);
        }
        delete this.calls[packet.id];
    }
    post(message) {
        message.counter = this.callCounter++;
        this.target.postMessage(message, this.origin);
    }
    replayQueue() {
        while (this.remoteCallQueue.length) {
            const next = this.remoteCallQueue[0];
            if (next.counter > this.lastSequentialCall + 1) {
                return;
            }
            this.dispatchIncoming(this.remoteCallQueue.shift());
        }
    }
    dispatchIncoming(packet) {
        this.lastSequentialCall = packet.counter;
        switch (packet.type) {
            case 'method':
                this.emit('recvMethod', packet);
                if (this.listeners(packet.method).length > 0) {
                    this.emit(packet.method, packet);
                    return;
                }
                this.post({
                    type: 'reply',
                    serviceID: RPC.serviceID,
                    id: packet.id,
                    error: { code: 4003, message: 'Unknown method name' },
                    result: null,
                });
                break;
            case 'reply':
                this.emit('recvReply', packet);
                this.handleReply(packet);
                break;
            default:
        }
    }
}
/**
 * Service ID for this module. This is used to prevent
 * multiple postMessage-based APIs for clobbering each other.
 */
RPC.serviceID = '8f5b3a83-dd7b-4b8a-84ad-146948bc8d27';