/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { Watcher } from "./Watcher"; import { Market } from "./../interface/Market"; import { WssClient } from "../base/WssClient"; import { EventEmitter } from "events"; import { IClient } from "../interface/IClient"; export type MarketMap = Map; export type WssFactoryFn = (path: string) => WssClient; export type SendFn = (remoteId: string, market: Market) => void; 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: WssClient; protected _watcher: Watcher; constructor( readonly wssUrl: string, readonly name: string, wssFactory?: WssFactoryFn, watcher_ms?: 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, watcher_ms); this.hasTickers = false; this.hasTrades = true; this.hasCandles = false; this.hasLevel2Snapshots = false; this.hasLevel2Updates = false; this.hasLevel3Snapshots = false; this.hasLevel3Updates = false; this._wssFactory = wssFactory || (url => new WssClient(url)); } 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 { 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 { 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 { 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 { 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 { 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 { // 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 { 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): boolean { 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): void { 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(): void { if (!this._wss) { this._wss = this._wssFactory(this.wssUrl); 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: Error): void { this.emit("error", err); } /** * Handles the connecting event. This is fired any time the * underlying websocket begins a connection. */ protected _onConnecting(): void { 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(): void { 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(): void { this._watcher.stop(); this.emit("disconnected"); } /** * Handles the closing event */ protected _onClosing(): void { this._watcher.stop(); this.emit("closing"); } /** * Fires before connect */ protected _beforeConnect(): void { // } /** * Fires before close */ protected _beforeClose(): void { // } /** * Handles the closed event */ protected _onClosed(): void { 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); }