@nevuamarkets/poly-websockets 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +661 -0
- package/README.md +251 -0
- package/dist/WSSubscriptionManager.d.ts +18 -0
- package/dist/WSSubscriptionManager.js +174 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +21 -0
- package/dist/logger.d.ts +2 -0
- package/dist/logger.js +34 -0
- package/dist/modules/GroupRegistry.d.ts +85 -0
- package/dist/modules/GroupRegistry.js +261 -0
- package/dist/modules/GroupSocket.d.ts +22 -0
- package/dist/modules/GroupSocket.js +311 -0
- package/dist/modules/OrderBookCache.d.ts +49 -0
- package/dist/modules/OrderBookCache.js +173 -0
- package/dist/types/PolymarketWebSocket.d.ts +242 -0
- package/dist/types/PolymarketWebSocket.js +50 -0
- package/dist/types/WebSocketSubscriptions.d.ts +19 -0
- package/dist/types/WebSocketSubscriptions.js +10 -0
- package/package.json +49 -0
- package/src/WSSubscriptionManager.ts +201 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +37 -0
- package/src/modules/GroupRegistry.ts +274 -0
- package/src/modules/GroupSocket.ts +338 -0
- package/src/modules/OrderBookCache.ts +208 -0
- package/src/types/PolymarketWebSocket.ts +280 -0
- package/src/types/WebSocketSubscriptions.ts +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Poly-WebSockets
|
|
2
|
+
|
|
3
|
+
A TypeScript library for real-time Polymarket WebSocket price alerts with automatic connection management and intelligent reconnection handling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install poly-websockets
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🔄 **Automatic Connection Management**: Handles WebSocket connections, reconnections, and cleanup for grouped subscriptions
|
|
14
|
+
- 📊 **Real-time Price Updates**: Get live price data, order book updates, and trade events from Polymarket
|
|
15
|
+
- 🎯 **Smart Price Logic**: Implements Polymarket's price calculation logic (midpoint vs last trade price based on spread)
|
|
16
|
+
- 🚦 **Rate Limiting**: Built-in rate limiting to respect Polymarket API limits
|
|
17
|
+
- 🔗 **Group Management**: Efficiently manages multiple asset subscriptions across connection groups
|
|
18
|
+
- 💪 **TypeScript Support**: Full TypeScript definitions for all events and handlers
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { WSSubscriptionManager, WebSocketHandlers } from '@nevuamarkets/poly-websockets';
|
|
24
|
+
|
|
25
|
+
// Define your event handlers
|
|
26
|
+
const handlers: WebSocketHandlers = {
|
|
27
|
+
onPolymarketPriceUpdate: async (events) => {
|
|
28
|
+
events.forEach(event => {
|
|
29
|
+
console.log(`Price update for ${event.asset_id}: $${event.price}`);
|
|
30
|
+
});
|
|
31
|
+
},
|
|
32
|
+
onError: async (error) => {
|
|
33
|
+
console.error('WebSocket error:', error);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Create the subscription manager
|
|
38
|
+
const manager = new WSSubscriptionManager(handlers);
|
|
39
|
+
|
|
40
|
+
// Subscribe to assets
|
|
41
|
+
await manager.addSubscriptions(['asset-id-1', 'asset-id-2']);
|
|
42
|
+
|
|
43
|
+
// Remove subscriptions
|
|
44
|
+
await manager.removeSubscriptions(['asset-id-1']);
|
|
45
|
+
|
|
46
|
+
// Clear all subscriptions and connections
|
|
47
|
+
await manager.clearState();
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## API Reference
|
|
51
|
+
|
|
52
|
+
### WSSubscriptionManager
|
|
53
|
+
|
|
54
|
+
The main class that manages WebSocket connections and subscriptions.
|
|
55
|
+
|
|
56
|
+
#### Constructor
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
new WSSubscriptionManager(handlers: WebSocketHandlers, options?: SubscriptionManagerOptions)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Parameters:**
|
|
63
|
+
- `handlers` - Event handlers for different WebSocket events
|
|
64
|
+
- `options` - Optional configuration object:
|
|
65
|
+
- `maxMarketsPerWS?: number` - Maximum assets per WebSocket connection (default: 100)
|
|
66
|
+
- `reconnectAndCleanupIntervalMs?: number` - Interval for reconnection attempts (default: 10s)
|
|
67
|
+
- `burstLimiter?: Bottleneck` - Custom rate limiter instance
|
|
68
|
+
|
|
69
|
+
**Connection Management:**
|
|
70
|
+
The WSSubscriptionManager automatically:
|
|
71
|
+
- Groups asset subscriptions into efficient WebSocket connections
|
|
72
|
+
- Handles reconnections when connections drop
|
|
73
|
+
- Manages connection lifecycle and cleanup
|
|
74
|
+
- Balances load across multiple WebSocket groups
|
|
75
|
+
|
|
76
|
+
#### Methods
|
|
77
|
+
|
|
78
|
+
##### `addSubscriptions(assetIds: string[]): Promise<void>`
|
|
79
|
+
|
|
80
|
+
Adds new asset subscriptions. The manager will:
|
|
81
|
+
- Filter out already subscribed assets
|
|
82
|
+
- Find available connection groups or create new ones
|
|
83
|
+
- Establish WebSocket connections as needed
|
|
84
|
+
|
|
85
|
+
##### `removeSubscriptions(assetIds: string[]): Promise<void>`
|
|
86
|
+
|
|
87
|
+
Removes asset subscriptions. Connections are kept alive to avoid missing events, and unused groups are cleaned up during the next reconnection cycle.
|
|
88
|
+
|
|
89
|
+
##### `clearState(): Promise<void>`
|
|
90
|
+
|
|
91
|
+
Clears all subscriptions and state:
|
|
92
|
+
- Removes all asset subscriptions
|
|
93
|
+
- Closes all WebSocket connections
|
|
94
|
+
- Clears the internal order book cache
|
|
95
|
+
|
|
96
|
+
### WebSocketHandlers
|
|
97
|
+
|
|
98
|
+
Interface defining event handlers for different WebSocket events.
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
interface WebSocketHandlers {
|
|
102
|
+
// Core Polymarket WebSocket events
|
|
103
|
+
onBook?: (events: BookEvent[]) => Promise<void>;
|
|
104
|
+
onLastTradePrice?: (events: LastTradePriceEvent[]) => Promise<void>;
|
|
105
|
+
onPriceChange?: (events: PriceChangeEvent[]) => Promise<void>;
|
|
106
|
+
onTickSizeChange?: (events: TickSizeChangeEvent[]) => Promise<void>;
|
|
107
|
+
|
|
108
|
+
// Aggregated price update event (recommended for most use cases)
|
|
109
|
+
onPolymarketPriceUpdate?: (events: PolymarketPriceUpdateEvent[]) => Promise<void>;
|
|
110
|
+
|
|
111
|
+
// Connection lifecycle events
|
|
112
|
+
onWSOpen?: (groupId: string, assetIds: string[]) => Promise<void>;
|
|
113
|
+
onWSClose?: (groupId: string, code: number, reason: string) => Promise<void>;
|
|
114
|
+
onError?: (error: Error) => Promise<void>;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
#### Key Event Types
|
|
119
|
+
|
|
120
|
+
**PolymarketPriceUpdateEvent** (Recommended)
|
|
121
|
+
- Aggregated price update following Polymarket's display logic
|
|
122
|
+
- Uses midpoint when spread < $0.10, otherwise uses last trade price
|
|
123
|
+
- Includes full order book context
|
|
124
|
+
|
|
125
|
+
**BookEvent**
|
|
126
|
+
- Complete order book snapshots with bids and asks
|
|
127
|
+
- Triggered on significant order book changes
|
|
128
|
+
|
|
129
|
+
**PriceChangeEvent**
|
|
130
|
+
- Individual price level changes in the order book
|
|
131
|
+
- More granular than book events
|
|
132
|
+
|
|
133
|
+
**LastTradePriceEvent**
|
|
134
|
+
- Real-time trade executions
|
|
135
|
+
- Includes trade side, size, and price
|
|
136
|
+
|
|
137
|
+
## Advanced Usage
|
|
138
|
+
|
|
139
|
+
### Custom Rate Limiting
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import Bottleneck from 'bottleneck';
|
|
143
|
+
|
|
144
|
+
const customLimiter = new Bottleneck({
|
|
145
|
+
reservoir: 3,
|
|
146
|
+
reservoirRefreshAmount: 3,
|
|
147
|
+
reservoirRefreshInterval: 1000,
|
|
148
|
+
maxConcurrent: 3
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const manager = new WSSubscriptionManager(handlers, {
|
|
152
|
+
burstLimiter: customLimiter
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Connection Group Configuration
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
const manager = new WSSubscriptionManager(handlers, {
|
|
160
|
+
maxMarketsPerWS: 50, // Smaller groups for more granular control
|
|
161
|
+
reconnectAndCleanupIntervalMs: 5000 // More frequent reconnection checks
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Handling All Event Types
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const comprehensiveHandlers: WebSocketHandlers = {
|
|
169
|
+
onPolymarketPriceUpdate: async (events) => {
|
|
170
|
+
// Primary price updates for UI display
|
|
171
|
+
events.forEach(event => {
|
|
172
|
+
updatePriceDisplay(event.asset_id, event.price);
|
|
173
|
+
});
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
onBook: async (events) => {
|
|
177
|
+
// Order book depth for trading interfaces
|
|
178
|
+
events.forEach(event => {
|
|
179
|
+
updateOrderBook(event.asset_id, event.bids, event.asks);
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
onLastTradePrice: async (events) => {
|
|
184
|
+
// Real-time trade feed
|
|
185
|
+
events.forEach(event => {
|
|
186
|
+
logTrade(event.asset_id, event.price, event.size, event.side);
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
onWSOpen: async (groupId, assetIds) => {
|
|
191
|
+
console.log(`Connected group ${groupId} with ${assetIds.length} assets`);
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
onWSClose: async (groupId, code, reason) => {
|
|
195
|
+
console.log(`Disconnected group ${groupId}: ${reason} (${code})`);
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
onError: async (error) => {
|
|
199
|
+
console.error('WebSocket error:', error);
|
|
200
|
+
// Implement your error handling/alerting logic
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Examples
|
|
206
|
+
|
|
207
|
+
Check the [examples](./examples) folder for complete working examples including:
|
|
208
|
+
- Basic price monitoring
|
|
209
|
+
- Market data aggregation
|
|
210
|
+
- Real-time trading interfaces
|
|
211
|
+
|
|
212
|
+
## Error Handling
|
|
213
|
+
|
|
214
|
+
The library includes comprehensive error handling:
|
|
215
|
+
- Automatic reconnection on connection drops
|
|
216
|
+
- Rate limiting to prevent API blocking
|
|
217
|
+
- Graceful handling of malformed messages
|
|
218
|
+
- User-defined error callbacks for custom handling
|
|
219
|
+
|
|
220
|
+
## Rate Limits
|
|
221
|
+
|
|
222
|
+
Respects Polymarket's API rate limits:
|
|
223
|
+
- Default: 5 requests per second burst limit
|
|
224
|
+
- Configurable through custom Bottleneck instances
|
|
225
|
+
- Automatic backoff on rate limit hits
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
AGPL-3
|
|
230
|
+
|
|
231
|
+
## Testing
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
npm test
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## TypeScript Support
|
|
238
|
+
|
|
239
|
+
Full TypeScript definitions included.
|
|
240
|
+
|
|
241
|
+
## Disclaimer
|
|
242
|
+
|
|
243
|
+
This software is provided "as is", without warranty of any kind, express or implied. The author(s) are not responsible for:
|
|
244
|
+
|
|
245
|
+
- Any financial losses incurred from using this software
|
|
246
|
+
- Trading decisions made based on the data provided
|
|
247
|
+
- Bugs, errors, or inaccuracies in the data
|
|
248
|
+
- System failures or downtime
|
|
249
|
+
- Any other damages arising from the use of this software
|
|
250
|
+
|
|
251
|
+
Use at your own risk. Always verify data independently and never rely solely on automated systems for trading decisions.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { WebSocketHandlers } from './types/PolymarketWebSocket';
|
|
2
|
+
import { SubscriptionManagerOptions } from './types/WebSocketSubscriptions';
|
|
3
|
+
declare class WSSubscriptionManager {
|
|
4
|
+
private handlers;
|
|
5
|
+
private burstLimiter;
|
|
6
|
+
private groupRegistry;
|
|
7
|
+
private bookCache;
|
|
8
|
+
private reconnectAndCleanupIntervalMs;
|
|
9
|
+
private maxMarketsPerWS;
|
|
10
|
+
constructor(userHandlers: WebSocketHandlers, options?: SubscriptionManagerOptions);
|
|
11
|
+
clearState(): Promise<void>;
|
|
12
|
+
private actOnSubscribedEvents;
|
|
13
|
+
addSubscriptions(assetIdsToAdd: string[]): Promise<void>;
|
|
14
|
+
removeSubscriptions(assetIdsToRemove: string[]): Promise<void>;
|
|
15
|
+
private reconnectAndCleanupGroups;
|
|
16
|
+
private createWebSocketClient;
|
|
17
|
+
}
|
|
18
|
+
export { WSSubscriptionManager, WebSocketHandlers };
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.WSSubscriptionManager = void 0;
|
|
7
|
+
const ms_1 = __importDefault(require("ms"));
|
|
8
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
9
|
+
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
10
|
+
const GroupRegistry_1 = require("./modules/GroupRegistry");
|
|
11
|
+
const OrderBookCache_1 = require("./modules/OrderBookCache");
|
|
12
|
+
const GroupSocket_1 = require("./modules/GroupSocket");
|
|
13
|
+
const logger_1 = require("./logger");
|
|
14
|
+
// Keeping a burst limit under 10/s to avoid rate limiting
|
|
15
|
+
// See https://docs.polymarket.com/quickstart/introduction/rate-limits#api-rate-limits
|
|
16
|
+
const BURST_LIMIT_PER_SECOND = 5;
|
|
17
|
+
const DEFAULT_RECONNECT_AND_CLEANUP_INTERVAL_MS = (0, ms_1.default)('10s');
|
|
18
|
+
const DEFAULT_MAX_MARKETS_PER_WS = 100;
|
|
19
|
+
class WSSubscriptionManager {
|
|
20
|
+
constructor(userHandlers, options) {
|
|
21
|
+
this.groupRegistry = new GroupRegistry_1.GroupRegistry();
|
|
22
|
+
this.bookCache = new OrderBookCache_1.OrderBookCache();
|
|
23
|
+
this.burstLimiter = (options === null || options === void 0 ? void 0 : options.burstLimiter) || new bottleneck_1.default({
|
|
24
|
+
reservoir: BURST_LIMIT_PER_SECOND,
|
|
25
|
+
reservoirRefreshAmount: BURST_LIMIT_PER_SECOND,
|
|
26
|
+
reservoirRefreshInterval: (0, ms_1.default)('1s'),
|
|
27
|
+
maxConcurrent: BURST_LIMIT_PER_SECOND
|
|
28
|
+
});
|
|
29
|
+
this.reconnectAndCleanupIntervalMs = (options === null || options === void 0 ? void 0 : options.reconnectAndCleanupIntervalMs) || DEFAULT_RECONNECT_AND_CLEANUP_INTERVAL_MS;
|
|
30
|
+
this.maxMarketsPerWS = (options === null || options === void 0 ? void 0 : options.maxMarketsPerWS) || DEFAULT_MAX_MARKETS_PER_WS;
|
|
31
|
+
this.handlers = {
|
|
32
|
+
onBook: async (events) => {
|
|
33
|
+
await this.actOnSubscribedEvents(events, userHandlers.onBook);
|
|
34
|
+
},
|
|
35
|
+
onLastTradePrice: async (events) => {
|
|
36
|
+
await this.actOnSubscribedEvents(events, userHandlers.onLastTradePrice);
|
|
37
|
+
},
|
|
38
|
+
onTickSizeChange: async (events) => {
|
|
39
|
+
await this.actOnSubscribedEvents(events, userHandlers.onTickSizeChange);
|
|
40
|
+
},
|
|
41
|
+
onPriceChange: async (events) => {
|
|
42
|
+
await this.actOnSubscribedEvents(events, userHandlers.onPriceChange);
|
|
43
|
+
},
|
|
44
|
+
onPolymarketPriceUpdate: async (events) => {
|
|
45
|
+
await this.actOnSubscribedEvents(events, userHandlers.onPolymarketPriceUpdate);
|
|
46
|
+
},
|
|
47
|
+
onWSClose: userHandlers.onWSClose,
|
|
48
|
+
onWSOpen: userHandlers.onWSOpen,
|
|
49
|
+
onError: userHandlers.onError
|
|
50
|
+
};
|
|
51
|
+
this.burstLimiter.on('error', (err) => {
|
|
52
|
+
var _a, _b;
|
|
53
|
+
(_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
|
|
54
|
+
});
|
|
55
|
+
// Check for dead groups every 10s and reconnect them if needed
|
|
56
|
+
setInterval(() => {
|
|
57
|
+
this.reconnectAndCleanupGroups();
|
|
58
|
+
}, this.reconnectAndCleanupIntervalMs);
|
|
59
|
+
}
|
|
60
|
+
/*
|
|
61
|
+
Clears all WebSocket subscriptions and state.
|
|
62
|
+
|
|
63
|
+
This will:
|
|
64
|
+
|
|
65
|
+
1. Remove all subscriptions and groups
|
|
66
|
+
2. Close all WebSocket connections
|
|
67
|
+
3. Clear the order book cache
|
|
68
|
+
*/
|
|
69
|
+
async clearState() {
|
|
70
|
+
const previousGroups = await this.groupRegistry.clearAllGroups();
|
|
71
|
+
// Close sockets outside the lock
|
|
72
|
+
for (const group of previousGroups) {
|
|
73
|
+
this.groupRegistry.disconnectGroup(group);
|
|
74
|
+
}
|
|
75
|
+
// Also clear the order book cache
|
|
76
|
+
this.bookCache.clear();
|
|
77
|
+
}
|
|
78
|
+
/*
|
|
79
|
+
This function is called when:
|
|
80
|
+
- a websocket event is received from the Polymarket WS
|
|
81
|
+
- a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
|
|
82
|
+
depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
|
|
83
|
+
|
|
84
|
+
The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
|
|
85
|
+
*/
|
|
86
|
+
async actOnSubscribedEvents(events, action) {
|
|
87
|
+
// Filter out events that are not subscribed to by any groups
|
|
88
|
+
events = lodash_1.default.filter(events, (event) => {
|
|
89
|
+
const groupIndices = this.groupRegistry.getGroupIndicesForAsset(event.asset_id);
|
|
90
|
+
if (groupIndices.length > 1) {
|
|
91
|
+
logger_1.logger.warn({
|
|
92
|
+
message: 'Found multiple groups for asset',
|
|
93
|
+
asset_id: event.asset_id,
|
|
94
|
+
group_indices: groupIndices
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
return groupIndices.length > 0;
|
|
98
|
+
});
|
|
99
|
+
await (action === null || action === void 0 ? void 0 : action(events));
|
|
100
|
+
}
|
|
101
|
+
/*
|
|
102
|
+
Edits wsGroups: Adds new subscriptions.
|
|
103
|
+
|
|
104
|
+
- Filters out assets that are already subscribed
|
|
105
|
+
- Finds a group with capacity or creates a new one
|
|
106
|
+
- Creates a new WebSocket client and adds it to the group
|
|
107
|
+
*/
|
|
108
|
+
async addSubscriptions(assetIdsToAdd) {
|
|
109
|
+
var _a, _b;
|
|
110
|
+
try {
|
|
111
|
+
const groupIdsToConnect = await this.groupRegistry.addAssets(assetIdsToAdd, this.maxMarketsPerWS);
|
|
112
|
+
for (const groupId of groupIdsToConnect) {
|
|
113
|
+
await this.createWebSocketClient(groupId, this.handlers);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const msg = `Error adding subscriptions: ${error instanceof Error ? error.message : String(error)}`;
|
|
118
|
+
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(msg)));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/*
|
|
122
|
+
Edits wsGroups: Removes subscriptions.
|
|
123
|
+
The group will use the updated subscriptions when it reconnects.
|
|
124
|
+
We do that because we don't want to miss events by reconnecting.
|
|
125
|
+
*/
|
|
126
|
+
async removeSubscriptions(assetIdsToRemove) {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
try {
|
|
129
|
+
await this.groupRegistry.removeAssets(assetIdsToRemove, this.bookCache);
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
const errMsg = `Error removing subscriptions: ${error instanceof Error ? error.message : String(error)}`;
|
|
133
|
+
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(errMsg)));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/*
|
|
137
|
+
This function runs periodically and:
|
|
138
|
+
|
|
139
|
+
- Tries to reconnect groups that have assets and are disconnected
|
|
140
|
+
- Cleans up groups that have no assets
|
|
141
|
+
*/
|
|
142
|
+
async reconnectAndCleanupGroups() {
|
|
143
|
+
var _a, _b;
|
|
144
|
+
try {
|
|
145
|
+
const reconnectIds = await this.groupRegistry.getGroupsToReconnectAndCleanup();
|
|
146
|
+
for (const groupId of reconnectIds) {
|
|
147
|
+
await this.createWebSocketClient(groupId, this.handlers);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async createWebSocketClient(groupId, handlers) {
|
|
155
|
+
var _a, _b;
|
|
156
|
+
const group = this.groupRegistry.findGroupById(groupId);
|
|
157
|
+
/*
|
|
158
|
+
Should never happen, but just in case.
|
|
159
|
+
*/
|
|
160
|
+
if (!group) {
|
|
161
|
+
await ((_a = handlers.onError) === null || _a === void 0 ? void 0 : _a.call(handlers, new Error(`Group ${groupId} not found in registry`)));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const groupSocket = new GroupSocket_1.GroupSocket(group, this.burstLimiter, this.bookCache, handlers);
|
|
165
|
+
try {
|
|
166
|
+
await groupSocket.connect();
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const errorMessage = `Error creating WebSocket client for group ${groupId}: ${error instanceof Error ? error.message : String(error)}`;
|
|
170
|
+
await ((_b = handlers.onError) === null || _b === void 0 ? void 0 : _b.call(handlers, new Error(errorMessage)));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.WSSubscriptionManager = WSSubscriptionManager;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.WSSubscriptionManager = void 0;
|
|
18
|
+
var WSSubscriptionManager_1 = require("./WSSubscriptionManager");
|
|
19
|
+
Object.defineProperty(exports, "WSSubscriptionManager", { enumerable: true, get: function () { return WSSubscriptionManager_1.WSSubscriptionManager; } });
|
|
20
|
+
__exportStar(require("./types/PolymarketWebSocket"), exports);
|
|
21
|
+
__exportStar(require("./types/WebSocketSubscriptions"), exports);
|
package/dist/logger.d.ts
ADDED
package/dist/logger.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.logger = void 0;
|
|
7
|
+
const winston_1 = __importDefault(require("winston"));
|
|
8
|
+
// Override with LOG_LEVEL environment variable (e.g., LOG_LEVEL=info npm start)
|
|
9
|
+
exports.logger = winston_1.default.createLogger({
|
|
10
|
+
level: process.env.LOG_LEVEL || 'error',
|
|
11
|
+
format: winston_1.default.format.combine(winston_1.default.format.timestamp(), winston_1.default.format.errors({ stack: true }), winston_1.default.format.colorize(), winston_1.default.format.printf(({ level, message, timestamp, ...rest }) => {
|
|
12
|
+
// Ensure consistent order: timestamp, level, message, then rest of fields
|
|
13
|
+
const restString = Object.keys(rest)
|
|
14
|
+
.filter(key => key !== 'service') // Exclude service since we add it in defaultMeta
|
|
15
|
+
.sort()
|
|
16
|
+
.map(key => `${key}: ${JSON.stringify(rest[key])}`)
|
|
17
|
+
.join(', ');
|
|
18
|
+
return `${timestamp} ${level}: ${message}${restString ? ` (${restString})` : ''}`;
|
|
19
|
+
})),
|
|
20
|
+
defaultMeta: { service: 'poly-websockets' },
|
|
21
|
+
transports: [
|
|
22
|
+
new winston_1.default.transports.Console({
|
|
23
|
+
format: winston_1.default.format.combine(winston_1.default.format.colorize({
|
|
24
|
+
all: true,
|
|
25
|
+
colors: {
|
|
26
|
+
error: 'red',
|
|
27
|
+
warn: 'yellow',
|
|
28
|
+
info: 'cyan',
|
|
29
|
+
debug: 'green'
|
|
30
|
+
}
|
|
31
|
+
}))
|
|
32
|
+
})
|
|
33
|
+
]
|
|
34
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { WebSocketGroup } from '../types/WebSocketSubscriptions';
|
|
2
|
+
import { OrderBookCache } from './OrderBookCache';
|
|
3
|
+
export declare class GroupRegistry {
|
|
4
|
+
/**
|
|
5
|
+
* Atomic mutate helper.
|
|
6
|
+
*
|
|
7
|
+
* @param fn - The function to run atomically.
|
|
8
|
+
* @returns The result of the function.
|
|
9
|
+
*/
|
|
10
|
+
mutate<T>(fn: (groups: WebSocketGroup[]) => T | Promise<T>): Promise<T>;
|
|
11
|
+
/**
|
|
12
|
+
* Read-only copy of the registry.
|
|
13
|
+
*
|
|
14
|
+
* Only to be used in test suite.
|
|
15
|
+
*/
|
|
16
|
+
snapshot(): WebSocketGroup[];
|
|
17
|
+
/**
|
|
18
|
+
* Find the first group with capacity to hold new assets.
|
|
19
|
+
*
|
|
20
|
+
* Returns the groupId if found, otherwise null.
|
|
21
|
+
*/
|
|
22
|
+
findGroupWithCapacity(newAssetLen: number, maxPerWS: number): string | null;
|
|
23
|
+
/**
|
|
24
|
+
* Get the indices of all groups that contain the asset.
|
|
25
|
+
*
|
|
26
|
+
* Returns an array of indices.
|
|
27
|
+
*/
|
|
28
|
+
getGroupIndicesForAsset(assetId: string): number[];
|
|
29
|
+
/**
|
|
30
|
+
* Check if any group contains the asset.
|
|
31
|
+
*/
|
|
32
|
+
hasAsset(assetId: string): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Find the group by groupId.
|
|
35
|
+
*
|
|
36
|
+
* Returns the group if found, otherwise undefined.
|
|
37
|
+
*/
|
|
38
|
+
findGroupById(groupId: string): WebSocketGroup | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* Atomically remove **all** groups from the registry and return them so the
|
|
41
|
+
* caller can perform any asynchronous cleanup (closing sockets, etc.)
|
|
42
|
+
* outside the lock.
|
|
43
|
+
*
|
|
44
|
+
* Returns the removed groups.
|
|
45
|
+
*/
|
|
46
|
+
clearAllGroups(): Promise<WebSocketGroup[]>;
|
|
47
|
+
/**
|
|
48
|
+
* Add new asset subscriptions.
|
|
49
|
+
*
|
|
50
|
+
* – Ignores assets that are already subscribed.
|
|
51
|
+
* – Either reuses an existing group with capacity or creates new groups (size ≤ maxPerWS).
|
|
52
|
+
* – If appending to a group:
|
|
53
|
+
* - A new group is created with the updated assetIds.
|
|
54
|
+
* - The old group is marked for cleanup.
|
|
55
|
+
* - The group is added to the list of groups to connect.
|
|
56
|
+
*
|
|
57
|
+
* @param assetIds - The assetIds to add.
|
|
58
|
+
* @param maxPerWS - The maximum number of assets per WebSocket group.
|
|
59
|
+
* @returns An array of *new* groupIds that need websocket connections.
|
|
60
|
+
*/
|
|
61
|
+
addAssets(assetIds: string[], maxPerWS: number): Promise<string[]>;
|
|
62
|
+
/**
|
|
63
|
+
* Remove asset subscriptions from every group that contains the asset.
|
|
64
|
+
*
|
|
65
|
+
* It should be only one group that contains the asset, we search all of them
|
|
66
|
+
* regardless.
|
|
67
|
+
*
|
|
68
|
+
* Returns the list of assetIds that were removed.
|
|
69
|
+
*/
|
|
70
|
+
removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Disconnect a group.
|
|
73
|
+
*/
|
|
74
|
+
disconnectGroup(group: WebSocketGroup): void;
|
|
75
|
+
/**
|
|
76
|
+
* Check status of groups and reconnect or cleanup as needed.
|
|
77
|
+
*
|
|
78
|
+
* – Empty groups are removed from the global array and returned.
|
|
79
|
+
* – Dead (but non-empty) groups are reset so that caller can reconnect them.
|
|
80
|
+
* – Pending groups are returned so that caller can connect them.
|
|
81
|
+
*
|
|
82
|
+
* Returns an array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
|
|
83
|
+
*/
|
|
84
|
+
getGroupsToReconnectAndCleanup(): Promise<string[]>;
|
|
85
|
+
}
|