thywill
Version:
A Node.js clustered framework for single page web applications based on asynchronous messaging.
1,006 lines (925 loc) • 35.9 kB
JavaScript
/**
* @fileOverview
* Class definition for the SocketIO-based clientInterface implementation.
*/
var util = require('util');
var fs = require('fs');
var urlHelpers = require('url');
var pathHelpers = require('path');
var async = require('async');
var handlebars = require('handlebars');
var io = require('socket.io');
var send = require('send');
var Thywill = require('thywill');
var Message = require('./message');
var Client = require('./client');
// -----------------------------------------------------------
// Class Definition
// -----------------------------------------------------------
/**
* @class
* A web browser client interface built on top of Socket.IO. See
* http://socket.io/ for details.
*
* This manages the server side of a single page web application that uses
* websockets as means of passing messages back and forth between server and
* client. It does the following:
*
* 1) Set up the Resources delivered to the client on the initial page load,
* including the initial page HTML.
* 2) Deliver messages between client and server via websockets.
*
* This implementation does not provide sessions; client information uses
* connection IDs as session IDs and includes no session instance.
*/
function SocketIoClientInterface () {
SocketIoClientInterface.super_.call(this);
this.socketFactory = null;
this.bootstrapResourceClientPaths = [];
// Convenience reference.
this.templates = SocketIoClientInterface.TEMPLATES;
}
util.inherits(SocketIoClientInterface, Thywill.getBaseClass('ClientInterface'));
var p = SocketIoClientInterface.prototype;
// -----------------------------------------------------------
// 'Static' parameters
// -----------------------------------------------------------
SocketIoClientInterface.CONFIG_TEMPLATE = {
baseClientPath: {
_configInfo: {
description: 'The base path for all Thywill URLs with a leading but no trailing slash. e.g. "/thywill".',
types: 'string',
required: true
}
},
minifyCss: {
_configInfo: {
description: 'If true, merge and minify CSS resources.',
types: 'boolean',
required: true
}
},
minifyJavascript : {
_configInfo: {
description: 'If true, merge and minify Javascript resources.',
types: 'boolean',
required: true
}
},
namespace : {
_configInfo: {
description: 'Socket.IO allows connection multiplexing by assigning a namespace; this is generally a good idea.',
types: 'string',
required: true
}
},
pageEncoding: {
_configInfo: {
description: 'The content encoding for the web page provided by the client interface.',
types: 'string',
required: true
}
},
server: {
server: {
_configInfo: {
description: 'An http.Server instance.',
types: 'object',
required: true
}
}
},
socketClientConfig: {
_configInfo: {
description: 'Container object for Socket.IO client configuration parameters.',
types: 'object',
required: false
}
},
socketConfig: {
_configInfo: {
description: 'Container object for environment-specific Socket.IO configuration objects.',
types: 'object',
required: false
}
},
textEncoding: {
_configInfo: {
description: 'The content encoding for text resources.',
types: 'string',
required: true
}
},
upCheckClientPath: {
_configInfo: {
description: 'The client path for a page that declares the process running - for use with proxy and monitoring checks.',
types: 'string',
required: true
}
},
usePubSubForSending: {
_configInfo: {
description: 'If true each connection is signed up for its own pub/sub channel and messages are sent that way. This and configuring Socket.IO to use a RedisStore is usually necessary for applications with multiple backend Node.js processes.',
types: 'boolean',
required: true
}
}
};
// Handlebars templates for assembling various odds and ends. The spacing is for
// the most common use in the HTML <head> element.
SocketIoClientInterface.TEMPLATES = {
bootstrapCss: '<style type="text/css" media="all">' +
'\n{{#each resources.[text/css]}} @import url("{{{clientPath}}}");\n{{/each}}' +
' </style>',
bootstrapJs: '{{#each resources.[application/javascript]}}' +
'<script type="text/javascript" src="{{{clientPath}}}"></script>\n {{/each}}',
// 'this' gives you the template Resource object and calls toString() on it.
bootstrapTemplates: '{{#each resources.[text/template]}}' +
'<script type="text/template" id="{{{id}}}">\n {{{this}}} </script>\n {{/each}}'
};
// Compile the templates here rather than later.
for (var template in SocketIoClientInterface.TEMPLATES) {
SocketIoClientInterface.TEMPLATES[template] = handlebars.compile(SocketIoClientInterface.TEMPLATES[template]);
}
// -----------------------------------------------------------
// Initialization
// -----------------------------------------------------------
/**
* @see Component#_configure
*
* The clientInterface component is the last core component to be configured,
* and so it has access to fully configured instances of all the other core
* components and their data.
*/
p._configure = function (thywill, config, callback) {
var self = this;
this.thywill = thywill;
this.config = config;
// Create this once; it'll see a lot of use.
this.config.baseClientPathRegExp = new RegExp('^' + this.config.baseClientPath.replace('/', '\\/') + '\\/?');
// Make sure there's an object present for the <html> and <body> attributes.
this.config.elementAttributesBody = this.config.elementAttributesBody || {};
this.config.elementAttributesHtml = this.config.elementAttributesHtml || {};
// Reduce array attributes down to strings.
var name;
for (name in this.config.elementAttributesBody) {
if (Array.isArray(this.config.elementAttributesBody[name])) {
this.config.elementAttributesBody[name] = this.config.elementAttributesBody[name].join(' ');
}
}
for (name in this.config.elementAttributesHtml) {
if (Array.isArray(this.config.elementAttributesHtml[name])) {
this.config.elementAttributesHtml[name] = this.config.elementAttributesHtml[name].join(' ');
}
}
// -----------------------------------------------------------------
// Cluster configuration: communication between processes.
// -----------------------------------------------------------------
// Task names used internally by this clientInterface implementation.
this.clusterTask = {
// Request to subscribe a client if they exist locally.
subscribe: 'thywill:clientInterface:subscribeClient',
// Request to unsubscribe a client if they exist locally.
unsubscribe: 'thywill:clientInterface:unsubscribeClient'
};
// Subscribe a connectionId if it exists locally.
this.thywill.cluster.on(this.clusterTask.subscribe, function (data) {
self._subscribeLocalSocket(data.connectionId, data.channelIds);
});
// Subscribe a connectionId if it exists locally.
this.thywill.cluster.on(this.clusterTask.unsubscribe, function (data) {
self._unsubscribeLocalSocket(data.connectionId, data.channelIds);
});
// -----------------------------------------------------------------
// Finish up configuration.
// -----------------------------------------------------------------
// Set ready status.
this.readyCallback = callback;
this._announceReady(this.NO_ERRORS);
};
/**
* Start the client interface running. This is the final function called during
* the Thywill launch, once everything else is in place and ready.
*
* @param {Function} callback
* Of the form function (error).
*/
p._startup = function (callback) {
this._setHttpServerToServeResources();
this._initializeSocketIo();
this._setupBootstrapResources(callback);
};
/**
* Assemble all of the bootstrap resources to be delivered to the client on the
* first page load.
*
* @param {Function} callback
* Of the form function (error).
*/
p._setupBootstrapResources = function (callback) {
var self = this;
var resourceManager = this.thywill.resourceManager;
// Set up an array of functions to be executed in series that will build the
// array of bootstrap resources needed for the main Thywill page.
var resources = [];
/**
* Helper function for creating and storing a bootstrap resource.
*
* @param {string} relativePath
* A relative path from this file to the file to be loaded.
* @param {Object} attributes
* An attributes object for creating a resource - see the Resource class
* for more information.
* @param {Function} callback
* Of the form function (error) where error === null on success.
*/
function createBootstrapResourceFromFile (relativePath, attributes, callback) {
// Add some attributes.
attributes.originFilePath = pathHelpers.resolve(__dirname, relativePath);
// Create the resource and pass it back out in the callback.
resourceManager.createResourceFromFile(attributes.originFilePath, attributes, function (error, resource) {
if (error) {
callback.call(self, error);
} else {
self.storeBootstrapResource(resource, callback);
}
});
}
// Array of setup functions to be called in series.
var fns = {
// Create a bootstrap Resource for the socket.IO client code. Make sure it's
// first.
createSocketIoJSResource: function (asyncCallback) {
var path = '../../../../node_modules/socket.io-client/dist/socket.io.min.js';
var originFilePath = pathHelpers.resolve(__dirname, path);
var data = fs.readFileSync(originFilePath, self.config.textEncoding);
// Generate a resource.
var resource = resourceManager.createResource(data, {
clientPath: self.config.baseClientPath + '/js/socket.io.js',
encoding: self.config.textEncoding,
originFilePath: originFilePath,
type: resourceManager.types.JAVASCRIPT,
weight: -99999
});
self.storeBootstrapResource(resource, asyncCallback);
},
// Create a Resource for the main client-side Thywill Javascript. We are
// passing in some additional code and values via templating.
createMainThywillJsResource: function (asyncCallback) {
var path = '../../../client/socketIoClientInterface/thywill.js';
var originFilePath = pathHelpers.resolve(__dirname, path);
var data = fs.readFileSync(originFilePath, self.config.textEncoding);
var thywillTemplate = handlebars.compile(data);
// Template parameters.
var params = {
messageClass: self.classToCodeString(Message, ' ', ' '),
namespace: self.config.namespace,
config: JSON.stringify({})
};
if (self.config.socketClientConfig) {
params.config = JSON.stringify(self.config.socketClientConfig);
}
// Generate a Resource from the rendered template.
var resource = resourceManager.createResource(thywillTemplate(params), {
clientPath: self.config.baseClientPath + '/js/thywill.js',
encoding: self.config.textEncoding,
originFilePath: originFilePath,
type: resourceManager.types.JAVASCRIPT,
weight: 0
});
self.storeBootstrapResource(resource, asyncCallback);
},
// Client-side Javascript for the ApplicationInterface.
createClientInterfaceJsResource: function (asyncCallback) {
var path = '../../../client/socketIoClientInterface/socketIoApplicationInterface.js';
createBootstrapResourceFromFile(path, {
clientPath: self.config.baseClientPath + '/js/applicationInterface.js',
encoding: self.config.textEncoding,
type: resourceManager.types.JAVASCRIPT,
weight: 1
}, asyncCallback);
},
// Load up bootstrap resources defined here and elsewhere (i.e. in
// applications) and stash them in an accessible array.
loadAllBootstrapResources: function (asyncCallback) {
self.getBootstrapResources(function(error, otherResources) {
if (Array.isArray(otherResources)) {
resources = resources.concat(otherResources);
}
asyncCallback(error);
});
},
// Order the bootstrap resources by weight.
orderAllBootstrapResources: function (asyncCallback) {
async.sortBy(resources, function (resource, innerAsyncCallback) {
innerAsyncCallback.call(self, null, resource.weight);
}, function(error, sortedResources) {
resources = sortedResources;
asyncCallback(error);
});
},
// If we are minifying and aggregating CSS and Javascript, then get to it.
minifyResources: function (asyncCallback) {
self.thywill.minifier.minifyResources(
resources,
self.config.minifyJavascript,
self.config.minifyCss,
function (error, minifiedResources, addedResources) {
// Replace the resource array with the new minified array.
resources = minifiedResources;
// Store any newly added resources, which should be the
// merged/minified Javascript and CSS.
async.forEach(addedResources, function (resource, innerAsyncCallback) {
self.storeResource(resource, innerAsyncCallback);
}, function (error) {
asyncCallback(error);
});
}
);
},
// Template all of the text/html Resources and then save them. The expected
// text/html Resource is the main application page, which needs CSS,
// Javascript, and template elements added.
//
// We use Handlebars to template up the necessary <style> and <script>
// element blocks into strings, and then use the defined Thywill template
// engine to render the page.
templateHtmlResources: function (asyncCallback) {
// Split out the assembled resources into arrays by type.
var resourcesByType = {};
for (var i = 0, length = resources.length; i < length; i++) {
if (!resourcesByType[resources[i].type]) {
resourcesByType[resources[i].type] = [];
}
resourcesByType[resources[i].type].push(resources[i]);
}
// If there are no text/html bootstrap Resources then there is nothing to
// do here. This shouldn't happen: every service setup should provide at
// least one text/html page as a bootstrap Resource.
if (!resourcesByType[resourceManager.types.HTML]) {
self.thywill.log.warn('No text/html bootstrap Resource is defined. There should be at least one to act as the landing page.');
asyncCallback();
return;
}
// Assemble the locals for the resource HTML and the full page template
// by running the resources by type through the Handlebars templates.
// This produces the <style> and <script> elements for the page header.
var resourceLocals = {
resources : resourcesByType
};
var htmlLocals = {
encoding : self.config.pageEncoding
};
for (var name in self.templates) {
htmlLocals[name] = self.templates[name](resourceLocals);
}
// Run through the text/html Resources, template them, and update them.
async.forEach(resourcesByType[resourceManager.types.HTML], function (resource, innerAsyncCallback) {
var template = resource.toString();
var html = self.thywill.templateEngine.render(resource.toString(), htmlLocals);
resource.buffer = new Buffer(html, resource.encoding);
self.storeResource(resource, innerAsyncCallback);
}, asyncCallback);
},
// Create a trivial resource to be used for up-checks by a proxy server.
createUpCheckResource: function (asyncCallback) {
var resource = new Buffer ('{ alive: true }', self.config.textEncoding);
resource = resourceManager.createResource(resource, {
clientPath: self.config.baseClientPath + self.config.upCheckClientPath,
encoding: self.config.textEncoding,
type: resourceManager.types.JSON
});
self.storeResource(resource, asyncCallback);
}
};
// Call the above functions in series. Any errors will abort part-way through
// and be passed back in the callback.
async.series(fns, callback);
};
/**
* Take over the listeners already added to the http.Server passed in
* configuration, and have it serve Resources.
*
* We only need to do this if Express is not being used.
*/
p._setHttpServerToServeResources = function () {
var self = this;
// Remove all existing listeners from the server, while keeping a copy of
// them.
this.serverListeners = this.config.server.server.listeners('request').splice(0);
this.config.server.server.removeAllListeners('request');
/**
* Helper function - send the request on to other listeners.
*/
function passToOtherListeners (req, res) {
self.serverListeners.forEach(function (listener, index, array) {
listener.call(self.config.server.server, req, res);
});
}
// Set our own listener to manage resource requests.
this.config.server.server.on('request', function (req, res) {
// Is this request in the right base path?
if (!req.url.match(self.config.baseClientPathRegExp)) {
passToOtherListeners(req, res);
return;
}
// But do we have a resource for this?
var requestData = urlHelpers.parse(req.url);
self.getResource(requestData.pathname, function (error, resource) {
// If there's an error or a resource, handle it.
if (error || resource) {
self.handleResourceRequest(req, res, error, resource);
} else {
passToOtherListeners(req, res);
}
});
});
};
/**
* Set Socket.IO running on the http.Server instance.
*/
p._initializeSocketIo = function () {
var self = this;
// Establish Socket.IO on the server instance.
this.config.socketConfig = this.config.socketConfig || {};
this.config.socketConfig.global = this.config.socketConfig.global || {};
this.socketFactory = io.listen(this.config.server.server, this.config.socketConfig.global);
// Now walk through to apply whatever environment-specific Socket.IO
// configuration is provided in the configuration object. These
// configuration properties will override the general ones, but will
// only be used if the NODE_ENV environment variable matches the
// environmentName - e.g. 'production', 'development', etc.
var environmentConfig = this.config.socketConfig[process.env.NODE_ENV];
if (environmentConfig) {
for (var property in environmentConfig) {
this.socketFactory.set(property, environmentConfig[property]);
}
}
// Tell socket.io what to do when a connection starts - this will kick off the
// necessary setup for an ongoing connection with a client.
this.socketFactory.of(this.config.namespace).on('connection', function (socket) {
self.initializeNewSocketConnection(socket);
});
};
// -----------------------------------------------------------
// Connection and serving data methods
// -----------------------------------------------------------
/**
* A new connection is made over Socket.IO, so sort out what needs to be done
* with it, and set up its ability to send and receive.
*
* @param {Object} socket
* If the clientInterface is configured to use sessions, then we are
* expecting a socket object with socket.handshake,
* socket.handshake.session, and socket.handshake.sessionId, where session is
* an Express session.
*/
p.initializeNewSocketConnection = function (socket) {
var self = this;
var data = this.connectionDataFromSocket(socket);
// If we're sending via pub/sub, then sign this socket up for its own channel.
if (this.config.usePubSubForSending) {
socket.join(socket.id);
}
/**
* A message arrives and the raw data object from Socket.IO code is passed in.
*/
socket.on('fromClient', function (applicationId, rawMessage) {
if (!applicationId || !rawMessage || typeof rawMessage !== 'object') {
self.thywill.log.debug('Invalid message emit arguments from connection: ' + data.connectionId + ' for application ' + applicationId + ' with contents: ' + JSON.stringify(arguments));
return;
}
var message = new Message(rawMessage.data, rawMessage._);
// If valid now, send it onward.
if (message.isValid()) {
// Don't pass the session from the data, as it'll be out of date.
var client = new Client({
connectionId: data.connectionId,
sessionId: data.sessionId
});
self.emit(self.events.FROM_CLIENT, client, applicationId, message);
} else {
self.thywill.log.debug('Empty or broken message received from connection: ' + data.connectionId + ' for application ' + applicationId + ' with contents: ' + JSON.stringify(rawMessage));
}
});
/**
* The socket disconnects. Note that in practice this event is unreliable -
* it works nearly all of the time, but not all of the time.
*/
socket.on('disconnect', function() {
// Don't pass the session from the data, as it'll be out of date.
var client = new Client({
connectionId: data.connectionId,
sessionId: data.sessionId
});
self.emit(self.events.DISCONNECTION, client);
});
// Socket.ioconnections will try to reconnect automatically if configured
// to do so, and emit this event on successful reconnection.
/*
socket.on('reconnect', function() {
// Do nothing here, as a reconnection also emits 'connect', and so it'll
// be handled. This code is left as a reminder that this happens.
});
*/
// Finally, emit a notice.
this.emit(this.events.CONNECTION, new Client(data));
};
/**
* Extract the connection data we're going to be using from the socket.
*
* @param {Object} socket
* A socket.
* @param {Object}
*/
p.connectionDataFromSocket = function (socket) {
var data = {
connectionId: socket.id,
// Use the socket ID as a sessionID. As remarked elsewhere, not very
// useful for anything other than example code, as it will change if
// the socket disconnects and then reconnects.
sessionId: socket.id,
session: undefined
};
return data;
};
/**
* Handle a request.
*
* @param {Object} req
* Request object from the server.
* @param {Object} res
* Response object from the server.
* @param {mixed} error
* Any error resulting from finding the resource.
* @param {Resource} resource
* The resource to server up.
*/
p.handleResourceRequest = function (req, res, error, resource) {
var self = this;
if (error) {
this._send500ResourceResponse(req, res, error);
return;
} else if (!resource) {
this._send500ResourceResponse(req, res, new Error('Missing resource.'));
return;
}
if (resource.isInMemory()) {
res.setHeader('Content-Type', resource.type);
// TODO: Using the buffer length is only going to be correct if people
// always use right-sized buffers for the content they contain.
res.setHeader('Content-Length', resource.buffer.length);
res.end(resource.buffer);
} else if (resource.isPiped()) {
res.setHeader('Content-Type', resource.type);
// Define an error handler.
var errorHandler = function (error) {
self.thywill.log.error(error);
};
send(req, resource.filePath)
// TODO: maxage
//.maxage(options.maxAge || 0)
// Add an error handler.
.on('error', errorHandler)
// And remove the error handler once done.
.on('finish', function () {
req.socket.removeListener('error', errorHandler);
})
.pipe(res);
} else {
// This resource is probably not set up correctly. It should either have
// data in memory or be set up to be piped.
this._send500ResourceResponse(req, res, new Error('Resource incorrectly configured: ' + req.path));
}
};
/**
* Send a 500 error response down to the client when a resource is requested.
*
* @param {Object} req
* Request object from the server.
* @param {Object} res
* Response object from the server.
* @param {mixed} error
* The error.
*/
p._send500ResourceResponse = function (req, res, error) {
// TODO: better error responses, actual HTML would be nice.
this.thywill.log.error(error);
res.statusCode = 500;
res.end('Error loading resource.');
};
/**
* @see ClientInterface#sendToClient
*/
p.sendToConnection = function (applicationId, client, message) {
// If there are multiple Node processes in the backend and the application
// is structured such that process A may want to send a message to a socket
// connected to process B, then usePubSubForSending must be set true so
// that messages can be published to the individual connection's channel.
var connectionId = this._clientOrConnectionIdToConnectionId(client);
message = this._wrapAsMessage(message);
var destinationSocket = this.socketFactory.of(this.config.namespace).socket(connectionId);
if (destinationSocket) {
destinationSocket.emit('toClient', applicationId, message);
} else if (this.config.usePubSubForSending) {
this.socketFactory.of(this.config.namespace).to(connectionId).emit('toClient', applicationId, message);
} else {
this.thywill.log.debug('No socket connected to this process with id: ' + connectionId);
}
};
/**
* @see ClientInterface#sendToSession
*/
p.sendToSession = function (applicationId, client, message) {
// ClientTracker functionality is required to send to a session, as we
// need to determine all of the associated connections.
if (!this.thywill.clientTracker) {
this.thywill.log.error(new Error('Sending a message to all the connections for a session requires a clientTracker component.'));
return;
}
var sessionId = this._clientOrSessionIdToSessionId(client);
message = this._wrapAsMessage(message);
var self = this;
this.thywill.clientTracker.connectionIdsForSession(sessionId, function (error, connectionIds) {
if (error) {
self.thywill.log.error(error);
} else {
connectionIds.forEach(function (connectionId, index, array) {
self.sendToConnection(applicationId, connectionId, message);
});
}
});
};
/**
* @see ClientInterface#sendToChannel
*/
p.sendToChannel = function (applicationId, channelId, message, excludeClients) {
var self = this;
message = this._wrapAsMessage(message);
var s = this.socketFactory.of(this.config.namespace).to(channelId);
if (excludeClients) {
if (!Array.isArray(excludeClients)) {
excludeClients = [excludeClients];
}
excludeClients.forEach(function (client, index, array) {
// Using except() is the same as setting the broadcast flag on a socket
// instance when emitting. It will work even for clustered processes
// using a Redis store: the socket instance for socketId might not be
// available in this instance, as the client might be connected to
// another of the clustered Node.js processes.
s = s.except(self._clientOrConnectionIdToConnectionId(client));
});
}
s.emit('toClient', applicationId, message);
};
/**
* Ensure message data is wrapped in a Message instance.
*
* @param {mixed|Message} message
* The message data.
*/
p._wrapAsMessage = function (message) {
if (!(message instanceof Message)) {
message = new Message(message);
}
return message;
};
/**
* @see ClientInterface#subscribe
*/
p.subscribe = function (clients, channelIds, callback) {
var self = this;
if (!Array.isArray(clients)) {
clients = [clients];
}
if (!clients.length) {
callback();
return;
}
clients.forEach(function (client, index, array) {
var connectionId = self._clientOrConnectionIdToConnectionId(client);
// If a local subscription attempt returns true, then we're done.
if(self._subscribeLocalSocket(connectionId, channelIds)) {
return;
}
// Otherwise the connection isn't local, but we know which cluster member
// the socket is connected to, so tell it directly.
for (var clusterMemberId in self.connections) {
if (self.connections[clusterMemberId].connections[connectionId]) {
self.thywill.cluster.sendTo(clusterMemberId, self.clusterTask.subscribe, {
connectionId: connectionId,
channelIds: channelIds
});
}
}
});
callback();
};
/**
* @see ClientInterface#subscribe
*
* @return {boolean}
* True if there was a local socket with this connectionId.
*/
p._subscribeLocalSocket = function (connectionId, channelIds) {
var socket = this.socketFactory.of(this.config.namespace).socket(connectionId);
// Is this connection connected to this process?
if (socket) {
if (!Array.isArray(channelIds)) {
channelIds = [channelIds];
}
channelIds.forEach(function (channelId, index, array) {
socket.join(channelId);
});
return true;
} else {
return false;
}
};
/**
* @see ClientInterface#unsubscribe
*/
p.unsubscribe = function (clients, channelIds, callback) {
var self = this;
if (!Array.isArray(clients)) {
clients = [clients];
}
if (!clients.length) {
callback();
return;
}
clients.forEach(function (client, index, array) {
var connectionId = self._clientOrConnectionIdToConnectionId(client);
// If a local unsubscription attempt returns true, then we're done.
if(self._unsubscribeLocalSocket(connectionId, channelIds)) {
return;
}
// Otherwise the connection isn't local, but we know which cluster member
// the socket is connected to, so tell it directly.
for (var clusterMemberId in self.connections) {
if (self.connections[clusterMemberId].connections[connectionId]) {
self.thywill.cluster.sendTo(clusterMemberId, self.clusterTask.unsubscribe, {
connectionId: connectionId,
channelIds: channelIds
});
}
}
});
callback();
};
/**
* @see ClientInterface#unsubscribe
*
* @return {boolean}
* True if there was a local socket with this connectionId.
*/
p._unsubscribeLocalSocket = function (connectionId, channelIds) {
var socket = this.socketFactory.of(this.config.namespace).socket(connectionId);
// Is this connection connected to this process?
if (socket) {
if (!Array.isArray(channelIds)) {
channelIds = [channelIds];
}
channelIds.forEach(function (channelId, index, array) {
socket.leave(channelId);
});
return true;
} else {
return false;
}
};
/**
* @see ClientInterface#loadSession
*/
p.loadSession = function (client, callback) {
// Not using sessions.
callback();
};
/**
* @see ClientInterface#storeSession
*/
p.storeSession = function (client, session, callback) {
// Not using sessions.
callback();
};
// -----------------------------------------------------------
// Resource methods
// -----------------------------------------------------------
/**
* @see ClientInterface#storeBootstrapResource
*/
p.storeBootstrapResource = function (resource, callback) {
var self = this;
this.storeResource(resource, function (error, storedResource) {
if (!error) {
self.bootstrapResourceClientPaths.push(resource.clientPath);
}
callback(error, storedResource);
});
};
/**
* @see ClientInterface#getBootstrapResources
*/
p.getBootstrapResources = function (callback) {
var self = this;
// This will pass each element of bootstrapResourceClientPaths into the
// function (path, asyncCallback). The 2nd arguments passed to asyncCallback
// are assembled into an array to pass to the final callback - which will be
// called as callback(error, [resource, resource...])
async.concat(this.bootstrapResourceClientPaths, function (clientPath, asyncCallback) {
self.getResource(clientPath, asyncCallback);
}, callback);
};
/**
* @see ClientInterface#defineResource
*/
p.storeResource = function (resource, callback) {
var self = this;
this.thywill.resourceManager.store(resource.clientPath, resource, callback);
};
/**
* @see ClientInterface#getResource
*/
p.getResource = function (clientPath, callback) {
this.thywill.resourceManager.load(clientPath, callback);
};
// -----------------------------------------------------------
// Other utility methods
// -----------------------------------------------------------
/**
* Return a connection ID if given a Client instance or a connection ID.
*
* @param {Client|string} clientOrConnectionId
* Client instance or connection ID.
*/
p._clientOrConnectionIdToConnectionId = function (clientOrConnectionId) {
if (clientOrConnectionId instanceof Client) {
return clientOrConnectionId.getConnectionId();
} else {
return clientOrConnectionId;
}
};
/**
* Return a session ID if given a Client instance or a session Id.
*
* @param {Client|string} clientOrSessionId
* Client instance or session ID.
*/
p._clientOrSessionIdToSessionId = function (clientOrSessionId) {
if (clientOrSessionId instanceof Client) {
return clientOrSessionId.getSessionId();
} else {
return clientOrSessionId;
}
};
/**
* Given a class definition, create Javascript code for it. This is used to
* share some minor classes with the front end and save duplication of code.
*
* @param {function} classFunction
* A class definition.
* @param {string} [indent]
* Indent string, e.g. two spaces: ' '
* @param {string} [extraIndent]
* An additional indent to apply to all rows to get them to line up with
* whatever output they are put into. e.g. an extra two spaces ' ';
* @return {string}
* Javascript code.
*/
p.classToCodeString = function (classFunction, indent, extraIndent) {
indent = indent || ' ';
var code = '';
if (extraIndent) {
code += extraIndent;
}
code += classFunction.toString();
code += '\n\n';
function getPropertyCode(property) {
if (typeof property === 'function') {
return property.toString();
} else {
return JSON.stringify(property, null, indent);
}
}
var prop, propCode;
// 'Static' items.
for (prop in classFunction) {
propCode = getPropertyCode(classFunction[prop]);
code += classFunction.name + '.' + prop + ' = ' + propCode + ';\n';
}
code += '\n';
// Prototype functions.
for (prop in classFunction.prototype) {
propCode = getPropertyCode(classFunction.prototype[prop]);
code += classFunction.name + '.prototype.' + prop + ' = ' + propCode + ';\n\n';
}
// Add the extraIndent.
if (extraIndent) {
code = code.replace(/\n/g, '\n' + extraIndent);
}
return code;
};
// -----------------------------------------------------------
// Exports - Class Constructor
// -----------------------------------------------------------
module.exports = SocketIoClientInterface;