Skip to content

Commit dcfbace

Browse files
authored
Merge pull request #496 from f3rno/master
v2.0.9
2 parents 148c2b6 + 7400560 commit dcfbace

File tree

6 files changed

+142
-13
lines changed

6 files changed

+142
-13
lines changed

CHANGELOG

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1+
2.0.9
2+
3+
- WS2Manager: add managedUnsubscribe()
4+
- WS2Manager: add close()
5+
- WS2Manager: add getAuthenticatedSocket()
6+
- WSv2: add suppport for liquidations feed (status methods)
7+
- WSv2: add reconnect throttler in case of connection reset w/ many open sockets
8+
9+
2.0.8
10+
11+
- Bump dependency versions
12+
113
2.0.7
214

315
- WSv2: increase data chan limit to 30 (732499b)

examples/ws2/liquidations.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict'
2+
3+
process.env.DEBUG = '*'
4+
5+
const debug = require('debug')('bfx:examples:liquidations')
6+
const bfx = require('../bfx')
7+
const ws = bfx.ws(2, { transform: true })
8+
9+
ws.on('open', () => {
10+
debug('open')
11+
ws.subscribeStatus('liq:global')
12+
})
13+
14+
ws.onStatus({ key: 'liq:global' }, (data) => {
15+
console.log(data)
16+
})
17+
18+
ws.open()

