UNPKG

@nevuamarkets/poly-websockets

Version:

Plug-and-play Polymarket WebSocket price alerts

309 lines (307 loc) 14.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GroupSocket = void 0; const ws_1 = __importDefault(require("ws")); const logger_1 = require("../logger"); const WebSocketSubscriptions_1 = require("../types/WebSocketSubscriptions"); const PolymarketWebSocket_1 = require("../types/PolymarketWebSocket"); const lodash_1 = __importDefault(require("lodash")); const ms_1 = __importDefault(require("ms")); const crypto_1 = require("crypto"); const CLOB_WSS_URL = 'wss://ws-subscriptions-clob.polymarket.com/ws/market'; class GroupSocket { constructor(group, limiter, bookCache, handlers) { this.group = group; this.limiter = limiter; this.bookCache = bookCache; this.handlers = handlers; } /** * Establish the websocket connection using the provided Bottleneck limiter. * */ async connect() { if (this.group.assetIds.size === 0) { this.group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP; return; } try { logger_1.logger.info({ message: 'Connecting to CLOB WebSocket', groupId: this.group.groupId, assetIdsLength: this.group.assetIds.size, }); this.group.wsClient = await this.limiter.schedule({ priority: 0 }, async () => { const ws = new ws_1.default(CLOB_WSS_URL); /* This handler will be replaced by the handlers in setupEventHandlers */ ws.on('error', (err) => { logger_1.logger.warn({ message: 'Error connecting to CLOB WebSocket', error: err, groupId: this.group.groupId, assetIdsLength: this.group.assetIds.size, }); }); return ws; }); } catch (err) { this.group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD; throw err; // caller responsible for error handler } this.setupEventHandlers(); } setupEventHandlers() { const group = this.group; const handlers = this.handlers; /* Define handlers within this scope to capture 'this' context */ const handleOpen = async () => { var _a; if (group.assetIds.size === 0) { group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP; return; } group.status = WebSocketSubscriptions_1.WebSocketStatus.ALIVE; group.wsClient.send(JSON.stringify({ assets_ids: Array.from(group.assetIds), type: 'market' })); await ((_a = handlers.onWSOpen) === null || _a === void 0 ? void 0 : _a.call(handlers, group.groupId, Array.from(group.assetIds))); this.pingInterval = setInterval(() => { if (group.assetIds.size === 0) { clearInterval(this.pingInterval); group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP; return; } if (!group.wsClient) { clearInterval(this.pingInterval); group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD; return; } group.wsClient.ping(); }, (0, crypto_1.randomInt)((0, ms_1.default)('15s'), (0, ms_1.default)('25s'))); }; const handleMessage = async (data) => { var _a, _b; let events = []; try { const parsedData = JSON.parse(data.toString()); events = Array.isArray(parsedData) ? parsedData : [parsedData]; } catch (err) { await ((_a = handlers.onError) === null || _a === void 0 ? void 0 : _a.call(handlers, new Error(`Not JSON: ${data.toString()}`))); return; } events = lodash_1.default.filter(events, (event) => lodash_1.default.size(event.asset_id) > 0); const bookEvents = []; const lastTradeEvents = []; const tickEvents = []; const priceChangeEvents = []; for (const event of events) { /* Skip events for asset ids that are not in the group to ensure that we don't get stale events for assets that were removed. */ if (!group.assetIds.has(event.asset_id)) { 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 if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) { priceChangeEvents.push(event); } else { await ((_b = handlers.onError) === null || _b === void 0 ? void 0 : _b.call(handlers, new Error(`Unknown event: ${JSON.stringify(event)}`))); } } await this.handleBookEvents(bookEvents); await this.handleTickEvents(tickEvents); await this.handlePriceChangeEvents(priceChangeEvents); await this.handleLastTradeEvents(lastTradeEvents); }; const handlePong = () => { group.groupId; }; const handleError = (err) => { var _a; group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD; clearInterval(this.pingInterval); (_a = handlers.onError) === null || _a === void 0 ? void 0 : _a.call(handlers, new Error(`WebSocket error for group ${group.groupId}: ${err.message}`)); }; const handleClose = async (code, reason) => { var _a; group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD; clearInterval(this.pingInterval); await ((_a = handlers.onWSClose) === null || _a === void 0 ? void 0 : _a.call(handlers, group.groupId, code, (reason === null || reason === void 0 ? void 0 : reason.toString()) || '')); }; if (group.wsClient) { // Remove any existing handlers group.wsClient.removeAllListeners(); // Add the handlers group.wsClient.on('open', handleOpen); group.wsClient.on('message', handleMessage); group.wsClient.on('pong', handlePong); group.wsClient.on('error', handleError); group.wsClient.on('close', handleClose); } if (group.assetIds.size === 0) { group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP; return; } if (!group.wsClient) { group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD; return; } } 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)); } } 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)); } } 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`, asset_id: event.asset_id, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } 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 price_change: error calculating spread', asset_id: event.asset_id, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } if (!spreadOver10Cents) { let newPrice; try { newPrice = this.bookCache.midpoint(event.asset_id); } catch (err) { logger_1.logger.debug({ message: 'Skipping derived future price calculation for price_change: error calculating midpoint', asset_id: event.asset_id, event: event, error: err === null || err === void 0 ? void 0 : err.message }); continue; } const bookEntry = this.bookCache.getBookEntry(event.asset_id); if (!bookEntry) { logger_1.logger.debug({ message: 'Skipping derived future price calculation price_change: 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])); } } } } } async handleLastTradeEvents(lastTradeEvents) { var _a, _b, _c, _d; if (lastTradeEvents.length) { /* Note: There is no need to edit the book here. According to the docs, a separate book event is sent when a trade affects the book. See: https://docs.polymarket.com/developers/CLOB/websocket/market-channel#book-message */ 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) { // Ensure no trailing zeros 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.GroupSocket = GroupSocket;