happn-primus
Version:
Primus is a simple abstraction around real-time frameworks. It allows you to easily switch between different frameworks without any code changes.
1,155 lines (998 loc) • 32.8 kB
JavaScript
'use strict';
var PrimusError = require('./errors').PrimusError
, EventEmitter = require('eventemitter3')
, Transformer = require('./transformer')
, log = require('diagnostics')('primus')
, Spark = require('./spark')
, fuse = require('fusing')
, fs = require('fs');
/**
* Primus is a universal wrapper for real-time frameworks that provides a common
* interface for server and client interaction.
*
* @constructor
* @param {HTTP.Server} server HTTP or HTTPS server instance.
* @param {Object} options Configuration
* @api public
*/
function Primus(server, options) {
if (!(this instanceof Primus)) return new Primus(server, options);
if ('object' !== typeof server) {
var message = 'The first argument of the constructor must be ' +
'an HTTP or HTTPS server instance';
throw new PrimusError(message, this);
}
options = options || {};
this.fuse();
var primus = this
, key;
this.auth = options.authorization || null; // Do we have an authorization handler.
this.connections = Object.create(null); // Connection storage.
this.ark = Object.create(null); // Plugin storage.
this.layers = []; // Middleware layers.
this.transformer = null; // Reference to the real-time engine instance.
this.encoder = null; // Shorthand to the parser's encoder.
this.decoder = null; // Shorthand to the parser's decoder.
this.connected = 0; // Connection counter.
this.timeout = 'timeout' in options // The timeout used to detect zombie sparks.
? options.timeout
: 35000;
this.whitelist = []; // Forwarded-for white listing.
this.options = options; // The configuration.
// Server allows for skipping over missed pings/heartbeats from client so that
// pings caught behind a large payload don't cause the server to close the socket.
// Additionally the server sends an unsolicited pong to the client whenever the
// heartbeat skips. This prevents the client from closing the socket on missing pong.
// The server's heartbeat is set to `ping + (pong / 2)` so that the unsolicited pong
// is sent to the client before the pong timeout at the client but after the client
// would normally ping to prevent miltiple pongs and/or support clients that reset the
// timeout to the next ping upon pong arrival and would therefore never send a ping if
// all pongs are early.
// The server learns the values of clientside ping and pong from the client's
// connect url, see spark.js.
if (!this.options.allowSkippedHeartBeats) this.options.allowSkippedHeartBeats = 2;
// Skipped heartbeat must not be less than 2 to allow for high network latencies greater
// than `(pong / 2) / 2` causing the clients ping to already be inbound when the skip occurs.
if (this.options.allowSkippedHeartBeats < 2) this.options.allowSkippedHeartBeats = 2;
// De-duplicate pongs if multiple attempts to send one within this time.
// This is necessary because when pings are queued up behind a large payload
// they all arrive at once after the payload. This de-duplication causes only
// one pong in reply. If multiple pong replies were sent, then the client
// would respond with multiple pings again in an endless cycle later amplified
// by the next large payload.
options.pongSkipTime = options.pongSkipTime || 1000;
this.transformers = { // Message transformers.
outgoing: [],
incoming: []
};
this.server = server;
this.pathname = 'string' === typeof options.pathname
? options.pathname.charAt(0) !== '/'
? '/'+ options.pathname
: options.pathname
: '/primus';
//
// Create a specification file with the information that people might need to
// connect to the server.
//
this.spec = {
version: this.version,
pathname: this.pathname,
timeout: this.timeout
};
//
// Create a pre-bound Spark constructor. Doing a Spark.bind(Spark, this) doesn't
// work as we cannot extend the constructor of it anymore. The added benefit of
// approach listed below is that the prototype extensions are only applied to
// the Spark of this Primus instance.
//
this.Spark = function Sparky(headers, address, query, id, request) {
Spark.call(this, primus, headers, address, query, id, request);
};
this.Spark.prototype = Object.create(Spark.prototype, {
constructor: {
configurable: true,
value: this.Spark,
writable: true
},
__initialise: {
value: Spark.prototype.__initialise.slice(),
configurable: true,
writable: true
}
});
//
// Copy over the original Spark static properties and methods so readable and
// writable can also be used.
//
for (key in Spark) {
this.Spark[key] = Spark[key];
}
this.parsers(options.parser);
this.initialise(options.transformer || options.transport, options);
//
// If the plugins are supplied through the options, also initialise them.
// This also allows us to use plugins when creating a client constructor
// with the `Primus.createSocket({})` method.
//
if ('string' === typeof options.plugin) {
options.plugin.split(/[, ]+/).forEach(function register(name) {
primus.use(name, name);
});
return;
}
if ('object' === typeof options.plugin) {
for (key in options.plugin) {
this.use(key, options.plugin[key]);
}
}
//
// - Cluster node 0.10 lets the Operating System decide to which worker a request
// goes. This can result in a not even distribution where some workers are
// used at 10% while others at 90%. In addition to that the load balancing
// isn't sticky.
//
// - Cluster node 0.12 implements a custom round robin algorithm. This solves the
// not even distribution of work but it does not address our sticky session
// requirement.
//
// Projects like `sticky-session` attempt to implement sticky sessions but they
// are using `net` server instead of a HTTP server in combination with the
// remoteAddress of the connection to load balance. This does not work when you
// address your servers behind a load balancer as the IP is set to the load
// balancer, not the connecting clients. All in all, it only causes more
// scalability problems. So we've opted-in to warn users about the
// risks of using Primus in a cluster.
//
if (!options.iknowclusterwillbreakconnections && require('cluster').isWorker) [
'',
'The `cluster` module does not implement sticky sessions. Learn more about',
'this issue at:',
'',
'http://github.com/primus/primus#can-i-use-cluster',
''
].forEach(function warn(line) {
console.error('Primus: '+ line);
});
}
//
// Fuse and spice-up the Primus prototype with EventEmitter and predefine
// awesomeness.
//
fuse(Primus, EventEmitter);
//
// Lazy read the primus.js JavaScript client.
//
Object.defineProperty(Primus.prototype, 'client', {
get: function read() {
if (!read.primus) {
read.primus = fs.readFileSync(__dirname + '/dist/primus.js', 'utf-8');
}
return read.primus;
}
});
//
// Lazy compile the primus.js JavaScript client for Node.js
//
Object.defineProperty(Primus.prototype, 'Socket', {
get: function () {
return require('load').compiler(this.library(true), 'primus.js', {
__dirname: process.cwd(),
__filename: 'primus.js',
//
// The following globals are introduced so libraries that use `instanceof`
// checks for type checking do not fail as `load` runs the code in a new
// context.
//
Uint8Array: Uint8Array,
Object: Object,
RegExp: RegExp,
Array: Array,
Date: Date
}).Primus;
}
});
//
// Expose the current version number.
//
Primus.prototype.version = require('./package.json').version;
//
// A list of supported transformers and the required Node.js modules.
//
Primus.transformers = require('./transformers.json');
Primus.parsers = require('./parsers.json');
/**
* Simple function to output common errors.
*
* @param {String} what What is missing.
* @param {Object} where Either Primus.parsers or Primus.transformers.
* @returns {Object}
* @api private
*/
Primus.readable('is', function is(what, where) {
var missing = Primus.parsers !== where
? 'transformer'
: 'parser'
, dependency = where[what];
return {
missing: function write() {
console.error('Primus:');
console.error('Primus: Missing required npm dependency for '+ what);
console.error('Primus: Please run the following command and try again:');
console.error('Primus:');
console.error('Primus: npm install --save %s', dependency.server);
console.error('Primus:');
return 'Missing dependencies for '+ missing +': "'+ what + '"';
},
unknown: function write() {
console.error('Primus:');
console.error('Primus: Unsupported %s: "%s"', missing, what);
console.error('Primus: We only support the following %ss:', missing);
console.error('Primus:');
console.error('Primus: %s', Object.keys(where).join(', '));
console.error('Primus:');
return 'Unsupported '+ missing +': "'+ what +'"';
}
};
});
/**
* Initialise the real-time transport that was chosen.
*
* @param {Mixed} Transformer The name of the transformer or a constructor;
* @param {Object} options Options.
* @api private
*/
Primus.readable('initialise', function initialise(Transformer, options) {
Transformer = Transformer || 'websockets';
var primus = this
, transformer;
if ('string' === typeof Transformer) {
log('transformer `%s` is a string, attempting to resolve location', Transformer);
Transformer = transformer = Transformer.toLowerCase();
this.spec.transformer = transformer;
//
// This is a unknown transporter, it could be people made a typo.
//
if (!(Transformer in Primus.transformers)) {
log('the supplied transformer %s is not supported, please use %s', transformer, Primus.transformers);
throw new PrimusError(this.is(Transformer, Primus.transformers).unknown(), this);
}
try {
Transformer = require('./transformers/'+ transformer);
this.transformer = new Transformer(this);
} catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
log('the supplied transformer `%s` is missing', transformer);
throw new PrimusError(this.is(transformer, Primus.transformers).missing(), this);
} else {
log(e);
throw e;
}
}
} else {
log('received a custom transformer');
this.spec.transformer = 'custom';
}
if ('function' !== typeof Transformer) {
throw new PrimusError('The given transformer is not a constructor', this);
}
this.transformer = this.transformer || new Transformer(this);
this.on('connection', function connection(stream) {
this.connected++;
this.connections[stream.id] = stream;
log('connection: %s currently serving %d concurrent', stream.id, this.connected);
});
this.on('disconnection', function disconnected(stream) {
this.connected--;
delete this.connections[stream.id];
log('disconnection: %s currently serving %d concurrent', stream.id, this.connected);
});
//
// Add our default middleware layers.
//
this.before('forwarded', require('./middleware/forwarded'));
this.before('cors', require('./middleware/access-control'));
this.before('primus.js', require('./middleware/primus'));
this.before('spec', require('./middleware/spec'));
this.before('x-xss', require('./middleware/xss'));
this.before('no-cache', require('./middleware/no-cache'));
this.before('authorization', require('./middleware/authorization'));
//
// Emit the initialised event after the next tick so we have some time to
// attach listeners.
//
process.nextTick(function tock() {
primus.emit('initialised', primus.transformer, primus.parser, options);
});
});
/**
* Add a new authorization handler.
*
* @param {Function} auth The authorization handler.
* @returns {Primus}
* @api public
*/
Primus.readable('authorize', function authorize(auth) {
if ('function' !== typeof auth) {
throw new PrimusError('Authorize only accepts functions', this);
}
if (auth.length < 2) {
throw new PrimusError('Authorize function requires more arguments', this);
}
log('setting an authorization function');
this.auth = auth;
return this;
});
/**
* Iterate over the connections.
*
* @param {Function} fn The function that is called every iteration.
* @param {Function} done Optional callback, if you want to iterate asynchronously.
* @returns {Primus}
* @api public
*/
Primus.readable('forEach', function forEach(fn, done) {
if (!done) {
for (var id in this.connections) {
if (fn(this.spark(id), id, this.connections) === false) break;
}
return this;
}
var ids = Object.keys(this.connections)
, primus = this;
log('iterating over %d connections', ids.length);
function pushId(spark) {
ids.push(spark.id);
}
//
// We are going to iterate through the connections asynchronously so
// we should handle new connections as they come in.
//
primus.on('connection', pushId);
(function iterate() {
var id = ids.shift()
, spark;
if (!id) {
primus.removeListener('connection', pushId);
return done();
}
spark = primus.spark(id);
//
// The connection may have already been closed.
//
if (!spark) return iterate();
fn(spark, function next(err, forward) {
if (err || forward === false) {
primus.removeListener('connection', pushId);
return done(err);
}
iterate();
});
}());
return this;
});
/**
* Broadcast the message to all connections.
*
* @param {Mixed} data The data you want to send.
* @returns {Primus}
* @api public
*/
Primus.readable('write', function write(data) {
this.forEach(function forEach(spark) {
spark.write(data);
});
return this;
});
/**
* Install message parsers.
*
* @param {Mixed} parser Parse name or parser Object.
* @returns {Primus}
* @api private
*/
Primus.readable('parsers', function parsers(parser) {
parser = parser || 'json';
if ('string' === typeof parser) {
log('transformer `%s` is a string, attempting to resolve location', parser);
parser = parser.toLowerCase();
this.spec.parser = parser;
//
// This is a unknown parser, it could be people made a typo.
//
if (!(parser in Primus.parsers)) {
log('the supplied parser `%s` is not supported please use %s', parser, Primus.parsers);
throw new PrimusError(this.is(parser, Primus.parsers).unknown(), this);
}
try { parser = require('./parsers/'+ parser); }
catch (e) {
if (e.code === 'MODULE_NOT_FOUND') {
log('the supplied parser `%s` is missing', parser);
throw new PrimusError(this.is(parser, Primus.parsers).missing(), this);
} else {
log(e);
throw e;
}
}
} else {
this.spec.parser = 'custom';
}
if ('object' !== typeof parser) {
throw new PrimusError('The given parser is not an Object', this);
}
this.encoder = parser.encoder;
this.decoder = parser.decoder;
this.parser = parser;
return this;
});
/**
* Register a new message transformer. This allows you to easily manipulate incoming
* and outgoing data which is particularity handy for plugins that want to send
* meta data together with the messages.
*
* @param {String} type Incoming or outgoing
* @param {Function} fn A new message transformer.
* @returns {Primus}
* @api public
*/
Primus.readable('transform', function transform(type, fn) {
if (!(type in this.transformers)) {
throw new PrimusError('Invalid transformer type', this);
}
if (~this.transformers[type].indexOf(fn)) {
log('the %s message transformer already exists, not adding it', type);
return this;
}
this.transformers[type].push(fn);
return this;
});
/**
* Gets a spark by its id.
*
* @param {String} id The spark's id.
* @returns {Spark}
* @api private
*/
Primus.prototype.spark = function spark(id) {
return this.connections[id];
};
/**
* Generate a client library.
*
* @param {Boolean} nodejs Don't include the library, as we're running on Node.js.
* @returns {String} The client library.
* @api public
*/
Primus.readable('library', function compile(nodejs) {
var library = [ !nodejs ? this.transformer.library : null ]
, global = this.options.global || 'Primus'
, transport = this.transformer.client
, parser = this.parser.library || ''
, client = this.client;
//
// Add a simple export wrapper so it can be used as Node.js, AMD or browser
// client.
//
client = [
'(function UMDish(name, context, definition, plugins) {',
' context[name] = definition.call(context);',
' for (var i = 0; i < plugins.length; i++) {',
' plugins[i](context[name])',
' }',
' if (typeof module !== "undefined" && module.exports) {',
' module.exports = context[name];',
' } else if (typeof define === "function" && define.amd) {',
' define(function reference() { return context[name]; });',
' }',
'})("'+ global +'", this || {}, function wrapper() {',
' var define, module, exports',
' , Primus = '+ client.slice(client.indexOf('return ') + 7, -4) +';',
''
].join('\n');
//
// Replace some basic content.
//
client = client
.replace('null; // @import {primus::pathname}', '"'+ this.pathname.toString() +'"')
.replace('null; // @import {primus::version}', '"'+ this.version +'"')
.replace('null; // @import {primus::transport}', transport.toString())
.replace('null; // @import {primus::auth}', (!!this.auth).toString())
.replace('null; // @import {primus::encoder}', this.encoder.toString())
.replace('null; // @import {primus::decoder}', this.decoder.toString());
//
// As we're given a timeout value on the server side, we need to update the
// `ping` interval of the client to ensure that we've sent the server
// a message before the timeout gets triggered and we get disconnected.
//
if (this.timeout) {
log('updating the default value of the client `ping` option');
client = client.replace(
'options.ping : 25e3;',
'options.ping : '+ (this.timeout - 10000) +';'
);
} else {
log('setting the default value of the client `ping` option to `false`');
client = client.replace('options.ping : 25e3;', 'options.ping : false;');
}
//
// Add the parser inside the closure, to prevent global leaking.
//
if (parser && parser.length) {
log('adding parser to the client file');
client += parser;
}
//
// Iterate over the parsers, and register the client side plugins. If there's
// a library bundled, add it the library array as there were some issues with
// frameworks that get included in module wrapper as it forces strict mode.
//
var name, plugin;
for (name in this.ark) {
plugin = this.ark[name];
name = JSON.stringify(name);
if (plugin.library) {
log('adding the library of the %s plugin to the client file', name);
library.push(plugin.library);
}
if (!plugin.client) continue;
log('adding the client code of the %s plugin to the client file', name);
client += 'Primus.prototype.ark['+ name +'] = '+ plugin.client.toString() +';\n';
}
//
// Close the export wrapper and return the client. If we need to add
// a library, we should add them after we've created our closure and module
// exports. Some libraries seem to fail hard once they are wrapped in our
// closure so I'll rather expose a global variable instead of having to monkey
// patch too much code.
//
return client + [
' return Primus;',
'},',
'['
].concat(library.filter(Boolean).map(function expose(library) {
return [
'function (Primus) {',
library,
'}'
].join('\n');
}).join(',\n'))
.concat(']);')
.join('\n');
});
/**
* Save the library to disk.
*
* @param {String} dir The location that we need to save the library.
* @param {function} fn Optional callback, if you want an async save.
* @returns {Primus}
* @api public
*/
Primus.readable('save', function save(path, fn) {
if (!fn) fs.writeFileSync(path, this.library(), 'utf-8');
else fs.writeFile(path, this.library(), 'utf-8', fn);
return this;
});
/**
* Register a new Primus plugin.
*
* ```js
* primus.use('ack', {
* //
* // Only ran on the server.
* //
* server: function (primus, options) {
* // do stuff
* },
*
* //
* // Runs on the client, it's automatically bundled.
* //
* client: function (primus, options) {
* // do client stuff
* },
*
* //
* // Optional library that needs to be bundled on the client (should be a string)
* //
* library: ''
* });
* ```
*
* @param {String} name The name of the plugin.
* @param {Object} energon The plugin that contains client and server extensions.
* @returns {Primus}
* @api public
*/
Primus.readable('use', function use(name, energon) {
if ('object' === typeof name && !energon) {
energon = name;
name = energon.name;
}
if (!name) {
throw new PrimusError('Plugin should be specified with a name', this);
}
if ('string' !== typeof name) {
throw new PrimusError('Plugin names should be a string', this);
}
if ('string' === typeof energon) {
log('plugin was passed as a string, attempting to require %s', energon);
energon = require(energon);
}
//
// Plugin accepts an object or a function only.
//
if (!/^(object|function)$/.test(typeof energon)) {
throw new PrimusError('Plugin should be an object or function', this);
}
//
// Plugin require a client, server or both to be specified in the object.
//
if (!energon.server && !energon.client) {
throw new PrimusError('The plugin is missing a client or server function', this);
}
//
// Don't allow duplicate plugins or plugin override as this is most likely
// unintentional.
//
if (name in this.ark) {
throw new PrimusError('The plugin name was already defined', this);
}
log('adding %s as new plugin', name);
this.ark[name] = energon;
this.emit('plugin', name, energon);
if (!energon.server) return this;
log('calling the %s plugin\'s server code', name);
energon.server.call(this, this, this.options);
return this;
});
/**
* Return the given plugin.
*
* @param {String} name The name of the plugin.
* @returns {Mixed}
* @api public
*/
Primus.readable('plugin', function plugin(name) {
if (name) return this.ark[name];
var plugins = {};
for (name in this.ark) {
plugins[name] = this.ark[name];
}
return plugins;
});
/**
* Remove plugin from the ark.
*
* @param {String} name Name of the plugin we need to remove from the ark.
* @returns {Boolean} Successful removal of the plugin.
* @api public
*/
Primus.readable('plugout', function plugout(name) {
if (!(name in this.ark)) return false;
this.emit('plugout', name, this.ark[name]);
delete this.ark[name];
return true;
});
/**
* Add a new middleware layer. If no middleware name has been provided we will
* attempt to take the name of the supplied function. If that fails, well fuck,
* just random id it.
*
* @param {String} name The name of the middleware.
* @param {Function} fn The middleware that's called each time.
* @param {Object} options Middleware configuration.
* @param {Number} level 0 based optional index for the middleware.
* @returns {Primus}
* @api public
*/
Primus.readable('before', function before(name, fn, options, level) {
if ('function' === typeof name) {
level = options;
options = fn;
fn = name;
name = fn.name || 'pid_'+ Date.now();
}
if (!level && 'number' === typeof options) {
level = options;
options = {};
}
options = options || {};
//
// No or only 1 argument means that we need to initialise the middleware, this
// is a special initialisation process where we pass in a reference to the
// initialised Primus instance so a pre-compiling process can be done.
//
if (fn.length < 2) {
log('automatically configuring middleware `%s`', name);
fn = fn.call(this, options);
}
//
// Make sure that we have a function that takes at least 2 arguments.
//
if ('function' !== typeof fn || fn.length < 2) {
throw new PrimusError('Middleware should be a function that accepts at least 2 args');
}
var layer = {
length: fn.length, // Amount of arguments indicates if it's async.
enabled: true, // Middleware is enabled by default.
name: name, // Used for lookups.
fn: fn // The actual middleware.
}, index = this.indexOfLayer(name);
//
// Override middleware layer if we already have a middleware layer with
// exactly the same name.
//
if (!~index) {
if (level >= 0 && level < this.layers.length) {
log('adding middleware `%s` to the supplied index at %d', name, level);
this.layers.splice(level, 0, layer);
} else {
this.layers.push(layer);
}
} else {
this.layers[index] = layer;
}
return this;
});
/**
* Remove a middleware layer from the stack.
*
* @param {String} name The name of the middleware.
* @returns {Primus}
* @api public
*/
Primus.readable('remove', function remove(name) {
var index = this.indexOfLayer(name);
if (~index) {
log('removing middleware `%s`', name);
this.layers.splice(index, 1);
}
return this;
});
/**
* Enable a given middleware layer.
*
* @param {String} name The name of the middleware.
* @returns {Primus}
* @api public
*/
Primus.readable('enable', function enable(name) {
var index = this.indexOfLayer(name);
if (~index) {
log('enabling middleware `%s`', name);
this.layers[index].enabled = true;
}
return this;
});
/**
* Disable a given middleware layer.
*
* @param {String} name The name of the middleware.
* @returns {Primus}
* @api public
*/
Primus.readable('disable', function disable(name) {
var index = this.indexOfLayer(name);
if (~index) {
log('disabling middleware `%s`', name);
this.layers[index].enabled = false;
}
return this;
});
/**
* Find the index of a given middleware layer by name.
*
* @param {String} name The name of the layer.
* @returns {Number}
* @api private
*/
Primus.readable('indexOfLayer', function indexOfLayer(name) {
for (var i = 0, length = this.layers.length; i < length; i++) {
if (this.layers[i].name === name) return i;
}
return -1;
});
/**
* Destroy the created Primus instance.
*
* Options:
* - close (boolean) Close the given server.
* - reconnect (boolean) Trigger a client-side reconnect.
* - timeout (number) Close all active connections after x milliseconds.
*
* @param {Object} options Destruction instructions.
* @param {Function} fn Callback.
* @returns {Primus}
* @api public
*/
Primus.readable('destroy', function destroy(options, fn) {
if ('function' === typeof options) {
fn = options;
options = null;
}
options = options || {};
if (options.reconnect) options.close = true;
var primus = this;
setTimeout(function close() {
var transformer = primus.transformer;
//
// Ensure that the transformer receives the `close` event only once.
//
if (transformer) transformer.ultron.destroy();
//
// Close the connections that are left open.
//
primus.forEach(function shutdown(spark) {
spark.end(undefined, { reconnect: options.reconnect });
});
if (options.close !== false) {
//
// Closing a server that isn't started yet would throw an error.
//
try {
primus.server.close(function closed() {
primus.close(options, fn);
});
return;
}
catch (e) {}
}
primus.close(options, fn);
}, +options.timeout || 0);
return this;
});
/**
* Free resources after emitting a final `close` event.
*
* @param {Object} options Destruction instructions.
* @param {Function} fn Callback.
* @returns {Primus}
* @api private
*/
Primus.readable('close', function close(options, fn) {
var primus = this;
//
// Emit a final `close` event before removing all the listeners
// from all the event emitters.
//
primus.asyncemit('close', options, function done(err) {
if (err) {
if (fn) return fn(err);
throw err;
}
var transformer = primus.transformer
, server = primus.server;
//
// If we don't have a server we are most likely destroying an already
// destroyed Primus instance.
//
if (!server) return fn && fn();
server.removeAllListeners('request');
server.removeAllListeners('upgrade');
//
// Re-add the original listeners so that the server can be used again.
//
transformer.listeners('previous::request').forEach(function add(listener) {
server.on('request', listener);
});
transformer.listeners('previous::upgrade').forEach(function add(listener) {
server.on('upgrade', listener);
});
transformer.emit('close', options);
transformer.removeAllListeners();
primus.removeAllListeners();
//
// Null some potentially heavy objects to free some more memory instantly.
//
primus.transformers.outgoing.length = primus.transformers.incoming.length = 0;
primus.transformer = primus.encoder = primus.decoder = primus.server = null;
primus.connected = 0;
primus.connections = Object.create(null);
primus.ark = Object.create(null);
if (fn) fn();
});
return this;
});
/**
* Async emit an event. We make a really broad assumption here and that is they
* have the same amount of arguments as the supplied arguments (excluding the
* event name).
*
* @returns {Primus}
* @api private
*/
Primus.readable('asyncemit', require('asyncemit'));
//
// Alias for destroy.
//
Primus.readable('end', Primus.prototype.destroy);
/**
* Checks if the given event is an emitted event by Primus.
*
* @param {String} evt The event name.
* @returns {Boolean}
* @api public
*/
Primus.readable('reserved', function reserved(evt) {
return (/^(incoming|outgoing)::/).test(evt)
|| evt in reserved.events;
});
/**
* The actual events that are used by Primus.
*
* @type {Object}
* @api public
*/
Primus.prototype.reserved.events = {
'disconnection': 1,
'initialised': 1,
'connection': 1,
'plugout': 1,
'plugin': 1,
'close': 1,
'log': 1
};
/**
* Add a createSocket interface so we can create a Server client with the
* specified `transformer` and `parser`.
*
* ```js
* var Socket = Primus.createSocket({ transformer: transformer, parser: parser })
* , socket = new Socket(url);
* ```
*
* @param {Object} options The transformer / parser we need.
* @returns {Socket}
* @api public
*/
Primus.createSocket = function createSocket(options) {
options = options || {};
var primus = new Primus(new EventEmitter(), options);
return primus.Socket;
};
/**
* Create a new Primus server.
*
* @param {Function} fn Request listener.
* @param {Object} options Configuration.
* @returns {Pipe}
* @api public
*/
Primus.createServer = function createServer(fn, options) {
if ('object' === typeof fn) {
options = fn;
fn = null;
}
options = options || {};
var server = require('create-server')(Primus.prototype.merge.call(Primus, {
http: function warn() {
if (!options.iknowhttpsisbetter) [
'',
'We\'ve detected that you\'re using a HTTP instead of a HTTPS server.',
'Please be aware that real-time connections have less chance of being blocked',
'by firewalls and anti-virus scanners if they are encrypted (using SSL). If',
'you run your server behind a reverse and HTTPS terminating proxy ignore',
'this message, if not, you\'ve been warned.',
''
].forEach(function each(line) {
console.log('primus: '+ line);
});
}
}, options));
//
// Now that we've got a server, we can setup the Primus and start listening.
//
var application = new Primus(server, options);
if (fn) application.on('connection', fn);
return application;
};
//
// Expose the constructors of our Spark and Transformer so it can be extended by
// a third party if needed.
//
Primus.Transformer = Transformer;
Primus.Spark = Spark;
//
// Expose the module.
//
module.exports = Primus;