From b2e699803b5880e2c54bb74a54f3a86c40360cd8 Mon Sep 17 00:00:00 2001 From: Tiago Siebler Date: Mon, 11 Mar 2024 16:05:58 +0000 Subject: [PATCH] feat(): working spot & futures heartbeats --- examples/ws-spot-public.ts | 16 ++++++----- src/WebsocketClient.ts | 55 +++++++++++++++++++++++++++++++++++--- src/lib/BaseWSClient.ts | 18 ++++++++++--- src/lib/requestUtils.ts | 8 ------ 4 files changed, 76 insertions(+), 21 deletions(-) diff --git a/examples/ws-spot-public.ts b/examples/ws-spot-public.ts index bf1f32f..8615e30 100644 --- a/examples/ws-spot-public.ts +++ b/examples/ws-spot-public.ts @@ -1,14 +1,8 @@ import { - DefaultLogger, - LogParams, WebsocketClient, // WsSpotOperation, } from '../src'; -DefaultLogger.silly = (...params: LogParams): void => { - console.log('silly', ...params); -}; - async function start() { const client = new WebsocketClient(); @@ -60,6 +54,12 @@ async function start() { }); try { + /** + * Use the client subscribe(topic, market) pattern to subscribe to any websocket topic. + * + * You can subscribe to topics one at a time: + */ + // Ticker Channel // client.subscribe('spot/ticker:BTC_USDT', 'spot'); @@ -75,7 +75,9 @@ async function start() { // Trade Channel // client.subscribe('spot/trade:BTC_USDT', 'spot'); - // Or have multiple topics in one array: + /** + * Or have multiple topics in one array, in a single request: + */ client.subscribe( [ 'spot/ticker:BTC_USDT', diff --git a/src/WebsocketClient.ts b/src/WebsocketClient.ts index cdafb6b..a4abfdb 100644 --- a/src/WebsocketClient.ts +++ b/src/WebsocketClient.ts @@ -96,6 +96,53 @@ export class WebsocketClient extends BaseWebsocketClient< * */ + protected sendPingEvent(wsKey: WsKey) { + switch (wsKey) { + case WS_KEY_MAP.spotPublicV1: + case WS_KEY_MAP.spotPrivateV1: { + this.tryWsSend(wsKey, 'ping'); + break; + } + case WS_KEY_MAP.futuresPublicV1: + case WS_KEY_MAP.futuresPrivateV1: { + this.tryWsSend(wsKey, '{"action":"ping"}'); + break; + } + default: { + throw neverGuard(wsKey, `Unhandled ping format: "${wsKey}"`); + } + } + if ( + wsKey === WS_KEY_MAP.spotPrivateV1 || + wsKey === WS_KEY_MAP.spotPublicV1 + ) { + this.tryWsSend(wsKey, 'ping'); + return; + } + } + + protected isWsPong(msg: any): boolean { + // bitmart spot + if (msg?.data === 'pong') { + return true; + } + + // bitmart futures + // if (typeof event?.data === 'string') { + // return true; + // } + if ( + typeof msg?.event?.data === 'string' && + msg.event.data.startsWith('pong') + ) { + return true; + } + + // this.logger.info(`Not a pong: `, msg); + + return false; + } + protected resolveEmittableEvents(event: MessageEventLike): EmittableEvent[] { const results: EmittableEvent[] = []; @@ -103,9 +150,11 @@ export class WebsocketClient extends BaseWebsocketClient< const parsed = JSON.parse(event.data); const responseEvents = ['subscribe', 'unsubscribe']; - if (typeof parsed.event === 'string') { + + const eventAction = parsed.event || parsed.action; + if (typeof eventAction === 'string') { // These are request/reply pattern events (e.g. after subscribing to topics or authenticating) - if (responseEvents.includes(parsed.event)) { + if (responseEvents.includes(eventAction)) { results.push({ eventType: 'response', event: parsed, @@ -114,7 +163,7 @@ export class WebsocketClient extends BaseWebsocketClient< } this.logger.error( - `!! Unhandled string event type "${parsed.event}. Defaulting to "update" channel...`, + `!! Unhandled string event type "${eventAction}. Defaulting to "update" channel...`, parsed, ); } diff --git a/src/lib/BaseWSClient.ts b/src/lib/BaseWSClient.ts index 1c9f806..c44a4a5 100644 --- a/src/lib/BaseWSClient.ts +++ b/src/lib/BaseWSClient.ts @@ -7,7 +7,7 @@ import { } from '../types/websockets/client.js'; import { WS_LOGGER_CATEGORY } from '../WebsocketClient.js'; import { DefaultLogger } from './logger.js'; -import { isMessageEvent, isWsPong, MessageEventLike } from './requestUtils.js'; +import { isMessageEvent, MessageEventLike } from './requestUtils.js'; import { WsStore } from './websocket/WsStore.js'; import { WsConnectionStateEnum } from './websocket/WsStore.types.js'; @@ -91,6 +91,9 @@ export abstract class BaseWebsocketClient< isPrivate?: boolean, ): TWSKey; + protected abstract sendPingEvent(wsKey: TWSKey, ws: WebSocket): void; + protected abstract isWsPong(data: any): boolean; + protected abstract getWsAuthSignature(): Promise<{ expiresAt: number; signature: string; @@ -448,7 +451,7 @@ export abstract class BaseWebsocketClient< this.clearPongTimer(wsKey); this.logger.silly('Sending ping', { ...WS_LOGGER_CATEGORY, wsKey }); - this.tryWsSend(wsKey, 'ping'); + this.sendPingEvent(wsKey, this.wsStore.get(wsKey, true).ws); this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => { this.logger.info('Pong timeout - closing socket to reconnect', { @@ -651,7 +654,7 @@ export abstract class BaseWebsocketClient< // any message can clear the pong timer - wouldn't get a message if the ws wasn't working this.clearPongTimer(wsKey); - if (isWsPong(event)) { + if (this.isWsPong(event)) { this.logger.silly('Received pong', { ...WS_LOGGER_CATEGORY, wsKey }); return; } @@ -679,6 +682,15 @@ export abstract class BaseWebsocketClient< } for (const emittable of emittableEvents) { + if (this.isWsPong(emittable)) { + this.logger.silly('Received pong', { + ...WS_LOGGER_CATEGORY, + wsKey, + data, + }); + continue; + } + this.emit(emittable.eventType, { ...emittable.event, wsKey }); } diff --git a/src/lib/requestUtils.ts b/src/lib/requestUtils.ts index 8ceedde..0cb68f4 100644 --- a/src/lib/requestUtils.ts +++ b/src/lib/requestUtils.ts @@ -86,14 +86,6 @@ export function getRestBaseUrl( export const APIID = 'bitmartapinode1'; -export function isWsPong(msg: any): boolean { - // bitmart - if (msg?.data === 'pong') { - return true; - } - return false; -} - export interface MessageEventLike { target: WebSocket; type: 'message';