Skip to content

Commit f4cd660

Browse files
authored
Merge pull request #142 from tiagosiebler/wsnotice
feat(v3.0.6): handle reconnection notice automatically, allow authenticating for market data
2 parents 6ee35ab + 8668c5e commit f4cd660

File tree

7 files changed

+141
-32
lines changed

7 files changed

+141
-32
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ node_modules/
2020
.env.test
2121
.cache
2222
lib
23+
dist
2324
bundleReport.html
2425
.history/
2526
.idea
@@ -28,4 +29,4 @@ restClientRegex.ts
2829
testfile.ts
2930
localtest.sh
3031

31-
repomix.sh
32+
repomix.sh

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "okx-api",
3-
"version": "3.0.5",
3+
"version": "3.0.6",
44
"description": "Complete Node.js SDK for OKX's REST APIs and WebSockets, with TypeScript & end-to-end tests",
55
"scripts": {
66
"test": "jest",

src/util/BaseWSClient.ts

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { checkWebCryptoAPISupported } from './webCryptoAPI.js';
1717
import {
1818
getNormalisedTopicRequests,
1919
PRIVATE_CHANNELS,
20+
PUBLIC_CHANNELS_WITH_AUTH,
2021
safeTerminateWs,
2122
WS_LOGGER_CATEGORY,
2223
WsTopicRequest,
@@ -216,7 +217,10 @@ export abstract class BaseWebsocketClient<
216217

217218
protected abstract isWsPong(data: any): boolean;
218219

219-
protected abstract getWsAuthRequestEvent(wsKey: TWSKey): Promise<object>;
220+
protected abstract getWsAuthRequestEvent(
221+
wsKey: TWSKey,
222+
skipPublicWsKeyCheck: boolean,
223+
): Promise<object | null>;
220224

221225
protected abstract isPrivateTopicRequest(
222226
request: WsTopicRequest<string>,
@@ -339,7 +343,11 @@ export abstract class BaseWebsocketClient<
339343
// Queue immediate auth if so
340344
for (const topicRequest of normalisedTopicRequests) {
341345
if (PRIVATE_CHANNELS.includes(topicRequest.topic)) {
342-
await this.assertIsAuthenticated(wsKey);
346+
await this.assertIsAuthenticated(wsKey, false);
347+
break;
348+
}
349+
if (PUBLIC_CHANNELS_WITH_AUTH.includes(topicRequest.topic)) {
350+
await this.assertIsAuthenticated(wsKey, true);
343351
break;
344352
}
345353
}
@@ -449,6 +457,37 @@ export abstract class BaseWebsocketClient<
449457
});
450458
}
451459

