ccxws
Version:
Websocket client for 37 cryptocurrency exchanges
368 lines (315 loc) • 11.7 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import { EventEmitter } from "events";
import { IClient } from "./IClient";
import { SmartWss } from "./SmartWss";
import { Watcher } from "./Watcher";
import { Market } from "./Market";
export type MarketMap = Map<string, Market>;
export type WssFactoryFn = (path: string) => SmartWss;
export type SendFn = (remoteId: string, market: Market) => void;
/**
* Single websocket connection client with
* subscribe and unsubscribe methods. It is also an EventEmitter
* and broadcasts 'trade' events.
*
* Anytime the WSS client connects (such as a reconnect)
* it run the _onConnected method and will resubscribe.
*/
export abstract class BasicClient extends EventEmitter implements IClient {
public hasTickers: boolean;
public hasTrades: boolean;
public hasCandles: boolean;
public hasLevel2Snapshots: boolean;
public hasLevel2Updates: boolean;
public hasLevel3Snapshots: boolean;
public hasLevel3Updates: boolean;
protected _wssFactory: WssFactoryFn;
protected _tickerSubs: MarketMap;
protected _tradeSubs: MarketMap;
protected _candleSubs: MarketMap;
protected _level2SnapshotSubs: MarketMap;
protected _level2UpdateSubs: MarketMap;
protected _level3SnapshotSubs: MarketMap;
protected _level3UpdateSubs: MarketMap;
protected _wss: SmartWss;
protected _watcher: Watcher;
constructor(
readonly wssPath: string,
readonly name: string,
wssFactory?: WssFactoryFn,
watcherMs?: number,
) {
super();
this._tickerSubs = new Map();
this._tradeSubs = new Map();
this._candleSubs = new Map();
this._level2SnapshotSubs = new Map();
this._level2UpdateSubs = new Map();
this._level3SnapshotSubs = new Map();
this._level3UpdateSubs = new Map();
this._wss = undefined;
this._watcher = new Watcher(this, watcherMs);
this.hasTickers = false;
this.hasTrades = true;
this.hasCandles = false;
this.hasLevel2Snapshots = false;
this.hasLevel2Updates = false;
this.hasLevel3Snapshots = false;
this.hasLevel3Updates = false;
this._wssFactory = wssFactory || (path => new SmartWss(path));
}
//////////////////////////////////////////////
public close() {
if (this._beforeClose) {
this._beforeClose();
}
this._watcher.stop();
if (this._wss) {
this._wss.close();
this._wss = undefined;
}
}
public reconnect() {
this.emit("reconnecting");
if (this._wss) {
this._wss.once("closed", () => this._connect());
this.close();
} else {
this._connect();
}
}
public subscribeTicker(market: Market) {
if (!this.hasTickers) return;
return this._subscribe(market, this._tickerSubs, this._sendSubTicker.bind(this));
}
public unsubscribeTicker(market: Market): Promise<void> {
if (!this.hasTickers) return;
this._unsubscribe(market, this._tickerSubs, this._sendUnsubTicker.bind(this));
}
public subscribeCandles(market: Market) {
if (!this.hasCandles) return;
return this._subscribe(market, this._candleSubs, this._sendSubCandles.bind(this));
}
public unsubscribeCandles(market: Market): Promise<void> {
if (!this.hasCandles) return;
this._unsubscribe(market, this._candleSubs, this._sendUnsubCandles.bind(this));
}
public subscribeTrades(market: Market) {
if (!this.hasTrades) return;
return this._subscribe(market, this._tradeSubs, this._sendSubTrades.bind(this));
}
public unsubscribeTrades(market: Market): Promise<void> {
if (!this.hasTrades) return;
this._unsubscribe(market, this._tradeSubs, this._sendUnsubTrades.bind(this));
}
public subscribeLevel2Snapshots(market: Market) {
if (!this.hasLevel2Snapshots) return;
return this._subscribe(
market,
this._level2SnapshotSubs,
this._sendSubLevel2Snapshots.bind(this),
);
}
public unsubscribeLevel2Snapshots(market: Market): Promise<void> {
if (!this.hasLevel2Snapshots) return;
this._unsubscribe(
market,
this._level2SnapshotSubs,
this._sendUnsubLevel2Snapshots.bind(this),
);
}
public subscribeLevel2Updates(market: Market) {
if (!this.hasLevel2Updates) return;
return this._subscribe(
market,
this._level2UpdateSubs,
this._sendSubLevel2Updates.bind(this),
);
}
public unsubscribeLevel2Updates(market: Market): Promise<void> {
if (!this.hasLevel2Updates) return;
this._unsubscribe(market, this._level2UpdateSubs, this._sendUnsubLevel2Updates.bind(this));
}
public subscribeLevel3Snapshots(market: Market) {
if (!this.hasLevel3Snapshots) return;
return this._subscribe(
market,
this._level3SnapshotSubs,
this._sendSubLevel3Snapshots.bind(this),
);
}
public unsubscribeLevel3Snapshots(market: Market): Promise<void> {
throw new Error("Method not implemented.");
}
public subscribeLevel3Updates(market: Market) {
if (!this.hasLevel3Updates) return;
return this._subscribe(
market,
this._level3UpdateSubs,
this._sendSubLevel3Updates.bind(this),
);
}
public unsubscribeLevel3Updates(market: Market): Promise<void> {
if (!this.hasLevel3Updates) return;
this._unsubscribe(market, this._level3UpdateSubs, this._sendUnsubLevel3Updates.bind(this));
}
////////////////////////////////////////////
// PROTECTED
/**
* Helper function for performing a subscription operation
* where a subscription map is maintained and the message
* send operation is performed
* @param {Market} market
* @param {Map}} map
* @param {String} msg
* @param {Function} sendFn
* @returns {Boolean} returns true when a new subscription event occurs
*/
protected _subscribe(market: Market, map: MarketMap, sendFn: SendFn) {
this._connect();
const remote_id = market.id;
if (!map.has(remote_id)) {
map.set(remote_id, market);
// perform the subscription if we're connected
// and if not, then we'll reply on the _onConnected event
// to send the signal to our server!
if (this._wss && this._wss.isConnected) {
sendFn(remote_id, market);
}
return true;
}
return false;
}
/**
* Helper function for performing an unsubscription operation
* where a subscription map is maintained and the message
* send operation is performed
*/
protected _unsubscribe(market: Market, map: MarketMap, sendFn: SendFn) {
const remote_id = market.id;
if (map.has(remote_id)) {
map.delete(remote_id);
if (this._wss.isConnected) {
sendFn(remote_id, market);
}
}
}
/**
* Idempotent method for creating and initializing
* a long standing web socket client. This method
* is only called in the subscribe method. Multiple calls
* have no effect.
*/
protected _connect() {
if (!this._wss) {
this._wss = this._wssFactory(this.wssPath);
this._wss.on("error", this._onError.bind(this));
this._wss.on("connecting", this._onConnecting.bind(this));
this._wss.on("connected", this._onConnected.bind(this));
this._wss.on("disconnected", this._onDisconnected.bind(this));
this._wss.on("closing", this._onClosing.bind(this));
this._wss.on("closed", this._onClosed.bind(this));
this._wss.on("message", (msg: string) => {
try {
this._onMessage(msg);
} catch (ex) {
this._onError(ex);
}
});
this._beforeConnect();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this._wss.connect();
}
}
/**
* Handles the error event
* @param {Error} err
*/
protected _onError(err) {
this.emit("error", err);
}
/**
* Handles the connecting event. This is fired any time the
* underlying websocket begins a connection.
*/
protected _onConnecting() {
this.emit("connecting");
}
/**
* This method is fired anytime the socket is opened, whether
* the first time, or any subsequent reconnects. This allows
* the socket to immediate trigger resubscription to relevent
* feeds
*/
protected _onConnected() {
this.emit("connected");
for (const [marketSymbol, market] of this._tickerSubs) {
this._sendSubTicker(marketSymbol, market);
}
for (const [marketSymbol, market] of this._candleSubs) {
this._sendSubCandles(marketSymbol, market);
}
for (const [marketSymbol, market] of this._tradeSubs) {
this._sendSubTrades(marketSymbol, market);
}
for (const [marketSymbol, market] of this._level2SnapshotSubs) {
this._sendSubLevel2Snapshots(marketSymbol, market);
}
for (const [marketSymbol, market] of this._level2UpdateSubs) {
this._sendSubLevel2Updates(marketSymbol, market);
}
for (const [marketSymbol, market] of this._level3UpdateSubs) {
this._sendSubLevel3Updates(marketSymbol, market);
}
this._watcher.start();
}
/**
* Handles a disconnection event
*/
protected _onDisconnected() {
this._watcher.stop();
this.emit("disconnected");
}
/**
* Handles the closing event
*/
protected _onClosing() {
this._watcher.stop();
this.emit("closing");
}
/**
* Fires before connect
*/
protected _beforeConnect() {
//
}
/**
* Fires before close
*/
protected _beforeClose() {
//
}
/**
* Handles the closed event
*/
protected _onClosed() {
this.emit("closed");
}
////////////////////////////////////////////
// ABSTRACT
protected abstract _onMessage(msg: any);
protected abstract _sendSubTicker(remoteId: string, market: Market);
protected abstract _sendSubCandles(remoteId: string, market: Market);
protected abstract _sendUnsubCandles(remoteId: string, market: Market);
protected abstract _sendUnsubTicker(remoteId: string, market: Market);
protected abstract _sendSubTrades(remoteId: string, market: Market);
protected abstract _sendUnsubTrades(remoteId: string, market: Market);
protected abstract _sendSubLevel2Snapshots(remoteId: string, market: Market);
protected abstract _sendUnsubLevel2Snapshots(remoteId: string, market: Market);
protected abstract _sendSubLevel2Updates(remoteId: string, market: Market);
protected abstract _sendUnsubLevel2Updates(remoteId: string, market: Market);
protected abstract _sendSubLevel3Snapshots(remoteId: string, market: Market);
protected abstract _sendUnsubLevel3Snapshots(remoteId: string, market: Market);
protected abstract _sendSubLevel3Updates(remoteId: string, market: Market);
protected abstract _sendUnsubLevel3Updates(remoteId: string, market: Market);
}