"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;
}());