cryptomarket
Version:
The CryptoMarket for Node.js
154 lines (140 loc) • 4.98 kB
text/typescript
import { EventEmitter } from "events";
import WebSocket from "ws";
import { ResponsePromiseFactory } from "./ResponsePromiseFactory";
import { WSResponse } from "./WSResponse";
import { fromCamelCaseToSnakeCase } from "../paramStyleConverter";
export class WSClientBase {
private uri: string;
private subscriptionKeys: { [x: string]: any };
private multiResponsePromiseFactory: ResponsePromiseFactory;
private nextId: number;
private pingTimeout: NodeJS.Timeout;
private requestTimeoutMs?: number
protected ws: WebSocket;
protected emitter: EventEmitter;
constructor(uri: string, subscriptionKeys: { [x: string]: any } = {}, requestTimeoutMs?: number) {
this.uri = uri;
this.requestTimeoutMs = requestTimeoutMs;
this.subscriptionKeys = subscriptionKeys;
this.emitter = new EventEmitter();
this.nextId = 1;
this.ws = new WebSocket(null);
this.pingTimeout = setTimeout(() => { });
this.multiResponsePromiseFactory = new ResponsePromiseFactory(this.emitter);
}
protected getNextId(): number {
if (this.nextId < 1) this.nextId = 1;
const next = this.nextId;
this.nextId++;
return next;
}
protected heartbeat() {
clearTimeout(this.pingTimeout);
this.pingTimeout = setTimeout(() => {
this.ws.terminate();
}, 30_000 + 1_000);
}
/**
* Connects the client with the server
*
* Initializes the internal websocket, this method does not authenticate the client so it does not permit user related calls
*
* @returns A prommise that resolves after the connection
*/
public async connect(): Promise<void> {
return new Promise<void>((resolve) => {
if (this.ws.readyState === this.ws.OPEN) this.ws.close();
this.ws = new WebSocket(this.uri)
.on("close", () => {
clearTimeout(this.pingTimeout);
})
.on("ping", () => {
this.heartbeat();
})
.on("message", (msg: any) => {
this.handle({ msgJson: msg });
})
.once("open", () => {
this.heartbeat;
resolve();
});
});
}
/**
* Close the client connection with the server
*
* @returns A promise that resolve after the closure
*/
public close(): Promise<void> {
const promise = new Promise<void>((resolve) => {
this.ws.once("close", () => {
resolve();
});
});
this.ws.close();
return promise;
}
protected sendSubscription({
method,
callback,
params = {},
}: {
method: string;
callback: (...args: any[]) => void;
params?: {};
}): Promise<unknown> {
const { key } = this.subscriptionKeys[method];
this.emitter.on(key, callback);
return this.request({ method, params });
}
protected async sendUnsubscription({ method, params = {}, }: { method: any; params?: {}; }): Promise<Boolean> {
const { key } = this.subscriptionKeys[method]
this.emitter.removeAllListeners(key)
const response = await this.request({ method, params });
return response as Boolean;
}
protected request({ method, params: paramsRaw = {} }: { method: any; params?: {}; }): Promise<unknown> {
const params = fromCamelCaseToSnakeCase(paramsRaw)
const { id, promise } = this.multiResponsePromiseFactory.newOneResponsePromise();
const payload = { method, params, id };
this.ws.send(JSON.stringify(payload));
return withTimeout(this.requestTimeoutMs, promise);
}
protected requestList({ method, params: paramsRaw = {}, responseCount = 1 }: { method: any; params?: {}; responseCount?: number; }): Promise<unknown> {
const params = fromCamelCaseToSnakeCase(paramsRaw)
const { id, promise } = this.multiResponsePromiseFactory.newMultiResponsePromise(responseCount);
const payload = { method, params, id };
this.ws.send(JSON.stringify(payload));
return withTimeout(this.requestTimeoutMs, promise);
}
protected handle({ msgJson }: { msgJson: string }): void {
const message = JSON.parse(msgJson);
if ("id" in message) {
this.handleResponse(message);
} else if ("method" in message) {
this.handleNotification(message);
}
}
protected handleNotification({ method, params }: { method: string; params: any; }): void {
const { key, type } = this.subscriptionKeys[method];
this.emitter.emit(key, params, type);
}
protected handleResponse(response: WSResponse): void {
const id = response["id"];
if (id === null) return;
this.emitter.emit(id, response);
}
}
const withTimeout = (millis: number | undefined, promise: any) => {
if (millis === undefined || millis === null) {
return promise
}
let timeout: NodeJS.Timeout;
const timeoutPromise = new Promise((resolve, reject) => timeout = setTimeout(() => reject(new Error(`Timed out after ${millis} ms.`)), millis))
return Promise.race([promise, timeoutPromise])
.finally(() => {
if (timeout) {
clearTimeout(timeout)
}
})
};