@nevuamarkets/poly-websockets
Version:
Plug-and-play Polymarket WebSocket price alerts
175 lines (169 loc) • 7.97 kB
JavaScript
;
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;