@nevuamarkets/poly-websockets 0.3.0 → 0.3.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.
@@ -8,10 +8,52 @@ declare class WSSubscriptionManager {
8
8
  private reconnectAndCleanupIntervalMs;
9
9
  private maxMarketsPerWS;
10
10
  constructor(userHandlers: WebSocketHandlers, options?: SubscriptionManagerOptions);
11
+ /**
12
+ * Clears all WebSocket subscriptions and state.
13
+ *
14
+ * This will:
15
+ *
16
+ * 1. Remove all subscriptions and groups
17
+ * 2. Close all WebSocket connections
18
+ * 3. Clear the order book cache
19
+ */
11
20
  clearState(): Promise<void>;
21
+ /**
22
+ * This function is called when:
23
+ * - a websocket event is received from the Polymarket WS
24
+ * - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
25
+ * depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
26
+ *
27
+ * The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
28
+ *
29
+ * @param events - The events to process.
30
+ * @param action - The action to perform on the filtered events.
31
+ */
12
32
  private actOnSubscribedEvents;
33
+ /**
34
+ * Edits wsGroups: Adds new subscriptions.
35
+ *
36
+ * - Filters out assets that are already subscribed
37
+ * - Finds a group with capacity or creates a new one
38
+ * - Creates a new WebSocket client and adds it to the group
39
+ *
40
+ * @param assetIdsToAdd - The asset IDs to add subscriptions for.
41
+ */
13
42
  addSubscriptions(assetIdsToAdd: string[]): Promise<void>;
43
+ /**
44
+ * Edits wsGroups: Removes subscriptions.
45
+ * The group will use the updated subscriptions when it reconnects.
46
+ * We do that because we don't want to miss events by reconnecting.
47
+ *
48
+ * @param assetIdsToRemove - The asset IDs to remove subscriptions for.
49
+ */
14
50
  removeSubscriptions(assetIdsToRemove: string[]): Promise<void>;
51
+ /**
52
+ * This function runs periodically and:
53
+ *
54
+ * - Tries to reconnect groups that have assets and are disconnected
55
+ * - Cleans up groups that have no assets
56
+ */
15
57
  private reconnectAndCleanupGroups;
16
58
  getStatistics(): {
17
59
  openWebSockets: number;
@@ -58,15 +58,15 @@ class WSSubscriptionManager {
58
58
  this.reconnectAndCleanupGroups();
59
59
  }, this.reconnectAndCleanupIntervalMs);
60
60
  }
61
- /*
62
- Clears all WebSocket subscriptions and state.
63
-
64
- This will:
65
-
66
- 1. Remove all subscriptions and groups
67
- 2. Close all WebSocket connections
68
- 3. Clear the order book cache
69
- */
61
+ /**
62
+ * Clears all WebSocket subscriptions and state.
63
+ *
64
+ * This will:
65
+ *
66
+ * 1. Remove all subscriptions and groups
67
+ * 2. Close all WebSocket connections
68
+ * 3. Clear the order book cache
69
+ */
70
70
  async clearState() {
71
71
  const previousGroups = await this.groupRegistry.clearAllGroups();
72
72
  // Close sockets outside the lock
@@ -76,14 +76,17 @@ class WSSubscriptionManager {
76
76
  // Also clear the order book cache
77
77
  this.bookCache.clear();
78
78
  }
79
- /*
80
- This function is called when:
81
- - a websocket event is received from the Polymarket WS
82
- - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
83
- depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
84
-
85
- The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
86
- */
79
+ /**
80
+ * This function is called when:
81
+ * - a websocket event is received from the Polymarket WS
82
+ * - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
83
+ * depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
84
+ *
85
+ * The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
86
+ *
87
+ * @param events - The events to process.
88
+ * @param action - The action to perform on the filtered events.
89
+ */
87
90
  async actOnSubscribedEvents(events, action) {
88
91
  // Filter out events that are not subscribed to by any groups
89
92
  events = lodash_1.default.filter(events, (event) => {
@@ -118,13 +121,15 @@ class WSSubscriptionManager {
118
121
  });
119
122
  await (action === null || action === void 0 ? void 0 : action(events));
120
123
  }
121
- /*
122
- Edits wsGroups: Adds new subscriptions.
123
-
124
- - Filters out assets that are already subscribed
125
- - Finds a group with capacity or creates a new one
126
- - Creates a new WebSocket client and adds it to the group
127
- */
124
+ /**
125
+ * Edits wsGroups: Adds new subscriptions.
126
+ *
127
+ * - Filters out assets that are already subscribed
128
+ * - Finds a group with capacity or creates a new one
129
+ * - Creates a new WebSocket client and adds it to the group
130
+ *
131
+ * @param assetIdsToAdd - The asset IDs to add subscriptions for.
132
+ */
128
133
  async addSubscriptions(assetIdsToAdd) {
129
134
  var _a, _b;
130
135
  try {
@@ -138,11 +143,13 @@ class WSSubscriptionManager {
138
143
  await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(msg)));
139
144
  }
140
145
  }
141
- /*
142
- Edits wsGroups: Removes subscriptions.
143
- The group will use the updated subscriptions when it reconnects.
144
- We do that because we don't want to miss events by reconnecting.
145
- */
146
+ /**
147
+ * Edits wsGroups: Removes subscriptions.
148
+ * The group will use the updated subscriptions when it reconnects.
149
+ * We do that because we don't want to miss events by reconnecting.
150
+ *
151
+ * @param assetIdsToRemove - The asset IDs to remove subscriptions for.
152
+ */
146
153
  async removeSubscriptions(assetIdsToRemove) {
147
154
  var _a, _b;
148
155
  try {
@@ -153,12 +160,12 @@ class WSSubscriptionManager {
153
160
  await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(errMsg)));
154
161
  }
155
162
  }
