Merge pull request #444 from tiagosiebler/wsapiclient

feat(v4.1.8): introduce websocket API client wrapper, chore(): misc typo fixes & cleaning
This commit is contained in:
Tiago
2025-05-22 12:12:51 +01:00
committed by GitHub
15 changed files with 653 additions and 91 deletions

129
README.md
View File

@@ -1,4 +1,4 @@
# Node.js & JavaScript SDK for Bybit REST API & WebSockets # Node.js & JavaScript SDK for Bybit REST API, WebSocket API & WebSocket Events
[![Build & Test](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml/badge.svg?branch=master)](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml) [![Build & Test](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml/badge.svg?branch=master)](https://github.com/tiagosiebler/bybit-api/actions/workflows/e2etest.yml)
[![npm version](https://img.shields.io/npm/v/bybit-api)][1] [![npm version](https://img.shields.io/npm/v/bybit-api)][1]
@@ -19,7 +19,7 @@
[1]: https://www.npmjs.com/package/bybit-api [1]: https://www.npmjs.com/package/bybit-api
Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs and WebSockets: Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs, WebSocket APIs & WebSocket Events:
- Complete integration with all Bybit REST APIs & WebSockets, including the WebSocket API. - Complete integration with all Bybit REST APIs & WebSockets, including the WebSocket API.
- Actively maintained with a modern, promise-driven interface. - Actively maintained with a modern, promise-driven interface.
@@ -29,22 +29,22 @@ Professional Node.js, JavaScript & TypeScript SDK for the Bybit REST APIs and We
- Proxy support via axios integration. - Proxy support via axios integration.
- Robust WebSocket consumer integration with configurable heartbeats & automatic reconnect then resubscribe workflows. - Robust WebSocket consumer integration with configurable heartbeats & automatic reconnect then resubscribe workflows.
- Event driven messaging - Event driven messaging
- Smart websocket persistence - Smart WebSocket persistence
- Automatically handle silent websocket disconnections through timed heartbeats, including the scheduled 24hr disconnect. - Automatically handle silent websocket disconnections through timed heartbeats, including the scheduled 24hr disconnect.
- Automatically handle authentication. - Automatically handle authentication.
- Emit `reconnected` event when dropped connection is restored. - Emit `reconnected` event when dropped connection is restored.
- WebSocket API integration, with two design patterns to choose from: - WebSocket API integration, with two design patterns to choose from:
- Asynchronous promise-driven responses: 1. Asynchronous **promise**-driven responses:
- Make requests like a REST API, using the WebSocket API. No need to subscribe to asynchronous events. - Make requests like a REST API, using the WebSocket API. No need to subscribe to asynchronous events.
- Send commands with the await sendWSAPIRequest(...) method. - Import the `WebsocketAPIClient` and use it like the REST API client. Call functions and await responses.
- Await responses to commands directly in the fully typed sendWSAPIRequest() call. - See example for more details: [examples/ws-api-client.ts](./examples/ws-api-client.ts).
- The method directly returns a promise. Use a try/catch block for convenient error handling without the complexity of asynchronous WebSockets. - Prefer something more raw? Use the `sendWSAPIRequest(...)` method and await responses
- See example for more details: [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) - See example for more details: [examples/ws-api-raw-promises.ts](./examples/ws-api-raw-promises.ts)
- Asynchronous event-driven responses: 2. Asynchronous **event**-driven responses:
- Subscribe to `response` and `error` events from WebsocketClient's event emitter. - Subscribe to `response` and `error` events from WebsocketClient's event emitter.
- Send commands with the sendWSAPIRequest(...) method. - Send commands with the `sendWSAPIRequest(...)` method.
- Responses to commands will arrive via the `response` and `error` events. - Responses to commands will arrive via the `response` and `error` events emitted by the client.
- See example for more details: [examples/ws-api-events.ts](./examples/ws-api-events.ts) - See example for more details: [examples/ws-api-raw-events.ts](./examples/ws-api-raw-events.ts)
- Active community support & collaboration in telegram: [Node.js Algo Traders](https://t.me/nodetraders). - Active community support & collaboration in telegram: [Node.js Algo Traders](https://t.me/nodetraders).
# Table of Contents # Table of Contents
@@ -153,6 +153,7 @@ Here are the available REST clients and the corresponding API groups described i
| [ **V5 API** ] | The new unified V5 APIs (successor to previously fragmented APIs for all API groups). | | [ **V5 API** ] | The new unified V5 APIs (successor to previously fragmented APIs for all API groups). |
| [RestClientV5](src/rest-client-v5.ts) | Unified V5 all-in-one REST client for all [V5 REST APIs](https://bybit-exchange.github.io/docs/v5/intro) | | [RestClientV5](src/rest-client-v5.ts) | Unified V5 all-in-one REST client for all [V5 REST APIs](https://bybit-exchange.github.io/docs/v5/intro) |
| [WebsocketClient](src/websocket-client.ts) | All WebSocket features (Public & Private consumers for all API categories & the WebSocket API) | | [WebsocketClient](src/websocket-client.ts) | All WebSocket features (Public & Private consumers for all API categories & the WebSocket API) |
| [WebsocketAPIClient](src/websocket-api-client.ts) | Use the WebSocket API like a REST API. Call functions and await responses, powered by WebSockets. |
## REST API Usage ## REST API Usage
@@ -398,36 +399,41 @@ ws.on('reconnected', (data) => {
## Websocket API - Sending orders via WebSockets ## Websocket API - Sending orders via WebSockets
Bybit supports sending, amending and cancelling orders over a WebSocket connection. The [WebsocketClient](./src/WebsocketClient.ts) fully supports Bybit's WebSocket API via the `sendWSAPIRequest(...)` method. Bybit supports sending, amending and cancelling orders over a WebSocket connection. The [WebsocketClient](./src/WebsocketClient.ts) fully supports Bybit's WebSocket API via the `sendWSAPIRequest(...)` method. There is also a dedicated [WebsocketAPIClient](./src/websocket-api-client.ts), built over the WSClient's sendWSAPIRequest mechanism for a simpler experience.
Links for reference: Links for reference:
- [Bybit WebSocket API Documentation](https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline) - [Bybit WebSocket API Documentation](https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline)
- [WebSocket API Example Node.js/TypeScript/JavaScript](./examples/ws-api-promises.ts). - [WebsocketAPIClient example, use the Websocket API like a REST API](./examples/ws-api-client.ts)
- [Raw Asynchronous Websocket API Node.js/TypeScript/JavaScript example](./examples/ws-api-raw-promises.ts)
Note: as of January 2025, the demo trading environment does not support the WebSocket API. Note: as of January 2025, the demo trading environment does not support the WebSocket API.
There are two ways to use the WS API, depending on individual preference: There are two ways to use the WS API, depending on individual preference:
- event-driven: 1. event-driven:
- send requests via `client.sendWSAPIRequest(wsKey, operation, params)`, fire and forget - send requests via `client.sendWSAPIRequest(wsKey, operation, params)`, fire and forget
- handle async replies via event handlers on `client.on('exception', cb)` and `client.on('response', cb)` - handle async replies via event handlers on `client.on('exception', cb)` and `client.on('response', cb)`
- See example for more details: [examples/ws-api-events.ts](./examples/ws-api-events.ts) - See example for more details: [examples/ws-api-raw-events.ts](./examples/ws-api-raw-events.ts)
- promise-driven: 2. promise-driven:
- send requests via `const result = await client.sendWSAPIRequest(wsKey, operation, params)`, which returns a promise - import the `WebsocketAPIClient` and use it much like a REST API.
- await each call - make an instance & call the Websocket API with a function.
- use try/catch blocks to handle promise rejections - await responses, much like a REST API.
- See example for more details: [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) - use try/catch blocks to handle promise rejections
- See example for more details: [examples/ws-api-client.ts](./examples/ws-api-client.ts)
The below example demonstrates the promise-driven approach, which behaves similar to a REST API. The WebSocket API even accepts the same parameters as the corresponding REST API endpoints, so this approach should be compatible with existing REST implementations. Connectivity, authentication, and processing requests wrapped in promises - these are all handled automatically by the WebsocketClient without additional configuration. The below example demonstrates the promise-driven approach, which behaves similar to a REST API. The WebSocket API even accepts the same parameters as the corresponding REST API endpoints, so this approach should be compatible with existing REST implementations.
Connectivity, authentication and connecting requests & responses to promises - these are all handled automatically without additional configuration by the WebsocketClient. The WebsocketAPIClient is a wrapper built on top of this, providing dedicated methods for every available Websocket API command. Each method has fully typed requests & responses. Benefit from the capabilities of the WebSocket API without the complexity of managing asynchronous messaging over WebSockets.
```javascript ```javascript
const { WS_KEY_MAP, WebsocketClient } = require('bybit-api'); const { WS_KEY_MAP, WebsocketAPIClient } = require('bybit-api');
// or // or
// import { WS_KEY_MAP, WebsocketClient } from 'bybit-api'; // import { WS_KEY_MAP, WebsocketAPIClient } from 'bybit-api';
// Create an instance of the WebsocketClient. // Create an instance of the WebsocketAPIClient. This is built on
// This will automatically handle connectivity and authentication for you. // top of the WebsocketClient and will automatically handle WebSocket
const wsClient = new WebsocketClient( // persistence and authentication for you.
const wsClient = new WebsocketAPIClient(
{ {
key: 'yourApiKeyHere', key: 'yourApiKeyHere',
secret: 'yourApiSecretHere', secret: 'yourApiSecretHere',
@@ -440,6 +446,10 @@ const wsClient = new WebsocketClient(
// Note: As of Jan 2025, demo trading only supports consuming events, it does // Note: As of Jan 2025, demo trading only supports consuming events, it does
// NOT support the WS API. // NOT support the WS API.
// demoTrading: false, // demoTrading: false,
// If you want your own event handlers instead of the default ones with logs,
// disable this setting and see ws-api-client example for more details.
// attachEventListeners: false
} }
); );
@@ -452,32 +462,47 @@ async function main() {
* This is not necessary and will happen automatically when * This is not necessary and will happen automatically when
* sending a command, if you aren't connected/authenticated yet. * sending a command, if you aren't connected/authenticated yet.
*/ */
// await wsClient.connectWSAPI(); // await wsClient.getWSClient().connectWSAPI();
try { try {
console.log('Step 1: Create an order'); console.log('Step 1: Create an order');
const response = await wsClient.submitNewOrder({
// The type for `wsAPISubmitOrderResult` is automatically category: 'linear',
// resolved to `WSAPIResponse<OrderResultV5, "order.create">` symbol: 'BTCUSDT',
const wsAPISubmitOrderResult = await wsClient.sendWSAPIRequest( orderType: 'Limit',
WS_KEY_MAP.v5PrivateTrade, qty: '0.001',
'order.create', side: 'Buy',
{ price: '50000',
symbol: 'BTCUSDT', });
side: 'Buy', console.log('submitNewOrder response: ', response);
orderType: 'Limit',
price: '50000',
qty: '1',
category: 'linear',
},
);
console.log(
`Step 1: Order result (order ID: "${wsAPISubmitOrderResult.data.orderId}"): `,
wsAPISubmitOrderResult,
);
} catch (e) { } catch (e) {
console.error('Step 1: Order submit exception: ', e); console.log('submitNewOrder error: ', e);
}
try {
console.log('Step 2: Amend an order');
const response = await wsClient.amendOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
qty: '0.001',
price: '60000',
});
console.log('amendOrder response: ', response);
} catch (e) {
console.log('amendOrder error: ', e);
}
try {
console.log('Step 3: Cancel an order');
const response = await wsClient.cancelOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
});
console.log('cancelOrder response: ', response);
} catch (e) {
console.log('cancelOrder error: ', e);
} }
} }
@@ -486,8 +511,6 @@ main();
``` ```
See the [examples/ws-api-promises.ts](./examples/ws-api-promises.ts) example for a more detailed explanation.
--- ---
### Balancing load across multiple connections ### Balancing load across multiple connections

View File

@@ -20,6 +20,7 @@ All REST clients are in the [src](/src) folder. For usage examples, make sure to
List of clients: List of clients:
- [rest-client-v5](#rest-client-v5ts) - [rest-client-v5](#rest-client-v5ts)
- [websocket-api-client](#websocket-api-clientts)
If anything is missing or wrong, please open an issue or let us know in our [Node.js Traders](https://t.me/nodetraders) telegram group! If anything is missing or wrong, please open an issue or let us know in our [Node.js Traders](https://t.me/nodetraders) telegram group!
@@ -242,3 +243,18 @@ This table includes all endpoints from the official Exchange API docs and corres
| [getP2PUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3027) | :closed_lock_with_key: | POST | `/v5/p2p/user/personal/info` | | [getP2PUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3027) | :closed_lock_with_key: | POST | `/v5/p2p/user/personal/info` |
| [getP2PCounterpartyUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3034) | :closed_lock_with_key: | POST | `/v5/p2p/user/order/personal/info` | | [getP2PCounterpartyUserInfo()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3034) | :closed_lock_with_key: | POST | `/v5/p2p/user/order/personal/info` |
| [getP2PUserPayments()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3043) | :closed_lock_with_key: | POST | `/v5/p2p/user/payment/list` | | [getP2PUserPayments()](https://github.com/tiagosiebler/bybit-api/blob/master/src/rest-client-v5.ts#L3043) | :closed_lock_with_key: | POST | `/v5/p2p/user/payment/list` |
# websocket-api-client.ts
This table includes all endpoints from the official Exchange API docs and corresponding SDK functions for each endpoint that are found in [websocket-api-client.ts](/src/websocket-api-client.ts).
This client provides WebSocket API endpoints which allow for faster interactions with the Bybit API via a WebSocket connection.
| Function | AUTH | HTTP Method | Endpoint |
| -------- | :------: | :------: | -------- |
| [submitNewOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L95) | :closed_lock_with_key: | WS | `order.create` |
| [amendOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L111) | :closed_lock_with_key: | WS | `order.amend` |
| [cancelOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L127) | :closed_lock_with_key: | WS | `order.cancel` |
| [batchSubmitOrders()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L143) | :closed_lock_with_key: | WS | `order.create-batch` |
| [batchAmendOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L171) | :closed_lock_with_key: | WS | `order.amend-batch` |
| [batchCancelOrder()](https://github.com/tiagosiebler/bybit-api/blob/master/src/websocket-api-client.ts#L199) | :closed_lock_with_key: | WS | `order.cancel-batch` |

170
examples/ws-api-client.ts Normal file
View File

@@ -0,0 +1,170 @@
import { DefaultLogger, WebsocketAPIClient } from '../src';
// or
// import { DefaultLogger, WebsocketAPIClient } from 'bybit-api';
// const { DefaultLogger, WebsocketAPIClient } = require('bybit-api');
const key = process.env.API_KEY_COM;
const secret = process.env.API_SECRET_COM;
// function attachEventHandlers<TWSClient extends WebsocketClient>(
// wsClient: TWSClient,
// ): void {
// wsClient.on('update', (data) => {
// console.log('raw message received ', JSON.stringify(data));
// });
// wsClient.on('open', (data) => {
// console.log('ws connected', data.wsKey);
// });
// wsClient.on('reconnect', ({ wsKey }) => {
// console.log('ws automatically reconnecting.... ', wsKey);
// });
// wsClient.on('reconnected', (data) => {
// console.log('ws has reconnected ', data?.wsKey);
// });
// wsClient.on('authenticated', (data) => {
// console.log('ws has authenticated ', data?.wsKey);
// });
// }
async function main() {
// Optional
const logger = {
...DefaultLogger,
// For a more detailed view of the WebsocketClient, enable the `trace` level by uncommenting the below line:
// trace: (...params) => console.log('trace', ...params),
};
const wsClient = new WebsocketAPIClient(
{
key: key,
secret: secret,
// testnet: true, // Whether to use the testnet environment: https://testnet.bybit.com/app/user/api-management
// Whether to use the livenet demo trading environment
// Note: As of Jan 2025, demo trading only supports consuming events, it does
// NOT support the WS API.
// demoTrading: false,
// If you want your own event handlers instead of the default ones with logs,
// disable this setting and see the `attachEventHandlers` example below:
// attachEventListeners: false
},
logger, // Optional: inject a custom logger
);
// Optional, see above "attachEventListeners". Attach basic event handlers, so nothing is left unhandled
// attachEventHandlers(wsClient.getWSClient());
// Optional, if you see RECV Window errors, you can use this to manage time issues.
// ! However, make sure you sync your system clock first!
// https://github.com/tiagosiebler/awesome-crypto-examples/wiki/Timestamp-for-this-request-is-outside-of-the-recvWindow
// wsClient.setTimeOffsetMs(-5000);
// Optional: prepare the WebSocket API connection in advance.
// This happens automatically but you can do this early before making any API calls, to prevent delays from a cold start.
// await wsClient.getWSClient().connectWSAPI();
try {
const response = await wsClient.submitNewOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderType: 'Limit',
qty: '0.001',
side: 'Buy',
price: '50000',
});
console.log('submitNewOrder response: ', response);
} catch (e) {
console.log('submitNewOrder error: ', e);
}
try {
const response = await wsClient.amendOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
qty: '0.001',
price: '60000',
});
console.log('amendOrder response: ', response);
} catch (e) {
console.log('amendOrder error: ', e);
}
try {
const response = await wsClient.cancelOrder({
category: 'linear',
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
});
console.log('cancelOrder response: ', response);
} catch (e) {
console.log('cancelOrder error: ', e);
}
try {
const response = await wsClient.batchSubmitOrders('linear', [
{
symbol: 'BTCUSDT',
orderType: 'Limit',
qty: '0.001',
side: 'Buy',
price: '50000',
},
{
symbol: 'BTCUSDT',
orderType: 'Limit',
qty: '0.001',
side: 'Buy',
price: '60000',
},
{
symbol: 'BTCUSDT',
orderType: 'Limit',
qty: '0.001',
side: 'Buy',
price: '70000',
},
]);
console.log('batchSubmitOrders response: ', response);
} catch (e) {
console.log('batchSubmitOrders error: ', e);
}
try {
const response = await wsClient.batchAmendOrder('linear', [
{
symbol: 'BTCUSDT',
orderId: '2473ee58',
price: '80000',
},
{
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
price: '80000',
},
]);
console.log('batchAmendOrder response: ', response);
} catch (e) {
console.log('batchAmendOrder error: ', e);
}
try {
const response = await wsClient.batchCancelOrder('linear', [
{
symbol: 'BTCUSDT',
orderId: '2473ee58',
},
{
symbol: 'BTCUSDT',
orderId: 'b4b9e205-793c-4777-8112-0bf3c2d26b6e',
},
]);
console.log('batchCancelOrder response: ', response);
} catch (e) {
console.log('batchCancelOrder error: ', e);
}
}
main();

View File

@@ -55,7 +55,8 @@ async function main() {
* - Handle any exceptions in a catch block. * - Handle any exceptions in a catch block.
* *
* This is a more "raw" workflow in how WebSockets behave. For a more convenient & REST-like approach, using the * This is a more "raw" workflow in how WebSockets behave. For a more convenient & REST-like approach, using the
* promise-driven interface is recommended. See the `ws-api-promises.ts` example for a demonstration you can compare. * promise-driven interface is recommended. See the `ws-api-raw-promises.ts` and `ws-api-client.ts` examples for a
* demonstration you can compare.
* *
* Note: even without using promises, you should still tie on a .catch handler to each sendWSAPIRequest call, to prevent * Note: even without using promises, you should still tie on a .catch handler to each sendWSAPIRequest call, to prevent
* any unnecessary "unhandled promise rejection" exceptions. * any unnecessary "unhandled promise rejection" exceptions.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "bybit-api", "name": "bybit-api",
"version": "4.1.7", "version": "4.1.8",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "bybit-api", "name": "bybit-api",
"version": "4.1.7", "version": "4.1.8",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"axios": "^1.7.9", "axios": "^1.7.9",

View File

@@ -1,6 +1,6 @@
{ {
"name": "bybit-api", "name": "bybit-api",
"version": "4.1.7", "version": "4.1.8",
"description": "Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.", "description": "Complete & robust Node.js SDK for Bybit's REST APIs and WebSockets, with TypeScript & strong end to end tests.",
"main": "lib/index.js", "main": "lib/index.js",
"types": "lib/index.d.ts", "types": "lib/index.d.ts",

View File

@@ -1,6 +1,7 @@
export * from './rest-client-v5'; export * from './rest-client-v5';
export * from './spot-client-v3'; export * from './spot-client-v3';
export * from './websocket-client'; export * from './websocket-client';
export * from './websocket-api-client';
export * from './util/logger'; export * from './util/logger';
export * from './util'; export * from './util';
export * from './types'; export * from './types';

View File

@@ -65,6 +65,7 @@ export interface WSAPIRequest<
export interface WSAPIResponse< export interface WSAPIResponse<
TResponseData extends object = object, TResponseData extends object = object,
TOperation extends WSAPIOperation = WSAPIOperation, TOperation extends WSAPIOperation = WSAPIOperation,
TResponseExtInfo = {}, // added as optional for batch calls
> { > {
wsKey: WsKey; wsKey: WsKey;
/** Auto-generated */ /** Auto-generated */
@@ -73,6 +74,7 @@ export interface WSAPIResponse<
retMsg: 'OK' | string; retMsg: 'OK' | string;
op: TOperation; op: TOperation;
data: TResponseData; data: TResponseData;
retExtInfo: TResponseExtInfo;
header?: { header?: {
'X-Bapi-Limit': string; 'X-Bapi-Limit': string;
'X-Bapi-Limit-Status': string; 'X-Bapi-Limit-Status': string;

View File

@@ -124,7 +124,7 @@ export abstract class BaseWebsocketClient<
*/ */
private wsStore: WsStore<TWSKey, WsTopicRequest<string>>; private wsStore: WsStore<TWSKey, WsTopicRequest<string>>;
protected logger: typeof DefaultLogger; public logger: typeof DefaultLogger;
protected options: WebsocketClientOptions; protected options: WebsocketClientOptions;
@@ -413,7 +413,7 @@ export abstract class BaseWebsocketClient<
wsTopicRequests, wsTopicRequests,
}, },
); );
return; return isConnectionInProgress;
} }
// We're connected. Check if auth is needed and if already authenticated // We're connected. Check if auth is needed and if already authenticated
@@ -532,7 +532,11 @@ export abstract class BaseWebsocketClient<
/** /**
* Request connection to a specific websocket, instead of waiting for automatic connection. * Request connection to a specific websocket, instead of waiting for automatic connection.
*/ */
public async connect(wsKey: TWSKey): Promise<WSConnectedResult | undefined> { public async connect(
wsKey: TWSKey,
customUrl?: string | undefined,
throwOnError?: boolean,
): Promise<WSConnectedResult | undefined> {
try { try {
if (this.wsStore.isWsOpen(wsKey)) { if (this.wsStore.isWsOpen(wsKey)) {
this.logger.error( this.logger.error(
@@ -549,7 +553,7 @@ export abstract class BaseWebsocketClient<
'Refused to connect to ws, connection attempt already active', 'Refused to connect to ws, connection attempt already active',
{ ...WS_LOGGER_CATEGORY, wsKey }, { ...WS_LOGGER_CATEGORY, wsKey },
); );
return; return this.wsStore.getConnectionInProgressPromise(wsKey)?.promise;
} }
if ( if (
@@ -563,7 +567,7 @@ export abstract class BaseWebsocketClient<
this.wsStore.createConnectionInProgressPromise(wsKey, false); this.wsStore.createConnectionInProgressPromise(wsKey, false);
} }
const url = await this.getWsUrl(wsKey); const url = customUrl || (await this.getWsUrl(wsKey));
const ws = this.connectToWsUrl(url, wsKey); const ws = this.connectToWsUrl(url, wsKey);
this.wsStore.setWs(wsKey, ws); this.wsStore.setWs(wsKey, ws);
@@ -572,6 +576,10 @@ export abstract class BaseWebsocketClient<
} catch (err) { } catch (err) {
this.parseWsError('Connection failed', err, wsKey); this.parseWsError('Connection failed', err, wsKey);
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!); this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
if (throwOnError) {
throw err;
}
} }
} }
@@ -590,6 +598,8 @@ export abstract class BaseWebsocketClient<
this.parseWsError('Websocket onWsError', event, wsKey); this.parseWsError('Websocket onWsError', event, wsKey);
ws.onclose = (event: any) => this.onWsClose(event, wsKey); ws.onclose = (event: any) => this.onWsClose(event, wsKey);
ws.wsKey = wsKey;
return ws; return ws;
} }
@@ -668,12 +678,18 @@ export abstract class BaseWebsocketClient<
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING); this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
} }
this.logger.info('Reconnecting to websocket with delay...', {
...WS_LOGGER_CATEGORY,
wsKey,
connectionDelayMs,
});
if (this.wsStore.get(wsKey)?.activeReconnectTimer) { if (this.wsStore.get(wsKey)?.activeReconnectTimer) {
this.clearReconnectTimer(wsKey); this.clearReconnectTimer(wsKey);
} }
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => { this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
this.logger.info('Reconnecting to websocket', { this.logger.info('Reconnecting to websocket now', {
...WS_LOGGER_CATEGORY, ...WS_LOGGER_CATEGORY,
wsKey, wsKey,
}); });
@@ -1250,6 +1266,10 @@ export abstract class BaseWebsocketClient<
); );
this.getWsStore().rejectAllDeferredPromises(wsKey, 'disconnected'); this.getWsStore().rejectAllDeferredPromises(wsKey, 'disconnected');
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL); this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
// This was an intentional close, delete all state for this connection, as if it never existed:
this.wsStore.delete(wsKey);
this.emit('close', { wsKey, event }); this.emit('close', { wsKey, event });
} }
} }

