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,489 lines (1,289 loc) • 67.4 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;
let utils, CONSTANTS;
if (!browser) {
CONSTANTS = require('./constants-builder');
utils = require('happn-commons').utils;
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 {
//DO NOT DELETE
//{{utils}}
//DO NOT DELETE
//{{constants}}
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,
});
}
}
function HappnClient() {
STATUS = CONSTANTS.CLIENT_STATE;
}
HappnClient.__instance = function (options) {
return new HappnClient().client(options);
};
HappnClient.create = utils.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 || null;
}
var client = new HappnClient().client(options);
if (options != null) {
if (options.testMode) HappnClient.lastClient = client;
if (typeof options.cookieEventHandler === 'function' && browser) {
HappnClient.addCookieEventHandler(
options.cookieEventHandler,
options.cookieName,
options.interval
);
}
}
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.getCookieEventDefaults = function (cookieName, interval) {
return {
cookieName: cookieName || 'happn_token',
interval:
isNaN(parseInt(interval)) || parseInt(interval) < 500
? 1000 // if not a number or < 500 set to sane default
: parseInt(interval),
};
};
HappnClient.addCookieEventHandler = function (handler, cookieName, interval) {
const cookieEventDefaults = HappnClient.getCookieEventDefaults(cookieName, interval);
const cookieCheckKey = `${cookieEventDefaults.cookieName}_${cookieEventDefaults.interval}`;
if (!HappnClient.__cookieEventHandlers) {
HappnClient.__cookieEventHandlers = {};
HappnClient.__cookieCheckIntervals = {};
}
if (!HappnClient.__cookieEventHandlers[cookieCheckKey]) {
HappnClient.__cookieEventHandlers[cookieCheckKey] = [];
}
if (HappnClient.__cookieEventHandlers[cookieCheckKey].indexOf(handler) === -1) {
HappnClient.__cookieEventHandlers[cookieCheckKey].push(handler);
}
if (!HappnClient.__cookieCheckStatuses) {
HappnClient.__cookieCheckStatuses = {};
}
if (HappnClient.__cookieCheckIntervals[cookieCheckKey] == null) {
HappnClient.__cookieCheckIntervals[cookieCheckKey] = HappnClient.__setupCookieCheckInterval(
cookieEventDefaults.cookieName,
cookieEventDefaults.interval,
HappnClient.__cookieEventHandlers[cookieCheckKey],
handler
);
}
};
HappnClient.__setupCookieCheckInterval = function (cookieName, interval, handlers) {
const cookieInstance = HappnClient.__getCookieInstance(cookieName);
HappnClient.__cookieCheckStatuses[cookieName] =
cookieInstance === '' ? 'cookie-deleted' : 'cookie-created';
let lastCreatedCookie = cookieInstance;
return setInterval(function () {
let currentCookie = HappnClient.__getCookieInstance(cookieName);
let currentStatus = HappnClient.__cookieCheckStatuses[cookieName];
if (currentCookie === '' && currentStatus === 'cookie-created') {
HappnClient.__cookieCheckStatuses[cookieName] = 'cookie-deleted';
HappnClient.__emitCookieEvent('cookie-deleted', lastCreatedCookie, handlers);
} else if (currentCookie.length > 0 && currentStatus === 'cookie-deleted') {
lastCreatedCookie = currentCookie;
HappnClient.__cookieCheckStatuses[cookieName] = 'cookie-created';
HappnClient.__emitCookieEvent('cookie-created', currentCookie, handlers);
}
}, interval);
};
HappnClient.clearCookieEventObjects = function () {
if (HappnClient.__cookieEventHandlers == null) return;
Object.values(HappnClient.__cookieCheckIntervals).forEach((cookieEventInterval) => {
clearInterval(cookieEventInterval);
});
HappnClient.__cookieEventHandlers = {};
HappnClient.__cookieCheckIntervals = {};
};
HappnClient.__emitCookieEvent = function (event, cookie, handlers) {
handlers.forEach((handler) => {
handler(event, cookie);
});
};
HappnClient.__getCookieInstance = function (cname, sessionDocument) {
sessionDocument = sessionDocument || document;
const name = `${cname}=`;
const decodedCookie = decodeURIComponent(sessionDocument.cookie);
const ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return '';
};
HappnClient.prototype.client = function (options) {
this.__connectionQueue = this.createQueue();
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 = utils.maybePromisify(function (callback) {
//ensure session scope is not on the prototype
this.session = null;
if (browser) {
return this.__getResources((e) => {
if (e) return callback(e);
this.connect((e) => {
if (e) return callback(e);
this.status = STATUS.ACTIVE;
callback(null, this);
});
});
}
this.connect((e) => {
if (e) return callback(e);
this.status = STATUS.ACTIVE;
callback(null, this);
});
});
HappnClient.prototype.__prepareCookieOptions = function (options) {
if (options.info._browser) {
this.cookieEventHandler = options.cookieEventHandler;
this.cookieEventInterval = options.cookieEventInterval;
}
};
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 === 'localhost') 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 = Object.assign({}, options.config);
for (var optionProperty in options) {
if (optionProperty !== 'config' && !preparedOptions[optionProperty])
preparedOptions[optionProperty] = options[optionProperty];
}
} else preparedOptions = Object.assign({}, options);
if (!preparedOptions.callTimeout) preparedOptions.callTimeout = 60000; //1 minute
if (!preparedOptions.retryOnSocketErrorMaxInterval) {
preparedOptions.retryOnSocketErrorMaxInterval = 120e3;
}
if (!preparedOptions.reconnectWait) {
//wait a second
preparedOptions.reconnectWait = 1e3;
}
//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;
this.__prepareCookieOptions(preparedOptions);
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) {
const syncOption = (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) {
this.__connectionCleanup((e) => {
if (e) return callback(e);
this.options.socket.manual = true; //because we want to explicitly call open()
this.__connectSocket(callback);
});
};
HappnClient.prototype.__waitForConnection = function (socket, callback) {
return () => {
this.status = STATUS.ACTIVE;
this.serverDisconnected = false;
callback(null, socket);
};
};
HappnClient.prototype.__onSocketError = function (socket, callback) {
return (e) => {
if (this.status === STATUS.CONNECTING) {
// ERROR before connected,
// ECONNREFUSED etc. out as errors on callback
this.status = STATUS.CONNECT_ERROR;
this.__endAndDestroySocket(socket, (destroyErr) => {
if (destroyErr)
this.log.warn(
'socket.end failed in client connection failure: ' + destroyErr.toString()
);
callback(e.error || e);
});
return;
}
this.handle_error(e.error || e);
};
};
HappnClient.prototype.__onSocketTimeout = function (callback) {
return () => {
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'));
};
};
HappnClient.prototype.__connectSocket = function (callback) {
var socket;
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.once('timeout', this.__onSocketTimeout(callback));
socket.once('open', this.__waitForConnection(socket, callback));
socket.once('error', this.__onSocketError(socket, callback));
socket.open();
};
HappnClient.prototype.__initializeState = function () {
this.state = {};
this.__initializeEventState();
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 () {
this.onEvent = (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 = (handlerId) => {
var eventName = handlerId.split('|')[0];
var eventIndex = parseInt(handlerId.split('|')[1]);
this.state.eventHandlers[eventName][eventIndex] = null;
};
this.emit = (eventName, eventData) => {
if (this.state.eventHandlers[eventName]) {
this.state.eventHandlers[eventName].forEach((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 = utils.maybePromisify(function (callback) {
this.__connectionCleanup(callback);
});
HappnClient.prototype.__ensureCryptoLibrary = utils.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 {
const CryptoUtils = require('happn-util-crypto');
crypto = new CryptoUtils();
callback();
}
});
HappnClient.prototype.__writeCookie = function (session, sessionDocument) {
const cookie = this.__getCookie(session);
sessionDocument.cookie = cookie;
};
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, useCookie) {
delete result._meta;
this.session = result;
if (browser) {
if (!useCookie) {
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 login = (cb) => {
this.__performSystemRequest(
'login',
loginParameters,
{
timeout: this.options.loginTimeout,
},
(e, result) => {
if (e) return cb(e);
if (result._meta.status === 'ok') {
this.__attachSession(result, loginParameters.useCookie);
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;
utils.whilst(
() => {
return currentAttempt < this.options.loginRetry && loggedIn === false;
},
(_attempt, next) => {
currentAttempt++;
login((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) {
if (loginParameters.loginType !== 'digest') {
return callback(null, loginParameters);
}
this.__performSystemRequest(
'request-nonce',
{
publicKey: loginParameters.publicKey,
},
null,
(e, response) => {
if (e) return callback(e);
loginParameters.digest = this.__signNonce(response.nonce);
callback(null, loginParameters);
}
);
};
HappnClient.prototype.__prepareAndDoLogin = function (loginParameters, callback) {
this.__prepareLogin(loginParameters, (e, preparedParameters) => {
if (e) return callback(e);
this.__doLogin(preparedParameters, callback);
});
};
HappnClient.prototype.__sessionConfigureAndDescribe = function () {
return new Promise((resolve, reject) => {
this.__performSystemRequest(
'configure-session',
{
protocol: PROTOCOL,
version: HAPPN_VERSION,
browser: browser,
},
null,
(e) => {
if (e) return reject(e);
this.__performSystemRequest('describe', null, null, function (e, serverInfo) {
if (e) return reject(e);
resolve(serverInfo);
});
}
);
});
};
HappnClient.prototype.login = utils.maybePromisify(function (callback) {
const loginParameters = {
username: this.options.username,
info: this.options.info,
protocol: PROTOCOL,
};
if (this.options && this.options.authType) loginParameters.authType = this.options.authType;
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((serverInfo) => {
this.serverInfo = serverInfo;
if (!this.serverInfo.secure) return this.__doLogin(loginParameters, callback);
if (this.options.useCookie) {
if (!browser) {
return callback(new Error('Logging in with cookie only valid in browser'));
}
loginParameters.token = HappnClient.__getCookieInstance(serverInfo.cookieName);
loginParameters.useCookie = true;
}
if (!loginParameters.token && !loginParameters.username) {
return callback(new Error('happn server is secure, please specify a username or token'));
}
if (
!loginParameters.password &&
!loginParameters.token &&
loginParameters.username !== '_ANONYMOUS'
) {
if (!loginParameters.publicKey)
return callback(new Error('happn server is secure, please specify a password'));
loginParameters.loginType = 'digest';
}
if (loginParameters.loginType !== 'digest')
return this.__doLogin(loginParameters, callback);
this.__ensureCryptoLibrary((e) => {
if (e) return callback(e);
if (!this.options.privateKey || !this.options.publicKey)
return callback(
new Error('login type is digest, but no privateKey and publicKey specified')
);
loginParameters.publicKey = this.options.publicKey;
this.__prepareAndDoLogin(loginParameters, callback);
});
})
.catch((e) => {
this.emit('connect-error', e);
callback(e);
});
});
HappnClient.prototype.connect = utils.maybePromisify(function (callback) {
let contextError, contextResult;
this.__connectionQueue
.push(() => {
return new Promise((resolve, reject) => {
this.__getConnection((e, socket) => {
if (e) return reject(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));
this.login((e, result) => {
if (e) return reject(e);
resolve(result);
});
});
});
})
.then(
(result) => {
contextResult = result;
},
(e) => {
contextError = e;
}
)
.finally(() => {
callback(contextError, contextResult);
});
});
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 callbackHandler = {
eventId: message.eventId,
};
callbackHandler.handleResponse = (e, response) => {
clearTimeout(callbackHandler.timedout);
delete this.state.requestEvents[callbackHandler.eventId];
return callback(e, response);
};
callbackHandler.timedout = setTimeout(() => {
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.__checkRequestStatus = function (path, action, callback) {
if (this.status !== STATUS.ACTIVE) {
let errorMessage = 'client not active';
if ([STATUS.CONNECT_ERROR, STATUS.ERROR].includes(this.status)) {
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';
let errorDetail = 'action: ' + action + ', path: ' + path;
let error = new Error(errorMessage);
error.detail = errorDetail;
this.__asyncErrorCallback(error, callback);
return false;
}
return true;
};
HappnClient.prototype.__performDataRequest = function (path, action, data, options, callback) {
if (!this.__checkRequestStatus(path, action, callback)) {
return;
}
var message = {
action: action,
eventId: this.getEventId(),
path: path,
data: data,
sessionId: this.session.id,
};
if (!options || typeof options !== 'object') {
options = {};
} else {
message.options = options; //else skip sending up the options
}
if (['set', 'remove'].indexOf(action) >= 0) {
if (
options.consistency === CONSTANTS.CONSISTENCY.DEFERRED ||
options.consistency === CONSTANTS.CONSISTENCY.ACKNOWLEDGED
)
this.__attachPublishedAck(options, message);
}
if (!options.timeout) options.timeout = this.options.callTimeout;
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) {
if (typeof callback !== 'function') {
throw new Error('Invalid system call');
}
let 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) {
utils.checkPath(path);
return '/' + action.toUpperCase() + '@' + path;
};
HappnClient.prototype.get = utils.maybePromisify(function (path, parameters, handler) {
if (typeof parameters === 'function') {
handler = parameters;
parameters = {};
}
this.__performDataRequest(path, 'get', null, parameters, handler);
});
HappnClient.prototype.getCreatedBetween = utils.maybePromisify(function (
path,
from,
to,
handler
) {
if (typeof handler !== 'function') {
throw new Error('handler is null or not a function');
}
if (isNaN(from)) {
throw new Error('from is null or not a timestamp');
}
if (isNaN(to)) {
throw new Error('to is null or not a timestamp');
}
this.__performDataRequest(
path,
'get',
null,
{
criteria: {
$and: [
{
'_meta.created': {
$gte: from,
},
},
{
'_meta.created': {
$lte: to,
},
},
],
},
},
handler
);
});
HappnClient.prototype.count = utils.maybePromisify(function (path, parameters, handler) {
if (typeof parameters === 'function') {
handler = parameters;
parameters = {};
}
this.__performDataRequest(path, 'count', null, parameters, handler);
});
HappnClient.prototype.getPaths = utils.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 = utils.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 = utils.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
utils.checkPath(path, 'set');
this.__performDataRequest(path, 'set', data, options, handler);
} catch (e) {
return handler(e);
}
});
HappnClient.prototype.set = utils.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
utils.checkPath(path, 'set');
this.__performDataRequest(path, 'set', data, options, handler);
} catch (e) {
return handler(e);
}
});
HappnClient.prototype.remove = utils.maybePromisify(function (path, options, handler) {
if (typeof options === 'function') {
handler = options;
options = {};
}
return this.__performDataRequest(path, 'remove', null, options, 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.__reattachListenersOnPermissionChange = function (permissions) {
if (!permissions || Object.keys(permissions).length === 0) return;
let listenerPermPaths = Object.keys(permissions).filter(
(key) =>
permissions[key].actions &&
(permissions[key].actions.includes('*') || permissions[key].actions.includes('on'))
);
if (listenerPermPaths.length === 0) return;
this.state.refCount = this.state.refCount || {};
let channels = this.__getChannelsFromPaths(listenerPermPaths);
if (channels && channels.length) {
this.__resetListenersRefsOnChannels(channels);
channels.forEach((channel) => {
this.__reattachListenersOnChannel(channel);
});
}
};
HappnClient.prototype.__getChannelsFromPaths = function (paths) {
if (!paths) return;
let allChannels = Object.keys(this.state.events);
let channels = paths.reduce(
(selected, current) =>
selected.concat(allChannels.filter((channel) => channel.endsWith(current))),
[]
);
return channels;
};
HappnClient.prototype.__resetListenersRefsOnChannels = function (channels) {
if (!channels) return;
channels.forEach((channel) => this.__resetListenerRefs(this.state.events[channel]));
};
HappnClient.prototype.__resetListenerRefs = function (listeners) {
if (!listeners) return;
listeners.forEach((listener) => (this.state.refCount[listener.eventKey] = 0));
};
HappnClient.prototype.__reattachListenersOnChannel = function (channel) {
let listeners = this.state.events[channel];
if (listeners && listeners.length) {
listeners.forEach((listener) => this.__tryReattachListener(listener, channel));
} else {
//test channel and clean-up, possibly unnecessary
this.__testAndCleanupChannel(channel);
}
};
HappnClient.prototype.__tryReattachListener = function (listener, channel) {
if (!this.state.events[channel]) return this.__clearListenerRef(listener); // We have deleted this key.
if (this.state.refCount[listener.eventKey]) {
//we are already listening on this key
this.state.refCount[listener.eventKey]++;
return;
}
let parameters = {};
if (listener.meta) parameters.meta = listener.meta;
this._offPath(channel, (e) => {
if (e)
this.log.warn('failed detaching listener to channel, on re-establishment: ' + channel, e);
this._remoteOn(channel, parameters, (e, response) => {
if (e) {
if ([403, 401].indexOf(e.code) > -1) {
//permissions may have changed regarding this path
delete this.state.events[channel];
return this.__clearListenerRef(listener);
}
this.log.warn('failed re-establishing listener to channel: ' + channel, e);
return this.__clearListenerRef(listener);
}
this.state.refCount[listener.eventKey] = 1;
return this.__updateListenerRef(listener, response.id);
});
});
};
HappnClient.prototype.__testAndCleanupChannel = function (channel) {
this._remoteOn(channel, {}, (e, response) => {
if (e) {
if ([403, 401].indexOf(e.code) > -1) {
//permissions may have changed regarding this path
delete this.state.events[channel];
return;
}
this.log.warn('Error on channel cleanup after permissions change, channel: ' + channel, e);
} else {
this._remoteOff(channel, response.id, () => {});
}
});
};
HappnClient.prototype.__reattachListeners = function (callback) {
utils.async(
Object.keys(this.state.events),
(eventPath, _index, nextEvent) => {
var listeners = this.state.events[eventPath];
this.state.refCount = {};
// re-establish each listener individually to preserve original meta and listener id
utils.async(
listeners,
(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, (e) => {
if (e)
return nextListener(
new Error(
'failed detaching listener to path, on re-establishment: ' + eventPath,
e
)
);
this._remoteOn(eventPath, parameters, (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) {
if (this.__retryReconnectInterval == null) {
this.__retryReconnectInterval = 1e3;
} else {
if (this.__retryReconnectInterval * 2 > this.options.retryOnSocketErrorMaxInterval) {
// set the interval to max
this.__retryReconnectInterval = this.options.retryOnSocketErrorMaxInterval;
} else {
// double the interval
this.__retryReconnectInterval = this.__retryReconnectInterval * 2;
}
}
this.emit('reconnect-error', {
reason: reason,
error: e,
});
if (this.status === STATUS.DISCONNECTED) {
clearTimeout(this.__retryReconnectTimeout);
return;
}
this.__retryReconnectTimeout = setTimeout(() => {
this.log.warn(`retrying reconnection after ${this.__retryReconnectInterval}ms`);
this.reconnect(options);
}, this.__retryReconnectInterval);
};
HappnClient.prototype.reconnect = function (options = {}) {
this.status = STATUS.RECONNECT_ACTIVE;
this.emit('reconnect', options);
this.connect((e) => {
if (e) {
return this.__retryReconnect(options, 'authentication-failed', e);
}
//clear all reconnect cycle parameters
clearTimeout(this.__retryReconnectTimeout);
this.__retryReconnectInterval = null;
//we are connected and ready for data requests
this.status = STATUS.ACTIVE;
//ensure our subscriptions are re-established
this.__reattachListeners((e) => {
if (e) {
//back into a reconnect cycle
return this.__retryReconnect(options, 'reattach-listeners-failed', e);
}
this.emit('reconnect-successful', options);
});
});
};
HappnClient.prototype.handle_error = function (err, reconnect = true) {
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('socket error', err);
if (!this.socket || !reconnect) return;
this.status = STATUS.ERROR; // so we dont write any more messages until we have reconnected again
//wait a second, this is so we don't flood the stack when there are consistent socket failures
this.__reconnectTimeout = setTimeout(() => {
this.log.warn('attempting reconnection after socket error...');
this.reconnect();
}, this.options.reconnectWait);
};
HappnClient.prototype.__attachPublishedAck = function (options, message) {
if (typeof options.onPublished !== 'function')
throw new Error('onPublished handler in options is missing');
const publishedTimeout = options.onPublishedTimeout || 60000; //default is one minute
const ackHandler = {
id: message.sessionId + '-' + message.eventId,
onPublished: options.onPublished,
handle: (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._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 !== 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) {
this.__acknowledge(message, (acknowledged) => {
if (!this.state.events[path]) return;
if (acknowledged._meta.acknowledgedError)
this.log.warn(
'acknowledgement failure: ',
acknowledged._meta.acknowledgedError.toString(),
acknowledged._meta.acknowledgedError
);
var intermediateData = JSON.stringify(acknowledged.data);
let 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 listeners = this.state.events[eventListenerPath];
listeners.forEach((listener) => {
this.__clearListenerState(eventListenerPath, listener);
});
};
HappnClient.prototype.__clearSecurityDirectorySubscriptions = function (path) {
Object.keys(this.state.events).forEach((even