156
- /*
157
- This function runs periodically and:
158
-
159
- - Tries to reconnect groups that have assets and are disconnected
160
- - Cleans up groups that have no assets
161
- */
163
+ /**
164
+ * This function runs periodically and:
165
+ *
166
+ * - Tries to reconnect groups that have assets and are disconnected
167
+ * - Cleans up groups that have no assets
168
+ */
162
169
  async reconnectAndCleanupGroups() {
163
170
  var _a, _b;
164
171
  try {
@@ -17,23 +17,28 @@ export declare class GroupRegistry {
17
17
  /**
18
18
  * Find the first group with capacity to hold new assets.
19
19
  *
20
- * Returns the groupId if found, otherwise null.
20
+ * @returns The groupId if found, otherwise null.
21
21
  */
22
22
  findGroupWithCapacity(newAssetLen: number, maxPerWS: number): string | null;
23
23
  /**
24
24
  * Get the indices of all groups that contain the asset.
25
25
  *
26
- * Returns an array of indices.
26
+ * @param assetId - The tokenId of a market.
27
+ * @returns An array of indices.
27
28
  */
28
29
  getGroupIndicesForAsset(assetId: string): number[];
29
30
  /**
30
31
  * Check if any group contains the asset.
32
+ *
33
+ * @param assetId - The tokenId of a market.
34
+ * @returns True if found.
31
35
  */
32
36
  hasAsset(assetId: string): boolean;
33
37
  /**
34
38
  * Find the group by groupId.
35
39
  *
36
- * Returns the group if found, otherwise undefined.
40
+ * @param groupId - The group UUID.
41
+ * @returns The group if found, otherwise undefined.
37
42
  */
38
43
  findGroupById(groupId: string): WebSocketGroup | undefined;
39
44
  /**
@@ -52,7 +57,7 @@ export declare class GroupRegistry {
52
57
  * caller can perform any asynchronous cleanup (closing sockets, etc.)
53
58
  * outside the lock.
54
59
  *
55
- * Returns the removed groups.
60
+ * @returns The removed groups.
56
61
  */
57
62
  clearAllGroups(): Promise<WebSocketGroup[]>;
58
63
  /**
@@ -73,14 +78,18 @@ export declare class GroupRegistry {
73
78
  /**
74
79
  * Remove asset subscriptions from every group that contains the asset.
75
80
  *
76
- * It should be only one group that contains the asset, we search all of them
81
+ * It should be only one group that contains the asset, but we search all of them
77
82
  * regardless.
78
83
  *
79
- * Returns the list of assetIds that were removed.
84
+ * @param assetIds - The tokenIds of the markets to remove.
85
+ * @param bookCache - The stored orderbook.
86
+ * @returns The list of assetIds that were removed.
80
87
  */
81
88
  removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]>;
82
89
  /**
83
90
  * Disconnect a group.
91
+ *
92
+ * @param group - The group to disconnect.
84
93
  */
85
94
  disconnectGroup(group: WebSocketGroup): void;
86
95
  /**
@@ -90,7 +99,7 @@ export declare class GroupRegistry {
90
99
  * – Dead (but non-empty) groups are reset so that caller can reconnect them.
91
100
  * – Pending groups are returned so that caller can connect them.
92
101
  *
93
- * Returns an array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
102
+ * @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
94
103
  */
95
104
  getGroupsToReconnectAndCleanup(): Promise<string[]>;
96
105
  }
@@ -47,7 +47,7 @@ class GroupRegistry {
47
47
  /**
48
48
  * Find the first group with capacity to hold new assets.
49
49
  *
50
- * Returns the groupId if found, otherwise null.
50
+ * @returns The groupId if found, otherwise null.
51
51
  */
52
52
  findGroupWithCapacity(newAssetLen, maxPerWS) {
53
53
  for (const group of wsGroups) {
@@ -61,7 +61,8 @@ class GroupRegistry {
61
61
  /**
62
62
  * Get the indices of all groups that contain the asset.
63
63
  *
64
- * Returns an array of indices.
64
+ * @param assetId - The tokenId of a market.
65
+ * @returns An array of indices.
65
66
  */
66
67
  getGroupIndicesForAsset(assetId) {
67
68
  var _a;
@@ -74,6 +75,9 @@ class GroupRegistry {
74
75
  }
75
76
  /**
76
77
  * Check if any group contains the asset.
78
+ *
79
+ * @param assetId - The tokenId of a market.
80
+ * @returns True if found.
77
81
  */
78
82
  hasAsset(assetId) {
79
83
  return wsGroups.some(group => group.assetIds.has(assetId));
@@ -81,7 +85,8 @@ class GroupRegistry {
81
85
  /**
82
86
  * Find the group by groupId.
83
87
  *
84
- * Returns the group if found, otherwise undefined.
88
+ * @param groupId - The group UUID.
89
+ * @returns The group if found, otherwise undefined.
85
90
  */
86
91
  findGroupById(groupId) {
87
92
  return wsGroups.find(g => g.groupId === groupId);
@@ -116,7 +121,7 @@ class GroupRegistry {
116
121
  * caller can perform any asynchronous cleanup (closing sockets, etc.)
117
122
  * outside the lock.
118
123
  *
119
- * Returns the removed groups.
124
+ * @returns The removed groups.
120
125
  */
121
126
  async clearAllGroups() {
122
127
  let removed = [];
@@ -197,10 +202,12 @@ class GroupRegistry {
197
202
  /**
198
203
  * Remove asset subscriptions from every group that contains the asset.
199
204
  *
200
- * It should be only one group that contains the asset, we search all of them
205
+ * It should be only one group that contains the asset, but we search all of them
201
206
  * regardless.
202
207
  *
203
- * Returns the list of assetIds that were removed.
208
+ * @param assetIds - The tokenIds of the markets to remove.
209
+ * @param bookCache - The stored orderbook.
210
+ * @returns The list of assetIds that were removed.
204
211
  */
205
212
  async removeAssets(assetIds, bookCache) {
206
213
  const removedAssetIds = [];
@@ -225,11 +232,14 @@ class GroupRegistry {
225
232
  }
226
233
  /**
227
234
  * Disconnect a group.
235
+ *
236
+ * @param group - The group to disconnect.
228
237
  */
229
238
  disconnectGroup(group) {
230
239
  var _a;
231
240
  (_a = group.wsClient) === null || _a === void 0 ? void 0 : _a.close();
232
241
  group.wsClient = null;
242
+ group.connecting = false;
233
243
  logger_1.logger.info({
234
244
  message: 'Disconnected group',
235
245
  groupId: group.groupId,
@@ -244,7 +254,7 @@ class GroupRegistry {
244
254
  * – Dead (but non-empty) groups are reset so that caller can reconnect them.
245
255
  * – Pending groups are returned so that caller can connect them.
246
256
  *
247
- * Returns an array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
257
+ * @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
248
258
  */
249
259
  async getGroupsToReconnectAndCleanup() {
250
260
  const reconnectIds = [];
@@ -268,6 +278,9 @@ class GroupRegistry {
268
278
  continue;
269
279
  }
270
280
  if (group.status === WebSocketSubscriptions_1.WebSocketStatus.PENDING) {
281
+ if (group.connecting) {
282
+ continue;
283
+ }
271
284
  reconnectIds.push(group.groupId);
272
285
  }
273
286
  }
@@ -14,9 +14,46 @@ export declare class GroupSocket {
14
14
  *
15
15
  */
16
16
  connect(): Promise<void>;
17
+ /**
18
+ * Sets up event handlers for the WebSocket connection.
19
+ *
20
+ * Handles:
21
+ * - 'open': Authenticates and starts ping interval
22
+ * - 'message': Parses and routes events
23
+ * - 'pong': Handles pong responses
24
+ * - 'error': Handles errors
25
+ * - 'close': Handles connection closure
26
+ */
17
27
  private setupEventHandlers;
28
+ /**
29
+ * Handles book events by updating the cache and notifying listeners.
30
+ *
31
+ * @param bookEvents - The book events to process.
32
+ */
18
33
  private handleBookEvents;
34
+ /**
35
+ * Handles tick size change events by notifying listeners.
36
+ *
37
+ * @param tickEvents - The tick size change events to process.
38
+ */
19
39
  private handleTickEvents;
40
+ /**
41
+ * Handles price change events.
42
+ *
43
+ * - Updates the order book cache
44
+ * - Calculates derived price updates based on spread and midpoint
45
+ * - Notifies listeners of price changes and derived updates
46
+ *
47
+ * @param priceChangeEvents - The price change events to process.
48
+ */
20
49
  private handlePriceChangeEvents;
50
+ /**
51
+ * Handles last trade price events.
52
+ *
53
+ * - Notifies listeners of last trade price
54
+ * - Calculates derived price updates based on spread
55
+ *
56
+ * @param lastTradeEvents - The last trade price events to process.
57
+ */
21
58
  private handleLastTradeEvents;
22
59
  }
@@ -24,10 +24,14 @@ class GroupSocket {
24
24
  *
25
25
  */
26
26
  async connect() {
27
+ if (this.group.connecting) {
28
+ return;
29
+ }
27
30
  if (this.group.assetIds.size === 0) {
28
31
  this.group.status = WebSocketSubscriptions_1.WebSocketStatus.CLEANUP;
29
32
  return;
30
33
  }
34
+ this.group.connecting = true;
31
35
  try {
32
36
  logger_1.logger.info({
33
37
  message: 'Connecting to CLOB WebSocket',
@@ -54,8 +58,21 @@ class GroupSocket {
54
58
  this.group.status = WebSocketSubscriptions_1.WebSocketStatus.DEAD;
55
59
  throw err; // caller responsible for error handler
56
60
  }
61
+ finally {
62
+ this.group.connecting = false;
63
+ }
57
64
  this.setupEventHandlers();
58
65
  }
66
+ /**
67
+ * Sets up event handlers for the WebSocket connection.
68
+ *
69
+ * Handles:
70
+ * - 'open': Authenticates and starts ping interval
71
+ * - 'message': Parses and routes events
72
+ * - 'pong': Handles pong responses
73
+ * - 'error': Handles errors
74
+ * - 'close': Handles connection closure
75
+ */
59
76
  setupEventHandlers() {
60
77
  const group = this.group;
61
78
  const handlers = this.handlers;
@@ -220,6 +237,11 @@ class GroupSocket {
220
237
  return;
221
238
  }
222
239
  }
240
+ /**
241
+ * Handles book events by updating the cache and notifying listeners.
242
+ *
243
+ * @param bookEvents - The book events to process.
244
+ */
223
245
  async handleBookEvents(bookEvents) {
224
246
  var _a, _b;
225
247
  if (bookEvents.length) {
@@ -229,12 +251,26 @@ class GroupSocket {
229
251
  await ((_b = (_a = this.handlers).onBook) === null || _b === void 0 ? void 0 : _b.call(_a, bookEvents));
230
252
  }
231
253
  }
254
+ /**
255
+ * Handles tick size change events by notifying listeners.
256
+ *
257
+ * @param tickEvents - The tick size change events to process.
258
+ */
232
259
  async handleTickEvents(tickEvents) {
233
260
  var _a, _b;
234
261
  if (tickEvents.length) {
235
262
  await ((_b = (_a = this.handlers).onTickSizeChange) === null || _b === void 0 ? void 0 : _b.call(_a, tickEvents));
236
263
  }
237
264
  }
265
+ /**
266
+ * Handles price change events.
267
+ *
268
+ * - Updates the order book cache
269
+ * - Calculates derived price updates based on spread and midpoint
270
+ * - Notifies listeners of price changes and derived updates
271
+ *
272
+ * @param priceChangeEvents - The price change events to process.
273
+ */
238
274
  async handlePriceChangeEvents(priceChangeEvents) {
239
275
  var _a, _b, _c, _d;
240
276
  if (priceChangeEvents.length) {
@@ -309,6 +345,14 @@ class GroupSocket {
309
345
  }
310
346
  }
311
347
  }
348
+ /**
349
+ * Handles last trade price events.
350
+ *
351
+ * - Notifies listeners of last trade price
352
+ * - Calculates derived price updates based on spread
353
+ *
354
+ * @param lastTradeEvents - The last trade price events to process.
355
+ */
312
356
  async handleLastTradeEvents(lastTradeEvents) {
313
357
  var _a, _b, _c, _d;
314
358
  if (lastTradeEvents.length) {
@@ -6,27 +6,27 @@ export interface BookEntry {
6
6
  midpoint: string | null;
7
7
  spread: string | null;
8
8
  }
9
- export declare function sortDescendingInPlace(bookSide: PriceLevel[]): void;
10
9
  export declare class OrderBookCache {
11
10
  private bookCache;
12
11
  constructor();
13
12
  /**
14
13
  * Replace full book (after a `book` event)
14
+ * @param event new orderbook event
15
15
  */
16
16
  replaceBook(event: BookEvent): void;
17
17
  /**
18
18
  * Update a cached book from a `price_change` event.
19
19
  *
20
- * Returns true if the book was updated.
21
- * Throws if the book is not found.
20
+ * @param event PriceChangeEvent
21
+ * @returns true if the book was updated.
22
+ * @throws if the book is not found.
22
23
  */
23
24
  upsertPriceChange(event: PriceChangeEvent): void;
24
25
  /**
25
- * Return `true` if best-bid/best-ask spread exceeds `cents`.
26
- *
27
26
  * Side effect: updates the book's spread
28
27
  *
29
- * Throws if either side of the book is empty.
28
+ * @returns `true` if best-bid/best-ask spread exceeds `cents`.
29
+ * @throws if either side of the book is empty.
30
30
  */
31
31
  spreadOver(assetId: string, cents?: number): boolean;
32
32
  /**
@@ -39,11 +39,16 @@ export declare class OrderBookCache {
39
39
  * - the midpoint is NaN.
40
40
  */
41
41
  midpoint(assetId: string): string;
42
+ /**
43
+ * Removes a specific market from the orderbook if assetId is provided
44
+ * otherwise clears all orderbook
45
+ * @param assetId tokenId of a market
46
+ */
42
47
  clear(assetId?: string): void;
43
48
  /**
44
49
  * Get a book entry by asset id.
45
50
  *
46
- * Return null if the book is not found.
51
+ * @returns book entry if found, otherwise null
47
52
  */
48
53
  getBookEntry(assetId: string): BookEntry | null;
49
54
  }
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.OrderBookCache = void 0;
4
- exports.sortDescendingInPlace = sortDescendingInPlace;
5
4
  function sortDescendingInPlace(bookSide) {
6
5
  bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
7
6
  }
@@ -14,6 +13,7 @@ class OrderBookCache {
14
13
  }
15
14
  /**
16
15
  * Replace full book (after a `book` event)
16
+ * @param event new orderbook event
17
17
  */
18
18
  replaceBook(event) {
19
19
  let lastPrice = null;
@@ -43,8 +43,9 @@ class OrderBookCache {
43
43
  /**
44
44
  * Update a cached book from a `price_change` event.
45
45
  *
46
- * Returns true if the book was updated.
47
- * Throws if the book is not found.
46
+ * @param event PriceChangeEvent
47
+ * @returns true if the book was updated.
48
+ * @throws if the book is not found.
48
49
  */
49
50
  upsertPriceChange(event) {
50
51
  // Iterate through price_changes array
@@ -79,11 +80,10 @@ class OrderBookCache {
79
80
  }
80
81
  }
81
82
  /**
82
- * Return `true` if best-bid/best-ask spread exceeds `cents`.
83
- *
84
83
  * Side effect: updates the book's spread
85
84
  *
86
- * Throws if either side of the book is empty.
85
+ * @returns `true` if best-bid/best-ask spread exceeds `cents`.
86
+ * @throws if either side of the book is empty.
87
87
  */
88
88
  spreadOver(assetId, cents = 0.1) {
89
89
  const book = this.bookCache[assetId];
@@ -149,6 +149,11 @@ class OrderBookCache {
149
149
  book.midpoint = parseFloat(midpoint.toFixed(3)).toString();
150
150
  return parseFloat(midpoint.toFixed(3)).toString();
151
151
  }
152
+ /**
153
+ * Removes a specific market from the orderbook if assetId is provided
154
+ * otherwise clears all orderbook
155
+ * @param assetId tokenId of a market
156
+ */
152
157
  clear(assetId) {
153
158
  if (assetId) {
154
159
  delete this.bookCache[assetId];
@@ -162,7 +167,7 @@ class OrderBookCache {
162
167
  /**
163
168
  * Get a book entry by asset id.
164
169
  *
165
- * Return null if the book is not found.
170
+ * @returns book entry if found, otherwise null
166
171
  */
167
172
  getBookEntry(assetId) {
168
173
  if (!this.bookCache[assetId]) {
@@ -22,7 +22,7 @@ export type PriceChangeItem = {
22
22
  /**
23
23
  * Represents a price_change event from Polymarket WebSocket
24
24
  *
25
- * Schema example:
25
+ * @example
26
26
  * {
27
27
  * market: "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
28
28
  * price_changes: [
@@ -11,6 +11,7 @@ export type WebSocketGroup = {
11
11
  assetIds: Set<string>;
12
12
  wsClient: WebSocket | null;
13
13
  status: WebSocketStatus;
14
+ connecting?: boolean;
14
15
  };
15
16
  export type SubscriptionManagerOptions = {
16
17
  burstLimiter?: Bottleneck;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nevuamarkets/poly-websockets",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Plug-and-play Polymarket WebSocket price alerts",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,7 +11,9 @@
11
11
  "scripts": {
12
12
  "build": "tsc",
13
13
  "prepare": "npm run build && npm run test",
14
- "test": "vitest run"
14
+ "test": "vitest run --exclude LiveData.test.ts",
15
+ "test:live": "vitest run LiveData.test.ts",
16
+ "prepublishOnly": "npm run test:live"
15
17
  },
16
18
  "repository": {
17
19
  "type": "git",
@@ -79,15 +79,15 @@ class WSSubscriptionManager {
79
79
  }, this.reconnectAndCleanupIntervalMs);
80
80
  }
81
81
 
82
- /*
83
- Clears all WebSocket subscriptions and state.
84
-
85
- This will:
86
-
87
- 1. Remove all subscriptions and groups
88
- 2. Close all WebSocket connections
89
- 3. Clear the order book cache
90
- */
82
+ /**
83
+ * Clears all WebSocket subscriptions and state.
84
+ *
85
+ * This will:
86
+ *
87
+ * 1. Remove all subscriptions and groups
88
+ * 2. Close all WebSocket connections
89
+ * 3. Clear the order book cache
90
+ */
91
91
  public async clearState() {
92
92
  const previousGroups = await this.groupRegistry.clearAllGroups();
93
93
 
@@ -100,14 +100,17 @@ class WSSubscriptionManager {
100
100
  this.bookCache.clear();
101
101
  }
102
102
 
103
- /*
104
- This function is called when:
105
- - a websocket event is received from the Polymarket WS
106
- - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
107
- depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
108
-
109
- The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
110
- */
103
+ /**
104
+ * This function is called when:
105
+ * - a websocket event is received from the Polymarket WS
106
+ * - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
107
+ * depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
108
+ *
109
+ * The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
110
+ *
111
+ * @param events - The events to process.
112
+ * @param action - The action to perform on the filtered events.
113
+ */
111
114
  private async actOnSubscribedEvents<T extends PolymarketWSEvent | PolymarketPriceUpdateEvent>(events: T[], action?: (events: T[]) => Promise<void>) {
112
115
 
113
116
  // Filter out events that are not subscribed to by any groups
@@ -148,13 +151,15 @@ class WSSubscriptionManager {
148
151
  await action?.(events);
149
152
  }
150
153
 
151
- /*
152
- Edits wsGroups: Adds new subscriptions.
153
-
154
- - Filters out assets that are already subscribed
155
- - Finds a group with capacity or creates a new one
156
- - Creates a new WebSocket client and adds it to the group
157
- */
154
+ /**
155
+ * Edits wsGroups: Adds new subscriptions.
156
+ *
157
+ * - Filters out assets that are already subscribed
158
+ * - Finds a group with capacity or creates a new one
159
+ * - Creates a new WebSocket client and adds it to the group
160
+ *
161
+ * @param assetIdsToAdd - The asset IDs to add subscriptions for.
162
+ */
158
163
  public async addSubscriptions(assetIdsToAdd: string[]) {
159
164
  try {
160
165
  const groupIdsToConnect = await this.groupRegistry.addAssets(assetIdsToAdd, this.maxMarketsPerWS);
@@ -167,11 +172,13 @@ class WSSubscriptionManager {
167
172
  }
168
173
  }
169
174
 
170
- /*
171
- Edits wsGroups: Removes subscriptions.
172
- The group will use the updated subscriptions when it reconnects.
173
- We do that because we don't want to miss events by reconnecting.
174
- */
175
+ /**
176
+ * Edits wsGroups: Removes subscriptions.
177
+ * The group will use the updated subscriptions when it reconnects.
178
+ * We do that because we don't want to miss events by reconnecting.
179
+ *
180
+ * @param assetIdsToRemove - The asset IDs to remove subscriptions for.
181
+ */
175
182
  public async removeSubscriptions(assetIdsToRemove: string[]) {
176
183
  try {
177
184
  await this.groupRegistry.removeAssets(assetIdsToRemove, this.bookCache);
@@ -181,12 +188,12 @@ class WSSubscriptionManager {
181
188
  }
182
189
  }
183
190
 
184
- /*
185
- This function runs periodically and:
186
-
187
- - Tries to reconnect groups that have assets and are disconnected
188
- - Cleans up groups that have no assets
189
- */
191
+ /**
192
+ * This function runs periodically and:
193
+ *
194
+ * - Tries to reconnect groups that have assets and are disconnected
195
+ * - Cleans up groups that have no assets
196
+ */
190
197
  private async reconnectAndCleanupGroups() {
191
198
  try {
192
199
  const reconnectIds = await this.groupRegistry.getGroupsToReconnectAndCleanup();
@@ -43,7 +43,7 @@ export class GroupRegistry {
43
43
  /**
44
44
  * Find the first group with capacity to hold new assets.
45
45
  *
46
- * Returns the groupId if found, otherwise null.
46
+ * @returns The groupId if found, otherwise null.
47
47
  */
48
48
  public findGroupWithCapacity(newAssetLen: number, maxPerWS: number): string | null {
49
49
  for (const group of wsGroups) {
@@ -56,7 +56,8 @@ export class GroupRegistry {
56
56
  /**
57
57
  * Get the indices of all groups that contain the asset.
58
58
  *
59
- * Returns an array of indices.
59
+ * @param assetId - The tokenId of a market.
60
+ * @returns An array of indices.
60
61
  */
61
62
  public getGroupIndicesForAsset(assetId: string): number[] {
62
63
  const indices: number[] = [];
@@ -68,6 +69,9 @@ export class GroupRegistry {
68
69
 
69
70
  /**
70
71
  * Check if any group contains the asset.
72
+ *
73
+ * @param assetId - The tokenId of a market.
74
+ * @returns True if found.
71
75
  */
72
76
  public hasAsset(assetId: string): boolean {
73
77
  return wsGroups.some(group => group.assetIds.has(assetId));
@@ -76,7 +80,8 @@ export class GroupRegistry {
76
80
  /**
77
81
  * Find the group by groupId.
78
82
  *
79
- * Returns the group if found, otherwise undefined.
83
+ * @param groupId - The group UUID.
84
+ * @returns The group if found, otherwise undefined.
80
85
  */
81
86
  public findGroupById(groupId: string): WebSocketGroup | undefined {
82
87
  return wsGroups.find(g => g.groupId === groupId);
@@ -119,7 +124,7 @@ export class GroupRegistry {
119
124
  * caller can perform any asynchronous cleanup (closing sockets, etc.)
120
125
  * outside the lock.
121
126
  *
122
- * Returns the removed groups.
127
+ * @returns The removed groups.
123
128
  */
124
129
  public async clearAllGroups(): Promise<WebSocketGroup[]> {
125
130
  let removed: WebSocketGroup[] = [];
@@ -212,10 +217,12 @@ export class GroupRegistry {
212
217
  /**
213
218
  * Remove asset subscriptions from every group that contains the asset.
214
219
  *
215
- * It should be only one group that contains the asset, we search all of them
220
+ * It should be only one group that contains the asset, but we search all of them
216
221
  * regardless.
217
222
  *
218
- * Returns the list of assetIds that were removed.
223
+ * @param assetIds - The tokenIds of the markets to remove.
224
+ * @param bookCache - The stored orderbook.
225
+ * @returns The list of assetIds that were removed.
219
226
  */
220
227
  public async removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]> {
221
228
  const removedAssetIds: string[] = [];
@@ -241,10 +248,13 @@ export class GroupRegistry {
241
248
 
242
249
  /**
243
250
  * Disconnect a group.
251
+ *
252
+ * @param group - The group to disconnect.
244
253
  */
245
254
  public disconnectGroup(group: WebSocketGroup) {
246
255
  group.wsClient?.close();
247
256
  group.wsClient = null;
257
+ group.connecting = false;
248
258
 
249
259
  logger.info({
250
260
  message: 'Disconnected group',
@@ -260,7 +270,7 @@ export class GroupRegistry {
260
270
  * – Dead (but non-empty) groups are reset so that caller can reconnect them.
261
271
  * – Pending groups are returned so that caller can connect them.
262
272
  *
263
- * Returns an array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
273
+ * @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
264
274
  */
265
275
  public async getGroupsToReconnectAndCleanup(): Promise<string[]> {
266
276
  const reconnectIds: string[] = [];
@@ -287,8 +297,10 @@ export class GroupRegistry {
287
297
  group.assetIds = new Set();
288
298
  continue;
289
299
  }
290
-
291
300
  if (group.status === WebSocketStatus.PENDING) {
301
+ if (group.connecting) {
302
+ continue;
303
+ }
292
304
  reconnectIds.push(group.groupId);
293
305
  }
294
306
  }
@@ -304,4 +316,4 @@ export class GroupRegistry {
304
316
  });
305
317
  return reconnectIds;
306
318
  }
307
- }
319
+ }
@@ -37,11 +37,16 @@ export class GroupSocket {
37
37
  *
38
38
  */
39
39
  public async connect(): Promise<void> {
40
+ if (this.group.connecting) {
41
+ return;
42
+ }
40
43
  if (this.group.assetIds.size === 0) {
41
44
  this.group.status = WebSocketStatus.CLEANUP;
42
45
  return;
43
46
  }
44
47
 
48
+ this.group.connecting = true;
49
+
45
50
  try {
46
51
  logger.info({
47
52
  message: 'Connecting to CLOB WebSocket',
@@ -66,11 +71,23 @@ export class GroupSocket {
66
71
  } catch (err) {
67
72
  this.group.status = WebSocketStatus.DEAD;
68
73
  throw err; // caller responsible for error handler
74
+ } finally {
75
+ this.group.connecting = false;
69
76
  }
70
77
 
71
78
  this.setupEventHandlers();
72
79
  }
73
80
 
81
+ /**
82
+ * Sets up event handlers for the WebSocket connection.
83
+ *
84
+ * Handles:
85
+ * - 'open': Authenticates and starts ping interval
86
+ * - 'message': Parses and routes events
87
+ * - 'pong': Handles pong responses
88
+ * - 'error': Handles errors
89
+ * - 'close': Handles connection closure
90
+ */
74
91
  private setupEventHandlers() {
75
92
  const group = this.group;
76
93
  const handlers = this.handlers;
@@ -249,6 +266,11 @@ export class GroupSocket {
249
266
  }
250
267
  }
251
268
 
269
+ /**
270
+ * Handles book events by updating the cache and notifying listeners.
271
+ *
272
+ * @param bookEvents - The book events to process.
273
+ */
252
274
  private async handleBookEvents(bookEvents: BookEvent[]): Promise<void> {
253
275
  if (bookEvents.length) {
254
276
  for (const event of bookEvents) {
@@ -258,12 +280,26 @@ export class GroupSocket {
258
280
  }
259
281
  }
260
282
 
283
+ /**
284
+ * Handles tick size change events by notifying listeners.
285
+ *
286
+ * @param tickEvents - The tick size change events to process.
287
+ */
261
288
  private async handleTickEvents(tickEvents: TickSizeChangeEvent[]): Promise<void> {
262
289
  if (tickEvents.length) {
263
290
  await this.handlers.onTickSizeChange?.(tickEvents);
264
291
  }
265
292
  }
266
293
 
294
+ /**
295
+ * Handles price change events.
296
+ *
297
+ * - Updates the order book cache
298
+ * - Calculates derived price updates based on spread and midpoint
299
+ * - Notifies listeners of price changes and derived updates
300
+ *
301
+ * @param priceChangeEvents - The price change events to process.
302
+ */
267
303
  private async handlePriceChangeEvents(priceChangeEvents: PriceChangeEvent[]): Promise<void> {
268
304
  if (priceChangeEvents.length) {
269
305
  await this.handlers.onPriceChange?.(priceChangeEvents);
@@ -341,6 +377,14 @@ export class GroupSocket {
341
377
  }
342
378
  }
343
379
 
380
+ /**
381
+ * Handles last trade price events.
382
+ *
383
+ * - Notifies listeners of last trade price
384
+ * - Calculates derived price updates based on spread
385
+ *
386
+ * @param lastTradeEvents - The last trade price events to process.
387
+ */
344
388
  private async handleLastTradeEvents(lastTradeEvents: LastTradePriceEvent[]): Promise<void> {
345
389
  if (lastTradeEvents.length) {
346
390
  /*
@@ -396,4 +440,4 @@ export class GroupSocket {
396
440
  }
397
441
  }
398
442
  }
399
- }
443
+ }
@@ -16,8 +16,7 @@ export interface BookEntry {
16
16
  midpoint: string | null;
17
17
  spread: string | null;
18
18
  }
19
-
20
- export
19
+
21
20
 
22
21
  function sortDescendingInPlace(bookSide: PriceLevel[]): void {
23
22
  bookSide.sort((a, b) => parseFloat(b.price) - parseFloat(a.price));
@@ -36,6 +35,7 @@ export class OrderBookCache {
36
35
 
37
36
  /**
38
37
  * Replace full book (after a `book` event)
38
+ * @param event new orderbook event
39
39
  */
40
40
  public replaceBook(event: BookEvent): void {
41
41
  let lastPrice = null;
@@ -68,8 +68,9 @@ export class OrderBookCache {
68
68
  /**
69
69
  * Update a cached book from a `price_change` event.
70
70
  *
71
- * Returns true if the book was updated.
72
- * Throws if the book is not found.
71
+ * @param event PriceChangeEvent
72
+ * @returns true if the book was updated.
73
+ * @throws if the book is not found.
73
74
  */
74
75
  public upsertPriceChange(event: PriceChangeEvent): void {
75
76
  // Iterate through price_changes array
@@ -105,11 +106,10 @@ export class OrderBookCache {
105
106
  }
106
107
 
107
108
  /**
108
- * Return `true` if best-bid/best-ask spread exceeds `cents`.
109
- *
110
109
  * Side effect: updates the book's spread
111
110
  *
112
- * Throws if either side of the book is empty.
111
+ * @returns `true` if best-bid/best-ask spread exceeds `cents`.
112
+ * @throws if either side of the book is empty.
113
113
  */
114
114
  public spreadOver(assetId: string, cents = 0.1): boolean {
115
115
  const book = this.bookCache[assetId];
@@ -183,7 +183,11 @@ export class OrderBookCache {
183
183
 
184
184
  return parseFloat(midpoint.toFixed(3)).toString();
185
185
  }
186
-
186
+ /**
187
+ * Removes a specific market from the orderbook if assetId is provided
188
+ * otherwise clears all orderbook
189
+ * @param assetId tokenId of a market
190
+ */
187
191
  public clear(assetId?: string): void {
188
192
  if (assetId) {
189
193
  delete this.bookCache[assetId];
@@ -197,7 +201,7 @@ export class OrderBookCache {
197
201
  /**
198
202
  * Get a book entry by asset id.
199
203
  *
200
- * Return null if the book is not found.
204
+ * @returns book entry if found, otherwise null
201
205
  */
202
206
  public getBookEntry(assetId: string): BookEntry | null {
203
207
  if (!this.bookCache[assetId]) {
@@ -24,7 +24,7 @@ export type PriceChangeItem = {
24
24
  /**
25
25
  * Represents a price_change event from Polymarket WebSocket
26
26
  *
27
- * Schema example:
27
+ * @example
28
28
  * {
29
29
  * market: "0x5f65177b394277fd294cd75650044e32ba009a95022d88a0c1d565897d72f8f1",
30
30
  * price_changes: [
@@ -13,6 +13,7 @@ export type WebSocketGroup = {
13
13
  assetIds: Set<string>;
14
14
  wsClient: WebSocket | null;
15
15
  status: WebSocketStatus;
16
+ connecting?: boolean;
16
17
  };
17
18
 
18
19
  export type SubscriptionManagerOptions = {
@@ -23,4 +24,4 @@ export type SubscriptionManagerOptions = {
23
24
 
24
25
  // How many assets to allow per WebSocket
25
26
  maxMarketsPerWS?: number;
26
- }
27
+ }