lib/transports/ws2.js

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class WSv2 extends EventEmitter {
7171
* @param {boolean} opts.seqAudit - enable sequence numbers & verification
7272
* @param {boolean} opts.autoReconnect - if true, we will reconnect on close
7373
* @param {number} opts.reconnectDelay - optional, defaults to 1000 (ms)
74+
* @param {PromiseThrottle} opts.reconnectThrottler - optional pt to limit reconnect freq
7475
* @param {number} opts.packetWDDelay - watch-dog forced reconnection delay
7576
*/
7677
constructor (opts = { apiKey: '', apiSecret: '', url: WS_URL }) {
@@ -88,6 +89,7 @@ class WSv2 extends EventEmitter {
8889
this._seqAudit = opts.seqAudit === true
8990
this._autoReconnect = opts.autoReconnect === true
9091
this._reconnectDelay = opts.reconnectDelay || 1000
92+
this._reconnectThrottler = opts.reconnectThrottler
9193
this._manageOrderBooks = opts.manageOrderBooks === true
9294
this._manageCandles = opts.manageCandles === true
9395
this._packetWDDelay = opts.packetWDDelay
@@ -148,6 +150,11 @@ class WSv2 extends EventEmitter {
148150
return _includes(Object.keys(this._channelMap), chanId)
149151
}
150152

153+
hasSubscriptionRef (channel, identifier) {
154+
const key = `${channel}:${identifier}`
155+
return !!Object.keys(this._subscriptionRefs).find(ref => ref === key)
156+
}
157+
151158
getDataChannelId (type, filter) {
152159
return Object
153160
.keys(this._channelMap)
@@ -468,10 +475,19 @@ class WSv2 extends EventEmitter {
468475
// _autoReconnect = true - if the user likes to reconnect automatically
469476
if (this._isReconnecting || (this._autoReconnect && !this._isClosing)) {
470477
this._prevChannelMap = this._channelMap
478+
471479
setTimeout(() => {
472-
this.reconnectAfterClose().catch((err) => {
473-
debug('error reconnectAfterClose: %s', err.stack)
474-
})
480+
if (this._reconnectThrottler) {
481+
this._reconnectThrottler
482+
.add(this.reconnectAfterClose.bind(this))
483+
.catch((err) => {
484+
debug('error reconnectAfterClose: %s', err.stack)
485+
})
486+
} else {
487+
this.reconnectAfterClose().catch((err) => {
488+
debug('error reconnectAfterClose: %s', err.stack)
489+
})
490+
}
475491
}, this._reconnectDelay)
476492
}
477493

@@ -485,7 +501,7 @@ class WSv2 extends EventEmitter {
485501
_onWSError (err) {
486502
this.emit('error', err)
487503

488-
debug('error: %j', err)
504+
debug('error: %s', err)
489505
}
490506

491507
/**
@@ -595,6 +611,8 @@ class WSv2 extends EventEmitter {
595611
return this._handleTickerMessage(msg, channelData)
596612
} else if (channelData.channel === 'candles') {
597613
return this._handleCandleMessage(msg, channelData)
614+
} else if (channelData.channel === 'status') {
615+
return this._handleStatusMessage(msg, channelData)
598616
} else if (channelData.channel === 'auth') {
599617
return this._handleAuthMessage(msg, channelData)
600618
} else {
@@ -812,6 +830,24 @@ class WSv2 extends EventEmitter {
812830
this.emit('candle', data, key)
813831
}
814832

833+
/**
834+
* Called for messages from a 'status' channel.
835+
*
836+
* @param {Array|Array[]} msg
837+
* @param {Object} chanData - entry from _channelMap
838+
* @private
839+
*/
840+
_handleStatusMessage (msg, chanData) {
841+
const { key } = chanData
842+
const data = getMessagePayload(msg)
843+
844+
const internalMessage = [chanData.chanId, 'status', data]
845+
internalMessage.filterOverride = [chanData.key]
846+
847+
this._propagateMessageToListeners(internalMessage, chanData, false)
848+
this.emit('status', data, key)
849+
}
850+
815851
/**
816852
* @param {string} symbol
817853
* @param {number[]|number[][]} data
@@ -942,7 +978,7 @@ class WSv2 extends EventEmitter {
942978
listeners.forEach(({ cb, modelClass }) => {
943979
const ModelClass = modelClass
944980

945-
if (!transform || data.length === 0) {
981+
if (!ModelClass || !transform || data.length === 0) {
946982
cb(data, chanData)
947983
} else if (Array.isArray(data[0])) {
948984
cb(data.map((entry) => {
@@ -1375,6 +1411,14 @@ class WSv2 extends EventEmitter {
13751411
return this.managedSubscribe('candles', key, { key })
13761412
}
13771413

1414+
/**
1415+
* @param {string} key - 'liq:global'
1416+
* @return {boolean} subscribed
1417+
*/
1418+
subscribeStatus (key) {
1419+
return this.managedSubscribe('status', key, { key })
1420+
}
1421+
13781422
/**
13791423
* @param {number} chanId
13801424
*/
@@ -1420,6 +1464,14 @@ class WSv2 extends EventEmitter {
14201464
return this.managedUnsubscribe('candles', `trade:${frame}:${symbol}`)
14211465
}
14221466

1467+
/**
1468+
* @param {string} key
1469+
* @return {boolean} unsubscribed
1470+
*/
1471+
unsubscribeStatus (key) {
1472+
return this.managedUnsubscribe('status', key)
1473+
}
1474+
14231475
/**
14241476
* @param {string} cbGID
14251477
*/
@@ -1836,6 +1888,10 @@ class WSv2 extends EventEmitter {
18361888
this._registerListener('ticker', { 0: symbol }, m, cbGID, cb)
18371889
}
18381890

1891+
onStatus ({ key = '', cbGID } = {}, cb) {
1892+
this._registerListener('status', { 0: key }, null, cbGID, cb)
1893+
}
1894+
18391895
/**
18401896
* @param {Object} opts
18411897
* @param {string} opts.symbol

lib/ws2_manager.js

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@ const debug = require('debug')('bfx:ws2:manager')
55
const _isEqual = require('lodash/isEqual')
66
const _includes = require('lodash/includes')
77
const _pick = require('lodash/pick')
8+
const PromiseThrottle = require('promise-throttle')
89
const WSv2 = require('./transports/ws2')
910

1011
const DATA_CHANNEL_LIMIT = 30
12+
const reconnectThrottler = new PromiseThrottle({
13+
requestsPerSecond: 10 / 60.0,
14+
promiseImplementation: Promise
15+
})
1116

1217
/**
1318
* Provides a wrapper around the WSv2 class, opening new sockets when a
@@ -27,9 +32,20 @@ module.exports = class WS2Manager extends EventEmitter {
2732
super()
2833

2934
this.setMaxListeners(1000)
30-
this._socketArgs = socketArgs || {}
35+
3136
this._authArgs = authArgs
3237
this._sockets = []
38+
this._socketArgs = {
39+
...(socketArgs || {}),
40+
reconnectThrottler
41+
}
42+
}
43+
44+
/**
45+
* Closes all open sockets
46+
*/
47+
close () {
48+
this._sockets.forEach(socket => socket.ws.close())
3349
}
3450

3551
/**
@@ -144,7 +160,7 @@ module.exports = class WS2Manager extends EventEmitter {
144160

145161
const { chanId } = msg
146162
const i = wsState.pendingUnsubscriptions.findIndex(cid => (
147-
cid === chanId
163+
cid === `${chanId}`
148164
))
149165

150166
if (i === -1) {
@@ -177,6 +193,10 @@ module.exports = class WS2Manager extends EventEmitter {
177193
return wsState
178194
}
179195

196+
getAuthenticatedSocket () {
197+
return this._sockets.find(s => s.ws.isAuthenticated())
198+
}
199+
180200
/**
181201
* Returns the first socket that has less active/pending channels than the
182202
* DATA_CHANNEL_LIMIT
@@ -233,6 +253,15 @@ module.exports = class WS2Manager extends EventEmitter {
233253
})
234254
}
235255

256+
/**
257+
* @param {string} channel
258+
* @param {string} identifier
259+
* @return {Object} wsState - undefined if not found
260+
*/
261+
getSocketWithSubRef (channel, identifier) {
262+
return this._sockets.find(s => s.ws.hasSubscriptionRef(channel, identifier))
263+
}
264+
236265
/**
237266
* Calls the provided cb with all internal socket instances
238267
*
@@ -254,14 +283,14 @@ module.exports = class WS2Manager extends EventEmitter {
254283
*/
255284
subscribe (type, ident, filter) {
256285
let s = this.getFreeDataSocket()
257-
const doSub = () => {
258-
s.ws.managedSubscribe(type, ident, filter)
259-
}
260-
261286
if (!s) {
262287
s = this.openSocket()
263288
}
264289

290+
const doSub = () => {
291+
s.ws.managedSubscribe(type, ident, filter)
292+
}
293+
265294
if (!s.ws.isOpen()) {
266295
s.ws.once('open', doSub)
267296
} else {
@@ -271,6 +300,19 @@ module.exports = class WS2Manager extends EventEmitter {
271300
s.pendingSubscriptions.push([type, filter])
272301
}
273302

303+
managedUnsubscribe (channel, identifier) {
304+
const s = this.getSocketWithSubRef(channel, identifier)
305+
306+
if (!s) {
307+
debug('cannot unsub from unknown channel %s: %s', channel, identifier)
308+
return
309+
}
310+
311+
const chanId = s.ws._chanIdByIdentifier(channel, identifier)
312+
s.ws.managedUnsubscribe(channel, identifier)
313+
s.pendingUnsubscriptions.push(chanId)
314+
}
315+
274316
/**
275317
* Unsubscribes the first socket w/ the specified channel. Does nothing if no
276318
* such socket is found.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bitfinex-api-node",
3-
"version": "2.0.8",
3+
"version": "2.0.9",
44
"description": "Node reference library for Bitfinex API",
55
"engines": {
66
"node": ">=7"
@@ -79,6 +79,7 @@
7979
"lodash": "^4.17.4",
8080
"lodash.throttle": "^4.1.1",
8181
"p-iteration": "^1.1.8",
82+
"promise-throttle": "^1.0.1",
8283
"request": "^2.67.0",
8384
"request-promise": "^4.2.0",
8485
"ws": "^3.0.0"

test/lib/ws2_manager.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('WS2Manager', () => {
9797
const m = new WS2Manager()
9898
const s = m.openSocket()
9999

100-
s.pendingUnsubscriptions.push(42)
100+
s.pendingUnsubscriptions.push(`${42}`)
101101
s.ws.emit('unsubscribed', { chanId: 42 })
102102

103103
assert.strictEqual(s.pendingUnsubscriptions.length, 0)

0 commit comments

Comments
 (0)