happn-3
Version:
pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb
1,542 lines (1,268 loc) • 57.2 kB
JavaScript
/* eslint-disable no-console */
(function() {
// begin enclosed
var browser = false;
var Logger;
var crypto;
var Primus;
var PROTOCOL = 'happn_{{protocol}}';
var HAPPN_VERSION = '{{version}}';
var STATUS;
if (typeof window !== 'undefined' && typeof document !== 'undefined') browser = true;
// allow require when module is defined (needed for NW.js)
if (typeof module !== 'undefined') module.exports = HappnClient;
if (!browser) {
Logger = require('happn-logger');
PROTOCOL = 'happn_' + require('../package.json').protocol; //we can access our package
HAPPN_VERSION = require('../package.json').version; //we can access our package
Primus = require('happn-primus-wrapper');
} else {
window.HappnClient = HappnClient;
Primus = window.Primus;
// Object.assign polyfill for IE11 (from mozilla)
if (typeof Object.assign !== 'function') {
Object.defineProperty(Object, 'assign', {
value: function assign(target) {
'use strict';
if (target === null || target === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource !== null && nextSource !== undefined) {
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
},
writable: true,
configurable: true
});
}
}
var promisify = function(fn) {
// MIT License, - thanks to paulmillr.com
if (typeof fn !== 'function') throw new TypeError('micro-promisify must receive a function');
return Object.defineProperties(
function() {
var _this = this;
for (var args = new Array(arguments.length), i = 0; i < args.length; ++i)
args[i] = arguments[i]; // git.io/vk55A
return new Promise(function(resolve, reject) {
args.push(function(error, result) {
error == null ? resolve(result) : reject(error);
});
fn.apply(_this, args);
});
},
{
length: { value: Math.max(0, fn.length - 1) },
name: { value: fn.name }
}
);
};
var maybePromisify = function(originalFunction, opts) {
return function() {
var args = Array.prototype.slice.call(arguments);
var _this = this;
if (opts && opts.unshift) args.unshift(opts.unshift);
// No promisify if last passed arg is function (ie callback)
if (typeof args[args.length - 1] === 'function') {
return originalFunction.apply(this, args);
}
return new Promise(function(resolve, reject) {
// push false callback into arguments
args.push(function(error, result, more) {
if (error) return reject(error);
if (more) {
var args = Array.prototype.slice.call(arguments);
args.shift(); // toss undefined error
return resolve(args); // resolve array of args passed to callback
}
return resolve(result);
});
try {
return originalFunction.apply(_this, args);
} catch (error) {
return reject(error);
}
});
};
};
function HappnClient() {
if (!browser) {
this.CONSTANTS = require('.').constants;
this.utils = require('./services/utils/shared');
}
//DO NOT DELETE
//{{constants}}
//DO NOT DELETE
//{{utils}}
STATUS = this.CONSTANTS.CLIENT_STATE;
}
HappnClient.__instance = function(options) {
return new HappnClient().client(options);
};
HappnClient.create = maybePromisify(function(connection, options, callback) {
if (typeof connection === 'function') {
callback = connection;
options = {};
connection = null;
}
if (typeof options === 'function') {
callback = options;
options = connection ? connection : {};
connection = null;
}
if (!options) options = connection;
var client = new HappnClient().client(options);
if (options.testMode) HappnClient.lastClient = client;
return client.initialize(function(err, createdClient) {
if (!err) return callback(null, createdClient);
if (client.state.clientType !== 'eventemitter')
return client.disconnect(function() {
callback(err);
});
client.socket.disconnect();
callback(err);
});
});
HappnClient.prototype.client = function(options) {
options = options || {};
if (options.Logger && options.Logger.createLogger) {
this.log = options.Logger.createLogger('HappnClient');
} else if (Logger) {
if (!Logger.configured) Logger.configure(options.utils);
this.log = Logger.createLogger('HappnClient');
} else {
this.log = {
$$TRACE: function(msg, obj) {
if (obj) return console.info('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
$$DEBUG: function(msg, obj) {
if (obj) return console.info('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
trace: function(msg, obj) {
if (obj) return console.info('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
debug: function(msg, obj) {
if (obj) return console.info('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
info: function(msg, obj) {
if (obj) return console.info('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
warn: function(msg, obj) {
if (obj) return console.warn('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
error: function(msg, obj) {
if (obj) return console.error('HappnClient', msg, obj);
console.info('HappnClient', msg);
},
fatal: function(msg, obj) {
if (obj) return console.error('HappnClient', msg, obj);
console.info('HappnClient', msg);
}
};
}
this.log.$$TRACE('new client()');
this.__initializeState(); //local properties
this.__prepareInstanceOptions(options);
this.__initializeEvents(); //client events (connect/disconnect etc.)
return this;
};
HappnClient.prototype.initialize = maybePromisify(function(callback) {
var _this = this;
//ensure session scope is not on the prototype
_this.session = null;
if (browser) {
return _this.getResources(function(e) {
if (e) return callback(e);
_this.authenticate(function(e) {
if (e) return callback(e);
_this.status = STATUS.ACTIVE;
callback(null, _this);
});
});
}
_this.authenticate(function(e) {
if (e) return callback(e);
_this.status = STATUS.ACTIVE;
callback(null, _this);
});
});
HappnClient.prototype.__prepareSecurityOptions = function(options) {
if (options.keyPair && options.keyPair.publicKey) options.publicKey = options.keyPair.publicKey;
if (options.keyPair && options.keyPair.privateKey)
options.privateKey = options.keyPair.privateKey;
};
HappnClient.prototype.__prepareSocketOptions = function(options) {
//backward compatibility config options
if (!options.socket) options.socket = {};
if (!options.socket.reconnect) options.socket.reconnect = {};
if (options.reconnect) options.socket.reconnect = options.reconnect; //override, above config is very convoluted
if (!options.socket.reconnect.retries) options.socket.reconnect.retries = Infinity;
if (!options.socket.reconnect.max) options.socket.reconnect.max = 180e3; //3 minutes
if (options.connectTimeout != null) {
options.socket.timeout = options.connectTimeout;
} else {
options.socket.timeout = options.socket.timeout != null ? options.socket.timeout : 30e3; //30 seconds
}
options.socket.pingTimeout =
'pingTimeout' in options.socket ? options.socket.pingTimeout : 45e3; //45 second default
options.socket.strategy =
options.socket.reconnect.strategy || options.socket.strategy || 'disconnect,online';
};
HappnClient.prototype.__prepareConnectionOptions = function(options, defaults) {
var setDefaults = function(propertyName) {
if (!options[propertyName] && defaults[propertyName] != null)
options[propertyName] = defaults[propertyName];
};
if (defaults) {
setDefaults('host');
setDefaults('port');
setDefaults('url');
setDefaults('protocol');
setDefaults('allowSelfSignedCerts');
setDefaults('username');
setDefaults('password');
setDefaults('publicKey');
setDefaults('privateKey');
setDefaults('token');
}
if (!options.host) options.host = '127.0.0.1';
if (!options.port) options.port = 55000;
if (!options.url) {
options.protocol = options.protocol || 'http';
if (options.protocol === 'http' && parseInt(options.port) === 80) {
options.url = options.protocol + '://' + options.host;
} else if (options.protocol === 'https' && parseInt(options.port) === 443) {
options.url = options.protocol + '://' + options.host;
} else {
options.url = options.protocol + '://' + options.host + ':' + options.port;
}
}
return options;
};
HappnClient.prototype.__prepareInstanceOptions = function(options) {
var preparedOptions;
if (options.config) {
//we are going to standardise here, so no more config.config
preparedOptions = options.config;
for (var optionProperty in options) {
if (optionProperty !== 'config' && !preparedOptions[optionProperty])
preparedOptions[optionProperty] = options[optionProperty];
}
} else preparedOptions = options;
if (!preparedOptions.callTimeout) preparedOptions.callTimeout = 60000; //1 minute
//this is for local client connections
if (preparedOptions.context)
Object.defineProperty(this, 'context', {
value: preparedOptions.context
});
//how we override methods
if (preparedOptions.plugin) {
for (var overrideName in preparedOptions.plugin) {
// eslint-disable-next-line no-prototype-builtins
if (preparedOptions.plugin.hasOwnProperty(overrideName)) {
if (preparedOptions.plugin[overrideName].bind)
this[overrideName] = preparedOptions.plugin[overrideName].bind(this);
else this[overrideName] = preparedOptions.plugin[overrideName];
}
}
}
preparedOptions = this.__prepareConnectionOptions(preparedOptions);
this.__prepareSecurityOptions(preparedOptions);
if (preparedOptions.allowSelfSignedCerts) process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
this.__prepareSocketOptions(preparedOptions);
var info = preparedOptions.info != null ? preparedOptions.info : {};
if (typeof info !== 'object')
info = {
data: info
};
preparedOptions.info = info;
preparedOptions.info._browser = preparedOptions.info._browser || browser;
if (preparedOptions.loginRetry == null) preparedOptions.loginRetry = 4; // will attempt to login to the same address 4 times
if (preparedOptions.loginRetryInterval == null) preparedOptions.loginRetryInterval = 5000; // five seconds apart
if (preparedOptions.loginTimeout == null)
preparedOptions.loginTimeout = preparedOptions.callTimeout; // will wait a minute before failing the login
if (preparedOptions.defaultVariableDepth == null) preparedOptions.defaultVariableDepth = 5;
this.options = preparedOptions;
};
HappnClient.prototype.__updateOptions = function(possibility) {
var _this = this;
var syncOption = function(propertyName) {
if (possibility[propertyName] != null)
_this.options[propertyName] = possibility[propertyName];
};
syncOption('url');
syncOption('host');
syncOption('port');
syncOption('protocol');
syncOption('allowSelfSignedCerts');
syncOption('username');
syncOption('password');
syncOption('publicKey');
syncOption('privateKey');
syncOption('token');
};
HappnClient.prototype.__getConnection = function(callback) {
var _this = this;
this.__connectionCleanup(function(e) {
if (e) return callback(e);
_this.options.socket.manual = true; //because we want to explicitly call open()
_this.__connectSocket(callback);
});
};
HappnClient.prototype.__connectSocket = function(callback) {
var socket;
var _this = this;
_this.status = STATUS.CONNECTING;
if (browser) socket = new Primus(_this.options.url, _this.options.socket);
else {
var Socket = Primus.createSocket({
transformer: _this.options.transformer,
parser: _this.options.parser,
manual: true
});
socket = new Socket(_this.options.url, _this.options.socket);
}
socket.on('timeout', function() {
if (_this.status === STATUS.CONNECTING) {
_this.status = STATUS.CONNECT_ERROR;
return callback(new Error('connection timed out'));
}
_this.handle_error(new Error('connection timed out'));
});
socket.on('open', function waitForConnection() {
if (_this.status === STATUS.CONNECTING) {
_this.status = STATUS.ACTIVE;
_this.serverDisconnected = false;
socket.removeListener('open', waitForConnection);
callback(null, socket);
}
});
socket.on('error', function(e) {
if (_this.status === STATUS.CONNECTING) {
// ERROR before connected,
// ECONNREFUSED etc. out as errors on callback
_this.status = STATUS.CONNECT_ERROR;
_this.__endAndDestroySocket(socket, function(destroyErr) {
if (destroyErr)
_this.log.warn(
'socket.end failed in client connection failure: ' + destroyErr.toString()
);
callback(e.error || e);
});
}
_this.handle_error(e.error || e);
});
socket.open();
};
HappnClient.prototype.__initializeState = function() {
this.state = {};
this.state.events = {};
this.state.refCount = {};
this.state.listenerRefs = {};
this.state.requestEvents = {};
this.state.currentEventId = 0;
this.state.currentListenerId = 0;
this.state.errors = [];
this.state.clientType = 'socket';
this.state.systemMessageHandlers = [];
this.status = STATUS.UNINITIALIZED;
this.state.ackHandlers = {};
this.state.eventHandlers = {};
};
HappnClient.prototype.__initializeEvents = function() {
var _this = this;
_this.onEvent = function(eventName, eventHandler) {
if (!eventName) throw new Error('event name cannot be blank or null');
if (typeof eventHandler !== 'function') throw new Error('event handler must be a function');
if (!_this.state.eventHandlers[eventName]) _this.state.eventHandlers[eventName] = [];
_this.state.eventHandlers[eventName].push(eventHandler);
return eventName + '|' + (_this.state.eventHandlers[eventName].length - 1);
};
_this.offEvent = function(handlerId) {
var eventName = handlerId.split('|')[0];
var eventIndex = parseInt(handlerId.split('|')[1]);
_this.state.eventHandlers[eventName][eventIndex] = null;
};
_this.emit = function(eventName, eventData) {
if (_this.state.eventHandlers[eventName]) {
_this.state.eventHandlers[eventName].forEach(function(handler) {
if (!handler) return;
handler.call(handler, eventData);
});
}
};
};
HappnClient.prototype.getScript = function(url, callback) {
if (!browser) return callback(new Error('only for browser'));
var script = document.createElement('script');
script.src = url;
var head = document.getElementsByTagName('head')[0];
var done = false;
// Attach handlers for all browsers
script.onload = script.onreadystatechange = function() {
if (
!done &&
(!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')
) {
done = true;
script.onload = script.onreadystatechange = null;
head.removeChild(script);
callback();
}
};
head.appendChild(script);
};
HappnClient.prototype.getResources = function(callback) {
if (typeof Primus !== 'undefined') return callback();
this.getScript(this.options.url + '/browser_primus.js', function(e) {
if (e) return callback(e);
if (typeof Primus === 'undefined') {
if (window && window.Primus) Primus = window.Primus;
else if (document && document.Primus) Primus = document.Primus;
else return callback(new Error('unable to fetch Primus library'));
callback();
}
});
};
HappnClient.prototype.stop = maybePromisify(function(callback) {
this.__connectionCleanup(callback);
});
HappnClient.prototype.__encryptLogin = function(parameters, publicKey) {
return {
encrypted: crypto.asymmetricEncrypt(
publicKey,
this.options.privateKey,
JSON.stringify(parameters)
),
publicKey: parameters.publicKey,
loginType: parameters.loginType != null ? parameters.loginType : 'password'
};
};
HappnClient.prototype.__decryptLogin = function(loginResult) {
return JSON.parse(
crypto.asymmetricDecrypt(
this.serverInfo.publicKey,
this.options.privateKey,
loginResult.encrypted
)
);
};
HappnClient.prototype.__encryptPayload = function(message) {
var payload = crypto.symmetricEncryptObjectiv(
message,
this.session.secret,
this.utils.computeiv(this.session.secret)
);
return {
sessionId: message.sessionId,
eventId: message.eventId,
encrypted: payload
};
};
HappnClient.prototype.__decryptPayload = function(message) {
var self = this;
var payload = crypto.symmetricDecryptObjectiv(
message,
self.session.secret,
self.utils.computeiv(self.session.secret)
);
return payload;
};
HappnClient.prototype.__ensureCryptoLibrary = maybePromisify(function(callback) {
if (crypto) return callback();
if (browser) {
this.getScript(this.options.url + '/browser_crypto.js', function(e) {
if (e) return callback(e);
crypto = new window.Crypto();
callback();
});
} else {
Crypto = require('happn-util-crypto');
crypto = new Crypto();
callback();
}
});
HappnClient.prototype.__writeCookie = function(session, sessionDocument) {
sessionDocument.cookie = this.__getCookie(session);
};
HappnClient.prototype.__getCookie = function(session) {
var cookie = (session.cookieName || 'happn_token') + '=' + session.token + '; path=/;';
if (session.cookieDomain) cookie += ' domain=' + session.cookieDomain + ';';
if (this.options.protocol === 'https') cookie += ' Secure;';
return cookie;
};
HappnClient.prototype.__expireCookie = function(session, sessionDocument) {
session.token = '';
var cookie = this.__getCookie(session);
cookie += '; expires=Thu, 01 Jan 1970 00:00:00 UTC;';
sessionDocument.cookie = cookie;
};
HappnClient.prototype.__attachSession = function(result) {
delete result._meta;
this.session = result;
if (browser) this.__writeCookie(result, document);
};
HappnClient.prototype.__payloadToError = function(payload) {
var err = new Error(payload.toString());
if (payload.message) err.message = payload.message;
return err;
};
HappnClient.prototype.__doLogin = function(loginParameters, callback) {
var _this = this;
var login = function(cb) {
_this.__performSystemRequest(
'login',
loginParameters,
{
timeout: _this.options.loginTimeout
},
function(e, result) {
if (e) return cb(e);
if (result._meta.status === 'ok') {
_this.__attachSession(result);
cb();
} else cb(_this.__payloadToError(result.payload));
}
);
};
if (!_this.options.loginRetry) return login(callback);
if (!_this.options.loginRetryInterval || typeof _this.options.loginRetryInterval !== 'number')
_this.options.loginRetryInterval = 5000; //just in case, someone made it 0 or -1 or blah
var currentAttempt = 0;
var loggedIn = false;
_this.utils.whilst(
function() {
return currentAttempt < _this.options.loginRetry && loggedIn === false;
},
function(attempt, next) {
currentAttempt++;
login(function(e) {
if (e) {
if ([403, 401].indexOf(e.code) > -1) return next(e); //access was denied
if (currentAttempt === _this.options.loginRetry) return next(e);
return setTimeout(next, _this.options.loginRetryInterval);
}
loggedIn = true;
return next();
});
},
callback
);
};
HappnClient.prototype.__signNonce = function(nonce) {
return crypto.sign(nonce, this.options.privateKey);
};
HappnClient.prototype.__prepareLogin = function(loginParameters, callback) {
var _this = this;
var prepareCallback = function(prepared) {
if (_this.serverInfo.encryptPayloads)
prepared = _this.__encryptLogin(prepared, _this.serverInfo.publicKey);
callback(null, prepared);
};
if (loginParameters.loginType === 'digest') {
_this.__performSystemRequest(
'request-nonce',
{
publicKey: loginParameters.publicKey
},
null,
function(e, response) {
if (e) return callback(e);
loginParameters.digest = _this.__signNonce(response.nonce);
prepareCallback(loginParameters);
}
);
} else prepareCallback(loginParameters);
};
HappnClient.prototype.__prepareAndDoLogin = function(loginParameters, callback) {
var _this = this;
_this.__prepareLogin(loginParameters, function(e, preparedParameters) {
if (e) return callback(e);
_this.__doLogin(preparedParameters, callback);
});
};
HappnClient.prototype.__sessionConfigureAndDescribe = function() {
var _this = this;
return new Promise(function(resolve, reject) {
_this.__performSystemRequest(
'configure-session',
{
protocol: PROTOCOL,
version: HAPPN_VERSION,
browser: browser
},
null,
function(e) {
if (e) return reject(e);
_this.__performSystemRequest('describe', null, null, function(e, serverInfo) {
if (e) return reject(e);
resolve(serverInfo);
});
}
);
});
};
function getCookie(name) {
var value = '; ' + document.cookie;
var parts = value.split('; ' + name + '=');
if (parts.length === 2)
return parts
.pop()
.split(';')
.shift();
}
HappnClient.prototype.login = maybePromisify(function(callback) {
var _this = this;
var loginParameters = {
username: this.options.username,
info: this.options.info,
protocol: PROTOCOL
};
loginParameters.info._browser = loginParameters.info._browser || browser;
loginParameters.info._local = _this.socket._local ? true : false;
if (this.options.password) loginParameters.password = this.options.password;
if (this.options.publicKey) loginParameters.publicKey = this.options.publicKey;
if (this.options.token) loginParameters.token = this.options.token;
if (PROTOCOL === 'happn_{{protocol}}') PROTOCOL = 'happn'; //if this file is being used without a replace on the protocol
_this
.__sessionConfigureAndDescribe()
.then(function(serverInfo) {
_this.serverInfo = serverInfo;
if (!_this.serverInfo.secure) return _this.__doLogin(loginParameters, callback);
if (_this.options.useCookie) {
if (browser) {
loginParameters.token = getCookie(serverInfo.cookieName);
} else {
return callback(new Error('Logging in with cookie only valid in browser'));
}
}
if (!loginParameters.token && !loginParameters.username)
return callback(new Error('happn server is secure, please specify a username or token'));
if (!loginParameters.password && !loginParameters.token) {
if (!loginParameters.publicKey)
return callback(new Error('happn server is secure, please specify a password'));
loginParameters.loginType = 'digest';
}
if (!_this.serverInfo.encryptPayloads && loginParameters.loginType !== 'digest')
return _this.__doLogin(loginParameters, callback);
_this.__ensureCryptoLibrary(function(e) {
if (e) return callback(e);
if (!_this.options.privateKey || !_this.options.publicKey) {
if (loginParameters.loginType === 'digest')
return callback(
new Error('login type is digest, but no privateKey and publicKey specified')
);
//We generate one
var keyPair = crypto.createKeyPair();
_this.options.publicKey = keyPair.publicKey;
_this.options.privateKey = keyPair.privateKey;
}
loginParameters.publicKey = _this.options.publicKey;
_this.__prepareAndDoLogin(loginParameters, callback);
});
})
.catch(function(e) {
_this.emit('connect-error', e);
callback(e);
});
});
HappnClient.prototype.authenticate = maybePromisify(function(callback) {
var _this = this;
if (_this.socket) {
// handle_reconnection also call through here to 're-authenticate'.
// this is that happening. Don't make new socket.
// if the login fails, a setTimeout 3000 and re-authenticate, retry happens
// see this.__retryReconnect
_this.login(callback);
return;
}
_this.__getConnection(function(e, socket) {
if (e) return callback(e);
_this.socket = socket;
_this.socket.on('data', _this.handle_publication.bind(_this));
_this.socket.on('reconnected', _this.reconnect.bind(_this));
_this.socket.on('end', _this.handle_end.bind(_this));
_this.socket.on('close', _this.handle_end.bind(_this));
_this.socket.on('reconnect timeout', _this.handle_reconnect_timeout.bind(_this));
_this.socket.on('reconnect scheduled', _this.handle_reconnect_scheduled.bind(_this));
// login is called before socket connection established...
// seems ok (streams must be paused till open)
_this.login(callback);
});
});
HappnClient.prototype.handle_end = function() {
this.status = STATUS.DISCONNECTED;
if (this.session) return this.emit('connection-ended', this.session.id);
this.emit('connection-ended');
};
HappnClient.prototype.handle_reconnect_timeout = function(err, opts) {
this.status = STATUS.DISCONNECTED;
this.emit('reconnect-timeout', {
err: err,
opts: opts
});
};
HappnClient.prototype.handle_reconnect_scheduled = function(opts) {
this.status = STATUS.RECONNECTING;
this.emit('reconnect-scheduled', opts);
};
HappnClient.prototype.getEventId = function() {
return (this.state.currentEventId += 1);
};
HappnClient.prototype.__requestCallback = function(
message,
callback,
options,
eventId,
path,
action
) {
var _this = this;
var callbackHandler = {
eventId: message.eventId
};
callbackHandler.handleResponse = function(e, response) {
clearTimeout(callbackHandler.timedout);
delete _this.state.requestEvents[callbackHandler.eventId];
return callback(e, response);
};
callbackHandler.timedout = setTimeout(function() {
delete _this.state.requestEvents[callbackHandler.eventId];
var errorMessage = 'api request timed out';
if (path) errorMessage += ' path: ' + path;
if (action) errorMessage += ' action: ' + action;
return callback(new Error(errorMessage));
}, options.timeout);
//we add our event handler to a queue, with the embedded timeout
_this.state.requestEvents[eventId] = callbackHandler;
};
HappnClient.prototype.__asyncErrorCallback = function(error, callback) {
if (!callback) {
throw error;
}
setTimeout(function() {
callback(error);
}, 0);
};
HappnClient.prototype.__performDataRequest = function(path, action, data, options, callback) {
if (this.status !== STATUS.ACTIVE) {
var errorMessage = 'client not active';
if (this.status === STATUS.CONNECT_ERROR) errorMessage = 'client in an error state';
if (this.status === STATUS.UNINITIALIZED) errorMessage = 'client not initialized yet';
if (this.status === STATUS.DISCONNECTED) errorMessage = 'client is disconnected';
var errorDetail = 'action: ' + action + ', path: ' + path;
var error = new Error(errorMessage);
error.detail = errorDetail;
return this.__asyncErrorCallback(error, callback);
}
var message = {
action: action,
eventId: this.getEventId(),
path: path,
data: data,
sessionId: this.session.id
};
if (!options) options = {};
else message.options = options; //else skip sending up the options
if (['set', 'remove'].indexOf(action) >= 0) {
if (
options.consistency === this.CONSTANTS.CONSISTENCY.DEFERRED ||
options.consistency === this.CONSTANTS.CONSISTENCY.ACKNOWLEDGED
)
this.__attachPublishedAck(options, message);
}
if (!options.timeout) options.timeout = this.options.callTimeout;
if (this.serverInfo.encryptPayloads) message = this.__encryptPayload(message);
if (callback) this.__requestCallback(message, callback, options, message.eventId, path, action); // if null we are firing and forgetting
this.socket.write(message);
};
HappnClient.prototype.__performSystemRequest = function(action, data, options, callback) {
var message = {
action: action,
eventId: this.getEventId()
};
if (data !== undefined) message.data = data;
if (this.session) message.sessionId = this.session.id;
if (!options) options = {};
//skip sending up the options
else message.options = options;
if (!options.timeout) options.timeout = this.options.callTimeout; //this is not used on the server side
this.__requestCallback(message, callback, options, message.eventId); // if null we are firing and forgetting
this.socket.write(message);
};
HappnClient.prototype.getChannel = function(path, action) {
this.utils.checkPath(path);
return '/' + action.toUpperCase() + '@' + path;
};
HappnClient.prototype.get = maybePromisify(function(path, parameters, handler) {
if (typeof parameters === 'function') {
handler = parameters;
parameters = {};
}
this.__performDataRequest(path, 'get', null, parameters, handler);
});
HappnClient.prototype.count = maybePromisify(function(path, parameters, handler) {
if (typeof parameters === 'function') {
handler = parameters;
parameters = {};
}
this.__performDataRequest(path, 'count', null, parameters, handler);
});
HappnClient.prototype.getPaths = maybePromisify(function(path, opts, handler) {
if (typeof opts === 'function') {
handler = opts;
opts = {};
}
opts.options = {
path_only: true
};
this.get(path, opts, handler);
});
HappnClient.prototype.increment = maybePromisify(function(path, gauge, increment, opts, handler) {
if (typeof opts === 'function') {
handler = opts;
opts = {};
}
if (typeof increment === 'function') {
handler = increment;
increment = gauge;
gauge = 'counter';
opts = {};
}
if (typeof gauge === 'function') {
handler = gauge;
increment = 1;
gauge = 'counter';
opts = {};
}
if (isNaN(increment)) return handler(new Error('increment must be a number'));
opts.increment = increment;
this.set(path, gauge, opts, handler);
});
HappnClient.prototype.publish = maybePromisify(function(path, data, options, handler) {
if (typeof options === 'function') {
handler = options;
options = {};
}
if (data === null) options.nullValue = true; //carry across the wire
options.noStore = true;
options.noDataResponse = true;
try {
//in a try/catch to catch checkPath failure
this.utils.checkPath(path, 'set');
this.__performDataRequest(path, 'set', data, options, handler);
} catch (e) {
return handler(e);
}
});
HappnClient.prototype.set = maybePromisify(function(path, data, options, handler) {
if (typeof options === 'function') {
handler = options;
options = {};
}
if (data === null) options.nullValue = true; //carry across the wire
try {
//in a try/catch to catch checkPath failure
this.utils.checkPath(path, 'set');
this.__performDataRequest(path, 'set', data, options, handler);
} catch (e) {
return handler(e);
}
});
HappnClient.prototype.setSibling = maybePromisify(function(path, data, opts, handler) {
if (typeof opts === 'function') {
handler = opts;
opts = {};
}
opts.set_type = 'sibling';
this.set(path, data, opts, handler);
});
HappnClient.prototype.remove = maybePromisify(function(path, parameters, handler) {
if (typeof parameters === 'function') {
handler = parameters;
parameters = {};
}
return this.__performDataRequest(path, 'remove', null, parameters, handler);
});
HappnClient.prototype.__updateListenerRef = function(listener, remoteRef) {
if (listener.initialEmit || listener.initialCallback)
this.state.listenerRefs[listener.id] = remoteRef;
else this.state.listenerRefs[listener.eventKey] = remoteRef;
};
HappnClient.prototype.__clearListenerRef = function(listener) {
if (listener.initialEmit || listener.initialCallback)
return delete this.state.listenerRefs[listener.id];
delete this.state.listenerRefs[listener.eventKey];
};
HappnClient.prototype.__getListenerRef = function(listener) {
if (listener.initialEmit || listener.initialCallback)
return this.state.listenerRefs[listener.id];
return this.state.listenerRefs[listener.eventKey];
};
HappnClient.prototype.__reattachListeners = function(callback) {
var _this = this;
_this.utils.async(
Object.keys(_this.state.events),
function(eventPath, index, nextEvent) {
var listeners = _this.state.events[eventPath];
_this.state.refCount = {};
// re-establish each listener individually to preserve original meta and listener id
_this.utils.async(
listeners,
function(listener, index, nextListener) {
if (_this.state.refCount[listener.eventKey]) {
//we are already listening on this key
_this.state.refCount[listener.eventKey]++;
return nextListener();
}
// we don't pass any additional parameters like initialValueEmit and initialValueCallback
var parameters = {};
if (listener.meta) parameters.meta = listener.meta;
_this._offPath(eventPath, function(e) {
if (e)
return nextListener(
new Error(
'failed detaching listener to path, on re-establishment: ' + eventPath,
e
)
);
_this._remoteOn(eventPath, parameters, function(e, response) {
if (e) {
if ([403, 401].indexOf(e.code) > -1) {
//permissions may have changed regarding this path
delete _this.state.events[eventPath];
return nextListener();
}
return nextListener(
new Error('failed re-establishing listener to path: ' + eventPath, e)
);
}
//update our ref count so we dont subscribe again
_this.state.refCount[listener.eventKey] = 1;
//create our mapping between the listener id and
_this.__updateListenerRef(listener, response.id);
nextListener();
});
});
},
nextEvent
);
},
callback
);
};
HappnClient.prototype.__retryReconnect = function(options, reason, e) {
var _this = this;
_this.emit('reconnect-error', {
reason: reason,
error: e
});
if (_this.status === STATUS.DISCONNECTED) {
clearTimeout(_this.__retryReconnectTimeout);
return;
}
_this.__retryReconnectTimeout = setTimeout(function() {
_this.reconnect.call(_this, options);
}, 3000);
};
HappnClient.prototype.reconnect = function(options) {
var _this = this;
_this.status = STATUS.RECONNECT_ACTIVE;
_this.emit('reconnect', options);
_this.authenticate(function(e) {
if (e) {
_this.handle_error(e);
return _this.__retryReconnect(options, 'authentication-failed', e);
}
//we are authenticated and ready for data requests
_this.status = STATUS.ACTIVE;
_this.__reattachListeners(function(e) {
if (e) {
_this.handle_error(e);
return _this.__retryReconnect(options, 'reattach-listeners-failed', e);
}
_this.emit('reconnect-successful', options);
});
});
};
HappnClient.prototype.handle_error = function(err) {
var errLog = {
timestamp: Date.now(),
error: err
};
if (this.state.errors.length === 100) this.state.errors.shift();
this.state.errors.push(errLog);
this.emit('error', err);
this.log.error('unhandled error', err);
};
HappnClient.prototype.__attachPublishedAck = function(options, message) {
var _this = this;
if (typeof options.onPublished !== 'function')
throw new Error('onPublished handler in options is missing');
var publishedTimeout = options.onPublishedTimeout || 60000; //default is one minute
var ackHandler = {
id: message.sessionId + '-' + message.eventId,
onPublished: options.onPublished,
handle: function(e, results) {
clearTimeout(ackHandler.timeout);
delete _this.state.ackHandlers[ackHandler.id];
ackHandler.onPublished(e, results);
},
timedout: function() {
ackHandler.handle(new Error('publish timed out'));
}
};
ackHandler.timeout = setTimeout(ackHandler.timedout, publishedTimeout);
_this.state.ackHandlers[ackHandler.id] = ackHandler;
};
HappnClient.prototype.handle_ack = function(message) {
if (this.state.ackHandlers[message.id]) {
if (message.status === 'error')
return this.state.ackHandlers[message.id].handle(new Error(message.error), message.result);
this.state.ackHandlers[message.id].handle(null, message.result);
}
};
HappnClient.prototype.handle_publication = function(message) {
if (message.encrypted) {
if (message._meta && message._meta.type === 'login') message = this.__decryptLogin(message);
else message = this.__decryptPayload(message.encrypted);
}
if (message._meta && message._meta.type === 'data')
return this.handle_data(message._meta.channel, message);
if (message._meta && message._meta.type === 'system')
return this.__handleSystemMessage(message);
if (message._meta && message._meta.type === 'ack') return this.handle_ack(message);
if (Array.isArray(message)) return this.handle_response_array(null, message, message.pop());
if (message._meta.status === 'error') {
var error = message._meta.error;
var e = new Error();
e.name = error.name || error.message || error;
Object.keys(error).forEach(function(key) {
if (!e[key]) e[key] = error[key];
});
return this.handle_response(e, message);
}
var decoded;
if (message.data) {
var meta = message._meta;
if (Array.isArray(message.data)) decoded = message.data.slice();
else decoded = Object.assign({}, message.data);
decoded._meta = meta;
} else decoded = message;
if (message.data === null) decoded._meta.nullData = true;
this.handle_response(null, decoded);
};
HappnClient.prototype.handle_response_array = function(e, response, meta) {
var responseHandler = this.state.requestEvents[meta.eventId];
if (responseHandler) responseHandler.handleResponse(e, response);
};
HappnClient.prototype.handle_response = function(e, response) {
var responseHandler = this.state.requestEvents[response._meta.eventId];
if (responseHandler) {
if (response._meta.nullData) return responseHandler.handleResponse(e, null);
responseHandler.handleResponse(e, response);
}
};
HappnClient.prototype.__acknowledge = function(message, callback) {
if (message._meta.consistency !== this.CONSTANTS.CONSISTENCY.ACKNOWLEDGED)
return callback(message);
this.__performDataRequest(message.path, 'ack', message._meta.publicationId, null, function(e) {
if (e) {
message._meta.acknowledged = false;
message._meta.acknowledgedError = e;
} else message._meta.acknowledged = true;
callback(message);
});
};
HappnClient.prototype.delegate_handover = function(data, meta, delegate) {
if (delegate.variableDepth && delegate.depth < meta.depth) return;
delegate.runcount++;
if (delegate.count > 0 && delegate.runcount > delegate.count) return;
//swallow any successive publicatiions
if (delegate.count === delegate.runcount) {
var _this = this;
return _this._offListener(delegate.id, function(e) {
if (e) return _this.handle_error(e);
delegate.handler.call(_this, JSON.parse(data), meta);
});
}
delegate.handler.call(this, JSON.parse(data), meta);
};
HappnClient.prototype.handle_data = function(path, message) {
var _this = this;
_this.__acknowledge(message, function(acknowledged) {
if (!_this.state.events[path]) return;
if (acknowledged._meta.acknowledgedError)
_this.log.error(
'acknowledgement failure: ',
acknowledged._meta.acknowledgedError.toString(),
acknowledged._meta.acknowledgedError
);
var intermediateData = JSON.stringify(acknowledged.data);
function doHandover(delegate) {
_this.delegate_handover(intermediateData, acknowledged._meta, delegate);
}
//slice necessary so order is not messed with for count based subscriptions
_this.state.events[path].slice().forEach(doHandover);
});
};
HappnClient.prototype.__clearSubscriptionsOnPath = function(eventListenerPath) {
var _this = this;
var listeners = this.state.events[eventListenerPath];
listeners.forEach(function(listener) {
_this.__clearListenerState(eventListenerPath, listener);
});
};
HappnClient.prototype.__clearSecurityDirectorySubscriptions = function(path) {
var _this = this;
Object.keys(this.state.events).forEach(function(eventListenerPath) {
if (path === eventListenerPath.substring(eventListenerPath.indexOf('@') + 1)) {
_this.__clearSubscriptionsOnPath(eventListenerPath);
}
});
};
HappnClient.prototype.__updateSecurityDirectory = function(message) {
var _this = this;
if (message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_REMOVED) {
if (['*', 'on'].indexOf(message.data.action))
return this.__clearSecurityDirectorySubscriptions(message.data.changedData.path);
}
if (message.data.whatHappnd === _this.CONSTANTS.SECURITY_DIRECTORY_EVENTS.UPSERT_GROUP) {
Object.keys(message.data.changedData.permissions).forEach(function(permissionPath) {
var permission = message.data.changedData.permissions[permissionPath];
if (
permission.prohibit &&
(permission.prohibit.indexOf('on') > -1 || permission.prohibit.indexOf('*') > -1)
)
return _this.__clearSecurityDirectorySubscriptions(permissionPath);
});
}
};
HappnClient.prototype.__handleServerSideDisconnect = function(message) {
this.emit('session-ended', message.data);
};
HappnClient.prototype.__handleSystemMessage = function(message) {
if (message.eventKey === 'server-side-disconnect') {
this.status = STATUS.DISCONNECTED;
this.__handleServerSideDisconnect(message);
}
if (message.eventKey === 'security-data-changed') this.__updateSecurityDirectory(message);
this.state.systemMessageHandlers.every(function(messageHandler) {
return messageHandler.apply(messageHandler, [message.eventKey, message.data]);
});
};
HappnClient.prototype.offSystemMessage = function(index) {
this.state.systemMessageHandlers.splice(index, 1);
};
HappnClient.prototype.onSystemMessage = function(handler) {
this.state.systemMessageHandlers.push(handler);
return this.state.systemMessageHandlers.length - 1;
};
HappnClient.prototype._remoteOn = function(path, parameters, callback) {
this.__performDataRequest(path, 'on', null, parameters, callback);
};
HappnClient.prototype.__clearListenerState = function(path, listener) {
this.state.events[path].splice(this.state.events[path].indexOf(listener), 1);
if (this.state.events[path].length === 0) delete this.state.events[path];
this.state.refCount[listener.eventKey]--;
if (this.state.refCount[listener.eventKey] === 0) delete this.state.refCount[listener.eventKey];
this.__clearListenerRef(listener);
};
HappnClient.prototype.__confirmRemoteOn = function(path, parameters, listener, callback) {
var _this = this;
_this._remoteOn(path, parameters, function(e, response) {
if (e || response.status === 'error') {
_this.__clearListenerState(path, listener);
//TODO: do we want to return something that is not an error (response.payload)
if (response && response.status === 'error') return callback(response.payload);
return callback(e);
}
if (listener.initialEmit) {
//emit data as events immediately
response.forEach(function(message) {
listener.handler(message);
});
_this.__updateListenerRef(listener, response._meta.referenceId);
} else if (listener.initialCallback) {
//emit the data in the callback
_this.__updateListenerRef(listener, response._meta.referenceId);
} else {
_this.__updateListenerRef(listener, response.id);
}
if (parameters.onPublished) return callback(null, listener.id);
if (listener.initialCallback) return callback(null, listener.id, response);
callback(null, listener.id);
});
};
HappnClient.prototype.__getListener = function(handler, parameters, path, variableDepth) {
return {
handler: handler,
count: parameters.count,
eventKey: JSON.stringify({
path: path,
event_type: parameters.event_type,
count: parameters.count,
initialEmit: parameters.initialEmit,
initialCallback: parameters.initialCallback,
meta: parameters.meta,
depth: parameters.depth
}),
runcount: 0,
meta: parameters.meta,
id: this.state.currentListenerId++,
initialEmit: parameters.initialEmit,
initialCallback: parameters.initialCallback,
depth: parameters.depth,
variableDepth: variableDepth
};
};
HappnClient.prototype.once = promisify(function(path, parameters, handler, callback) {
if (typeof parameters === 'function') {
callback = handler;
handler = parameters;
parameters = {};
}
parameters.count = 1;
return this.on(path, parameters, handler, callback);
});
HappnClient.prototype.on = promisify(function(path, parameters, handler, callback) {
if (typeof parameters === 'function') {
callback = handler;
handler = parameters;
parameters = {};
}
var variableDepth =
typeof path === 'string' && (path === '**' || path.substring(path.length - 3) === '/**');
if (!parameters) parameters = {};
if (!parameters.event_type || parameters.event_type === '*') parameters.event_type = 'all';
if (!parameters.count) parameters.count = 0;
if (variableDepth && !parameters.depth) parameters.depth = this.options.defaultVariableDepth; //5 by default
if (!callback) {
if (typeof parameters.onPublished !== 'function')
throw new Error('you cannot subscribe without passing in a subscription callback');
if (typeof handler !== 'function')
throw new Error('callback cannot be null when using the onPublished event handler');
callback = handler;
handler = parameters.onPublished;
}
path = this.getChannel(path, parameters.event_type);
var listener = this.__getListener(handler, parameters, path, variableDepth);
if (!this.state.events[path]) this.state.events[path] = [];
if (!this.state.refCount[listener.eventKey]) this.state.refCount[listener.eventKey] = 0;
this.state.events[path].push(listener)