View File

@@ -3,6 +3,8 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export type LogParams = null | any; export type LogParams = null | any;
export type DefaultLogger = typeof DefaultLogger;
export const DefaultLogger = { export const DefaultLogger = {
/** Ping/pong events and other raw messages that might be noisy. Enable this while troubleshooting. */ /** Ping/pong events and other raw messages that might be noisy. Enable this while troubleshooting. */
trace: (..._params: LogParams): void => { trace: (..._params: LogParams): void => {

View File

@@ -47,9 +47,9 @@ export class WsStore<
private wsState: Record<string, WsStoredState<TWSTopicSubscribeEventArgs>> = private wsState: Record<string, WsStoredState<TWSTopicSubscribeEventArgs>> =
{}; {};
private logger: typeof DefaultLogger; private logger: DefaultLogger;
constructor(logger: typeof DefaultLogger) { constructor(logger: DefaultLogger) {
this.logger = logger || DefaultLogger; this.logger = logger || DefaultLogger;
} }
@@ -131,6 +131,10 @@ export class WsStore<
return wsConnection; return wsConnection;
} }
/**
* deferred promises
*/
getDeferredPromise<TSuccessResult = any>( getDeferredPromise<TSuccessResult = any>(
wsKey: WsKey, wsKey: WsKey,
promiseRef: string | DeferredPromiseRef, promiseRef: string | DeferredPromiseRef,
@@ -206,9 +210,15 @@ export class WsStore<
if (promise?.reject) { if (promise?.reject) {
this.logger.trace( this.logger.trace(
`rejectDeferredPromise(): rejecting ${wsKey}/${promiseRef}/${value}`, `rejectDeferredPromise(): rejecting ${wsKey}/${promiseRef}`,
value,
); );
promise.reject(value);
if (typeof value === 'string') {
promise.reject(new Error(value));
} else {
promise.reject(value);
}
} }
if (removeAfter) { if (removeAfter) {
@@ -252,6 +262,9 @@ export class WsStore<
} }
try { try {
this.logger.trace(
`rejectAllDeferredPromises(): rejecting ${wsKey}/${promiseRef}/${reason}`,
);
this.rejectDeferredPromise(wsKey, promiseRef, reason, true); this.rejectDeferredPromise(wsKey, promiseRef, reason, true);
} catch (e) { } catch (e) {
this.logger.error( this.logger.error(
@@ -339,6 +352,7 @@ export class WsStore<
setConnectionState(key: WsKey, state: WsConnectionStateEnum) { setConnectionState(key: WsKey, state: WsConnectionStateEnum) {
this.get(key, true).connectionState = state; this.get(key, true).connectionState = state;
this.get(key, true).connectionStateChangedAt = new Date();
} }
isConnectionState(key: WsKey, state: WsConnectionStateEnum): boolean { isConnectionState(key: WsKey, state: WsConnectionStateEnum): boolean {
@@ -355,6 +369,22 @@ export class WsStore<
this.isConnectionState(key, WsConnectionStateEnum.CONNECTING) || this.isConnectionState(key, WsConnectionStateEnum.CONNECTING) ||
this.isConnectionState(key, WsConnectionStateEnum.RECONNECTING); this.isConnectionState(key, WsConnectionStateEnum.RECONNECTING);
if (isConnectionInProgress) {
const wsState = this.get(key, true);
const stateLastChangedAt = wsState?.connectionStateChangedAt;
const stateChangedAtTimestamp = stateLastChangedAt?.getTime();
if (stateChangedAtTimestamp) {
const timestampNow = new Date().getTime();
const stateChangedTimeAgo = timestampNow - stateChangedAtTimestamp;
const stateChangeTimeout = 15000; // allow a max 15 second timeout since the last state change before assuming stuck;
if (stateChangedTimeAgo >= stateChangeTimeout) {
const msg = 'State change timed out, reconnect workflow stuck?';
this.logger.error(msg, { key, wsState });
this.setConnectionState(key, WsConnectionStateEnum.ERROR);
}
}
}
return isConnectionInProgress; return isConnectionInProgress;
} }
@@ -366,13 +396,14 @@ export class WsStore<
getTopicsByKey(): Record<string, Set<TWSTopicSubscribeEventArgs>> { getTopicsByKey(): Record<string, Set<TWSTopicSubscribeEventArgs>> {
const result: any = {}; const result: any = {};
for (const refKey in this.wsState) { for (const refKey in this.wsState) {
result[refKey] = this.getTopics(refKey as WsKey); result[refKey] = this.getTopics(refKey as WsKey);
} }
return result; return result;
} }
// Since topics are objects we can't rely on the set to detect duplicates
/** /**
* Find matching "topic" request from the store * Find matching "topic" request from the store
* @param key * @param key

View File

@@ -8,7 +8,7 @@ export enum WsConnectionStateEnum {
CLOSING = 3, CLOSING = 3,
RECONNECTING = 4, RECONNECTING = 4,
// ERROR_RECONNECTING = 5, // ERROR_RECONNECTING = 5,
// ERROR = 5, ERROR = 5,
} }
export interface DeferredPromise<TSuccess = any, TError = any> { export interface DeferredPromise<TSuccess = any, TError = any> {
@@ -26,6 +26,7 @@ export interface WsStoredState<TWSTopicSubscribeEvent extends string | object> {
ws?: WebSocket; ws?: WebSocket;
/** The current lifecycle state of the connection (enum) */ /** The current lifecycle state of the connection (enum) */
connectionState?: WsConnectionStateEnum; connectionState?: WsConnectionStateEnum;
connectionStateChangedAt?: Date;
/** A timer that will send an upstream heartbeat (ping) when it expires */ /** A timer that will send an upstream heartbeat (ping) when it expires */
activePingTimer?: ReturnType<typeof setTimeout> | undefined; activePingTimer?: ReturnType<typeof setTimeout> | undefined;
/** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */ /** A timer tracking that an upstream heartbeat was sent, expecting a reply before it expires */

262
src/websocket-api-client.ts Normal file
View File

@@ -0,0 +1,262 @@
import {
AmendOrderParamsV5,
BatchAmendOrderParamsV5,
BatchAmendOrderResultV5,
BatchCancelOrderParamsV5,
BatchCancelOrderResultV5,
BatchCreateOrderResultV5,
BatchOrderParamsV5,
BatchOrdersRetExtInfoV5,
CancelOrderParamsV5,
OrderParamsV5,
OrderResultV5,
} from './types';
import { WSAPIResponse } from './types/websockets/ws-api';
import { WSClientConfigurableOptions } from './types/websockets/ws-general';
import { DefaultLogger } from './util';
import { WS_KEY_MAP } from './util/websockets/websocket-util';
import { WebsocketClient } from './websocket-client';
/**
* Configurable options specific to only the REST-like WebsocketAPIClient
*/
export interface WSAPIClientConfigurableOptions {
/**
* Default: true
*
* Attach default event listeners, which will console log any high level
* events (opened/reconnecting/reconnected/etc).
*
* If you disable this, you should set your own event listeners
* on the embedded WS Client `wsApiClient.getWSClient().on(....)`.
*/
attachEventListeners: boolean;
}
/**
* This is a minimal Websocket API wrapper around the WebsocketClient.
*
* Some methods support passing in a custom "wsKey". This is a reference to which WS connection should
* be used to transmit that message. This is only useful if you wish to use an alternative wss
* domain that is supported by the SDK.
*
* Note: To use testnet, don't set the wsKey - use `testnet: true` in
* the constructor instead.
*
* Note: You can also directly use the sendWSAPIRequest() method to make WS API calls, but some
* may find the below methods slightly more intuitive.
*
* Refer to the WS API promises example for a more detailed example on using sendWSAPIRequest() directly:
* https://github.com/tiagosiebler/bybit-api/blob/master/examples/ws-api-raw-promises.ts
*/
export class WebsocketAPIClient {
private wsClient: WebsocketClient;
private logger: DefaultLogger;
private options: WSClientConfigurableOptions & WSAPIClientConfigurableOptions;
constructor(
options?: WSClientConfigurableOptions &
Partial<WSAPIClientConfigurableOptions>,
logger?: DefaultLogger,
) {
this.wsClient = new WebsocketClient(options, logger);
this.options = {
attachEventListeners: true,
...options,
};
this.logger = this.wsClient.logger;
this.setupDefaultEventListeners();
}
public getWSClient(): WebsocketClient {
return this.wsClient;
}
public setTimeOffsetMs(newOffset: number): void {
return this.getWSClient().setTimeOffsetMs(newOffset);
}
/*
* Bybit WebSocket API Methods
* https://bybit-exchange.github.io/docs/v5/websocket/trade/guideline
*/
/**
* Submit a new order
*
* @param params
* @returns
*/
submitNewOrder(
params: OrderParamsV5,
): Promise<WSAPIResponse<OrderResultV5, 'order.create'>> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.create',
params,
);
}
/**
* Amend an order
*
* @param params
* @returns
*/
amendOrder(
params: AmendOrderParamsV5,
): Promise<WSAPIResponse<OrderResultV5, 'order.amend'>> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.amend',
params,
);
}
/**
* Cancel an order
*
* @param params
* @returns
*/
cancelOrder(
params: CancelOrderParamsV5,
): Promise<WSAPIResponse<OrderResultV5, 'order.cancel'>> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.cancel',
params,
);
}
/**
* Batch submit orders
*
* @param params
* @returns
*/
batchSubmitOrders(
category: 'option' | 'linear',
orders: BatchOrderParamsV5[],
): Promise<
WSAPIResponse<
{
list: BatchCreateOrderResultV5[];
},
'order.create-batch',
BatchOrdersRetExtInfoV5
>
> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.create-batch',
{
category,
request: orders,
},
);
}
/**
* Batch amend orders
*
* @param params
* @returns
*/
batchAmendOrder(
category: 'option' | 'linear',
orders: BatchAmendOrderParamsV5[],
): Promise<
WSAPIResponse<
{
list: BatchAmendOrderResultV5[];
},
'order.amend-batch',
BatchOrdersRetExtInfoV5
>
> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.amend-batch',
{
category,
request: orders,
},
);
}
/**
* Batch cancel orders
*
* @param params
* @returns
*/
batchCancelOrder(
category: 'option' | 'linear',
orders: BatchCancelOrderParamsV5[],
): Promise<
WSAPIResponse<
{
list: BatchCancelOrderResultV5[];
},
'order.cancel-batch',
BatchOrdersRetExtInfoV5
>
> {
return this.wsClient.sendWSAPIRequest(
WS_KEY_MAP.v5PrivateTrade,
'order.cancel-batch',
{
category,
request: orders,
},
);
}
/**
*
*
*
*
*
*
*
* Private methods for handling some of the convenience/automation provided by the WS API Client
*
*
*
*
*
*
*
*/
private setupDefaultEventListeners() {
if (this.options.attachEventListeners) {
/**
* General event handlers for monitoring the WebsocketClient
*/
this.wsClient
.on('open', (data) => {
console.log(new Date(), 'ws connected', data.wsKey);
})
.on('reconnect', ({ wsKey }) => {
console.log(new Date(), 'ws automatically reconnecting.... ', wsKey);
})
.on('reconnected', (data) => {
console.log(new Date(), 'ws has reconnected ', data?.wsKey);
})
.on('authenticated', (data) => {
console.info(new Date(), 'ws has authenticated ', data?.wsKey);
})
.on('exception', (data) => {
console.error(new Date(), 'ws exception: ', JSON.stringify(data));
});
}
}
}

