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