UNPKG

bitmex-realtime-api

Version:

A library for interacting with BitMEX's websocket API.

143 lines (117 loc) 4.68 kB
const _ = require('lodash'); const debug = require('debug')('BitMEX:realtime-api:socket'); const debugEmit = require('debug')('BitMEX:realtime-api:socket:emit'); const signMessage = require('./signMessage'); const WebSocketClient = require('./ReconnectingSocket'); module.exports = function createSocket(options, bmexClient) { const endpoint = makeEndpoint(options); debug('connecting to %s', endpoint); // Create client and bind listeners. const wsClient = new WebSocketClient(); function onReconnect() { wsClient.url = makeEndpoint(options); debug('Reconnecting to BitMEX at ', wsClient.url); } wsClient.onopen = function() { wsClient.opened = true; debug('Connection to BitMEX at', wsClient.url, 'opened.'); bmexClient.emit('open'); // Have to regenerate endpoint on reconnection so we have a new nonce. wsClient.addListener('reconnect', onReconnect); }; wsClient.onclose = function() { wsClient.opened = false; debug('Connection to BitMEX at', wsClient.url, 'closed.'); bmexClient.emit('close'); }; wsClient.onmessage = function(input, isBinary) { let data = isBinary ? input : input.toString(); try { if (data === "pong") { return; } data = JSON.parse(data); debug('Received %j', data); } catch(e) { bmexClient.emit('error', 'Unable to parse incoming data:', data); return; } if (data.error) return bmexClient.emit('error', data.error); if (!data.data) return; // connection or subscription notice const tableNoSymbol = _.includes(bmexClient.constructor.noSymbolTables, data.table); if (tableNoSymbol) { // For no-symbol tables, emit a '*' event. emitFullData(bmexClient, data); } else { // Emit all data on partials for full tables. Thie ensures all downstream listeners // know the table is fully initialized. Split symbol data will emit too. if (data.action === 'partial' && !(data.filter && data.filter.symbol)) { emitFullData(bmexClient, data); } // Fires events as <table>:<action>:<symbol>, such as // instrument:update:XBTUSD. // Consumers may be listening on: // * action filter (e.g. `instrument:partial:*`) // * symbol filter (e.g. `instrument:*:XBTUSD`) // * table filter (e.g. `instrument:*:*`) emitSplitData(bmexClient, data); } }; wsClient.onerror = function(e) { const listeners = bmexClient.listeners('error'); // If no error listeners are attached, throw. if (!listeners.length) throw e; else bmexClient.emit('error', e); }; wsClient.onend = function(code) { const listeners = bmexClient.listeners('end'); // If no end listeners are attached, throw. if (!listeners.length) throw new Error('WebSocket closed. Please check errors above.'); else bmexClient.emit('end', code); // Cleanup wsClient.removeListener('reconnect', onReconnect); }; wsClient.open(endpoint); return wsClient; }; // Emits data split by symbol. function emitSplitData(emitter, data) { const {table, action} = data; // Technically, we can change this (e.g. chat channels) const filterKey = data.filterKey || 'symbol'; // Generate data by symbol const symbolData = data.data.reduce((accumulator, currentValue) => { if (currentValue[filterKey] in accumulator) { accumulator[currentValue[filterKey]].push(currentValue); } else { accumulator[currentValue[filterKey]] = [currentValue]; } return accumulator; }, {}); // If an empty partial is emitted, it will look like the following: // {"table":"order","action":"partial",..."filter":{"symbol":"XBTZ19"}} // This allows us to know which filtered partial is empty. // // We need to emit it so upstream knows we received this, it was just an empty list. if (data.action === 'partial' && data.filter && data.filter.symbol) { symbolData[data.filter.symbol] = symbolData[data.filter.symbol] || []; } Object.keys(symbolData).forEach((symbol) => { const key = `${table}:${action}:${symbol}`; debugEmit('emitting %s with data %j', key, symbolData[symbol]); emitter.emit(key, _.extend({}, data, {data: symbolData[symbol]})); }); } function emitFullData(emitter, data) { const {table, action} = data; const key = `${table}:${action}:*`; debugEmit('emitting %s with data %j', key, data.data); emitter.emit(key, data); } function makeEndpoint(options) { let endpoint = options.endpoint; if (options.apiKeyID && options.apiKeySecret) { endpoint += '?' + signMessage.getWSAuthQuery(options.apiKeyID, options.apiKeySecret); } return endpoint; }