@nevuamarkets/poly-websockets 0.2.2 → 0.3.1
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/README.md +6 -0
- package/dist/WSSubscriptionManager.d.ts +46 -0
- package/dist/WSSubscriptionManager.js +52 -35
- package/dist/modules/GroupRegistry.d.ts +27 -7
- package/dist/modules/GroupRegistry.js +46 -7
- package/dist/modules/GroupSocket.d.ts +37 -0
- package/dist/modules/GroupSocket.js +44 -0
- package/dist/modules/OrderBookCache.d.ts +12 -7
- package/dist/modules/OrderBookCache.js +12 -7
- package/dist/types/PolymarketWebSocket.d.ts +1 -1
- package/dist/types/WebSocketSubscriptions.d.ts +1 -0
- package/package.json +4 -2
- package/src/WSSubscriptionManager.ts +56 -35
- package/src/modules/GroupRegistry.ts +54 -9
- package/src/modules/GroupSocket.ts +45 -1
- package/src/modules/OrderBookCache.ts +13 -9
- package/src/types/PolymarketWebSocket.ts +1 -1
- package/src/types/WebSocketSubscriptions.ts +2 -1
package/README.md
CHANGED
|
@@ -90,6 +90,12 @@ Clears all subscriptions and state:
|
|
|
90
90
|
- Closes all WebSocket connections
|
|
91
91
|
- Clears the internal order book cache
|
|
92
92
|
|
|
93
|
+
##### `getStatistics(): { openWebSockets: number; subscribedAssetIds: number }`
|
|
94
|
+
|
|
95
|
+
Returns statistics about the current state of the subscription manager:
|
|
96
|
+
- `openWebSockets`: The number of websockets that are currently in OPEN state
|
|
97
|
+
- `subscribedAssetIds`: The number of unique asset IDs that are currently subscribed
|
|
98
|
+
|
|
93
99
|
### WebSocketHandlers
|
|
94
100
|
|
|
95
101
|
Interface defining event handlers for different WebSocket events.
|
|
@@ -8,11 +8,57 @@ declare class WSSubscriptionManager {
|
|
|
8
8
|
private reconnectAndCleanupIntervalMs;
|
|
9
9
|
private maxMarketsPerWS;
|
|
10
10
|
constructor(userHandlers: WebSocketHandlers, options?: SubscriptionManagerOptions);
|
|
11
|
+
/**
|
|
12
|
+
* Clears all WebSocket subscriptions and state.
|
|
13
|
+
*
|
|
14
|
+
* This will:
|
|
15
|
+
*
|
|
16
|
+
* 1. Remove all subscriptions and groups
|
|
17
|
+
* 2. Close all WebSocket connections
|
|
18
|
+
* 3. Clear the order book cache
|
|
19
|
+
*/
|
|
11
20
|
clearState(): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* This function is called when:
|
|
23
|
+
* - a websocket event is received from the Polymarket WS
|
|
24
|
+
* - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
|
|
25
|
+
* depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
|
|
26
|
+
*
|
|
27
|
+
* The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
|
|
28
|
+
*
|
|
29
|
+
* @param events - The events to process.
|
|
30
|
+
* @param action - The action to perform on the filtered events.
|
|
31
|
+
*/
|
|
12
32
|
private actOnSubscribedEvents;
|
|
33
|
+
/**
|
|
34
|
+
* Edits wsGroups: Adds new subscriptions.
|
|
35
|
+
*
|
|
36
|
+
* - Filters out assets that are already subscribed
|
|
37
|
+
* - Finds a group with capacity or creates a new one
|
|
38
|
+
* - Creates a new WebSocket client and adds it to the group
|
|
39
|
+
*
|
|
40
|
+
* @param assetIdsToAdd - The asset IDs to add subscriptions for.
|
|
41
|
+
*/
|
|
13
42
|
addSubscriptions(assetIdsToAdd: string[]): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Edits wsGroups: Removes subscriptions.
|
|
45
|
+
* The group will use the updated subscriptions when it reconnects.
|
|
46
|
+
* We do that because we don't want to miss events by reconnecting.
|
|
47
|
+
*
|
|
48
|
+
* @param assetIdsToRemove - The asset IDs to remove subscriptions for.
|
|
49
|
+
*/
|
|
14
50
|
removeSubscriptions(assetIdsToRemove: string[]): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* This function runs periodically and:
|
|
53
|
+
*
|
|
54
|
+
* - Tries to reconnect groups that have assets and are disconnected
|
|
55
|
+
* - Cleans up groups that have no assets
|
|
56
|
+
*/
|
|
15
57
|
private reconnectAndCleanupGroups;
|
|
58
|
+
getStatistics(): {
|
|
59
|
+
openWebSockets: number;
|
|
60
|
+
subscribedAssetIds: number;
|
|
61
|
+
};
|
|
16
62
|
private createWebSocketClient;
|
|
17
63
|
}
|
|
18
64
|
export { WSSubscriptionManager, WebSocketHandlers };
|
|
@@ -58,15 +58,15 @@ class WSSubscriptionManager {
|
|
|
58
58
|
this.reconnectAndCleanupGroups();
|
|
59
59
|
}, this.reconnectAndCleanupIntervalMs);
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Clears all WebSocket subscriptions and state.
|
|
63
|
+
*
|
|
64
|
+
* This will:
|
|
65
|
+
*
|
|
66
|
+
* 1. Remove all subscriptions and groups
|
|
67
|
+
* 2. Close all WebSocket connections
|
|
68
|
+
* 3. Clear the order book cache
|
|
69
|
+
*/
|
|
70
70
|
async clearState() {
|
|
71
71
|
const previousGroups = await this.groupRegistry.clearAllGroups();
|
|
72
72
|
// Close sockets outside the lock
|
|
@@ -76,14 +76,17 @@ class WSSubscriptionManager {
|
|
|
76
76
|
// Also clear the order book cache
|
|
77
77
|
this.bookCache.clear();
|
|
78
78
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
79
|
+
/**
|
|
80
|
+
* This function is called when:
|
|
81
|
+
* - a websocket event is received from the Polymarket WS
|
|
82
|
+
* - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
|
|
83
|
+
* depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
|
|
84
|
+
*
|
|
85
|
+
* The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
|
|
86
|
+
*
|
|
87
|
+
* @param events - The events to process.
|
|
88
|
+
* @param action - The action to perform on the filtered events.
|
|
89
|
+
*/
|
|
87
90
|
async actOnSubscribedEvents(events, action) {
|
|
88
91
|
// Filter out events that are not subscribed to by any groups
|
|
89
92
|
events = lodash_1.default.filter(events, (event) => {
|
|
@@ -118,13 +121,15 @@ class WSSubscriptionManager {
|
|
|
118
121
|
});
|
|
119
122
|
await (action === null || action === void 0 ? void 0 : action(events));
|
|
120
123
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Edits wsGroups: Adds new subscriptions.
|
|
126
|
+
*
|
|
127
|
+
* - Filters out assets that are already subscribed
|
|
128
|
+
* - Finds a group with capacity or creates a new one
|
|
129
|
+
* - Creates a new WebSocket client and adds it to the group
|
|
130
|
+
*
|
|
131
|
+
* @param assetIdsToAdd - The asset IDs to add subscriptions for.
|
|
132
|
+
*/
|
|
128
133
|
async addSubscriptions(assetIdsToAdd) {
|
|
129
134
|
var _a, _b;
|
|
130
135
|
try {
|
|
@@ -138,11 +143,13 @@ class WSSubscriptionManager {
|
|
|
138
143
|
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(msg)));
|
|
139
144
|
}
|
|
140
145
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
/**
|
|
147
|
+
* Edits wsGroups: Removes subscriptions.
|
|
148
|
+
* The group will use the updated subscriptions when it reconnects.
|
|
149
|
+
* We do that because we don't want to miss events by reconnecting.
|
|
150
|
+
*
|
|
151
|
+
* @param assetIdsToRemove - The asset IDs to remove subscriptions for.
|
|
152
|
+
*/
|
|
146
153
|
async removeSubscriptions(assetIdsToRemove) {
|
|
147
154
|
var _a, _b;
|
|
148
155
|
try {
|
|
@@ -153,12 +160,12 @@ class WSSubscriptionManager {
|
|
|
153
160
|
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(errMsg)));
|
|
154
161
|
}
|
|
155
162
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
/**
|
|
164
|
+
* This function runs periodically and:
|
|
165
|
+
*
|
|
166
|
+
* - Tries to reconnect groups that have assets and are disconnected
|
|
167
|
+
* - Cleans up groups that have no assets
|
|
168
|
+
*/
|
|
162
169
|
async reconnectAndCleanupGroups() {
|
|
163
170
|
var _a, _b;
|
|
164
171
|
try {
|
|
@@ -171,6 +178,16 @@ class WSSubscriptionManager {
|
|
|
171
178
|
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err));
|
|
172
179
|
}
|
|
173
180
|
}
|
|
181
|
+
/*
|
|
182
|
+
Returns statistics about the current state of the subscription manager.
|
|
183
|
+
|
|
184
|
+
Returns an object with:
|
|
185
|
+
- openWebSockets: The number of websockets that are currently in OPEN state
|
|
186
|
+
- subscribedAssetIds: The number of unique asset IDs that are currently subscribed
|
|
187
|
+
*/
|
|
188
|
+
getStatistics() {
|
|
189
|
+
return this.groupRegistry.getStatistics();
|
|
190
|
+
}
|
|
174
191
|
async createWebSocketClient(groupId, handlers) {
|
|
175
192
|
var _a, _b;
|
|
176
193
|
const group = this.groupRegistry.findGroupById(groupId);
|
|
@@ -17,31 +17,47 @@ export declare class GroupRegistry {
|
|
|
17
17
|
/**
|
|
18
18
|
* Find the first group with capacity to hold new assets.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* @returns The groupId if found, otherwise null.
|
|
21
21
|
*/
|
|
22
22
|
findGroupWithCapacity(newAssetLen: number, maxPerWS: number): string | null;
|
|
23
23
|
/**
|
|
24
24
|
* Get the indices of all groups that contain the asset.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* @param assetId - The tokenId of a market.
|
|
27
|
+
* @returns An array of indices.
|
|
27
28
|
*/
|
|
28
29
|
getGroupIndicesForAsset(assetId: string): number[];
|
|
29
30
|
/**
|
|
30
31
|
* Check if any group contains the asset.
|
|
32
|
+
*
|
|
33
|
+
* @param assetId - The tokenId of a market.
|
|
34
|
+
* @returns True if found.
|
|
31
35
|
*/
|
|
32
36
|
hasAsset(assetId: string): boolean;
|
|
33
37
|
/**
|
|
34
38
|
* Find the group by groupId.
|
|
35
39
|
*
|
|
36
|
-
*
|
|
40
|
+
* @param groupId - The group UUID.
|
|
41
|
+
* @returns The group if found, otherwise undefined.
|
|
37
42
|
*/
|
|
38
43
|
findGroupById(groupId: string): WebSocketGroup | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* Get statistics about the current state of the registry.
|
|
46
|
+
*
|
|
47
|
+
* Returns an object with:
|
|
48
|
+
* - openWebSockets: The number of websockets that are currently in OPEN state
|
|
49
|
+
* - subscribedAssetIds: The number of unique asset IDs that are currently subscribed
|
|
50
|
+
*/
|
|
51
|
+
getStatistics(): {
|
|
52
|
+
openWebSockets: number;
|
|
53
|
+
subscribedAssetIds: number;
|
|
54
|
+
};
|
|
39
55
|
/**
|
|
40
56
|
* Atomically remove **all** groups from the registry and return them so the
|
|
41
57
|
* caller can perform any asynchronous cleanup (closing sockets, etc.)
|
|
42
58
|
* outside the lock.
|
|
43
59
|
*
|
|
44
|
-
*
|
|
60
|
+
* @returns The removed groups.
|
|
45
61
|
*/
|
|
46
62
|
clearAllGroups(): Promise<WebSocketGroup[]>;
|
|
47
63
|
/**
|
|
@@ -62,14 +78,18 @@ export declare class GroupRegistry {
|
|
|
62
78
|
/**
|
|
63
79
|
* Remove asset subscriptions from every group that contains the asset.
|
|
64
80
|
*
|
|
65
|
-
* It should be only one group that contains the asset, we search all of them
|
|
81
|
+
* It should be only one group that contains the asset, but we search all of them
|
|
66
82
|
* regardless.
|
|
67
83
|
*
|
|
68
|
-
*
|
|
84
|
+
* @param assetIds - The tokenIds of the markets to remove.
|
|
85
|
+
* @param bookCache - The stored orderbook.
|
|
86
|
+
* @returns The list of assetIds that were removed.
|
|
69
87
|
*/
|
|
70
88
|
removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]>;
|
|
71
89
|
/**
|
|
72
90
|
* Disconnect a group.
|
|
91
|
+
*
|
|
92
|
+
* @param group - The group to disconnect.
|
|
73
93
|
*/
|
|
74
94
|
disconnectGroup(group: WebSocketGroup): void;
|
|
75
95
|
/**
|
|
@@ -79,7 +99,7 @@ export declare class GroupRegistry {
|
|
|
79
99
|
* – Dead (but non-empty) groups are reset so that caller can reconnect them.
|
|
80
100
|
* – Pending groups are returned so that caller can connect them.
|
|
81
101
|
*
|
|
82
|
-
*
|
|
102
|
+
* @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
|
|
83
103
|
*/
|
|
84
104
|
getGroupsToReconnectAndCleanup(): Promise<string[]>;
|
|
85
105
|
}
|
|
@@ -7,6 +7,7 @@ exports.GroupRegistry = void 0;
|
|
|
7
7
|
const async_mutex_1 = require("async-mutex");
|
|
8
8
|
const lodash_1 = __importDefault(require("lodash"));
|
|
9
9
|
const uuid_1 = require("uuid");
|
|
10
|
+
const ws_1 = __importDefault(require("ws"));
|
|
10
11
|
const WebSocketSubscriptions_1 = require("../types/WebSocketSubscriptions");
|
|
11
12
|
const logger_1 = require("../logger");
|
|
12
13
|
/*
|
|
@@ -46,7 +47,7 @@ class GroupRegistry {
|
|
|
46
47
|
/**
|
|
47
48
|
* Find the first group with capacity to hold new assets.
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
+
* @returns The groupId if found, otherwise null.
|
|
50
51
|
*/
|
|
51
52
|
findGroupWithCapacity(newAssetLen, maxPerWS) {
|
|
52
53
|
for (const group of wsGroups) {
|
|
@@ -60,7 +61,8 @@ class GroupRegistry {
|
|
|
60
61
|
/**
|
|
61
62
|
* Get the indices of all groups that contain the asset.
|
|
62
63
|
*
|
|
63
|
-
*
|
|
64
|
+
* @param assetId - The tokenId of a market.
|
|
65
|
+
* @returns An array of indices.
|
|
64
66
|
*/
|
|
65
67
|
getGroupIndicesForAsset(assetId) {
|
|
66
68
|
var _a;
|
|
@@ -73,6 +75,9 @@ class GroupRegistry {
|
|
|
73
75
|
}
|
|
74
76
|
/**
|
|
75
77
|
* Check if any group contains the asset.
|
|
78
|
+
*
|
|
79
|
+
* @param assetId - The tokenId of a market.
|
|
80
|
+
* @returns True if found.
|
|
76
81
|
*/
|
|
77
82
|
hasAsset(assetId) {
|
|
78
83
|
return wsGroups.some(group => group.assetIds.has(assetId));
|
|
@@ -80,17 +85,43 @@ class GroupRegistry {
|
|
|
80
85
|
/**
|
|
81
86
|
* Find the group by groupId.
|
|
82
87
|
*
|
|
83
|
-
*
|
|
88
|
+
* @param groupId - The group UUID.
|
|
89
|
+
* @returns The group if found, otherwise undefined.
|
|
84
90
|
*/
|
|
85
91
|
findGroupById(groupId) {
|
|
86
92
|
return wsGroups.find(g => g.groupId === groupId);
|
|
87
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Get statistics about the current state of the registry.
|
|
96
|
+
*
|
|
97
|
+
* Returns an object with:
|
|
98
|
+
* - openWebSockets: The number of websockets that are currently in OPEN state
|
|
99
|
+
* - subscribedAssetIds: The number of unique asset IDs that are currently subscribed
|
|
100
|
+
*/
|
|
101
|
+
getStatistics() {
|
|
102
|
+
let openWebSockets = 0;
|
|
103
|
+
const uniqueAssetIds = new Set();
|
|
104
|
+
for (const group of wsGroups) {
|
|
105
|
+
// Count open websockets
|
|
106
|
+
if (group.wsClient && group.wsClient.readyState === ws_1.default.OPEN) {
|
|
107
|
+
openWebSockets++;
|
|
108
|
+
}
|
|
109
|
+
// Collect unique asset IDs
|
|
110
|
+
for (const assetId of group.assetIds) {
|
|
111
|
+
uniqueAssetIds.add(assetId);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
openWebSockets,
|
|
116
|
+
subscribedAssetIds: uniqueAssetIds.size,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
88
119
|
/**
|
|
89
120
|
* Atomically remove **all** groups from the registry and return them so the
|
|
90
121
|
* caller can perform any asynchronous cleanup (closing sockets, etc.)
|
|
91
122
|
* outside the lock.
|
|
92
123
|
*
|
|
93
|
-
*
|
|
124
|
+
* @returns The removed groups.
|
|
94
125
|
*/
|
|
95
126
|
async clearAllGroups() {
|
|
96
127
|
let removed = [];
|
|
@@ -171,10 +202,12 @@ class GroupRegistry {
|
|
|
171
202
|
/**
|
|
172
203
|
* Remove asset subscriptions from every group that contains the asset.
|
|
173
204
|
*
|
|
174
|
-
* It should be only one group that contains the asset, we search all of them
|
|
205
|
+
* It should be only one group that contains the asset, but we search all of them
|
|
175
206
|
* regardless.
|
|
176
207
|
*
|
|
177
|
-
*
|
|
208
|
+
* @param assetIds - The tokenIds of the markets to remove.
|
|
209
|
+
* @param bookCache - The stored orderbook.
|
|
210
|
+
* @returns The list of assetIds that were removed.
|
|
178
211
|
*/
|
|
179
212
|
async removeAssets(assetIds, bookCache) {
|
|
180
213
|
const removedAssetIds = [];
|
|
@@ -199,11 +232,14 @@ class GroupRegistry {
|
|
|
199
232
|
}
|
|
200
233
|
/**
|
|
201
234
|
* Disconnect a group.
|
|
235
|
+
*
|
|
236
|
+
* @param group - The group to disconnect.
|
|
202
237
|
*/
|
|
203
238
|
disconnectGroup(group) {
|
|
204
239
|
var _a;
|
|
205
240
|
(_a = group.wsClient) === null || _a === void 0 ? void 0 : _a.close();
|
|
206
241
|
group.wsClient = null;
|
|
242
|
+
group.connecting = false;
|
|
207
243
|
logger_1.logger.info({
|
|
208
244
|
message: 'Disconnected group',
|
|
209
245
|
groupId: group.groupId,
|
|
@@ -218,7 +254,7 @@ class GroupRegistry {
|
|
|
218
254
|
* – Dead (but non-empty) groups are reset so that caller can reconnect them.
|
|
219
255
|
* – Pending groups are returned so that caller can connect them.
|
|
220
256
|
*
|
|
221
|
-
*
|
|
257
|
+
* @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
|
|
222
258
|
*/
|
|
223
259
|
async getGroupsToReconnectAndCleanup() {
|
|
224
260
|
const reconnectIds = [];
|
|
@@ -242,6 +278,9 @@ class GroupRegistry {
|
|
|
242
278
|
continue;
|
|
243
279
|
}
|
|
244
280
|
if (group.status === WebSocketSubscriptions_1.WebSocketStatus.PENDING) {
|
|
281
|
+
if (group.connecting) {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
245
284
|
reconnectIds.push(group.groupId);
|
|
246
285
|
}
|
|
247
286
|
}
|
|
@@ -14,9 +14,46 @@ export declare class GroupSocket {
|
|
|
14
14
|
*
|
|
15
15
|
*/
|
|
16
16
|
connect(): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Sets up event handlers for the WebSocket connection.
|
|
19
|
+
*
|
|
20
|
+
* Handles:
|
|
21
|
+
* - 'open': Authenticates and starts ping interval
|
|
22
|
+
* - 'message': Parses and routes events
|
|
23
|
+
* - 'pong': Handles pong responses
|
|
24
|
+
* - 'error': Handles errors
|
|
25
|
+
* - 'close': Handles connection closure
|
|
26
|
+
*/
|
|
17
27
|
private setupEventHandlers;
|
|
28
|
+
/**
|
|
29
|
+
* Handles book events by updating the cache and notifying listeners.
|
|
30
|
+
*
|
|
31
|
+
* @param bookEvents - The book events to process.
|
|
32
|
+
*/
|
|
18
33
|
private handleBookEvents;
|
|
34
|
+
/**
|
|
35
|
+
* Handles tick size change events by notifying listeners.
|
|
36
|
+
*
|
|
37
|
+
* @param tickEvents - The tick size change events to process.
|
|
38
|
+
*/
|
|
19
39
|
private handleTickEvents;
|
|
40
|
+
/**
|
|
41
|
+
* Handles price change events.
|
|
42
|
+
*
|
|
43
|
+
* - Updates the order book cache
|
|
44
|
+
* - Calculates derived price updates based on spread and midpoint
|
|
45
|
+
* - Notifies listeners of price changes and derived updates
|
|
46
|
+
*
|
|
47
|
+
* @param priceChangeEvents - The price change events to process.
|
|
48
|
+
*/
|
|
20
49
|
private handlePriceChangeEvents;
|
|
50
|
+
/**
|
|
51
|
+
* Handles last trade price events.
|
|
52
|
+
*
|
|
53
|
+
* - Notifies listeners of last trade price
|
|
54
|
+
* - Calculates derived price updates based on spread
|
|
55
|
+
*
|
|
56
|
+
* @param lastTradeEvents - The last trade price events to process.
|
|
57
|
+
*/
|
|
21
58
|
private handleLastTradeEvents;
|
|
22
59
|
}
|
|
@@ -24,10 +24,14 @@ class GroupSocket {
|
|
|
24
24
|
*
|
|
25
25
|
*/
|
|
26
26
|
async connect() {
|
|
27
|
+
if (this.group.connecting) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
27
30
|
if (this.group.assetIds.size === 0) {
|
|
28
31
|
this.group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP;
|
|
29
32
|
return;
|
|
30
33
|
}
|
|
34
|
+
this.group.connecting = true;
|
|
31
35
|
try {
|
|
32
36
|
logger_1.logger.info({
|
|
33
37
|
message: 'Connecting to CLOB WebSocket',
|
|
@@ -54,8 +58,21 @@ class GroupSocket {
|
|
|
54
58
|
this.group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD;
|
|
55
59
|
throw err; // caller responsible for error handler
|
|
56
60
|
}
|
|
61
|
+
finally {
|
|
62
|
+
this.group.connecting = false;
|
|
63
|
+
}
|
|
57
64
|
this.setupEventHandlers();
|
|
58
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Sets up event handlers for the WebSocket connection.
|
|
68
|
+
*
|
|
69
|
+
* Handles:
|
|
70
|
+
* - 'open': Authenticates and starts ping interval
|
|
71
|
+
* - 'message': Parses and routes events
|
|
72
|
+
* - 'pong': Handles pong responses
|
|
73
|
+
* - 'error': Handles errors
|
|
74
|
+
* - 'close': Handles connection closure
|
|
75
|
+
*/
|
|
59
76
|
setupEventHandlers() {
|
|
60
77
|
const group = this.group;
|
|
61
78
|
const handlers = this.handlers;
|
|
@@ -220,6 +237,11 @@ class GroupSocket {
|
|
|
220
237
|
return;
|
|
221
238
|
}
|
|
222
239
|
}
|
|
240
|
+
/**
|
|
241
|
+
* Handles book events by updating the cache and notifying listeners.
|
|
242
|
+
*
|
|
243
|
+
* @param bookEvents - The book events to process.
|
|
244
|
+
*/
|
|
223
245
|
async handleBookEvents(bookEvents) {
|
|
224
246
|
var _a, _b;
|
|
225
247
|
if (bookEvents.length) {
|
|
@@ -229,12 +251,26 @@ class GroupSocket {
|
|
|
229
251
|
await ((_b = (_a = this.handlers).onBook) === null || _b === void 0 ? void 0 : _b.call(_a, bookEvents));
|
|
230
252
|
}
|
|
231
253
|
}
|
|
254
|
+
/**
|
|
255
|
+
* Handles tick size change events by notifying listeners.
|
|
256
|
+
*
|
|
257
|
+
* @param tickEvents - The tick size change events to process.
|
|
258
|
+
*/
|
|
232
259
|
async handleTickEvents(tickEvents) {
|
|
233
260
|
var _a, _b;
|
|
234
261
|
if (tickEvents.length) {
|
|
235
262
|
await ((_b = (_a = this.handlers).onTickSizeChange) === null || _b === void 0 ? void 0 : _b.call(_a, tickEvents));
|
|
236
263
|
}
|
|
237
264
|
}
|
|
265
|
+
/**
|
|
266
|
+
* Handles price change events.
|
|
267
|
+
*
|
|
268
|
+
* - Updates the order book cache
|
|
269
|
+
* - Calculates derived price updates based on spread and midpoint
|
|
270
|
+
* - Notifies listeners of price changes and derived updates
|
|
271
|
+
*
|
|
272
|
+
* @param priceChangeEvents - The price change events to process.
|
|
273
|
+
*/
|
|
238
274
|
async handlePriceChangeEvents(priceChangeEvents) {
|
|
239
275
|
var _a, _b, _c, _d;
|
|
240
276
|
if (priceChangeEvents.length) {
|
|
@@ -309,6 +345,14 @@ class GroupSocket {
|
|
|
309
345
|
}
|
|
310
346
|
}
|
|
311
347
|
}
|
|
348
|
+
/**
|
|
349
|
+
* Handles last trade price events.
|
|
350
|
+
*
|
|
351
|
+
* - Notifies listeners of last trade price
|
|
352
|
+
* - Calculates derived price updates based on spread
|
|
353
|
+
*
|
|
354
|
+
* @param lastTradeEvents - The last trade price events to process.
|
|
355
|
+
*/
|
|
312
356
|
async handleLastTradeEvents(lastTradeEvents) {
|
|
313
357
|
var _a, _b, _c, _d;
|
|
314
358
|
if (lastTradeEvents.length) {
|
|
@@ -6,27 +6,27 @@ export interface BookEntry {
|
|
|
6
6
|
midpoint: string | null;
|
|
7
7
|
spread: string | null;
|
|
8
8
|
}
|
|
9
|
-
export declare function sortDescendingInPlace(bookSide: PriceLevel[]): void;
|
|
10
9
|
export declare class OrderBookCache {
|
|
11
10
|
private bookCache;
|
|
12
11
|
constructor();
|
|
13
12
|
/**
|
|
14
13
|
* Replace full book (after a `book` event)
|
|
14
|
+
* @param event new orderbook event
|
|
15
15
|
*/
|
|
16
16
|
replaceBook(event: BookEvent): void;
|
|
17
17
|
/**
|
|
18
18
|
* Update a cached book from a `price_change` event.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
20
|
+
* @param event PriceChangeEvent
|
|
21
|
+
* @returns true if the book was updated.
|
|
22
|
+
* @throws if the book is not found.
|
|
22
23
|
*/
|
|
23
24
|
upsertPriceChange(event: PriceChangeEvent): void;
|
|
24
25
|
/**
|
|
25
|
-
* Return `true` if best-bid/best-ask spread exceeds `cents`.
|
|
26
|
-
*
|
|
27
26
|
* Side effect: updates the book's spread
|
|
28
27
|
*
|
|
29
|
-
*
|
|
28
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
29
|
+
* @throws if either side of the book is empty.
|
|
30
30
|
*/
|
|
31
31
|
spreadOver(assetId: string, cents?: number): boolean;
|
|
32
32
|
/**
|
|
@@ -39,11 +39,16 @@ export declare class OrderBookCache {
|
|
|
39
39
|
* - the midpoint is NaN.
|
|
40
40
|
*/
|
|
41
41
|
midpoint(assetId: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
44
|
+
* otherwise clears all orderbook
|
|
45
|
+
* @param assetId tokenId of a market
|
|
46
|
+
*/
|
|
42
47
|
clear(assetId?: string): void;
|
|
43
48
|
/**
|
|
44
49
|
* Get a book entry by asset id.
|
|
45
50
|
*
|
|
46
|
-
*
|
|
51
|
+
* @returns book entry if found, otherwise null
|
|
47
52
|
*/
|
|
48
53
|
getBookEntry(assetId: string): BookEntry | null;
|
|
49
54
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OrderBookCache = void 0;
|
|
4
|
-
exports.sortDescendingInPlace = sortDescendingInPlace;
|
|
5
4
|
function sortDescendingInPlace(bookSide) {
|
|
6
5
|
bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
|
|
7
6
|
}
|
|
@@ -14,6 +13,7 @@ class OrderBookCache {
|
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
15
|
* Replace full book (after a `book` event)
|
|
16
|
+
* @param event new orderbook event
|
|
17
17
|
*/
|
|
18
18
|
replaceBook(event) {
|
|
19
19
|
let lastPrice = null;
|
|
@@ -43,8 +43,9 @@ class OrderBookCache {
|
|
|
43
43
|
/**
|
|
44
44
|
* Update a cached book from a `price_change` event.
|
|
45
45
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* @param event PriceChangeEvent
|
|
47
|
+
* @returns true if the book was updated.
|
|
48
|
+
* @throws if the book is not found.
|
|
48
49
|
*/
|
|
49
50
|
upsertPriceChange(event) {
|
|
50
51
|
// Iterate through price_changes array
|
|
@@ -79,11 +80,10 @@ class OrderBookCache {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
|
-
* Return `true` if best-bid/best-ask spread exceeds `cents`.
|
|
83
|
-
*
|
|
84
83
|
* Side effect: updates the book's spread
|
|
85
84
|
*
|
|
86
|
-
*
|
|
85
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
86
|
+
* @throws if either side of the book is empty.
|
|
87
87
|
*/
|
|
88
88
|
spreadOver(assetId, cents = 0.1) {
|
|
89
89
|
const book = this.bookCache[assetId];
|
|
@@ -149,6 +149,11 @@ class OrderBookCache {
|
|
|
149
149
|
book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
|
|
150
150
|
return parseFloat(midpoint.toFixed(3)).toString();
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
154
|
+
* otherwise clears all orderbook
|
|
155
|
+
* @param assetId tokenId of a market
|
|
156
|
+
*/
|
|
152
157
|
clear(assetId) {
|
|
153
158
|
if (assetId) {
|
|
154
159
|
delete this.bookCache[assetId];
|
|
@@ -162,7 +167,7 @@ class OrderBookCache {
|
|
|
162
167
|
/**
|
|
163
168
|
* Get a book entry by asset id.
|
|
164
169
|
*
|
|
165
|
-
*
|
|
170
|
+
* @returns book entry if found, otherwise null
|
|
166
171
|
*/
|
|
167
172
|
getBookEntry(assetId) {
|
|
168
173
|
if (!this.bookCache[assetId]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nevuamarkets/poly-websockets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Plug-and-play Polymarket WebSocket price alerts",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc",
|
|
13
13
|
"prepare": "npm run build && npm run test",
|
|
14
|
-
"test": "vitest run"
|
|
14
|
+
"test": "vitest run --exclude LiveData.test.ts",
|
|
15
|
+
"test:live": "vitest run LiveData.test.ts",
|
|
16
|
+
"prepublishOnly": "npm run test:live"
|
|
15
17
|
},
|
|
16
18
|
"repository": {
|
|
17
19
|
"type": "git",
|
|
@@ -79,15 +79,15 @@ class WSSubscriptionManager {
|
|
|
79
79
|
}, this.reconnectAndCleanupIntervalMs);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Clears all WebSocket subscriptions and state.
|
|
84
|
+
*
|
|
85
|
+
* This will:
|
|
86
|
+
*
|
|
87
|
+
* 1. Remove all subscriptions and groups
|
|
88
|
+
* 2. Close all WebSocket connections
|
|
89
|
+
* 3. Clear the order book cache
|
|
90
|
+
*/
|
|
91
91
|
public async clearState() {
|
|
92
92
|
const previousGroups = await this.groupRegistry.clearAllGroups();
|
|
93
93
|
|
|
@@ -100,14 +100,17 @@ class WSSubscriptionManager {
|
|
|
100
100
|
this.bookCache.clear();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
103
|
+
/**
|
|
104
|
+
* This function is called when:
|
|
105
|
+
* - a websocket event is received from the Polymarket WS
|
|
106
|
+
* - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
|
|
107
|
+
* depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
|
|
108
|
+
*
|
|
109
|
+
* The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
|
|
110
|
+
*
|
|
111
|
+
* @param events - The events to process.
|
|
112
|
+
* @param action - The action to perform on the filtered events.
|
|
113
|
+
*/
|
|
111
114
|
private async actOnSubscribedEvents<T extends PolymarketWSEvent | PolymarketPriceUpdateEvent>(events: T[], action?: (events: T[]) => Promise<void>) {
|
|
112
115
|
|
|
113
116
|
// Filter out events that are not subscribed to by any groups
|
|
@@ -148,13 +151,15 @@ class WSSubscriptionManager {
|
|
|
148
151
|
await action?.(events);
|
|
149
152
|
}
|
|
150
153
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Edits wsGroups: Adds new subscriptions.
|
|
156
|
+
*
|
|
157
|
+
* - Filters out assets that are already subscribed
|
|
158
|
+
* - Finds a group with capacity or creates a new one
|
|
159
|
+
* - Creates a new WebSocket client and adds it to the group
|
|
160
|
+
*
|
|
161
|
+
* @param assetIdsToAdd - The asset IDs to add subscriptions for.
|
|
162
|
+
*/
|
|
158
163
|
public async addSubscriptions(assetIdsToAdd: string[]) {
|
|
159
164
|
try {
|
|
160
165
|
const groupIdsToConnect = await this.groupRegistry.addAssets(assetIdsToAdd, this.maxMarketsPerWS);
|
|
@@ -167,11 +172,13 @@ class WSSubscriptionManager {
|
|
|
167
172
|
}
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Edits wsGroups: Removes subscriptions.
|
|
177
|
+
* The group will use the updated subscriptions when it reconnects.
|
|
178
|
+
* We do that because we don't want to miss events by reconnecting.
|
|
179
|
+
*
|
|
180
|
+
* @param assetIdsToRemove - The asset IDs to remove subscriptions for.
|
|
181
|
+
*/
|
|
175
182
|
public async removeSubscriptions(assetIdsToRemove: string[]) {
|
|
176
183
|
try {
|
|
177
184
|
await this.groupRegistry.removeAssets(assetIdsToRemove, this.bookCache);
|
|
@@ -181,12 +188,12 @@ class WSSubscriptionManager {
|
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
/**
|
|
192
|
+
* This function runs periodically and:
|
|
193
|
+
*
|
|
194
|
+
* - Tries to reconnect groups that have assets and are disconnected
|
|
195
|
+
* - Cleans up groups that have no assets
|
|
196
|
+
*/
|
|
190
197
|
private async reconnectAndCleanupGroups() {
|
|
191
198
|
try {
|
|
192
199
|
const reconnectIds = await this.groupRegistry.getGroupsToReconnectAndCleanup();
|
|
@@ -199,6 +206,20 @@ class WSSubscriptionManager {
|
|
|
199
206
|
}
|
|
200
207
|
}
|
|
201
208
|
|
|
209
|
+
/*
|
|
210
|
+
Returns statistics about the current state of the subscription manager.
|
|
211
|
+
|
|
212
|
+
Returns an object with:
|
|
213
|
+
- openWebSockets: The number of websockets that are currently in OPEN state
|
|
214
|
+
- subscribedAssetIds: The number of unique asset IDs that are currently subscribed
|
|
215
|
+
*/
|
|
216
|
+
public getStatistics(): {
|
|
217
|
+
openWebSockets: number;
|
|
218
|
+
subscribedAssetIds: number;
|
|
219
|
+
} {
|
|
220
|
+
return this.groupRegistry.getStatistics();
|
|
221
|
+
}
|
|
222
|
+
|
|
202
223
|
private async createWebSocketClient(groupId: string, handlers: WebSocketHandlers) {
|
|
203
224
|
const group = this.groupRegistry.findGroupById(groupId);
|
|
204
225
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Mutex } from 'async-mutex';
|
|
2
2
|
import _ from 'lodash';
|
|
3
3
|
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
+
import WebSocket from 'ws';
|
|
4
5
|
import { WebSocketGroup, WebSocketStatus } from '../types/WebSocketSubscriptions';
|
|
5
6
|
import { OrderBookCache } from './OrderBookCache';
|
|
6
7
|
import { logger } from '../logger';
|
|
@@ -42,7 +43,7 @@ export class GroupRegistry {
|
|
|
42
43
|
/**
|
|
43
44
|
* Find the first group with capacity to hold new assets.
|
|
44
45
|
*
|
|
45
|
-
*
|
|
46
|
+
* @returns The groupId if found, otherwise null.
|
|
46
47
|
*/
|
|
47
48
|
public findGroupWithCapacity(newAssetLen: number, maxPerWS: number): string | null {
|
|
48
49
|
for (const group of wsGroups) {
|
|
@@ -55,7 +56,8 @@ export class GroupRegistry {
|
|
|
55
56
|
/**
|
|
56
57
|
* Get the indices of all groups that contain the asset.
|
|
57
58
|
*
|
|
58
|
-
*
|
|
59
|
+
* @param assetId - The tokenId of a market.
|
|
60
|
+
* @returns An array of indices.
|
|
59
61
|
*/
|
|
60
62
|
public getGroupIndicesForAsset(assetId: string): number[] {
|
|
61
63
|
const indices: number[] = [];
|
|
@@ -67,6 +69,9 @@ export class GroupRegistry {
|
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
71
|
* Check if any group contains the asset.
|
|
72
|
+
*
|
|
73
|
+
* @param assetId - The tokenId of a market.
|
|
74
|
+
* @returns True if found.
|
|
70
75
|
*/
|
|
71
76
|
public hasAsset(assetId: string): boolean {
|
|
72
77
|
return wsGroups.some(group => group.assetIds.has(assetId));
|
|
@@ -75,18 +80,51 @@ export class GroupRegistry {
|
|
|
75
80
|
/**
|
|
76
81
|
* Find the group by groupId.
|
|
77
82
|
*
|
|
78
|
-
*
|
|
83
|
+
* @param groupId - The group UUID.
|
|
84
|
+
* @returns The group if found, otherwise undefined.
|
|
79
85
|
*/
|
|
80
86
|
public findGroupById(groupId: string): WebSocketGroup | undefined {
|
|
81
87
|
return wsGroups.find(g => g.groupId === groupId);
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Get statistics about the current state of the registry.
|
|
92
|
+
*
|
|
93
|
+
* Returns an object with:
|
|
94
|
+
* - openWebSockets: The number of websockets that are currently in OPEN state
|
|
95
|
+
* - subscribedAssetIds: The number of unique asset IDs that are currently subscribed
|
|
96
|
+
*/
|
|
97
|
+
public getStatistics(): {
|
|
98
|
+
openWebSockets: number;
|
|
99
|
+
subscribedAssetIds: number;
|
|
100
|
+
} {
|
|
101
|
+
let openWebSockets = 0;
|
|
102
|
+
const uniqueAssetIds = new Set<string>();
|
|
103
|
+
|
|
104
|
+
for (const group of wsGroups) {
|
|
105
|
+
// Count open websockets
|
|
106
|
+
if (group.wsClient && group.wsClient.readyState === WebSocket.OPEN) {
|
|
107
|
+
openWebSockets++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Collect unique asset IDs
|
|
111
|
+
for (const assetId of group.assetIds) {
|
|
112
|
+
uniqueAssetIds.add(assetId);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
openWebSockets,
|
|
118
|
+
subscribedAssetIds: uniqueAssetIds.size,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
84
122
|
/**
|
|
85
123
|
* Atomically remove **all** groups from the registry and return them so the
|
|
86
124
|
* caller can perform any asynchronous cleanup (closing sockets, etc.)
|
|
87
125
|
* outside the lock.
|
|
88
126
|
*
|
|
89
|
-
*
|
|
127
|
+
* @returns The removed groups.
|
|
90
128
|
*/
|
|
91
129
|
public async clearAllGroups(): Promise<WebSocketGroup[]> {
|
|
92
130
|
let removed: WebSocketGroup[] = [];
|
|
@@ -179,10 +217,12 @@ export class GroupRegistry {
|
|
|
179
217
|
/**
|
|
180
218
|
* Remove asset subscriptions from every group that contains the asset.
|
|
181
219
|
*
|
|
182
|
-
* It should be only one group that contains the asset, we search all of them
|
|
220
|
+
* It should be only one group that contains the asset, but we search all of them
|
|
183
221
|
* regardless.
|
|
184
222
|
*
|
|
185
|
-
*
|
|
223
|
+
* @param assetIds - The tokenIds of the markets to remove.
|
|
224
|
+
* @param bookCache - The stored orderbook.
|
|
225
|
+
* @returns The list of assetIds that were removed.
|
|
186
226
|
*/
|
|
187
227
|
public async removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]> {
|
|
188
228
|
const removedAssetIds: string[] = [];
|
|
@@ -208,10 +248,13 @@ export class GroupRegistry {
|
|
|
208
248
|
|
|
209
249
|
/**
|
|
210
250
|
* Disconnect a group.
|
|
251
|
+
*
|
|
252
|
+
* @param group - The group to disconnect.
|
|
211
253
|
*/
|
|
212
254
|
public disconnectGroup(group: WebSocketGroup) {
|
|
213
255
|
group.wsClient?.close();
|
|
214
256
|
group.wsClient = null;
|
|
257
|
+
group.connecting = false;
|
|
215
258
|
|
|
216
259
|
logger.info({
|
|
217
260
|
message: 'Disconnected group',
|
|
@@ -227,7 +270,7 @@ export class GroupRegistry {
|
|
|
227
270
|
* – Dead (but non-empty) groups are reset so that caller can reconnect them.
|
|
228
271
|
* – Pending groups are returned so that caller can connect them.
|
|
229
272
|
*
|
|
230
|
-
*
|
|
273
|
+
* @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
|
|
231
274
|
*/
|
|
232
275
|
public async getGroupsToReconnectAndCleanup(): Promise<string[]> {
|
|
233
276
|
const reconnectIds: string[] = [];
|
|
@@ -254,8 +297,10 @@ export class GroupRegistry {
|
|
|
254
297
|
group.assetIds = new Set();
|
|
255
298
|
continue;
|
|
256
299
|
}
|
|
257
|
-
|
|
258
300
|
if (group.status === WebSocketStatus.PENDING) {
|
|
301
|
+
if (group.connecting) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
259
304
|
reconnectIds.push(group.groupId);
|
|
260
305
|
}
|
|
261
306
|
}
|
|
@@ -271,4 +316,4 @@ export class GroupRegistry {
|
|
|
271
316
|
});
|
|
272
317
|
return reconnectIds;
|
|
273
318
|
}
|
|
274
|
-
}
|
|
319
|
+
}
|
|
@@ -37,11 +37,16 @@ export class GroupSocket {
|
|
|
37
37
|
*
|
|
38
38
|
*/
|
|
39
39
|
public async connect(): Promise<void> {
|
|
40
|
+
if (this.group.connecting) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
40
43
|
if (this.group.assetIds.size === 0) {
|
|
41
44
|
this.group.status = WebSocketStatus.CLEANUP;
|
|
42
45
|
return;
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
this.group.connecting = true;
|
|
49
|
+
|
|
45
50
|
try {
|
|
46
51
|
logger.info({
|
|
47
52
|
message: 'Connecting to CLOB WebSocket',
|
|
@@ -66,11 +71,23 @@ export class GroupSocket {
|
|
|
66
71
|
} catch (err) {
|
|
67
72
|
this.group.status = WebSocketStatus.DEAD;
|
|
68
73
|
throw err; // caller responsible for error handler
|
|
74
|
+
} finally {
|
|
75
|
+
this.group.connecting = false;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
this.setupEventHandlers();
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Sets up event handlers for the WebSocket connection.
|
|
83
|
+
*
|
|
84
|
+
* Handles:
|
|
85
|
+
* - 'open': Authenticates and starts ping interval
|
|
86
|
+
* - 'message': Parses and routes events
|
|
87
|
+
* - 'pong': Handles pong responses
|
|
88
|
+
* - 'error': Handles errors
|
|
89
|
+
* - 'close': Handles connection closure
|
|
90
|
+
*/
|
|
74
91
|
private setupEventHandlers() {
|
|
75
92
|
const group = this.group;
|
|
76
93
|
const handlers = this.handlers;
|
|
@@ -249,6 +266,11 @@ export class GroupSocket {
|
|
|
249
266
|
}
|
|
250
267
|
}
|
|
251
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Handles book events by updating the cache and notifying listeners.
|
|
271
|
+
*
|
|
272
|
+
* @param bookEvents - The book events to process.
|
|
273
|
+
*/
|
|
252
274
|
private async handleBookEvents(bookEvents: BookEvent[]): Promise<void> {
|
|
253
275
|
if (bookEvents.length) {
|
|
254
276
|
for (const event of bookEvents) {
|
|
@@ -258,12 +280,26 @@ export class GroupSocket {
|
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Handles tick size change events by notifying listeners.
|
|
285
|
+
*
|
|
286
|
+
* @param tickEvents - The tick size change events to process.
|
|
287
|
+
*/
|
|
261
288
|
private async handleTickEvents(tickEvents: TickSizeChangeEvent[]): Promise<void> {
|
|
262
289
|
if (tickEvents.length) {
|
|
263
290
|
await this.handlers.onTickSizeChange?.(tickEvents);
|
|
264
291
|
}
|
|
265
292
|
}
|
|
266
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Handles price change events.
|
|
296
|
+
*
|
|
297
|
+
* - Updates the order book cache
|
|
298
|
+
* - Calculates derived price updates based on spread and midpoint
|
|
299
|
+
* - Notifies listeners of price changes and derived updates
|
|
300
|
+
*
|
|
301
|
+
* @param priceChangeEvents - The price change events to process.
|
|
302
|
+
*/
|
|
267
303
|
private async handlePriceChangeEvents(priceChangeEvents: PriceChangeEvent[]): Promise<void> {
|
|
268
304
|
if (priceChangeEvents.length) {
|
|
269
305
|
await this.handlers.onPriceChange?.(priceChangeEvents);
|
|
@@ -341,6 +377,14 @@ export class GroupSocket {
|
|
|
341
377
|
}
|
|
342
378
|
}
|
|
343
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Handles last trade price events.
|
|
382
|
+
*
|
|
383
|
+
* - Notifies listeners of last trade price
|
|
384
|
+
* - Calculates derived price updates based on spread
|
|
385
|
+
*
|
|
386
|
+
* @param lastTradeEvents - The last trade price events to process.
|
|
387
|
+
*/
|
|
344
388
|
private async handleLastTradeEvents(lastTradeEvents: LastTradePriceEvent[]): Promise<void> {
|
|
345
389
|
if (lastTradeEvents.length) {
|
|
346
390
|
/*
|
|
@@ -396,4 +440,4 @@ export class GroupSocket {
|
|
|
396
440
|
}
|
|
397
441
|
}
|
|
398
442
|
}
|
|
399
|
-
}
|
|
443
|
+
}
|
|
@@ -16,8 +16,7 @@ export interface BookEntry {
|
|
|
16
16
|
midpoint: string | null;
|
|
17
17
|
spread: string | null;
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
export
|
|
19
|
+
|
|
21
20
|
|
|
22
21
|
function sortDescendingInPlace(bookSide: PriceLevel[]): void {
|
|
23
22
|
bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
|
|
@@ -36,6 +35,7 @@ export class OrderBookCache {
|
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
37
|
* Replace full book (after a `book` event)
|
|
38
|
+
* @param event new orderbook event
|
|
39
39
|
*/
|
|
40
40
|
public replaceBook(event: BookEvent): void {
|
|
41
41
|
let lastPrice = null;
|
|
@@ -68,8 +68,9 @@ export class OrderBookCache {
|
|
|
68
68
|
/**
|
|
69
69
|
* Update a cached book from a `price_change` event.
|
|
70
70
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
71
|
+
* @param event PriceChangeEvent
|
|
72
|
+
* @returns true if the book was updated.
|
|
73
|
+
* @throws if the book is not found.
|
|
73
74
|
*/
|
|
74
75
|
public upsertPriceChange(event: PriceChangeEvent): void {
|
|
75
76
|
// Iterate through price_changes array
|
|
@@ -105,11 +106,10 @@ export class OrderBookCache {
|
|
|
105
106
|
}
|
|
106
107
|
|
|
107
108
|
/**
|
|
108
|
-
* Return `true` if best-bid/best-ask spread exceeds `cents`.
|
|
109
|
-
*
|
|
110
109
|
* Side effect: updates the book's spread
|
|
111
110
|
*
|
|
112
|
-
*
|
|
111
|
+
* @returns `true` if best-bid/best-ask spread exceeds `cents`.
|
|
112
|
+
* @throws if either side of the book is empty.
|
|
113
113
|
*/
|
|
114
114
|
public spreadOver(assetId: string, cents = 0.1): boolean {
|
|
115
115
|
const book = this.bookCache[assetId];
|
|
@@ -183,7 +183,11 @@ export class OrderBookCache {
|
|
|
183
183
|
|
|
184
184
|
return parseFloat(midpoint.toFixed(3)).toString();
|
|
185
185
|
}
|
|
186
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Removes a specific market from the orderbook if assetId is provided
|
|
188
|
+
* otherwise clears all orderbook
|
|
189
|
+
* @param assetId tokenId of a market
|
|
190
|
+
*/
|
|
187
191
|
public clear(assetId?: string): void {
|
|
188
192
|
if (assetId) {
|
|
189
193
|
delete this.bookCache[assetId];
|
|
@@ -197,7 +201,7 @@ export class OrderBookCache {
|
|
|
197
201
|
/**
|
|
198
202
|
* Get a book entry by asset id.
|
|
199
203
|
*
|
|
200
|
-
*
|
|
204
|
+
* @returns book entry if found, otherwise null
|
|
201
205
|
*/
|
|
202
206
|
public getBookEntry(assetId: string): BookEntry | null {
|
|
203
207
|
if (!this.bookCache[assetId]) {
|
|
@@ -13,6 +13,7 @@ export type WebSocketGroup = {
|
|
|
13
13
|
assetIds: Set<string>;
|
|
14
14
|
wsClient: WebSocket | null;
|
|
15
15
|
status: WebSocketStatus;
|
|
16
|
+
connecting?: boolean;
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
export type SubscriptionManagerOptions = {
|
|
@@ -23,4 +24,4 @@ export type SubscriptionManagerOptions = {
|
|
|
23
24
|
|
|
24
25
|
// How many assets to allow per WebSocket
|
|
25
26
|
maxMarketsPerWS?: number;
|
|
26
|
-
}
|
|
27
|
+
}
|