feat(): improve wiring on promise-subscribe workflows, fixes #399 (with caveat described in PR)
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
||||
WS_KEY_MAP,
|
||||
WsTopicRequest,
|
||||
getMaxTopicsPerSubscribeEvent,
|
||||
getNormalisedTopicRequests,
|
||||
getPromiseRefForWSAPIRequest,
|
||||
getWsKeyForTopic,
|
||||
getWsUrl,
|
||||
@@ -28,7 +29,11 @@ import {
|
||||
neverGuard,
|
||||
} from './util';
|
||||
import { signMessage } from './util/node-support';
|
||||
import { BaseWebsocketClient, EmittableEvent } from './util/BaseWSClient';
|
||||
import {
|
||||
BaseWebsocketClient,
|
||||
EmittableEvent,
|
||||
MidflightWsRequestEvent,
|
||||
} from './util/BaseWSClient';
|
||||
import {
|
||||
WSAPIRequest,
|
||||
WsAPIOperationResponseMap,
|
||||
@@ -41,7 +46,10 @@ import {
|
||||
const WS_LOGGER_CATEGORY = { category: 'bybit-ws' };
|
||||
|
||||
// export class WebsocketClient extends EventEmitter {
|
||||
export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
export class WebsocketClient extends BaseWebsocketClient<
|
||||
WsKey,
|
||||
WsRequestOperationBybit<WsTopic>
|
||||
> {
|
||||
/**
|
||||
* Request connection of all dependent (public & private) websockets, instead of waiting for automatic connection by library
|
||||
*/
|
||||
@@ -72,7 +80,18 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
}
|
||||
}
|
||||
|
||||
public connectPublic(): Promise<WebSocket | undefined>[] {
|
||||
/**
|
||||
* Ensures the WS API connection is active and ready.
|
||||
*
|
||||
* You do not need to call this, but if you call this before making any WS API requests,
|
||||
* it can accelerate the first request (by preparing the connection in advance).
|
||||
*/
|
||||
public connectWSAPI(): Promise<unknown> {
|
||||
/** This call automatically ensures the connection is active AND authenticated before resolving */
|
||||
return this.assertIsAuthenticated(WS_KEY_MAP.v5PrivateTrade);
|
||||
}
|
||||
|
||||
public connectPublic(): Promise<WSConnectedResult | undefined>[] {
|
||||
switch (this.options.market) {
|
||||
case 'v5':
|
||||
default: {
|
||||
@@ -152,9 +171,134 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Subscribe to V5 topics & track/persist them.
|
||||
* @param wsTopics - topic or list of topics
|
||||
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
||||
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
||||
*/
|
||||
public subscribeV5(
|
||||
wsTopics: WsTopic[] | WsTopic,
|
||||
category: CategoryV5,
|
||||
isPrivateTopic?: boolean,
|
||||
): Promise<unknown>[] {
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
|
||||
const perWsKeyTopics: { [key in WsKey]?: WsTopicRequest<WsTopic>[] } = {};
|
||||
|
||||
// Sort into per-WsKey batches, in case there is a mix of topics here
|
||||
for (const topic of topics) {
|
||||
const derivedWsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
category,
|
||||
);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
category: category,
|
||||
};
|
||||
|
||||
if (
|
||||
!perWsKeyTopics[derivedWsKey] ||
|
||||
!Array.isArray(perWsKeyTopics[derivedWsKey])
|
||||
) {
|
||||
perWsKeyTopics[derivedWsKey] = [];
|
||||
}
|
||||
|
||||
perWsKeyTopics[derivedWsKey].push(wsRequest);
|
||||
}
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// Batch sub topics per ws key
|
||||
for (const wsKey in perWsKeyTopics) {
|
||||
const wsKeyTopicRequests = perWsKeyTopics[wsKey as WsKey];
|
||||
if (wsKeyTopicRequests?.length) {
|
||||
const requestPromise = this.subscribeTopicsForWsKey(
|
||||
wsKeyTopicRequests,
|
||||
wsKey as WsKey,
|
||||
);
|
||||
|
||||
if (Array.isArray(requestPromise)) {
|
||||
promises.push(...requestPromise);
|
||||
} else {
|
||||
promises.push(requestPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return promise to resolve midflight WS request (only works if already connected before request)
|
||||
return promises;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
|
||||
* @param wsTopics - topic or list of topics
|
||||
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
||||
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
||||
*/
|
||||
public unsubscribeV5(
|
||||
wsTopics: WsTopic[] | WsTopic,
|
||||
category: CategoryV5,
|
||||
isPrivateTopic?: boolean,
|
||||
): Promise<unknown>[] {
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
|
||||
const perWsKeyTopics: { [key in WsKey]?: WsTopicRequest<WsTopic>[] } = {};
|
||||
|
||||
// Sort into per-WsKey batches, in case there is a mix of topics here
|
||||
for (const topic of topics) {
|
||||
const derivedWsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
category,
|
||||
);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
category: category,
|
||||
};
|
||||
|
||||
if (
|
||||
!perWsKeyTopics[derivedWsKey] ||
|
||||
!Array.isArray(perWsKeyTopics[derivedWsKey])
|
||||
) {
|
||||
perWsKeyTopics[derivedWsKey] = [];
|
||||
}
|
||||
|
||||
perWsKeyTopics[derivedWsKey].push(wsRequest);
|
||||
}
|
||||
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// Batch sub topics per ws key
|
||||
for (const wsKey in perWsKeyTopics) {
|
||||
const wsKeyTopicRequests = perWsKeyTopics[wsKey as WsKey];
|
||||
if (wsKeyTopicRequests?.length) {
|
||||
const requestPromise = this.unsubscribeTopicsForWsKey(
|
||||
wsKeyTopicRequests,
|
||||
wsKey as WsKey,
|
||||
);
|
||||
|
||||
if (Array.isArray(requestPromise)) {
|
||||
promises.push(...requestPromise);
|
||||
} else {
|
||||
promises.push(requestPromise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return promise to resolve midflight WS request (only works if already connected before request)
|
||||
return promises;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request subscription to one or more topics. Pass topics as either an array of strings, or array of objects (if the topic has parameters).
|
||||
* Objects should be formatted as {topic: string, params: object}.
|
||||
* Objects should be formatted as {topic: string, params: object, category: CategoryV5}.
|
||||
*
|
||||
* - Subscriptions are automatically routed to the correct websocket connection.
|
||||
* - Authentication/connection is automatic.
|
||||
@@ -166,15 +310,42 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
requests:
|
||||
| (WsTopicRequest<WsTopic> | WsTopic)
|
||||
| (WsTopicRequest<WsTopic> | WsTopic)[],
|
||||
wsKey: WsKey,
|
||||
wsKey?: WsKey,
|
||||
) {
|
||||
if (!Array.isArray(requests)) {
|
||||
this.subscribeTopicsForWsKey([requests], wsKey);
|
||||
return;
|
||||
const topicRequests = Array.isArray(requests) ? requests : [requests];
|
||||
const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests);
|
||||
|
||||
const isPrivateTopic = undefined;
|
||||
|
||||
const perWsKeyTopics: { [key in WsKey]?: WsTopicRequest<WsTopic>[] } = {};
|
||||
|
||||
// Sort into per wsKey arrays, in case topics are mixed together for different wsKeys
|
||||
for (const topicRequest of normalisedTopicRequests) {
|
||||
const derivedWsKey =
|
||||
wsKey ||
|
||||
getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topicRequest.topic,
|
||||
isPrivateTopic,
|
||||
topicRequest.category,
|
||||
);
|
||||
|
||||
if (
|
||||
!perWsKeyTopics[derivedWsKey] ||
|
||||
!Array.isArray(perWsKeyTopics[derivedWsKey])
|
||||
) {
|
||||
perWsKeyTopics[derivedWsKey] = [];
|
||||
}
|
||||
|
||||
perWsKeyTopics[derivedWsKey].push(topicRequest);
|
||||
}
|
||||
|
||||
if (requests.length) {
|
||||
this.subscribeTopicsForWsKey(requests, wsKey);
|
||||
// Batch sub topics per ws key
|
||||
for (const wsKey in perWsKeyTopics) {
|
||||
const wsKeyTopicRequests = perWsKeyTopics[wsKey];
|
||||
if (wsKeyTopicRequests?.length) {
|
||||
this.subscribeTopicsForWsKey(wsKeyTopicRequests, wsKey as WsKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,15 +359,42 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
requests:
|
||||
| (WsTopicRequest<WsTopic> | WsTopic)
|
||||
| (WsTopicRequest<WsTopic> | WsTopic)[],
|
||||
wsKey: WsKey,
|
||||
wsKey?: WsKey,
|
||||
) {
|
||||
if (!Array.isArray(requests)) {
|
||||
this.unsubscribeTopicsForWsKey([requests], wsKey);
|
||||
return;
|
||||
const topicRequests = Array.isArray(requests) ? requests : [requests];
|
||||
const normalisedTopicRequests = getNormalisedTopicRequests(topicRequests);
|
||||
|
||||
const isPrivateTopic = undefined;
|
||||
|
||||
const perWsKeyTopics: { [key in WsKey]?: WsTopicRequest<WsTopic>[] } = {};
|
||||
|
||||
// Sort into per wsKey arrays, in case topics are mixed together for different wsKeys
|
||||
for (const topicRequest of normalisedTopicRequests) {
|
||||
const derivedWsKey =
|
||||
wsKey ||
|
||||
getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topicRequest.topic,
|
||||
isPrivateTopic,
|
||||
topicRequest.category,
|
||||
);
|
||||
|
||||
if (
|
||||
!perWsKeyTopics[derivedWsKey] ||
|
||||
!Array.isArray(perWsKeyTopics[derivedWsKey])
|
||||
) {
|
||||
perWsKeyTopics[derivedWsKey] = [];
|
||||
}
|
||||
|
||||
perWsKeyTopics[derivedWsKey].push(topicRequest);
|
||||
}
|
||||
|
||||
if (requests.length) {
|
||||
this.unsubscribeTopicsForWsKey(requests, wsKey);
|
||||
// Batch sub topics per ws key
|
||||
for (const wsKey in perWsKeyTopics) {
|
||||
const wsKeyTopicRequests = perWsKeyTopics[wsKey];
|
||||
if (wsKeyTopicRequests?.length) {
|
||||
this.unsubscribeTopicsForWsKey(wsKeyTopicRequests, wsKey as WsKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,78 +412,6 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* Subscribe to V5 topics & track/persist them.
|
||||
* @param wsTopics - topic or list of topics
|
||||
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
||||
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
||||
*/
|
||||
public subscribeV5(
|
||||
wsTopics: WsTopic[] | WsTopic,
|
||||
category: CategoryV5,
|
||||
isPrivateTopic?: boolean,
|
||||
) {
|
||||
// TODO: sort into WS key then bulk sub per wskey
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
|
||||
return new Promise<void>((resolver, rejector) => {
|
||||
topics.forEach((topic) => {
|
||||
const wsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
category,
|
||||
);
|
||||
|
||||
// TODO: move this to base client
|
||||
this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
category: category,
|
||||
};
|
||||
|
||||
// Persist topic for reconnects
|
||||
this.subscribeTopicsForWsKey([wsRequest], wsKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from V5 topics & remove them from memory. They won't be re-subscribed to if the connection reconnects.
|
||||
* @param wsTopics - topic or list of topics
|
||||
* @param category - the API category this topic is for (e.g. "linear"). The value is only important when connecting to public topics and will be ignored for private topics.
|
||||
* @param isPrivateTopic - optional - the library will try to detect private topics, you can use this to mark a topic as private (if the topic isn't recognised yet)
|
||||
*/
|
||||
public unsubscribeV5(
|
||||
wsTopics: WsTopic[] | WsTopic,
|
||||
category: CategoryV5,
|
||||
isPrivateTopic?: boolean,
|
||||
) {
|
||||
// TODO: sort into WS key then bulk sub per wskey
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
|
||||
topics.forEach((topic) => {
|
||||
const wsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
category,
|
||||
);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
category: category,
|
||||
};
|
||||
|
||||
this.removeTopicPendingSubscription(wsKey, topic);
|
||||
|
||||
// Remove topic from persistence for reconnects and unsubscribe
|
||||
this.unsubscribeTopicsForWsKey([wsRequest], wsKey);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to V1-V3 topics & track/persist them.
|
||||
*
|
||||
@@ -298,7 +424,7 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
public subscribeV3(
|
||||
wsTopics: WsTopic[] | WsTopic,
|
||||
isPrivateTopic?: boolean,
|
||||
): Promise<void> {
|
||||
): Promise<unknown>[] {
|
||||
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
|
||||
if (this.options.market === 'v5') {
|
||||
topics.forEach((topic) => {
|
||||
@@ -310,25 +436,27 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
});
|
||||
}
|
||||
|
||||
return new Promise<void>((resolver, rejector) => {
|
||||
topics.forEach((topic) => {
|
||||
const wsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
);
|
||||
const promises: Promise<unknown>[] = [];
|
||||
|
||||
// TODO: move to base client
|
||||
this.upsertPendingTopicsSubscriptions(wsKey, topic, resolver, rejector);
|
||||
topics.forEach((topic) => {
|
||||
const wsKey = getWsKeyForTopic(
|
||||
this.options.market,
|
||||
topic,
|
||||
isPrivateTopic,
|
||||
);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
};
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
};
|
||||
|
||||
// Persist topic for reconnects
|
||||
this.subscribeTopicsForWsKey([wsRequest], wsKey);
|
||||
});
|
||||
// Persist topic for reconnects
|
||||
const requestPromise = this.subscribeTopicsForWsKey([wsRequest], wsKey);
|
||||
|
||||
promises.push(requestPromise);
|
||||
});
|
||||
|
||||
// Return promise to resolve midflight WS request (only works if already connected before request)
|
||||
return promises;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -361,9 +489,6 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
isPrivateTopic,
|
||||
);
|
||||
|
||||
// TODO: move to base client
|
||||
this.removeTopicPendingSubscription(wsKey, topic);
|
||||
|
||||
const wsRequest: WsTopicRequest<WsTopic> = {
|
||||
topic: topic,
|
||||
};
|
||||
@@ -484,8 +609,10 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
requests: WsTopicRequest<string>[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
wsKey: WsKey,
|
||||
): Promise<WsRequestOperationBybit<WsTopic>[]> {
|
||||
const wsRequestEvents: WsRequestOperationBybit<WsTopic>[] = [];
|
||||
): Promise<MidflightWsRequestEvent<WsRequestOperationBybit<WsTopic>>[]> {
|
||||
const wsRequestEvents: MidflightWsRequestEvent<
|
||||
WsRequestOperationBybit<WsTopic>
|
||||
>[] = [];
|
||||
const wsRequestBuildingErrors: unknown[] = [];
|
||||
|
||||
switch (market) {
|
||||
@@ -496,8 +623,15 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
args: requests.map((r) => r.topic),
|
||||
};
|
||||
|
||||
const midflightWsEvent: MidflightWsRequestEvent<
|
||||
WsRequestOperationBybit<WsTopic>
|
||||
> = {
|
||||
requestKey: wsEvent.req_id,
|
||||
requestEvent: wsEvent,
|
||||
};
|
||||
|
||||
wsRequestEvents.push({
|
||||
...wsEvent,
|
||||
...midflightWsEvent,
|
||||
});
|
||||
break;
|
||||
}
|
||||
@@ -625,11 +759,14 @@ export class WebsocketClient extends BaseWebsocketClient<WsKey> {
|
||||
// parsed: JSON.stringify(parsed),
|
||||
// });
|
||||
|
||||
if (isTopicSubscriptionConfirmation(parsed)) {
|
||||
// Only applies to the V5 WS topics
|
||||
if (isTopicSubscriptionConfirmation(parsed) && parsed.req_id) {
|
||||
const isTopicSubscriptionSuccessEvent =
|
||||
isTopicSubscriptionSuccess(parsed);
|
||||
|
||||
this.updatePendingTopicSubscriptionStatus(
|
||||
wsKey,
|
||||
parsed.req_id,
|
||||
parsed,
|
||||
isTopicSubscriptionSuccessEvent,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user