LivePersonInc/chronosjs

View on GitHub
src/courier/PostMessageCourier.js

Summary

Maintainability
F
4 days
Test Coverage
/**
 * LIMITATIONS:
 * 1) Only supports browsers which implements postMessage API and have native JSON implementation (IE8+, Chrome, FF, Safari, Opera, IOS, Opera Mini, Android)
 * 2) IE9-, FF & Opera Mini does not support MessageChannel and therefore we fallback to using basic postMessage.
 *    This makes the communication opened to any handler registered for messages on the same origin.
 * 3) All passDataByRef flags (in LPEventChannel) are obviously ignored
 * 4) In case the browser does not support passing object using postMessage (IE8+, Opera Mini), and no special serialize/deserialize methods are supplied to PostMessageCourier,
 *    All data is serialized using JSON.stringify/JSON.parse which means that Object data is limited to JSON which supports types like:
 *    strings, numbers, null, arrays, and objects (and does not allow circular references).
 *    Trying to serialize other types, will result in conversion to null (like Infinity or NaN) or to a string (Dates)
 *    that must be manually deserialized on the other side
 * 5) When Iframe is managed outside of PostMessageCourier (passed by reference to the constructor),
 *    a targetOrigin option is expected to be passed to the constructor, and a query parameter with the name "lpHost" is expected on the iframe url (unless the PostMessageCourier
 *    at the iframe side, had also been initialized with a valid targetOrigin option)
 */
