crypto-exchanges-ws-client
Version:
Node.js implementation of websocket protocol used by Crypto Exchange Gateway
1,308 lines (1,210 loc) • 34.6 kB
JavaScript
;
const debug = require('debug')('CEWSC:Client');
const EventEmitter = require('events');
const _ = require('lodash');
const url = require('url');
const querystring = require('querystring');
const WebSocketConnection = require('./websocket-connection');
// how long should we wait before trying to reconnect upon disconnection
const RETRY_DELAY = 10 * 1000;
class Client extends EventEmitter
{
/*
Following events related to connection can be emitted
1) connectionError, when a connection/reconnection error occurs (ie: WS cannot be connnected)
Data will be an object {connectionId:integer,attempts:integer,error:err}
- connectionId : id of WS connection
- attempts : number of attempts to connect
- error : the connection error which occurred
Reconnection will be automatic
2) disconnected, when WS has been disconnected by gateway
Data will be an object {connectionId,code:integer,reason:string}
- connectionId : id of WS connection
- code: disconnection code
- reason : disconnection reason
Reconnection will be automatic
3) terminated, when connection failed after last connection retry
This is a final event. Client will need to call reconnect
Data will be an object {connectionId:integer,attempts:integer,error:err}
- connectionId : id of WS connection
- attempts : number of attempts to connect
- error : the connection error which occurred
4) connected, when websocket connection is connected/reconnected (but not yet ready)
Data will be an object {connectionId:integer}
5) ready, when websocket connection is ready to receive/send messages
Event will only be emitted once in the lifetime of the object, after receiving initial 'hello' message
Data will be an object {sessionId:string,isNew:boolean}
Following other events can be emitted
- ticker : one per exchange/pair combination
- orderBook : one per exchange/pair combination for full order book
- orderBookUpdate : one per exchange/pair combination for order book diff with previous full order book
- trades : one per exchange/pair combination
- kline : one per exchange/pair/interval combination
- tickerMonitor : one per alert
If options.globalListener is true, all exchange related events will be emitted using 'notification' event with following format :
{
"notification":string (ticker|orderBook|orderBookUpdate|trades),
"exchange":string,
"pair":string,
"data":object
}
*/
constructor(uri, options)
{
super();
let u = url.parse(uri);
// the uri we want to connect to
if (u.protocol != 'ws:' && u.protocol != 'wss:')
{
throw new Error("Argument 'uri' should start with 'ws://' or 'wss://'");
}
this._uri = `${u.protocol}//${u.host}`;
if (null !== u.pathname)
{
this._uri += u.pathname;
}
else
{
this._uri += '/';
}
this._queryParams = {};
// sessionId to use
this._sessionId = null;
// parse query
if (null !== u.query)
{
let hash = querystring.parse(u.query);
_.forEach(hash, (value, key) => {
if ('sid' == key)
{
this._sessionId = value;
return;
}
this._queryParams[key] = value;
});
}
this._isEphemeral = false;
if (('true' === this._queryParams['expires'] || '1' === this._queryParams['expires']) &&
('0' === this._queryParams['timeout']))
{
this._isEphemeral = true;
}
// ticker monitor connection
this._tickerMonitor = {
enabled:false,
uri:`${this._uri}tickerMonitor/`,
connection:null
}
// whether or not client wants to have a global listener for all exchange related events
this._globalListener = false;
// whether or not socket should be connected automatically
let autoConnect = true;
this._retryDelay = RETRY_DELAY;
this._connectionOptions = {}
if (undefined !== options)
{
if (false === options.autoConnect)
{
autoConnect = false;
}
if (undefined !== options.globalListener)
{
if (true === options.globalListener)
{
this._globalListener = true;
}
}
if (undefined !== options.sessionId)
{
let sid = options.sessionId.trim();
if ('' != sid)
{
this._sessionId = sid;
}
}
if (undefined !== options.apiKey && '' != options.apiKey)
{
this._connectionOptions.apiKey = options.apiKey;
}
// retry count
if (undefined !== options.retryCount)
{
if ('always' === options.retryCount)
{
this._connectionOptions.retryCount = -1;
}
else
{
let value = parseInt(options.retryCount);
if (isNaN(value) || value < 0)
{
throw new Error("Argument 'options.retryCount' should be an integer >= 0");
}
this._connectionOptions.retryCount = value;
}
}
if (undefined !== options.retryDelay)
{
let value = parseInt(options.retryDelay);
if (isNaN(value) || value < 1000)
{
throw new Error("Argument 'options.retryDelay' should be an integer >= 1000");
}
this._connectionOptions.retryDelay = value;
this._retryDelay = value;
}
if (undefined !== options.pingTimeout)
{
let value = parseInt(options.pingTimeout);
if (isNaN(value) || value < 1000)
{
throw new Error("Argument 'options.pingTimeout' should be an integer >= 1000");
}
this._connectionOptions.pingTimeout = value;
}
// ticker monitor
if (undefined !== options.tickerMonitor)
{
if (true === options.tickerMonitor.enabled)
{
this._tickerMonitor.enabled = true;
let params = {types:'active','emit':false};
if (undefined !== options.tickerMonitor.types)
{
let types = [];
if (true === options.tickerMonitor.types.active)
{
types.push('active');
}
if (true === options.tickerMonitor.types.inactive)
{
types.push('inactive');
}
if (0 != types.length)
{
params.types = types.join(',');
}
}
if (true === options.tickerMonitor.getInitialState)
{
params.emit = true;
}
this._tickerMonitor.uri += `?types=${params.types}&emit=${params.emit}`;
}
}
}
// keep track of how many connections were performed
this._connectionCounter = 0;
this._connection = null;
// timestamp of last connected event
this._connectedTimestamp = null;
// timestamp when 'ready' event was emitted
this._readyTimestamp = null;
// id of next command
this._nextCommandId = 1;
// mapping commandId => callback
this._callbacks = {};
// queue used when trying to send commands while ws is not connected yet
this._queue = [];
if (autoConnect)
{
this.connect();
}
}
_getUri()
{
let uri = this._uri;
let params = {};
if (null !== this._sessionId)
{
params['sid'] = this._sessionId;
}
_.forEach(this._queryParams, (value, key) => {
switch (key)
{
// only send expires & timeout on initial connection
case 'expires':
case 'timeout':
if (null !== this._readyTimestamp)
{
break;
}
default:
params[key] = value;
}
});
if (!_.isEmpty(params))
{
uri = uri + '?' + querystring.stringify(params);
}
return uri;
}
getSessionId()
{
return this._sessionId;
}
/**
* Reconnect WS
*
*/
reconnect()
{
if (null === this._connection)
{
return;
}
let connection = this._connection;
connection.disconnect();
this._createConnection();
}
/*
* Connect WS
*
* Should not be necessary since connection will happen automatically on first call to 'execute' method
*/
connect()
{
// create if needed
if (null !== this._connection)
{
return;
}
this._createConnection();
}
isConnected()
{
if (null === this._connection)
{
return false;
}
return this._connection.isConnected()
}
isReady()
{
return null !== this._readyTimestamp;
}
_checkExchange(exchange)
{
if ('string' !== typeof exchange || '' == exchange)
{
throw new Error("Argument 'exchange' should be a non-empty string");
}
}
_checkExchangeAndPairs(exchange, pairs)
{
if ('string' !== typeof exchange || '' == exchange)
{
throw new Error("Argument 'exchange' should be a non-empty string");
}
if (!Array.isArray(pairs))
{
throw new Error("Argument 'pairs' should be an array");
}
}
/**
* Retrieves existing pairs
*
* @param {string} exchange exchange id
* @param {object} filter {currency:string,baseCurrency:string}
* @param {function} cb callback to call (mandatory)
*/
getPairs(exchange, filter, cb)
{
this._checkExchange(exchange);
let params = {
exchange:exchange
}
// cb might have been passed as second parameter
if (undefined === cb)
{
if ('function' != typeof(filter))
{
throw new Error("Argument 'cb' is mandatory");
}
cb = filter;
}
else
{
if (undefined !== filter)
{
if (undefined !== filter.currency && '' !== filter.currency)
{
params.filter = {currency:filter.currency};
}
else if (undefined !== filter.baseCurrency && '' !== filter.baseCurrency)
{
params.filter = {baseCurrency:filter.baseCurrency};
}
}
}
this.execute('getPairs', params, cb);
}
/**
* Subscribe to tickers for a list of pairs
*
* NB: it is also possible to call method using subscribeToTickers(exchange, pairs, cb)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {boolean} reset if true, existing subscriptions will be discarded and replaced by new ones (optional, default = false)
* @param {function} cb callback to call upon receiving command result (optional)
*/
/*
Following events will be triggered :
- one 'ticker' event per pair, with following data
{
"exchange":"bittrex",
"pair":"USDT-ETH",
"data":{
"pair":"USDT-ETH",
"timestamp":1508970583.279,
"priceChangePercent":-4.43,
"high":323.81945883,
"low":304.64171183,
"last":308.36163935,
"buy":308.33244613,
"sell":308.48062765,
"volume":10061.40862720042
}
}
*/
subscribeToTickers(exchange, pairs, reset, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
reset:false,
pairs:pairs
}
if (undefined !== reset)
{
if (true === reset || false === reset)
{
params.reset = reset;
}
// probably a callback
else if ('function' == typeof reset)
{
cb = reset;
}
}
this.execute('subscribeToTickers', params, cb);
}
/**
* Unsubscribe from tickers for a list of pairs
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromTickers(exchange, pairs, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
pairs:pairs
}
this.execute('unsubscribeFromTickers', params, cb);
}
/**
* Unsubscribe from all tickers we are currently subscribed to
*
* @param {string} exchange exchange identifier
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromAllTickers(exchange, cb)
{
this._checkExchange(exchange);
let params = {
exchange:exchange
}
this.execute('unsubscribeFromAllTickers', params, cb);
}
/**
* Subscribe to order books for a list of pairs
*
* This will trigger following events :
*
* - orderBook : {exchange:string,pair:string,data:object}
*
* NB: it is also possible to call method using subscribeToOrderBooks(exchange, pairs, cb)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {boolean} reset if true, existing subscriptions will be discarded and replaced by new ones (optional, default = false)
* @param {function} cb callback to call upon receiving command result (optional)
*/
/*
Following events will be triggered :
- one 'orderBook' event per pair, with following data (event will be sent only once after each connection/reconnection or if 'resyncOrderBooks' is called)
{
"exchange":"bittrex",
"pair":"USDT-BTC",
"data":{
"cseq":1508933973,
"buy":[
{
"quantity":0.0725237,
"rate":5770.22611328
},
{
"quantity":0.07289463,
"rate":5762.79913753
},...
],
"sell":[
{
"quantity":0.08658098,
"rate":5771.06827704
},
{
"quantity":0.08536155,
"rate":5771.50324368
},...
]
}
}
- one 'orderBookUpdate' per pair (will be sent on each orderbook update)
{
"exchange":"bittrex",
"pair":"USDT-BTC",
"data":{
"cseq":1508933974,
"buy":[
{
"quantity":0.0725237,
"rate":5770.25730242,
"action":"update"
},
{
"quantity":0.0725237,
"rate":5770.22611328,
"action":"remove"
},...
],
"sell":[
{
"quantity":0.08658098,
"rate":5771.04349666,
"action":"update"
},
{
"quantity":0.08658098,
"rate":5771.06827704,
"action":"remove"
},...
]
}
}
*/
subscribeToOrderBooks(exchange, pairs, reset, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
reset:false,
pairs:pairs
}
if (undefined !== reset)
{
if (true === reset || false === reset)
{
params.reset = reset;
}
// probably a callback
else if ('function' == typeof reset)
{
cb = reset;
}
}
this.execute('subscribeToOrderBooks', params, cb);
}
/**
* Ask for order books resync for a list of pairs (this will trigger new 'orderBook' event)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {function} cb callback to call upon receiving command result (optional)
*/
/*
Following events will be triggered :
- one orderBook event for each pair (see 'subscribeToOrderBooks' method)
*/
resyncOrderBooks(exchange, pairs, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
pairs:pairs
}
this.execute('resyncOrderBooks', params, cb);
}
/**
* Unsubscribe from order books for a list of pairs
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromOrderBooks(exchange, pairs, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
pairs:pairs
}
this.execute('unsubscribeFromOrderBooks', params, cb);
}
/**
* Unsubscribe from all order books we are currently subscribed to
*
* @param {string} exchange exchange identifier
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromAllOrderBooks(exchange, cb)
{
this._checkExchange(exchange);
let params = {
exchange:exchange
}
this.execute('unsubscribeFromAllOrderBooks', params, cb);
}
/**
* Subscribe to trades for a list of pairs
*
* NB: it is also possible to call method using subscribeToTrades(exchange, pairs, cb)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {boolean} reset if true, existing subscriptions will be discarded and replaced by new ones (optional, default = false)
* @param {function} cb callback to call upon receiving command result (optional)
*/
/*
Following events will be triggered :
- one trades event for each pair
{
"exchange":"bittrex",
"pair":"USDT-BTC",
"data":[
{
"rate":5771,
"quantity":0.59180118,
"type":"sell",
"timestamp":1508956034.149,
"id":1508940328
},
{
"rate":5771,
"quantity":0.16332246,
"type":"buy",
"timestamp":1508956034.149,
"id":1508940329
},...
]
}
*/
subscribeToTrades(exchange, pairs, reset, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
reset:false,
pairs:pairs
}
if (undefined !== reset)
{
if (true === reset || false === reset)
{
params.reset = reset;
}
// probably a callback
else if ('function' == typeof reset)
{
cb = reset;
}
}
this.execute('subscribeToTrades', params, cb);
}
/**
* Unsubscribe from trades for a list of pairs
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromTrades(exchange, pairs, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
pairs:pairs
}
this.execute('unsubscribeFromTrades', params, cb);
}
/**
* Unsubscribe from all trades we are currently subscribed to
*
* @param {string} exchange exchange identifier
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromAllTrades(exchange, cb)
{
this._checkExchange(exchange);
let params = {
exchange:exchange
}
this.execute('unsubscribeFromAllTrades', params, cb);
}
/**
* Subscribe to klines for a list of pairs
*
* NB: it is also possible to call method using subscribeToKlines(exchange, pairs, interval, cb)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {string} klines interval (ex: 5m)
* @param {boolean} reset if true, existing subscriptions will be discarded and replaced by new ones (optional, default = false)
* @param {function} cb callback to call upon receiving command result (optional)
*/
/*
Following events will be triggered :
- one 'kline' event per pair/interval, with following data
{
"exchange":"binance",
"pair":"USDT-ETH",
"interval":"5m",
"data":{
"timestamp":1515410100,
"open":1135.5,
"close":1131.76,
"high":1136.3,
"low":1130.13,
"volume":74.30783
}
}
*/
subscribeToKlines(exchange, pairs, interval, reset, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
if ('string' !== typeof interval || '' == interval)
{
throw new Error("Argument 'interval' should be a non-empty string");
}
let params = {
exchange:exchange,
interval:interval,
reset:false,
pairs:pairs
}
if (undefined !== reset)
{
if (true === reset || false === reset)
{
params.reset = reset;
}
// probably a callback
else if ('function' == typeof reset)
{
cb = reset;
}
}
this.execute('subscribeToKlines', params, cb);
}
/**
* Unsubscribe from klines for a list of pairs
*
* NB: it is also possible to call method using unsubscribeFromKlines(exchange, pairs, cb)
*
* @param {string} exchange exchange identifier
* @param {array} pairs list of pairs (ex: ["USDT-BTC",...])
* @param {interval} string kline interval (ex: 5m) (optional, if not defined will unsubscribe for all intervals)
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromKlines(exchange, pairs, interval, cb)
{
this._checkExchangeAndPairs(exchange, pairs);
let params = {
exchange:exchange,
pairs:pairs
}
if (undefined !== interval)
{
// probably a callback
if ('function' == typeof reset)
{
cb = interval;
}
else
{
if ('string' !== typeof interval || '' == interval)
{
throw new Error("Argument 'interval' should be a non-empty string");
}
params.interval = interval;
}
}
this.execute('unsubscribeFromKlines', params, cb);
}
/**
* Unsubscribe from all klines we are currently subscribed to
*
* @param {string} exchange exchange identifier
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribeFromAllKlines(exchange, cb)
{
this._checkExchange(exchange);
let params = {
exchange:exchange
}
this.execute('unsubscribeFromAllKlines', params, cb);
}
///
/**
* Unsubscribe globally for a given exchange or all exchanges
*
* @param {string} exchange exchange identifier (optional, if not defined subscriptions will be cancelled for all exchanges)
* @param {function} cb callback to call upon receiving command result (optional)
*/
unsubscribe(exchange, cb)
{
let params = {};
if (undefined !== exchange)
{
if ('function' != typeof exchange)
{
this._checkExchange(exchange);
params.exchange = exchange;
}
else
{
cb = exchange;
}
}
this.execute('unsubscribe', params, cb);
}
/*
* Used to call any methods
*/
execute(command, params, cb)
{
let message = {
m:command
}
if (undefined !== params)
{
message.p = params;
}
if (undefined !== cb)
{
message.i = this._nextCommandId++;
this._callbacks[message.i] = cb;
}
this._send([message]);
}
/**
* Send a list of objects over WS
*
* @param {object} list list of data to send (each entry will be serialized to JSON and sent individually)
*/
_send(list)
{
// create if needed
if (null === this._connection)
{
this._queueMessages(list);
this._createConnection();
return;
}
if (!this._connection.isConnected())
{
this._queueMessages(list);
return;
}
// we didn't receive 'hello' message yet
if (null === this._readyTimestamp)
{
this._queueMessages(list);
return;
}
for (var i = 0; i < list.length; ++i)
{
if (debug.enabled)
{
debug(`Sending message : ${JSON.stringify(list[i])}`);
}
this._connection.send(JSON.stringify(list[i]));
}
}
/**
* Adds a list of object to the queue
*/
_queueMessages(list)
{
for (var i = 0; i < list.length; ++i)
{
if (debug.enabled)
{
debug(`Queuing message : ${JSON.stringify(list[i])}`);
}
this._queue.push(list[i]);
}
}
/**
* Sends each message from queue
*/
_processQueue()
{
if (0 == this._queue.length)
{
return;
}
// disconnection probably requested by client
if (null === this._connection)
{
return;
}
for (var i = 0; i < this._queue.length; ++i)
{
if (debug.enabled)
{
debug(`Sending message from queue : ${JSON.stringify(this._queue[i])}`);
}
this._connection.send(JSON.stringify(this._queue[i]));
}
this._queue = [];
}
_createConnection(delay)
{
// try to connect tickerMonitor socket if necessary
this._connectToTickerMonitor(false);
let self = this;
let counter = ++this._connectionCounter;
let connection = new WebSocketConnection(this._getUri(), this._connectionOptions);
/*
WS connection has been disconnected by gateway
*/
connection.on('disconnected', function(data){
if (debug.enabled)
{
debug("Connection #%d disconnected (will try to reconnect in %dms) : code = %d, reason = '%s'", counter, self._retryDelay, data.code, data.reason);
}
self.emit('disconnected', {connectionId:counter,code:data.code,reason:data.reason});
// reset session information
if (self._isEphemeral)
{
self._sessionId = null;
self._readyTimestamp = null;
}
self._createConnection.call(self, self._retryDelay);
});
/*
A connection error occured (connection retry will be automatic if possible)
*/
connection.on('connectionError', function(err){
// retry is possible
if (err.retry)
{
if (debug.enabled)
{
debug("Connection #%d failed (will try to reconnect in %dms) : attempts = %d, error = '%s'", counter, self._retryDelay, err.attempts, JSON.stringify(err.error));
}
self.emit('connectionError', {connectionId:counter,attempts:err.attempts,error:err.error});
return;
}
// no more retry
if (debug.enabled)
{
debug("Connection #%d failed (no more retry left) : attempts = %d, error = '%s'", counter, err.attempts, JSON.stringify(err.error));
}
self.emit('terminated', {connectionId:counter,attempts:err.attempts,error:err.error});
});
/*
* WS is ready to receive messages
*/
connection.on('connected', function(){
if (debug.enabled)
{
debug("Connection #%d connected", counter);
}
self._connectedTimestamp = new Date().getTime();
self.emit('connected', {connectionId:counter});
});
connection.on('message', function(message){
self._processMessage.call(self, message);
});
self._connection = connection;
try
{
// connect immediately
if (undefined === delay)
{
connection.connect();
}
else
{
setTimeout(function(){
// disconnection probably requested by client
if (null === self._connection)
{
return;
}
connection.connect();
}, delay);
}
}
catch (e)
{
throw e;
}
}
_connectToTickerMonitor(force, delay)
{
if (!this._tickerMonitor.enabled)
{
return;
}
if (null !== this._tickerMonitor.connection && !force)
{
return;
}
let self = this;
let connection = new WebSocketConnection(this._tickerMonitor.uri);
/*
WS connection has been disconnected by gateway
*/
connection.on('disconnected', function(data){
if (debug.enabled)
{
debug("TickerMonitor connection disconnected (will try to reconnect in %dms) : code = %d, reason = '%s'", self._retryDelay, data.code, data.reason);
}
// if we got code 4404, it means that tickerMonitor service is not enabled (don't try to reconnect)
if (4404 == data.code)
{
return;
}
self._connectToTickerMonitor.call(self, true, self._retryDelay);
});
/*
A connection error occured (connection retry will be automatic if possible)
*/
connection.on('connectionError', function(err){
// retry should always be possible
if (debug.enabled)
{
debug("TickerMonitor connection failed (will try to reconnect in %dms) : attempts = %d, error = '%s'", self._retryDelay, err.attempts, JSON.stringify(err.error));
}
});
/*
* WS is ready to receive messages
*/
connection.on('connected', function(){
if (debug.enabled)
{
debug("TickerMonitor connection connected");
}
});
connection.on('message', function(message){
self._processTickerMonitorMessage.call(self, message);
});
self._tickerMonitor.connection = connection;
try
{
// connect immediately
if (undefined === delay)
{
connection.connect();
}
else
{
setTimeout(function(){
// disconnection probably requested by client
if (null === self._tickerMonitor.connection)
{
return;
}
connection.connect();
}, delay);
}
}
catch (e)
{
throw e;
}
}
/*
* Can be called to disconnect. Client won't reconnect automatically unless methods (connect,execute) are called again
*/
disconnect()
{
if (null === this._connection && null === this._tickerMonitor.connection)
{
return;
}
// close RPC connection
if (null !== this._connection)
{
if (debug.enabled)
{
debug("Client will be disconnected (%d connections have been made)", this._connectionCounter);
}
let connection = this._connection;
this._connection = null;
connection.disconnect();
}
// close tickerMonitor connection
if (null !== this._tickerMonitor.connection)
{
if (debug.enabled)
{
debug("Ticker Monitor socket will be disconnected");
}
let connection = this._tickerMonitor.connection;
this._tickerMonitor.connection = null;
connection.disconnect();
}
}
/**
* Where we handle messages received from Ticker Monitor stream
*/
_processTickerMonitorMessage(message)
{
try
{
let data = JSON.parse(message);
if (debug.enabled)
{
let obj = {id:data.d.id, name:data.d.name,status:data.d.status.value,timestamp:data.d.status.timestamp};
debug(`Received TickerMonitor message : ${JSON.stringify(obj)}`);
}
this.emit(data.n, data.d);
}
// ignore non json messages
catch (e)
{
if (debug.enabled)
{
debug(`Received invalid JSON message : ${message}`);
}
return;
}
}
/**
* Where we handle all messages received from exchange
*/
_processMessage(message)
{
let data;
try
{
data = JSON.parse(message);
}
// ignore non json messages
catch (e)
{
if (debug.enabled)
{
debug(`Received invalid JSON message : ${message}`);
}
return;
}
// process hello message
if (undefined !== data.hello)
{
if (debug.enabled)
{
debug(`Received 'hello' message : sid = '${data.hello.sid}'`);
}
this._sessionId = data.hello.sid;
if (null === this._readyTimestamp || data.hello.isNew)
{
this._readyTimestamp = new Date().getTime();
this.emit('ready', {sessionId:data.hello.sid,isNew:data.hello.isNew});
}
this._processQueue();
return;
}
if (debug.enabled)
{
let obj = {i:data.i,e:data.e,n:data.n};
debug(`Received message : ${JSON.stringify(obj)}`);
}
if (null === this._readyTimestamp)
{
if (debug.enabled)
{
let obj = {i:data.i,e:data.e,n:data.n};
debug(`Ignoring message since we didn't receive 'hello' notification yet : ${JSON.stringify(obj)}`);
}
return;
}
if (undefined !== data.n)
{
this._processNotificationMessage(data);
}
else if (undefined !== data.r)
{
this._processResultMessage(data);
}
else if (undefined !== data.e)
{
this._processErrorMessage(data);
}
else
{
if (debug.enabled)
{
debug(`Received unsupported message : ${message}`);
}
}
}
_processNotificationMessage(data)
{
if (!this._globalListener)
{
this.emit(data.n, data.d);
return;
}
data.d.notification = data.n;
this.emit('notification', data.d);
}
_processResultMessage(data)
{
// unlikely to happen
if (undefined === this._callbacks[data.i])
{
return;
}
let cb = this._callbacks[data.i];
delete this._callbacks[data.i];
try
{
cb(data.r, null);
}
catch (e)
{
// we're not responsible for this, just retrow the error
throw e;
}
}
_processErrorMessage(data)
{
// unlikely to happen
if (undefined === this._callbacks[data.i])
{
return;
}
let cb = this._callbacks[data.i];
delete this._callbacks[data.i];
try
{
cb(null, data.e);
}
catch (e)
{
// we're not responsible for this, just retrow the error
throw e;
}
}
}
module.exports = Client;