UNPKG

okx-v5-ws

Version:

This is a non-official OKX V5 websocket SDK for nodejs.

205 lines (171 loc) 6.22 kB
import websocket, { IStringified } from 'websocket' const WebSocketClient = websocket.client import { sleep } from './util' import EventEmitter from 'events' /** * Handle WS connection level logic */ class WSConnector { #serverBaseUrl: string // connection state #connectState: 'closed' | 'connecting' | 'connected' | 'reconnecting' = 'closed' /* reconnect state data */ #reconnectionAttempts = 0 #reconnectionMaxAttempts = Infinity /* WS connection instances */ #connection: websocket.connection | null = null #client = new WebSocketClient() // after-connected handler #afterConnected: () => Promise<void> /* states & timers for handle ping pong */ #pingTimer: any = null #waitPongTimer: any = null #lastMessageReceiveTimestamp: number // events connector #eventEmitter = new EventEmitter() /** * constructor */ constructor({ serverBaseUrl, afterConnected }: { serverBaseUrl: string; afterConnected: () => Promise<void> }) { this.#serverBaseUrl = serverBaseUrl this.#afterConnected = afterConnected this.#lastMessageReceiveTimestamp = Date.now() this.#client.on('connectFailed', function (error) { console.error('Connect Error: ' + error.toString()) }) this.#client.on('connect', (connection) => { this.#connectState = 'connected' this.#connection = connection this.#reconnectionAttempts = 0 console.log('WebSocket Client Connected') connection.on('error', (error: Error) => { console.error('Connection Error: ' + error.toString()) this.#eventEmitter.emit('error', error) }) connection.on('close', (code: number, desc: string) => { this.#eventEmitter.emit('close', code, desc) // reset ping pong state clearInterval(this.#pingTimer) clearInterval(this.#waitPongTimer) console.log(`Connection Closed. code=${code}, desc=${desc}`) this.#connectState = 'closed' if (code !== 1000) { this.reconnect() } else { this.#eventEmitter.emit('closed', code, desc) } }) connection.on('message', (message) => { // reset ping pong state this.#lastMessageReceiveTimestamp = Date.now() clearTimeout(this.#pingTimer) clearTimeout(this.#waitPongTimer) this.#pingTimer = setTimeout(this.#ping, 15_000) if (message.type === 'utf8') { this.#eventEmitter.emit('message', message.utf8Data) } }) this.#eventEmitter.emit('connect') }) } get event() { return this.#eventEmitter } get connected() { return this.#connectState === 'connected' } get connection() { return this.#connection } /** * connect to server */ async connect(): Promise<boolean> { if (this.#connectState === 'connected') { return true } // connect if not already connecting if (this.#connectState !== 'connecting') { this.#client.connect(this.#serverBaseUrl) } this.#connectState = 'connecting' return new Promise((resolve, reject) => { const connected = () => { this.#client.removeListener('connectFailed', connectFailed) resolve(true) } const connectFailed = (error: Error) => { this.#client.removeListener('connect', connected) reject(error) } this.#client.once('connect', connected) this.#client.once('connectFailed', connectFailed) }) .then(this.#afterConnected) .then(() => true) } /** * do reconnect */ async reconnect(): Promise<void> { if (this.#connectState === 'connected' || this.#connectState === 'reconnecting') { return } this.#eventEmitter.emit('reconnect') this.#connectState = 'reconnecting' return new Promise((resolve, reject) => { this.#client.once('connect', () => { resolve() }) ;(async () => { while (this.#connectState === 'reconnecting' && this.#reconnectionAttempts < this.#reconnectionMaxAttempts) { await sleep(5000) this.#reconnectionAttempts++ await this.connect() } if (this.#connectState === 'reconnecting') { reject(new Error('Reconnet fail after all attempts')) } })() }) } /** * send message to server * * @param data * @returns */ async send(data: Buffer | IStringified): Promise<void> { console.debug('send: ', data) return new Promise((resolve, reject) => { this.#connection?.send(data, (err?: Error) => { if (err) { reject(err) } else { resolve() } }) }) } /** * close connection */ close = () => { this.#connection?.close(1000) } /** * send ping and wait pong or disconnect */ #ping = async () => { if (this.#connectState === 'connected') { clearTimeout(this.#waitPongTimer) await this.send('ping') this.#waitPongTimer = setTimeout(() => { if (this.#connectState === 'connected' && Date.now() - this.#lastMessageReceiveTimestamp >= 15_000) { this.#connection?.close(1001) } }, 15_000) } } } export { WSConnector }