@elliemae/em-ssf-host
Version:
ICE Secure Scripting Framework Host Library
986 lines (822 loc) • 37.4 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["ssf"] = factory();
else
root["elli"] = root["elli"] || {}, root["elli"]["ssf"] = factory();
})(typeof self !== 'undefined' ? self : this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, {
/******/ configurable: false,
/******/ enumerable: true,
/******/ get: getter
/******/ });
/******/ }
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
;
//
// Provides very basic logging capabilities to the SSF system
//
module.exports = {
// Constants for the different log levels
levels: {
Verbose: 0,
Trace: 0,
Info: 1,
Warning: 2,
Error: 3,
None: 10
},
// Colors used for logging
colors: ['grey', 'black', 'darkorange', 'firebrick'],
// The current log verbosity level
logLevel: 3,
// Writes an entry to the log
log: function log(text, level, src) {
level = level ? level : this.levels.Verbose;
if (level >= this.logLevel) {
var val = (src ? '(' + src + '): ' : '') + text;
if (this.colors && this.colors[level]) console.log('%c' + val, 'color: ' + this.colors[level]);else console.log(val);
}
},
// Writes a warning to the log
info: function info(text, src) {
this.log(text, this.levels.Info, src);
},
// Writes a warning to the log
warn: function warn(text, src) {
this.log(text, this.levels.Warning, src);
},
// Writes a warning to the log
error: function error(text, src) {
this.log(text, this.levels.Error, src);
},
// Writes a verbose/trace to the log
trace: function trace(text, src) {
this.log(text, this.levels.Trace, src);
}
};
/***/ }),
/* 1 */
/***/ (function(module, exports, __webpack_require__) {
;
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
//
// The elli.automation object provides a namespace for static functions and objects
// related to the scritpable controls
//
// Create the elli namespace, if not already present
var remoting = __webpack_require__(2);
var logger = __webpack_require__(0);
var logSource = "ssf-host";
var automation = function () {
// String constants
var eventMessageType = "object:event";
var configMessageType = "host:config";
// Represents the set of windows to which events will be broadcast
var isConnected = false;
var guests = [];
// Initializes a guest when the guest is full loaded
function handleGuestReady(sourceWin, msgId, msgType, msgBody) {
// Verify the guest belongs to this host
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Received ready event for unknown guest. Known guests = ' + guests.length, logSource);
return;
}
if (!guest.initialized) {
// Initialize the capabilities object
guest.initialized = true;
guest.capabilities = msgBody || {};
// Invoke the control function on the guest
dispatchConfigEvent(guest);
logger.info('Guest (id = ' + guest.id + ') in frame ' + guest.domElement.id + ' is initialized.', logSource);
}
// If the guest has not indicated that it will provide explicit notification
// when it's ready, we'll fire the ready event now
if (!msgBody || !msgBody.onReady) {
handleGuestReadyComplete(sourceWin, msgId, msgType, msgBody);
}
}
// Handles the ReadyComplete event from the guest (invoked by calling ready())
function handleGuestReadyComplete(sourceWin, msgId, msgType, msgBody) {
// Verify the guest belongs to this host
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Received ready event for unknown guest. Known guests = ' + guests.length, logSource);
return;
}
// Ensure this guest is initialized
if (!guest.initialized) {
throw "Guest must be initialized before it is marked as ready";
}
if (!guest.ready) {
// Set the ready state
guest.ready = true;
// Invoke the registered callback, if any
if (guest.host.options && guest.host.options.readyStateCallback) {
guest.host.options.readyStateCallback(guest);
}
logger.info('Guest (id = ' + guest.id + ') in frame ' + guest.domElement.id + ' is ready.', logSource);
}
}
// Dispatches a message to the guest to update configuration settings that
// must propagate from the host to the guests.
function dispatchConfigEvent(guest) {
if (guest.ready) {
guest.dispatchMessage(configMessageType, {
logLevel: logger.logLevel
});
}
}
// Handles object get requests from the remote automation framework
function handleObjectGet(sourceWin, msgId, msgType, msgBody) {
var objectId = msgBody.objectId;
logger.info('Processing request getObject("' + objectId + '") (requestId = ' + msgId + ')...', logSource);
// Get the guest from which the request came
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Rejected object request from unknown guest window', logSource);
return false;
}
// Get the object from the guest's host
var obj = guest.host.getObjectById(objectId);
if (obj == null) {
logger.warn('Rejected object request from unauthorized guest window', logSource);
throw "Invocation of unknown or unauthorized object '" + objectId + "' from guest";
}
remoting.respond(sourceWin, msgId, encodeResponse(guest, obj));
logger.info('Returned scripting object "' + objectId + '" (requestId = ' + msgId + ')', logSource);
}
// Handles object invoke requests from the remote automation framework
function handleObjectInvoke(sourceWin, msgId, msgType, msgBody) {
var objectId = msgBody.objectId;
// Get the guest from which the request came
var guest = getGuestForWindow(sourceWin);
if (guest == null) {
logger.warn('Rejected invocation of ' + objectId + '.' + msgBody.functionName + ' from unknown guest window', logSource);
return false;
}
logger.info('Function ' + objectId + '.' + msgBody.functionName + '() called from guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
// Get the object from the guest's host
var obj = guest.host.getObjectById(objectId);
if (obj == null) {
logger.warn('Invocation of unknown or unauthorized object (' + objectId + ') from guest "' + guest.id + '"', logSource);
remoting.raiseException('The requested object (' + objectId + ') is not available');
return false;
}
// Invoke the function, which will always return a Promise
invoke(guest, obj, msgBody.functionName, msgBody.functionParams).then(function (val) {
remoting.respond(sourceWin, msgId, encodeResponse(guest, val));
logger.info('Dispatched return value for ' + objectId + '.' + msgBody.functionName + '() to guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
}).catch(function (ex) {
remoting.raiseException(sourceWin, msgId, encodeException(guest, ex));
logger.info('Dispatched exception for ' + objectId + '.' + msgBody.functionName + '() to guest "' + guest.id + '" (requestId = ' + msgId + ')', logSource);
});
}
// Encodes the response to a function call to prepare it to be remoted
function encodeResponse(guest, val) {
if (val && val._toJSON) {
// Ensure the object is published so it can receive callbacks
guest.host.publish(val);
return { type: 'object', object: val._toJSON() };
} else {
return { type: 'value', value: val };
}
}
// Encodes an exception thrown by a host function
function encodeException(guest, ex) {
if (typeof ex === "string") {
return ex;
} else if (ex instanceof Error) {
return ex.message;
} else if (ex.toString && typeof ex.toString === "function") {
return ex.toString();
} else {
return 'An unexpected error occurred in the host application';
}
}
// Returns the guest in a given window
function getGuestForWindow(guestWindow) {
return guests.find(function (guest) {
return guest.window === guestWindow;
});
}
// Invokes a function on a elli.automation object
function invoke(guest, obj, functionName, functionParams) {
// Ensure the function exists
if (typeof obj[functionName] != "function") {
logger.warn("Attempt to call invalid function on object type " + obj.objectType + ": " + functionName, logSource);
return Promise.reject("Function not found");
}
// Invoke the object's function
logger.info('Invoking host implementation of ' + obj.id + '.' + functionName + '()', logSource);
return new Promise(function (resolve, reject) {
obj[functionName].callContext = { guest: guest };
resolve(obj[functionName].apply(obj, _toConsumableArray(functionParams)));
});
}
// Connects the elli.automation framework to the remoting systems
function connect() {
if (!isConnected) {
remoting.setLogSource(logSource);
remoting.listen("object:get", handleObjectGet);
remoting.listen("object:invoke", handleObjectInvoke);
remoting.listen("guest:ready", handleGuestReady);
remoting.listen("guest:readyComplete", handleGuestReadyComplete);
isConnected = true;
}
}
// Flattens an array or arrays into a single array (equivalent to the Array.flat() method,
// which is not supported in IE or Safari)
function flatten(source, target) {
var retVal = target || [];
if (source && source.forEach) {
source.forEach(function (item) {
flatten(item, retVal);
});
} else if (typeof source != "undefined") {
retVal.push(source);
}
return retVal;
}
// Return the public implementation of the class
return {
// Registers a callback for elli.automation events
registerGuest: function registerGuest(guest) {
if (!guest || !guest.host || !guest.window) {
throw "Invalid guest object";
}
// Connect the framework
connect();
// Add to the guest list
guests.push(guest);
},
// Removes a guest from the list of registered guests
unregisterGuest: function unregisterGuest(guestId) {
guests = guests.filter(function (guest) {
return guest.id != guestId;
});
},
// Raises an event from a elli.automation object. The eventOptions is an optional parameter
// to set other options for the event. Valid options include:
// window: the guest window handle for a targeted event
// eventHandler: the name of the event handler function in the guest, if known
// timeout: the timeout (in ms) for feedback to be returned
raiseEvent: function raiseEvent(elementOrId, eventName, eventParams, eventOptions) {
// Resolve the elli.automation object
var objectId = elementOrId.id || elementOrId;
logger.info('Raising event "' + objectId + '.' + eventName + '"...', logSource);
// Invoke the event handler in all guests which are exposed to this object
var guestPromises = [];
eventOptions = eventOptions || {};
guests.forEach(function (guest) {
// If a target is supplied, be sure to honor that target to send the message only to
// the intended window
if (!eventOptions.window || eventOptions.window === guest.window) {
// Get the object from the host
var obj = guest.host.getObjectById(objectId);
if (obj != null) {
var eventObj = {
object: obj._toJSON(),
eventName: eventName,
eventParams: eventParams,
eventHandler: eventOptions.eventHandler,
eventOptions: {
allowsFeedback: eventOptions.timeout ? true : false,
timeout: eventOptions.timeout
}
};
if (eventOptions.timeout && guest.capabilities.eventFeedback) {
// For an event with feedback, we call the invoke() method, which returns
// a Promise that will be fulfilled when the guest responds
guestPromises.push(remoting.invoke(guest.window, eventMessageType, eventObj, eventOptions.timeout));
logger.info('Dispatched interactive event "' + objectId + '.' + eventName + '" to guest (id = "' + guest.id + '"), ' + '"timeout = ' + eventOptions.timeout + 'ms', logSource);
} else {
// If no feedback is needed, we fire and forget
remoting.send(guest.window, eventMessageType, eventObj);
logger.info('Dispatched fire-and-forget event "' + objectId + '.' + eventName + '" to guest (id = "' + guest.id + '")', logSource);
}
}
}
});
// Return a promise that aggregates all of the guest promises
return Promise.all(guestPromises).then(function (values) {
// Flatten the values into a single array
logger.info('Completed event processing for "' + objectId + '.' + eventName + '"', logSource);
return flatten(values);
});
},
// Sets the log level for the host and guest
setLogLevel: function setLogLevel(level) {
// Set the logging level at the host
logger.logLevel = level;
// Notify all guests of the change
guests.forEach(dispatchConfigEvent);
logger.info('Dispatched config events to all guests', logSource);
},
// Expose the log levels from the logger object
logLevels: logger.levels
};
}();
// A Guest is a hosted iframe containing a sandboxed script
automation.Guest = function Guest(guestId, host, domElement, params) {
this.id = guestId;
this.params = params;
this.host = host;
this.domElement = domElement;
this.window = domElement.contentWindow;
this.ready = false;
this.capabilities = {};
// Dispatches a message to the window
this.dispatchMessage = function (messageType, messageBody) {
remoting.send(this.window, messageType, messageBody);
};
};
// A Host is an object which exposes automation objects to a guest
automation.Host = function Host(hostId, hostOptions) {
// Counter for the guests
var nextGuestId = 1;
// Set the local variables
this.hostId = hostId;
this.options = {
readyStateCallback: null
};
// Merge in the host options
if (hostOptions) {
for (var o in hostOptions) {
this.options[o] = hostOptions[o];
}
}
// The host's set of guests
var guests = new Map();
// A global set of automation objects which are exposed to the child window
var publishedObjects = new Map();
// Gets a elli.automation object from the host.
this.getObjectById = function (objectId) {
// Check the globals
var obj = publishedObjects.get(objectId);
if (obj) return obj;
return null;
};
// Publishes a global object to be available to the guest window(s)
this.publish = function (globalObject) {
if (!globalObject.id || !globalObject._toJSON) {
throw "Object is not derived from ScriptingObject";
}
publishedObjects.set(globalObject.id, globalObject);
};
// Disposes all published objects, as needed
this.unpublish = function (objectId) {
var obj = this.getObjectById(objectId);
if (obj) {
// Dispose of the object if it supports this capability
if (obj._dispose && typeof obj._dispose == "function") {
obj._dispose();
}
publishedObjects.delete(objectId);
}
};
// Unpublish all published objects
this.unpublishAll = function () {
var _this = this;
var objectIds = Array.from(publishedObjects.keys());
objectIds.forEach(function (objectId) {
_this.unpublish(objectId);
});
};
// Creates a guest of the host window
this.attachGuest = function (guestId, guestDomElement, guestOptions) {
var guest = new automation.Guest(guestId, this, guestDomElement, guestOptions);
// Register the guest with the automation framework
automation.registerGuest(guest);
// Add the guest
guests.set(guestId, guest);
return guest;
};
// Renders a list of guests into sandboxed iframes
// This is the primary method for kicking off the automation framework.
this.renderGuest = function (guestParams, sandboxSrc, targetElement, options) {
var guestId = "guest-" + this.hostId + "-sandbox-" + nextGuestId++;
// Build the querystring for the sanbox page
var querystring = "";
for (var p in guestParams) {
querystring += (querystring.length ? "&" : "") + encodeURIComponent(p) + "=" + encodeURIComponent(guestParams[p]);
}
// Generate the iframe
var targetElementDocument = targetElement.ownerDocument ? targetElement.ownerDocument : document;
var frame = targetElementDocument.createElement("iframe");
frame.setAttribute("id", guestId);
var guestOrigin = ((sandboxSrc || '').match(/^(https?:\/\/[^\/#?&]+)/) || [])[1] || '';
var parentOrigin = (window.location.origin || '').toLowerCase();
if (options && options.excludeSandboxAttribute && parentOrigin === guestOrigin.toLowerCase()) {
logger.info('Creating guest excluding sandbox attribute ...');
} else {
logger.info('Creating guest with sandbox attribute ...');
frame.setAttribute("sandbox", "allow-scripts allow-popups allow-modals allow-forms allow-downloads" + (options && options.allowSameOrigin ? " allow-same-origin" : "") + (options && options.allowPopupsToEscapeSandbox ? " allow-popups-to-escape-sandbox" : ""));
}
if (options && options.permissionPolicy) {
// PUI-16106 - support iframe permission policies
if (typeof options.permissionPolicy == "string") {
frame.setAttribute("allow", options.permissionPolicy);
} else {
frame.setAttribute("allow", (options && options.permissionPolicy.allowClipboardRead ? "clipboard-read;" : "") + (options && options.permissionPolicy.allowClipboardWrite ? " clipboard-write;" : ""));
}
}
frame.setAttribute("title", options && options.title ? options.title : "");
frame.setAttribute("src", sandboxSrc + (sandboxSrc.indexOf("?") >= 0 ? "&" : "?") + querystring);
targetElement.appendChild(frame);
// Find the window object and create the guest
var guestFrame = targetElementDocument.getElementById(guestId);
var guest = this.attachGuest(guestId, guestFrame, guestParams);
logger.info('Created guest window with id = ' + guest.id, logSource);
// Initialize the messenger
remoting.initialize(targetElementDocument.defaultView || targetElementDocument.parentWindow);
return guest;
};
// Returns an array with all of the guests for this host
this.getGuests = function () {
var guestList = [];
guests.forEach(function (guest, guestId) {
guestList.push(guest);
});
return guestList;
};
// Renders a list of guests into sandboxed iframes
// This is the primary method for kicking off the automation framework.
this.renderGuests = function (guestParamsList, sandboxSrc, targetElement, options) {
// Render the plugins into sandboxed iframe elements
guestParamsList.forEach(function (guestParams) {
this.renderGuest(guestParams, sandboxSrc, targetElement, options);
}, this);
};
// Destroys a guest
this.destroyGuest = function (guestIdOrWindow) {
// Find the guest object
var guest = guests.get(guestIdOrWindow);
if (!guest) {
guests.forEach(function (value, key) {
if (value.window === guestIdOrWindow) {
guest = value;
} else if (value.domElement === guestIdOrWindow) {
guest = value;
}
});
}
// If we still don't have a guest, throw an error
if (!guest) {
throw "Invalid guestId or guestWindow reference";
}
// Unregister the guest from the automation framework
automation.unregisterGuest(guest.id);
// Remove the guest element from the DOM
if (guest.domElement.remove) {
guest.domElement.remove();
} else {
guest.domElement.removeNode(true);
}
// Remove from the host's guest list
guests.delete(guest.id);
};
};
// Provides an Event class for use within the automation framework
automation.Event = function Event() {}
// This is just an empty object for now -- reserved for future use
// Provides a base class for all automation objects
;automation.ScriptingObject = function ScriptingObject(objectId) {
// Set the basic properties
this.id = objectId;
this.objectType = 'Object';
// Serializes to a JSON object
this._toJSON = function () {
// Enumerate the list of public functions
var functions = [];
var events = [];
for (var p in this) {
if (typeof this[p] === 'function' && !p.startsWith('_')) {
functions.push(p);
} else if (this[p] instanceof automation.Event) {
events.push(p);
}
}
// Construct the JSON object to serialize
return {
objectId: this.id,
objectType: this.objectType,
functions: functions,
events: events
};
};
};
// Create a Scripting Object from a proxy published by an upstream host.
// This allows an intermediate host to proxy a scripting object published
// by its parent to downstream guests.
automation.ScriptingObject.fromProxy = function (proxy) {
// Create a new, base ScriptingObject
var so = new automation.ScriptingObject(proxy.id);
so.objectType = proxy.objectType;
// Store the tokens for unsubscription
var unsubscribers = [];
// Copy the functions and events over
var _loop = function _loop(p) {
var propName = p;
if (typeof proxy[p] === 'function') {
// Create a function that proxies the call to the proxy
so[p] = function () {
return proxy[propName].apply(proxy, _toConsumableArray([].slice.call(arguments))).then(function (retVal) {
if (retVal && retVal.constructor && (retVal.constructor.name === "Proxy" || retVal.__TYPE__ === "Proxy")) {
return automation.ScriptingObject.fromProxy(retVal);
} else {
return retVal;
}
});
};
// Override the dispose function
if (p == "dispose") {
var defaultImpl = so[p];
so[p] = function () {
// Dispose of the object in the proxy tier
so._dispose();
return defaultImpl();
};
}
} else if (typeof proxy[p].subscribe == 'function') {
// Create the event on the object
so[p] = new automation.Event();
// Subscribe to the event and pass the callback down
var token = proxy[p].subscribe(function (obj, params, options) {
return automation.raiseEvent(obj.id, propName, params, options);
});
unsubscribers.push(function () {
proxy[propName].unsubscribe(token);
});
}
};
for (var p in proxy) {
_loop(p);
}
// Add a destroy method to clean up the proxy by unsubscribing to the
// proxied object's events and npublishing the object
so._dispose = function () {
unsubscribers.forEach(function (unsub) {
typeof unsub == 'function' && unsub();
});
unsubscribers = [];
};
return so;
};
// Export the automation object
module.exports = automation;
/***/ }),
/* 2 */
/***/ (function(module, exports, __webpack_require__) {
;
//
// Provides core messaging capabilities for cross-frame interactions in a sandboxed environment.
// The elli.remoting object is used by both the parent and child windows to pass messages in a
// consistent manner and allow for async invocations with responses.
//
var logger = __webpack_require__(0);
var logSource = "ssf-remoting";
var remoting = function () {
// The next invocation ID
var nextInvocationId = 1;
// Represents the set of active invocations, keyed by the message ID
var invocations = new Map();
// The set of listeners for the messages
var listeners = new Map();
// String constants
var messageSource = "elli:remoting";
var responseMessageType = "elli:remoting:response";
var exceptionMessageType = "elli:remoting:exception";
// Creates the shell of a message
function createMessage(messageType, messageBody) {
return {
source: messageSource,
type: messageType,
body: messageBody
};
}
// The handle of the timeout interval
var timeoutMonitorHandle = null;
// Evaluates the timeouts on any waiting invocations
function evaluateTimeouts() {
var ts = Date.now();
// Make a list of items to remove
var canceledItems = [];
// Check each invocation to see if it's been handled yet
invocations.forEach(function (eventData, key) {
logger.trace("Checking response timeout for request (id = " + eventData.requestId + ") @ " + eventData.cancelTime, logSource);
if (eventData.cancelTime && eventData.cancelTime < ts) {
logger.info("Detected response timeout for request (id = " + eventData.requestId + ")...", logSource);
canceledItems.push(key);
eventData.resolve();
logger.trace("Aborted waiting for response to request (id = " + eventData.requestId + ")", logSource);
}
});
// Remove all elements that were cancelled from the invocations map
canceledItems.forEach(function (key) {
invocations.delete(key);
});
// Stop the response monitor if there are no pending invocations
if (invocations.size == 0) {
stopResponseMonitor();
}
}
// Set a timer interval to catch any invocations that didn't respond in a timely manner
function startResponseMonitor() {
if (timeoutMonitorHandle == null) {
logger.trace("Staring response timeout evaluator", logSource);
timeoutMonitorHandle = window.setInterval(evaluateTimeouts, 200);
}
}
// Stops the timeout monitor interval
function stopResponseMonitor() {
if (timeoutMonitorHandle != null) {
window.clearInterval(timeoutMonitorHandle);
timeoutMonitorHandle = null;
logger.trace("Stopped response timeout evaluator", logSource);
}
}
// Pops an invocation from the incovation list
function popInvocation(requestId) {
var e = invocations.get(requestId);
invocations.delete(requestId);
return e;
}
// Return the public implementation of the object
return {
// Subscribes a listener to a message type
listen: function listen(messageType, callback) {
var items = listeners.get(messageType);
if (!items) items = [];
items.push(callback);
listeners.set(messageType, items);
},
// Sends an invocation which generates a Promise to be used to get a response
invoke: function invoke(targetWin, messageType, messageBody, responseTimeoutMs) {
return new Promise(function (resolve, reject) {
// Construct the message to post
var msg = createMessage(messageType, messageBody);
msg.requestId = nextInvocationId++,
// Register this invocation to connect the callback to the Promise
invocations.set(msg.requestId, {
requestId: msg.requestId,
resolve: resolve,
reject: reject,
cancelTime: responseTimeoutMs ? Date.now() + responseTimeoutMs : null
});
// Post the message to the parent window
targetWin.postMessage(msg, "*");
logger.trace('Posted invocation message of type ' + messageType + ' (requestId = ' + msg.requestId + ')', logSource);
// Start monitoring if needed
if (responseTimeoutMs) startResponseMonitor();
});
},
// Sends a message without any form of response
send: function send(targetWin, messageType, messageBody) {
// Construct the message to post
var msg = createMessage(messageType, messageBody);
// Post the message to the parent window
targetWin.postMessage(msg, "*");
logger.trace('Posted one-way message of type "' + messageType + '"', logSource);
},
// Receives a message from another window and invokes any event handlers
receive: function receive(sourceWin, message) {
logger.trace('Received message of type "' + message.type + '"', logSource);
// Find listeners for this message type
var callbacks = listeners.get(message.type);
if (!callbacks) return false;
callbacks.forEach(function (callback) {
logger.trace('Invoking message handler ' + callback.name + '...', logSource);
callback(sourceWin, message.requestId, message.type, message.body);
});
return true;
},
// Handles a response to a prior cross-frame invocation
processResponse: function processResponse(message) {
logger.trace('Response received for invocation (requestId = ' + message.requestId + ')', logSource);
// Retrieve and remove the promise callback values for this invocation
var eventData = popInvocation(message.requestId);
if (!eventData) {
logger.warn('Received response to stale/invalid request (requestId = ' + message.requestId + ')', logSource);
return false;
}
eventData.resolve(message.body);
},
// Handles a response to a prior cross-frame invocation
processException: function processException(message) {
logger.trace('Exception received for invocation (requestId = ' + message.requestId + ')', logSource);
// Retrieve and remove the promise callback values for this invocation
var eventData = popInvocation(message.requestId);
if (!eventData) {
logger.warn('Received exception for stale/invalid request (requestId = ' + message.requestId + ')', logSource);
return false;
}
eventData.reject(new Error(message.body));
},
// Sends a response message to a child frame
respond: function respond(targetWin, requestId, response) {
// Formulate the response message
var msg = createMessage(responseMessageType, response);
msg.requestId = requestId,
// Send to the target window
targetWin.postMessage(msg, "*");
logger.trace('Response sent to caller for invocation (requestId = ' + requestId + ')', logSource);
},
// Sends a response message to a child frame
raiseException: function raiseException(targetWin, requestId, ex) {
// Formulate the response message
var msg = createMessage(exceptionMessageType, ex);
msg.requestId = requestId,
// Send to the target window
targetWin.postMessage(msg, "*");
logger.trace('Exception sent to caller for invocation (requestId = ' + requestId + ')', logSource);
},
// Processes a message received thru the window's message event
processMessage: function processMessage(message) {
if (message.data && message.data.source && message.data.source == messageSource) {
if (message.data.type == responseMessageType) remoting.processResponse(message.data);else if (message.data.type == exceptionMessageType) remoting.processException(message.data);else remoting.receive(message.source, message.data);
return true;
}
return false;
},
// Updates the log source
setLogSource: function setLogSource(source) {
logSource = source;
},
// Initialize the messenger to listen to message from the given window
initialize: function initialize(win) {
win.removeEventListener("message", remoting.processMessage);
win.addEventListener("message", remoting.processMessage);
}
};
}();
// Return the remoting object
module.exports = remoting;
/***/ })
/******/ ]);
});