@siebly/kraken-api 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +7 -0
- package/README.md +624 -0
- package/dist/cjs/DerivativesClient.d.ts +657 -0
- package/dist/cjs/DerivativesClient.js +726 -0
- package/dist/cjs/DerivativesClient.js.map +1 -0
- package/dist/cjs/InstitutionalClient.d.ts +183 -0
- package/dist/cjs/InstitutionalClient.js +266 -0
- package/dist/cjs/InstitutionalClient.js.map +1 -0
- package/dist/cjs/PartnerClient.d.ts +257 -0
- package/dist/cjs/PartnerClient.js +344 -0
- package/dist/cjs/PartnerClient.js.map +1 -0
- package/dist/cjs/SpotClient.d.ts +547 -0
- package/dist/cjs/SpotClient.js +607 -0
- package/dist/cjs/SpotClient.js.map +1 -0
- package/dist/cjs/WebsocketAPIClient.d.ts +95 -0
- package/dist/cjs/WebsocketAPIClient.js +136 -0
- package/dist/cjs/WebsocketAPIClient.js.map +1 -0
- package/dist/cjs/WebsocketClient.d.ts +120 -0
- package/dist/cjs/WebsocketClient.js +669 -0
- package/dist/cjs/WebsocketClient.js.map +1 -0
- package/dist/cjs/index.d.ts +24 -0
- package/dist/cjs/index.js +41 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/lib/BaseRestClient.d.ts +66 -0
- package/dist/cjs/lib/BaseRestClient.js +548 -0
- package/dist/cjs/lib/BaseRestClient.js.map +1 -0
- package/dist/cjs/lib/BaseWSClient.d.ts +230 -0
- package/dist/cjs/lib/BaseWSClient.js +935 -0
- package/dist/cjs/lib/BaseWSClient.js.map +1 -0
- package/dist/cjs/lib/misc-util.d.ts +1 -0
- package/dist/cjs/lib/misc-util.js +7 -0
- package/dist/cjs/lib/misc-util.js.map +1 -0
- package/dist/cjs/lib/requestUtils.d.ts +68 -0
- package/dist/cjs/lib/requestUtils.js +82 -0
- package/dist/cjs/lib/requestUtils.js.map +1 -0
- package/dist/cjs/lib/webCryptoAPI.d.ts +15 -0
- package/dist/cjs/lib/webCryptoAPI.js +85 -0
- package/dist/cjs/lib/webCryptoAPI.js.map +1 -0
- package/dist/cjs/lib/websocket/WsStore.d.ts +75 -0
- package/dist/cjs/lib/websocket/WsStore.js +303 -0
- package/dist/cjs/lib/websocket/WsStore.js.map +1 -0
- package/dist/cjs/lib/websocket/WsStore.types.d.ts +51 -0
- package/dist/cjs/lib/websocket/WsStore.types.js +14 -0
- package/dist/cjs/lib/websocket/WsStore.types.js.map +1 -0
- package/dist/cjs/lib/websocket/logger.d.ts +7 -0
- package/dist/cjs/lib/websocket/logger.js +16 -0
- package/dist/cjs/lib/websocket/logger.js.map +1 -0
- package/dist/cjs/lib/websocket/rest-client-cache.d.ts +22 -0
- package/dist/cjs/lib/websocket/rest-client-cache.js +66 -0
- package/dist/cjs/lib/websocket/rest-client-cache.js.map +1 -0
- package/dist/cjs/lib/websocket/websocket-util.d.ts +76 -0
- package/dist/cjs/lib/websocket/websocket-util.js +59 -0
- package/dist/cjs/lib/websocket/websocket-util.js.map +1 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/types/request/derivatives.types.d.ts +186 -0
- package/dist/cjs/types/request/derivatives.types.js +6 -0
- package/dist/cjs/types/request/derivatives.types.js.map +1 -0
- package/dist/cjs/types/request/institutional.types.d.ts +313 -0
- package/dist/cjs/types/request/institutional.types.js +4 -0
- package/dist/cjs/types/request/institutional.types.js.map +1 -0
- package/dist/cjs/types/request/partner.types.d.ts +291 -0
- package/dist/cjs/types/request/partner.types.js +4 -0
- package/dist/cjs/types/request/partner.types.js.map +1 -0
- package/dist/cjs/types/request/spot.types.d.ts +324 -0
- package/dist/cjs/types/request/spot.types.js +6 -0
- package/dist/cjs/types/request/spot.types.js.map +1 -0
- package/dist/cjs/types/request/wsapi.types.d.ts +90 -0
- package/dist/cjs/types/request/wsapi.types.js +6 -0
- package/dist/cjs/types/request/wsapi.types.js.map +1 -0
- package/dist/cjs/types/response/derivatives.types.d.ts +712 -0
- package/dist/cjs/types/response/derivatives.types.js +6 -0
- package/dist/cjs/types/response/derivatives.types.js.map +1 -0
- package/dist/cjs/types/response/institutional.types.d.ts +318 -0
- package/dist/cjs/types/response/institutional.types.js +4 -0
- package/dist/cjs/types/response/institutional.types.js.map +1 -0
- package/dist/cjs/types/response/partner.types.d.ts +332 -0
- package/dist/cjs/types/response/partner.types.js +4 -0
- package/dist/cjs/types/response/partner.types.js.map +1 -0
- package/dist/cjs/types/response/shared.types.d.ts +27 -0
- package/dist/cjs/types/response/shared.types.js +3 -0
- package/dist/cjs/types/response/shared.types.js.map +1 -0
- package/dist/cjs/types/response/spot.types.d.ts +590 -0
- package/dist/cjs/types/response/spot.types.js +6 -0
- package/dist/cjs/types/response/spot.types.js.map +1 -0
- package/dist/cjs/types/response/ws.d.ts +11 -0
- package/dist/cjs/types/response/ws.js +3 -0
- package/dist/cjs/types/response/ws.js.map +1 -0
- package/dist/cjs/types/response/wsapi.types.d.ts +44 -0
- package/dist/cjs/types/response/wsapi.types.js +6 -0
- package/dist/cjs/types/response/wsapi.types.js.map +1 -0
- package/dist/cjs/types/websockets/ws-api.d.ts +71 -0
- package/dist/cjs/types/websockets/ws-api.js +18 -0
- package/dist/cjs/types/websockets/ws-api.js.map +1 -0
- package/dist/cjs/types/websockets/ws-events.d.ts +11 -0
- package/dist/cjs/types/websockets/ws-events.js +11 -0
- package/dist/cjs/types/websockets/ws-events.js.map +1 -0
- package/dist/cjs/types/websockets/ws-general.d.ts +67 -0
- package/dist/cjs/types/websockets/ws-general.js +3 -0
- package/dist/cjs/types/websockets/ws-general.js.map +1 -0
- package/dist/cjs/types/websockets/ws-subscriptions.d.ts +11 -0
- package/dist/cjs/types/websockets/ws-subscriptions.js +31 -0
- package/dist/cjs/types/websockets/ws-subscriptions.js.map +1 -0
- package/dist/mjs/DerivativesClient.d.ts +657 -0
- package/dist/mjs/DerivativesClient.js +722 -0
- package/dist/mjs/DerivativesClient.js.map +1 -0
- package/dist/mjs/InstitutionalClient.d.ts +183 -0
- package/dist/mjs/InstitutionalClient.js +262 -0
- package/dist/mjs/InstitutionalClient.js.map +1 -0
- package/dist/mjs/PartnerClient.d.ts +257 -0
- package/dist/mjs/PartnerClient.js +340 -0
- package/dist/mjs/PartnerClient.js.map +1 -0
- package/dist/mjs/SpotClient.d.ts +547 -0
- package/dist/mjs/SpotClient.js +603 -0
- package/dist/mjs/SpotClient.js.map +1 -0
- package/dist/mjs/WebsocketAPIClient.d.ts +95 -0
- package/dist/mjs/WebsocketAPIClient.js +132 -0
- package/dist/mjs/WebsocketAPIClient.js.map +1 -0
- package/dist/mjs/WebsocketClient.d.ts +120 -0
- package/dist/mjs/WebsocketClient.js +665 -0
- package/dist/mjs/WebsocketClient.js.map +1 -0
- package/dist/mjs/index.d.ts +24 -0
- package/dist/mjs/index.js +25 -0
- package/dist/mjs/index.js.map +1 -0
- package/dist/mjs/lib/BaseRestClient.d.ts +66 -0
- package/dist/mjs/lib/BaseRestClient.js +541 -0
- package/dist/mjs/lib/BaseRestClient.js.map +1 -0
- package/dist/mjs/lib/BaseWSClient.d.ts +230 -0
- package/dist/mjs/lib/BaseWSClient.js +928 -0
- package/dist/mjs/lib/BaseWSClient.js.map +1 -0
- package/dist/mjs/lib/misc-util.d.ts +1 -0
- package/dist/mjs/lib/misc-util.js +4 -0
- package/dist/mjs/lib/misc-util.js.map +1 -0
- package/dist/mjs/lib/requestUtils.d.ts +68 -0
- package/dist/mjs/lib/requestUtils.js +76 -0
- package/dist/mjs/lib/requestUtils.js.map +1 -0
- package/dist/mjs/lib/webCryptoAPI.d.ts +15 -0
- package/dist/mjs/lib/webCryptoAPI.js +80 -0
- package/dist/mjs/lib/webCryptoAPI.js.map +1 -0
- package/dist/mjs/lib/websocket/WsStore.d.ts +75 -0
- package/dist/mjs/lib/websocket/WsStore.js +298 -0
- package/dist/mjs/lib/websocket/WsStore.js.map +1 -0
- package/dist/mjs/lib/websocket/WsStore.types.d.ts +51 -0
- package/dist/mjs/lib/websocket/WsStore.types.js +11 -0
- package/dist/mjs/lib/websocket/WsStore.types.js.map +1 -0
- package/dist/mjs/lib/websocket/logger.d.ts +7 -0
- package/dist/mjs/lib/websocket/logger.js +13 -0
- package/dist/mjs/lib/websocket/logger.js.map +1 -0
- package/dist/mjs/lib/websocket/rest-client-cache.d.ts +22 -0
- package/dist/mjs/lib/websocket/rest-client-cache.js +62 -0
- package/dist/mjs/lib/websocket/rest-client-cache.js.map +1 -0
- package/dist/mjs/lib/websocket/websocket-util.d.ts +76 -0
- package/dist/mjs/lib/websocket/websocket-util.js +54 -0
- package/dist/mjs/lib/websocket/websocket-util.js.map +1 -0
- package/dist/mjs/package.json +3 -0
- package/dist/mjs/types/request/derivatives.types.d.ts +186 -0
- package/dist/mjs/types/request/derivatives.types.js +5 -0
- package/dist/mjs/types/request/derivatives.types.js.map +1 -0
- package/dist/mjs/types/request/institutional.types.d.ts +313 -0
- package/dist/mjs/types/request/institutional.types.js +3 -0
- package/dist/mjs/types/request/institutional.types.js.map +1 -0
- package/dist/mjs/types/request/partner.types.d.ts +291 -0
- package/dist/mjs/types/request/partner.types.js +3 -0
- package/dist/mjs/types/request/partner.types.js.map +1 -0
- package/dist/mjs/types/request/spot.types.d.ts +324 -0
- package/dist/mjs/types/request/spot.types.js +5 -0
- package/dist/mjs/types/request/spot.types.js.map +1 -0
- package/dist/mjs/types/request/wsapi.types.d.ts +90 -0
- package/dist/mjs/types/request/wsapi.types.js +5 -0
- package/dist/mjs/types/request/wsapi.types.js.map +1 -0
- package/dist/mjs/types/response/derivatives.types.d.ts +712 -0
- package/dist/mjs/types/response/derivatives.types.js +5 -0
- package/dist/mjs/types/response/derivatives.types.js.map +1 -0
- package/dist/mjs/types/response/institutional.types.d.ts +318 -0
- package/dist/mjs/types/response/institutional.types.js +3 -0
- package/dist/mjs/types/response/institutional.types.js.map +1 -0
- package/dist/mjs/types/response/partner.types.d.ts +332 -0
- package/dist/mjs/types/response/partner.types.js +3 -0
- package/dist/mjs/types/response/partner.types.js.map +1 -0
- package/dist/mjs/types/response/shared.types.d.ts +27 -0
- package/dist/mjs/types/response/shared.types.js +2 -0
- package/dist/mjs/types/response/shared.types.js.map +1 -0
- package/dist/mjs/types/response/spot.types.d.ts +590 -0
- package/dist/mjs/types/response/spot.types.js +5 -0
- package/dist/mjs/types/response/spot.types.js.map +1 -0
- package/dist/mjs/types/response/ws.d.ts +11 -0
- package/dist/mjs/types/response/ws.js +2 -0
- package/dist/mjs/types/response/ws.js.map +1 -0
- package/dist/mjs/types/response/wsapi.types.d.ts +44 -0
- package/dist/mjs/types/response/wsapi.types.js +5 -0
- package/dist/mjs/types/response/wsapi.types.js.map +1 -0
- package/dist/mjs/types/websockets/ws-api.d.ts +71 -0
- package/dist/mjs/types/websockets/ws-api.js +15 -0
- package/dist/mjs/types/websockets/ws-api.js.map +1 -0
- package/dist/mjs/types/websockets/ws-events.d.ts +11 -0
- package/dist/mjs/types/websockets/ws-events.js +8 -0
- package/dist/mjs/types/websockets/ws-events.js.map +1 -0
- package/dist/mjs/types/websockets/ws-general.d.ts +67 -0
- package/dist/mjs/types/websockets/ws-general.js +2 -0
- package/dist/mjs/types/websockets/ws-general.js.map +1 -0
- package/dist/mjs/types/websockets/ws-subscriptions.d.ts +11 -0
- package/dist/mjs/types/websockets/ws-subscriptions.js +28 -0
- package/dist/mjs/types/websockets/ws-subscriptions.js.map +1 -0
- package/llms.txt +10925 -0
- package/package.json +89 -0
|
@@ -0,0 +1,928 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import WebSocket from 'isomorphic-ws';
|
|
3
|
+
import { isMessageEvent, } from '../types/websockets/ws-events.js';
|
|
4
|
+
import { checkWebCryptoAPISupported } from './webCryptoAPI.js';
|
|
5
|
+
import { DefaultLogger } from './websocket/logger.js';
|
|
6
|
+
import { safeTerminateWs, } from './websocket/websocket-util.js';
|
|
7
|
+
import { WsStore } from './websocket/WsStore.js';
|
|
8
|
+
import { WsConnectionStateEnum, } from './websocket/WsStore.types.js';
|
|
9
|
+
/**
|
|
10
|
+
* Appends wsKey and isWSAPIResponse to all events.
|
|
11
|
+
* Some events are arrays, this handles that nested scenario too.
|
|
12
|
+
*/
|
|
13
|
+
function getFinalEmittable(emittable, wsKey, isWSAPIResponse) {
|
|
14
|
+
if (Array.isArray(emittable)) {
|
|
15
|
+
return emittable.map((subEvent) => getFinalEmittable(subEvent, wsKey, isWSAPIResponse));
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(emittable.event)) {
|
|
18
|
+
// Some topics just emit an array.
|
|
19
|
+
// This is consistent with how it was before the WS API upgrade:
|
|
20
|
+
return emittable.event.map((subEvent) => getFinalEmittable(subEvent, wsKey, isWSAPIResponse));
|
|
21
|
+
// const { event, ...others } = emittable;
|
|
22
|
+
// return {
|
|
23
|
+
// ...others,
|
|
24
|
+
// event: event.map((subEvent) =>
|
|
25
|
+
// getFinalEmittable(subEvent, wsKey, isWSAPIResponse),
|
|
26
|
+
// ),
|
|
27
|
+
// };
|
|
28
|
+
}
|
|
29
|
+
if (emittable.event) {
|
|
30
|
+
return {
|
|
31
|
+
...emittable.event,
|
|
32
|
+
wsKey: wsKey,
|
|
33
|
+
isWSAPIResponse: !!isWSAPIResponse,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
...emittable,
|
|
38
|
+
wsKey: wsKey,
|
|
39
|
+
isWSAPIResponse: !!isWSAPIResponse,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Users can conveniently pass topics as strings or objects (object has topic name + optional params).
|
|
44
|
+
*
|
|
45
|
+
* This method normalises topics into objects (object has topic name + optional params).
|
|
46
|
+
*/
|
|
47
|
+
function getNormalisedTopicRequests(wsTopicRequests) {
|
|
48
|
+
const normalisedTopicRequests = [];
|
|
49
|
+
for (const wsTopicRequest of wsTopicRequests) {
|
|
50
|
+
// passed as string, convert to object
|
|
51
|
+
if (typeof wsTopicRequest === 'string') {
|
|
52
|
+
const topicRequest = {
|
|
53
|
+
topic: wsTopicRequest,
|
|
54
|
+
payload: undefined,
|
|
55
|
+
};
|
|
56
|
+
normalisedTopicRequests.push(topicRequest);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
// already a normalised object, thanks to user
|
|
60
|
+
normalisedTopicRequests.push(wsTopicRequest);
|
|
61
|
+
}
|
|
62
|
+
return normalisedTopicRequests;
|
|
63
|
+
}
|
|
64
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
|
|
65
|
+
export class BaseWebsocketClient extends EventEmitter {
|
|
66
|
+
wsStore;
|
|
67
|
+
logger;
|
|
68
|
+
options;
|
|
69
|
+
wsApiRequestId = 0;
|
|
70
|
+
timeOffsetMs = 0;
|
|
71
|
+
WS_LOGGER_CATEGORY;
|
|
72
|
+
constructor(options, logger) {
|
|
73
|
+
super();
|
|
74
|
+
this.logger = logger || DefaultLogger;
|
|
75
|
+
this.WS_LOGGER_CATEGORY = {
|
|
76
|
+
category: options?.wsLoggerCategory || 'WsClient',
|
|
77
|
+
};
|
|
78
|
+
this.wsStore = new WsStore(this.logger);
|
|
79
|
+
this.options = {
|
|
80
|
+
pongTimeout: 5000,
|
|
81
|
+
pingInterval: 10000,
|
|
82
|
+
reconnectTimeout: 500,
|
|
83
|
+
recvWindow: 5000,
|
|
84
|
+
// Requires a confirmation "response" from the ws connection before assuming it is ready
|
|
85
|
+
requireConnectionReadyConfirmation: false,
|
|
86
|
+
// Automatically auth after opening a connection?
|
|
87
|
+
authPrivateConnectionsOnConnect: false,
|
|
88
|
+
// Automatically include auth/sign/token with every WS request.
|
|
89
|
+
// Automatically handled during getWsRequestEvents.
|
|
90
|
+
authPrivateRequests: true,
|
|
91
|
+
// Automatically re-auth WS API, if we were auth'd before and get reconnected
|
|
92
|
+
reauthWSAPIOnReconnect: true,
|
|
93
|
+
// Whether to use native heartbeats (depends on the exchange)
|
|
94
|
+
useNativeHeartbeats: true,
|
|
95
|
+
...options,
|
|
96
|
+
};
|
|
97
|
+
// Check Web Crypto API support when credentials are provided and no custom sign function is used
|
|
98
|
+
if (this.options.apiKey &&
|
|
99
|
+
this.options.apiSecret &&
|
|
100
|
+
!this.options.customSignMessageFn) {
|
|
101
|
+
checkWebCryptoAPISupported();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
isPrivateWsKey(wsKey) {
|
|
105
|
+
return this.getPrivateWSKeys().includes(wsKey);
|
|
106
|
+
}
|
|
107
|
+
/** Returns auto-incrementing request ID, used to track promise references for async requests */
|
|
108
|
+
getNewRequestId() {
|
|
109
|
+
return ++this.wsApiRequestId;
|
|
110
|
+
}
|
|
111
|
+
getTimeOffsetMs() {
|
|
112
|
+
return this.timeOffsetMs;
|
|
113
|
+
}
|
|
114
|
+
setTimeOffsetMs(newOffset) {
|
|
115
|
+
this.timeOffsetMs = newOffset;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Don't call directly! Use subscribe() instead!
|
|
119
|
+
*
|
|
120
|
+
* Subscribe to one or more topics on a WS connection (identified by WS Key).
|
|
121
|
+
*
|
|
122
|
+
* - Topics are automatically cached
|
|
123
|
+
* - Connections are automatically opened, if not yet connected
|
|
124
|
+
* - Authentication is automatically handled
|
|
125
|
+
* - Topics are automatically resubscribed to, if something happens to the connection, unless you call unsubsribeTopicsForWsKey(topics, key).
|
|
126
|
+
*
|
|
127
|
+
* @param wsTopicRequests array of topics to subscribe to
|
|
128
|
+
* @param wsKey ws key referring to the ws connection these topics should be subscribed on
|
|
129
|
+
*/
|
|
130
|
+
async subscribeTopicsForWsKey(wsTopicRequests, wsKey) {
|
|
131
|
+
const normalisedTopicRequests = getNormalisedTopicRequests(wsTopicRequests);
|
|
132
|
+
// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically
|
|
133
|
+
for (const topic of normalisedTopicRequests) {
|
|
134
|
+
this.wsStore.addTopic(wsKey, topic);
|
|
135
|
+
}
|
|
136
|
+
const isConnected = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
137
|
+
const isConnectionInProgress = this.wsStore.isConnectionAttemptInProgress(wsKey);
|
|
138
|
+
// start connection process if it hasn't yet begun. Topics are automatically subscribed to on-connect
|
|
139
|
+
if (!isConnected && !isConnectionInProgress) {
|
|
140
|
+
return this.connect(wsKey);
|
|
141
|
+
}
|
|
142
|
+
// Subscribe should happen automatically once connected, nothing to do here after topics are added to wsStore.
|
|
143
|
+
if (!isConnected) {
|
|
144
|
+
/**
|
|
145
|
+
* Are we in the process of connection? Nothing to send yet.
|
|
146
|
+
*/
|
|
147
|
+
this.logger.trace('WS not connected - requests queued for retry once connected.', {
|
|
148
|
+
...this.WS_LOGGER_CATEGORY,
|
|
149
|
+
wsKey,
|
|
150
|
+
wsTopicRequests,
|
|
151
|
+
});
|
|
152
|
+
return isConnectionInProgress;
|
|
153
|
+
}
|
|
154
|
+
// We're connected. Check if auth is needed and if already authenticated
|
|
155
|
+
const isPrivateConnection = this.isPrivateWsKey(wsKey);
|
|
156
|
+
const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated;
|
|
157
|
+
if (isPrivateConnection && !isAuthenticated) {
|
|
158
|
+
/**
|
|
159
|
+
* If not authenticated yet and auth is required, don't request topics yet.
|
|
160
|
+
*
|
|
161
|
+
* Auth should already automatically be in progress, so no action needed from here. Topics will automatically subscribe post-auth success.
|
|
162
|
+
*/
|
|
163
|
+
this.logger.trace('WS connected but not authenticated yet - awaiting auth before subscribing');
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
// Finally, request subscription to topics if the connection is healthy and ready
|
|
167
|
+
return this.requestSubscribeTopics(wsKey, normalisedTopicRequests);
|
|
168
|
+
}
|
|
169
|
+
async unsubscribeTopicsForWsKey(wsTopicRequests, wsKey) {
|
|
170
|
+
const normalisedTopicRequests = getNormalisedTopicRequests(wsTopicRequests);
|
|
171
|
+
// Store topics, so future automation (post-auth, post-reconnect) has everything needed to resubscribe automatically
|
|
172
|
+
for (const topic of normalisedTopicRequests) {
|
|
173
|
+
this.wsStore.deleteTopic(wsKey, topic);
|
|
174
|
+
}
|
|
175
|
+
const isConnected = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
176
|
+
// If not connected, don't need to do anything.
|
|
177
|
+
// Removing the topic from the store is enough to stop it from being resubscribed to on reconnect.
|
|
178
|
+
if (!isConnected) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
// We're connected. Check if auth is needed and if already authenticated
|
|
182
|
+
const isPrivateConnection = this.isPrivateWsKey(wsKey);
|
|
183
|
+
const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated;
|
|
184
|
+
if (isPrivateConnection && !isAuthenticated) {
|
|
185
|
+
/**
|
|
186
|
+
* If not authenticated yet and auth is required, don't need to do anything.
|
|
187
|
+
* We don't subscribe to topics until auth is complete anyway.
|
|
188
|
+
*/
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Finally, request subscription to topics if the connection is healthy and ready
|
|
192
|
+
return this.requestUnsubscribeTopics(wsKey, normalisedTopicRequests);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Splits topic requests into two groups, public & private topic requests
|
|
196
|
+
*/
|
|
197
|
+
sortTopicRequestsIntoPublicPrivate(wsTopicRequests, wsKey) {
|
|
198
|
+
const publicTopicRequests = [];
|
|
199
|
+
const privateTopicRequests = [];
|
|
200
|
+
for (const topic of wsTopicRequests) {
|
|
201
|
+
if (this.isPrivateTopicRequest(topic, wsKey)) {
|
|
202
|
+
privateTopicRequests.push(topic);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
publicTopicRequests.push(topic);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
publicReqs: publicTopicRequests,
|
|
210
|
+
privateReqs: privateTopicRequests,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
/** Get the WsStore that tracks websockets & topics */
|
|
214
|
+
getWsStore() {
|
|
215
|
+
return this.wsStore;
|
|
216
|
+
}
|
|
217
|
+
close(wsKey, force) {
|
|
218
|
+
this.logger.info('Closing connection', {
|
|
219
|
+
...this.WS_LOGGER_CATEGORY,
|
|
220
|
+
wsKey,
|
|
221
|
+
});
|
|
222
|
+
this.setWsState(wsKey, WsConnectionStateEnum.CLOSING);
|
|
223
|
+
this.clearTimers(wsKey);
|
|
224
|
+
const ws = this.getWs(wsKey);
|
|
225
|
+
ws?.close();
|
|
226
|
+
if (force) {
|
|
227
|
+
safeTerminateWs(ws, false);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
closeAll(force) {
|
|
231
|
+
const keys = this.wsStore.getKeys();
|
|
232
|
+
this.logger.info(`Closing all ws connections: ${keys}`);
|
|
233
|
+
keys.forEach((key) => {
|
|
234
|
+
this.close(key, force);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
isConnected(wsKey) {
|
|
238
|
+
return this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Request connection to a specific websocket, instead of waiting for automatic connection.
|
|
242
|
+
*/
|
|
243
|
+
async connect(wsKey, customUrl, throwOnError) {
|
|
244
|
+
try {
|
|
245
|
+
if (this.wsStore.isWsOpen(wsKey)) {
|
|
246
|
+
this.logger.error('Refused to connect to ws with existing active connection', { ...this.WS_LOGGER_CATEGORY, wsKey });
|
|
247
|
+
return { wsKey, ws: this.wsStore.getWs(wsKey) };
|
|
248
|
+
}
|
|
249
|
+
if (
|
|
250
|
+
// Important: don't check for RECONNECTING here, or this clashes with reconnectWithDelay()!
|
|
251
|
+
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING)) {
|
|
252
|
+
this.logger.error('Refused to connect to ws, connection attempt already active', { ...this.WS_LOGGER_CATEGORY, wsKey });
|
|
253
|
+
return this.wsStore.getConnectionInProgressPromise(wsKey)?.promise;
|
|
254
|
+
}
|
|
255
|
+
if (!this.wsStore.getConnectionState(wsKey) ||
|
|
256
|
+
this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.INITIAL)) {
|
|
257
|
+
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTING);
|
|
258
|
+
}
|
|
259
|
+
if (!this.wsStore.getConnectionInProgressPromise(wsKey)) {
|
|
260
|
+
this.wsStore.createConnectionInProgressPromise(wsKey, false);
|
|
261
|
+
}
|
|
262
|
+
try {
|
|
263
|
+
const url = customUrl || (await this.getWsUrl(wsKey));
|
|
264
|
+
const ws = this.connectToWsUrl(url, wsKey);
|
|
265
|
+
this.wsStore.setWs(wsKey, ws);
|
|
266
|
+
}
|
|
267
|
+
catch (e) {
|
|
268
|
+
this.logger.error('Exception fetching WS URL', e);
|
|
269
|
+
throw e;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
const canRetry = this.parseWsError('Connection failed', err, wsKey);
|
|
274
|
+
if (canRetry) {
|
|
275
|
+
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
this.logger.info('Preventing retry due to error type to prevent deadlock');
|
|
279
|
+
}
|
|
280
|
+
if (throwOnError) {
|
|
281
|
+
throw err;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return this.wsStore.getConnectionInProgressPromise(wsKey)?.promise;
|
|
285
|
+
}
|
|
286
|
+
connectToWsUrl(url, wsKey) {
|
|
287
|
+
this.logger.trace(`Opening WS connection to URL: ${url}`, {
|
|
288
|
+
...this.WS_LOGGER_CATEGORY,
|
|
289
|
+
wsKey,
|
|
290
|
+
});
|
|
291
|
+
const { protocols = [], ...wsOptions } = this.options.wsOptions || {};
|
|
292
|
+
const ws = new WebSocket(url, protocols, wsOptions);
|
|
293
|
+
ws.onopen = (event) => this.onWsOpen(event, wsKey, url, ws);
|
|
294
|
+
ws.onmessage = (event) => this.onWsMessage(event, wsKey, ws);
|
|
295
|
+
ws.onerror = (event) => this.parseWsError('WebSocket onWsError', event, wsKey);
|
|
296
|
+
ws.onclose = (event) => this.onWsClose(event, wsKey);
|
|
297
|
+
//
|
|
298
|
+
if (this.options.useNativeHeartbeats) {
|
|
299
|
+
if (typeof ws.on === 'function') {
|
|
300
|
+
ws.on('ping', (event) => this.onWsPing(event, wsKey, ws, 'frame'));
|
|
301
|
+
ws.on('pong', (event) => this.onWsPong(event, wsKey, 'frame'));
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
ws.wsKey = wsKey;
|
|
305
|
+
return ws;
|
|
306
|
+
}
|
|
307
|
+
parseWsError(context, error, wsKey) {
|
|
308
|
+
if (this.wsStore.isConnectionAttemptInProgress(wsKey)) {
|
|
309
|
+
this.setWsState(wsKey, WsConnectionStateEnum.ERROR);
|
|
310
|
+
}
|
|
311
|
+
// Allow retry by default (in some places that call this). Prevent deadloop in hard failure (401)
|
|
312
|
+
let canRetry = true;
|
|
313
|
+
if (!error.message) {
|
|
314
|
+
this.logger.error(`${context} due to unexpected error: `, error);
|
|
315
|
+
this.emit('response', { ...error, wsKey });
|
|
316
|
+
this.emit('exception', { ...error, wsKey });
|
|
317
|
+
return canRetry;
|
|
318
|
+
}
|
|
319
|
+
switch (error.message) {
|
|
320
|
+
case 'Unexpected server response: 401':
|
|
321
|
+
this.logger.error(`${context} due to 401 authorization failure.`, {
|
|
322
|
+
...this.WS_LOGGER_CATEGORY,
|
|
323
|
+
wsKey,
|
|
324
|
+
});
|
|
325
|
+
canRetry = false;
|
|
326
|
+
break;
|
|
327
|
+
default:
|
|
328
|
+
if (error?.code === 'ENOTFOUND') {
|
|
329
|
+
this.logger.error(`${context} due to lookup exception: "${error?.msg || error?.message || error}"`, {
|
|
330
|
+
...this.WS_LOGGER_CATEGORY,
|
|
331
|
+
wsKey,
|
|
332
|
+
error,
|
|
333
|
+
connectionState: this.wsStore.getConnectionState(wsKey),
|
|
334
|
+
});
|
|
335
|
+
canRetry = true;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
if (error?.code === 401) {
|
|
339
|
+
this.logger.error(`${context} due to 401 authorization failure.`, {
|
|
340
|
+
...this.WS_LOGGER_CATEGORY,
|
|
341
|
+
wsKey,
|
|
342
|
+
});
|
|
343
|
+
canRetry = false;
|
|
344
|
+
break;
|
|
345
|
+
}
|
|
346
|
+
if (this.wsStore.getConnectionState(wsKey) !==
|
|
347
|
+
WsConnectionStateEnum.CLOSING) {
|
|
348
|
+
this.logger.error(`${context} due to unexpected response error: "${error?.msg || error?.message || error}"`, { ...this.WS_LOGGER_CATEGORY, wsKey, error });
|
|
349
|
+
canRetry = true;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
this.logger.info(`${wsKey} socket forcefully closed. Will not reconnect.`);
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
this.emit('response', { ...error, wsKey });
|
|
357
|
+
this.emit('exception', { ...error, wsKey });
|
|
358
|
+
return canRetry;
|
|
359
|
+
}
|
|
360
|
+
/** Get a signature, build the auth request and send it */
|
|
361
|
+
async sendAuthRequest(wsKey, eventToAuth) {
|
|
362
|
+
try {
|
|
363
|
+
this.logger.trace('Sending auth request...', {
|
|
364
|
+
...this.WS_LOGGER_CATEGORY,
|
|
365
|
+
wsKey,
|
|
366
|
+
eventToAuth,
|
|
367
|
+
});
|
|
368
|
+
await this.assertIsConnected(wsKey);
|
|
369
|
+
// If not required, this won't return anything
|
|
370
|
+
const request = await this.getWsAuthRequestEvent(wsKey, eventToAuth);
|
|
371
|
+
if (!request) {
|
|
372
|
+
// Short-circuit this for the next time it's called
|
|
373
|
+
const wsState = this.wsStore.get(wsKey, true);
|
|
374
|
+
wsState.isAuthenticated = true;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (!this.wsStore.getAuthenticationInProgressPromise(wsKey)) {
|
|
378
|
+
this.wsStore.createAuthenticationInProgressPromise(wsKey, false);
|
|
379
|
+
}
|
|
380
|
+
if (request === 'waitForEvent') {
|
|
381
|
+
return this.wsStore.getAuthenticationInProgressPromise(wsKey)?.promise;
|
|
382
|
+
}
|
|
383
|
+
this.tryWsSend(wsKey, typeof request === 'string' ? request : JSON.stringify(request));
|
|
384
|
+
return this.wsStore.getAuthenticationInProgressPromise(wsKey)?.promise;
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
this.logger.trace(e, { ...this.WS_LOGGER_CATEGORY, wsKey });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
reconnectWithDelay(wsKey, connectionDelayMs) {
|
|
391
|
+
this.clearTimers(wsKey);
|
|
392
|
+
if (!this.wsStore.isConnectionAttemptInProgress(wsKey)) {
|
|
393
|
+
this.setWsState(wsKey, WsConnectionStateEnum.RECONNECTING);
|
|
394
|
+
}
|
|
395
|
+
this.logger.info('Reconnecting to websocket with delay...', {
|
|
396
|
+
...this.WS_LOGGER_CATEGORY,
|
|
397
|
+
wsKey,
|
|
398
|
+
connectionDelayMs,
|
|
399
|
+
});
|
|
400
|
+
if (this.wsStore.get(wsKey)?.activeReconnectTimer) {
|
|
401
|
+
this.clearReconnectTimer(wsKey);
|
|
402
|
+
}
|
|
403
|
+
this.wsStore.get(wsKey, true).activeReconnectTimer = setTimeout(() => {
|
|
404
|
+
this.clearReconnectTimer(wsKey);
|
|
405
|
+
// Some streams need a specialist reconnection workflow.
|
|
406
|
+
// E.g. the user data stream can't just be reconnected as is.
|
|
407
|
+
if (this.isCustomReconnectionNeeded(wsKey)) {
|
|
408
|
+
this.wsStore.delete(wsKey);
|
|
409
|
+
return this.triggerCustomReconnectionWorkflow(wsKey);
|
|
410
|
+
}
|
|
411
|
+
this.logger.info('Reconnecting to websocket', {
|
|
412
|
+
...this.WS_LOGGER_CATEGORY,
|
|
413
|
+
wsKey,
|
|
414
|
+
});
|
|
415
|
+
this.connect(wsKey);
|
|
416
|
+
}, connectionDelayMs);
|
|
417
|
+
}
|
|
418
|
+
ping(wsKey) {
|
|
419
|
+
if (this.wsStore.get(wsKey, true).activePongTimer) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.clearPongTimer(wsKey);
|
|
423
|
+
this.logger.trace('Sending ping', { ...this.WS_LOGGER_CATEGORY, wsKey });
|
|
424
|
+
const ws = this.wsStore.get(wsKey, true).ws;
|
|
425
|
+
if (!ws) {
|
|
426
|
+
this.logger.error(`Unable to send ping for wsKey "${wsKey}" - no connection found`);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
this.sendPingEvent(wsKey, ws);
|
|
430
|
+
this.wsStore.get(wsKey, true).activePongTimer = setTimeout(() => this.executeReconnectableClose(wsKey, 'Pong timeout'), this.options.pongTimeout);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
|
|
434
|
+
* If closed, trigger a reconnect immediately
|
|
435
|
+
*/
|
|
436
|
+
executeReconnectableClose(wsKey, reason) {
|
|
437
|
+
this.logger.info(`${reason} - closing socket to reconnect`, {
|
|
438
|
+
...this.WS_LOGGER_CATEGORY,
|
|
439
|
+
wsKey,
|
|
440
|
+
reason,
|
|
441
|
+
});
|
|
442
|
+
const wasOpen = this.wsStore.isWsOpen(wsKey);
|
|
443
|
+
this.clearPingTimer(wsKey);
|
|
444
|
+
this.clearPongTimer(wsKey);
|
|
445
|
+
const ws = this.getWs(wsKey);
|
|
446
|
+
if (ws) {
|
|
447
|
+
ws.close();
|
|
448
|
+
safeTerminateWs(ws, false);
|
|
449
|
+
}
|
|
450
|
+
if (!wasOpen) {
|
|
451
|
+
this.logger.info(`${reason} - socket already closed - trigger immediate reconnect`, {
|
|
452
|
+
...this.WS_LOGGER_CATEGORY,
|
|
453
|
+
wsKey,
|
|
454
|
+
reason,
|
|
455
|
+
});
|
|
456
|
+
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
clearTimers(wsKey) {
|
|
460
|
+
this.clearPingTimer(wsKey);
|
|
461
|
+
this.clearPongTimer(wsKey);
|
|
462
|
+
this.clearReconnectTimer(wsKey);
|
|
463
|
+
}
|
|
464
|
+
// Send a ping at intervals
|
|
465
|
+
clearPingTimer(wsKey) {
|
|
466
|
+
const wsState = this.wsStore.get(wsKey);
|
|
467
|
+
if (wsState?.activePingTimer) {
|
|
468
|
+
clearInterval(wsState.activePingTimer);
|
|
469
|
+
wsState.activePingTimer = undefined;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
// Expect a pong within a time limit
|
|
473
|
+
clearPongTimer(wsKey) {
|
|
474
|
+
const wsState = this.wsStore.get(wsKey);
|
|
475
|
+
if (wsState?.activePongTimer) {
|
|
476
|
+
clearTimeout(wsState.activePongTimer);
|
|
477
|
+
wsState.activePongTimer = undefined;
|
|
478
|
+
// this.logger.trace(`Cleared pong timeout for "${wsKey}"`);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
// this.logger.trace(`No active pong timer for "${wsKey}"`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
clearReconnectTimer(wsKey) {
|
|
485
|
+
const wsState = this.wsStore.get(wsKey);
|
|
486
|
+
if (wsState?.activeReconnectTimer) {
|
|
487
|
+
clearTimeout(wsState.activeReconnectTimer);
|
|
488
|
+
wsState.activeReconnectTimer = undefined;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Returns a list of string events that can be individually sent upstream to complete subscribing/unsubscribing/etc to these topics
|
|
493
|
+
*
|
|
494
|
+
* If events are an object, these should be stringified (`return JSON.stringify(event);`)
|
|
495
|
+
* Each event returned by this will be sent one at a time
|
|
496
|
+
*
|
|
497
|
+
* Events are automatically split into smaller batches, by this method, if needed.
|
|
498
|
+
*/
|
|
499
|
+
async getWsOperationEventsForTopics(topics, wsKey, operation) {
|
|
500
|
+
if (!topics.length) {
|
|
501
|
+
return [];
|
|
502
|
+
}
|
|
503
|
+
// Events that are ready to send (usually stringified JSON)
|
|
504
|
+
const requestEvents = [];
|
|
505
|
+
const maxTopicsPerEvent = this.getMaxTopicsPerSubscribeEvent(wsKey);
|
|
506
|
+
if (maxTopicsPerEvent &&
|
|
507
|
+
maxTopicsPerEvent !== null &&
|
|
508
|
+
topics.length > maxTopicsPerEvent) {
|
|
509
|
+
for (let i = 0; i < topics.length; i += maxTopicsPerEvent) {
|
|
510
|
+
const batch = topics.slice(i, i + maxTopicsPerEvent);
|
|
511
|
+
const subscribeRequestEvents = await this.getWsRequestEvents(wsKey, operation, batch);
|
|
512
|
+
requestEvents.push(...subscribeRequestEvents);
|
|
513
|
+
}
|
|
514
|
+
return requestEvents;
|
|
515
|
+
}
|
|
516
|
+
const subscribeRequestEvents = await this.getWsRequestEvents(wsKey, operation, topics);
|
|
517
|
+
return subscribeRequestEvents;
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* Simply builds and sends subscribe events for a list of topics for a ws key
|
|
521
|
+
*
|
|
522
|
+
* @private Use the `subscribe(topics)` or `subscribeTopicsForWsKey(topics, wsKey)` method to subscribe to topics. Send WS message to subscribe to topics.
|
|
523
|
+
*/
|
|
524
|
+
async requestSubscribeTopics(wsKey, wsTopicRequests) {
|
|
525
|
+
if (!wsTopicRequests.length) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
// Automatically splits requests into smaller batches, if needed
|
|
529
|
+
const subscribeWsMessages = await this.getWsOperationEventsForTopics(wsTopicRequests, wsKey, 'subscribe');
|
|
530
|
+
this.logger.trace(`Subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
|
|
531
|
+
for (const midflightRequest of subscribeWsMessages) {
|
|
532
|
+
const wsMessage = midflightRequest.requestEvent;
|
|
533
|
+
this.logger.trace(`Sending batch via message: "${JSON.stringify(wsMessage)}", cached with key "${midflightRequest.requestKey}"`);
|
|
534
|
+
try {
|
|
535
|
+
this.tryWsSend(wsKey, JSON.stringify(wsMessage), true);
|
|
536
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
537
|
+
}
|
|
538
|
+
catch (e) {
|
|
539
|
+
throw e;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
this.logger.trace(`Finished subscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Simply builds and sends unsubscribe events for a list of topics for a ws key
|
|
546
|
+
*
|
|
547
|
+
* @private Use the `unsubscribe(topics)` method to unsubscribe from topics. Send WS message to unsubscribe from topics.
|
|
548
|
+
*/
|
|
549
|
+
async requestUnsubscribeTopics(wsKey, wsTopicRequests) {
|
|
550
|
+
if (!wsTopicRequests.length) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const subscribeWsMessages = await this.getWsOperationEventsForTopics(wsTopicRequests, wsKey, 'unsubscribe');
|
|
554
|
+
this.logger.trace(`Unsubscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches. Events: "${JSON.stringify(wsTopicRequests)}"`);
|
|
555
|
+
for (const midflightRequest of subscribeWsMessages) {
|
|
556
|
+
const wsMessage = midflightRequest.requestEvent;
|
|
557
|
+
this.logger.trace(`Sending batch via message: "${wsMessage}"`);
|
|
558
|
+
try {
|
|
559
|
+
this.tryWsSend(wsKey, JSON.stringify(wsMessage));
|
|
560
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
561
|
+
}
|
|
562
|
+
catch (e) {
|
|
563
|
+
throw e;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
this.logger.trace(`Finished unsubscribing to ${wsTopicRequests.length} "${wsKey}" topics in ${subscribeWsMessages.length} batches.`);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Try sending a string event on a WS connection (identified by the WS Key)
|
|
570
|
+
*/
|
|
571
|
+
tryWsSend(wsKey, wsMessage, throwExceptions) {
|
|
572
|
+
try {
|
|
573
|
+
this.logger.trace('Sending upstream ws message: ', {
|
|
574
|
+
...this.WS_LOGGER_CATEGORY,
|
|
575
|
+
wsMessage,
|
|
576
|
+
wsKey,
|
|
577
|
+
});
|
|
578
|
+
if (!wsKey) {
|
|
579
|
+
throw new Error('Cannot send message due to no known websocket for this wsKey');
|
|
580
|
+
}
|
|
581
|
+
const ws = this.getWs(wsKey);
|
|
582
|
+
if (!ws) {
|
|
583
|
+
throw new Error(`${wsKey} socket not connected yet, call "connectAll()" first then try again when the "open" event arrives`);
|
|
584
|
+
}
|
|
585
|
+
ws.send(wsMessage);
|
|
586
|
+
}
|
|
587
|
+
catch (e) {
|
|
588
|
+
this.logger.error('Failed to send WS message', {
|
|
589
|
+
...this.WS_LOGGER_CATEGORY,
|
|
590
|
+
wsMessage,
|
|
591
|
+
wsKey,
|
|
592
|
+
exception: e,
|
|
593
|
+
});
|
|
594
|
+
if (throwExceptions) {
|
|
595
|
+
throw e;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
async onWsOpen(event, wsKey, url, ws) {
|
|
600
|
+
const didReconnectSuccessfully = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.RECONNECTING) || this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.ERROR);
|
|
601
|
+
const isFreshConnectionAttempt = this.wsStore.isConnectionState(wsKey, WsConnectionStateEnum.CONNECTING);
|
|
602
|
+
if (isFreshConnectionAttempt) {
|
|
603
|
+
this.logger.info('Websocket connected', {
|
|
604
|
+
...this.WS_LOGGER_CATEGORY,
|
|
605
|
+
wsKey,
|
|
606
|
+
});
|
|
607
|
+
this.emit('open', { wsKey, event, wsUrl: url, ws });
|
|
608
|
+
}
|
|
609
|
+
else {
|
|
610
|
+
this.logger.info('Websocket reconnected', {
|
|
611
|
+
...this.WS_LOGGER_CATEGORY,
|
|
612
|
+
wsKey,
|
|
613
|
+
didReconnectSuccessfully,
|
|
614
|
+
});
|
|
615
|
+
this.emit('reconnected', {
|
|
616
|
+
wsKey,
|
|
617
|
+
event,
|
|
618
|
+
wsUrl: url,
|
|
619
|
+
ws,
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
this.setWsState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
623
|
+
this.logger.trace('Enabled ping timer', {
|
|
624
|
+
...this.WS_LOGGER_CATEGORY,
|
|
625
|
+
wsKey,
|
|
626
|
+
});
|
|
627
|
+
this.wsStore.get(wsKey, true).activePingTimer = setInterval(() => this.ping(wsKey), this.options.pingInterval);
|
|
628
|
+
if (!this.options.requireConnectionReadyConfirmation) {
|
|
629
|
+
return await this.onWsReadyForEvents(wsKey);
|
|
630
|
+
}
|
|
631
|
+
else {
|
|
632
|
+
this.resolveConnectionInProgressPromise(wsKey);
|
|
633
|
+
}
|
|
634
|
+
this.wsStore.removeConnectingInProgressPromise(wsKey);
|
|
635
|
+
}
|
|
636
|
+
resolveConnectionInProgressPromise(wsKey) {
|
|
637
|
+
try {
|
|
638
|
+
// Resolve & cleanup deferred "connection attempt in progress" promise
|
|
639
|
+
const connectionInProgressPromise = this.wsStore.getConnectionInProgressPromise(wsKey);
|
|
640
|
+
if (connectionInProgressPromise?.resolve) {
|
|
641
|
+
connectionInProgressPromise.resolve({
|
|
642
|
+
wsKey,
|
|
643
|
+
ws: this.wsStore.getWs(wsKey),
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (e) {
|
|
648
|
+
this.logger.error('Exception trying to resolve "connectionInProgress" promise');
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* Called automatically once a connection is ready.
|
|
653
|
+
* - Some exchanges are ready immediately after the connections open.
|
|
654
|
+
* - Some exchanges send an event to confirm the connection is ready for us.
|
|
655
|
+
*
|
|
656
|
+
* This method is called to act when the connection is ready. Use `requireConnectionReadyConfirmation` to control how this is called.
|
|
657
|
+
*/
|
|
658
|
+
async onWsReadyForEvents(wsKey) {
|
|
659
|
+
this.resolveConnectionInProgressPromise(wsKey);
|
|
660
|
+
this.wsStore.removeConnectingInProgressPromise(wsKey);
|
|
661
|
+
// Some websockets require an auth packet to be sent after opening the connection
|
|
662
|
+
if (this.authPrivateConnectionsOnConnect(wsKey)) {
|
|
663
|
+
await this.assertIsAuthenticated(wsKey);
|
|
664
|
+
}
|
|
665
|
+
// Reconnect to topics known before it connected
|
|
666
|
+
const { privateReqs, publicReqs } = this.sortTopicRequestsIntoPublicPrivate([...this.wsStore.getTopics(wsKey)], wsKey);
|
|
667
|
+
// Request sub to public topics, if any
|
|
668
|
+
try {
|
|
669
|
+
await this.requestSubscribeTopics(wsKey, publicReqs);
|
|
670
|
+
}
|
|
671
|
+
catch (e) {
|
|
672
|
+
this.logger.error(`onWsOpen(): exception in public requestSubscribeTopics(${wsKey}): `, publicReqs, e);
|
|
673
|
+
}
|
|
674
|
+
// Request sub to private topics, if auth on connect isn't needed
|
|
675
|
+
if (!this.authPrivateConnectionsOnConnect(wsKey)) {
|
|
676
|
+
try {
|
|
677
|
+
this.requestSubscribeTopics(wsKey, privateReqs);
|
|
678
|
+
}
|
|
679
|
+
catch (e) {
|
|
680
|
+
this.logger.error(`onWsOpen(): exception in private requestSubscribeTopics(${wsKey}: `, privateReqs, e);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Handle subscription to private topics _after_ authentication successfully completes asynchronously.
|
|
686
|
+
*
|
|
687
|
+
* Only used for exchanges that require auth before sending private topic subscription requests
|
|
688
|
+
*/
|
|
689
|
+
onWsAuthenticated(wsKey, event) {
|
|
690
|
+
const wsState = this.wsStore.get(wsKey, true);
|
|
691
|
+
wsState.isAuthenticated = true;
|
|
692
|
+
// Resolve & cleanup deferred "auth attempt in progress" promise
|
|
693
|
+
try {
|
|
694
|
+
const inProgressPromise = this.wsStore.getAuthenticationInProgressPromise(wsKey);
|
|
695
|
+
if (inProgressPromise?.resolve) {
|
|
696
|
+
inProgressPromise.resolve({
|
|
697
|
+
wsKey,
|
|
698
|
+
event,
|
|
699
|
+
ws: wsState.ws,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
catch (e) {
|
|
704
|
+
this.logger.error('Exception trying to resolve "authenticationInProgress" promise', e);
|
|
705
|
+
}
|
|
706
|
+
// Remove before continuing, in case there's more requests queued
|
|
707
|
+
this.wsStore.removeAuthenticationInProgressPromise(wsKey);
|
|
708
|
+
if (this.authPrivateConnectionsOnConnect(wsKey)) {
|
|
709
|
+
const topics = [...this.wsStore.getTopics(wsKey)];
|
|
710
|
+
const privateTopics = topics.filter((topic) => this.isPrivateTopicRequest(topic, wsKey));
|
|
711
|
+
if (privateTopics.length) {
|
|
712
|
+
this.subscribeTopicsForWsKey(privateTopics, wsKey);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (event?.isWSAPI) {
|
|
716
|
+
wsState.didAuthWSAPI = true;
|
|
717
|
+
if (event?.WSAPIAuthChannel) {
|
|
718
|
+
wsState.WSAPIAuthChannel = event.WSAPIAuthChannel;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
onWsPing(event, wsKey, ws, source) {
|
|
723
|
+
this.logger.trace(`Received PING ${source}`, {
|
|
724
|
+
...this.WS_LOGGER_CATEGORY,
|
|
725
|
+
wsKey,
|
|
726
|
+
event,
|
|
727
|
+
source,
|
|
728
|
+
});
|
|
729
|
+
this.sendPongEvent(wsKey, ws);
|
|
730
|
+
}
|
|
731
|
+
onWsPong(event, wsKey, source) {
|
|
732
|
+
this.logger.trace(`Received PONG ${source}`, {
|
|
733
|
+
...this.WS_LOGGER_CATEGORY,
|
|
734
|
+
wsKey,
|
|
735
|
+
event: event?.data,
|
|
736
|
+
source,
|
|
737
|
+
});
|
|
738
|
+
// Necessary when native heartbeats are used
|
|
739
|
+
this.clearPongTimer(wsKey);
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
onWsMessage(event, wsKey, ws) {
|
|
743
|
+
try {
|
|
744
|
+
// console.log('onMessageRaw: ', (event as any).data);
|
|
745
|
+
// any message can clear the pong timer - wouldn't get a message if the ws wasn't working
|
|
746
|
+
this.clearPongTimer(wsKey);
|
|
747
|
+
if (this.isWsPong(event)) {
|
|
748
|
+
return this.onWsPong(event, wsKey, 'event');
|
|
749
|
+
}
|
|
750
|
+
if (this.isWsPing(event)) {
|
|
751
|
+
return this.onWsPing(event, wsKey, ws, 'event');
|
|
752
|
+
}
|
|
753
|
+
if (isMessageEvent(event)) {
|
|
754
|
+
const data = event.data;
|
|
755
|
+
const dataType = event.type;
|
|
756
|
+
const emittableEvents = this.resolveEmittableEvents(wsKey, event);
|
|
757
|
+
if (!emittableEvents.length) {
|
|
758
|
+
// console.log(`raw event: `, { data, dataType, emittableEvents });
|
|
759
|
+
this.logger.error('Unhandled/unrecognised ws event message - returned no emittable data', {
|
|
760
|
+
...this.WS_LOGGER_CATEGORY,
|
|
761
|
+
message: data || 'no message',
|
|
762
|
+
dataType,
|
|
763
|
+
event,
|
|
764
|
+
wsKey,
|
|
765
|
+
});
|
|
766
|
+
return this.emit('message', { ...event, wsKey });
|
|
767
|
+
}
|
|
768
|
+
for (const emittable of emittableEvents) {
|
|
769
|
+
if (this.isWsPong(emittable) || emittable.eventType === 'pong') {
|
|
770
|
+
this.logger.trace('Received pong2', {
|
|
771
|
+
...this.WS_LOGGER_CATEGORY,
|
|
772
|
+
wsKey,
|
|
773
|
+
data,
|
|
774
|
+
});
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
this.logger.trace('getFinalEmittable()->pre(): ', JSON.stringify(emittable));
|
|
778
|
+
const emittableFinalEvent = getFinalEmittable(emittable, wsKey, emittable.isWSAPIResponse);
|
|
779
|
+
if (emittable.eventType === 'connectionReady') {
|
|
780
|
+
this.logger.trace('Successfully connected - connection ready for events', {
|
|
781
|
+
...this.WS_LOGGER_CATEGORY,
|
|
782
|
+
wsKey,
|
|
783
|
+
emittable,
|
|
784
|
+
});
|
|
785
|
+
const wsState = this.wsStore.get(wsKey);
|
|
786
|
+
if (wsState && !this.authPrivateConnectionsOnConnect(wsKey)) {
|
|
787
|
+
wsState.isAuthenticated = true;
|
|
788
|
+
}
|
|
789
|
+
this.emit('response', emittableFinalEvent);
|
|
790
|
+
this.onWsReadyForEvents(wsKey);
|
|
791
|
+
continue;
|
|
792
|
+
}
|
|
793
|
+
// Not used for kraken. At least for spot, auth is included per req during subscribe
|
|
794
|
+
if (emittable.eventType === 'connectionReadyForAuth') {
|
|
795
|
+
this.logger.trace('Ready for auth - requesting auth submission...', {
|
|
796
|
+
...this.WS_LOGGER_CATEGORY,
|
|
797
|
+
wsKey,
|
|
798
|
+
});
|
|
799
|
+
this.sendAuthRequest(wsKey, emittable.event);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
// Not used for kraken. At least for spot, auth is included per req during subscribe
|
|
803
|
+
if (emittable.eventType === 'authenticated') {
|
|
804
|
+
this.logger.trace('Successfully authenticated', {
|
|
805
|
+
...this.WS_LOGGER_CATEGORY,
|
|
806
|
+
wsKey,
|
|
807
|
+
emittable,
|
|
808
|
+
});
|
|
809
|
+
this.emit(emittable.eventType, emittableFinalEvent);
|
|
810
|
+
this.onWsAuthenticated(wsKey, emittable.event);
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
// Other event types are automatically emitted here
|
|
814
|
+
this.logger.trace(`onWsMessage().emit(${emittable.eventType})`, emittableFinalEvent);
|
|
815
|
+
try {
|
|
816
|
+
this.emit(emittable.eventType, emittableFinalEvent);
|
|
817
|
+
}
|
|
818
|
+
catch (e) {
|
|
819
|
+
this.logger.error(`Exception in onWsMessage().emit(${emittable.eventType}) handler:`, e);
|
|
820
|
+
}
|
|
821
|
+
// this.logger.trace(
|
|
822
|
+
// `onWsMessage().emit(${emittable.eventType}).done()`,
|
|
823
|
+
// emittableFinalEvent,
|
|
824
|
+
// );
|
|
825
|
+
}
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
this.logger.error('Unhandled/unrecognised ws event message - unexpected message format', {
|
|
829
|
+
...this.WS_LOGGER_CATEGORY,
|
|
830
|
+
message: event || 'no message',
|
|
831
|
+
event,
|
|
832
|
+
wsKey,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
catch (e) {
|
|
836
|
+
this.logger.error('Failed to parse ws event message', {
|
|
837
|
+
...this.WS_LOGGER_CATEGORY,
|
|
838
|
+
error: e,
|
|
839
|
+
event,
|
|
840
|
+
wsKey,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
onWsClose(event, wsKey) {
|
|
845
|
+
this.logger.info('Websocket connection closed', {
|
|
846
|
+
...this.WS_LOGGER_CATEGORY,
|
|
847
|
+
wsKey,
|
|
848
|
+
});
|
|
849
|
+
const wsState = this.wsStore.get(wsKey, true);
|
|
850
|
+
wsState.isAuthenticated = false;
|
|
851
|
+
const wsConnectionState = this.wsStore.getConnectionState(wsKey);
|
|
852
|
+
if (wsConnectionState !== WsConnectionStateEnum.CLOSING) {
|
|
853
|
+
// unintentional close, attempt recovery
|
|
854
|
+
this.logger.trace(`onWsClose(${wsKey}): rejecting all deferred promises...`);
|
|
855
|
+
// clean up any pending promises for this connection
|
|
856
|
+
this.getWsStore().rejectAllDeferredPromises(wsKey, 'connection lost, reconnecting');
|
|
857
|
+
// this.clearTopicsPendingSubscriptions(wsKey, true, 'WS Closed');
|
|
858
|
+
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
|
|
859
|
+
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
|
|
860
|
+
this.emit('reconnecting', { wsKey, event });
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
// intentional close - clean up
|
|
864
|
+
// clean up any pending promises for this connection
|
|
865
|
+
this.logger.trace(`onWsClose(${wsKey}): rejecting all deferred promises...`);
|
|
866
|
+
this.getWsStore().rejectAllDeferredPromises(wsKey, 'disconnected');
|
|
867
|
+
this.setWsState(wsKey, WsConnectionStateEnum.INITIAL);
|
|
868
|
+
// This was an intentional close, delete all state for this connection, as if it never existed:
|
|
869
|
+
this.wsStore.delete(wsKey);
|
|
870
|
+
this.emit('close', { wsKey, event });
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
getWs(wsKey) {
|
|
874
|
+
return this.wsStore.getWs(wsKey);
|
|
875
|
+
}
|
|
876
|
+
setWsState(wsKey, state) {
|
|
877
|
+
this.wsStore.setConnectionState(wsKey, state);
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Promise-driven method to assert that a ws has successfully connected (will await until connection is open)
|
|
881
|
+
*/
|
|
882
|
+
async assertIsConnected(wsKey) {
|
|
883
|
+
const isConnected = this.getWsStore().isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
884
|
+
if (isConnected) {
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
const inProgressPromise = this.getWsStore().getConnectionInProgressPromise(wsKey);
|
|
888
|
+
// Already in progress? Await shared promise and retry
|
|
889
|
+
if (inProgressPromise) {
|
|
890
|
+
this.logger.trace('assertIsConnected(): awaiting...');
|
|
891
|
+
await inProgressPromise.promise;
|
|
892
|
+
this.logger.trace('assertIsConnected(): awaiting...connected!');
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
// Start connection, it should automatically store/return a promise.
|
|
896
|
+
this.logger.trace('assertIsConnected(): connecting...');
|
|
897
|
+
await this.connect(wsKey);
|
|
898
|
+
this.logger.trace('assertIsConnected(): connecting...newly connected!');
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Promise-driven method to assert that a ws has been successfully authenticated (will await until auth is confirmed)
|
|
902
|
+
*/
|
|
903
|
+
async assertIsAuthenticated(wsKey) {
|
|
904
|
+
const isConnected = this.getWsStore().isConnectionState(wsKey, WsConnectionStateEnum.CONNECTED);
|
|
905
|
+
if (!isConnected) {
|
|
906
|
+
this.logger.trace('assertIsAuthenticated(): connecting...');
|
|
907
|
+
await this.assertIsConnected(wsKey);
|
|
908
|
+
}
|
|
909
|
+
const inProgressPromise = this.getWsStore().getAuthenticationInProgressPromise(wsKey);
|
|
910
|
+
// Already in progress? Await shared promise and retry
|
|
911
|
+
if (inProgressPromise) {
|
|
912
|
+
this.logger.trace('assertIsAuthenticated(): awaiting...');
|
|
913
|
+
await inProgressPromise.promise;
|
|
914
|
+
this.logger.trace('assertIsAuthenticated(): authenticated!');
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const isAuthenticated = this.wsStore.get(wsKey)?.isAuthenticated;
|
|
918
|
+
if (isAuthenticated) {
|
|
919
|
+
this.logger.trace('assertIsAuthenticated(): ok');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// Start authentication, it should automatically store/return a promise.
|
|
923
|
+
this.logger.trace('assertIsAuthenticated(): authenticating...');
|
|
924
|
+
await this.sendAuthRequest(wsKey);
|
|
925
|
+
this.logger.trace('assertIsAuthenticated(): newly authenticated!');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
//# sourceMappingURL=BaseWSClient.js.map
|