View File

@@ -343,23 +343,23 @@ export class WebsocketClient extends BaseWebsocketClient<
// do not trigger excess property checks // do not trigger excess property checks
// Without these overloads, TypeScript won't complain if you include an // Without these overloads, TypeScript won't complain if you include an
// unexpected property with your request (if it doesn't clash with an existing property) // unexpected property with your request (if it doesn't clash with an existing property)
sendWSAPIRequest<TWSOpreation extends WSAPIOperation = 'order.create'>( sendWSAPIRequest<TWSOperation extends WSAPIOperation = 'order.create'>(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade, wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: TWSOpreation, operation: TWSOperation,
params: WsAPITopicRequestParamMap[TWSOpreation], params: WsAPITopicRequestParamMap[TWSOperation],
): Promise<WsAPIOperationResponseMap[TWSOpreation]>; ): Promise<WsAPIOperationResponseMap[TWSOperation]>;
sendWSAPIRequest<TWSOpreation extends WSAPIOperation = 'order.amend'>( sendWSAPIRequest<TWSOperation extends WSAPIOperation = 'order.amend'>(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade, wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: TWSOpreation, operation: TWSOperation,
params: WsAPITopicRequestParamMap[TWSOpreation], params: WsAPITopicRequestParamMap[TWSOperation],
): Promise<WsAPIOperationResponseMap[TWSOpreation]>; ): Promise<WsAPIOperationResponseMap[TWSOperation]>;
sendWSAPIRequest<TWSOpreation extends WSAPIOperation = 'order.cancel'>( sendWSAPIRequest<TWSOperation extends WSAPIOperation = 'order.cancel'>(
wsKey: typeof WS_KEY_MAP.v5PrivateTrade, wsKey: typeof WS_KEY_MAP.v5PrivateTrade,
operation: TWSOpreation, operation: TWSOperation,
params: WsAPITopicRequestParamMap[TWSOpreation], params: WsAPITopicRequestParamMap[TWSOperation],
): Promise<WsAPIOperationResponseMap[TWSOpreation]>; ): Promise<WsAPIOperationResponseMap[TWSOperation]>;
async sendWSAPIRequest< async sendWSAPIRequest<
TWSKey extends keyof WsAPIWsKeyTopicMap, TWSKey extends keyof WsAPIWsKeyTopicMap,
@@ -379,11 +379,13 @@ export class WebsocketClient extends BaseWebsocketClient<
await this.assertIsAuthenticated(wsKey); await this.assertIsAuthenticated(wsKey);
this.logger.trace('sendWSAPIRequest()->assertIsAuthenticated() ok'); this.logger.trace('sendWSAPIRequest()->assertIsAuthenticated() ok');
const timestampMs = Date.now() + (this.getTimeOffsetMs() || 0);
const requestEvent: WSAPIRequest<TWSParams> = { const requestEvent: WSAPIRequest<TWSParams> = {
reqId: this.getNewRequestId(), reqId: this.getNewRequestId(),
header: { header: {
'X-BAPI-RECV-WINDOW': `${this.options.recvWindow}`, 'X-BAPI-RECV-WINDOW': `${this.options.recvWindow}`,
'X-BAPI-TIMESTAMP': `${Date.now()}`, 'X-BAPI-TIMESTAMP': `${timestampMs}`,
Referer: APIID, Referer: APIID,
}, },
op: operation, op: operation,
@@ -396,21 +398,52 @@ export class WebsocketClient extends BaseWebsocketClient<
// Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events // Store deferred promise, resolved within the "resolveEmittableEvents" method while parsing incoming events
const promiseRef = getPromiseRefForWSAPIRequest(requestEvent); const promiseRef = getPromiseRefForWSAPIRequest(requestEvent);
const deferredPromise = const deferredPromise = this.getWsStore().createDeferredPromise<
this.getWsStore().createDeferredPromise<TWSAPIResponse>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
wsKey, TWSAPIResponse & { request: any }
promiseRef, >(wsKey, promiseRef, false);
false,
); // Enrich returned promise with request context for easier debugging
deferredPromise.promise
?.then((res) => {
if (!Array.isArray(res)) {
res.request = {
wsKey,
...signedEvent,
};
}
return res;
})
.catch((e) => {
if (typeof e === 'string') {
this.logger.error('Unexpected string thrown without Error object:', {
e,
wsKey,
signedEvent,
});
return e;
}
e.request = {
wsKey,
operation,
params: params,
};
// throw e;
return e;
});
this.logger.trace( this.logger.trace(
`sendWSAPIRequest(): sending raw request: ${JSON.stringify(signedEvent, null, 2)}`, `sendWSAPIRequest(): sending raw request: ${JSON.stringify(signedEvent, null, 2)}`,
); );
// Send event // Send event
this.tryWsSend(wsKey, JSON.stringify(signedEvent)); const throwExceptions = false;
this.tryWsSend(wsKey, JSON.stringify(signedEvent), throwExceptions);
this.logger.trace(`sendWSAPIRequest(): sent ${operation} event`); this.logger.trace(
`sendWSAPIRequest(): sent "${operation}" event with promiseRef(${promiseRef})`,
);
// Return deferred promise, so caller can await this call // Return deferred promise, so caller can await this call
return deferredPromise.promise!; return deferredPromise.promise!;