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
JavaScript
!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 =