@tentou-tech/poly-websockets 1.0.0
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 +661 -0
- package/README.md +212 -0
- package/dist/WSSubscriptionManager.d.ts +160 -0
- package/dist/WSSubscriptionManager.js +1020 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +21 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +34 -0
- package/dist/modules/OrderBookCache.d.ts +54 -0
- package/dist/modules/OrderBookCache.js +194 -0
- package/dist/types/PolymarketWebSocket.d.ts +460 -0
- package/dist/types/PolymarketWebSocket.js +86 -0
- package/dist/types/WebSocketSubscriptions.d.ts +32 -0
- package/dist/types/WebSocketSubscriptions.js +12 -0
- package/package.json +54 -0
- package/src/WSSubscriptionManager.ts +1126 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +36 -0
- package/src/modules/OrderBookCache.ts +227 -0
- package/src/types/PolymarketWebSocket.ts +538 -0
- package/src/types/WebSocketSubscriptions.ts +35 -0
|
@@ -0,0 +1,1020 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WSSubscriptionManager = void 0;
|
|
7
|
+
const ms_1 = __importDefault(require("ms"));
|
|
8
|
+
const ws_1 = __importDefault(require("ws"));
|
|
9
|
+
const uuid_1 = require("uuid");
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const PolymarketWebSocket_1 = require("./types/PolymarketWebSocket");
|
|
12
|
+
const WebSocketSubscriptions_1 = require("./types/WebSocketSubscriptions");
|
|
13
|
+
const OrderBookCache_1 = require("./modules/OrderBookCache");
|
|
14
|
+
const logger_1 = require("./logger");
|
|
15
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
16
|
+
// Note: We intentionally use a static reconnection interval rather than exponential backoff.
|
|
17
|
+
// Perhaps change this to exponential backoff in the future.
|
|
18
|
+
const DEFAULT_RECONNECT_INTERVAL_MS = (0, ms_1.default)('5s');
|
|
19
|
+
const DEFAULT_PENDING_FLUSH_INTERVAL_MS = (0, ms_1.default)('100ms');
|
|
20
|
+
const CLOB_WSS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market';
|
|
21
|
+
/**
|
|
22
|
+
* WebSocket Subscription Manager for Polymarket CLOB WebSocket.
|
|
23
|
+
*
|
|
24
|
+
* Each instance manages a single WebSocket connection and tracks:
|
|
25
|
+
* - Subscribed assets: Successfully subscribed to the WebSocket
|
|
26
|
+
* - Pending assets: Waiting to be subscribed (batched and flushed periodically)
|
|
27
|
+
*
|
|
28
|
+
* Instances are fully independent - no shared state between managers.
|
|
29
|
+
*/
|
|
30
|
+
class WSSubscriptionManager {
|
|
31
|
+
constructor(userHandlers, options) {
|
|
32
|
+
// WebSocket connection
|
|
33
|
+
this.wsClient = null;
|
|
34
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
35
|
+
this.connecting = false;
|
|
36
|
+
// Asset tracking
|
|
37
|
+
this.subscribedAssetIds = new Set();
|
|
38
|
+
this.pendingSubscribeAssetIds = new Set();
|
|
39
|
+
this.pendingUnsubscribeAssetIds = new Set();
|
|
40
|
+
this.managerId = (0, uuid_1.v4)();
|
|
41
|
+
this.bookCache = new OrderBookCache_1.OrderBookCache();
|
|
42
|
+
this.reconnectIntervalMs = (options === null || options === void 0 ? void 0 : options.reconnectAndCleanupIntervalMs) || DEFAULT_RECONNECT_INTERVAL_MS;
|
|
43
|
+
this.pendingFlushIntervalMs = (options === null || options === void 0 ? void 0 : options.pendingFlushIntervalMs) || DEFAULT_PENDING_FLUSH_INTERVAL_MS;
|
|
44
|
+
this.enableCustomFeatures = (options === null || options === void 0 ? void 0 : options.enableCustomFeatures) || false;
|
|
45
|
+
this.handlers = {
|
|
46
|
+
onBook: async (events) => {
|
|
47
|
+
await this.actOnSubscribedEvents(events, userHandlers.onBook);
|
|
48
|
+
},
|
|
49
|
+
onLastTradePrice: async (events) => {
|
|
50
|
+
await this.actOnSubscribedEvents(events, userHandlers.onLastTradePrice);
|
|
51
|
+
},
|
|
52
|
+
onTickSizeChange: async (events) => {
|
|
53
|
+
await this.actOnSubscribedEvents(events, userHandlers.onTickSizeChange);
|
|
54
|
+
},
|
|
55
|
+
onPriceChange: async (events) => {
|
|
56
|
+
await this.actOnSubscribedEvents(events, userHandlers.onPriceChange);
|
|
57
|
+
},
|
|
58
|
+
onBestBidAsk: async (events) => {
|
|
59
|
+
await this.actOnSubscribedEvents(events, userHandlers.onBestBidAsk);
|
|
60
|
+
},
|
|
61
|
+
onPolymarketPriceUpdate: async (events) => {
|
|
62
|
+
await this.actOnSubscribedEvents(events, userHandlers.onPolymarketPriceUpdate);
|
|
63
|
+
},
|
|
64
|
+
onNewMarket: userHandlers.onNewMarket,
|
|
65
|
+
onMarketResolved: userHandlers.onMarketResolved,
|
|
66
|
+
onWSClose: userHandlers.onWSClose,
|
|
67
|
+
onWSOpen: userHandlers.onWSOpen,
|
|
68
|
+
onError: userHandlers.onError
|
|
69
|
+
};
|
|
70
|
+
// Periodic reconnection check
|
|
71
|
+
this.scheduleReconnectionCheck();
|
|
72
|
+
// Periodic pending flush
|
|
73
|
+
this.pendingFlushInterval = setInterval(() => {
|
|
74
|
+
this.flushPendingSubscriptions();
|
|
75
|
+
}, this.pendingFlushIntervalMs);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Clears all WebSocket subscriptions and state.
|
|
79
|
+
*
|
|
80
|
+
* This will:
|
|
81
|
+
* 1. Stop all timers
|
|
82
|
+
* 2. Close the WebSocket connection
|
|
83
|
+
* 3. Clear all asset tracking
|
|
84
|
+
* 4. Clear the order book cache
|
|
85
|
+
*/
|
|
86
|
+
async clearState() {
|
|
87
|
+
// Stop all timers (reconnectInterval is now a timeout, not interval)
|
|
88
|
+
if (this.reconnectInterval) {
|
|
89
|
+
clearTimeout(this.reconnectInterval);
|
|
90
|
+
this.reconnectInterval = undefined;
|
|
91
|
+
}
|
|
92
|
+
if (this.pendingFlushInterval) {
|
|
93
|
+
clearInterval(this.pendingFlushInterval);
|
|
94
|
+
this.pendingFlushInterval = undefined;
|
|
95
|
+
}
|
|
96
|
+
if (this.pingInterval) {
|
|
97
|
+
clearInterval(this.pingInterval);
|
|
98
|
+
this.pingInterval = undefined;
|
|
99
|
+
}
|
|
100
|
+
if (this.connectionTimeout) {
|
|
101
|
+
clearTimeout(this.connectionTimeout);
|
|
102
|
+
this.connectionTimeout = undefined;
|
|
103
|
+
}
|
|
104
|
+
// Close WebSocket
|
|
105
|
+
if (this.wsClient) {
|
|
106
|
+
this.wsClient.removeAllListeners();
|
|
107
|
+
this.wsClient.close();
|
|
108
|
+
this.wsClient = null;
|
|
109
|
+
}
|
|
110
|
+
// Clear all asset tracking
|
|
111
|
+
this.subscribedAssetIds.clear();
|
|
112
|
+
this.pendingSubscribeAssetIds.clear();
|
|
113
|
+
this.pendingUnsubscribeAssetIds.clear();
|
|
114
|
+
// Reset status
|
|
115
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
116
|
+
this.connecting = false;
|
|
117
|
+
// Clear the order book cache
|
|
118
|
+
this.bookCache.clear();
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Filters events to only include those for subscribed assets.
|
|
122
|
+
* Wraps user handler calls in try-catch to prevent user errors from breaking internal logic.
|
|
123
|
+
* Does not call the handler if all events are filtered out.
|
|
124
|
+
*/
|
|
125
|
+
async actOnSubscribedEvents(events, action) {
|
|
126
|
+
events = events.filter((event) => {
|
|
127
|
+
if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) {
|
|
128
|
+
return event.price_changes.some(pc => this.subscribedAssetIds.has(pc.asset_id));
|
|
129
|
+
}
|
|
130
|
+
if ('asset_id' in event) {
|
|
131
|
+
return this.subscribedAssetIds.has(event.asset_id);
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
});
|
|
135
|
+
// Skip if no events passed the filter
|
|
136
|
+
if (events.length === 0) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
// Wrap user handler calls in try-catch
|
|
140
|
+
try {
|
|
141
|
+
await (action === null || action === void 0 ? void 0 : action(events));
|
|
142
|
+
}
|
|
143
|
+
catch (handlerErr) {
|
|
144
|
+
logger_1.logger.warn({
|
|
145
|
+
message: 'Error in user event handler',
|
|
146
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
147
|
+
managerId: this.managerId,
|
|
148
|
+
eventCount: events.length,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Adds new subscriptions.
|
|
154
|
+
*
|
|
155
|
+
* Assets are added to a pending queue and will be subscribed when:
|
|
156
|
+
* - The WebSocket connects (initial subscription)
|
|
157
|
+
* - The pending flush timer fires (for new assets on an existing connection)
|
|
158
|
+
*
|
|
159
|
+
* @param assetIdsToAdd - The asset IDs to add subscriptions for.
|
|
160
|
+
*/
|
|
161
|
+
async addSubscriptions(assetIdsToAdd) {
|
|
162
|
+
try {
|
|
163
|
+
for (const assetId of assetIdsToAdd) {
|
|
164
|
+
// Remove from pending unsubscribe if it's there (cancel the unsubscription)
|
|
165
|
+
// This must happen BEFORE the subscribed check, so that adding an asset
|
|
166
|
+
// that was pending unsubscribe correctly cancels the unsubscription.
|
|
167
|
+
this.pendingUnsubscribeAssetIds.delete(assetId);
|
|
168
|
+
// Skip if already subscribed (pending subscribe is safe to re-add due to Set behavior)
|
|
169
|
+
if (this.subscribedAssetIds.has(assetId))
|
|
170
|
+
continue;
|
|
171
|
+
// Add to pending subscribe (no-op if already pending due to Set)
|
|
172
|
+
this.pendingSubscribeAssetIds.add(assetId);
|
|
173
|
+
}
|
|
174
|
+
// Restart intervals if they were cleared (e.g. after clearState)
|
|
175
|
+
this.ensureIntervalsRunning();
|
|
176
|
+
// Ensure we have a connection
|
|
177
|
+
if (!this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN) {
|
|
178
|
+
await this.connect();
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
const msg = `Error adding subscriptions: ${error instanceof Error ? error.message : String(error)}`;
|
|
183
|
+
await this.safeCallErrorHandler(new Error(msg));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Ensures the periodic intervals are running.
|
|
188
|
+
* Called after clearState() when new subscriptions are added.
|
|
189
|
+
*/
|
|
190
|
+
ensureIntervalsRunning() {
|
|
191
|
+
if (!this.reconnectInterval) {
|
|
192
|
+
this.scheduleReconnectionCheck();
|
|
193
|
+
}
|
|
194
|
+
if (!this.pendingFlushInterval) {
|
|
195
|
+
this.pendingFlushInterval = setInterval(() => {
|
|
196
|
+
this.flushPendingSubscriptions();
|
|
197
|
+
}, this.pendingFlushIntervalMs);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Schedules the next reconnection check.
|
|
202
|
+
* Uses a fixed interval (default 5 seconds) between checks.
|
|
203
|
+
*/
|
|
204
|
+
scheduleReconnectionCheck() {
|
|
205
|
+
if (this.reconnectInterval) {
|
|
206
|
+
clearTimeout(this.reconnectInterval);
|
|
207
|
+
}
|
|
208
|
+
this.reconnectInterval = setTimeout(async () => {
|
|
209
|
+
await this.checkReconnection();
|
|
210
|
+
// Schedule next check (only if not cleared)
|
|
211
|
+
if (this.reconnectInterval) {
|
|
212
|
+
this.scheduleReconnectionCheck();
|
|
213
|
+
}
|
|
214
|
+
}, this.reconnectIntervalMs);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Safely calls the error handler, catching any exceptions thrown by it.
|
|
218
|
+
* Prevents user handler exceptions from breaking internal logic.
|
|
219
|
+
*/
|
|
220
|
+
async safeCallErrorHandler(error) {
|
|
221
|
+
var _a, _b;
|
|
222
|
+
try {
|
|
223
|
+
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error));
|
|
224
|
+
}
|
|
225
|
+
catch (handlerErr) {
|
|
226
|
+
logger_1.logger.warn({
|
|
227
|
+
message: 'Error in onError handler',
|
|
228
|
+
originalError: error.message,
|
|
229
|
+
handlerError: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
230
|
+
managerId: this.managerId,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Removes subscriptions.
|
|
236
|
+
*
|
|
237
|
+
* @param assetIdsToRemove - The asset IDs to remove subscriptions for.
|
|
238
|
+
*/
|
|
239
|
+
async removeSubscriptions(assetIdsToRemove) {
|
|
240
|
+
try {
|
|
241
|
+
for (const assetId of assetIdsToRemove) {
|
|
242
|
+
// Remove from pending subscribe if it's there
|
|
243
|
+
if (this.pendingSubscribeAssetIds.delete(assetId)) {
|
|
244
|
+
continue; // Was only pending, no need to send unsubscribe
|
|
245
|
+
}
|
|
246
|
+
// If subscribed, add to pending unsubscribe
|
|
247
|
+
if (this.subscribedAssetIds.has(assetId)) {
|
|
248
|
+
this.pendingUnsubscribeAssetIds.add(assetId);
|
|
249
|
+
}
|
|
250
|
+
// Note: We don't clear the book cache here because the unsubscription
|
|
251
|
+
// hasn't been sent yet. The cache entry will be cleared when the
|
|
252
|
+
// unsubscription is flushed (after the asset is removed from subscribedAssetIds).
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
const errMsg = `Error removing subscriptions: ${error instanceof Error ? error.message : String(error)}`;
|
|
257
|
+
await this.safeCallErrorHandler(new Error(errMsg));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Get all currently monitored asset IDs.
|
|
262
|
+
* This includes both successfully subscribed assets and pending subscriptions.
|
|
263
|
+
*
|
|
264
|
+
* @returns Array of asset IDs being monitored.
|
|
265
|
+
*/
|
|
266
|
+
getAssetIds() {
|
|
267
|
+
const allAssets = new Set(this.subscribedAssetIds);
|
|
268
|
+
for (const assetId of this.pendingSubscribeAssetIds) {
|
|
269
|
+
allAssets.add(assetId);
|
|
270
|
+
}
|
|
271
|
+
// Exclude pending unsubscribes
|
|
272
|
+
for (const assetId of this.pendingUnsubscribeAssetIds) {
|
|
273
|
+
allAssets.delete(assetId);
|
|
274
|
+
}
|
|
275
|
+
return Array.from(allAssets);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Returns statistics about the current state of the subscription manager.
|
|
279
|
+
*/
|
|
280
|
+
getStatistics() {
|
|
281
|
+
var _a;
|
|
282
|
+
const isOpen = ((_a = this.wsClient) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN;
|
|
283
|
+
return {
|
|
284
|
+
openWebSockets: isOpen ? 1 : 0,
|
|
285
|
+
assetIds: this.getAssetIds().length,
|
|
286
|
+
pendingSubscribeCount: this.pendingSubscribeAssetIds.size,
|
|
287
|
+
pendingUnsubscribeCount: this.pendingUnsubscribeAssetIds.size,
|
|
288
|
+
pendingAssetIds: this.pendingSubscribeAssetIds.size + this.pendingUnsubscribeAssetIds.size,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Flush pending subscriptions and unsubscriptions to the WebSocket.
|
|
293
|
+
*
|
|
294
|
+
* SUBSCRIPTION PROTOCOL NOTE:
|
|
295
|
+
* The Polymarket WebSocket protocol does NOT send any confirmation or acknowledgment
|
|
296
|
+
* messages for subscribe/unsubscribe operations. The server silently processes these
|
|
297
|
+
* requests. We optimistically assume success after sending. If the server rejects
|
|
298
|
+
* a request (e.g., invalid asset ID), events for those assets simply won't arrive -
|
|
299
|
+
* there is no error response to handle.
|
|
300
|
+
*
|
|
301
|
+
* This means:
|
|
302
|
+
* - We cannot definitively know if a subscription succeeded
|
|
303
|
+
* - We cannot definitively know if an unsubscription succeeded
|
|
304
|
+
* - The only indication of failure is the absence of expected events
|
|
305
|
+
*/
|
|
306
|
+
flushPendingSubscriptions() {
|
|
307
|
+
if (!this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// Process unsubscriptions first
|
|
311
|
+
if (this.pendingUnsubscribeAssetIds.size > 0) {
|
|
312
|
+
const toUnsubscribe = Array.from(this.pendingUnsubscribeAssetIds);
|
|
313
|
+
const message = {
|
|
314
|
+
operation: 'unsubscribe',
|
|
315
|
+
assets_ids: toUnsubscribe,
|
|
316
|
+
custom_feature_enabled: this.enableCustomFeatures,
|
|
317
|
+
};
|
|
318
|
+
// IMPORTANT: The Polymarket WebSocket protocol does NOT send any confirmation
|
|
319
|
+
// or acknowledgment message for subscribe/unsubscribe operations. The server
|
|
320
|
+
// silently accepts the request. We optimistically assume success after sending.
|
|
321
|
+
// If the server rejects the request, events for those assets simply won't arrive
|
|
322
|
+
// (there is no error response to handle).
|
|
323
|
+
try {
|
|
324
|
+
this.wsClient.send(JSON.stringify(message));
|
|
325
|
+
// Remove from subscribed, clear pending, and clear book cache for unsubscribed assets
|
|
326
|
+
for (const assetId of toUnsubscribe) {
|
|
327
|
+
this.subscribedAssetIds.delete(assetId);
|
|
328
|
+
this.bookCache.clear(assetId);
|
|
329
|
+
}
|
|
330
|
+
this.pendingUnsubscribeAssetIds.clear();
|
|
331
|
+
logger_1.logger.info({
|
|
332
|
+
message: `Unsubscribed from ${toUnsubscribe.length} asset(s)`,
|
|
333
|
+
managerId: this.managerId,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
logger_1.logger.warn({
|
|
338
|
+
message: 'Failed to send unsubscribe message',
|
|
339
|
+
error: error instanceof Error ? error.message : String(error),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// Process subscriptions
|
|
344
|
+
if (this.pendingSubscribeAssetIds.size > 0) {
|
|
345
|
+
const toSubscribe = Array.from(this.pendingSubscribeAssetIds);
|
|
346
|
+
const message = {
|
|
347
|
+
operation: 'subscribe',
|
|
348
|
+
assets_ids: toSubscribe,
|
|
349
|
+
custom_feature_enabled: this.enableCustomFeatures,
|
|
350
|
+
};
|
|
351
|
+
// IMPORTANT: The Polymarket WebSocket protocol does NOT send any confirmation
|
|
352
|
+
// or acknowledgment message for subscribe/unsubscribe operations. The server
|
|
353
|
+
// silently accepts the request. We optimistically assume success after sending.
|
|
354
|
+
// If the server rejects the request, events for those assets simply won't arrive
|
|
355
|
+
// (there is no error response to handle).
|
|
356
|
+
try {
|
|
357
|
+
this.wsClient.send(JSON.stringify(message));
|
|
358
|
+
// Move to subscribed and clear pending
|
|
359
|
+
for (const assetId of toSubscribe) {
|
|
360
|
+
this.subscribedAssetIds.add(assetId);
|
|
361
|
+
}
|
|
362
|
+
this.pendingSubscribeAssetIds.clear();
|
|
363
|
+
logger_1.logger.info({
|
|
364
|
+
message: `Subscribed to ${toSubscribe.length} asset(s)`,
|
|
365
|
+
managerId: this.managerId,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
catch (error) {
|
|
369
|
+
logger_1.logger.warn({
|
|
370
|
+
message: 'Failed to send subscribe message',
|
|
371
|
+
error: error instanceof Error ? error.message : String(error),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Close WebSocket if no assets remain and no pending unsubscriptions
|
|
376
|
+
// (pendingUnsubscribeAssetIds is already cleared at this point)
|
|
377
|
+
if (this.subscribedAssetIds.size === 0 &&
|
|
378
|
+
this.pendingSubscribeAssetIds.size === 0 &&
|
|
379
|
+
this.pendingUnsubscribeAssetIds.size === 0) {
|
|
380
|
+
this.closeWebSocket();
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Closes the WebSocket connection and cleans up related resources.
|
|
385
|
+
*/
|
|
386
|
+
closeWebSocket() {
|
|
387
|
+
if (this.wsClient) {
|
|
388
|
+
logger_1.logger.info({
|
|
389
|
+
message: 'Closing WebSocket - no assets to monitor',
|
|
390
|
+
managerId: this.managerId,
|
|
391
|
+
});
|
|
392
|
+
this.wsClient.removeAllListeners();
|
|
393
|
+
this.wsClient.close();
|
|
394
|
+
this.wsClient = null;
|
|
395
|
+
}
|
|
396
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
397
|
+
this.connecting = false;
|
|
398
|
+
if (this.pingInterval) {
|
|
399
|
+
clearInterval(this.pingInterval);
|
|
400
|
+
this.pingInterval = undefined;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check if we need to reconnect.
|
|
405
|
+
* Note: Assets are moved to pending in handleClose/handleError handlers,
|
|
406
|
+
* so this method only needs to check if reconnection is needed.
|
|
407
|
+
*/
|
|
408
|
+
async checkReconnection() {
|
|
409
|
+
// If we have pending assets but no connection, reconnect
|
|
410
|
+
const hasPendingAssets = this.pendingSubscribeAssetIds.size > 0;
|
|
411
|
+
const isDisconnected = !this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN;
|
|
412
|
+
if (hasPendingAssets && isDisconnected && !this.connecting) {
|
|
413
|
+
logger_1.logger.info({
|
|
414
|
+
message: 'Reconnection check - attempting to reconnect',
|
|
415
|
+
managerId: this.managerId,
|
|
416
|
+
pendingCount: this.pendingSubscribeAssetIds.size,
|
|
417
|
+
});
|
|
418
|
+
// Clear stale book cache data - will be repopulated after reconnection
|
|
419
|
+
this.bookCache.clear();
|
|
420
|
+
await this.connect();
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Establish the WebSocket connection.
|
|
425
|
+
*/
|
|
426
|
+
async connect() {
|
|
427
|
+
var _a;
|
|
428
|
+
if (this.connecting) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (((_a = this.wsClient) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// No assets to subscribe to
|
|
435
|
+
if (this.pendingSubscribeAssetIds.size === 0 && this.subscribedAssetIds.size === 0) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
this.connecting = true;
|
|
439
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.CONNECTING;
|
|
440
|
+
try {
|
|
441
|
+
logger_1.logger.info({
|
|
442
|
+
message: 'Connecting to CLOB WebSocket',
|
|
443
|
+
managerId: this.managerId,
|
|
444
|
+
pendingAssetCount: this.pendingSubscribeAssetIds.size,
|
|
445
|
+
});
|
|
446
|
+
this.wsClient = new ws_1.default(CLOB_WSS_URL);
|
|
447
|
+
// Set up event handlers immediately (handlers are set up before any events can fire)
|
|
448
|
+
this.setupEventHandlers();
|
|
449
|
+
// Connection timeout
|
|
450
|
+
this.connectionTimeout = setTimeout(async () => {
|
|
451
|
+
if (this.connecting && this.wsClient && this.wsClient.readyState !== ws_1.default.OPEN) {
|
|
452
|
+
logger_1.logger.warn({
|
|
453
|
+
message: 'WebSocket connection timeout',
|
|
454
|
+
managerId: this.managerId,
|
|
455
|
+
});
|
|
456
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
457
|
+
this.connecting = false;
|
|
458
|
+
if (this.wsClient) {
|
|
459
|
+
this.wsClient.removeAllListeners();
|
|
460
|
+
this.wsClient.close();
|
|
461
|
+
this.wsClient = null;
|
|
462
|
+
}
|
|
463
|
+
// Notify error handler about the timeout
|
|
464
|
+
await this.safeCallErrorHandler(new Error('WebSocket connection timeout after 30s'));
|
|
465
|
+
}
|
|
466
|
+
}, (0, ms_1.default)('30s'));
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
470
|
+
this.connecting = false;
|
|
471
|
+
throw err;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Sets up event handlers for the WebSocket connection.
|
|
476
|
+
*/
|
|
477
|
+
setupEventHandlers() {
|
|
478
|
+
const ws = this.wsClient;
|
|
479
|
+
if (!ws)
|
|
480
|
+
return;
|
|
481
|
+
const handleOpen = async () => {
|
|
482
|
+
var _a, _b;
|
|
483
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.CONNECTED;
|
|
484
|
+
this.connecting = false;
|
|
485
|
+
if (this.connectionTimeout) {
|
|
486
|
+
clearTimeout(this.connectionTimeout);
|
|
487
|
+
this.connectionTimeout = undefined;
|
|
488
|
+
}
|
|
489
|
+
// Send an empty MarketSubscriptionMessage as the initial handshake.
|
|
490
|
+
// The Polymarket WebSocket protocol requires a 'market' type message as the first message.
|
|
491
|
+
// We send it with an empty assets_ids array, and then use 'subscribe' operation messages
|
|
492
|
+
// for all actual subscriptions (via flushPendingSubscriptions) to keep the subscription
|
|
493
|
+
// logic consistent in one place.
|
|
494
|
+
try {
|
|
495
|
+
const initMessage = {
|
|
496
|
+
assets_ids: [],
|
|
497
|
+
type: 'market',
|
|
498
|
+
custom_feature_enabled: this.enableCustomFeatures,
|
|
499
|
+
};
|
|
500
|
+
ws.send(JSON.stringify(initMessage));
|
|
501
|
+
}
|
|
502
|
+
catch (error) {
|
|
503
|
+
logger_1.logger.warn({
|
|
504
|
+
message: 'Failed to send initial market message',
|
|
505
|
+
error: error instanceof Error ? error.message : String(error),
|
|
506
|
+
managerId: this.managerId,
|
|
507
|
+
});
|
|
508
|
+
// Close and let reconnection logic handle retry
|
|
509
|
+
// Wrap in try-catch as close() can throw in edge cases
|
|
510
|
+
try {
|
|
511
|
+
ws.close();
|
|
512
|
+
}
|
|
513
|
+
catch (closeErr) {
|
|
514
|
+
logger_1.logger.debug({
|
|
515
|
+
message: 'Error closing WebSocket after init message failure (safe to ignore)',
|
|
516
|
+
error: closeErr instanceof Error ? closeErr.message : String(closeErr),
|
|
517
|
+
managerId: this.managerId,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const pendingAssets = Array.from(this.pendingSubscribeAssetIds);
|
|
523
|
+
// Safely call open handler
|
|
524
|
+
try {
|
|
525
|
+
await ((_b = (_a = this.handlers).onWSOpen) === null || _b === void 0 ? void 0 : _b.call(_a, this.managerId, pendingAssets));
|
|
526
|
+
}
|
|
527
|
+
catch (handlerErr) {
|
|
528
|
+
logger_1.logger.warn({
|
|
529
|
+
message: 'Error in onWSOpen handler',
|
|
530
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
531
|
+
managerId: this.managerId,
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
// Immediately flush pending subscriptions now that we're connected
|
|
535
|
+
this.flushPendingSubscriptions();
|
|
536
|
+
// Start ping interval with jitter per-ping
|
|
537
|
+
const basePingIntervalMs = (0, ms_1.default)('20s');
|
|
538
|
+
this.pingInterval = setInterval(() => {
|
|
539
|
+
if (!ws || ws.readyState !== ws_1.default.OPEN) {
|
|
540
|
+
if (this.pingInterval) {
|
|
541
|
+
clearInterval(this.pingInterval);
|
|
542
|
+
this.pingInterval = undefined;
|
|
543
|
+
}
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// Add jitter by randomly delaying the ping within a ±5s window
|
|
547
|
+
const jitterMs = (0, crypto_1.randomInt)(0, (0, ms_1.default)('5s'));
|
|
548
|
+
setTimeout(() => {
|
|
549
|
+
if (ws && ws.readyState === ws_1.default.OPEN) {
|
|
550
|
+
try {
|
|
551
|
+
ws.ping();
|
|
552
|
+
}
|
|
553
|
+
catch (pingErr) {
|
|
554
|
+
// Ping can fail if the socket is closing or in a bad state.
|
|
555
|
+
// This is not critical - the socket will be cleaned up on close/error events.
|
|
556
|
+
logger_1.logger.debug({
|
|
557
|
+
message: 'Ping failed (safe to ignore)',
|
|
558
|
+
error: pingErr instanceof Error ? pingErr.message : String(pingErr),
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}, jitterMs);
|
|
563
|
+
}, basePingIntervalMs);
|
|
564
|
+
};
|
|
565
|
+
const handleMessage = async (data) => {
|
|
566
|
+
try {
|
|
567
|
+
const messageStr = data.toString();
|
|
568
|
+
const normalizedMessageStr = messageStr.trim().toUpperCase();
|
|
569
|
+
if (normalizedMessageStr === 'PONG') {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
let events = [];
|
|
573
|
+
try {
|
|
574
|
+
const parsedData = JSON.parse(messageStr);
|
|
575
|
+
events = Array.isArray(parsedData) ? parsedData : [parsedData];
|
|
576
|
+
}
|
|
577
|
+
catch (err) {
|
|
578
|
+
await this.safeCallErrorHandler(new Error(`Not JSON: ${messageStr}`));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
events = lodash_1.default.filter(events, (event) => {
|
|
582
|
+
if (!event)
|
|
583
|
+
return false;
|
|
584
|
+
if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) {
|
|
585
|
+
return event.price_changes && event.price_changes.length > 0;
|
|
586
|
+
}
|
|
587
|
+
// Market-level events don't have asset_id, so include them
|
|
588
|
+
if ((0, PolymarketWebSocket_1.isNewMarketEvent)(event) || (0, PolymarketWebSocket_1.isMarketResolvedEvent)(event)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
// Asset-level events must have an asset_id
|
|
592
|
+
return 'asset_id' in event && lodash_1.default.size(event.asset_id) > 0;
|
|
593
|
+
});
|
|
594
|
+
const bookEvents = [];
|
|
595
|
+
const lastTradeEvents = [];
|
|
596
|
+
const tickEvents = [];
|
|
597
|
+
const priceChangeEvents = [];
|
|
598
|
+
const bestBidAskEvents = [];
|
|
599
|
+
const newMarketEvents = [];
|
|
600
|
+
const marketResolvedEvents = [];
|
|
601
|
+
for (const event of events) {
|
|
602
|
+
if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) {
|
|
603
|
+
const relevantChanges = event.price_changes.filter(pc => this.subscribedAssetIds.has(pc.asset_id));
|
|
604
|
+
if (relevantChanges.length === 0)
|
|
605
|
+
continue;
|
|
606
|
+
priceChangeEvents.push({
|
|
607
|
+
...event,
|
|
608
|
+
price_changes: relevantChanges
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
else if ((0, PolymarketWebSocket_1.isNewMarketEvent)(event)) {
|
|
612
|
+
// Market-level events bypass asset filtering
|
|
613
|
+
newMarketEvents.push(event);
|
|
614
|
+
}
|
|
615
|
+
else if ((0, PolymarketWebSocket_1.isMarketResolvedEvent)(event)) {
|
|
616
|
+
// Market-level events bypass asset filtering
|
|
617
|
+
marketResolvedEvents.push(event);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Safely check asset_id existence
|
|
621
|
+
const assetId = 'asset_id' in event ? event.asset_id : undefined;
|
|
622
|
+
if (!assetId || !this.subscribedAssetIds.has(assetId))
|
|
623
|
+
continue;
|
|
624
|
+
if ((0, PolymarketWebSocket_1.isBookEvent)(event)) {
|
|
625
|
+
bookEvents.push(event);
|
|
626
|
+
}
|
|
627
|
+
else if ((0, PolymarketWebSocket_1.isLastTradePriceEvent)(event)) {
|
|
628
|
+
lastTradeEvents.push(event);
|
|
629
|
+
}
|
|
630
|
+
else if ((0, PolymarketWebSocket_1.isTickSizeChangeEvent)(event)) {
|
|
631
|
+
tickEvents.push(event);
|
|
632
|
+
}
|
|
633
|
+
else if ((0, PolymarketWebSocket_1.isBestBidAskEvent)(event)) {
|
|
634
|
+
bestBidAskEvents.push(event);
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
await this.safeCallErrorHandler(new Error(`Unknown event: ${JSON.stringify(event)}`));
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
// Wrap each handler call in try-catch to prevent one failure
|
|
642
|
+
// from breaking the entire event loop
|
|
643
|
+
try {
|
|
644
|
+
await this.handleBookEvents(bookEvents);
|
|
645
|
+
}
|
|
646
|
+
catch (err) {
|
|
647
|
+
logger_1.logger.warn({
|
|
648
|
+
message: 'Error in handleBookEvents',
|
|
649
|
+
error: err instanceof Error ? err.message : String(err),
|
|
650
|
+
managerId: this.managerId,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
try {
|
|
654
|
+
await this.handleTickEvents(tickEvents);
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
logger_1.logger.warn({
|
|
658
|
+
message: 'Error in handleTickEvents',
|
|
659
|
+
error: err instanceof Error ? err.message : String(err),
|
|
660
|
+
managerId: this.managerId,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
try {
|
|
664
|
+
await this.handlePriceChangeEvents(priceChangeEvents);
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
logger_1.logger.warn({
|
|
668
|
+
message: 'Error in handlePriceChangeEvents',
|
|
669
|
+
error: err instanceof Error ? err.message : String(err),
|
|
670
|
+
managerId: this.managerId,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
await this.handleLastTradeEvents(lastTradeEvents);
|
|
675
|
+
}
|
|
676
|
+
catch (err) {
|
|
677
|
+
logger_1.logger.warn({
|
|
678
|
+
message: 'Error in handleLastTradeEvents',
|
|
679
|
+
error: err instanceof Error ? err.message : String(err),
|
|
680
|
+
managerId: this.managerId,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
try {
|
|
684
|
+
await this.handleBestBidAskEvents(bestBidAskEvents);
|
|
685
|
+
}
|
|
686
|
+
catch (err) {
|
|
687
|
+
logger_1.logger.warn({
|
|
688
|
+
message: 'Error in handleBestBidAskEvents',
|
|
689
|
+
error: err instanceof Error ? err.message : String(err),
|
|
690
|
+
managerId: this.managerId,
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
await this.handleNewMarketEvents(newMarketEvents);
|
|
695
|
+
}
|
|
696
|
+
catch (err) {
|
|
697
|
+
logger_1.logger.warn({
|
|
698
|
+
message: 'Error in handleNewMarketEvents',
|
|
699
|
+
error: err instanceof Error ? err.message : String(err),
|
|
700
|
+
managerId: this.managerId,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
try {
|
|
704
|
+
await this.handleMarketResolvedEvents(marketResolvedEvents);
|
|
705
|
+
}
|
|
706
|
+
catch (err) {
|
|
707
|
+
logger_1.logger.warn({
|
|
708
|
+
message: 'Error in handleMarketResolvedEvents',
|
|
709
|
+
error: err instanceof Error ? err.message : String(err),
|
|
710
|
+
managerId: this.managerId,
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
await this.safeCallErrorHandler(new Error(`Error handling message: ${err}`));
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
const handlePong = () => {
|
|
719
|
+
// Pong received - connection is alive
|
|
720
|
+
};
|
|
721
|
+
const handleError = async (err) => {
|
|
722
|
+
var _a, _b;
|
|
723
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
724
|
+
this.connecting = false;
|
|
725
|
+
// Clean up WebSocket reference - close the socket before nullifying
|
|
726
|
+
// Wrap in try-catch because close() can throw if socket is in a bad state
|
|
727
|
+
if (this.wsClient) {
|
|
728
|
+
this.wsClient.removeAllListeners();
|
|
729
|
+
try {
|
|
730
|
+
this.wsClient.close();
|
|
731
|
+
}
|
|
732
|
+
catch (closeErr) {
|
|
733
|
+
logger_1.logger.debug({
|
|
734
|
+
message: 'Error closing WebSocket in error handler (safe to ignore)',
|
|
735
|
+
error: closeErr instanceof Error ? closeErr.message : String(closeErr),
|
|
736
|
+
managerId: this.managerId,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
this.wsClient = null;
|
|
740
|
+
}
|
|
741
|
+
if (this.pingInterval) {
|
|
742
|
+
clearInterval(this.pingInterval);
|
|
743
|
+
this.pingInterval = undefined;
|
|
744
|
+
}
|
|
745
|
+
if (this.connectionTimeout) {
|
|
746
|
+
clearTimeout(this.connectionTimeout);
|
|
747
|
+
this.connectionTimeout = undefined;
|
|
748
|
+
}
|
|
749
|
+
// Move subscribed assets back to pending for re-subscription,
|
|
750
|
+
// but skip assets that user wanted to unsubscribe (preserve user intent)
|
|
751
|
+
for (const assetId of this.subscribedAssetIds) {
|
|
752
|
+
if (!this.pendingUnsubscribeAssetIds.has(assetId)) {
|
|
753
|
+
this.pendingSubscribeAssetIds.add(assetId);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
this.subscribedAssetIds.clear();
|
|
757
|
+
// Clear pending unsubscribes - they were either:
|
|
758
|
+
// 1. Successfully excluded from re-subscription (above), or
|
|
759
|
+
// 2. Were never subscribed anyway (user's intent is preserved)
|
|
760
|
+
this.pendingUnsubscribeAssetIds.clear();
|
|
761
|
+
// Clear the book cache - data is stale and will be repopulated on reconnection
|
|
762
|
+
this.bookCache.clear();
|
|
763
|
+
// Safely call error handler
|
|
764
|
+
try {
|
|
765
|
+
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(`WebSocket error: ${err.message}`)));
|
|
766
|
+
}
|
|
767
|
+
catch (handlerErr) {
|
|
768
|
+
logger_1.logger.warn({
|
|
769
|
+
message: 'Error in onError handler',
|
|
770
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
771
|
+
managerId: this.managerId,
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
const handleClose = async (code, reason) => {
|
|
776
|
+
var _a, _b;
|
|
777
|
+
this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED;
|
|
778
|
+
this.connecting = false;
|
|
779
|
+
// Clean up WebSocket reference
|
|
780
|
+
if (this.wsClient) {
|
|
781
|
+
this.wsClient.removeAllListeners();
|
|
782
|
+
this.wsClient = null;
|
|
783
|
+
}
|
|
784
|
+
if (this.pingInterval) {
|
|
785
|
+
clearInterval(this.pingInterval);
|
|
786
|
+
this.pingInterval = undefined;
|
|
787
|
+
}
|
|
788
|
+
if (this.connectionTimeout) {
|
|
789
|
+
clearTimeout(this.connectionTimeout);
|
|
790
|
+
this.connectionTimeout = undefined;
|
|
791
|
+
}
|
|
792
|
+
// Move subscribed assets back to pending for re-subscription,
|
|
793
|
+
// but skip assets that user wanted to unsubscribe (preserve user intent)
|
|
794
|
+
for (const assetId of this.subscribedAssetIds) {
|
|
795
|
+
if (!this.pendingUnsubscribeAssetIds.has(assetId)) {
|
|
796
|
+
this.pendingSubscribeAssetIds.add(assetId);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
this.subscribedAssetIds.clear();
|
|
800
|
+
// Clear pending unsubscribes - they were either:
|
|
801
|
+
// 1. Successfully excluded from re-subscription (above), or
|
|
802
|
+
// 2. Were never subscribed anyway (user's intent is preserved)
|
|
803
|
+
this.pendingUnsubscribeAssetIds.clear();
|
|
804
|
+
// Clear the book cache - data is stale and will be repopulated on reconnection
|
|
805
|
+
this.bookCache.clear();
|
|
806
|
+
// Safely call close handler
|
|
807
|
+
try {
|
|
808
|
+
await ((_b = (_a = this.handlers).onWSClose) === null || _b === void 0 ? void 0 : _b.call(_a, this.managerId, code, (reason === null || reason === void 0 ? void 0 : reason.toString()) || ''));
|
|
809
|
+
}
|
|
810
|
+
catch (handlerErr) {
|
|
811
|
+
logger_1.logger.warn({
|
|
812
|
+
message: 'Error in onWSClose handler',
|
|
813
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
814
|
+
managerId: this.managerId,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
ws.removeAllListeners();
|
|
819
|
+
ws.on('open', handleOpen);
|
|
820
|
+
ws.on('message', handleMessage);
|
|
821
|
+
ws.on('pong', handlePong);
|
|
822
|
+
ws.on('error', handleError);
|
|
823
|
+
ws.on('close', handleClose);
|
|
824
|
+
}
|
|
825
|
+
/**
|
|
826
|
+
* Handles book events by updating the cache and notifying listeners.
|
|
827
|
+
*/
|
|
828
|
+
async handleBookEvents(bookEvents) {
|
|
829
|
+
var _a, _b;
|
|
830
|
+
if (bookEvents.length) {
|
|
831
|
+
for (const event of bookEvents) {
|
|
832
|
+
this.bookCache.replaceBook(event);
|
|
833
|
+
}
|
|
834
|
+
await ((_b = (_a = this.handlers).onBook) === null || _b === void 0 ? void 0 : _b.call(_a, bookEvents));
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Handles tick size change events by notifying listeners.
|
|
839
|
+
*/
|
|
840
|
+
async handleTickEvents(tickEvents) {
|
|
841
|
+
var _a, _b;
|
|
842
|
+
if (tickEvents.length) {
|
|
843
|
+
await ((_b = (_a = this.handlers).onTickSizeChange) === null || _b === void 0 ? void 0 : _b.call(_a, tickEvents));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Handles price change events.
|
|
848
|
+
*/
|
|
849
|
+
async handlePriceChangeEvents(priceChangeEvents) {
|
|
850
|
+
var _a, _b, _c, _d;
|
|
851
|
+
if (priceChangeEvents.length) {
|
|
852
|
+
await ((_b = (_a = this.handlers).onPriceChange) === null || _b === void 0 ? void 0 : _b.call(_a, priceChangeEvents));
|
|
853
|
+
for (const event of priceChangeEvents) {
|
|
854
|
+
try {
|
|
855
|
+
this.bookCache.upsertPriceChange(event);
|
|
856
|
+
}
|
|
857
|
+
catch (err) {
|
|
858
|
+
logger_1.logger.debug({
|
|
859
|
+
message: `Skipping derived future price calculation price_change: book not found for asset`,
|
|
860
|
+
event: event,
|
|
861
|
+
error: err === null || err === void 0 ? void 0 : err.message
|
|
862
|
+
});
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
const assetIds = event.price_changes.map(pc => pc.asset_id);
|
|
866
|
+
for (const assetId of assetIds) {
|
|
867
|
+
let spreadOver10Cents;
|
|
868
|
+
try {
|
|
869
|
+
spreadOver10Cents = this.bookCache.spreadOver(assetId, 0.1);
|
|
870
|
+
}
|
|
871
|
+
catch (err) {
|
|
872
|
+
logger_1.logger.debug({
|
|
873
|
+
message: 'Skipping derived future price calculation for price_change: error calculating spread',
|
|
874
|
+
asset_id: assetId,
|
|
875
|
+
event: event,
|
|
876
|
+
error: err === null || err === void 0 ? void 0 : err.message
|
|
877
|
+
});
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (!spreadOver10Cents) {
|
|
881
|
+
let newPrice;
|
|
882
|
+
try {
|
|
883
|
+
newPrice = this.bookCache.midpoint(assetId);
|
|
884
|
+
}
|
|
885
|
+
catch (err) {
|
|
886
|
+
logger_1.logger.debug({
|
|
887
|
+
message: 'Skipping derived future price calculation for price_change: error calculating midpoint',
|
|
888
|
+
asset_id: assetId,
|
|
889
|
+
event: event,
|
|
890
|
+
error: err === null || err === void 0 ? void 0 : err.message
|
|
891
|
+
});
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
const bookEntry = this.bookCache.getBookEntry(assetId);
|
|
895
|
+
if (!bookEntry) {
|
|
896
|
+
logger_1.logger.debug({
|
|
897
|
+
message: 'Skipping derived future price calculation price_change: book not found for asset',
|
|
898
|
+
asset_id: assetId,
|
|
899
|
+
event: event,
|
|
900
|
+
});
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
if (newPrice !== bookEntry.price) {
|
|
904
|
+
bookEntry.price = newPrice;
|
|
905
|
+
const priceUpdateEvent = {
|
|
906
|
+
asset_id: assetId,
|
|
907
|
+
event_type: 'price_update',
|
|
908
|
+
triggeringEvent: event,
|
|
909
|
+
timestamp: event.timestamp,
|
|
910
|
+
book: { bids: bookEntry.bids, asks: bookEntry.asks },
|
|
911
|
+
price: newPrice,
|
|
912
|
+
midpoint: bookEntry.midpoint || '',
|
|
913
|
+
spread: bookEntry.spread || '',
|
|
914
|
+
};
|
|
915
|
+
await ((_d = (_c = this.handlers).onPolymarketPriceUpdate) === null || _d === void 0 ? void 0 : _d.call(_c, [priceUpdateEvent]));
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Handles last trade price events.
|
|
924
|
+
*/
|
|
925
|
+
async handleLastTradeEvents(lastTradeEvents) {
|
|
926
|
+
var _a, _b, _c, _d;
|
|
927
|
+
if (lastTradeEvents.length) {
|
|
928
|
+
await ((_b = (_a = this.handlers).onLastTradePrice) === null || _b === void 0 ? void 0 : _b.call(_a, lastTradeEvents));
|
|
929
|
+
for (const event of lastTradeEvents) {
|
|
930
|
+
let spreadOver10Cents;
|
|
931
|
+
try {
|
|
932
|
+
spreadOver10Cents = this.bookCache.spreadOver(event.asset_id, 0.1);
|
|
933
|
+
}
|
|
934
|
+
catch (err) {
|
|
935
|
+
logger_1.logger.debug({
|
|
936
|
+
message: 'Skipping derived future price calculation for last_trade_price: error calculating spread',
|
|
937
|
+
asset_id: event.asset_id,
|
|
938
|
+
event: event,
|
|
939
|
+
error: err === null || err === void 0 ? void 0 : err.message
|
|
940
|
+
});
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (spreadOver10Cents) {
|
|
944
|
+
const newPrice = parseFloat(event.price).toString();
|
|
945
|
+
const bookEntry = this.bookCache.getBookEntry(event.asset_id);
|
|
946
|
+
if (!bookEntry) {
|
|
947
|
+
logger_1.logger.debug({
|
|
948
|
+
message: 'Skipping derived future price calculation last_trade_price: book not found for asset',
|
|
949
|
+
asset_id: event.asset_id,
|
|
950
|
+
event: event,
|
|
951
|
+
});
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (newPrice !== bookEntry.price) {
|
|
955
|
+
bookEntry.price = newPrice;
|
|
956
|
+
const priceUpdateEvent = {
|
|
957
|
+
asset_id: event.asset_id,
|
|
958
|
+
event_type: 'price_update',
|
|
959
|
+
triggeringEvent: event,
|
|
960
|
+
timestamp: event.timestamp,
|
|
961
|
+
book: { bids: bookEntry.bids, asks: bookEntry.asks },
|
|
962
|
+
price: newPrice,
|
|
963
|
+
midpoint: bookEntry.midpoint || '',
|
|
964
|
+
spread: bookEntry.spread || '',
|
|
965
|
+
};
|
|
966
|
+
await ((_d = (_c = this.handlers).onPolymarketPriceUpdate) === null || _d === void 0 ? void 0 : _d.call(_c, [priceUpdateEvent]));
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Handles best bid/ask events.
|
|
974
|
+
*/
|
|
975
|
+
async handleBestBidAskEvents(bestBidAskEvents) {
|
|
976
|
+
var _a, _b;
|
|
977
|
+
if (bestBidAskEvents.length) {
|
|
978
|
+
await ((_b = (_a = this.handlers).onBestBidAsk) === null || _b === void 0 ? void 0 : _b.call(_a, bestBidAskEvents));
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
/**
|
|
982
|
+
* Handles new market events.
|
|
983
|
+
* These events bypass asset-level filtering as they are market-level events.
|
|
984
|
+
*/
|
|
985
|
+
async handleNewMarketEvents(newMarketEvents) {
|
|
986
|
+
var _a, _b;
|
|
987
|
+
if (newMarketEvents.length) {
|
|
988
|
+
try {
|
|
989
|
+
await ((_b = (_a = this.handlers).onNewMarket) === null || _b === void 0 ? void 0 : _b.call(_a, newMarketEvents));
|
|
990
|
+
}
|
|
991
|
+
catch (handlerErr) {
|
|
992
|
+
logger_1.logger.warn({
|
|
993
|
+
message: 'Error in onNewMarket handler',
|
|
994
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
995
|
+
managerId: this.managerId,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* Handles market resolved events.
|
|
1002
|
+
* These events bypass asset-level filtering as they are market-level events.
|
|
1003
|
+
*/
|
|
1004
|
+
async handleMarketResolvedEvents(marketResolvedEvents) {
|
|
1005
|
+
var _a, _b;
|
|
1006
|
+
if (marketResolvedEvents.length) {
|
|
1007
|
+
try {
|
|
1008
|
+
await ((_b = (_a = this.handlers).onMarketResolved) === null || _b === void 0 ? void 0 : _b.call(_a, marketResolvedEvents));
|
|
1009
|
+
}
|
|
1010
|
+
catch (handlerErr) {
|
|
1011
|
+
logger_1.logger.warn({
|
|
1012
|
+
message: 'Error in onMarketResolved handler',
|
|
1013
|
+
error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr),
|
|
1014
|
+
managerId: this.managerId,
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
exports.WSSubscriptionManager = WSSubscriptionManager;
|