postmate
Version:
A powerful, simple, promise-based postMessage library
492 lines (398 loc) • 13.3 kB
JavaScript
/**
postmate - A powerful, simple, promise-based postMessage library
@version v1.5.2
@link https://github.com/dollarshaveclub/postmate
@author Jacob Kelley <jakie8@gmail.com>
@license MIT
**/
'use strict';
/**
* The type of messages our frames our sending
* @type {String}
*/
var messageType = 'application/x-postmate-v1+json';
/**
* The maximum number of attempts to send a handshake request to the parent
* @type {Number}
*/
var maxHandshakeRequests = 5;
/**
* A unique message ID that is used to ensure responses are sent to the correct requests
* @type {Number}
*/
var _messageId = 0;
/**
* Increments and returns a message ID
* @return {Number} A unique ID for a message
*/
var generateNewMessageId = function generateNewMessageId() {
return ++_messageId;
};
/**
* Postmate logging function that enables/disables via config
* @param {Object} ...args Rest Arguments
*/
var log = function log() {
var _console;
return Postmate.debug ? (_console = console).log.apply(_console, arguments) : null;
}; // eslint-disable-line no-console
/**
* Takes a URL and returns the origin
* @param {String} url The full URL being requested
* @return {String} The URLs origin
*/
var resolveOrigin = function resolveOrigin(url) {
var a = document.createElement('a');
a.href = url;
var protocol = a.protocol.length > 4 ? a.protocol : window.location.protocol;
var host = a.host.length ? a.port === '80' || a.port === '443' ? a.hostname : a.host : window.location.host;
return a.origin || protocol + "//" + host;
};
var messageTypes = {
handshake: 1,
'handshake-reply': 1,
call: 1,
emit: 1,
reply: 1,
request: 1
/**
* Ensures that a message is safe to interpret
* @param {Object} message The postmate message being sent
* @param {String|Boolean} allowedOrigin The whitelisted origin or false to skip origin check
* @return {Boolean}
*/
};
var sanitize = function sanitize(message, allowedOrigin) {
if (typeof allowedOrigin === 'string' && message.origin !== allowedOrigin) return false;
if (!message.data) return false;
if (typeof message.data === 'object' && !('postmate' in message.data)) return false;
if (message.data.type !== messageType) return false;
if (!messageTypes[message.data.postmate]) return false;
return true;
};
/**
* Takes a model, and searches for a value by the property
* @param {Object} model The dictionary to search against
* @param {String} property A path within a dictionary (i.e. 'window.location.href')
* @param {Object} data Additional information from the get request that is
* passed to functions in the child model
* @return {Promise}
*/
var resolveValue = function resolveValue(model, property) {
var unwrappedContext = typeof model[property] === 'function' ? model[property]() : model[property];
return Postmate.Promise.resolve(unwrappedContext);
};
/**
* Composes an API to be used by the parent
* @param {Object} info Information on the consumer
*/
var ParentAPI =
/*#__PURE__*/
function () {
function ParentAPI(info) {
var _this = this;
this.parent = info.parent;
this.frame = info.frame;
this.child = info.child;
this.childOrigin = info.childOrigin;
this.events = {};
if (process.env.NODE_ENV !== 'production') {
log('Parent: Registering API');
log('Parent: Awaiting messages...');
}
this.listener = function (e) {
if (!sanitize(e, _this.childOrigin)) return false;
/**
* the assignments below ensures that e, data, and value are all defined
*/
var _ref = ((e || {}).data || {}).value || {},
data = _ref.data,
name = _ref.name;
if (e.data.postmate === 'emit') {
if (process.env.NODE_ENV !== 'production') {
log("Parent: Received event emission: " + name);
}
if (name in _this.events) {
_this.events[name].call(_this, data);
}
}
};
this.parent.addEventListener('message', this.listener, false);
if (process.env.NODE_ENV !== 'production') {
log('Parent: Awaiting event emissions from Child');
}
}
var _proto = ParentAPI.prototype;
_proto.get = function get(property) {
var _this2 = this;
return new Postmate.Promise(function (resolve) {
// Extract data from response and kill listeners
var uid = generateNewMessageId();
var transact = function transact(e) {
if (e.data.uid === uid && e.data.postmate === 'reply') {
_this2.parent.removeEventListener('message', transact, false);
resolve(e.data.value);
}
}; // Prepare for response from Child...
_this2.parent.addEventListener('message', transact, false); // Then ask child for information
_this2.child.postMessage({
postmate: 'request',
type: messageType,
property: property,
uid: uid
}, _this2.childOrigin);
});
};
_proto.call = function call(property, data) {
// Send information to the child
this.child.postMessage({
postmate: 'call',
type: messageType,
property: property,
data: data
}, this.childOrigin);
};
_proto.on = function on(eventName, callback) {
this.events[eventName] = callback;
};
_proto.destroy = function destroy() {
if (process.env.NODE_ENV !== 'production') {
log('Parent: Destroying Postmate instance');
}
window.removeEventListener('message', this.listener, false);
this.frame.parentNode.removeChild(this.frame);
};
return ParentAPI;
}();
/**
* Composes an API to be used by the child
* @param {Object} info Information on the consumer
*/
var ChildAPI =
/*#__PURE__*/
function () {
function ChildAPI(info) {
var _this3 = this;
this.model = info.model;
this.parent = info.parent;
this.parentOrigin = info.parentOrigin;
this.child = info.child;
if (process.env.NODE_ENV !== 'production') {
log('Child: Registering API');
log('Child: Awaiting messages...');
}
this.child.addEventListener('message', function (e) {
if (!sanitize(e, _this3.parentOrigin)) return;
if (process.env.NODE_ENV !== 'production') {
log('Child: Received request', e.data);
}
var _e$data = e.data,
property = _e$data.property,
uid = _e$data.uid,
data = _e$data.data;
if (e.data.postmate === 'call') {
if (property in _this3.model && typeof _this3.model[property] === 'function') {
_this3.model[property](data);
}
return;
} // Reply to Parent
resolveValue(_this3.model, property).then(function (value) {
return e.source.postMessage({
property: property,
postmate: 'reply',
type: messageType,
uid: uid,
value: value
}, e.origin);
});
});
}
var _proto2 = ChildAPI.prototype;
_proto2.emit = function emit(name, data) {
if (process.env.NODE_ENV !== 'production') {
log("Child: Emitting Event \"" + name + "\"", data);
}
this.parent.postMessage({
postmate: 'emit',
type: messageType,
value: {
name: name,
data: data
}
}, this.parentOrigin);
};
return ChildAPI;
}();
/**
* The entry point of the Parent.
* @type {Class}
*/
var Postmate =
/*#__PURE__*/
function () {
// eslint-disable-line no-undef
// Internet Explorer craps itself
/**
* Sets options related to the Parent
* @param {Object} object The element to inject the frame into, and the url
* @return {Promise}
*/
function Postmate(_ref2) {
var _ref2$container = _ref2.container,
container = _ref2$container === void 0 ? typeof container !== 'undefined' ? container : document.body : _ref2$container,
model = _ref2.model,
url = _ref2.url,
name = _ref2.name,
_ref2$classListArray = _ref2.classListArray,
classListArray = _ref2$classListArray === void 0 ? [] : _ref2$classListArray;
// eslint-disable-line no-undef
this.parent = window;
this.frame = document.createElement('iframe');
this.frame.name = name || '';
this.frame.classList.add.apply(this.frame.classList, classListArray);
container.appendChild(this.frame);
this.child = this.frame.contentWindow || this.frame.contentDocument.parentWindow;
this.model = model || {};
return this.sendHandshake(url);
}
/**
* Begins the handshake strategy
* @param {String} url The URL to send a handshake request to
* @return {Promise} Promise that resolves when the handshake is complete
*/
var _proto3 = Postmate.prototype;
_proto3.sendHandshake = function sendHandshake(url) {
var _this4 = this;
var childOrigin = resolveOrigin(url);
var attempt = 0;
var responseInterval;
return new Postmate.Promise(function (resolve, reject) {
var reply = function reply(e) {
if (!sanitize(e, childOrigin)) return false;
if (e.data.postmate === 'handshake-reply') {
clearInterval(responseInterval);
if (process.env.NODE_ENV !== 'production') {
log('Parent: Received handshake reply from Child');
}
_this4.parent.removeEventListener('message', reply, false);
_this4.childOrigin = e.origin;
if (process.env.NODE_ENV !== 'production') {
log('Parent: Saving Child origin', _this4.childOrigin);
}
return resolve(new ParentAPI(_this4));
} // Might need to remove since parent might be receiving different messages
// from different hosts
if (process.env.NODE_ENV !== 'production') {
log('Parent: Invalid handshake reply');
}
return reject('Failed handshake');
};
_this4.parent.addEventListener('message', reply, false);
var doSend = function doSend() {
attempt++;
if (process.env.NODE_ENV !== 'production') {
log("Parent: Sending handshake attempt " + attempt, {
childOrigin: childOrigin
});
}
_this4.child.postMessage({
postmate: 'handshake',
type: messageType,
model: _this4.model
}, childOrigin);
if (attempt === maxHandshakeRequests) {
clearInterval(responseInterval);
}
};
var loaded = function loaded() {
doSend();
responseInterval = setInterval(doSend, 500);
};
if (_this4.frame.attachEvent) {
_this4.frame.attachEvent('onload', loaded);
} else {
_this4.frame.onload = loaded;
}
if (process.env.NODE_ENV !== 'production') {
log('Parent: Loading frame', {
url: url
});
}
_this4.frame.src = url;
});
};
return Postmate;
}();
/**
* The entry point of the Child
* @type {Class}
*/
Postmate.debug = false;
Postmate.Promise = function () {
try {
return window ? window.Promise : Promise;
} catch (e) {
return null;
}
}();
Postmate.Model =
/*#__PURE__*/
function () {
/**
* Initializes the child, model, parent, and responds to the Parents handshake
* @param {Object} model Hash of values, functions, or promises
* @return {Promise} The Promise that resolves when the handshake has been received
*/
function Model(model) {
this.child = window;
this.model = model;
this.parent = this.child.parent;
return this.sendHandshakeReply();
}
/**
* Responds to a handshake initiated by the Parent
* @return {Promise} Resolves an object that exposes an API for the Child
*/
var _proto4 = Model.prototype;
_proto4.sendHandshakeReply = function sendHandshakeReply() {
var _this5 = this;
return new Postmate.Promise(function (resolve, reject) {
var shake = function shake(e) {
if (!e.data.postmate) {
return;
}
if (e.data.postmate === 'handshake') {
if (process.env.NODE_ENV !== 'production') {
log('Child: Received handshake from Parent');
}
_this5.child.removeEventListener('message', shake, false);
if (process.env.NODE_ENV !== 'production') {
log('Child: Sending handshake reply to Parent');
}
e.source.postMessage({
postmate: 'handshake-reply',
type: messageType
}, e.origin);
_this5.parentOrigin = e.origin; // Extend model with the one provided by the parent
var defaults = e.data.model;
if (defaults) {
Object.keys(defaults).forEach(function (key) {
_this5.model[key] = defaults[key];
});
if (process.env.NODE_ENV !== 'production') {
log('Child: Inherited and extended model from Parent');
}
}
if (process.env.NODE_ENV !== 'production') {
log('Child: Saving Parent origin', _this5.parentOrigin);
}
return resolve(new ChildAPI(_this5));
}
return reject('Handshake Reply Failed');
};
_this5.child.addEventListener('message', shake, false);
});
};
return Model;
}();
module.exports = Postmate;