@nevuamarkets/poly-websockets 0.3.0 β†’ 1.0.0-beta.0

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/README.md CHANGED
@@ -14,10 +14,11 @@ npm install @nevuamarkets/poly-websockets
14
14
 
15
15
  - πŸ“Š **Real-time Market Updates**: Get `book` , `price_change`, `tick_size_change` and `last_trade_price` real-time market events from Polymarket WSS
16
16
  - 🎯 **Derived Future Price Event**: Implements Polymarket's [price calculation logic](https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated#future-price) (midpoint vs last trade price based on spread)
17
- - πŸ”— **Group Management**: Efficiently manages multiple asset subscriptions across connection groups **without losing events** when subscribing / unsubscribing assets.
18
- - πŸ”„ **Automatic Connection Management**: Handles WebSocket connections, reconnections, and cleanup for grouped assetId (i.e. clobTokenId) subscriptions
17
+ - πŸ”— **Dynamic Subscriptions**: Subscribe and unsubscribe to assets on existing WebSocket connections without reconnecting
18
+ - πŸ”„ **Automatic Connection Management**: Handles WebSocket connections, reconnections, and cleanup
19
19
  - 🚦 **Rate Limiting**: Built-in rate limiting to respect Polymarket API limits
20
20
  - πŸ’ͺ **TypeScript Support**: Full TypeScript definitions for all events and handlers
21
+ - ♾️ **Unlimited Assets**: By default, all subscriptions go through a single WebSocket connection (Polymarket now supports unlimited assets per connection)
21
22
 
22
23
  ## Quick Start
23
24
 
