UNPKG

chronosjs

Version:

JS Channels Mechanism

1,326 lines (1,170 loc) 106 kB
;(function (root, chronosRoot, factory) { "use strict"; /* istanbul ignore if */ //<amd> if ("function" === typeof define && define.amd) { // AMD. Register as an anonymous module. define("Chronos.PostMessageUtilities", [], function () { return factory(root, chronosRoot, true); }); return; } //</amd> /* istanbul ignore next */ if ("object" !== typeof exports) { chronosRoot.Chronos = chronosRoot.Chronos || {}; factory(root, chronosRoot.Chronos); } }(this, typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, hide) { "use strict"; var SEQUENCE_FORMAT = "_xxxxxx-4xxx-yxxx"; /** * This function was added because of incompatibility between the JSON.stringify and Prototype.js library * When a customer uses Prototype.js library, It overrides the Array.prototype.toJSON function of the native JSON * uses. This causes arrays to be double quoted and Shark to fail on those SDEs. * The function accepts a value and uses the native JSON.stringify * Can throw an exception (same as JSON.stringify). * @returns {String} the strigified object */ function stringify() { var stringified; var toJSONPrototype; if ("function" === typeof Array.prototype.toJSON) { toJSONPrototype = Array.prototype.toJSON; Array.prototype.toJSON = void 0; try { stringified = JSON.stringify.apply(null, arguments); } catch (ex) { /* istanbul ignore next */ Array.prototype.toJSON = toJSONPrototype; /* istanbul ignore next */ throw ex; } Array.prototype.toJSON = toJSONPrototype; } else { stringified = JSON.stringify.apply(null, arguments); } return stringified; } /** * Method to identify whether the browser supports passing object references to postMessage API * @returns {Boolean} whether the browser supports passing object references to postMessage API */ function hasPostMessageObjectsSupport() { var hasObjectsSupport = true; try { root.postMessage({ toString:function() { hasObjectsSupport = false; } }, "*"); } catch (ex) { // Browsers which has postMessage Objects support sends messages using // the structured clone algorithm - https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm // In which Error and Function objects cannot be duplicated by the structured clone algorithm; attempting to do so will throw a DATA_CLONE_ERR exception. if (ex && 'DataCloneError' !== ex.name) { hasObjectsSupport = false; } } return hasObjectsSupport; } /** * Method to create a unique sequence * @param {String} format - the format for the unique name eg. xxxxx-xx4xxx-yxxxx * @returns {String} the unique iFrame name */ function createUniqueSequence(format) { return format && format.replace(/[xy]/g, function(chr) { var rnd = Math.random() * 16 | 0; var val = chr === "x" ? rnd : (rnd & 0x3 | 0x8); return val.toString(16); }); } /** * Method to validate and parse an input number * @param {Number} input - the input value to parse * @param {Number} defaultValue - the default value to return in case of invalid input * @returns {Number} the number to return */ function parseNumber(input, defaultValue) { return !isNaN(input) && 0 < input ? parseInt(input, 10) : defaultValue; } /** * Method to validate and parse a function reference * @param {Function} input - the input value to parse * @param {Function|Boolean} defaultValue - the default value to return in case of invalid input or true for empty function * @returns {Function} the function to return */ function parseFunction(input, defaultValue) { return (("function" === typeof input) ? input : (true === defaultValue ? function() {} : defaultValue)); } /** * Function to extract the host domain from any URL * @param {String} url - the url to resolve the host for * @param {Object} [win] - the window to resolve the host for * @param {Boolean} [top] - boolean indication for using helper of the top window if needed * @returns {String} the host */ function getHost(url, win, top) { var domainRegEx = new RegExp(/(http{1}s{0,1}?:\/\/)([^\/\?]+)(\/?)/ig); var matches; var domain; var frame; if (url && 0 === url.indexOf("http")) { matches = domainRegEx.exec(url); } else { // This is a partial url so we assume it's relative, this is mainly nice for tests frame = top ? (win.top || (win.contentWindow && win.contentWindow.parent) || window) : win; return frame.location.protocol + "//" + frame.location.host; } if (matches && 3 <= matches.length && "" !== matches[2]) { domain = matches[1].toLowerCase() + matches[2].toLowerCase(); // 0 - full match 1- HTTPS 2- domain } return domain; } /** * Method to resolve the needed origin parameters from url * @param {String} [hostParam] - string to represent the name of the host parameter in querystring * @param {String} [url] - string to represent the url to resolve parameters from * @returns {String} the parameter from the url */ function resolveParameters(hostParam, url) { var param; var value = getURLParameter("lpHost", url); if (!value) { param = getURLParameter("hostParam", url) || hostParam; if (param) { value = getURLParameter(param, url); } } return value; } /** * Method to resolve the needed origin * @param {Object} [target] - the target to resolve the host for * @param {Boolean} [top] - boolean indication for using helper of the top window if needed * @param {String} [hostParam] - string to represent the name of the host parameter in querystring * @returns {String} the origin for the target */ function resolveOrigin(target, top, hostParam) { var origin; var url; var ref; try { url = target && target.contentWindow && "undefined" !== typeof Window && !(target instanceof Window) && target.getAttribute && target.getAttribute("src"); } catch(ex) {} try { if (!url) { url = resolveParameters(hostParam); } if (!url) { url = document.referrer; ref = true; } if (url) { url = decodeURIComponent(url); if (ref) { url = resolveParameters(hostParam, url); } } origin = getHost(url, target, top); } catch(ex) { log("Cannot parse origin", "ERROR", "PostMessageUtilities"); } return origin || "*"; } /** * Method to retrieve a url parameter from querystring by name * @param {String} name - the name of the parameter * @param {String} [url] - optional url to parse * @returns {String} the url parameter value */ function getURLParameter(name, url) { return decodeURIComponent((new RegExp("[?|&]" + name + "=" + "([^&;]+?)(&|#|;|$)").exec(url || document.location.search) || [void 0, ""])[1].replace(/\+/g, "%20")) || null; } /** * Method to delay a message execution (async) * @param {Function} method - the function to delay * @param {Number} [milliseconds] - optional milliseconds to delay or false to run immediately */ function delay(method, milliseconds) { var timer; /* istanbul ignore if */ if ("undefined" !== typeof setImmediate && (isNaN(milliseconds) || 0 >= milliseconds)) { timer = setImmediate(method); } else if (false === milliseconds) { method(); } else { timer = setTimeout(method, (isNaN(milliseconds) || 0 >= milliseconds) ? 0 : parseInt(milliseconds, 10)); } return function() { clearDelay(timer); }; } /** * Method to clear the delay of a message execution (async) * @param {Number} id - the id of the timer to clear */ function clearDelay(timer) { var timerId = parseNumber(timer); if (timerId) { /* istanbul ignore if */ if ("undefined" !== typeof clearImmediate) { clearImmediate(timerId); } else { clearTimeout(timerId); } } } /** * Method to add DOM events listener to an element * @param {Object} element - the element we're binding to * @param {String} event - the event we want to bind * @param {Function} callback - the function to execute */ function addEventListener(element, event, callback) { /* istanbul ignore else: IE9- only */ if (element.addEventListener) { element.addEventListener(event, callback, false); } else { element.attachEvent("on" + event, callback); } return function() { removeEventListener(element, event, callback); }; } /** * Method to add DOM events listener to an element * @param {Object} element - the element we're binding to * @param {String} event - the event we want to bind * @param {Function} callback - the function to execute */ function removeEventListener(element, event, callback) { /* istanbul ignore else: IE9- only */ if (element.removeEventListener) { element.removeEventListener(event, callback, false); } else { element.detachEvent("on" + event, callback); } } /** * Method to implement a simple logging based on lptag * @param {String} msg - the message to log * @param {String} level - the logging level of the message * @param {String} app - the app which logs */ /* istanbul ignore next */ function log(msg, level, app) { if (root && "function" === typeof root.log) { root.log(msg, level, app); } } /** * Method to polyfill bind native functionality in case it does not exist * Based on implementation from: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind * @param {Object} object - the object to bind to * @returns {Function} the bound function */ /* istanbul ignore next */ function bind(object) { /*jshint validthis:true */ var args; var fn; if ("function" !== typeof this) { // Closest thing possible to the ECMAScript 5 // Internal IsCallable function throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); } args = Array.prototype.slice.call(arguments, 1); fn = this; function Empty() {} function bound() { return fn.apply(this instanceof Empty && object ? this : object, args.concat(Array.prototype.slice.call(arguments))); } Empty.prototype = this.prototype; bound.prototype = new Empty(); return bound; } /* istanbul ignore if */ if (!Function.prototype.bind) { Function.prototype.bind = bind; } // attach properties to the exports object to define // the exported module properties. var ret = { SEQUENCE_FORMAT: SEQUENCE_FORMAT, stringify: stringify, hasPostMessageObjectsSupport: hasPostMessageObjectsSupport, createUniqueSequence: createUniqueSequence, parseNumber: parseNumber, parseFunction: parseFunction, getHost: getHost, resolveOrigin: resolveOrigin, getURLParameter: getURLParameter, delay: delay, addEventListener: addEventListener, removeEventListener: removeEventListener, log: log, bind: bind }; if (!hide) { exports.PostMessageUtilities = exports.PostMessageUtilities || ret; } return ret; })); ;(function (root, chronosRoot, factory) { "use strict"; /* istanbul ignore if */ //<amd> if ("function" === typeof define && define.amd) { // AMD. Register as an anonymous module. define("Chronos.PostMessagePromise", ["exports"], function () { return factory(root, chronosRoot, true); }); return; } //</amd> /* istanbul ignore next */ if ("object" !== typeof exports) { chronosRoot.Chronos = chronosRoot.Chronos || {}; factory(root, chronosRoot.Chronos); } }(this, typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, hide) { "use strict"; /*jshint validthis:true */ var ACTION_TYPE = { RESOLVE: "resolve", REJECT: "reject", PROGRESS: "progress" }; /** * PostMessagePromise constructor * @constructor * @param {Function} [executer] - optional method to be invoked during initialization that will have * arguments of resolve and reject according to ES6 Promise A+ spec */ function PostMessagePromise(executer) { // For forcing new keyword /* istanbul ignore if */ if (false === (this instanceof PostMessagePromise)) { return new PostMessagePromise(executer); } this.initialize(executer); } PostMessagePromise.prototype = (function () { /** * Method for initialization * @param {Function} [executor] - optional method to be invoked during initialization that will have * arguments of resolve and reject according to ES6 Promise A+ spec * */ function initialize(executor) { if (!this.initialized) { this.queue = []; this.actions = { resolve: resolve.bind(this), reject: reject.bind(this), progress: progress.bind(this) }; // Option to pass executor method if ("function" === typeof executor) { executor.call(this, this.actions.resolve, this.actions.reject); } this.initialized = true; } } /** * Method for assigning a defer execution * Code waiting for this promise uses this method * @param {Function} onresolve - the resolve callback * @param {Function} onreject - the reject callback * @param {Function} onprogress - the onprogress handler */ function then(onresolve, onreject, onprogress) { // Queue the calls to then() this.queue.push({ resolve: onresolve, reject: onreject, progress: onprogress }); } /** * Method for resolving the promise * @param {Object} [data] - the data to pass the resolve callback */ function resolve(data) { _complete.call(this, ACTION_TYPE.RESOLVE, data); } /** * Method for rejecting the promise * @param {Object} [data] - the data to pass the resolve callback */ function reject(data) { _complete.call(this, ACTION_TYPE.REJECT, data); } /** * Method for calling the progress handler * @param {Object} [status] - the status to pass the progress handler */ function progress(status) { _completeQueue.call(this, ACTION_TYPE.PROGRESS, status); } /** * Method for calling all queued handlers with a specified type to complete the queue * @param {PostMessagePromise.ACTION_TYPE} type - the type of handlers to invoke * @param {Object} [arg] - the arg to pass the handler handler * @param {Boolean} empty - a flag to indicate whether the queue should be empty after completion * @private */ function _completeQueue(type, arg, empty) { var i; var item; if (this.queue && this.queue.length) { i = 0; item = this.queue[i++]; while (item) { if (item[type]) { item[type].call(this, arg); } item = this.queue[i++]; } if (empty) { // Clear this.queue.length = 0; } } } /** * Method for completing the promise (resolve/reject) * @param {PostMessagePromise.ACTION_TYPE} type - resolve/reject * @param {Object} [arg] - the data to pass the handler * @private */ function _complete(type, arg) { // Sync/Override then() var action = this.actions[type]; // Override then to invoke the needed action this.then = function (resolve, reject) { if (action) { action.call(this, arg); } }.bind(this); // Block multiple calls to resolve or reject by overriding this.resolve = this.reject = function () { throw new Error("This Promise instance had already been completed."); }; // Block progress by overriding with false result this.progress = function () { return false; }; // Complete all waiting (async) queue _completeQueue.call(this, type, arg, true); // Clean if (this.queue) { this.queue.length = 0; delete this.queue; } } return { initialize: initialize, then: then, resolve: resolve, reject: reject, progress: progress }; }()); /** * Method for polyfilling Promise support if not exist */ /* istanbul ignore next */ PostMessagePromise.polyfill = function() { if (!root.Promise) { root.Promise = PostMessagePromise; } }; // attach properties to the exports object to define // the exported module properties. if (!hide) { exports.PostMessagePromise = exports.PostMessagePromise || PostMessagePromise; } return PostMessagePromise; })); ;(function (root, factory) { "use strict"; /* istanbul ignore if */ //<amd> if ("function" === typeof define && define.amd) { // AMD. Register as an anonymous module. define("Chronos.PostMessageMapper", ["Chronos.PostMessageUtilities"], function (PostMessageUtilities) { return factory(root, root, PostMessageUtilities, true); }); return; } //</amd> /* istanbul ignore next */ if ("object" !== typeof exports) { /** * @depend ./PostMessageUtilities.js */ root.Chronos = root.Chronos || {}; factory(root, root.Chronos, root.Chronos.PostMessageUtilities); } }(typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, PostMessageUtilities, hide) { "use strict"; /*jshint validthis:true */ /** * PostMessageMapper constructor * @constructor * @param {Channels} [eventChannel] - the event channel on which events/commands/requests will be bind/triggered (must implement the Channels API) */ function PostMessageMapper(eventChannel) { // For forcing new keyword /* istanbul ignore if */ if (false === (this instanceof PostMessageMapper)) { return new PostMessageMapper(eventChannel); } this.initialize(eventChannel); } PostMessageMapper.prototype = (function () { /** * Method for initialization * @param {Channels} [eventChannel] - the event channel on which events/commands/requests will be bind/triggered (must implement the Channels API) */ function initialize(eventChannel) { if (!this.initialized) { this.eventChannel = eventChannel; this.initialized = true; } } /** * Method mapping the message to the correct event on the event channel and invoking it * @param {Object} message - the message to be mapped * @returns {Function} the handler function to invoke on the event channel */ function toEvent(message) { if (message) { if (message.error) { PostMessageUtilities.log("Error on message: " + message.error, "ERROR", "PostMessageMapper"); return function() { return message; }; } else { return _getMappedMethod.call(this, message); } } } /** * Method mapping the method call on the event aggregator to a message which can be posted * @param {String} id - the id for the call * @param {String} name - the name of the method * optional additional method arguments * @returns {Object} the mapped method */ function toMessage(id, name) { return { method: { id: id, name: name, args: Array.prototype.slice.call(arguments, 2) } }; } /** * Method getting the mapped method on the event channel after which it can be invoked * @param {Object} message - the message to be mapped * optional additional method arguments * @return {Function} the function to invoke on the event channel * @private */ function _getMappedMethod(message) { var method = message && message.method; var name = method && method.name; var args = method && method.args; var eventChannel = this.eventChannel; return function() { if (eventChannel && eventChannel[name]) { return eventChannel[name].apply(eventChannel, args); } else { /* istanbul ignore next */ PostMessageUtilities.log("No channel exists", "ERROR", "PostMessageMapper"); } }; } return { initialize: initialize, toEvent: toEvent, toMessage: toMessage }; }()); // attach properties to the exports object to define // the exported module properties. if (!hide) { exports.PostMessageMapper = exports.PostMessageMapper || PostMessageMapper; } return PostMessageMapper; })); ;(function (root, chronosRoot, factory) { "use strict"; /* istanbul ignore if */ //<amd> if ("function" === typeof define && define.amd) { // AMD. Register as an anonymous module. define("Chronos.PostMessageChannelPolyfill", ["Chronos.PostMessageUtilities"], function (PostMessageUtilities) { return factory(root, chronosRoot, PostMessageUtilities, true); }); return; } //</amd> /* istanbul ignore next */ if ("object" !== typeof exports) { /** * @depend ./PostMessageUtilities.js */ chronosRoot.Chronos = chronosRoot.Chronos || {}; factory(root, chronosRoot.Chronos, chronosRoot.Chronos.PostMessageUtilities); } }(this, typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, PostMessageUtilities, hide) { "use strict"; /*jshint validthis:true */ var PORT_PREFIX = "LPPort_"; /** * PostMessageChannelPolyfill constructor * @constructor * @param {Object} target - The DOM node of the target iframe or window * @param {Object} [options] the configuration options for the instance * @param {Function} [options.serialize = JSON.stringify] - optional serialization method for post message * @param {Function} [options.deserialize = JSON.parse] - optional deserialization method for post message */ function PostMessageChannelPolyfill(target, options) { /* istanbul ignore if */ // For forcing new keyword if (false === (this instanceof PostMessageChannelPolyfill)) { return new PostMessageChannelPolyfill(target, options); } this.initialize(target, options); } PostMessageChannelPolyfill.prototype = (function () { /** * Method for initialization * @param {Object} target - The DOM node of the target iframe or window * @param {Object} [options] the configuration options for the instance * @param {Function} [options.serialize = JSON.stringify] - optional serialization method for post message * @param {Function} [options.deserialize = JSON.parse] - optional deserialization method for post message */ function initialize(target, options) { if (!this.initialized) { options = options || {}; this.target = target || root.top; this.hosted = this.target === root || this.target === root.top; this.portId = PostMessageUtilities.createUniqueSequence(PORT_PREFIX + PostMessageUtilities.SEQUENCE_FORMAT); this.serialize = PostMessageUtilities.parseFunction(options.serialize, PostMessageUtilities.stringify); this.deserialize = PostMessageUtilities.parseFunction(options.deserialize, JSON.parse); this.initialized = true; } } /** * Method for posting the message to the target * @param {Object} message - the message to be post */ function postMessage(message) { var wrapped; var parsed; var origin = _getOrigin.call(this); var receiver = this.target; if (message) { try { if (!this.hosted) { receiver = this.target.contentWindow; } wrapped = _wrapMessage.call(this, message); parsed = this.serialize(wrapped); receiver.postMessage(parsed, origin); } catch(ex) { /* istanbul ignore next */ PostMessageUtilities.log("Error while trying to post the message", "ERROR", "PostMessageChannelPolyfill"); /* istanbul ignore next */ return false; } } } /** * Method for receiving the incoming message * @param {Object} event - the event object on message */ function receive(event) { var message; if ("function" === typeof this.onmessage) { message = _unwrapMessage.call(this, event); return this.onmessage(message); } } /** * Method for getting the origin to be used * @private */ function _getOrigin() { if (!this.origin) { this.origin = PostMessageUtilities.resolveOrigin(this.target); } return this.origin; } /** * Method for wrapping the outgoing message with port and id * @param {Object} message - the message to be wrapped * @returns {Object} the wrapped message * @private */ function _wrapMessage(message) { return { port: this.portId, message: message }; } /** * Method for unwrapping the incoming message from port and id * @param {Object} event - the event object on message * @returns {Object} the unwrapped message * @private */ function _unwrapMessage(event) { var msgObject; if (event && event.data) { try { msgObject = this.deserialize(event.data); if (msgObject.port && 0 === msgObject.port.indexOf(PORT_PREFIX)) { return { origin: event.origin, data: msgObject.message }; } } catch (ex) { /* istanbul ignore next */ PostMessageUtilities.log("Error while trying to deserialize the message", "ERROR", "PostMessageChannelPolyfill"); } } return msgObject || event; } return { initialize: initialize, postMessage: postMessage, receive: receive }; }()); // attach properties to the exports object to define // the exported module properties. if (!hide) { exports.PostMessageChannelPolyfill = exports.PostMessageChannelPolyfill || PostMessageChannelPolyfill; } return PostMessageChannelPolyfill; })); ;(function (root, chronosRoot, factory) { "use strict"; /* istanbul ignore if */ //<amd> if ("function" === typeof define && define.amd) { // AMD. Register as an anonymous module. define("Chronos.PostMessageChannel", ["Chronos.PostMessageUtilities", "Chronos.PostMessageChannelPolyfill"], function (PostMessageUtilities, PostMessageChannelPolyfill) { return factory(root, chronosRoot, PostMessageUtilities, PostMessageChannelPolyfill, true); }); return; } //</amd> /* istanbul ignore next */ if ("object" !== typeof exports) { /** * @depend ./PostMessageUtilities.js * @depend ./PostMessageChannelPolyfill.js */ chronosRoot.Chronos = chronosRoot.Chronos || {}; factory(root, chronosRoot.Chronos, chronosRoot.Chronos.PostMessageUtilities, chronosRoot.Chronos.PostMessageChannelPolyfill); } }(this, typeof ChronosRoot === "undefined" ? this : ChronosRoot, function (root, exports, PostMessageUtilities, PostMessageChannelPolyfill, hide) { "use strict"; /*jshint validthis:true */ var IFRAME_PREFIX = "LPFRM"; var TOKEN_PREFIX = "LPTKN"; var HANSHAKE_PREFIX = "HNDSK"; var DEFAULT_CONCURRENCY = 100; var DEFAULT_HANDSHAKE_RETRY_INTERVAL = 5000; var DEFAULT_HANDSHAKE_RETRY_ATTEMPTS = 3; var DEFAULT_BODY_LOAD_DELAY = 100; /** * PostMessageChannel constructor * @constructor * @param {Object} options the configuration options for the instance * @param {Object} options.target - the target iframe or iframe configuration * @param {String} [options.target.url] - the url to load * @param {Object} [options.target.container] - the container in which the iframe should be created (if not supplied, document.body will be used) * @param {String} [options.target.style] - the CSS style to apply * @param {String} [options.target.style.width] width of iframe * @param {String} [options.target.style.height] height of iframe * ..... * @param {Boolean} [options.target.bust = true] - optional flag to indicate usage of cache buster when loading the iframe (default to true) * @param {Function} [options.target.callback] - a callback to invoke after the iframe had been loaded, * @param {Object} [options.target.context] - optional context for the callback * @param {Function|Object} [options.onready] - optional data for usage when iframe had been loaded { * @param {Function} [options.onready.callback] - a callback to invoke after the iframe had been loaded * @param {Object} [options.onready.context] - optional context for the callback * @param {Boolean} [options.removeDispose] - optional flag for removal of the iframe on dispose * @param {Function} [options.serialize = JSON.stringify] - optional serialization method for post message * @param {Function} [options.deserialize = JSON.parse] - optional deserialization method for post message * @param {String} [options.targetOrigin] optional targetOrigin to be used when posting the message (must be supplied in case of external iframe) * @param {Number} [options.maxConcurrency = 100] - optional maximum concurrency that can be managed by the component before dropping * @param {Number} [options.handshakeInterval = 5000] - optional handshake interval for retries * @param {Number} [options.handshakeAttempts = 3] - optional number of retries handshake attempts * @param {String} [options.hostParam] - optional parameter of the host parameter name (default is lpHost) * @param {Function} onmessage - the handler for incoming messages */ function PostMessageChannel(options, onmessage) { /* istanbul ignore if */ // For forcing new keyword if (false === (this instanceof PostMessageChannel)) { return new PostMessageChannel(options, onmessage); } this.initialize(options, onmessage); } PostMessageChannel.prototype = (function () { /** * Method for initialization * @param {Object} options the configuration options for the instance * @param {Object} options.target - the target iframe or iframe configuration * @param {String} [options.target.url] - the url to load * @param {Object} [options.target.container] - the container in which the iframe should be created (if not supplied, document.body will be used) * @param {String} [options.target.style] - the CSS style to apply * @param {String} [options.target.style.width] width of iframe * @param {String} [options.target.style.height] height of iframe * ..... * @param {Boolean} [options.target.bust = true] - optional flag to indicate usage of cache buster when loading the iframe (default to true) * @param {Function} [options.target.callback] - a callback to invoke after the iframe had been loaded, * @param {Object} [options.target.context] - optional context for the callback * @param {Function|Object} [options.onready] - optional data for usage when iframe had been loaded { * @param {Function} [options.onready.callback] - a callback to invoke after the iframe had been loaded * @param {Object} [options.onready.context] - optional context for the callback * @param {Boolean} [options.removeDispose] - optional flag for removal of the iframe on dispose * @param {Function} [options.serialize = JSON.stringify] - optional serialization method for post message * @param {Function} [options.deserialize = JSON.parse] - optional deserialization method for post message * @param {String} [options.targetOrigin] optional targetOrigin to be used when posting the message (must be supplied in case of external iframe) * @param {Number} [options.maxConcurrency = 100] - optional maximum concurrency that can be managed by the component before dropping * @param {Number} [options.handshakeInterval = 5000] - optional handshake interval for retries * @param {Number} [options.handshakeAttempts = 3] - optional number of retries handshake attempts * @param {String} [options.hostParam] - optional parameter of the host parameter name (default is lpHost) * @param {Function} onmessage - the handler for incoming messages */ function initialize(options, onmessage) { var handleMessage; var handler; if (!this.initialized) { this.hosted = false; this.messageQueue = []; options = options || {}; handler = _initParameters.call(this, options, onmessage); if (!_isNativeMessageChannelSupported.call(this)) { this.receiver = new PostMessageChannelPolyfill(this.target, { serialize: this.serialize, deserialize: this.deserialize }); this.receiver.onmessage = handler; } if (this.hosted || !_isNativeMessageChannelSupported.call(this)) { handleMessage = _getHandleMessage(handler).bind(this); this.removeListener = PostMessageUtilities.addEventListener(root, "message", handleMessage); } else if (_isNativeMessageChannelSupported.call(this)) { this.channelFactory(); } if (this.target && !this.loading && !this.ready) { _kickStartHandshake.call(this, handler, handleMessage); } this.initialized = true; } } /** * Method for removing the handler * @param {String} name - a name of the reference which holds the remove handler on this context, * @param {Boolean} ignore - optional flag to indicate whether to ignore the execution of the remove handler * */ function _removeHandler(name, ignore) { // Remove handler if needed var func = PostMessageUtilities.parseFunction(this[name]); if (func) { if (!ignore) { func.call(this); } this[name] = void 0; delete this[name]; } } /** * Method for removing the timer */ function _removeTimer(ignore) { // Remove timer if needed _removeHandler.call(this, "rmtimer", ignore); } /** * Method for removing the timer */ function _removeLoadedHandler(ignore) { // Remove load handler if needed _removeHandler.call(this, "rmload", ignore); } /** * Method for disposing the object */ function dispose() { if (!this.disposed) { if (this.removeListener) { this.removeListener.call(this); this.removeListener = void 0; } if (this.targetUrl && this.target || this.removeDispose) { try { if (this.targetContainer) { this.targetContainer.removeChild(this.target); } else { document.body.removeChild(this.target); } } catch(ex) { /* istanbul ignore next */ PostMessageUtilities.log("Error while trying to remove the iframe from the container", "ERROR", "PostMessageChannel"); } } // Remove load handler if needed _removeTimer.call(this); // Remove timer if needed _removeLoadedHandler.call(this); this.messageQueue.length = 0; this.messageQueue = void 0; this.channel = void 0; this.onready = void 0; this.disposed = true; } } /** * Method to post the message to the target * @param {Object} message - the message to post * @param {Object} [target] - optional target for post * @param {Boolean} [force = false] - force post even if not ready */ function postMessage(message, target, force) { var consumer; var parsed; if (!this.disposed) { try { if (message) { if (this.ready || force) { // Post the message consumer = target || this.receiver; parsed = _prepareMessage.call(this, message); consumer.postMessage(parsed); return true; } else if (this.maxConcurrency >= this.messageQueue.length) { // Need to delay/queue messages till target is ready this.messageQueue.push(message); return true; } else { return false; } } } catch(ex) { /* istanbul ignore next */ PostMessageUtilities.log("Error while trying to post the message", "ERROR", "PostMessageChannel"); return false; } } } function _kickStartHandshake(handler, handleMessage) { var initiated; try { initiated = _handshake.call(this); } catch (ex) { initiated = false; } if (!initiated) { // Fallback to pure postMessage this.channel = false; this.receiver = new PostMessageChannelPolyfill(this.target, { serialize: this.serialize, deserialize: this.deserialize }); this.receiver.onmessage = handler; if (!this.hosted) { handleMessage = _getHandleMessage(handler).bind(this); this.removeListener = PostMessageUtilities.addEventListener(root, "message", handleMessage); } _handshake.call(this); } this.handshakeAttempts--; PostMessageUtilities.delay(function () { if (!this.disposed && !this.hosted && !this.ready) { this.rmload = _addLoadHandler.call(this, this.target); this.rmtimer = PostMessageUtilities.delay(_handshake.bind(this, this.handshakeInterval), this.handshakeInterval); } }.bind(this)); } function _initParameters(options, onmessage) { var handler; _simpleParametersInit.call(this, options); handler = _wrapMessageHandler(onmessage).bind(this); this.channelFactory = _hookupMessageChannel.call(this, handler); // No Iframe - We are inside it (hosted) initialized by the host/container if (!options.target || (options.target !== root || options.target === root.top) && "undefined" !== typeof Window && options.target instanceof Window) { this.hosted = true; this.target = options.target || root.top; } else if (options.target.contentWindow) { // We've got a reference to an "external" iframe this.target = options.target; } else if (options.target.url) { // We've got the needed configuration for creating an iframe this.targetUrl = options.target.url; this.targetOrigin = this.targetOrigin || PostMessageUtilities.getHost(options.target.url); } if (!this.hosted) { this.token = PostMessageUtilities.createUniqueSequence(TOKEN_PREFIX + PostMessageUtilities.SEQUENCE_FORMAT); } if (this.targetUrl) { // We've got the needed configuration for creating an iframe this.loading = true; this.targetContainer = options.target.container || document.body; this.target = _createIFrame.call(this, options.target, this.targetContainer); } return handler; } function _simpleParametersInit(options) { this.serialize = PostMessageUtilities.parseFunction(options.serialize, PostMessageUtilities.stringify); this.deserialize = PostMessageUtilities.parseFunction(options.deserialize, JSON.parse); this.targetOrigin = options.targetOrigin; this.maxConcurrency = PostMessageUtilities.parseNumber(options.maxConcurrency, DEFAULT_CONCURRENCY); this.handshakeInterval = PostMessageUtilities.parseNumber(options.handshakeInterval, DEFAULT_HANDSHAKE_RETRY_INTERVAL); this.handshakeAttemptsOrig = PostMessageUtilities.parseNumber(options.handshakeAttempts, DEFAULT_HANDSHAKE_RETRY_ATTEMPTS); this.handshakeAttempts = this.handshakeAttemptsOrig; this.hostParam = options.hostParam; this.channel = "undefined" !== typeof options.channel ? options.channel : _getChannelUrlIndicator(); this.useObjects = options.useObjects; this.onready = _wrapReadyCallback(options.onready, options.target).bind(this); this.removeDispose = options.removeDispose; } /** * Method for handling the initial handler binding for needed event listeners * @param {Object} handler - the event object on message */ function _getHandleMessage(handler) { return function(event) { var handshake; var previous; if (event.ports && 0 < event.ports.length) { this.receiver = event.ports[0]; if (_isHandshake.call(this, event)) { if (!this.token) { this.token = event.data; } } this.receiver.start(); // Swap Listeners previous = this.removeListener.bind(this); this.removeListener = PostMessageUtilities.addEventListener(this.receiver, "message", handler); previous(); if (!this.disposed && this.hosted && !this.ready) { handshake = true; } } else { if (_isHandshake.call(this, event)) { if (!this.token) { this.token = event.data; } if (!this.disposed && this.hosted && !this.ready) { handshake = true; } } else if (this.token) { this.receiver.receive.call(this.receiver, event); } } if (handshake) { this.receiver.postMessage(HANSHAKE_PREFIX + this.token); _onReady.call(this); } }; } /** * Method to prepare the message for posting to the target * @param message * @returns {*} * @private */ function _prepareMessage(message) { _tokenize.call(this, message); return this.serialize(message); } /** * Method to get url indication for using message channel or polyfill * @returns {Boolean} indication for message channel usage * @private */ /* istanbul ignore next: it is being covered at the iframe side - cannot add it to coverage matrix */ function _getChannelUrlIndicator() { if ("true" === PostMessageUtilities.getURLParameter("lpPMCPolyfill")) { return false; } } /** * Method to create and hookup message channel factory for further use * @param {Function} onmessage - the message handler to be used with the channel * @private */ function _hookupMessageChannel(onmessage) { return function() { this.channel = new MessageChannel(); this.receiver = this.channel.port1; this.dispatcher = this.channel.port2; this.receiver.onmessage = onmessage; this.neutered = false; }.bind(this); } /** * Method for applying the token if any on the message * @param {Object} message - the message to be tokenize * @private */ function _tokenize(message) { if (this.token) { message.token = this.token; } } /** * Method for applying the token if any on the message * @param {Object} message - the message to be tokenize * @private */ function _validateToken(message) { return (message && message.token === this.token); } /** * Method to validate whether an event is for handshake * @param {Object} event - the event object on message * @private */ f