UNPKG

@nevuamarkets/poly-websockets

Version:

Plug-and-play Polymarket WebSocket price alerts

916 lines (915 loc) 41.4 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WSSubscriptionManager = void 0; const ms_1 = __importDefault(require("ms")); const ws_1 = __importDefault(require("ws")); const uuid_1 = require("uuid"); const crypto_1 = require("crypto"); const PolymarketWebSocket_1 = require("./types/PolymarketWebSocket"); const WebSocketSubscriptions_1 = require("./types/WebSocketSubscriptions"); const OrderBookCache_1 = require("./modules/OrderBookCache"); const logger_1 = require("./logger"); const lodash_1 = __importDefault(require("lodash")); // Note: We intentionally use a static reconnection interval rather than exponential backoff. // Perhaps change this to exponential backoff in the future. const DEFAULT_RECONNECT_INTERVAL_MS = (0, ms_1.default)('5s'); const DEFAULT_PENDING_FLUSH_INTERVAL_MS = (0, ms_1.default)('100ms'); const CLOB_WSS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market'; /** * WebSocket Subscription Manager for Polymarket CLOB WebSocket. * * Each instance manages a single WebSocket connection and tracks: * - Subscribed assets: Successfully subscribed to the WebSocket * - Pending assets: Waiting to be subscribed (batched and flushed periodically) * * Instances are fully independent - no shared state between managers. */ class WSSubscriptionManager { constructor(userHandlers, options) { // WebSocket connection this.wsClient = null; this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; // Asset tracking this.subscribedAssetIds = new Set(); this.pendingSubscribeAssetIds = new Set(); this.pendingUnsubscribeAssetIds = new Set(); this.managerId = (0, uuid_1.v4)(); this.bookCache = new OrderBookCache_1.OrderBookCache(); this.reconnectIntervalMs = (options === null || options === void 0 ? void 0 : options.reconnectAndCleanupIntervalMs) || DEFAULT_RECONNECT_INTERVAL_MS; this.pendingFlushIntervalMs = (options === null || options === void 0 ? void 0 : options.pendingFlushIntervalMs) || DEFAULT_PENDING_FLUSH_INTERVAL_MS; this.handlers = { onBook: async (events) => { await this.actOnSubscribedEvents(events, userHandlers.onBook); }, onLastTradePrice: async (events) => { await this.actOnSubscribedEvents(events, userHandlers.onLastTradePrice); }, onTickSizeChange: async (events) => { await this.actOnSubscribedEvents(events, userHandlers.onTickSizeChange); }, onPriceChange: async (events) => { await this.actOnSubscribedEvents(events, userHandlers.onPriceChange); }, onPolymarketPriceUpdate: async (events) => { await this.actOnSubscribedEvents(events, userHandlers.onPolymarketPriceUpdate); }, onWSClose: userHandlers.onWSClose, onWSOpen: userHandlers.onWSOpen, onError: userHandlers.onError }; // Periodic reconnection check this.scheduleReconnectionCheck(); // Periodic pending flush this.pendingFlushInterval = setInterval(() => { this.flushPendingSubscriptions(); }, this.pendingFlushIntervalMs); } /** * Clears all WebSocket subscriptions and state. * * This will: * 1. Stop all timers * 2. Close the WebSocket connection * 3. Clear all asset tracking * 4. Clear the order book cache */ async clearState() { // Stop all timers (reconnectInterval is now a timeout, not interval) if (this.reconnectInterval) { clearTimeout(this.reconnectInterval); this.reconnectInterval = undefined; } if (this.pendingFlushInterval) { clearInterval(this.pendingFlushInterval); this.pendingFlushInterval = undefined; } if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = undefined; } // Close WebSocket if (this.wsClient) { this.wsClient.removeAllListeners(); this.wsClient.close(); this.wsClient = null; } // Clear all asset tracking this.subscribedAssetIds.clear(); this.pendingSubscribeAssetIds.clear(); this.pendingUnsubscribeAssetIds.clear(); // Reset status this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; // Clear the order book cache this.bookCache.clear(); } /** * Filters events to only include those for subscribed assets. * Wraps user handler calls in try-catch to prevent user errors from breaking internal logic. * Does not call the handler if all events are filtered out. */ async actOnSubscribedEvents(events, action) { events = events.filter((event) => { if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) { return event.price_changes.some(pc => this.subscribedAssetIds.has(pc.asset_id)); } if ('asset_id' in event) { return this.subscribedAssetIds.has(event.asset_id); } return false; }); // Skip if no events passed the filter if (events.length === 0) { return; } // Wrap user handler calls in try-catch try { await (action === null || action === void 0 ? void 0 : action(events)); } catch (handlerErr) { logger_1.logger.warn({ message: 'Error in user event handler', error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr), managerId: this.managerId, eventCount: events.length, }); } } /** * Adds new subscriptions. * * Assets are added to a pending queue and will be subscribed when: * - The WebSocket connects (initial subscription) * - The pending flush timer fires (for new assets on an existing connection) * * @param assetIdsToAdd - The asset IDs to add subscriptions for. */ async addSubscriptions(assetIdsToAdd) { try { for (const assetId of assetIdsToAdd) { // Remove from pending unsubscribe if it's there (cancel the unsubscription) // This must happen BEFORE the subscribed check, so that adding an asset // that was pending unsubscribe correctly cancels the unsubscription. this.pendingUnsubscribeAssetIds.delete(assetId); // Skip if already subscribed (pending subscribe is safe to re-add due to Set behavior) if (this.subscribedAssetIds.has(assetId)) continue; // Add to pending subscribe (no-op if already pending due to Set) this.pendingSubscribeAssetIds.add(assetId); } // Restart intervals if they were cleared (e.g. after clearState) this.ensureIntervalsRunning(); // Ensure we have a connection if (!this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN) { await this.connect(); } } catch (error) { const msg = `Error adding subscriptions: ${error instanceof Error ? error.message : String(error)}`; await this.safeCallErrorHandler(new Error(msg)); } } /** * Ensures the periodic intervals are running. * Called after clearState() when new subscriptions are added. */ ensureIntervalsRunning() { if (!this.reconnectInterval) { this.scheduleReconnectionCheck(); } if (!this.pendingFlushInterval) { this.pendingFlushInterval = setInterval(() => { this.flushPendingSubscriptions(); }, this.pendingFlushIntervalMs); } } /** * Schedules the next reconnection check. * Uses a fixed interval (default 5 seconds) between checks. */ scheduleReconnectionCheck() { if (this.reconnectInterval) { clearTimeout(this.reconnectInterval); } this.reconnectInterval = setTimeout(async () => { await this.checkReconnection(); // Schedule next check (only if not cleared) if (this.reconnectInterval) { this.scheduleReconnectionCheck(); } }, this.reconnectIntervalMs); } /** * Safely calls the error handler, catching any exceptions thrown by it. * Prevents user handler exceptions from breaking internal logic. */ async safeCallErrorHandler(error) { var _a, _b; try { await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, error)); } catch (handlerErr) { logger_1.logger.warn({ message: 'Error in onError handler', originalError: error.message, handlerError: handlerErr instanceof Error ? handlerErr.message : String(handlerErr), managerId: this.managerId, }); } } /** * Removes subscriptions. * * @param assetIdsToRemove - The asset IDs to remove subscriptions for. */ async removeSubscriptions(assetIdsToRemove) { try { for (const assetId of assetIdsToRemove) { // Remove from pending subscribe if it's there if (this.pendingSubscribeAssetIds.delete(assetId)) { continue; // Was only pending, no need to send unsubscribe } // If subscribed, add to pending unsubscribe if (this.subscribedAssetIds.has(assetId)) { this.pendingUnsubscribeAssetIds.add(assetId); } // Note: We don't clear the book cache here because the unsubscription // hasn't been sent yet. The cache entry will be cleared when the // unsubscription is flushed (after the asset is removed from subscribedAssetIds). } } catch (error) { const errMsg = `Error removing subscriptions: ${error instanceof Error ? error.message : String(error)}`; await this.safeCallErrorHandler(new Error(errMsg)); } } /** * Get all currently monitored asset IDs. * This includes both successfully subscribed assets and pending subscriptions. * * @returns Array of asset IDs being monitored. */ getAssetIds() { const allAssets = new Set(this.subscribedAssetIds); for (const assetId of this.pendingSubscribeAssetIds) { allAssets.add(assetId); } // Exclude pending unsubscribes for (const assetId of this.pendingUnsubscribeAssetIds) { allAssets.delete(assetId); } return Array.from(allAssets); } /** * Returns statistics about the current state of the subscription manager. */ getStatistics() { var _a; const isOpen = ((_a = this.wsClient) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN; return { openWebSockets: isOpen ? 1 : 0, assetIds: this.getAssetIds().length, pendingSubscribeCount: this.pendingSubscribeAssetIds.size, pendingUnsubscribeCount: this.pendingUnsubscribeAssetIds.size, pendingAssetIds: this.pendingSubscribeAssetIds.size + this.pendingUnsubscribeAssetIds.size, }; } /** * Flush pending subscriptions and unsubscriptions to the WebSocket. * * SUBSCRIPTION PROTOCOL NOTE: * The Polymarket WebSocket protocol does NOT send any confirmation or acknowledgment * messages for subscribe/unsubscribe operations. The server silently processes these * requests. We optimistically assume success after sending. If the server rejects * a request (e.g., invalid asset ID), events for those assets simply won't arrive - * there is no error response to handle. * * This means: * - We cannot definitively know if a subscription succeeded * - We cannot definitively know if an unsubscription succeeded * - The only indication of failure is the absence of expected events */ flushPendingSubscriptions() { if (!this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN) { return; } // Process unsubscriptions first if (this.pendingUnsubscribeAssetIds.size > 0) { const toUnsubscribe = Array.from(this.pendingUnsubscribeAssetIds); const message = { operation: 'unsubscribe', assets_ids: toUnsubscribe, }; // IMPORTANT: The Polymarket WebSocket protocol does NOT send any confirmation // or acknowledgment message for subscribe/unsubscribe operations. The server // silently accepts the request. We optimistically assume success after sending. // If the server rejects the request, events for those assets simply won't arrive // (there is no error response to handle). try { this.wsClient.send(JSON.stringify(message)); // Remove from subscribed, clear pending, and clear book cache for unsubscribed assets for (const assetId of toUnsubscribe) { this.subscribedAssetIds.delete(assetId); this.bookCache.clear(assetId); } this.pendingUnsubscribeAssetIds.clear(); logger_1.logger.info({ message: `Unsubscribed from ${toUnsubscribe.length} asset(s)`, managerId: this.managerId, }); } catch (error) { logger_1.logger.warn({ message: 'Failed to send unsubscribe message', error: error instanceof Error ? error.message : String(error), }); } } // Process subscriptions if (this.pendingSubscribeAssetIds.size > 0) { const toSubscribe = Array.from(this.pendingSubscribeAssetIds); const message = { operation: 'subscribe', assets_ids: toSubscribe, }; // IMPORTANT: The Polymarket WebSocket protocol does NOT send any confirmation // or acknowledgment message for subscribe/unsubscribe operations. The server // silently accepts the request. We optimistically assume success after sending. // If the server rejects the request, events for those assets simply won't arrive // (there is no error response to handle). try { this.wsClient.send(JSON.stringify(message)); // Move to subscribed and clear pending for (const assetId of toSubscribe) { this.subscribedAssetIds.add(assetId); } this.pendingSubscribeAssetIds.clear(); logger_1.logger.info({ message: `Subscribed to ${toSubscribe.length} asset(s)`, managerId: this.managerId, }); } catch (error) { logger_1.logger.warn({ message: 'Failed to send subscribe message', error: error instanceof Error ? error.message : String(error), }); } } // Close WebSocket if no assets remain and no pending unsubscriptions // (pendingUnsubscribeAssetIds is already cleared at this point) if (this.subscribedAssetIds.size === 0 && this.pendingSubscribeAssetIds.size === 0 && this.pendingUnsubscribeAssetIds.size === 0) { this.closeWebSocket(); } } /** * Closes the WebSocket connection and cleans up related resources. */ closeWebSocket() { if (this.wsClient) { logger_1.logger.info({ message: 'Closing WebSocket - no assets to monitor', managerId: this.managerId, }); this.wsClient.removeAllListeners(); this.wsClient.close(); this.wsClient = null; } this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } } /** * Check if we need to reconnect. * Note: Assets are moved to pending in handleClose/handleError handlers, * so this method only needs to check if reconnection is needed. */ async checkReconnection() { // If we have pending assets but no connection, reconnect const hasPendingAssets = this.pendingSubscribeAssetIds.size > 0; const isDisconnected = !this.wsClient || this.wsClient.readyState !== ws_1.default.OPEN; if (hasPendingAssets && isDisconnected && !this.connecting) { logger_1.logger.info({ message: 'Reconnection check - attempting to reconnect', managerId: this.managerId, pendingCount: this.pendingSubscribeAssetIds.size, }); // Clear stale book cache data - will be repopulated after reconnection this.bookCache.clear(); await this.connect(); } } /** * Establish the WebSocket connection. */ async connect() { var _a; if (this.connecting) { return; } if (((_a = this.wsClient) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) { return; } // No assets to subscribe to if (this.pendingSubscribeAssetIds.size === 0 && this.subscribedAssetIds.size === 0) { return; } this.connecting = true; this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.CONNECTING; try { logger_1.logger.info({ message: 'Connecting to CLOB WebSocket', managerId: this.managerId, pendingAssetCount: this.pendingSubscribeAssetIds.size, }); this.wsClient = new ws_1.default(CLOB_WSS_URL); // Set up event handlers immediately (handlers are set up before any events can fire) this.setupEventHandlers(); // Connection timeout this.connectionTimeout = setTimeout(async () => { if (this.connecting && this.wsClient && this.wsClient.readyState !== ws_1.default.OPEN) { logger_1.logger.warn({ message: 'WebSocket connection timeout', managerId: this.managerId, }); this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; if (this.wsClient) { this.wsClient.removeAllListeners(); this.wsClient.close(); this.wsClient = null; } // Notify error handler about the timeout await this.safeCallErrorHandler(new Error('WebSocket connection timeout after 30s')); } }, (0, ms_1.default)('30s')); } catch (err) { this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; throw err; } } /** * Sets up event handlers for the WebSocket connection. */ setupEventHandlers() { const ws = this.wsClient; if (!ws) return; const handleOpen = async () => { var _a, _b; this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.CONNECTED; this.connecting = false; if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = undefined; } // Send an empty MarketSubscriptionMessage as the initial handshake. // The Polymarket WebSocket protocol requires a 'market' type message as the first message. // We send it with an empty assets_ids array, and then use 'subscribe' operation messages // for all actual subscriptions (via flushPendingSubscriptions) to keep the subscription // logic consistent in one place. try { const initMessage = { assets_ids: [], type: 'market', }; ws.send(JSON.stringify(initMessage)); } catch (error) { logger_1.logger.warn({ message: 'Failed to send initial market message', error: error instanceof Error ? error.message : String(error), managerId: this.managerId, }); // Close and let reconnection logic handle retry // Wrap in try-catch as close() can throw in edge cases try { ws.close(); } catch (closeErr) { logger_1.logger.debug({ message: 'Error closing WebSocket after init message failure (safe to ignore)', error: closeErr instanceof Error ? closeErr.message : String(closeErr), managerId: this.managerId, }); } return; } const pendingAssets = Array.from(this.pendingSubscribeAssetIds); // Safely call open handler try { await ((_b = (_a = this.handlers).onWSOpen) === null || _b === void 0 ? void 0 : _b.call(_a, this.managerId, pendingAssets)); } catch (handlerErr) { logger_1.logger.warn({ message: 'Error in onWSOpen handler', error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr), managerId: this.managerId, }); } // Immediately flush pending subscriptions now that we're connected this.flushPendingSubscriptions(); // Start ping interval with jitter per-ping const basePingIntervalMs = (0, ms_1.default)('20s'); this.pingInterval = setInterval(() => { if (!ws || ws.readyState !== ws_1.default.OPEN) { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } return; } // Add jitter by randomly delaying the ping within a ±5s window const jitterMs = (0, crypto_1.randomInt)(0, (0, ms_1.default)('5s')); setTimeout(() => { if (ws && ws.readyState === ws_1.default.OPEN) { try { ws.ping(); } catch (pingErr) { // Ping can fail if the socket is closing or in a bad state. // This is not critical - the socket will be cleaned up on close/error events. logger_1.logger.debug({ message: 'Ping failed (safe to ignore)', error: pingErr instanceof Error ? pingErr.message : String(pingErr), }); } } }, jitterMs); }, basePingIntervalMs); }; const handleMessage = async (data) => { try { const messageStr = data.toString(); const normalizedMessageStr = messageStr.trim().toUpperCase(); if (normalizedMessageStr === 'PONG') { return; } let events = []; try { const parsedData = JSON.parse(messageStr); events = Array.isArray(parsedData) ? parsedData : [parsedData]; } catch (err) { await this.safeCallErrorHandler(new Error(`Not JSON: ${messageStr}`)); return; } events = lodash_1.default.filter(events, (event) => { if (!event) return false; if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) { return event.price_changes && event.price_changes.length > 0; } return lodash_1.default.size(event.asset_id) > 0; }); const bookEvents = []; const lastTradeEvents = []; const tickEvents = []; const priceChangeEvents = []; for (const event of events) { if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) { const relevantChanges = event.price_changes.filter(pc => this.subscribedAssetIds.has(pc.asset_id)); if (relevantChanges.length === 0) continue; priceChangeEvents.push({ ...event, price_changes: relevantChanges }); } else { // Safely check asset_id existence const assetId = 'asset_id' in event ? event.asset_id : undefined; if (!assetId || !this.subscribedAssetIds.has(assetId)) continue; if ((0, PolymarketWebSocket_1.isBookEvent)(event)) { bookEvents.push(event); } else if ((0, PolymarketWebSocket_1.isLastTradePriceEvent)(event)) { lastTradeEvents.push(event); } else if ((0, PolymarketWebSocket_1.isTickSizeChangeEvent)(event)) { tickEvents.push(event); } else { await this.safeCallErrorHandler(new Error(`Unknown event: ${JSON.stringify(event)}`)); } } } // Wrap each handler call in try-catch to prevent one failure // from breaking the entire event loop try { await this.handleBookEvents(bookEvents); } catch (err) { logger_1.logger.warn({ message: 'Error in handleBookEvents', error: err instanceof Error ? err.message : String(err), managerId: this.managerId, }); } try { await this.handleTickEvents(tickEvents); } catch (err) { logger_1.logger.warn({ message: 'Error in handleTickEvents', error: err instanceof Error ? err.message : String(err), managerId: this.managerId, }); } try { await this.handlePriceChangeEvents(priceChangeEvents); } catch (err) { logger_1.logger.warn({ message: 'Error in handlePriceChangeEvents', error: err instanceof Error ? err.message : String(err), managerId: this.managerId, }); } try { await this.handleLastTradeEvents(lastTradeEvents); } catch (err) { logger_1.logger.warn({ message: 'Error in handleLastTradeEvents', error: err instanceof Error ? err.message : String(err), managerId: this.managerId, }); } } catch (err) { await this.safeCallErrorHandler(new Error(`Error handling message: ${err}`)); } }; const handlePong = () => { // Pong received - connection is alive }; const handleError = async (err) => { var _a, _b; this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; // Clean up WebSocket reference - close the socket before nullifying // Wrap in try-catch because close() can throw if socket is in a bad state if (this.wsClient) { this.wsClient.removeAllListeners(); try { this.wsClient.close(); } catch (closeErr) { logger_1.logger.debug({ message: 'Error closing WebSocket in error handler (safe to ignore)', error: closeErr instanceof Error ? closeErr.message : String(closeErr), managerId: this.managerId, }); } this.wsClient = null; } if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = undefined; } // Move subscribed assets back to pending for re-subscription, // but skip assets that user wanted to unsubscribe (preserve user intent) for (const assetId of this.subscribedAssetIds) { if (!this.pendingUnsubscribeAssetIds.has(assetId)) { this.pendingSubscribeAssetIds.add(assetId); } } this.subscribedAssetIds.clear(); // Clear pending unsubscribes - they were either: // 1. Successfully excluded from re-subscription (above), or // 2. Were never subscribed anyway (user's intent is preserved) this.pendingUnsubscribeAssetIds.clear(); // Clear the book cache - data is stale and will be repopulated on reconnection this.bookCache.clear(); // Safely call error handler try { await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(`WebSocket error: ${err.message}`))); } catch (handlerErr) { logger_1.logger.warn({ message: 'Error in onError handler', error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr), managerId: this.managerId, }); } }; const handleClose = async (code, reason) => { var _a, _b; this.status = WebSocketSubscriptions_1.WebSocketConnectionStatus.DISCONNECTED; this.connecting = false; // Clean up WebSocket reference if (this.wsClient) { this.wsClient.removeAllListeners(); this.wsClient = null; } if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = undefined; } if (this.connectionTimeout) { clearTimeout(this.connectionTimeout); this.connectionTimeout = undefined; } // Move subscribed assets back to pending for re-subscription, // but skip assets that user wanted to unsubscribe (preserve user intent) for (const assetId of this.subscribedAssetIds) { if (!this.pendingUnsubscribeAssetIds.has(assetId)) { this.pendingSubscribeAssetIds.add(assetId); } } this.subscribedAssetIds.clear(); // Clear pending unsubscribes - they were either: // 1. Successfully excluded from re-subscription (above), or // 2. Were never subscribed anyway (user's intent is preserved) this.pendingUnsubscribeAssetIds.clear(); // Clear the book cache - data is stale and will be repopulated on reconnection this.bookCache.clear(); // Safely call close handler try { 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()) || '')); } catch (handlerErr) { logger_1.logger.warn({ message: 'Error in onWSClose handler', error: handlerErr instanceof Error ? handlerErr.message : String(handlerErr), managerId: this.managerId, }); } }; ws.removeAllListeners(); ws.on('open', handleOpen); ws.on('message', handleMessage); ws.on('pong', handlePong); ws.on('error', handleError); ws.on('close', handleClose); } /** * Handles book events by updating the cache and notifying listeners. */ async handleBookEvents(bookEvents) { var _a, _b; if (bookEvents.length) { for (const event of bookEvents) { this.bookCache.replaceBook(event); } await ((_b = (_a = this.handlers).onBook) === null || _b === void 0 ? void 0 : _b.call(_a, bookEvents)); } } /** * Handles tick size change events by notifying listeners. */ async handleTickEvents(tickEvents) { var _a, _b; if (tickEvents.length) { await ((_b = (_a = this.handlers).onTickSizeChange) === null || _b === void 0 ? void 0 : _b.call(_a, tickEvents)); } } /** * Handles price change events. */ async handlePriceChangeEvents(priceChangeEvents) { var _a, _b, _c, _d; if (priceChangeEvents.length) { await ((_b = (_a = this.handlers).onPriceChange) === null || _b === void 0 ? void 0 : _b.call(_a, priceChangeEvents)); for (const event of priceChangeEvents) { try { this.bookCache.upsertPriceChange(event); } catch (err) { logger_1.logger.debug({ message: `Skipping derived future price calculation price_change: book not found for asset`, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } const assetIds = event.price_changes.map(pc => pc.asset_id); for (const assetId of assetIds) { let spreadOver10Cents; try { spreadOver10Cents = this.bookCache.spreadOver(assetId, 0.1); } catch (err) { logger_1.logger.debug({ message: 'Skipping derived future price calculation for price_change: error calculating spread', asset_id: assetId, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } if (!spreadOver10Cents) { let newPrice; try { newPrice = this.bookCache.midpoint(assetId); } catch (err) { logger_1.logger.debug({ message: 'Skipping derived future price calculation for price_change: error calculating midpoint', asset_id: assetId, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } const bookEntry = this.bookCache.getBookEntry(assetId); if (!bookEntry) { logger_1.logger.debug({ message: 'Skipping derived future price calculation price_change: book not found for asset', asset_id: assetId, event: event, }); continue; } if (newPrice !== bookEntry.price) { bookEntry.price = newPrice; const priceUpdateEvent = { asset_id: assetId, event_type: 'price_update', triggeringEvent: event, timestamp: event.timestamp, book: { bids: bookEntry.bids, asks: bookEntry.asks }, price: newPrice, midpoint: bookEntry.midpoint || '', spread: bookEntry.spread || '', }; await ((_d = (_c = this.handlers).onPolymarketPriceUpdate) === null || _d === void 0 ? void 0 : _d.call(_c, [priceUpdateEvent])); } } } } } } /** * Handles last trade price events. */ async handleLastTradeEvents(lastTradeEvents) { var _a, _b, _c, _d; if (lastTradeEvents.length) { await ((_b = (_a = this.handlers).onLastTradePrice) === null || _b === void 0 ? void 0 : _b.call(_a, lastTradeEvents)); for (const event of lastTradeEvents) { let spreadOver10Cents; try { spreadOver10Cents = this.bookCache.spreadOver(event.asset_id, 0.1); } catch (err) { logger_1.logger.debug({ message: 'Skipping derived future price calculation for last_trade_price: error calculating spread', asset_id: event.asset_id, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } if (spreadOver10Cents) { const newPrice = parseFloat(event.price).toString(); const bookEntry = this.bookCache.getBookEntry(event.asset_id); if (!bookEntry) { logger_1.logger.debug({ message: 'Skipping derived future price calculation last_trade_price: book not found for asset', asset_id: event.asset_id, event: event, }); continue; } if (newPrice !== bookEntry.price) { bookEntry.price = newPrice; const priceUpdateEvent = { asset_id: event.asset_id, event_type: 'price_update', triggeringEvent: event, timestamp: event.timestamp, book: { bids: bookEntry.bids, asks: bookEntry.asks }, price: newPrice, midpoint: bookEntry.midpoint || '', spread: bookEntry.spread || '', }; await ((_d = (_c = this.handlers).onPolymarketPriceUpdate) === null || _d === void 0 ? void 0 : _d.call(_c, [priceUpdateEvent])); } } } } } } exports.WSSubscriptionManager = WSSubscriptionManager;