chronosjs
Version:
JS Channels Mechanism
1,326 lines (1,170 loc) • 106 kB
JavaScript
;(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