// TODO: Add Support for target management when there is a problem that requires re-initialization of the target
;(function (root, cacherRoot, circuitRoot, factory) {
    "use strict";

    /* istanbul ignore if  */
    //<amd>
    if ("function" === typeof define && define.amd) {

        // AMD. Register as an anonymous module.
        define("Chronos.PostMessageCourier", ["Chronos.PostMessageUtilities", "Chronos.Channels", "cacher", "CircuitBreaker", "Chronos.PostMessageChannel", "Chronos.PostMessagePromise", "Chronos.PostMessageMapper"],
            function (PostMessageUtilities, Channels, cacher, CircuitBreaker, PostMessageChannel, PostMessagePromise, PostMessageMapper) {
                return factory(root, root, PostMessageUtilities, Channels,
                    cacher, CircuitBreaker, PostMessageChannel, PostMessagePromise, PostMessageMapper, true);
        });
        return;
    }
    //</amd>
    /* istanbul ignore next  */
    if ("object" !== typeof exports) {
        /**
         * @depend ../Channels.js
         * @depend ../../node_modules/circuit-breakerjs/src/CircuitBreaker.js
         * @depend ../../node_modules/cacherjs/src/cacher.js
         * @depend ./PostMessageUtilities.js
         * @depend ./PostMessageChannel.js
         * @depend ./PostMessagePromise.js
         * @depend ./PostMessageMapper.js
         */
        root.Chronos = root.Chronos || {};
        factory(root, root.Chronos, root.Chronos.PostMessageUtilities, root.Chronos.Channels,
            cacherRoot.Cacher,  circuitRoot.CircuitBreaker,
            root.Chronos.PostMessageChannel, root.Chronos.PostMessagePromise, root.Chronos.PostMessageMapper);
    }
}(typeof ChronosRoot === "undefined" ? this : ChronosRoot,
    typeof CacherRoot === "undefined" ? this : CacherRoot,
    typeof CircuitRoot === "undefined" ? this : CircuitRoot,
    function (root, exports, PostMessageUtilities, Channels, Cacher, CircuitBreaker, PostMessageChannel, PostMessagePromise, PostMessageMapper, hide) {
        "use strict";

        /*jshint validthis:true */
        var MESSAGE_PREFIX = "LPMSG_";
        var ACTION_TYPE = {
            TRIGGER: "trigger",
            COMMAND: "command",
            REQUEST: "request",
            RETURN: "return"
        };
        var DEFAULT_TIMEOUT = 30 * 1000;
        var DEFAULT_CONCURRENCY = 100;
        var DEFAULT_MESSURE_TIME = 30 * 1000;
        var DEFAULT_MESSURE_TOLERANCE = 30;
        var DEFAULT_MESSURE_CALIBRATION = 10;
        var CACHE_EVICTION_INTERVAL = 1000;

        /**
         * PostMessageCourier 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
         * @param {Object} [options.eventChannel] - optional events channel to be used (if not supplied, a new one will be created OR optional events, optional commands, optional reqres to be used
         * @param {Number} [options.timeout = 30000] - optional milliseconds parameter for waiting before timeout to responses (default is 30 seconds)
         * @param {Number} [options.messureTime = 30000] - optional milliseconds parameter for time measurement indicating the time window to apply when implementing the internal fail fast mechanism (default is 30 seconds)
         * @param {Number} [options.messureTolerance = 30] - optional percentage parameter indicating the tolerance to apply on the measurements when implementing the internal fail fast mechanism (default is 30 percents)
         * @param {Number} [options.messureCalibration = 10] optional numeric parameter indicating the calibration of minimum calls before starting to validate measurements when implementing the internal fail fast mechanism (default is 10 calls)
         * @param {Function} [options.ondisconnect] - optional disconnect handler that will be invoked when the fail fast mechanism disconnects the component upon to many failures
         * @param {Function} [options.onreconnect] - optional reconnect handler that will be invoked when the fail fast mechanism reconnects the component upon back to normal behaviour
         *
         * @example
         * var courier = new Chronos.PostMessageCourier({
         *     target: {
         *         url: "http://localhost/chronosjs/debug/courier.frame.html",
         *         style: {
         *             width: "100px",
         *             height: "100px"
         *         }
         *     }
         * });
         */
        function PostMessageCourier(options) {
            // For forcing new keyword
            /* istanbul ignore if  */
            if (false === (this instanceof PostMessageCourier)) {
                return new PostMessageCourier(options);
            }

            this.initialize(options);
        }

        PostMessageCourier.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
             * @param {Boolean} [options.registerExternal] - allows registering external components for triggering to them as well
             * @param {Object} [options.eventChannel] - optional events channel to be used (if not supplied, a new one will be created OR optional events, optional commands, optional reqres to be used
             * @param {Number} [options.timeout = 30000] - optional milliseconds parameter for waiting before timeout to responses (default is 30 seconds)
             * @param {Number} [options.messureTime = 30000] - optional milliseconds parameter for time measurement indicating the time window to apply when implementing the internal fail fast mechanism (default is 30 seconds)
             * @param {Number} [options.messureTolerance = 30] - optional percentage parameter indicating the tolerance to apply on the measurements when implementing the internal fail fast mechanism (default is 30 percents)
             * @param {Number} [options.messureCalibration = 10] optional numeric parameter indicating the calibration of minimum calls before starting to validate measurements when implementing the internal fail fast mechanism (default is 10 calls)
             * @param {Function} [options.ondisconnect] - optional disconnect handler that will be invoked when the fail fast mechanism disconnects the component upon to many failures
             * @param {Function} [options.onreconnect] - optional reconnect handler that will be invoked when the fail fast mechanism reconnects the component upon back to normal behaviour
             */
            function initialize(options) {

                if (!this.initialized) {
                    options = options || {};

                    // Init options for serialization of messages
                    _initializeSerialization.call(this, options);

                    // Init the communication components
                    _initializeCommunication.call(this, options);

                    // Init the cache for handling responses
                    _initializeCache.call(this, options);

                    // Init the fail fast mechanism which monitors responses
                    _initializeFailFast.call(this, options);

                    _registerProxy.call(this, this.eventChannel);

                    // Dumb Proxy methods
                    this.once = this.eventChannel.once;
                    this.hasFiredEvents = this.eventChannel.hasFiredEvents;
                    this.bind = this.eventChannel.bind;
                    this.register = this.eventChannel.register;
                    this.unbind = this.eventChannel.unbind;
                    this.unregister = this.eventChannel.unregister;
                    this.hasFiredCommands = this.eventChannel.hasFiredCommands;
                    this.comply = this.eventChannel.comply;
                    this.stopComplying = this.eventChannel.stopComplying;
                    this.hasFiredReqres = this.eventChannel.hasFiredReqres;
                    this.reply = this.eventChannel.reply;
                    this.stopReplying = this.eventChannel.stopReplying;
                    this.initialized = true;
                }
            }

            /**
             * Registers an external call to trigger for events to propagate calls to Channels.trigger automatically
             * @param eventChannel
             * @private
             */
            function _registerProxy(eventChannel) {
                if (eventChannel && "function" === typeof eventChannel.registerProxy) {
                    eventChannel.registerProxy({
                        trigger: function () {
                            _postMessage.call(this, Array.prototype.slice.apply(arguments), ACTION_TYPE.TRIGGER);
                        },
                        context: this
                    });
                }
            }

            /**
             * Method to get the member instance of the message channel
             * @returns {PostMessageChannel} the member message channel
             */
            function getMessageChannel() {
                return this.messageChannel;
            }

            /**
             * Method to get the member instance of the event channel
             * @returns {Events} the member event channel
             */
            function getEventChannel() {
                return this.eventChannel;
            }

            /**
             * Method to trigger event via post message
             * @link Chronos.Events#trigger
             * @param {Object|String} options - Configuration object or app name
             * @param {String} [options.eventName] - the name of the event triggered
             * @param {String} [options.appName] - optional specifies the identifier it is bound to
             * @param {Boolean} [options.passDataByRef = false] - boolean flag whether this callback will get the reference information of the event or a copy (this allows control of data manipulation)
             * @param {Object} [options.data] - optional event parameters to be passed to the listeners
             * @param {String|Boolean} [evName] - the name of the event triggered || [noLocal] - optional boolean flag indicating whether to trigger the event on the local event channel too
             * @param {Object} [data] - optional event parameters to be passed to the listeners
             * @param {Boolean} [noLocal] - optional boolean flag indicating whether to trigger the event on the local event channel too
             * @returns {*}
             *
             * @example
             * courier.trigger({
             *     appName: "frame",
             *     eventName: "got_it",
             *     data: 2
             * });
             */
            function trigger() {
                if (!this.disposed) {
                    var args = Array.prototype.slice.apply(arguments);

                    // We are looking for a "noLocal" param which can only be second or forth
                    // And only if its value is true, we will not trigger the event on the local event channel
                    if (!((2 === arguments.length || 4 === arguments.length) &&
                        true === arguments[arguments.length - 1])) {
                        this.eventChannel.trigger.apply(this.eventChannel, args);
                    }

                    return _postMessage.call(this, args, ACTION_TYPE.TRIGGER);
                }
            }

            /**
             * Method to trigger a command via post message
             * @link Chronos.Commands#command
             * @param {Object|String} options - Configuration object or app name
             * @param {String} [options.cmdName] - the name of the command triggered
             * @param {String} [options.appName] - optional specifies the identifier it is bound to
             * @param {Boolean} [options.passDataByRef = false] - boolean flag whether this callback will get the reference information of the event or a copy (this allows control of data manipulation)
             * @param {Object} [options.data] - optional event parameters to be passed to the listeners
             * @param {Function} [callback] - optional callback method to be triggered when the command had finished executing
             * @returns {*}
             *
             * @example
             * courier.command({
             *     appName: "frame",
             *     cmdName: "expect",
             *     data: data
             * }, function(err) {
             *     if (err) {
             *         console.log("Problem invoking command");
             *     }
             * });
             */
            function command() {
                if (!this.disposed) {
                    var args = Array.prototype.slice.apply(arguments);
                    return _postMessage.call(this, args, ACTION_TYPE.COMMAND);
                }
            }

            /**
             * Method to trigger a request via post message
             * @link Chronos.ReqRes#request
             * @param {Object|String} options - Configuration object or app name
             * @param {String} [options.reqName] - the name of the request triggered
             * @param {String} [options.appName] - optional specifies the identifier it is bound to
             * @param {Boolean} [options.passDataByRef = false] - boolean flag whether this callback will get the reference information of the event or a copy (this allows control of data manipulation)
             * @param {Object} [options.data] - optional event parameters to be passed to the listeners
             * @param {Function} [callback] - optional callback method to be triggered when the command had finished executing
             * @return {*}
             *
             * @example
             * courier.request({
             *     appName: "iframe",
             *     reqName: "Ma Shlomha?",
             *     data: data
             * }, function(err, data) {
             *      if (err) {
             *          console.log("Problem invoking request");
             *          return;
             *      }
             *
             *      // Do Something with data
             * });
             */
            function request() {
                if (!this.disposed) {
                    var args = Array.prototype.slice.apply(arguments);
                    return _postMessage.call(this, args, ACTION_TYPE.REQUEST);
                }
            }

            /**
             * Method for disposing the object
             */
            function dispose() {
                if (!this.disposed) {
                    this.messageChannel.dispose();
                    this.messageChannel = void 0;
                    this.eventChannel = void 0;
                    this.mapper = void 0;
                    this.callbackCache = void 0;
                    this.circuit = void 0;
                    this.disposed = true;
                }
            }

            /**
             * Method for initializing the options for serialization of messages
             * @param {Boolean} [options.useObjects = true upon browser support] - optional indication for passing objects by reference
             * @param {Function} [options.serialize = JSON.stringify] - optional serialization method for post message
             * @param {Function} [options.deserialize = JSON.parse] - optional deserialization method for post message
             * @private
             */
            function _initializeSerialization(options) {
                this.useObjects = false === options.useObjects ? options.useObjects : _getUseObjectsUrlIndicator();
                if ("undefined" === typeof this.useObjects) {
                    // Defaults to true
                    this.useObjects = true;
                }
                options.useObjects = this.useObjects;

                // Define the serialize/deserialize methods to be used
                if ("function" !== typeof options.serialize || "function" !== typeof options.deserialize) {
                    if (this.useObjects && PostMessageUtilities.hasPostMessageObjectsSupport()) {
                        this.serialize = _de$serializeDummy;
                        this.deserialize = _de$serializeDummy;
                    }
                    else {
                        this.serialize = PostMessageUtilities.stringify;
                        this.deserialize = JSON.parse;
                    }

                    options.serialize = this.serialize;
                    options.deserialize = this.deserialize;
                }
                else {
                    this.serialize = options.serialize;
                    this.deserialize = options.deserialize;
                }
            }

            /**
             * Method for initializing the communication elements
             * @param {Object} [options.eventChannel] - optional events channel to be used (if not supplied, a new one will be created OR optional events, optional commands, optional reqres to be used
             * @private
             */
            function _initializeCommunication(options) {
                var mapping;
                var onmessage;

                // Grab the event channel and initialize a new mapper
                this.eventChannel = options.eventChannel || new Channels({
                    events: options.events,
                    commands: options.commands,
                    reqres: options.reqres
                });
                this.mapper = new PostMessageMapper(this.eventChannel);

                // Bind the mapping method to the mapper
                mapping = this.mapper.toEvent.bind(this.mapper);
                // Create the message handler which uses the mapping method
                onmessage = _createMessageHandler(mapping).bind(this);

                // Initialize a message channel with the message handler
                this.messageChannel = new PostMessageChannel(options, onmessage);
            }

            /**
             * Method for initializing the cache for responses
             * @param {Number} [options.maxConcurrency = 100] - optional maximum concurrency that can be managed by the component before dropping
             * @param {Number} [options.timeout = 30000] - optional milliseconds parameter for waiting before timeout to responses (default is 30 seconds)
             * @private
             */
            function _initializeCache(options) {
                this.callbackCache = new Cacher({
                    max: PostMessageUtilities.parseNumber(options.maxConcurrency, DEFAULT_CONCURRENCY),
                    ttl: PostMessageUtilities.parseNumber(options.timeout, DEFAULT_TIMEOUT),
                    interval: CACHE_EVICTION_INTERVAL
                });
            }

            /**
             * Method for initializing the fail fast mechanisms
             * @param {Number} [options.messureTime = 30000] - optional milliseconds parameter for time measurement indicating the time window to apply when implementing the internal fail fast mechanism (default is 30 seconds)
             * @param {Number} [options.messureTolerance = 30] - optional percentage parameter indicating the tolerance to apply on the measurements when implementing the internal fail fast mechanism (default is 30 percents)
             * @param {Number} [options.messureCalibration = 10] optional numeric parameter indicating the calibration of minimum calls before starting to validate measurements when implementing the internal fail fast mechanism (default is 10 calls)
             * @param {Function} [options.ondisconnect] - optional disconnect handler that will be invoked when the fail fast mechanism disconnects the component upon to many failures
             * @param {Function} [options.onreconnect] - optional reconnect handler that will be invoked when the fail fast mechanism reconnects the component upon back to normal behaviour
             * @private
             */
            function _initializeFailFast(options) {
                var messureTime = PostMessageUtilities.parseNumber(options.messureTime, DEFAULT_MESSURE_TIME);
                this.circuit = new CircuitBreaker({
                    timeWindow: messureTime,
                    slidesNumber: Math.ceil(messureTime / 100),
                    tolerance: PostMessageUtilities.parseNumber(options.messureTolerance, DEFAULT_MESSURE_TOLERANCE),
                    calibration: PostMessageUtilities.parseNumber(options.messureCalibration, DEFAULT_MESSURE_CALIBRATION),
                    onopen: PostMessageUtilities.parseFunction(options.ondisconnect, true),
                    onclose: PostMessageUtilities.parseFunction(options.onreconnect, true)
                });
            }

            /**
             * Method to get url indication for using serialization/deserialization
             * @returns {Boolean} indication for serialization/deserialization usage
             * @private
             */
            function _getUseObjectsUrlIndicator() {
                var deserialize = PostMessageUtilities.getURLParameter("lpPMDeSerialize");

                if ("true" === deserialize) {
                    return false;
                }
            }

            /**
             * Just a dummy serialization/deserialization method for browsers supporting objects with postMessage API
             * @param {Object} object - the object to (NOT) serialize/deserialize.
             * @returns {Object} The same object
             */
            function _de$serializeDummy(object) {
                return object;
            }

            /**
             * Method for posting the message via the circuit breaker
             * @param {Array} args - the arguments for the message to be processed.
             * @param {String} name - name of type of command.
             * @private
             */
            function _postMessage(args, name) {
                return this.circuit.run(function (success, failure, timeout) {
                    var message = _prepare.call(this, args, name, timeout);

                    if (message) {
                        try {
                            var initiated = this.messageChannel.postMessage.call(this.messageChannel, message);

                            if (false === initiated) {
                                failure();
                            }
                            else {
                                success();
                            }
                        }
                        catch (ex) {
                            failure();
                        }
                    }
                    else {
                        // Cache is full, as a fail fast mechanism, we should not continue
                        failure();
                    }
                }.bind(this));
            }

            /**
             * Method for posting the returned message via the circuit breaker
             * @param {Object} message - the message to post.
             * @param {bject} [target] - optional target for post.
             * @private
             */
            function _returnMessage(message, target) {
                return this.circuit.run(function (success, failure) {
                    try {
                        var initiated = this.messageChannel.postMessage.call(this.messageChannel, message, target);

                        if (false === initiated) {
                            failure();
                        }
                        else {
                            success();
                        }
                    }
                    catch (ex) {
                        failure();
                    }
                }.bind(this));
            }

            /**
             * Method for preparing the message to be posted via the postmessage and caching the callback to be called if needed
             * @param {Array} args - the arguments to pass to the message mapper
             * @param {String} name - the action type name (trigger, command, request)
             * @param {Function} [ontimeout] - the ontimeout measurement handler
             * @returns {Function} handler function for messages
             * @private
             */
            function _prepare(args, name, ontimeout) {
                var method;
                var ttl;
                var id = PostMessageUtilities.createUniqueSequence(MESSAGE_PREFIX + name + PostMessageUtilities.SEQUENCE_FORMAT);

                args.unshift(id, name);

                if (_isTwoWay(name)) {
                    if (1 < args.length && "function" === typeof args[args.length - 1]) {
                        method = args.pop();
                    }
                    else if (2 < args.length && !isNaN(args[args.length - 1]) && "function" === typeof args[args.length - 2]) {
                        ttl = parseInt(args.pop(), 10);
                        method = args.pop();
                    }

                    if (method) {
                        if (!this.callbackCache.set(id, method, ttl, function (id, callback) {
                                ontimeout();
                                _handleTimeout.call(this, id, callback);
                            }.bind(this))) {
                            // Cache is full, as a fail fast mechanism, we will not continue
                            return void 0;
                        }
                    }
                }

                return this.mapper.toMessage.apply(this.mapper, args);
            }

            /**
             * Method for checking two way communication for action
             * @param {PostMessageCourier.ACTION_TYPE} action - the action type name
             * @returns {Boolean} flag to indicate whether the action is two way (had return call)
             * @private
             */
            function _isTwoWay(action) {
                return ACTION_TYPE.REQUEST === action || ACTION_TYPE.COMMAND === action;
            }

            /**
             * Method for handling timeout of a cached callback
             * @param {String} id - the id of the timed out callback
             * @param {Function} callback - the callback object from cache
             * @private
             */
            function _handleTimeout(id, callback) {
                // Handle timeout
                if (id && "function" === typeof callback) {
                    try {
                        callback.call(null, new Error("Callback: Operation Timeout!"));
                    }
                    catch (ex) {
                        /* istanbul ignore next  */
                        PostMessageUtilities.log("Error while trying to handle the timeout using the callback", "ERROR", "PostMessageCourier");
                    }
                }
            }

            /**
             * Method for handling return messages from the callee
             * @param {String} id - the id of the callback
             * @param {Object} method - the method object with needed args
             * @private
             */
            function _handleReturnMessage(id, method) {
                var callback = this.callbackCache.get(id, true);
                var args = method && method.args;

                if ("function" === typeof callback) {
                    // First try to parse the first parameter in case the error is an object
                    if (args && args.length && args[0] && "Error" === args[0].type && "string" === typeof args[0].message) {
                        args[0] = new Error(args[0].message);
                    }

                    try {
                        callback.apply(null, args);
                    }
                    catch (ex) {
                        PostMessageUtilities.log("Error while trying to handle the returned message from request/command", "ERROR", "PostMessageCourier");
                    }
                }
            }

            /**
             * Method for creation of a return message handler from the callee
             * @param {String} id - the id of the message
             * @param {String} name - message name
             * @param {Object} message - original message structure
             * @private
             */
            function _getReturnMessageHandler(id, name, message) {
                /* istanbul ignore next: it is being covered at the iframe side - cannot add it to coverage matrix  */
                return function(err, result) {
                    var retMsg;
                    var params;
                    var error = err;

                    // In case of Error Object, create a special object that can be parsed
                    if (err instanceof Error) {
                        error = {
                            type: "Error",
                            message: err.message
                        };
                    }

                    // Call the mapping method to receive the message structure
                    params = [id, ACTION_TYPE.RETURN, error];

                    if (ACTION_TYPE.REQUEST === name) {
                        params.push(result);
                    }

                    retMsg = this.mapper.toMessage.apply(this.mapper, params);

                    // Post the message
                    _returnMessage.call(this, retMsg, message.source);
                }.bind(this);
            }

            /**
             * Method for handling the result of the call (this can be the response or a defer/promise)
             * @param {String} id - the id of the message
             * @param {String} name - message name
             * @param {Object} result - the result object
             * @param {Object} message - original message structure
             * @private
             */
            function _handleResult(id, name, result, message) {
                var retMsg;
                var params;

                // If the result is async (promise) we need to defer the execution of the results data
                if (("undefined" !== typeof Promise && result instanceof Promise) || result instanceof PostMessagePromise) {
                    // Handle async using promises
                    result.then(function (data) {
                        params = [id, ACTION_TYPE.RETURN, null];

                        if (ACTION_TYPE.REQUEST === name) {
                            params.push(data);
                        }

                        // Call the mapping method to receive the message structure
                        retMsg = this.mapper.toMessage.apply(this.mapper, params);

                        // Post the message
                        _returnMessage.call(this, retMsg, message.source);
                    }.bind(this), function (data) {
                        params = [id, ACTION_TYPE.RETURN, data];

                        // Call the mapping method to receive the message structure
                        retMsg = this.mapper.toMessage.apply(this.mapper, params);

                        // Post the message
                        _returnMessage.call(this, retMsg, message.source);
                    }.bind(this));
                }
                else {
                    if (result && result.error) {
                        params = [id, ACTION_TYPE.RETURN, result];

                        // Call the mapping method to receive the message structure
                        retMsg = this.mapper.toMessage.apply(this.mapper, params);

                        // Post the message
                        _returnMessage.call(this, retMsg, message.source);
                    }
                    else if ("undefined" !== typeof result) {
                        params = [id, ACTION_TYPE.RETURN, null];

                        if (ACTION_TYPE.REQUEST === name) {
                            params.push(result);
                        }

                        // Call the mapping method to receive the message structure
                        retMsg = this.mapper.toMessage.apply(this.mapper, params);

                        // Post the message
                        _returnMessage.call(this, retMsg, message.source);
                    }
                }
            }
            /**
             * Method for wrapping the handler of the postmessage for parsing
             * @param {Object} mapping - the handler for incoming messages to invoke which maps the message to event
             * @returns {Function} handler function for messages
             * @private
             */
            function _createMessageHandler(mapping) {
                return function(message) {
                    var handler;
                    var result;
                    var params;
                    var retMsg;
                    var id;
                    var name;
                    var args;

                    if (message) {
                        id = message.method && message.method.id;
                        name = message.method && message.method.name;
                        args = message.method && message.method.args;

                        // In case the message is a return value from a request/response call
                        // It is marked as a "return" message and we need to call the supplied cached callback
                        if (ACTION_TYPE.RETURN === name) {
                            _handleReturnMessage.call(this, id, message.method);
                        }
                        else {
                            // Call the mapping method to receive the handling method on the event channel
                            // Invoke the handling method
                            try {
                                if (_isTwoWay(name)) {
                                    if (args.length) {
                                        args.push(_getReturnMessageHandler.call(this, id, name, message));
                                    }
                                }

                                handler = mapping(message);
                                result = handler && handler();
                            }
                            catch (ex) {
                                /* istanbul ignore next: special handling for other implementations of channels which does not catch exceptions from triggers (like backbone) - when working with chronos channels it will not be called  */
                                PostMessageUtilities.log("Error while trying to invoke the handler on the events channel", "ERROR", "PostMessageCourier");
                                /* istanbul ignore next: special handling for other implementations of channels which does not catch exceptions from triggers (like backbone) - when working with chronos channels it will not be called  */
                                if (_isTwoWay(name)) {
                                    params = [id, ACTION_TYPE.RETURN, {error: ex.toString()}];
                                    retMsg = this.mapper.toMessage.apply(this.mapper, params);
                                    _returnMessage.call(this, retMsg, message.source);
                                }
                            }

                            // In case the method is two way and returned a result
                            if (_isTwoWay(name) && "undefined" !== typeof result) {
                                _handleResult.call(this, id, name, result, message);
                            }
                        }
                    }
                };
            }

            return {
                initialize: initialize,
                getMessageChannel: getMessageChannel,
                getEventChannel: getEventChannel,
                trigger: trigger,
                publish: trigger,
                command: command,
                request: request,
                dispose: dispose
            };
        }());

        // attach properties to the exports object to define
        // the exported module properties.
        if (!hide) {
            exports.PostMessageCourier = exports.PostMessageCourier || PostMessageCourier;
        }
        return PostMessageCourier;
    }));