460+
/**
461+
* Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
462+
* If closed, trigger a reconnect immediately
463+
*/
464+
public executeReconnectableClose(wsKey: TWSKey, reason: string) {
465+
this.logger.info(`${reason} - closing socket to reconnect`, {
466+
...WS_LOGGER_CATEGORY,
467+
wsKey,
468+
reason,
469+
});
470+
471+
this.clearTimers(wsKey);
472+
473+
const wasOpen = this.wsStore.isWsOpen(wsKey);
474+
if (wasOpen) {
475+
safeTerminateWs(this.wsStore.getWs(wsKey), true);
476+
}
477+
478+
if (!wasOpen) {
479+
this.logger.info(
480+
`${reason} - socket already closed - trigger immediate reconnect`,
481+
{
482+
...WS_LOGGER_CATEGORY,
483+
wsKey,
484+
reason,
485+
},
486+
);
487+
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout);
488+
}
489+
}
490+
452491
public isConnected(wsKey: TWSKey): boolean {
453492
return this.wsStore.isConnectionState(
454493
wsKey,
@@ -568,7 +607,10 @@ export abstract class BaseWebsocketClient<
568607
}
569608

570609
/** Get a signature, build the auth request and send it */
571-
private async sendAuthRequest(wsKey: TWSKey): Promise<unknown> {
610+
private async sendAuthRequest(
611+
wsKey: TWSKey,
612+
skipPublicWsKeyCheck: boolean,
613+
): Promise<unknown> {
572614
try {
573615
this.logger.trace('Sending auth request...', {
574616
...WS_LOGGER_CATEGORY,
@@ -577,19 +619,22 @@ export abstract class BaseWebsocketClient<
577619

578620
await this.assertIsConnected(wsKey);
579621

622+
const request = await this.getWsAuthRequestEvent(
623+
wsKey,
624+
skipPublicWsKeyCheck,
625+
);
626+
580627
if (!this.wsStore.getAuthenticationInProgressPromise(wsKey)) {
581628
this.wsStore.createAuthenticationInProgressPromise(wsKey, false);
582629
}
583630

584-
const request = await this.getWsAuthRequestEvent(wsKey);
585-
586631
// console.log('ws auth req', request);
587632

588633
this.tryWsSend(wsKey, JSON.stringify(request));
589634

590635
return this.wsStore.getAuthenticationInProgressPromise(wsKey)?.promise;
591636
} catch (e) {
592-
this.logger.trace(e, { ...WS_LOGGER_CATEGORY, wsKey });
637+
this.logger.error(e, { ...WS_LOGGER_CATEGORY, wsKey });
593638
}
594639
}
595640

@@ -919,12 +964,22 @@ export abstract class BaseWebsocketClient<
919964
this.isAuthOnConnectWsKey(wsKey) &&
920965
this.options.authPrivateConnectionsOnConnect
921966
) {
922-
await this.assertIsAuthenticated(wsKey);
967+
await this.assertIsAuthenticated(wsKey, false);
968+
}
969+
970+
const topicsForWsKey = [...this.wsStore.getTopics(wsKey)];
971+
972+
// Guard to assert auth for some of the public topics that require it
973+
for (const topicRequest of topicsForWsKey) {
974+
if (PUBLIC_CHANNELS_WITH_AUTH.includes(topicRequest.topic)) {
975+
await this.assertIsAuthenticated(wsKey, true);
976+
break;
977+
}
923978
}
924979

925980
// Reconnect to topics known before it connected
926981
const { privateReqs, publicReqs } = this.sortTopicRequestsIntoPublicPrivate(
927-
[...this.wsStore.getTopics(wsKey)],
982+
topicsForWsKey,
928983
wsKey,
929984
);
930985

@@ -1206,7 +1261,10 @@ export abstract class BaseWebsocketClient<
12061261
/**
12071262
* Promise-driven method to assert that a ws has been successfully authenticated (will await until auth is confirmed)
12081263
*/
1209-
public async assertIsAuthenticated(wsKey: TWSKey): Promise<unknown> {
1264+
public async assertIsAuthenticated(
1265+
wsKey: TWSKey,
1266+
skipPublicWsKeyCheck: boolean,
1267+
): Promise<unknown> {
12101268
const isConnected = this.getWsStore().isConnectionState(
12111269
wsKey,
12121270
WsConnectionStateEnum.CONNECTED,
@@ -1237,7 +1295,7 @@ export abstract class BaseWebsocketClient<
12371295
// Start authentication, it should automatically store/return a promise.
12381296
this.logger.trace('assertIsAuthenticated(): authenticating...');
12391297

1240-
await this.sendAuthRequest(wsKey);
1298+
await this.sendAuthRequest(wsKey, skipPublicWsKeyCheck);
12411299

12421300
this.logger.trace('assertIsAuthenticated(): newly authenticated!');
12431301
}

src/util/websocket-util.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,12 @@ export const PRIVATE_CHANNELS = [
234234
'grid-sub-orders',
235235
];
236236

237+
export const PUBLIC_CHANNELS_WITH_AUTH = [
238+
// While these are market data topics on the PUBLIC channel, they do require the public connection to be authenticated to subscribe to these. See #140.
239+
'books-l2-tbt',
240+
'books50-l2-tpt',
241+
];
242+
237243
/**
238244
* The following channels only support the new business wss endpoint:
239245
* https://www.okx.com/help-center/changes-to-v5-api-websocket-subscription-parameter-and-url

src/websocket-client-legacy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ export class WebsocketClientLegacy extends EventEmitter {
470470

471471
/**
472472
* Closes a connection, if it's even open. If open, this will trigger a reconnect asynchronously.
473-
* If closed, trigger a reconnect immediately
473+
* If closed, trigger a reconnect immediately.
474474
*/
475475
private executeReconnectableClose(wsKey: WsKey, reason: string) {
476476
this.logger.info(`${reason} - closing socket to reconnect`, {

src/websocket-client.ts

Lines changed: 55 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,8 @@ export class WebsocketClient extends BaseWebsocketClient<
125125
public connectWSAPI(): Promise<unknown[]> {
126126
/** This call automatically ensures the connection is active AND authenticated before resolving */
127127
return Promise.allSettled([
128-
this.assertIsAuthenticated(this.getMarketWsKey('private')),
129-
this.assertIsAuthenticated(this.getMarketWsKey('business')),
128+
this.assertIsAuthenticated(this.getMarketWsKey('private'), false),
129+
this.assertIsAuthenticated(this.getMarketWsKey('business'), false),
130130
]);
131131
}
132132

@@ -422,17 +422,36 @@ export class WebsocketClient extends BaseWebsocketClient<
422422

423423
protected async getWsAuthRequestEvent(
424424
wsKey: WsKey,
425-
): Promise<WsRequestOperationOKX<WsAuthRequestArg>> {
425+
skipIsPublicWsKeyCheck: boolean,
426+
): Promise<WsRequestOperationOKX<WsAuthRequestArg> | null> {
426427
const isPublicWsKey = PUBLIC_WS_KEYS.includes(wsKey);
427428
const accounts = this.options.accounts;
428429
const hasAccountsToAuth = !!accounts?.length;
429-
if (isPublicWsKey || !accounts || !hasAccountsToAuth) {
430-
this.logger.trace('Starting public only websocket client.', {
431-
...WS_LOGGER_CATEGORY,
432-
wsKey,
433-
isPublicWsKey,
434-
hasAccountsToAuth,
435-
});
430+
if (isPublicWsKey && !skipIsPublicWsKeyCheck) {
431+
this.logger.trace(
432+
'Starting public only websocket client. No auth needed.',
433+
{
434+
...WS_LOGGER_CATEGORY,
435+
wsKey,
436+
isPublicWsKey,
437+
hasAccountsToAuth,
438+
skipIsPublicWsKeyCheck,
439+
},
440+
);
441+
return null;
442+
}
443+
444+
if (!accounts || !hasAccountsToAuth) {
445+
this.logger.trace(
446+
'Starting public only websocket client - missing keys.',
447+
{
448+
...WS_LOGGER_CATEGORY,
449+
wsKey,
450+
isPublicWsKey,
451+
hasAccountsToAuth,
452+
skipIsPublicWsKeyCheck,
453+
},
454+
);
436455
throw new Error('Cannot auth - missing api or secret or pass in config');
437456
}
438457

@@ -704,6 +723,31 @@ export class WebsocketClient extends BaseWebsocketClient<
704723
return results;
705724
}
706725

726+
if (msg.event === 'notice') {
727+
const WSNOTICE = {
728+
CLOSING_FOR_UPGRADE_RECOMMEND_RECONNECT: '64008',
729+
};
730+
if (msg?.code === WSNOTICE.CLOSING_FOR_UPGRADE_RECOMMEND_RECONNECT) {
731+
const closeReason = `Received notice (${msg.code} - "${msg?.msg}") - closing socket to reconnect`;
732+
this.logger.info(closeReason, {
733+
...WS_LOGGER_CATEGORY,
734+
...msg,
735+
wsKey,
736+
});
737+
738+
// Queue immediate reconnection workflow
739+
this.executeReconnectableClose(wsKey, closeReason);
740+
741+
// Emit notice to client for visibility
742+
results.push({
743+
eventType: 'update',
744+
event: emittableEvent,
745+
});
746+
747+
return results;
748+
}
749+
}
750+
707751
this.logger.info('Unhandled/unrecognised ws event message', {
708752
...WS_LOGGER_CATEGORY,
709753
message: msg || 'no message',
@@ -765,7 +809,7 @@ export class WebsocketClient extends BaseWebsocketClient<
765809
this.logger.trace(
766810
'sendWSAPIRequest(): assertIsAuthenticated(${wsKey})...',
767811
);
768-
await this.assertIsAuthenticated(wsKey);
812+
await this.assertIsAuthenticated(wsKey, false);
769813
this.logger.trace(
770814
'sendWSAPIRequest(): assertIsAuthenticated(${wsKey}) ok',
771815
);

0 commit comments

Comments
 (0)