@@ -66,7 +67,7 @@ new WSSubscriptionManager(handlers: WebSocketHandlers, options?: SubscriptionMan
66
67
  **Parameters:**
67
68
  - `handlers` - Event handlers for different WebSocket events
68
69
  - `options` - Optional configuration object:
69
- - `maxMarketsPerWS?: number` - Maximum assets per WebSocket connection (default: 100)
70
+ - `maxMarketsPerWS?: number` - Maximum assets per WebSocket connection (default: `Infinity` - unlimited, single connection for all assets)
70
71
  - `reconnectAndCleanupIntervalMs?: number` - Interval for reconnection attempts (default: 10s)
71
72
  - `burstLimiter?: Bottleneck` - Custom rate limiter instance. If none is provided, one will be created and used internally in the component.
72
73
 
@@ -76,12 +77,15 @@ new WSSubscriptionManager(handlers: WebSocketHandlers, options?: SubscriptionMan
76
77
 
77
78
  Adds new asset subscriptions. The manager will:
78
79
  - Filter out already subscribed assets
79
- - Find available connection groups or create new ones
80
- - Establish WebSocket connections as needed
80
+ - If an active WebSocket connection exists with capacity, send a subscribe message on that connection
81
+ - Otherwise, create a new WebSocket connection
81
82
 
82
83
  ##### `removeSubscriptions(assetIds: string[]): Promise<void>`
83
84
 
84
- Removes asset subscriptions. **Connections are kept alive to avoid missing events**, and unused groups are cleaned up during the next reconnection cycle.
85
+ Removes asset subscriptions. The manager will:
86
+ - Send an unsubscribe message on the active connection
87
+ - Clear the internal order book cache for removed assets
88
+ - Connections are kept alive to avoid missing events for other subscribed assets
85
89
 
86
90
  ##### `clearState(): Promise<void>`
87
91
 
@@ -7,11 +7,54 @@ declare class WSSubscriptionManager {
7
7
  private bookCache;
8
8
  private reconnectAndCleanupIntervalMs;
9
9
  private maxMarketsPerWS;
10
+ private reconnectInterval?;
10
11
  constructor(userHandlers: WebSocketHandlers, options?: SubscriptionManagerOptions);
12
+ /**
13
+ * Clears all WebSocket subscriptions and state.
14
+ *
15
+ * This will:
16
+ *
17
+ * 1. Stop the reconnection interval
18
+ * 2. Remove all subscriptions and groups (including GroupSockets)
19
+ * 3. Close all WebSocket connections
20
+ * 4. Clear the order book cache
21
+ */
11
22
  clearState(): Promise<void>;
23
+ /**
24
+ * This function is called when:
25
+ * - a websocket event is received from the Polymarket WS
26
+ * - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
27
+ * depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
28
+ *
29
+ * The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
30
+ *
31
+ * @param events - The events to process.
32
+ * @param action - The action to perform on the filtered events.
33
+ */
12
34
  private actOnSubscribedEvents;
35
+ /**
36
+ * Adds new subscriptions.
37
+ *
38
+ * - Filters out assets that are already subscribed
39
+ * - If an ALIVE group exists with capacity, sends subscribe message on that connection
40
+ * - Otherwise creates a new group and WebSocket connection
41
+ *
42
+ * @param assetIdsToAdd - The asset IDs to add subscriptions for.
43
+ */
13
44
  addSubscriptions(assetIdsToAdd: string[]): Promise<void>;
45
+ /**
46
+ * Removes subscriptions.
47
+ * Sends unsubscribe messages to ALIVE connections.
48
+ *
49
+ * @param assetIdsToRemove - The asset IDs to remove subscriptions for.
50
+ */
14
51
  removeSubscriptions(assetIdsToRemove: string[]): Promise<void>;
52
+ /**
53
+ * This function runs periodically and:
54
+ *
55
+ * - Tries to reconnect groups that have assets and are disconnected
56
+ * - Cleans up groups that have no assets
57
+ */
15
58
  private reconnectAndCleanupGroups;
16
59
  getStatistics(): {
17
60
  openWebSockets: number;
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.WSSubscriptionManager = void 0;
7
7
  const ms_1 = __importDefault(require("ms"));
8
- const lodash_1 = __importDefault(require("lodash"));
9
8
  const bottleneck_1 = __importDefault(require("bottleneck"));
10
9
  const PolymarketWebSocket_1 = require("./types/PolymarketWebSocket");
11
10
  const GroupRegistry_1 = require("./modules/GroupRegistry");
@@ -16,7 +15,9 @@ const logger_1 = require("./logger");
16
15
  // See https://docs.polymarket.com/quickstart/introduction/rate-limits#api-rate-limits
17
16
  const BURST_LIMIT_PER_SECOND = 5;
18
17
  const DEFAULT_RECONNECT_AND_CLEANUP_INTERVAL_MS = (0, ms_1.default)('10s');
19
- const DEFAULT_MAX_MARKETS_PER_WS = 100;
18
+ // Default to Infinity - no limit on assets per WebSocket (single connection)
19
+ // Polymarket now supports unlimited asset subscriptions per connection
20
+ const DEFAULT_MAX_MARKETS_PER_WS = Infinity;
20
21
  class WSSubscriptionManager {
21
22
  constructor(userHandlers, options) {
22
23
  this.groupRegistry = new GroupRegistry_1.GroupRegistry();
@@ -54,20 +55,26 @@ class WSSubscriptionManager {
54
55
  (_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, err);
55
56
  });
56
57
  // Check for dead groups every 10s and reconnect them if needed
57
- setInterval(() => {
58
+ this.reconnectInterval = setInterval(() => {
58
59
  this.reconnectAndCleanupGroups();
59
60
  }, this.reconnectAndCleanupIntervalMs);
60
61
  }
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
- */
62
+ /**
63
+ * Clears all WebSocket subscriptions and state.
64
+ *
65
+ * This will:
66
+ *
67
+ * 1. Stop the reconnection interval
68
+ * 2. Remove all subscriptions and groups (including GroupSockets)
69
+ * 3. Close all WebSocket connections
70
+ * 4. Clear the order book cache
71
+ */
70
72
  async clearState() {
73
+ // Stop the reconnection interval to prevent zombie behavior
74
+ if (this.reconnectInterval) {
75
+ clearInterval(this.reconnectInterval);
76
+ this.reconnectInterval = undefined;
77
+ }
71
78
  const previousGroups = await this.groupRegistry.clearAllGroups();
72
79
  // Close sockets outside the lock
73
80
  for (const group of previousGroups) {
@@ -76,17 +83,20 @@ class WSSubscriptionManager {
76
83
  // Also clear the order book cache
77
84
  this.bookCache.clear();
78
85
  }
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
- */
86
+ /**
87
+ * This function is called when:
88
+ * - a websocket event is received from the Polymarket WS
89
+ * - a price update event detected, either by after a 'last_trade_price' event or a 'price_change' event
90
+ * depending on the current bid-ask spread (see https://docs.polymarket.com/polymarket-learn/trading/how-are-prices-calculated)
91
+ *
92
+ * The user handlers will be called **ONLY** for assets that are actively subscribed to by any groups.
93
+ *
94
+ * @param events - The events to process.
95
+ * @param action - The action to perform on the filtered events.
96
+ */
87
97
  async actOnSubscribedEvents(events, action) {
88
98
  // Filter out events that are not subscribed to by any groups
89
- events = lodash_1.default.filter(events, (event) => {
99
+ events = events.filter((event) => {
90
100
  // Handle PriceChangeEvent which doesn't have asset_id at root
91
101
  if ((0, PolymarketWebSocket_1.isPriceChangeEvent)(event)) {
92
102
  // Check if any of the price_changes are subscribed
@@ -118,17 +128,20 @@ class WSSubscriptionManager {
118
128
  });
119
129
  await (action === null || action === void 0 ? void 0 : action(events));
120
130
  }
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
- */
131
+ /**
132
+ * Adds new subscriptions.
133
+ *
134
+ * - Filters out assets that are already subscribed
135
+ * - If an ALIVE group exists with capacity, sends subscribe message on that connection
136
+ * - Otherwise creates a new group and WebSocket connection
137
+ *
138
+ * @param assetIdsToAdd - The asset IDs to add subscriptions for.
139
+ */
128
140
  async addSubscriptions(assetIdsToAdd) {
129
141
  var _a, _b;
130
142
  try {
131
143
  const groupIdsToConnect = await this.groupRegistry.addAssets(assetIdsToAdd, this.maxMarketsPerWS);
144
+ // Create new connections for any new groups
132
145
  for (const groupId of groupIdsToConnect) {
133
146
  await this.createWebSocketClient(groupId, this.handlers);
134
147
  }
@@ -138,11 +151,12 @@ class WSSubscriptionManager {
138
151
  await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(msg)));
139
152
  }
140
153
  }
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
- */
154
+ /**
155
+ * Removes subscriptions.
156
+ * Sends unsubscribe messages to ALIVE connections.
157
+ *
158
+ * @param assetIdsToRemove - The asset IDs to remove subscriptions for.
159
+ */
146
160
  async removeSubscriptions(assetIdsToRemove) {
147
161
  var _a, _b;
148
162
  try {
@@ -153,12 +167,12 @@ class WSSubscriptionManager {
153
167
  await ((_b = (_a = this.handlers).onError) === null || _b === void 0 ? void 0 : _b.call(_a, new Error(errMsg)));
154
168
  }
155
169
  }
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
- */
170
+ /**
171
+ * This function runs periodically and:
172
+ *
173
+ * - Tries to reconnect groups that have assets and are disconnected
174
+ * - Cleans up groups that have no assets
175
+ */
162
176
  async reconnectAndCleanupGroups() {
163
177
  var _a, _b;
164
178
  try {
@@ -192,6 +206,8 @@ class WSSubscriptionManager {
192
206
  return;
193
207
  }
194
208
  const groupSocket = new GroupSocket_1.GroupSocket(group, this.burstLimiter, this.bookCache, handlers);
209
+ // Store the groupSocket in the registry for later subscribe/unsubscribe operations
210
+ this.groupRegistry.setGroupSocket(groupId, groupSocket);
195
211
  try {
196
212
  await groupSocket.connect();
197
213
  }
@@ -1,6 +1,46 @@
1
1
  import { WebSocketGroup } from '../types/WebSocketSubscriptions';
2
2
  import { OrderBookCache } from './OrderBookCache';
3
+ import { GroupSocket } from './GroupSocket';
4
+ /**
5
+ * WebSocketStatus State Transitions:
6
+ *
7
+ * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
8
+ * β”‚ PENDING β”‚ ──── connect() succeeds ────► ALIVE
9
+ * β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜ β”‚
10
+ * β”‚ β”‚
11
+ * assets removed error/close
12
+ * before connect β”‚
13
+ * β”‚ β–Ό
14
+ * β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
15
+ * └────────────────────────────► β”‚ DEAD β”‚
16
+ * β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜
17
+ * β”‚
18
+ * reconnect attempt
19
+ * β”‚
20
+ * β–Ό
21
+ * β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
22
+ * β”‚ PENDING β”‚
23
+ * β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
24
+ *
25
+ * Any state ──── all assets removed ────► CLEANUP ────► removed from registry
26
+ *
27
+ * Valid transitions:
28
+ * PENDING -> ALIVE (on successful connection)
29
+ * PENDING -> CLEANUP (if all assets removed before connect)
30
+ * ALIVE -> DEAD (on error or connection close)
31
+ * ALIVE -> CLEANUP (if all assets removed while connected)
32
+ * DEAD -> PENDING (on reconnection attempt - status reset)
33
+ * DEAD -> CLEANUP (if all assets removed while dead)
34
+ * CLEANUP -> removed (group removed from registry)
35
+ *
36
+ * Note: If a subscribe/unsubscribe message fails to send (e.g., WebSocket closing),
37
+ * there may be a brief window where local state and server state diverge and events
38
+ * are missed. This self-heals on reconnection when the full asset list is re-sent.
39
+ */
3
40
  export declare class GroupRegistry {
41
+ private wsGroups;
42
+ private wsGroupsMutex;
43
+ private groupSockets;
4
44
  /**
5
45
  * Atomic mutate helper.
6
46
  *
@@ -14,31 +54,41 @@ export declare class GroupRegistry {
14
54
  * Only to be used in test suite.
15
55
  */
16
56
  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
57
  /**
24
58
  * Get the indices of all groups that contain the asset.
25
59
  *
26
- * Returns an array of indices.
60
+ * Note: This read operation does not acquire the mutex for performance reasons.
61
+ * It provides eventual consistency - the result may be slightly stale if a
62
+ * concurrent mutation is in progress. This is acceptable given the self-healing
63
+ * nature of the system (see note at top of file).
64
+ *
65
+ * @param assetId - The tokenId of a market.
66
+ * @returns An array of indices.
27
67
  */
28
68
  getGroupIndicesForAsset(assetId: string): number[];
29
69
  /**
30
70
  * Check if any group contains the asset.
71
+ *
72
+ * Note: This read operation provides eventual consistency (see getGroupIndicesForAsset).
73
+ *
74
+ * @param assetId - The tokenId of a market.
75
+ * @returns True if found.
31
76
  */
32
77
  hasAsset(assetId: string): boolean;
33
78
  /**
34
79
  * Find the group by groupId.
35
80
  *
36
- * Returns the group if found, otherwise undefined.
81
+ * Note: This read operation provides eventual consistency (see getGroupIndicesForAsset).
82
+ *
83
+ * @param groupId - The group UUID.
84
+ * @returns The group if found, otherwise undefined.
37
85
  */
38
86
  findGroupById(groupId: string): WebSocketGroup | undefined;
39
87
  /**
40
88
  * Get statistics about the current state of the registry.
41
89
  *
90
+ * Note: This read operation provides eventual consistency (see getGroupIndicesForAsset).
91
+ *
42
92
  * Returns an object with:
43
93
  * - openWebSockets: The number of websockets that are currently in OPEN state
44
94
  * - subscribedAssetIds: The number of unique asset IDs that are currently subscribed
@@ -47,40 +97,71 @@ export declare class GroupRegistry {
47
97
  openWebSockets: number;
48
98
  subscribedAssetIds: number;
49
99
  };
100
+ /**
101
+ * Set the GroupSocket for a group.
102
+ * Disposes the old socket if one exists to prevent interval leaks.
103
+ *
104
+ * @param groupId - The group UUID.
105
+ * @param socket - The GroupSocket instance.
106
+ */
107
+ setGroupSocket(groupId: string, socket: GroupSocket): void;
108
+ /**
109
+ * Get the GroupSocket for a group.
110
+ *
111
+ * @param groupId - The group UUID.
112
+ * @returns The GroupSocket if found, otherwise undefined.
113
+ */
114
+ getGroupSocket(groupId: string): GroupSocket | undefined;
115
+ /**
116
+ * Remove the GroupSocket for a group.
117
+ * Disposes the socket to prevent interval leaks.
118
+ *
119
+ * @param groupId - The group UUID.
120
+ */
121
+ removeGroupSocket(groupId: string): void;
122
+ /**
123
+ * Clear all GroupSockets.
124
+ * Disposes all sockets to prevent interval leaks.
125
+ */
126
+ clearAllGroupSockets(): void;
50
127
  /**
51
128
  * Atomically remove **all** groups from the registry and return them so the
52
129
  * caller can perform any asynchronous cleanup (closing sockets, etc.)
53
- * outside the lock.
130
+ * outside the lock. Also clears all GroupSockets.
54
131
  *
55
- * Returns the removed groups.
132
+ * @returns The removed groups.
56
133
  */
57
134
  clearAllGroups(): Promise<WebSocketGroup[]>;
58
135
  /**
59
136
  * Add new asset subscriptions.
60
137
  *
61
138
  * – Ignores assets that are already subscribed.
62
- * – Either reuses an existing group with capacity or creates new groups (size ≀ maxPerWS).
63
- * – If appending to a group:
64
- * - A new group is created with the updated assetIds.
65
- * - The old group is marked for cleanup.
66
- * - The group is added to the list of groups to connect.
139
+ * – If there's an existing non-CLEANUP group with capacity, adds assets to it.
140
+ * - ALIVE groups: sends subscribe message immediately
141
+ * - PENDING/DEAD groups: assets included when connection is (re)established
142
+ * – Otherwise creates new groups (size ≀ maxPerWS).
67
143
  *
68
144
  * @param assetIds - The assetIds to add.
69
145
  * @param maxPerWS - The maximum number of assets per WebSocket group.
70
- * @returns An array of *new* groupIds that need websocket connections.
146
+ * @returns An array of groupIds that need new websocket connections.
71
147
  */
72
148
  addAssets(assetIds: string[], maxPerWS: number): Promise<string[]>;
73
149
  /**
74
150
  * Remove asset subscriptions from every group that contains the asset.
151
+ * Sends unsubscribe messages on ALIVE connections.
75
152
  *
76
- * It should be only one group that contains the asset, we search all of them
153
+ * It should be only one group that contains the asset, but we search all of them
77
154
  * regardless.
78
155
  *
79
- * Returns the list of assetIds that were removed.
156
+ * @param assetIds - The tokenIds of the markets to remove.
157
+ * @param bookCache - The stored orderbook.
158
+ * @returns The list of assetIds that were removed (deduplicated).
80
159
  */
81
160
  removeAssets(assetIds: string[], bookCache: OrderBookCache): Promise<string[]>;
82
161
  /**
83
- * Disconnect a group.
162
+ * Disconnect a group and reset its status for reconnection.
163
+ *
164
+ * @param group - The group to disconnect.
84
165
  */
85
166
  disconnectGroup(group: WebSocketGroup): void;
86
167
  /**
@@ -90,7 +171,7 @@ export declare class GroupRegistry {
90
171
  * – Dead (but non-empty) groups are reset so that caller can reconnect them.
91
172
  * – Pending groups are returned so that caller can connect them.
92
173
  *
93
- * Returns an array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
174
+ * @returns An array of group IDs that need to be reconnected, after cleaning up empty and cleanup-marked groups.
94
175
  */
95
176
  getGroupsToReconnectAndCleanup(): Promise<string[]>;
96
177
  }