pipe.js
Version:
The client-side library which is used in BigPipe to orchestrate the pagelets.
329 lines (278 loc) • 8.48 kB
JavaScript
/*globals Primus */
'use strict';
var EventEmitter = require('eventemitter3')
, collection = require('./collection')
, Pagelet = require('./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;