UNPKG

@nevuamarkets/poly-websockets

Version:

Plug-and-play Polymarket WebSocket price alerts

175 lines (169 loc) 7.97 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 lodash_1 = __importDefault(require("lodash")); const bottleneck_1 = __importDefault(require("bottleneck")); const GroupRegistry_1 = require("./modules/GroupRegistry"); const OrderBookCache_1 = require("./modules/OrderBookCache"); const GroupSocket_1 = require("./modules/GroupSocket"); const logger_1 = require("./logger"); // Keeping a burst limit under 10/s to avoid rate limiting // See https://docs.polymarket.com/quickstart/introduction/rate-limits#api-rate-limits const BURST_LIMIT_PER_SECOND = 5; const DEFAULT_RECONNECT_AND_CLEANUP_INTERVAL_MS = (0, ms_1.default)('10s'); const DEFAULT_MAX_MARKETS_PER_WS = 100; class WSSubscriptionManager { constructor(userHandlers, options) { this.groupRegistry = new GroupRegistry_1.GroupRegistry(); this.bookCache = new OrderBookCache_1.OrderBookCache(); this.burstLimiter = (options === null || options === void 0 ? void 0 : options.burstLimiter) || new bottleneck_1.default({ reservoir: BURST_LIMIT_PER_SECOND, reservoirRefreshAmount: BURST_LIMIT_PER_SECOND, reservoirRefreshInterval: (0, ms_1.default)('1s'), maxConcurrent: BURST_LIMIT_PER_SECOND }); this.reconnectAndCleanupIntervalMs = (options === null || options === void 0 ? void 0 : options.reconnectAndCleanupIntervalMs) || DEFAULT_RECONNECT_AND_CLEANUP_INTERVAL_MS; this.maxMarketsPerWS = (options === null || options === void 0 ? void 0 : options.maxMarketsPerWS) || DEFAULT_MAX_MARKETS_PER_WS; 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 }; this.burstLimiter.on('error', (err) => { var _a, _b; (_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err); }); // Check for dead groups every 10s and reconnect them if needed setInterval(() => { this.reconnectAndCleanupGroups(); }, this.reconnectAndCleanupIntervalMs); } /* Clears all WebSocket subscriptions and state. This will: 1. Remove all subscriptions and groups 2. Close all WebSocket connections 3. Clear the order book cache */ async clearState() { const previousGroups = await this.groupRegistry.clearAllGroups(); // Close sockets outside the lock for (const group of previousGroups) { this.groupRegistry.disconnectGroup(group); } // Also clear the order book cache this.bookCache.clear(); } /* This function is called when: - a websocket event is received from the Polymarket WS - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated) The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups. */ async actOnSubscribedEvents(events, action) { // Filter out events that are not subscribed to by any groups events = lodash_1.default.filter(events, (event) => { const groupIndices = this.groupRegistry.getGroupIndicesForAsset(event.asset_id); if (groupIndices.length > 1) { logger_1.logger.warn({ message: 'Found multiple groups for asset', asset_id: event.asset_id, group_indices: groupIndices }); } return groupIndices.length > 0; }); await (action === null || action === void 0 ? void 0 : action(events)); } /* Edits wsGroups: Adds new subscriptions. - Filters out assets that are already subscribed - Finds a group with capacity or creates a new one - Creates a new WebSocket client and adds it to the group */ async addSubscriptions(assetIdsToAdd) { var _a, _b; try { const groupIdsToConnect = await this.groupRegistry.addAssets(assetIdsToAdd, this.maxMarketsPerWS); for (const groupId of groupIdsToConnect) { await this.createWebSocketClient(groupId, this.handlers); } } catch (error) { const msg = `Error adding subscriptions: ${error instanceof Error ? error.message : String(error)}`; await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(msg))); } } /* Edits wsGroups: Removes subscriptions. The group will use the updated subscriptions when it reconnects. We do that because we don't want to miss events by reconnecting. */ async removeSubscriptions(assetIdsToRemove) { var _a, _b; try { await this.groupRegistry.removeAssets(assetIdsToRemove, this.bookCache); } catch (error) { const errMsg = `Error removing subscriptions: ${error instanceof Error ? error.message : String(error)}`; await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(errMsg))); } } /* This function runs periodically and: - Tries to reconnect groups that have assets and are disconnected - Cleans up groups that have no assets */ async reconnectAndCleanupGroups() { var _a, _b; try { const reconnectIds = await this.groupRegistry.getGroupsToReconnectAndCleanup(); for (const groupId of reconnectIds) { await this.createWebSocketClient(groupId, this.handlers); } } catch (err) { await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err)); } } async createWebSocketClient(groupId, handlers) { var _a, _b; const group = this.groupRegistry.findGroupById(groupId); /* Should never happen, but just in case. */ if (!group) { await ((_a = handlers.onError) === null || _a === void 0 ? void 0 : _a.call(handlers, new Error(`Group ${groupId} not found in registry`))); return; } const groupSocket = new GroupSocket_1.GroupSocket(group, this.burstLimiter, this.bookCache, handlers); try { await groupSocket.connect(); } catch (error) { const errorMessage = `Error creating WebSocket client for group ${groupId}: ${error instanceof Error ? error.message : String(error)}`; await ((_b = handlers.onError) === null || _b === void 0 ? void 0 : _b.call(handlers, new Error(errorMessage))); } } } exports.WSSubscriptionManager = WSSubscriptionManager;