Skip to content

Commit 1046bb0

Browse files
committed
fix(): isWsPartialBookDepthEventFormatted type guard event recognition
1 parent a75ea69 commit 1046bb0

File tree

9 files changed

+146
-21
lines changed

9 files changed

+146
-21
lines changed

examples/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,33 @@ Samples that API credentials using `process.env.API_KEY_COM` can be spawned with
1515
APIKEY="apikeypastedhere" APISECRET="apisecretpastedhere" tsx examples/WebSockets/ws-userdata-listenkey.ts
1616
```
1717

18+
### WebSockets
19+
20+
All examples relating to WebSockets can be found in the [examples/WebSockets](./WebSockets/) folder. High level summary of available examples:
21+
22+
#### Consumers
23+
24+
These are purely for receiving data from Binance's WebSockets (market data, account updates, etc).
25+
26+
##### Market Data
27+
- ws-public.ts
28+
- Demonstration on general usage of the WebSocket client to subscribe to / unsubscribe from one or more market data topics.
29+
- ws-public-spot-orderbook.ts
30+
- ws-public-spot-trades.ts
31+
32+
##### Account Data
33+
34+
- ws-close.ts
35+
- ws-unsubscribe.ts
36+
- ws-proxy-socks.ts
37+
- ws-public-usdm-funding.ts
38+
- ws-userdata-testnet.ts
39+
- ws-userdata-listenkey.ts
40+
- ws-userdata-connection-safety.ts
41+
- ws-userdata-wsapi.ts
42+
- ws-api-client.ts
43+
- ws-api-raw-promises.ts
44+
- deprecated-ws-public.ts
1845

1946
## REST USDM Examples
2047

examples/WebSockets/ws-public-spot-orderbook.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
/* eslint-disable @typescript-eslint/no-unused-vars */
2+
/* eslint-disable no-unused-vars */
13
import {
24
DefaultLogger,
3-
getContextFromWsKey,
45
isWsPartialBookDepthEventFormatted,
56
WebsocketClient,
7+
WsMessagePartialBookDepthEventFormatted,
68
} from '../../src/index';
79

810
// or, with the npm package
@@ -27,18 +29,19 @@ import {
2729
logger,
2830
);
2931

32+
const topicsPartialBookDepths: string[] = [];
33+
3034
/**
3135
* Simple example for receiving depth snapshots from spot orderbooks
3236
*/
3337
wsClient.on('formattedMessage', (data) => {
3438
if (isWsPartialBookDepthEventFormatted(data)) {
35-
const context = getContextFromWsKey(data.wsKey);
36-
37-
if (!context?.symbol) {
39+
const [symbol] = data.streamName.split('@');
40+
if (!symbol) {
3841
throw new Error('Failed to extract context from event?');
3942
}
4043

41-
console.log(`ws book event for "${context.symbol.toUpperCase()}"`, data);
44+
console.log(`ws book event for "${symbol.toUpperCase()}"`, data);
4245
return;
4346
}
4447

@@ -61,13 +64,27 @@ import {
6164
console.error('ws exception: ', data?.wsKey, data);
6265
});
6366

64-
// Request subscription to the following symbol trade events:
65-
const symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT'];
66-
// const symbols = ['BTCUSDT'];
67+
// Request subscription to the following symbol events:
68+
const symbols = ['BTCUSDT'];
69+
// const symbols = ['BTCUSDT', 'ETHUSDT', 'BNBUSDT'];
6770

6871
// Loop through symbols
6972
for (const symbol of symbols) {
7073
console.log('subscribing to trades for: ', symbol);
71-
wsClient.subscribePartialBookDepths(symbol, 20, 1000, 'spot');
74+
// The old way, convenient but unnecessary:
75+
// wsClient.subscribePartialBookDepths(symbol, levels, 1000, 'spot');
76+
77+
// Manually build a topic matching the structure expected by binance:
78+
// btcusdt@depth20@1000ms
79+
80+
const streamName = 'depth';
81+
const levels = 20;
82+
const updateMs = '@' + 1000 + 'ms';
83+
84+
const topic = `${symbol.toLowerCase()}@${streamName}${levels}${updateMs}`;
85+
topicsPartialBookDepths.push(topic);
7286
}
87+
88+
// Request subscribe for these topics in the main product group (spot markets are under "main")
89+
wsClient.subscribe(topicsPartialBookDepths, 'main');
7390
})();

src/types/websockets/ws-events-formatted.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ export interface WsMessageBookTickerEventFormatted extends WsSharedBase {
172172
}
173173

174174
export interface WsMessagePartialBookDepthEventFormatted extends WsSharedBase {
175-
eventType: 'partialBookDepth';
175+
eventType: 'partialBookDepth' | 'string';
176176
lastUpdateId: number;
177177
bids: OrderBookRowFormatted[];
178178
asks: OrderBookRowFormatted[];

src/types/websockets/ws-general.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export type WsMarket =
3636
export interface WsSharedBase {
3737
wsMarket: WsMarket;
3838
wsKey: WsKey;
39+
streamName: string;
3940
}
4041

4142
export interface WsResponse {
@@ -123,6 +124,11 @@ export interface WSClientConfigurableOptions {
123124

124125
beautify?: boolean;
125126

127+
/**
128+
* If true, log a warning if the beautifier is missing anything for an event
129+
*/
130+
beautifyWarnIfMissing?: boolean;
131+
126132
/**
127133
* Set to `true` to connect to Binance's testnet environment.
128134
*

src/util/beautifier-maps.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ const rollingTickerEventMap = {
1818
n: 'totalTrades',
1919
};
2020

21+
/**
22+
* The beautifier map defines how specific properties are renamed into a more readable property name.
23+
*
24+
* At first, it's a simple key:value map. The key is the original property name (e.g. aggTrades or bookTickerEvent (for the top-level name of an event: ("bookTicker" + "Event"))).
25+
*
26+
* The values have different behaviours:
27+
* - Value is an object, each child property is renamed to the value
28+
* e.g. aggTrades: { a: "aggTradeId" } -> aggTrades: { aggTradeId: value }
29+
* - Value is a string, this points to another key in the map.
30+
* e.g. klineEvent: { k: "kline" } resolves to
31+
* klineEvent: { kline: { BEAUTIFIER_EVENT_MAP["kline"] } }
32+
* - Value is an array, each element in that array is transformed into an object.
33+
*/
2134
export const BEAUTIFIER_EVENT_MAP = {
2235
aggTrades: {
2336
a: 'aggTradeId',

src/util/beautifier.ts

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@ export interface BeautifierConfig {
66
}
77

88
export default class Beautifier {
9-
private beautificationMap: Record<string, Record<string, any>>;
9+
private beautificationMap: Record<string, Record<string, any> | string> =
10+
BEAUTIFIER_EVENT_MAP;
1011

1112
private floatKeys: string[];
1213

1314
private floatKeysHashMap: Record<string, boolean>;
1415

15-
private config: BeautifierConfig | undefined;
16+
private config: BeautifierConfig;
1617

1718
constructor(config: BeautifierConfig) {
1819
this.config = config;
@@ -158,11 +159,14 @@ export default class Beautifier {
158159
this.floatKeys.forEach((keyName) => {
159160
this.floatKeysHashMap[keyName] = true;
160161
});
162+
}
161163

162-
this.beautificationMap = BEAUTIFIER_EVENT_MAP;
164+
setWarnIfMissing(value: boolean) {
165+
this.config.warnKeyMissingInMap = value;
163166
}
164167

165168
beautifyValueWithKey(key: string | number, val: unknown) {
169+
// console.log('beautifier.beautifyValueWithKey()', { key, val });
166170
if (typeof val === 'string' && this.floatKeysHashMap[key] && val !== '') {
167171
const result = parseFloat(val);
168172
if (isNaN(result)) {
@@ -177,6 +181,7 @@ export default class Beautifier {
177181
* Beautify array or object, recurisvely
178182
*/
179183
beautifyObjectValues(data: any | any[]) {
184+
// console.log('beautifier.beautifyObjectValues()', { data });
180185
if (Array.isArray(data)) {
181186
return this.beautifyArrayValues(data);
182187
}
@@ -197,6 +202,7 @@ export default class Beautifier {
197202
}
198203

199204
beautifyArrayValues(data: any[], parentKey?: string | number) {
205+
// console.log('beautifier.beautifyArrayValues()', { data, parentKey });
200206
const beautifedArray: any[] = [];
201207
for (const [key, val] of data.entries()) {
202208
const type = typeof val;
@@ -212,7 +218,6 @@ export default class Beautifier {
212218
}
213219

214220
beautify(data: any, key?: string | number) {
215-
// console.log('beautify()', { key });
216221
if (typeof key !== 'string' && typeof key !== 'number') {
217222
console.warn(
218223
`beautify(object, ${key}) is not valid key - beautification failed `,
@@ -223,6 +228,7 @@ export default class Beautifier {
223228
}
224229

225230
const knownBeautification = this.beautificationMap[key];
231+
// console.log('beautify: ', { key, knownBeautification }, data);
226232
if (!knownBeautification) {
227233
const valueType = typeof data;
228234
const isPrimitive =
@@ -251,18 +257,21 @@ export default class Beautifier {
251257
}
252258

253259
const newItem = {};
254-
for (const key in data) {
255-
const value = data[key];
260+
for (const propertyKey in data) {
261+
const value = data[propertyKey];
256262
const valueType = typeof value;
257263

258-
let newKey = knownBeautification[key] || key;
264+
let newKey = knownBeautification[propertyKey] || propertyKey;
259265
if (Array.isArray(newKey)) {
260266
newKey = newKey[0];
261267
}
262268

263269
if (!Array.isArray(value)) {
264270
if (valueType === 'object' && value !== null) {
265-
newItem[newKey] = this.beautify(value, knownBeautification[key]);
271+
newItem[newKey] = this.beautify(
272+
value,
273+
knownBeautification[propertyKey],
274+
);
266275
} else {
267276
newItem[newKey] = this.beautifyValueWithKey(newKey, value);
268277
}
@@ -271,10 +280,43 @@ export default class Beautifier {
271280

272281
const newArray: any[] = [];
273282
if (Array.isArray(this.beautificationMap[newKey])) {
283+
// console.log('beautify().isArray(): ', {
284+
// newKey,
285+
// arrayFromMap: this.beautificationMap[newKey],
286+
// });
274287
for (const elementValue of value) {
275288
const mappedBeautification =
276-
this.beautificationMap[knownBeautification[key]];
289+
this.beautificationMap[knownBeautification[propertyKey]];
290+
291+
// console.log('mapped meautification: ', {
292+
// knownBeautification: knownBeautification[propertyKey],
293+
// mappedBeautification,
294+
// key,
295+
// subKey: propertyKey,
296+
// newKey,
297+
// });
298+
299+
if (!mappedBeautification) {
300+
// console.warn(
301+
// `Beautifier(): found map for "${key}" but property with array ("${propertyKey}") is missing in map: `,
302+
// {
303+
// eventMapKey: key,
304+
// propertyKey: propertyKey,
305+
// elementValue,
306+
// knownBeautification,
307+
// value,
308+
// // beautfTest1: this.beautify(value, propertyKey),
309+
// },
310+
// );
311+
newArray.push(elementValue);
312+
313+
continue;
314+
}
277315
const childMapping = mappedBeautification[0];
316+
// const childMapping =
317+
// typeof mappedBeautification === 'string' // Pointer to another key
318+
// ? this.beautificationMap[mappedBeautification]
319+
// : mappedBeautification[0];
278320

279321
if (typeof childMapping === 'object' && childMapping !== null) {
280322
const mappedResult = {};

src/util/typeGuards.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,23 @@ export function isWsAggTradeFormatted(
125125
return !Array.isArray(data) && data.eventType === 'aggTrade';
126126
}
127127

128+
const partialBookDepthEventTypeMap = new Map()
129+
// For dedicated connection
130+
.set('partialBookDepth', true)
131+
// For multiplex connection
132+
.set('depth5@100ms', true)
133+
.set('depth10@100ms', true)
134+
.set('depth20@100ms', true)
135+
.set('depth5@1000ms', true)
136+
.set('depth10@1000ms', true)
137+
.set('depth20@1000ms', true);
138+
128139
export function isWsPartialBookDepthEventFormatted(
129140
data: WsFormattedMessage,
130141
): data is WsMessagePartialBookDepthEventFormatted {
131-
return !Array.isArray(data) && data.eventType === 'partialBookDepth';
142+
return (
143+
!Array.isArray(data) && partialBookDepthEventTypeMap.has(data.eventType)
144+
);
132145
}
133146

134147
/**

src/util/websockets/websocket-util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,9 @@ interface WsContext {
614614
otherParams: undefined | string[];
615615
}
616616

617+
/**
618+
* @deprecated Only works for legacy WS client, where one connection exists per key
619+
*/
617620
export function getContextFromWsKey(legacyWsKey: any): WsContext {
618621
const [market, streamName, symbol, listenKey, wsKey, ...otherParams] =
619622
legacyWsKey.split('_');

src/websocket-client.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export class WebsocketClient extends BaseWebsocketClient<
8686
private restClientCache: RestClientCache = new RestClientCache();
8787

8888
private beautifier: Beautifier = new Beautifier({
89-
warnKeyMissingInMap: true,
89+
warnKeyMissingInMap: false,
9090
});
9191

9292
private userDataStreamManager: UserDataStreamManager;
@@ -97,6 +97,10 @@ export class WebsocketClient extends BaseWebsocketClient<
9797
constructor(options?: WSClientConfigurableOptions, logger?: DefaultLogger) {
9898
super(options, logger);
9999

100+
if (options?.beautifyWarnIfMissing) {
101+
this.beautifier.setWarnIfMissing(options.beautifyWarnIfMissing);
102+
}
103+
100104
/**
101105
* Binance uses native WebSocket ping/pong frames, which cannot be directly used in
102106
* some environents (e.g. most browsers do not support sending raw ping/pong frames).

0 commit comments

Comments
 (0)