index.js

"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (_) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
exports.__esModule = true;
var service_worker_logger_1 = require("@holo-host/service-worker-logger");
var postmate_1 = __importDefault(require("postmate"));
var async_with_timeout_1 = __importDefault(require("./async_with_timeout"));
var async_with_timeout_2 = require("./async_with_timeout");
var log = service_worker_logger_1.logging.getLogger('COMB');
log.setLevel('error');
/**
 * @module COMB
 *
 * @description
 * Parent window
 * ```html
 * <script type="text/javascript" src="./holo_hosting_comb.js"></script>
 * <script type="text/javascript">
 * (async () => {
 *     const child = await comb.connect( url );
 *
 *     await child.set("mode", mode );
 *
 *     let response = await child.run("signIn");
 * })();
 * </script>
 * ```
 *
 * Child frame
 * ```html
 * <script type="text/javascript" src="./holo_hosting_comb.js"></script>
 * <script type="text/javascript">
 * (async () => {
 *     const parent = comb.listen({
 *         "signIn": async function ( ...args ) {
 *             if ( this.mode === DEVELOP )
 *                 ...
 *             else
 *                 ...
 *             return response;
 *         },
 *     });
 * })();
 * </script>
 * ```
 *
 */
var COMB = {
    /**
     * Turn on debugging and set the logging level.  If 'debug' is not called, the default log level
     * is 'error'.
     *
     * @function debug
     *
     * @param {string} level		- Log level (default: "debug", options: "error", "warn", "info", "debug", "trace")
     *
     * @example
     * COMB.debug( "info" );
     */
    debug: function (level) {
        if (level === void 0) { level = 'debug'; }
        postmate_1["default"].debug = true;
        log.setLevel(level);
    },
    /**
     * Insert an iframe (pointing at the given URL) into the `document.body` and wait for COMB to
     * connect.
     *
     * @async
     * @function connect
     *
     * @param {string} url		- URL that is used as 'src' for the iframe
     *
     * @return {ChildAPI} Connection to child frame
     *
     * @example
     * const child = await COMB.connect( "http://localhost:8002" );
     */
    connect: function (url) {
        return __awaiter(this, void 0, void 0, function () {
            var child;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        child = new ChildAPI(url);
                        return [4 /*yield*/, child.connect()];
                    case 1:
                        _a.sent();
                        return [2 /*return*/, child];
                }
            });
        });
    },
    /**
     * Listen to 'postMessage' requests and wait for a parent window to connect.
     *
     * @async
     * @function listen
     *
     * @param {object} methods		- Functions that are available for the parent to call.
     *
     * @return {ParentAPI} Connection to parent window
     *
     * @example
     * const parent = await COMB.listen({
     *     "hello": async function () {
     *         return "Hello world";
     *     }
     * });
     */
    listen: function (methods) {
        return __awaiter(this, void 0, void 0, function () {
            var parent;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        parent = new ParentAPI(methods);
                        return [4 /*yield*/, parent.connect()];
                    case 1:
                        _a.sent();
                        return [2 /*return*/, parent];
                }
            });
        });
    }
};
exports.COMB = COMB;
var ChildAPI = /** @class */ (function () {
    /**
     * Initialize a child frame using the given URL.
     *
     * @class ChildAPI
     *
     * @param {string} url		- URL that is used as 'src' for the iframe
     *
     * @prop {string} url 		- iFrame URL
     * @prop {number} msg_count		- Incrementing message ID
     * @prop {object} responses		- Dictionary of request Promises waiting for their responses
     * @prop {object} msg_bus		- Postmate instance
     * @prop {promise} handshake	- Promise that is waiting for connection confirmation
     * @prop {string} class_name	- iFrame's unique class name
     * @prop {boolean} loaded		- Indicates if iFrame successfully loaded
     *
     * @example
     * const child = new ChildAPI( url );
     * await child.connect();
     *
     * await child.set("mode", mode );
     * let response = await child.run("signIn");
     */
    function ChildAPI(url) {
        var _this = this;
        this.url = url;
        this.msg_count = 0;
        this.responses = {};
        this.loaded = false;
        this.class_name = "comb-frame-" + ChildAPI.frame_count++;
        this.handshake = async_with_timeout_1["default"](function () { return __awaiter(_this, void 0, void 0, function () {
            var handshake, iframe;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        log.info("Init Postmate handshake");
                        handshake = new postmate_1["default"]({
                            "container": document.body,
                            "url": this.url,
                            "classListArray": [this.class_name]
                        });
                        iframe = document.querySelector('iframe.' + this.class_name);
                        log.debug("Listening for iFrame load event", iframe);
                        iframe['contentWindow'].addEventListener("domcontentloaded", function () {
                            log.debug("iFrame content has loaded");
                            _this.loaded = true;
                        });
                        return [4 /*yield*/, handshake];
                    case 1: return [2 /*return*/, _a.sent()];
                }
            });
        }); }, 1000);
    }
    /**
     * Wait for handshake to complete and then attach response listener.
     *
     * @async
     *
     * @return {this}
     *
     * @example
     * const child = new ChildAPI( url );
     * await child.connect();
     */
    ChildAPI.prototype.connect = function () {
        return __awaiter(this, void 0, void 0, function () {
            var child, err_1;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        _a.trys.push([0, 2, , 3]);
                        return [4 /*yield*/, this.handshake];
                    case 1:
                        child = _a.sent();
                        return [3 /*break*/, 3];
                    case 2:
                        err_1 = _a.sent();
                        if (err_1.name === "TimeoutError") {
                            if (this.loaded) {
                                log.error("iFrame loaded but could not communicate with COMB");
                                throw new async_with_timeout_2.TimeoutError("Failed to complete COMB handshake", err_1.timeout);
                            }
                            else {
                                log.error("iFrame did not trigger load event");
                                throw new async_with_timeout_2.TimeoutError("Failed to load iFrame", err_1.timeout);
                            }
                        }
                        else
                            throw err_1;
                        return [3 /*break*/, 3];
                    case 3:
                        log.info("Finished handshake");
                        child.on('response', function (data) {
                            var k = data[0], v = data[1];
                            log.info("Received response for msg_id:", k);
                            var _a = _this.responses[k], f = _a[0], r = _a[1];
                            if (v instanceof Error)
                                r(v);
                            else
                                f(v);
                            delete _this.responses[k];
                        });
                        this.msg_bus = child;
                        return [2 /*return*/, this];
                }
            });
        });
    };
    /**
     * Internal method that wraps requests in a timeout.
     *
     * @async
     * @private
     *
     * @param {string} method		- Internally consistent Postmate method
     * @param {string} name		- Function name or property name
     * @param {*} data			- Variable input that is handled by child API
     *
     * @return {*} Response from child
     */
    ChildAPI.prototype.request = function (method, name, data) {
        var _this = this;
        var msg_id = this.msg_count++;
        this.msg_bus.call(method, [msg_id, name, data]);
        log.info("Sent request with msg_id:", msg_id);
        return async_with_timeout_1["default"](function () { return __awaiter(_this, void 0, void 0, function () {
            var request;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        request = new Promise(function (f, r) {
                            _this.responses[msg_id] = [f, r];
                        });
                        return [4 /*yield*/, request];
                    case 1: return [2 /*return*/, _a.sent()];
                }
            });
        }); }, 1000);
    };
    /**
     * Set a property on the child instance and wait for the confirmation. Properties set that way
     * can be accessed as properties of `this` in the functions passed via listen() to the parentAPI.
     *
     * Essentially, it is a shortcut to remember some state instead of having to write a method to
     * remember some state.  Example `child.set("development_mode", true)` vs
     * `child.call("setDevelopmentMode", true)`.  The latter requires you to define
     * `setDevelopmentMode` on the child model where the former does not require any
     * pre-configuration.
     *
     * @async
     *
     * @param {string} key		- Property name
     * @param {*} value			- Property value
     *
     * @return {boolean} Success status
     *
     * @example
     * let success = await child.set( "key", "value" );
     */
    ChildAPI.prototype.set = function (key, value) {
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.request("prop", key, value)];
                    case 1: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    /**
     * Call an exposed function on the child instance and wait for the response.
     *
     * @async
     *
     * @param {string} method		- Name of exposed function to call
     * @param {...*} args		- Arguments that are passed to function
     *
     * @return {*}
     *
     * @example
     * let response = await child.run( "some_method", "argument 1", 2, 3 );
     */
    ChildAPI.prototype.run = function (method) {
        var args = [];
        for (var _i = 1; _i < arguments.length; _i++) {
            args[_i - 1] = arguments[_i];
        }
        return __awaiter(this, void 0, void 0, function () {
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0: return [4 /*yield*/, this.request("exec", method, args)];
                    case 1: return [2 /*return*/, _a.sent()];
                }
            });
        });
    };
    ChildAPI.frame_count = 0;
    return ChildAPI;
}());
var ParentAPI = /** @class */ (function () {
    /**
     * Initialize a listening instance and set available methods.
     *
     * @class ParentAPI
     *
     * @param {object} methods		- Functions that are available for the parent to call.
     * @param {object} properties	- Properties to memorize in the instance for later use, optional
     *
     * @prop {promise} listener		- Promise that is waiting for parent to connect
     * @prop {object} msg_bus		- Postmate instance
     * @prop {object} methods		- Method storage
     * @prop {object} properties	- Set properties storage
     *
     * @example
     * const parent = new ParentAPI({
     *     "hello": async function () {
     *         return "Hello world";
     *     }
     * });
     * await parent.connect();
     */
    function ParentAPI(methods, properties) {
        var _this = this;
        if (properties === void 0) { properties = {}; }
        this.methods = methods;
        this.properties = properties;
        this.listener = new postmate_1["default"].Model({
            "exec": function (data) { return __awaiter(_this, void 0, void 0, function () {
                var msg_id, method, args, fn, resp;
                return __generator(this, function (_a) {
                    switch (_a.label) {
                        case 0:
                            msg_id = data[0], method = data[1], args = data[2];
                            fn = this.methods[method];
                            if (fn === undefined) {
                                log.error("Method does not exist", method);
                                return [2 /*return*/, this.msg_bus.emit("response", [msg_id, new Error("Method '" + method + "' does not exist")])];
                            }
                            if (typeof fn !== "function") {
                                log.error("Method is not a function: type", typeof fn);
                                return [2 /*return*/, this.msg_bus.emit("response", [msg_id, new Error("Method '" + method + "' is not a function. Found type '" + typeof fn + "'")])];
                            }
                            return [4 /*yield*/, fn.apply(this.properties, args)];
                        case 1:
                            resp = _a.sent();
                            this.msg_bus.emit("response", [msg_id, resp]);
                            return [2 /*return*/];
                    }
                });
            }); },
            "prop": function (data) { return __awaiter(_this, void 0, void 0, function () {
                var msg_id, key, value;
                return __generator(this, function (_a) {
                    msg_id = data[0], key = data[1], value = data[2];
                    this.properties[key] = value;
                    this.msg_bus.emit("response", [msg_id, true]);
                    return [2 /*return*/];
                });
            }); }
        });
    }
    /**
     * Wait for parent to connect.
     *
     * @async
     *
     * @return {this}
     *
     * @example
     * const parent = new ParentAPI({
     *     "hello": async function () {
     *         return "Hello world";
     *     }
     * });
     * await parent.connect();
     */
    ParentAPI.prototype.connect = function () {
        return __awaiter(this, void 0, void 0, function () {
            var _a;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = this;
                        return [4 /*yield*/, this.listener];
                    case 1:
                        _a.msg_bus = _b.sent();
                        return [2 /*return*/, this];
                }
            });
        });
    };
    return ParentAPI;
}());