UNPKG

cross-storage

Version:
456 lines (387 loc) 14.6 kB
;(function(root) { /** * Constructs a new cross storage client given the url to a hub. By default, * an iframe is created within the document body that points to the url. It * also accepts an options object, which may include a timeout, frameId, and * promise. The timeout, in milliseconds, is applied to each request and * defaults to 5000ms. The options object may also include a frameId, * identifying an existing frame on which to install its listeners. If the * promise key is supplied the constructor for a Promise, that Promise library * will be used instead of the default window.Promise. * * @example * var storage = new CrossStorageClient('https://store.example.com/hub.html'); * * @example * var storage = new CrossStorageClient('https://store.example.com/hub.html', { * timeout: 5000, * frameId: 'storageFrame' * }); * * @constructor * * @param {string} url The url to a cross storage hub * @param {object} [opts] An optional object containing additional options, * including timeout, frameId, and promise * * @property {string} _id A UUID v4 id * @property {function} _promise The Promise object to use * @property {string} _frameId The id of the iFrame pointing to the hub url * @property {string} _origin The hub's origin * @property {object} _requests Mapping of request ids to callbacks * @property {bool} _connected Whether or not it has connected * @property {bool} _closed Whether or not the client has closed * @property {int} _count Number of requests sent * @property {function} _listener The listener added to the window * @property {Window} _hub The hub window */ function CrossStorageClient(url, opts) { opts = opts || {}; this._id = CrossStorageClient._generateUUID(); this._promise = opts.promise || Promise; this._frameId = opts.frameId || 'CrossStorageClient-' + this._id; this._origin = CrossStorageClient._getOrigin(url); this._requests = {}; this._connected = false; this._closed = false; this._count = 0; this._timeout = opts.timeout || 5000; this._listener = null; this._installListener(); var frame; if (opts.frameId) { frame = document.getElementById(opts.frameId); } // If using a passed iframe, poll the hub for a ready message if (frame) { this._poll(); } // Create the frame if not found or specified frame = frame || this._createFrame(url); this._hub = frame.contentWindow; } /** * The styles to be applied to the generated iFrame. Defines a set of properties * that hide the element by positioning it outside of the visible area, and * by modifying its display. * * @member {Object} */ CrossStorageClient.frameStyle = { display: 'none', position: 'absolute', top: '-999px', left: '-999px' }; /** * Returns the origin of an url, with cross browser support. Accommodates * the lack of location.origin in IE, as well as the discrepancies in the * inclusion of the port when using the default port for a protocol, e.g. * 443 over https. Defaults to the origin of window.location if passed a * relative path. * * @param {string} url The url to a cross storage hub * @returns {string} The origin of the url */ CrossStorageClient._getOrigin = function(url) { var uri, protocol, origin; uri = document.createElement('a'); uri.href = url; if (!uri.host) { uri = window.location; } if (!uri.protocol || uri.protocol === ':') { protocol = window.location.protocol; } else { protocol = uri.protocol; } origin = protocol + '//' + uri.host; origin = origin.replace(/:80$|:443$/, ''); return origin; }; /** * UUID v4 generation, taken from: http://stackoverflow.com/questions/ * 105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 * * @returns {string} A UUID v4 string */ CrossStorageClient._generateUUID = function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8); return v.toString(16); }); }; /** * Returns a promise that is fulfilled when a connection has been established * with the cross storage hub. Its use is required to avoid sending any * requests prior to initialization being complete. * * @returns {Promise} A promise that is resolved on connect */ CrossStorageClient.prototype.onConnect = function() { var client = this; if (this._connected) { return this._promise.resolve(); } else if (this._closed) { return this._promise.reject(new Error('CrossStorageClient has closed')); } // Queue connect requests for client re-use if (!this._requests.connect) { this._requests.connect = []; } return new this._promise(function(resolve, reject) { var timeout = setTimeout(function() { reject(new Error('CrossStorageClient could not connect')); }, client._timeout); client._requests.connect.push(function(err) { clearTimeout(timeout); if (err) return reject(err); resolve(); }); }); }; /** * Sets a key to the specified value. Returns a promise that is fulfilled on * success, or rejected if any errors setting the key occurred, or the request * timed out. * * @param {string} key The key to set * @param {*} value The value to assign * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype.set = function(key, value) { return this._request('set', { key: key, value: value }); }; /** * Accepts one or more keys for which to retrieve their values. Returns a * promise that is settled on hub response or timeout. On success, it is * fulfilled with the value of the key if only passed a single argument. * Otherwise it's resolved with an array of values. On failure, it is rejected * with the corresponding error message. * * @param {...string} key The key to retrieve * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype.get = function(key) { var args = Array.prototype.slice.call(arguments); return this._request('get', {keys: args}); }; /** * Accepts one or more keys for deletion. Returns a promise that is settled on * hub response or timeout. * * @param {...string} key The key to delete * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype.del = function() { var args = Array.prototype.slice.call(arguments); return this._request('del', {keys: args}); }; /** * Returns a promise that, when resolved, indicates that all localStorage * data has been cleared. * * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype.clear = function() { return this._request('clear'); }; /** * Returns a promise that, when resolved, passes an array of all keys * currently in storage. * * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype.getKeys = function() { return this._request('getKeys'); }; /** * Deletes the iframe and sets the connected state to false. The client can * no longer be used after being invoked. */ CrossStorageClient.prototype.close = function() { var frame = document.getElementById(this._frameId); if (frame) { frame.parentNode.removeChild(frame); } // Support IE8 with detachEvent if (window.removeEventListener) { window.removeEventListener('message', this._listener, false); } else { window.detachEvent('onmessage', this._listener); } this._connected = false; this._closed = true; }; /** * Installs the necessary listener for the window message event. When a message * is received, the client's _connected status is changed to true, and the * onConnect promise is fulfilled. Given a response message, the callback * corresponding to its request is invoked. If response.error holds a truthy * value, the promise associated with the original request is rejected with * the error. Otherwise the promise is fulfilled and passed response.result. * * @private */ CrossStorageClient.prototype._installListener = function() { var client = this; this._listener = function(message) { var i, origin, error, response; // Ignore invalid messages or those after the client has closed if (client._closed || !message.data || typeof message.data !== 'string') { return; } // postMessage returns the string "null" as the origin for "file://" origin = (message.origin === 'null') ? 'file://' : message.origin; // Ignore messages not from the correct origin if (origin !== client._origin) return; // LocalStorage isn't available in the hub if (message.data === 'cross-storage:unavailable') { if (!client._closed) client.close(); if (!client._requests.connect) return; error = new Error('Closing client. Could not access localStorage in hub.'); for (i = 0; i < client._requests.connect.length; i++) { client._requests.connect[i](error); } return; } // Handle initial connection if (message.data.indexOf('cross-storage:') !== -1 && !client._connected) { client._connected = true; if (!client._requests.connect) return; for (i = 0; i < client._requests.connect.length; i++) { client._requests.connect[i](error); } delete client._requests.connect; } if (message.data === 'cross-storage:ready') return; // All other messages try { response = JSON.parse(message.data); } catch(e) { return; } if (!response.id) return; if (client._requests[response.id]) { client._requests[response.id](response.error, response.result); } }; // Support IE8 with attachEvent if (window.addEventListener) { window.addEventListener('message', this._listener, false); } else { window.attachEvent('onmessage', this._listener); } }; /** * Invoked when a frame id was passed to the client, rather than allowing * the client to create its own iframe. Polls the hub for a ready event to * establish a connected state. */ CrossStorageClient.prototype._poll = function() { var client, interval, targetOrigin; client = this; // postMessage requires that the target origin be set to "*" for "file://" targetOrigin = (client._origin === 'file://') ? '*' : client._origin; interval = setInterval(function() { if (client._connected) return clearInterval(interval); if (!client._hub) return; client._hub.postMessage('cross-storage:poll', targetOrigin); }, 1000); }; /** * Creates a new iFrame containing the hub. Applies the necessary styles to * hide the element from view, prior to adding it to the document body. * Returns the created element. * * @private * * @param {string} url The url to the hub * returns {HTMLIFrameElement} The iFrame element itself */ CrossStorageClient.prototype._createFrame = function(url) { var frame, key; frame = window.document.createElement('iframe'); frame.id = this._frameId; // Style the iframe for (key in CrossStorageClient.frameStyle) { if (CrossStorageClient.frameStyle.hasOwnProperty(key)) { frame.style[key] = CrossStorageClient.frameStyle[key]; } } window.document.body.appendChild(frame); frame.src = url; return frame; }; /** * Sends a message containing the given method and params to the hub. Stores * a callback in the _requests object for later invocation on message, or * deletion on timeout. Returns a promise that is settled in either instance. * * @private * * @param {string} method The method to invoke * @param {*} params The arguments to pass * @returns {Promise} A promise that is settled on hub response or timeout */ CrossStorageClient.prototype._request = function(method, params) { var req, client; if (this._closed) { return this._promise.reject(new Error('CrossStorageClient has closed')); } client = this; client._count++; req = { id: this._id + ':' + client._count, method: 'cross-storage:' + method, params: params }; return new this._promise(function(resolve, reject) { var timeout, originalToJSON, targetOrigin; // Timeout if a response isn't received after 4s timeout = setTimeout(function() { if (!client._requests[req.id]) return; delete client._requests[req.id]; reject(new Error('Timeout: could not perform ' + req.method)); }, client._timeout); // Add request callback client._requests[req.id] = function(err, result) { clearTimeout(timeout); delete client._requests[req.id]; if (err) return reject(new Error(err)); resolve(result); }; // In case we have a broken Array.prototype.toJSON, e.g. because of // old versions of prototype if (Array.prototype.toJSON) { originalToJSON = Array.prototype.toJSON; Array.prototype.toJSON = null; } // postMessage requires that the target origin be set to "*" for "file://" targetOrigin = (client._origin === 'file://') ? '*' : client._origin; // Send serialized message client._hub.postMessage(JSON.stringify(req), targetOrigin); // Restore original toJSON if (originalToJSON) { Array.prototype.toJSON = originalToJSON; } }); }; /** * Export for various environments. */ if (typeof module !== 'undefined' && module.exports) { module.exports = CrossStorageClient; } else if (typeof exports !== 'undefined') { exports.CrossStorageClient = CrossStorageClient; } else if (typeof define === 'function' && define.amd) { define([], function() { return CrossStorageClient; }); } else { root.CrossStorageClient = CrossStorageClient; } }(this));