UNPKG

bigpipe.js

Version:

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

1,925 lines (1,643 loc) 92.9 kB
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.BigPipe = f()}})(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);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.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(require,module,exports){ 'use strict'; var collection = require('./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 || iterator, 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":3}],2:[function(require,module,exports){ 'use strict'; var EventEmitter = require('eventemitter3') , collection = require('./collection') , Pagelet = require('./pagelet') , destroy = require('demolish'); /** * BigPipe is the client-side library which is automatically added to pages which * uses the BigPipe framework. * * 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 {Object} options BigPipe configuration. * @api public */ function BigPipe(options) { if (!(this instanceof BigPipe)) return new BigPipe(options); options = options || {}; this.expected = +options.pagelets || 0; // Pagelets that this page requires. this.allowed = +options.pagelets || 0; // Pagelets that are allowed for this page. this.maximum = options.limit || 20; // Max Pagelet instances we can reuse. this.readyState = BigPipe.LOADING; // Current readyState. this.options = options; // Reference to the used options. this.templates = {}; // Collection of templates. this.pagelets = []; // Collection of different pagelets. this.freelist = []; // Collection of unused Pagelet instances. this.rendered = []; // List of already rendered pagelets. this.progress = 0; // Percentage loaded. this.assets = {}; // Asset cache. this.root = document.documentElement; // The <html> element. EventEmitter.call(this); this.configure(options); } // // Inherit from EventEmitter3, use old school inheritance because that's the way // we roll. Oh and it works in every browser. // BigPipe.prototype = new EventEmitter(); BigPipe.prototype.constructor = BigPipe; // // The various of readyStates that our class can be in. // BigPipe.LOADING = 1; // Still loading pagelets. BigPipe.INTERACTIVE = 2; // All pagelets received, you can safely modify. BigPipe.COMPLETE = 3; // All assets and pagelets loaded. /** * The BigPipe plugins will contain all our plugins definitions. * * @type {Object} * @private */ BigPipe.prototype.plugins = {}; /** * Process a change in BigPipe. * * @param {Object} changed Data that is changed. * @returns {BigPipe} * @api private */ BigPipe.prototype.change = require('modification')(' changed'); /** * Configure the BigPipe. * * @param {Object} options Configuration. * @return {BigPipe} * @api private */ BigPipe.prototype.configure = function configure(options) { var bigpipe = this; // // Process the potential plugins. // for (var plugin in this.plugins) { this.plugins[plugin].call(this, this, options); } // // Setup our completion handler. // var remaining = this.expected; bigpipe.on('arrive', function arrived(name) { bigpipe.once(name +':initialized', function initialize() { if (!--remaining) { bigpipe.change({ readyState: BigPipe.COMPLETE }); } }); }); return this; }; /** * Horrible hack, but needed to prevent memory leaks caused by * `document.createDocumentFragment()` while maintaining sublime performance. * * @type {Number} * @private */ BigPipe.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. * @param {Object} state Pagelet state * @returns {BigPipe} * @api public */ BigPipe.prototype.arrive = function arrive(name, data, state) { data = data || {}; var index , bigpipe = this , parent = data.parent , remaining = data.remaining , rendered = bigpipe.rendered; bigpipe.progress = Math.round(((bigpipe.expected - remaining) / bigpipe.expected) * 100); bigpipe.emit('arrive', name, data, state); // // Create child pagelet after parent has finished rendering. // if (!bigpipe.has(name)) { if (parent !== 'bootstrap' && !~collection.index(bigpipe.rendered, parent)) { bigpipe.once(parent +':render', function render() { bigpipe.create(name, data, state, bigpipe.get(parent).placeholders); }); } else { bigpipe.create(name, data, state); } } // // Keep track of how many pagelets have been fully initialized, e.g. assets // loaded and all rendering logic processed. Also count destroyed pagelets as // processed. // if (data.remove) bigpipe.allowed--; else bigpipe.once(name +':render', function finished() { if (rendered.length === bigpipe.allowed) return bigpipe.broadcast('finished'); }); // // Emit progress information about the amount of pagelet's that we've // received. // bigpipe.emit('progress', bigpipe.progress, remaining); // // Check if all pagelets have been received from the server. // if (remaining) return bigpipe; bigpipe.change({ readyState: BigPipe.INTERACTIVE }); bigpipe.emit('received'); return this; }; /** * Create a new Pagelet instance. * * @param {String} name The name of the pagelet. * @param {Object} data Data for the pagelet. * @param {Object} state State for the pagelet. * @param {Array} roots Root elements we can search can search for. * @returns {BigPipe} * @api private */ BigPipe.prototype.create = function create(name, data, state, roots) { data = data || {}; var bigpipe = this , pagelet = bigpipe.alloc(); bigpipe.pagelets.push(pagelet); pagelet.configure(name, data, state, roots); // // A new pagelet has been loaded, emit a progress event. // bigpipe.emit('create', pagelet); }; /** * Check if the pagelet has already been loaded. * * @param {String} name The name of the pagelet. * @returns {Boolean} * @api public */ BigPipe.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 */ BigPipe.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 {BigPipe} * @api public */ BigPipe.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 {BigPipe} * @api public */ BigPipe.prototype.broadcast = function broadcast(event) { var args = arguments; collection.each(this.pagelets, function each(pagelet) { if (!pagelet.reserved(event)) { EventEmitter.prototype.emit.apply(pagelet, args); } }); return this; }; /** * Check if the event we're about to emit is a reserved event and should be * blocked. * * Assume that every <name>: prefixed event is internal and should not be * emitted by user code. * * @param {String} event Name of the event we want to emit * @returns {Boolean} * @api public */ BigPipe.prototype.reserved = function reserved(event) { return this.has(event.split(':')[0]) || event in this.reserved.events; }; /** * The actual reserved events. * * @type {Object} * @api private */ BigPipe.prototype.reserved.events = { remove: 1, // Pagelet has been removed. received: 1, // Pagelets have been received. finished: 1, // Pagelets have been loaded, processed and rendered. progress: 1, // Loaded a new Pagelet. create: 1 // Created a new Pagelet }; /** * 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 */ BigPipe.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 */ BigPipe.prototype.free = function free(pagelet) { if (this.freelist.length < this.maximum) { this.freelist.push(pagelet); return true; } return false; }; /** * Check if we've probed the client for gzip support yet. * * @param {String} version Version number of the zipline we support. * @returns {Boolean} * @api public */ BigPipe.prototype.ziplined = function zipline(version) { if (~document.cookie.indexOf('zipline='+ version)) return true; try { if (sessionStorage.getItem('zipline') === version) return true; } catch (e) {} try { if (localStorage.getItem('zipline') === version) return true; } catch (e) {} var bigpipe = document.createElement('bigpipe') , iframe = document.createElement('iframe') , doc; bigpipe.style.display = 'none'; iframe.frameBorder = 0; bigpipe.appendChild(iframe); this.root.appendChild(bigpipe); doc = iframe.contentWindow.document; doc.open().write('<body onload="' + 'var d = document;d.getElementsByTagName(\'head\')[0].' + 'appendChild(d.createElement(\'script\')).src' + '=\'\/zipline.js\'">'); doc.close(); return false; }; /** * Completely destroy the BigPipe instance. * * @type {Function} * @returns {Boolean} * @api public */ BigPipe.prototype.destroy = destroy('options, templates, pagelets, freelist, rendered, assets, root', { before: function before() { var bigpipe = this; collection.each(bigpipe.pagelets, function remove(pagelet) { bigpipe.remove(pagelet.name); }); }, after: 'removeAllListeners' }); // // Expose the BigPipe client library and Pagelet constructor for easy extending. // BigPipe.Pagelet = Pagelet; module.exports = BigPipe; },{"./collection":3,"./pagelet":16,"demolish":6,"eventemitter3":7,"modification":13}],3:[function(require,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++) { if (false === iterator.call(context || iterator, collection[i], i, collection)) { return; // If false is returned by the callback we need to bail out. } } } else { for (i in collection) { if (hasOwn.call(collection, i)) { if (false === iterator.call(context || iterator, collection[i], i, collection)) { return; // If false is returned by the callback we need to bail out. } } } } } /** * 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) { if ('function' === typeof arr.indexOf) return arr.indexOf(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; },{}],4:[function(require,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.dependent = 0; this.cleanup = []; this.url = url; if ('function' === typeof fn) { this.add(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; }; /** * Add a new dependent. * * @param {Function} fn Completion callback. * @returns {Boolean} Callback successfully added or queued. * @api private */ File.prototype.add = function add(fn) { if (File.LOADING === this.readyState) { this.callbacks.push(fn); } else if (File.LOADED === this.readyState) { fn(); } else { return false; } this.dependent++; return true; }; /** * Remove a dependent. If all dependent's are removed we will automatically * destroy the loaded file from the environment. * * @returns { * @api private */ File.prototype.remove = function remove() { if (0 === --this.dependent) { this.destroy(); return true; } return false; }; /** * 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 = this.dependent = 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) { var file = this.files[url]; if (!file) return this; // // If we are fully removed, just nuke the reference. // if (file.remove()) { delete this.files[url]; } return this; }; /** * 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) { var type = this.type(url); if (this.progress(url, fn)) return this; if ('js' === type) return this.script(url, fn); if ('css' === type) return this.style(url, fn); throw new Error('Unsupported file type: '+ 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; return this.files[url].add(fn); }; /** * 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; }; /** * Prefetch resources without executing them. This ensures that the next lookup * is primed in the cache when we need them. Of course this is only possible * when the server sends the correct caching headers. * * @param {Array} urls The URLS that need to be cached. * @returns {AsyncAsset} * @api private */ AsyncAsset.prototype.prefetch = function prefetch(urls) { // // This check is here because I'm lazy, I don't want to add an `isArray` check // to the code. So we're just going to flip the logic here. If it's an string // transform it to an array. // if ('string' === typeof urls) urls = [urls]; var IE = navigator.userAgent.indexOf(' Trident/') , img = /\.(jpg|jpeg|png|gif|webp)$/ , node; for (var i = 0, l = urls.length; i < l; i++) { if (IE || img.test(urls[i])) { new Image().src = urls[i]; continue; } node = document.createElement('object'); node.height = node.width = 0; // // Position absolute is required because it can still add some minor spacing // at the bottom of a page and that will break sticky footer // implementations. // node.style.position = 'absolute'; document.body.appendChild(node); } 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(require,module,exports){ },{}],6:[function(require,module,exports){ 'use strict'; /** * Create a function that will cleanup the instance. * * @param {Array|String} keys Properties on the instance that needs to be cleared. * @param {Object} options Additional configuration. * @returns {Function} Destroy function * @api public */ module.exports = function demolish(keys, options) { var split = /[, ]+/; options = options || {}; keys = keys || []; if ('string' === typeof keys) keys = keys.split(split); /** * Run addition cleanup hooks. * * @param {String} key Name of the clean up hook to run. * @param {Mixed} selfie Reference to the instance we're cleaning up. * @api private */ function run(key, selfie) { if (!options[key]) return; if ('string' === typeof options[key]) options[key] = options[key].split(split); if ('function' === typeof options[key]) return options[key].call(selfie); for (var i = 0, type, what; i < options[key].length; i++) { what = options[key][i]; type = typeof what; if ('function' === type) { what.call(selfie); } else if ('string' === type && 'function' === typeof selfie[what]) { selfie[what](); } } } /** * Destroy the instance completely and clean up all the existing references. * * @returns {Boolean} * @api public */ return function destroy() { var selfie = this , i = 0 , prop; if (selfie[keys[0]] === null) return false; run('before', selfie); for (; i < keys.length; i++) { prop = keys[i]; if (selfie[prop]) { if ('function' === typeof selfie[prop].destroy) selfie[prop].destroy(); selfie[prop] = null; } } if (selfie.emit) selfie.emit('destroy'); run('after', selfie); return true; }; }; },{}],7:[function(require,module,exports){ 'use strict'; /** * Representation of a single EventEmitter function. * * @param {Function} fn Event handler to be called. * @param {Mixed} context Context for function execution. * @param {Boolean} once Only emit once * @api private */ function EE(fn, context, once) { this.fn = fn; this.context = context; this.once = once || false; } /** * Minimal EventEmitter interface that is molded against the Node.js * EventEmitter interface. * * @constructor * @api public */ function EventEmitter() { /* Nothing to set */ } /** * Holds the assigned EventEmitters by name. * * @type {Object} * @private */ EventEmitter.prototype._events = undefined; /** * 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) { if (!this._events || !this._events[event]) return []; if (this._events[event].fn) return [this._events[event].fn]; for (var i = 0, l = this._events[event].length, ee = new Array(l); i < l; i++) { ee[i] = this._events[event][i].fn; } return ee; }; /** * 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] , len = arguments.length , args , i; if ('function' === typeof listeners.fn) { if (listeners.once) this.removeListener(event, listeners.fn, true); switch (len) { case 1: return listeners.fn.call(listeners.context), true; case 2: return listeners.fn.call(listeners.context, a1), true; case 3: return listeners.fn.call(listeners.context, a1, a2), true; case 4: return listeners.fn.call(listeners.context, a1, a2, a3), true; case 5: return listeners.fn.call(listeners.context, a1, a2, a3, a4), true; case 6: return listeners.fn.call(listeners.context, a1, a2, a3, a4, a5), true; } for (i = 1, args = new Array(len -1); i < len; i++) { args[i - 1] = arguments[i]; } listeners.fn.apply(listeners.context, args); } else { var length = listeners.length , j; for (i = 0; i < length; i++) { if (listeners[i].once) this.removeListener(event, listeners[i].fn, true); switch (len) { case 1: listeners[i].fn.call(listeners[i].context); break; case 2: listeners[i].fn.call(listeners[i].context, a1); break; case 3: listeners[i].fn.call(listeners[i].context, a1, a2); break; default: if (!args) for (j = 1, args = new Array(len -1); j < len; j++) { args[j - 1] = arguments[j]; } listeners[i].fn.apply(listeners[i].context, 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) { var listener = new EE(fn, context || this); if (!this._events) this._events = {}; if (!this._events[event]) this._events[event] = listener; else { if (!this._events[event].fn) this._events[event].push(listener); else this._events[event] = [ this._events[event], listener ]; } 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) { var listener = new EE(fn, context || this, true); if (!this._events) this._events = {}; if (!this._events[event]) this._events[event] = listener; else { if (!this._events[event].fn) this._events[event].push(listener); else this._events[event] = [ this._events[event], listener ]; } return this; }; /** * Remove event listeners. * * @param {String} event The event we want to remove. * @param {Function} fn The listener that we need to find. * @param {Boolean} once Only remove once listeners. * @api public */ EventEmitter.prototype.removeListener = function removeListener(event, fn, once) { if (!this._events || !this._events[event]) return this; var listeners = this._events[event] , events = []; if (fn) { if (listeners.fn && (listeners.fn !== fn || (once && !listeners.once))) { events.push(listeners); } if (!listeners.fn) for (var i = 0, length = listeners.length; i < length; i++) { if (listeners[i].fn !== fn || (once && !listeners[i].once)) { events.push(listeners[i]); } } } // // Reset the array, or remove it completely if we have no more listeners. // if (events.length) { this._events[event] = events.length === 1 ? events[0] : events; } else { delete this._events[event]; } 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) delete this._events[event]; 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; // // Expose the module. // module.exports = EventEmitter; },{}],8:[function(require,module,exports){ 'use strict'; var Container = require('containerization') , EventEmitter = require('eventemitter3') , iframe = require('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 = require('fs').readFileSync(code, 'utf-8'); } return transfer ? JSON.stringify(code) : code; }; // // Expose the module. // module.exports = Fortress; },{"containerization":9,"eventemitter3":11,"frames":12,"fs":5}],9:[function(require,module,exports){ 'use strict'; var EventEmitter = require('eventemitter3') , BaseImage = require('alcatraz') , slice = Array.prototype.slice , iframe = require('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(