UNPKG

pipe.js

Version:

The client-side library which is used in BigPipe to orchestrate the pagelets.

1,962 lines (1,671 loc) 87.2 kB
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.BigPipe=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);throw new Error("Cannot find module '"+o+"'")}var f=n[o]={exports:{}};t[o][0].call(f.exports,function(e){var n=t[o][1][e];return s(n?n:e)},f,f.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){ 'use strict'; var collection = _dereq_('./collection'); // // Pointless function that will replace callbacks once they are executed to // prevent double execution from ever happening. // function noop() { /* you waste your time by reading this, see, i told you.. */ } /** * Asynchronously iterate over the given data. * * @param {Mixed} data The data we need to iterate over * @param {Function} iterator Function that's called for each item. * @param {Function} fn The completion callback * @param {Object} options Async options. * @api public */ exports.each = function each(data, iterator, fn, options) { options = options || {}; var size = collection.size(data) , completed = 0 , timeout; if (!size) return fn(); collection.each(data, function iterating(item) { iterator.call(options.context, item, function done(err) { if (err) { fn(err); return fn = noop; } if (++completed === size) { fn(); if (timeout) clearTimeout(timeout); return fn = noop; } }); }); // // Optional timeout for when the operation takes to long. // if (options.timeout) timeout = setTimeout(function kill() { fn(new Error('Operation timed out')); fn = noop; }, options.timeout); }; },{"./collection":2}],2:[function(_dereq_,module,exports){ 'use strict'; var hasOwn = Object.prototype.hasOwnProperty , undef; /** * Get an accurate type check for the given Object. * * @param {Mixed} obj The object that needs to be detected. * @returns {String} The object type. * @api public */ function type(obj) { return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase(); } /** * Iterate over a collection. * * @param {Mixed} collection The object we want to iterate over. * @param {Function} iterator The function that's called for each iteration. * @param {Mixed} context The context of the function. * @api public */ function each(collection, iterator, context) { var i = 0; if ('array' === type(collection)) { for (; i < collection.length; i++) { iterator.call(context || iterator, collection[i], i, collection); } } else { for (i in collection) { if (hasOwn.call(collection, i)) { iterator.call(context || iterator, collection[i], i, collection); } } } } /** * Checks if the given object is empty. The only edge case here would be * objects. Most object's have a `length` attribute that indicate if there's * anything inside the object. * * @param {Mixed} collection The collection that needs to be checked. * @returns {Boolean} * @api public */ function empty(obj) { if (undef === obj) return false; return size(obj) === 0; } /** * Determine the size of a collection. * * @param {Mixed} collection The object we want to know the size of. * @returns {Number} The size of the collection. * @api public */ function size(collection) { var x, i = 0; if ('object' === type(collection)) { for (x in collection) i++; return i; } return +collection.length; } /** * Wrap the given object in an array if it's not an array already. * * @param {Mixed} obj The thing we might need to wrap. * @returns {Array} We promise! * @api public */ function array(obj) { if ('array' === type(obj)) return obj; if ('arguments' === type(obj)) return Array.prototype.slice.call(obj, 0); return obj // Only transform objects in to an array when they exist. ? [obj] : []; } /** * Find the index of an item in the given array. * * @param {Array} arr The array we search in * @param {Mixed} o The object/thing we search for. * @returns {Number} Index of the thing. * @api public */ function index(arr, o) { for ( var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; i < j && arr[i] !== o; i++ ); return j <= i ? -1 : i; } /** * Merge all given objects in to one objects. * * @returns {Object} * @api public */ function copy() { var result = {} , depth = 2 , seen = []; (function worker() { each(array(arguments), function each(obj) { for (var prop in obj) { if (hasOwn.call(obj, prop) && !~index(seen, obj[prop])) { if (type(obj[prop]) !== 'object' || !depth) { result[prop] = obj[prop]; seen.push(obj[prop]); } else { depth--; worker(result[prop], obj[prop]); } } } }); }).apply(null, arguments); return result; } // // Expose the collection utilities. // exports.array = array; exports.empty = empty; exports.index = index; exports.copy = copy; exports.size = size; exports.type = type; exports.each = each; },{}],3:[function(_dereq_,module,exports){ /*globals Primus */ 'use strict'; var EventEmitter = _dereq_('eventemitter3') , collection = _dereq_('./collection') , Pagelet = _dereq_('./pagelet'); /** * Pipe is the client-side library which is automatically added to pages which * uses the BigPipe framework. It assumes that this library is bundled with * a Primus instance which uses the `substream` plugin. * * Options: * * - limit: The amount pagelet instances we can reuse. * - pagelets: The amount of pagelets we're expecting to load. * - id: The id of the page that we're loading. * * @constructor * @param {String} server The server address we need to connect to. * @param {Object} options Pipe configuration. * @api public */ function Pipe(server, options) { if (!(this instanceof Pipe)) return new Pipe(server, options); if ('object' === typeof server) { options = server; server = undefined; } options = options || {}; this.expected = +options.pagelets || 0; // Pagelets that this page requires. this.maximum = options.limit || 20; // Max Pagelet instances we can reuse. this.options = options; // Reference to the used options. this.server = server; // The server address we connect to. this.templates = {}; // Collection of templates. this.stream = null; // Reference to the connected Primus socket. this.pagelets = []; // Collection of different pagelets. this.freelist = []; // Collection of unused Pagelet instances. this.rendered = []; // List of already rendered pagelets. this.assets = {}; // Asset cache. this.root = document.documentElement; // The <html> element. EventEmitter.call(this); this.configure(options); this.visit(location.pathname, options.id); } // // Inherit from EventEmitter3, use old school inheritance because that's the way // we roll. Oh and it works in every browser. // Pipe.prototype = new EventEmitter(); Pipe.prototype.constructor = Pipe; /** * Configure the Pipe. * * @param {Object} options Configuration. * @return {Pipe} * @api private */ Pipe.prototype.configure = function configure(options) { var root = this.root , className = (root.className || '').replace(/no[_-]js\s?/, ''); // // Add a loading className so we can style the page accordingly and add all // classNames back to the root element. // className = className.length ? className.split(' ') : []; if (!~className.indexOf('pagelets-loading')) { className.push('pagelets-loading'); } root.className = className.join(' '); return this; }; /** * Horrible hack, but needed to prevent memory leaks caused by * `document.createDocumentFragment()` while maintaining sublime performance. * * @type {Number} * @private */ Pipe.prototype.IEV = document.documentMode || +(/MSIE.(\d+)/.exec(navigator.userAgent) || [])[1]; /** * A new Pagelet is flushed by the server. We should register it and update the * content. * * @param {String} name The name of the pagelet. * @param {Object} data Pagelet data. * @returns {Pipe} * @api public */ Pipe.prototype.arrive = function arrive(name, data) { data = data || {}; var pipe = this , root = pipe.root , className = (root.className || '').split(' '); // // Create child pagelet after parent has finished rendering. // if (!pipe.has(name)) { if (data.parent && !~pipe.rendered.indexOf(data.parent)) { pipe.once(data.parent +':render', function render() { pipe.create(name, data, pipe.get(data.parent).placeholders); }); } else { pipe.create(name, data); } } if (data.processed !== pipe.expected) return pipe; if (~className.indexOf('pagelets-loading')) { className.splice(className.indexOf('pagelets-loading'), 1); } root.className = className.join(' '); pipe.emit('loaded'); return this; }; /** * Create a new Pagelet instance. * * @param {String} name The name of the pagelet. * @param {Object} data Data for the pagelet. * @param {Array} roots Root elements we can search can search for. * @returns {Pipe} * @api private */ Pipe.prototype.create = function create(name, data, roots) { data = data || {}; var pipe = this , pagelet = pipe.alloc() , nr = data.processed || 0; pipe.pagelets.push(pagelet); pagelet.configure(name, data, roots); // // A new pagelet has been loaded, emit a progress event. // pipe.emit('progress', Math.round((nr / pipe.expected) * 100), nr, pagelet); pipe.emit('create', pagelet); }; /** * Check if the pagelet has already been loaded. * * @param {String} name The name of the pagelet. * @returns {Boolean} * @api public */ Pipe.prototype.has = function has(name) { return !!this.get(name); }; /** * Get a pagelet that has already been loaded. * * @param {String} name The name of the pagelet. * @param {String} parent Optional name of the parent. * @returns {Pagelet|undefined} The found pagelet. * @api public */ Pipe.prototype.get = function get(name, parent) { var found; collection.each(this.pagelets, function each(pagelet) { if (name === pagelet.name) { found = !parent || pagelet.parent && parent === pagelet.parent.name ? pagelet : found; } return !found; }); return found; }; /** * Remove the pagelet. * * @param {String} name The name of the pagelet that needs to be removed. * @returns {Pipe} * @api public */ Pipe.prototype.remove = function remove(name) { var pagelet = this.get(name) , index = collection.index(this.pagelets, pagelet); if (~index && pagelet) { this.emit('remove', pagelet); this.pagelets.splice(index, 1); pagelet.destroy(); } return this; }; /** * Broadcast an event to all connected pagelets. * * @param {String} event The event that needs to be broadcasted. * @returns {Pipe} * @api public */ Pipe.prototype.broadcast = function broadcast(event) { var args = arguments; collection.each(this.pagelets, function each(pagelet) { EventEmitter.prototype.emit.apply(pagelet, args); }); return this; }; /** * Allocate a new Pagelet instance, retrieve it from our pagelet cache if we * have free pagelets available in order to reduce garbage collection. * * @returns {Pagelet} * @api private */ Pipe.prototype.alloc = function alloc() { return this.freelist.length ? this.freelist.shift() : new Pagelet(this); }; /** * Free an allocated Pagelet instance which can be re-used again to reduce * garbage collection. * * @param {Pagelet} pagelet The pagelet instance. * @returns {Boolean} * @api private */ Pipe.prototype.free = function free(pagelet) { if (this.freelist.length < this.maximum) { this.freelist.push(pagelet); return true; } return false; }; /** * Register a new URL that we've joined. * * @param {String} url The current URL. * @param {String} id The id of the Page that rendered this page. * @api public */ Pipe.prototype.visit = function visit(url, id) { this.id = id || this.id; // Unique ID of the page. this.url = url; // Location of the page. if (!this.orchestrate) return this.connect(); this.orchestrate.write({ url: this.url, type: 'page', id: this.id }); return this; }; /** * Setup a real-time connection to the pagelet server. * * @param {String} url The server address. * @param {Object} options The Primus configuration. * @returns {Pipe} * @api private */ Pipe.prototype.connect = function connect(url, options) { options = options || {}; options.manual = true; var primus = this.stream = new Primus(url, options) , pipe = this; this.orchestrate = primus.substream('pipe:orchestrate'); /** * Upgrade the connection with URL information about the current page. * * @param {Object} options The connection options. * @api private */ primus.on('outgoing::url', function url(options) { var querystring = primus.querystring(options.query || ''); querystring._bp_pid = pipe.id; querystring._bp_url = pipe.url; options.query = primus.querystringify(querystring); }); // // We forced manual opening of the connection so we can listen to the correct // event as it will be executed directly after the `.open` call. // primus.open(); return this; }; // // Expose the pipe // module.exports = Pipe; },{"./collection":2,"./pagelet":13,"eventemitter3":6}],4:[function(_dereq_,module,exports){ 'use strict'; /** * Representation of one single file that will be loaded. * * @constructor * @param {String} url The file URL. * @param {Function} fn Optional callback. * @api private */ function File(url, fn) { if (!(this instanceof File)) return new File(url, fn); this.readyState = File.LOADING; this.start = +new Date(); this.callbacks = []; this.cleanup = []; this.url = url; if ('function' === typeof fn) { this.callbacks.push(fn); } } // // The different readyStates for our File class. // File.DEAD = -1; File.LOADING = 0; File.LOADED = 1; /** * Added cleanup hook. * * @param {Function} fn Clean up callback * @api public */ File.prototype.unload = function unload(fn) { this.cleanup.push(fn); return this; }; /** * Execute the callbacks. * * @param {Error} err Optional error. * @api public */ File.prototype.exec = function exec(err) { this.readyState = File.LOADED; if (!this.callbacks.length) return this; for (var i = 0; i < this.callbacks.length; i++) { this.callbacks[i].apply(this.callbacks[i], arguments); } this.callbacks.length = 0; if (err) this.destroy(); return this; }; /** * Destroy the file. * * @api public */ File.prototype.destroy = function destroy() { this.exec(new Error('Resource has been destroyed before it was loaded')); if (this.cleanup.length) for (var i = 0; i < this.cleanup.length; i++) { this.cleanup[i](); } this.readyState = File.DEAD; this.cleanup.length = 0; return this; }; /** * Asynchronously load JavaScript and Stylesheets. * * Options: * * - document: Document where elements should be created from. * - prefix: Prefix for the id that we use to poll for stylesheet completion. * - timeout: Load timeout. * - onload: Stylesheet onload supported. * * @constructor * @param {HTMLElement} root The root element we should append to. * @param {Object} options Configuration. * @api public */ function AsyncAsset(root, options) { if (!(this instanceof AsyncAsset)) return new AsyncAsset(root, options); options = options || {}; this.document = 'document' in options ? options.document : document; this.prefix = 'prefix' in options ? options.prefix : 'pagelet_'; this.timeout = 'timeout' in options ? options.timeout : 30000; this.onload = 'onload' in options ? options.onload : null; this.root = root || this.document.head || this.document.body; this.sheets = []; // List of active stylesheets. this.files = {}; // List of loaded or loading files. this.meta = {}; // List of meta elements for polling. if (null === this.onload) { this.feature(); } } /** * Remove a asset. * * @param {String} url URL we need to load. * @returns {AsyncAsset} * @api public */ AsyncAsset.prototype.remove = function remove(url) { if (!(url in this.files)) return this; this.files[url].destroy(); delete this.files[url]; }; /** * Load a new asset. * * @param {String} url URL we need to load. * @param {Function} fn Completion callback. * @returns {AsyncAsset} * @api public */ AsyncAsset.prototype.add = function add(url, fn) { if (this.progress(url, fn)) return this; if ('js' === this.type(url)) return this.script(url, fn); if ('css' === this.type(url)) return this.style(url, fn); throw new Error('Unsupported file type'); }; /** * Check if the given URL has already loaded or is currently in progress of * being loaded. * * @param {String} url URL that needs to be loaded. * @returns {Boolean} The loading is already in progress. * @api private */ AsyncAsset.prototype.progress = function progress(url, fn) { if (!(url in this.files)) return false; var file = this.files[url]; if (File.LOADING === file.readyState) { file.callbacks.push(fn); } else if (File.LOADED === file.readyState) { fn(); } else if (File.DEAD === file.readyState) { return false; } return true; }; /** * Trigger the callbacks for a given URL. * * @param {String} url URL that has been loaded. * @param {Error} err Optional error argument when shit fails. * @api private */ AsyncAsset.prototype.callback = function callback(url, err) { var file = this.files[url] , meta = this.meta[url]; if (!file) return; file.exec(err); if (err) delete this.files[url]; if (meta) { meta.parentNode.removeChild(meta); delete this.meta[url]; } }; /** * Determine the file type for a given URL. * * @param {String} url File URL. * @returns {String} The extension of the URL. * @api private */ AsyncAsset.prototype.type = function type(url) { return url.split('.').pop().toLowerCase(); }; /** * Load a new script with a source. * * @param {String} url The script file that needs to be loaded in to the page. * @param {Function} fn The completion callback. * @returns {AsyncAsset} * @api private */ AsyncAsset.prototype.script = function scripts(url, fn) { var script = this.document.createElement('script') , file = this.files[url] = new File(url, fn) , async = this; // // Add an unload handler which removes the DOM node from the root element. // file.unload(function unload() { script.onerror = script.onload = script.onreadystatechange = null; if (script.parentNode) script.parentNode.removeChild(script); }); // // Required for FireFox 3.6 / Opera async loading. Normally browsers would // load the script async without this flag because we're using createElement // but these browsers need explicit flags. // script.async = true; // // onerror is not triggered by all browsers, but should give us a clean // indication of failures so it doesn't matter if you're browser supports it // or not, we still want to listen for it. // script.onerror = function onerror() { script.onerror = script.onload = script.onreadystatechange = null; async.callback(url, new Error('Failed to load the script.')); }; // // All "latest" browser seem to support the onload event for detecting full // script loading. Internet Explorer 11 no longer needs to use the // onreadystatechange method for completion indication. // script.onload = function onload() { script.onerror = script.onload = script.onreadystatechange = null; async.callback(url); }; // // Fall-back for older IE versions, they do not support the onload event on the // script tag and we need to check the script readyState to see if it's // successfully loaded. // script.onreadystatechange = function onreadystatechange() { if (this.readyState in { loaded: 1, complete: 1 }) { script.onerror = script.onload = script.onreadystatechange = null; async.callback(url); } }; // // The src needs to be set after the element has been added to the document. // If I remember correctly it had to do something with an IE8 bug. // this.root.appendChild(script); script.src = url; return this; }; /** * Load CSS files by using @import statements. * * @param {String} url URL to load. * @param {Function} fn Completion callback. * @returns {AsyncAsset} * @api private */ AsyncAsset.prototype.style = function style(url, fn) { if (!this.document.styleSheet) return this.link(url, fn); var file = this.file[url] = new File(url, fn) , sheet, i = 0; // // Internet Explorer can only have 31 style tags on a single page. One single // style tag is also limited to 31 @import statements so this gives us room to // have 961 style sheets totally. So we should queue style sheets. This // limitation has been removed in Internet Explorer 10. // // @see http://john.albin.net/ie-css-limits/two-style-test.html // @see http://support.microsoft.com/kb/262161 // @see http://blogs.msdn.com/b/ieinternals/archive/2011/05/14/internet-explorer-stylesheet-rule-selector-import-sheet-limit-maximum.aspx // for (; i < this.sheets.length; i++) { if (this.sheets[i].imports.length < 31) { sheet = this.sheets[i]; break; } } // // We didn't find suitable style Sheet to add another @import statement, // create a new one so we can leverage that instead. // // @TODO we should probably check the amount of `document.styleSheets.length` // to check if we're allowed to add more style sheets. // if (!sheet) { sheet = this.document.createStyleSheet(); this.sheets.push(sheet); } // // Remove the import from the stylesheet. // file.unload(function unload() { sheet.removeImport(i); }); sheet.addImport(url); return this.setInterval(url); }; /** * Load CSS by adding link tags on to the page. * * @param {String} url URL to load. * @param {Function} fn Completion callback. * @returns {AsyncAsset} * @api private */ AsyncAsset.prototype.link = function links(url, fn) { var link = this.document.createElement('link') , file = this.files[url] = new File(url, fn) , async = this; file.unload(function unload() { link.onload = link.onerror = null; link.parentNode.removeChild(link); }); if (this.onload) { link.onload = function onload() { link.onload = link.onerror = null; async.callback(url); }; link.onerror = function onerror() { link.onload = link.onerror = null; async.callback(url, new Error('Failed to load the stylesheet')); }; } link.href = url; link.type = 'text/css'; link.rel = 'stylesheet'; this.root.appendChild(link); return this.setInterval(url); }; /** * Poll our stylesheets to see if the style's have been applied. * * @param {String} url URL to check * @api private */ AsyncAsset.prototype.setInterval = function setIntervals(url) { if (url in this.meta) return this; // // Create a meta tag which we can inject in to the page and give it the id of // the prefixed CSS rule so we know when the style sheet is loaded based on the // style of this meta element. // var meta = this.meta[url] = this.document.createElement('meta') , async = this; meta.id = [ this.prefix, url.split('/').pop().split('.').shift() ].join('').toLowerCase(); this.root.appendChild(meta); if (this.setInterval.timer) return this; // // Start the reaping process. // this.setInterval.timer = setInterval(function interval() { var now = +new Date() , url, file, style, meta , compute = window.getComputedStyle; for (url in async.meta) { meta = async.meta[url]; if (!meta) continue; file = async.files[url]; style = compute ? getComputedStyle(meta) : meta.currentStyle; // // We assume that CSS added an increased style to the given prefixed CSS // tag. // if (file && style && parseInt(style.height, 10) > 1) { file.exec(); } if ( !file || file.readyState === File.DEAD || file.readyState === File.LOADED || (now - file.start > async.timeout) ) { if (file) file.exec(new Error('Stylesheet loading has timed out')); meta.parentNode.removeChild(meta); delete async.meta[url]; } } // // If we can iterate over the async.meta object there are still objects // left that needs to be polled. // for (url in async.meta) return; clearInterval(async.setInterval.timer); delete async.setInterval.timer; }, 20); return this; }; /** * Try to detect if this browser supports the onload events on the link tag. * It's a known cross browser bug that can affect WebKit, FireFox and Opera. * Internet Explorer is the only browser that supports the onload event * consistency but it has other bigger issues that prevents us from using this * method. * * @returns {AsyncAsset} * @api private */ AsyncAsset.prototype.feature = function detect() { if (this.feature.detecting) return this; this.feature.detecting = true; var link = document.createElement('link') , async = this; link.rel = 'stylesheet'; link.href = 'data:text/css;base64,'; link.onload = function loaded() { link.parentNode.removeChild(link); link.onload = false; async.onload = true; }; this.root.appendChild(link); return this; }; // // Expose the file instance. // AsyncAsset.File = File; // // Expose the asset loader // module.exports = AsyncAsset; },{}],5:[function(_dereq_,module,exports){ },{}],6:[function(_dereq_,module,exports){ 'use strict'; /** * Minimal EventEmitter interface that is molded against the Node.js * EventEmitter interface. * * @constructor * @api public */ function EventEmitter() { this._events = {}; } /** * Return a list of assigned event listeners. * * @param {String} event The events that should be listed. * @returns {Array} * @api public */ EventEmitter.prototype.listeners = function listeners(event) { return Array.apply(this, this._events[event] || []); }; /** * Emit an event to all registered event listeners. * * @param {String} event The name of the event. * @returns {Boolean} Indication if we've emitted an event. * @api public */ EventEmitter.prototype.emit = function emit(event, a1, a2, a3, a4, a5) { if (!this._events || !this._events[event]) return false; var listeners = this._events[event] , length = listeners.length , len = arguments.length , fn = listeners[0] , args , i; if (1 === length) { if (fn.__EE3_once) this.removeListener(event, fn); switch (len) { case 1: fn.call(fn.__EE3_context || this); break; case 2: fn.call(fn.__EE3_context || this, a1); break; case 3: fn.call(fn.__EE3_context || this, a1, a2); break; case 4: fn.call(fn.__EE3_context || this, a1, a2, a3); break; case 5: fn.call(fn.__EE3_context || this, a1, a2, a3, a4); break; case 6: fn.call(fn.__EE3_context || this, a1, a2, a3, a4, a5); break; default: for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } fn.apply(fn.__EE3_context || this, args); } } else { for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } for (i = 0; i < length; fn = listeners[++i]) { if (fn.__EE3_once) this.removeListener(event, fn); fn.apply(fn.__EE3_context || this, args); } } return true; }; /** * Register a new EventListener for the given event. * * @param {String} event Name of the event. * @param {Functon} fn Callback function. * @param {Mixed} context The context of the function. * @api public */ EventEmitter.prototype.on = function on(event, fn, context) { if (!this._events) this._events = {}; if (!this._events[event]) this._events[event] = []; fn.__EE3_context = context; this._events[event].push(fn); return this; }; /** * Add an EventListener that's only called once. * * @param {String} event Name of the event. * @param {Function} fn Callback function. * @param {Mixed} context The context of the function. * @api public */ EventEmitter.prototype.once = function once(event, fn, context) { fn.__EE3_once = true; return this.on(event, fn, context); }; /** * Remove event listeners. * * @param {String} event The event we want to remove. * @param {Function} fn The listener that we need to find. * @api public */ EventEmitter.prototype.removeListener = function removeListener(event, fn) { if (!this._events || !this._events[event]) return this; var listeners = this._events[event] , events = []; for (var i = 0, length = listeners.length; i < length; i++) { if (fn && listeners[i] !== fn) { events.push(listeners[i]); } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) this._events[event] = events; else this._events[event] = null; return this; }; /** * Remove all listeners or only the listeners for the specified event. * * @param {String} event The event want to remove all listeners for. * @api public */ EventEmitter.prototype.removeAllListeners = function removeAllListeners(event) { if (!this._events) return this; if (event) this._events[event] = null; else this._events = {}; return this; }; // // Alias methods names because people roll like that. // EventEmitter.prototype.off = EventEmitter.prototype.removeListener; EventEmitter.prototype.addListener = EventEmitter.prototype.on; // // This function doesn't apply anymore. // EventEmitter.prototype.setMaxListeners = function setMaxListeners() { return this; }; // // Expose the module. // EventEmitter.EventEmitter = EventEmitter; EventEmitter.EventEmitter2 = EventEmitter; EventEmitter.EventEmitter3 = EventEmitter; try { module.exports = EventEmitter; } catch (e) {} },{}],7:[function(_dereq_,module,exports){ 'use strict'; var Container = _dereq_('containerization') , EventEmitter = _dereq_('eventemitter3') , iframe = _dereq_('frames'); /** * Fortress: Container and Image management for front-end code. * * @constructor * @param {Object} options Fortress configuration * @api private */ function Fortress(options) { if (!(this instanceof Fortress)) return new Fortress(options); options = options || {}; // // Create a small dedicated container that houses all our iframes. This might // add an extra DOM node to the page in addition to each iframe but it will // ultimately result in a cleaner DOM as everything is nicely tucked away. // var scripts = document.getElementsByTagName('script') , append = scripts[scripts.length - 1] || document.body , div = document.createElement('div'); append.parentNode.insertBefore(div, append); this.global = (function global() { return this; })() || window; this.containers = {}; this.mount = div; scripts = null; EventEmitter.call(this); } // // Fortress inherits from EventEmitter3. // Fortress.prototype = new EventEmitter(); Fortress.prototype.constructor = Fortress; /** * Detect the current globals that are loaded in to this page. This way we can * see if we are leaking data. * * @param {Array} old Optional array with previous or known leaks. * @returns {Array} Names of the leaked globals. * @api private */ Fortress.prototype.globals = function globals(old) { var i = iframe(this.mount, 'iframe_'+ (+new Date())) , windoh = i.add().window() , global = this.global , result = []; i.remove(); // // Detect the globals and return them. // for (var key in global) { var introduced = !(key in windoh); // // We've been given an array, so we should use that as the source of previous // and acknowledged leaks and only return an array that contains newly // introduced leaks. // if (introduced) { if (old && old.length && !!~old.indexOf(key)) continue; result.push(key); } } return result; }; /** * List all active containers. * * @returns {Array} Active containers. * @api public */ Fortress.prototype.all = function all() { var everything = []; for (var id in this.containers) { everything.push(this.containers[id]); } return everything; }; /** * Generate an unique, unknown id that we can use for our container storage. * * @returns {String} * @api private */ Fortress.prototype.id = function id() { for (var i = 0, generated = []; i < 4; i++) { generated.push(Math.random().toString(36).substring(2)); } generated = 'fortress_'+ generated.join('_'); // // Ensure that we didn't generate a pre-existing id, if we did, generate // another id. // if (generated in this.containers) return this.id(); return generated; }; /** * Create a new container. * * @param {String} code * @param {Object} options Options for the container * @returns {Container} * @api public */ Fortress.prototype.create = function create(code, options) { var container = new Container(this.mount, this.id(), code, options); this.containers[container.id] = container; return container; }; /** * Get a container based on it's unique id. * * @param {String} id The container id. * @returns {Container} * @api public */ Fortress.prototype.get = function get(id) { return this.containers[id]; }; /** * Inspect a running Container in order to get more detailed information about * the process and the state of the container. * * @param {String} id The container id. * @api public */ Fortress.prototype.inspect = Fortress.prototype.top = function inspect(id) { var container = this.get(id); if (!container) return {}; return container.inspect(); }; /** * Start the container with the given id. * * @param {String} id The container id. * @api public */ Fortress.prototype.start = function start(id) { var container = this.get(id); if (!container) return this; container.start(); return this; }; /** * Stop a running container, this does not fully destroy the container. It * merely stops it from running. Stopping an container will cause the container * to start from the beginning again once it's started. This is not a pause * function. * * @param {String} id The container id. * @api public */ Fortress.prototype.stop = function stop(id) { var container = this.get(id); if (!container) return this; container.stop(); return this; }; /** * Restart a container. Basically, just a start and stop. * * @param {String} id The container id. * @api public */ Fortress.prototype.restart = function restart(id) { var container = this.get(id); if (!container) return this; container.stop().start(); return this; }; /** * Completely remove and shutdown the given container id. * * @param {String} id The container id. * @api public */ Fortress.prototype.kill = function kill(id) { var container = this.get(id); if (!container) return this; container.destroy(); delete this.containers[id]; return this; }; /** * Start streaming logging information and cached logs. * * @param {String} id The container id. * @param {String} method The log method name. * @param {Function} fn The function that needs to be called for each stream. * @api public */ Fortress.prototype.attach = function attach(id, method, fn) { var container = this.get(id); if (!container) return this; if ('function' === typeof method) { fn = method; method = 'attach'; } else { method += 'attach::'+ method; } container.on(method, fn); return this; }; /** * Stop streaming logging information and cached logs. * * @param {String} id The container id. * @param {String} method The log method name. * @param {Function} fn The function that needs to be called for each stream. * @api public */ Fortress.prototype.detach = function detach(id, method, fn) { var container = this.get(id); if (!container) return this; if ('function' === typeof method) { fn = method; method = 'attach'; } else { method += 'attach::'+ method; } if (!fn) container.removeAllListeners(method); else container.on(method, fn); return this; }; /** * Destroy all active containers and clean up all references. We expect no more * further calls to this Fortress instance. * * @api public */ Fortress.prototype.destroy = function destroy() { for (var id in this.containers) { this.kill(id); } this.mount.parentNode.removeChild(this.mount); this.global = this.mount = this.containers = null; }; /** * Prepare a file or function to be loaded in to a Fortress based Container. * When the transfer boolean is set we assume that you want to load pass the * result of to a function or assign it a variable from the server to the client * side: * * ``` * <script> * var code = <%- Fortress.stringify(code, true) %> * </script> * ``` * * @param {String|Function} code The code that needs to be transformed. * @param {Boolean} transfer Prepare the code for transfer. * @returns {String} * @api public */ Fortress.stringify = function stringify(code, transfer) { if ('function' === typeof code) { // // We've been given a pure function, so we need to wrap it a little bit // after we've done a `toString` for the source retrieval so the function // will automatically execute when it's activated. // code = '('+ code.toString() +'())'; } else { // // We've been given a string, so we're going to assume that it's path to file // that should be included instead. // code = _dereq_('fs').readFileSync(code, 'utf-8'); } return transfer ? JSON.stringify(code) : code; }; // // Expose the module. // module.exports = Fortress; },{"containerization":8,"eventemitter3":10,"frames":11,"fs":5}],8:[function(_dereq_,module,exports){ 'use strict'; var EventEmitter = _dereq_('eventemitter3') , BaseImage = _dereq_('alcatraz') , slice = Array.prototype.slice , iframe = _dereq_('frames'); /** * Representation of a single container. * * Options: * * - retries; When an error occurs, how many times should we attempt to restart * the code before we automatically stop() the container. * - stop; Stop the container when an error occurs. * - timeout; How long can a ping packet timeout before we assume that the * container has died and should be restarted. * * @constructor * @param {Element} mount The element we should attach to. * @param {String} id A unique id for this container. * @param {String} code The actual that needs to run within the sandbox. * @param {Object} options Container configuration. * @api private */ function Container(mount, id, code, options) { if (!(this instanceof Container)) return new Container(mount, id, code, options); if ('object' === typeof code) { options = code; code = null; } options = options || {}; this.i = iframe(mount, id); // The generated iframe. this.mount = mount; // Mount point of the container. this.console = []; // Historic console.* output. this.setTimeout = {}; // Stores our setTimeout references. this.id = id; // Unique id. this.readyState = Container.CLOSED; // The readyState of the container. this.created = +new Date(); // Creation EPOCH. this.started = null; // Start EPOCH. this.retries = 'retries' in options // How many times should we reload ? +options.retries || 3 : 3; this.timeout = 'timeout' in options // Ping timeout before we reboot. ? +options.timeout || 1050 : 1050; // // Initialise as an EventEmitter before we start loading in the code. // EventEmitter.call(this); // // Optional code to load in the container and start it directly. // if (code) this.load(code).start(); } // // The container inherits from the EventEmitter3. // Container.prototype = new EventEmitter(); Container.prototype.constructor = Container; /** * Internal readyStates for the container. * * @type {Number} * @private */ Container.CLOSING = 1; Container.OPENING = 2; Container.CLOSED = 3; Container.OPEN = 4; /** * Start a new ping timeout. * * @api private */ Container.prototype.ping = function ping() { if (this.setTimeout.pong) clearTimeout(this.setTimeout.pong); var self = this; this.setTimeout.pong = setTimeout(function pong() { self.onmessage({ type: 'error', scope: 'iframe.timeout', args: [ 'the iframe is no longer responding with ping packets' ] }); }, this.timeout); return this; }; /** * Retry loading the code in the iframe. The container will be restored to a new * state or completely reset the iframe. * * @api private */ Container.prototype.retry = function retry() { switch (this.retries) { // // This is our last attempt, we've tried to have the iframe restart the code // it self, so for our last attempt we're going to completely create a new // iframe and re-compile the code for it. // case 1: this.stop(); // Clear old iframe and nuke it's references this.i = iframe(this.mount, this.id); this.load(this.image.source).start(); break; // // No more attempts left. // case 0: this.stop(); this.emit('end'); return; // // By starting and stopping (and there for removing and adding it back to // the DOM) the iframe will reload it's HTML and the added code. // default: this.stop().start(); break; } this.emit('retry', this.retries); this.retries--; return this; }; /** * Inspect the container to get some useful statistics about it and it's health. * * @returns {Object} * @api public */ Container.prototype.inspect = function inspect() { if (!this.i.attached()) return {}; var date = new Date() , memory; // // Try to read out the `performance` information from the iframe. // if (this.i.window() && this.i.window().performance) { memory = this.i.window().performance.memory; } memory = memory || {}; return { readyState: this.readyState, retries: this.retries, uptime: this.started ? (+date) - this.started : 0, date: date, memory: { limit: memory.jsHeapSizeLimit || 0, total: memory.totalJSHeapSize || 0, used: memory.usedJSHeapSize || 0 } }; }; /** * Parse and process incoming messages from the iframe. The incoming messages * should be objects that have a `type` property. The main reason why we have * this as a separate method is to give us flexibility. We are leveraging iframes * at the moment, but in the future we might want to leverage WebWorkers for the * sand boxing of JavaScript. * * @param {Object} packet The incoming message. * @returns {Boolean} Message was handled y/n. * @api private */ Container.prototype.onmessage = function onmessage(packet) { if ('object' !== typeof packet) return false; if (!('type' in packet)) return false; packet.args = packet.args || []; switch (packet.type) { // // The code in the iframe used the `console` method. // case 'console': this.console.push({ scope: packet.scope, epoch: +new Date(), args: packet.args }); if (packet.attach) { this.emit.apply(this, ['attach::'+ packet.scope].concat(packet.args)); this.emit.apply(this, ['attach', packet.scope].concat(packet.args)); } break; // // An error happened in the iframe, process it. // case 'error': var failure = packet.args[0].stack ? packet.args[0] : new Error(packet.args[0]); failure.scope = packet.scope || 'generic'; this.emit('error', failure); this.retry(); break; // // The iframe and it's code has been loaded. // case 'load': if (this.readyState !== Container.OPEN) { this.readyState = Container.OPEN; this.emit('start'); } break; // // The iframe is unloading, attaching // case 'unload': if (this.readyState !== Container.CLOSED) { this.readyState = Container.CLOSED; this.emit('stop'); } break; // // We've received a ping response from the iframe, so we know it's still // running as intended. // case 'ping': this.ping(); this.emit('ping'); break; // // Handle unknown package types by just returning false after we've emitted // it as an `regular` message. // default: this.emit.apply(this, ['message'].concat(packet.args)); return false; } return true; }; /** * Small wrapper around sandbox evaluation. * * @param {String} cmd The command to executed in the iframe. * @param {Function} fn Callback * @api public */ Container.prototype.eval = function evil(cmd, fn) { var data; try { data = this.i.add().window().eval(cmd); } catch (e) { return fn(e); } return fn(undefined, data); }; /** * Start the container. * * @returns {Container} * @api public */ Container.prototype.start = function start() { this.readyState = Container.OPENING; var self = this; /** * Simple argument proxy. * * @api private */ function onmessage() { self.onmessage.apply(self, arguments); } // // Code loading is an sync process, but this COULD cause huge stack traces // and really odd feedback loops in the stack trace. So we deliberately want // to destroy the stack trace here. // this.setTimeout.start = setTimeout(function async() { var doc = self.i.document(); // // No doc.open, the iframe has already been destroyed! // if (!doc.open || !self.i) return; // // We need to open and close the iframe in order for it to trigger an onload // event. Certain scripts might require in order to execute properly. // doc.open(); doc.write([ '<!doctype html>', '<html><head>', // // iFrames can generate pointless requests by searching for a favicon. // This can add up to three extra requests for a simple iframe. To battle // this, we need to supply an empty icon. // // @see http://stackoverflow.com/questions/1321878/how-to-prevent-favicon-ico-requests // '<link rel="icon" href="data:;base64,=">', '</head><body>' ].join('\n')); // // Introduce our messaging variable, this needs to be done before we eval // our code. If we set this value before the setTimeout, it doesn't work in // Opera due to reasons. // self.i.window()[self.id] = onmessage; self.eval(self.image.toString(), function evil(err) { if (err) return self.onmessage({ type: 'error', scope: 'iframe.eval', args: [ err ] }); }); // // If executing the code results to an error we could actually be stopping // and removing the iframe from the source before we're able to close it. // This is because executing the code inside the iframe is actually an sync // operation. // if (doc.close) doc.close(); }, 0); // // We can only write to the iframe if it's actually in the DOM. The `i.add()` // method ensures that the iframe is added to the DOM. // this.i.add(); this.started = +new Date(); return this; }; /** * Stop running the code inside the container. * * @returns {Container} * @api private */ Container.prototype.stop = function stop() { if (this.readyState !== Container.CLOSED && this.readyState !== Container.CLOSING) { this.readyState = Container.CLOSING; } this.i.remove(); // // Opera doesn't support unload events. So adding an listener inside the // iframe for `unload` doesn't work. This is the only way around it. // this.onmessage({ type: 'unload' }); // // It's super important that this removed AFTER we've cleaned up all other // references as we might need to communicate back to our container when we // are unloading or when an `unload` event causes an error. // this.i.window()[this.id] = null; // // Clear the timeouts. // for (var timeout in this.setTimeout) { clearTimeout(this.setTimeout[timeout]); delete this.setTimeout[timeout]; } return this; }; /** * Load the given code as image on to the container. * * @param {String} code The code that should run on the container. * @returns {Container} * @api public */ Container.prototype.load = function load(code) { this.image = new BaseImage(this.id, code); return this; }; /** * Completely destroy the given container and ensure that all references are * nuked so we can clean up as much memory as possible. * * @returns {Container} * @api private */ Container.prototype.destroy = function destroy() { if (!this.i) return this; this.stop(); // // Remove all possible references to release as much memory as possible. // this.mount = this.image =