bitmex-realtime-api
Version:
A library for interacting with BitMEX's websocket API.
198 lines (172 loc) • 7.14 kB
JavaScript
;
/*eslint no-unused-vars:0*/
const _ = require('lodash');
const debug = require('debug')('BitMEX:realtime-api:deltaParser');
/**
* A mixin that binds a store's data to a websocket.
* Accepts multiple socket descriptors.
* @param {Object|Array} socketDescriptors Description of a socket connection.
*/
module.exports = {
/**
* Called when data comes in via the websocket.
*
* Returns new data. We ensure that the object identity check will fail by constructing a new array
* or object whenever data updates.
*
* We handle four different kinds of events:
*
* 'partial': Replace the store entirely.
* 'update': Iterate through items sent. Find the items that match those items (via keys)
* and replace them with new items. Do not merge properties; create new objects
* for faster shouldComponentUpdate.
* 'insert': Insert a new item to the front or to the back.
* 'delete': Delete an item from the store.
*
* @param {String} action Action name.
* @param {String} tableName Table to update.
* @param {String} symbol Symbol we're updating (instrument).
* @param {Object} client Client store.
* @param {Array} data Array of new data.
* @return {Array} Updated data.
*/
onAction(action, tableName, symbol, client, data) {
// Deltas before the getSymbol() call returns can be safely discarded.
if (action !== 'partial' && !isInitialized(tableName, symbol, client)) return [];
// Partials initialize the table, so there's a different signature.
if (action === 'partial') return this._partial(tableName, symbol, client, data);
// Some tables don't have keys, like 'trade' and 'quote'. They are insert-only tables
// and you should never see updates or deletes on them.
const keys = client._keys[tableName];
if ((action === 'update' || action === 'delete') && keys.length === 0) {
throw new Error("The data in the store " + tableName + " is not keyed for " + action + "s. " +
"Please email support@bitmex.com if you see this.");
}
// This dispatches delete/insert/update.
return module.exports['_' + action](client._data[tableName], symbol, data.data, client._keys[tableName]);
},
_delete(context, key, data, keys) {
return removeFromStore.apply(null, arguments);
},
_insert(context, key, data, keys) {
return insertIntoStore.apply(null, arguments);
},
_partial(tableName, symbol, client, data) {
if (!client._data[tableName]) client._data[tableName] = {};
debug('partial: %s %s, data: %j', tableName, symbol, data);
const dataArr = data.data || [];
// Intitialize data.
client._data[tableName][symbol] = dataArr;
// Initialize keys.
client._keys[tableName] = data.keys;
// Return inserted data
return dataArr;
},
_update(context, key, data, keys) {
return updateStore.apply(null, arguments);
}
};
// Returns `true` if a partial has already been received, and therefore the data saubscription is live.
// We check if either a symbol partial has been received, or a '*' partial (all symbols)
function isInitialized(tableName, symbol, client) {
const out = Boolean(client._data[tableName] && (client._data[tableName][symbol] || client._data[tableName]['*']));
debug('isInitialized(%s, %s, client)?: %s', tableName, symbol, out);
return out;
}
/**
* Add items to a store.
* @param {Object} context Context.
* @param {String} key Key of store.
* @param {Array} newData Replacement data.
* @param {Array} keys Key names that uniquely identify a datum.
*/
function insertIntoStore(context, key, newData) {
const store = context[key] || [];
// Create a new working object.
const storeData = [...store, ...newData];
return replaceStore(context, key, storeData);
}
/**
* Update items in a store.
* @param {Object} context Context.
* @param {String} key Key of store.
* @param {Array} newData Replacement data.
* @param {Array} keys Key names that uniquely identify a datum.
*/
function updateStore(context, key, newData, keys) {
const store = context[key] || [];
// Create a new working object.
const storeData = [...store];
// Loop through data, updating items in `storeData` when necessary.
for (let i = 0; i < newData.length; i++) {
let newDatum = newData[i];
// Find the item we're updating, if it exists.
const criteria = _.pick(newDatum, keys);
const itemToUpdate = _.find(storeData, criteria);
// If the item exists, replace it with an updated item.
// This will actually replace the existing store with a new array
// containing a completely new updated object. A little more GC work
// but unique object references, for better shouldComponentUpdate.
if (itemToUpdate) {
newDatum = updateItem(itemToUpdate, newDatum);
storeData[storeData.indexOf(itemToUpdate)] = newDatum;
}
// This is bad - the item didn't exist and we're trying to update it.
// A lot of bad things can happen here since we basically have an incomplete
// data set. An insert should have come first, but we can't treat this as an
// insert because we'd end up with an item that has missing properties.
else {
throw new Error(`Update for missing item came through on ${key}. Data: ${JSON.stringify(newDatum)}`);
}
}
return replaceStore(context, key, storeData);
}
/**
* Removes items from a store.
* @param {Object} context Context.
* @param {String} key Key of store.
* @param {Array} newData Replacement data.
* @param {Array} keys Key names that uniquely identify a datum.
*/
function removeFromStore(context, key, newData, keys) {
const store = context[key] || [];
// Create a new working object.
let storeData = [...store];
// Loop through incoming data and remove items that match.
for (let i = 0; i < newData.length; i++) {
// Find the item to remove and remove it.
const criteria = _.pick(newData[i], keys);
const itemToRemove = _.find(storeData, criteria);
if (itemToRemove) {
storeData = _.without(storeData, itemToRemove);
}
}
return replaceStore(context, key, storeData);
}
/**
* Replaces an object at a path with a new object.
* @param {Object} obj Context.
* @param {String} key Key of store.
* @param {Array} newData New data.
* @return {Object} Context.
*/
function replaceStore(context, key, newData) {
// Store could be an array or singular object/model.
if (context[key] && !Array.isArray(context[key])) {
// Not an array - simply replace with the first item in our new array.
// This is for single object stores, like margin.
context[key] = newData[0];
} else {
context[key] = newData;
}
return context[key];
}
/**
* Update an item. Creates a new item, does not modify the old one.
* @param {Object|Model} item Item to update.
* @param {Object} newData Hash of new data.
* @return {Object|Model} A new item with new data.
*/
function updateItem(item, newData) {
return {...item, ...newData};
}