web-socket-router-client.js
'use strict';
/*
* Router Receives a connection String
*
* it will perform the conneciton to the server. Anytime the connection si closed
* it will try to reconnect. Until it fails several times.
*
*/
var WebSocketRouter = function WebSocketRouter(connection_query, ctx) {
//
var BEFORE = 'BEFORE';
//
var AFTER = 'AFTER;';
/*
* @name Connection Handler
*
* The connection handler will keep the connection with the server
* Established. If the connection is closed the connection handler will
* attempt to connect up to a maximum o 10 retries.
* Connection handler will also keep a connection queue in which
* messages delivered while the connection was closed, closing or opening
* will be re-sent as soon as the connection is ready.
*
* @constructor
* @param {String} connection_query
* Defines the FQDM to connect to (eg: ws(s)://localhost:8080)
* @param {Function} on_message
* Defines the callback function that will be exectued every time
* a new message is received from the server
* @return {ConnectionHandler}
*
*/
function ConnectionHandler(connection_query, on_message) {
// Connection query could be used to re-establish
// The connection in case is lots
this.connection_query = connection_query;
// Connection on message will be used to dispatch
// Messages
this.on_message = on_message;
// Holds any message dispatched before the connection
// was established
// Also, if the connection is closed, the WS, will Hold
// the message and try to reconnect to send it
this.messageQueue = [];
// Connection retry will hold the attempts
// Before giviging up 10 retries
this.connectionsAttemp = 0;
// Attemp to connect for the first time
this.connect();
}
/*
* @name Connect
*
* Establish the connection with the server and sets the
* on* listeners of the websocket object.
*
* @function
* @return {None}
*
*/
ConnectionHandler.prototype.connect = function () {
// Increase the attemp count by one to let know
// That a connection has been alread tried
this.connectionsAttemp++;
// The connection is established using WebSocket
// If the connection is closed, the Handler will try
// To reconnect
this.connection = new WebSocket(this.connection_query);
// Whenever the new connection is open, the connection
// Handler will flush the messageQueue
this.connection.onopen = this.flush.bind(this);
// Whenever the connection is closed, the WS
// Will try to reconnect up to 10 times.
this.connection.onclose = this.reconnect.bind(this);
// Connection on-message will dispatch all received
// Messages to the wrapper class. Messages will be dispatched
// Already parsed
this.connection.onmessage = this.dispatch.bind(this);
};
/*
* @name Dispatch
*
* Sends the new arrived message to the provided callback
*
* @function
* @return {None}
*
*/
ConnectionHandler.prototype.dispatch = function (message) {
this.on_message(JSON.parse(message.data));
};
/*
* @name Reconnect
*
* Attempts to reconnect to the server up to ten 10 times.
* Logs an error if the maximum retry limit is reached.
*
* @function
* @return {None}
*
*/
ConnectionHandler.prototype.reconnect = function () {
// If 10 attempts reached, web-socket-router will assume that
// the server is permanently down
if (this.connectionsAttemp < 10) {
// Waits for a second to make sure that,
// if the server was down for a moment, the connecction wont fail
setTimeout(this.connect.bind(this), 1000);
} else {
console.log('10 Attemps to connect failed to the server ' + this.connection_query + '. Is server up?');
}
};
/*
* @name Flush
*
* Execute all messages in the Message Queue
*
* @function
* @return {None}
*
*/
ConnectionHandler.prototype.flush = function () {
// Reset the connections attempt since the connection,
// Has been stablished sucesfully
this.connectionsAttemp = 0;
// Execute all queued messages
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = this.messageQueue[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var message = _step.value;
this.send(message);
}
// Empty the message Queue could provoke an issue when The
// messages beeing flushed are producing erros that push them back
// To the queue. Messages could be lost
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
this.messageQueue = [];
};
/*
* @name Send
*
* Sends the object as a stringified message to the server
* If the connection fails, stores the message in the message Queue
*
* @function
* @param {Object}
* Object containing all the data that will be sent to the server
* @return {None}
*
*/
ConnectionHandler.prototype.send = function (message) {
try {
this.connection.send(JSON.stringify(message));
} catch (err) {
this.messageQueue.push(message);
}
};
/*
* @name Close
*
* Closes the connection and clears all data related to the object
*
* @function
* @return {None}
*
*/
Router.prototype.close = function () {
this.messageQueue = [];
if (this.connection.readyState == 1) {
this.connection.close();
} else {
this.connection.onopen = function () {
this.connection.close();
};
}
};
/*
* @name Router
*
* Implements a routifier which lets you define URL to your handlers
* The URLs will respond to methods and chainable, meaning that a chai
* of responsability (by the given order) will be respected.
* If one of the handlers, fails, it will break the chain.
* Optionally, actions (in a similar fashion to REST) can be defined,
* also filters.
* You can attach hooks to your url handlers which will let you prepare your
* data or scenario before executing a particular task or set of tasks
*
* @constructor
* @param {String} connection_query
* Defines the FQDM to connect to (eg: ws(s)://localhost:8080)
* @param {*} ctx
* Defines the root context to which all the Handlers will be attached by
* default, if no other handler is specified
* @return {Router}
*
*/
function Router(connection_query, ctx) {
this.action = {
UPDATE: 'UPDATE',
DELETE: 'DELETE',
CREATE: 'CREATE',
SUBSCRIBE: 'SUBSCRIBE',
REQUEST: 'REQUEST'
};
//Due to several version of the router beeing used on different places of the application
this.version = '1.3.2';
//
this.events = [];
//
this.lastEvents = [];
//
this.registeredTasks = [];
// Will manage all connection-related aspects
// Of the WebSocket
this.connection = new ConnectionHandler(connection_query, this.dispatch.bind(this));
}
/*
* @name Send
*
* Executes the current message, and the BEFORE and AFTER hooks
*
* @function
* @param {Object} message
* Object message to be sent to the server
* @return {None}
*
*/
Router.prototype.send = function (message) {
try {
this.executeTaks(message, BEFORE);
this.connection.send(message);
this.executeTaks(message, AFTER);
} catch (err) {
console.log(err);
}
};
/*
* @name Exception Handler
*
* Receives an error and returns a message ready to be
* delivered to the client.
*
* @function
* @param {Error} error
* Object message to be sent to the server
* @return {None}
*
*/
Router.prototype.ExceptionHandler = function (error) {
return {
'message': error.stack
};
};
/*
* @name On
*
* Receives a route and a callback and returns a builder object
* that will let you define, chainable, action, filters, bind and executeLast,
* properties.
*
* action - receives a constante (or value) and filter also by defined
* action in the server.
*
* filters - receives an object and filter also by key-values defined in the
* filters, and also present in the received message.
*
* bind - Receives any object and binds the callback to the specified context.
*
* executeLast - will push this callback to the end of the event queue, beeing
* making it the last in the chain of responsability.
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.on = function (n, fn) {
var event = { n: this.routify(n), fn: fn, action: "*" };
this.events.push(event);
return new function () {
this.action = function (ACTION) {
if (ACTION) event.action = ACTION;
return this;
};
this.filters = function (filters) {
if (filters) event.filters = filters;
return this;
};
this.bind = function (bind) {
if (bind) event.bind = bind;
return this;
};
this.executeLast = function () {
lastEvents.push(events[events.indexOf(event)]);
delete events[events.indexOf(event)];
events.length = events.length - 1;
};
}();
};
/*
* @name Delete
*
* Receives a route, a callback and filters, creates a new listener for
* those criterias and the DELETE action
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @param {Object} filters
* Object used to match parameters in the body and filter messages.
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.delete = function (n, fn, filters) {
return this.on(n, fn).action(this.action.DELETE).filters(filters);
};
/*
* @name Update
*
* Receives a route, a callback and filters, creates a new listener for
* those criterias and the UPDATE action
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @param {Object} filters
* Object used to match parameters in the body and filter messages.
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.update = function (n, fn, filters) {
return this.on(n, fn).action(this.action.UPDATE).filters(filters);
};
/*
* @name Create
*
* Receives a route, a callback and filters, creates a new listener for
* those criterias and the CREATE action
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @param {Object} filters
* Object used to match parameters in the body and filter messages.
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.create = function (n, fn, filters) {
return this.on(n, fn).action(this.action.CREATE).filters(filters);
};
/*
* @name Request
*
* Receives a route, a callback and filters, creates a new listener for
* those criterias and the REQUEST action
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @param {Object} filters
* Object used to match parameters in the body and filter messages.
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.request = function (n, fn, filters) {
return this.on(n, fn).action(this.action.REQUEST).filters(filters);
};
/*
* @name Subscribe
*
* Receives a route, a callback and filters, creates a new listener for
* those criterias and the SUBSCRIBE action
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @param {Object} filters
* Object used to match parameters in the body and filter messages.
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.subscribe = function (n, fn, filters) {
return this.on(n, fn).action(this.action.SUBSCRIBE).filters(filters);
};
/*
* @name Intercept
*
* Mirros send function. Create for more client legibility and code
* Maintanability
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @return {{
* action: {Function} (action),
* filters: {Function} (object),
* bind: {Function} (context),
* executeLast: {Function} ()
* }}
*
*/
Router.prototype.intercept = function (n, fn) {
this.events.push({ n: this.routify(n), fn: fn });
};
/*
* @name Before Send
*
* Attachs a before send hook to a particular route.
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @return {None}
*
*/
Router.prototype.beforeSend = function (route, fn) {
this.registerSendTask(route, fn, BEFORE);
};
/*
* @name After Send
*
* Attachs a after send hook to a particular route.
*
* @function
* @param {String} name
* Route to match
* @param {Function} callback
* Callback to execute whenever the criterias ar meet
* @return {None}
*
*/
Router.prototype.afterSend = function (route, fn) {
this.registerSendTask(route, fn, AFTER);
};
/*
* @name Register Send Task
*
* Attachs a hook, by the defined criterias. (Not intended for public use yet)
*
*/
Router.prototype.registerSendTask = function (route, fn, position) {
this.registeredTasks.push({
route: this.routify(route),
fn: fn,
position: position
});
};
/*
* @name Message
*
* Sends a message to the server. The default route is * which can be
* Overriten. All parameters are optional.
*
* @function
* @param {{
* route: {String},
* action: {String},
* data: {Object}
* }} options
* @return {None}
*
*/
Router.prototype.message = function (options) {
this.send(Object.assign({ route: "*" }, options));
};
/*
* @name Apply Filters
*
* Checks if an object meets the filters criteria (Not intended for public use)
*
* @function
* @param {Object} target_filters
* @param {Object} destiny_filters
* @return {Boolean}
*
*/
Router.prototype.applyFilters = function (dataFilters, eventFilters) {
var _this = this;
var isFilterMatching = true;
Object.keys(eventFilters).forEach(function (key) {
if (!_this.routify(dataFilters[key].toString()).exec(eventFilters[key].toString())) {
isFilterMatching = false;
}
});
return isFilterMatching;
};
/*
* @name Should Event Execute
*
* Checks if a event should execute or not (Not intended for public use)
*
* @function
* @param {Object} data
* @param {Object} evt
* @return {Boolean}
*
*/
Router.prototype.shouldEventExecute = function (data, evt) {
if (!this.routify(evt.action).exec(data.action)) {
return false;
}
if (evt.filters && !data.data || data.data && evt.filters && !this.applyFilters(data.data, evt.filters)) {
return false;
}
return evt.n.exec(data.route);
};
/*
* @name Should Task Execute
*
* Checks if a task should execute or not (Not intended for public use)
*
* @function
* @param {Object} data
* @param {Object} task
* @param {String} position
* @return {Boolean}
*
*/
Router.prototype.shouldTaskExecute = function (data, task, position) {
if (position != task.position) {
return false;
}
return task.route.exec(data.route);
};
/*
* @name Apply To Queue
*
* Adds the current event to the execution queue (Not intended for public use)
*
* @function
* @param {Object} event
* @param {Array} run
* @return {None}
*
*/
Router.prototype.applyToQueue = function (evt, run) {
if (evt.fn instanceof Array) {
for (var j = 0; j < evt.fn.length; j++) {
run.push(evt.fn[j].bind(evt.bind ? evt.bind : ctx));
}
} else {
run.push(evt.fn.bind(evt.bind ? evt.bind : ctx));
}
};
/*
* @name Dispatch
*
* Dispatchs the current event and the associated hooks (Not intended for public use)
*
* @function
* @param {Object} data
* @return {None}
*
*/
Router.prototype.dispatch = function (data) {
var run = [];
//Events
for (var i = 0; i < this.events.length; i++) {
if (this.shouldEventExecute(data, this.events[i])) {
this.applyToQueue(this.events[i], run);
}
}
//Execute Last Events
for (var i = 0; i < this.lastEvents.length; i++) {
if (this.shouldEventExecute(data, this.lastEvents[i])) {
this.applyToQueue(this.lastEvents[i], run);
}
}
this.runner(run, data.data);
};
/*
* @name Runner
*
* Runs the provided callbacks array (Not intended for public use)
*
* @function
* @param {Array} run
* @param {Object} data
* @return {None}
*
*/
Router.prototype.runner = function (run, data) {
try {
for (var i = 0; i < run.length; i++) {
run[i](data);
}
} catch (e) {
this.error(this.ExceptionHandler(e));
}
};
/*
* @name Routify
*
* Turns a Route String into a regex (Not intended for public use)
*
* @function
* @param {String} route
* @return {Regex}
*
*/
Router.prototype.routify = function (n) {
return new RegExp('^' + n.replace(/\*/g, '[^\/]+').replace(/\//g, '\\/') + '$', '');
};
/*
* @name Error
*
* Sends an error whenever the server or the client
* has failed (Not intended for public use)
*
* @function
* @param {Error} error
* @return {None}
*
*/
Router.prototype.error = function (error) {
this.send({ route: '/socket/error', data: error });
};
/*
* @name Execute Tasks
*
* Executes all the tasks by a hook identifier (Not intended for public use)
*
* @function
* @param {Object} data
* @param {String} position
* @return {None}
*
*/
Router.prototype.executeTaks = function (data, position) {
for (var i = 0; i < this.registeredTasks.length; i++) {
if (this.shouldTaskExecute(data, this.registeredTasks[i], position)) {
this.registeredTasks[i].fn.bind(ctx)(data);
}
}
};
return new Router(connection_query, ctx);
};
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') module.exports = WebSocketRouter;