pmxtjs 2.46.3 → 2.46.5

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.
@@ -57,6 +57,7 @@ export interface ExchangeOptions {
57
57
  * prediction market platforms (Polymarket, Kalshi, etc.).
58
58
  */
59
59
  export declare abstract class Exchange {
60
+ private static readonly OBDATA_WATCH_ALL_SOURCES;
60
61
  protected exchangeName: string;
61
62
  protected apiKey?: string;
62
63
  protected privateKey?: string;
@@ -116,6 +117,7 @@ export declare abstract class Exchange {
116
117
  private watchViaWs;
117
118
  private wsTransportUnavailableError;
118
119
  private isWsTransportUnavailableError;
120
+ private defaultWatchAllOrderBookVenues;
119
121
  private getWsInternals;
120
122
  private wsSubscriptionKey;
121
123
  private getWsSubscriptionId;
@@ -282,7 +284,8 @@ export declare abstract class Exchange {
282
284
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
283
285
  * Requires hosted mode (`pmxtApiKey` set).
284
286
  *
285
- * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
287
+ * @param venues - Optional venue filter. Defaults to this exchange's venue
288
+ * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues.
286
289
  * @returns Next event with source, symbol, and orderbook
287
290
  *
288
291
  * @example
@@ -126,6 +126,12 @@ function convertSubscriptionSnapshot(raw) {
126
126
  * prediction market platforms (Polymarket, Kalshi, etc.).
127
127
  */
128
128
  export class Exchange {
129
+ static OBDATA_WATCH_ALL_SOURCES = new Set([
130
+ "polymarket",
131
+ "limitless",
132
+ "kalshi",
133
+ "opinion",
134
+ ]);
129
135
  exchangeName;
130
136
  apiKey;
131
137
  privateKey;
@@ -358,6 +364,12 @@ export class Exchange {
358
364
  return error instanceof PmxtError
359
365
  && /connection failed|no websocket|websocket.*not connected/i.test(error.message);
360
366
  }
367
+ defaultWatchAllOrderBookVenues() {
368
+ if (Exchange.OBDATA_WATCH_ALL_SOURCES.has(this.exchangeName)) {
369
+ return [this.exchangeName];
370
+ }
371
+ return undefined;
372
+ }
361
373
  getWsInternals(ws) {
362
374
  return ws;
363
375
  }
@@ -384,6 +396,7 @@ export class Exchange {
384
396
  }
385
397
  internals.activeSubs.delete(subKey);
386
398
  internals.subscriptions.delete(requestId);
399
+ internals.dataQueues.delete(requestId);
387
400
  internals.dataStore.delete(requestId);
388
401
  const firstArg = args[0] ?? "";
389
402
  const symbols = Array.isArray(firstArg)
@@ -1563,7 +1576,8 @@ export class Exchange {
1563
1576
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1564
1577
  * Requires hosted mode (`pmxtApiKey` set).
1565
1578
  *
1566
- * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1579
+ * @param venues - Optional venue filter. Defaults to this exchange's venue
1580
+ * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues.
1567
1581
  * @returns Next event with source, symbol, and orderbook
1568
1582
  *
1569
1583
  * @example
@@ -1580,7 +1594,8 @@ export class Exchange {
1580
1594
  if (!this.isHosted) {
1581
1595
  throw new PmxtError("watchAllOrderBooks() requires hosted mode (set pmxtApiKey)");
1582
1596
  }
1583
- const args = venues ? [venues] : [];
1597
+ const effectiveVenues = venues ?? this.defaultWatchAllOrderBookVenues();
1598
+ const args = effectiveVenues?.length ? [effectiveVenues] : [];
1584
1599
  const wsData = await this.watchViaWs("watchAllOrderBooks", args);
1585
1600
  if (wsData !== null) {
1586
1601
  return {
@@ -18,7 +18,9 @@ export declare class SidecarWsClient {
18
18
  private accessToken;
19
19
  private authParamName;
20
20
  private closed;
21
- /** requestId -> latest data payload */
21
+ /** requestId -> queued data payloads for single-event watch methods */
22
+ private dataQueues;
23
+ /** requestId[:symbol] -> latest data payload for batch snapshots */
22
24
  private dataStore;
23
25
  /** requestId -> subscription metadata */
24
26
  private subscriptions;
@@ -30,6 +32,7 @@ export declare class SidecarWsClient {
30
32
  private connect;
31
33
  private getWebSocketConstructor;
32
34
  private dispatch;
35
+ private deliverOrQueue;
33
36
  subscribe(exchange: string, method: string, args: any[], credentials?: Record<string, any>, timeoutMs?: number): Promise<any>;
34
37
  subscribeBatch(exchange: string, method: string, args: any[], credentials?: Record<string, any>, timeoutMs?: number): Promise<Record<string, any>>;
35
38
  close(): void;
@@ -7,6 +7,7 @@
7
7
  * does not support the /ws endpoint.
8
8
  */
9
9
  import { PmxtError } from "./errors.js";
10
+ const MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION = 100_000;
10
11
  /**
11
12
  * Multiplexed WebSocket client for the pmxt sidecar.
12
13
  *
@@ -19,7 +20,9 @@ export class SidecarWsClient {
19
20
  accessToken;
20
21
  authParamName;
21
22
  closed = false;
22
- /** requestId -> latest data payload */
23
+ /** requestId -> queued data payloads for single-event watch methods */
24
+ dataQueues = new Map();
25
+ /** requestId[:symbol] -> latest data payload for batch snapshots */
23
26
  dataStore = new Map();
24
27
  /** requestId -> subscription metadata */
25
28
  subscriptions = new Map();
@@ -154,16 +157,27 @@ export class SidecarWsClient {
154
157
  if (eventType === "data" && requestId) {
155
158
  const symbol = msg.symbol || "";
156
159
  const data = msg.data || {};
157
- // Store by (requestId:symbol) for batch methods
160
+ // Store by (requestId:symbol) for batch methods.
158
161
  this.dataStore.set(`${requestId}:${symbol}`, data);
159
- // Store by requestId alone for single-symbol methods
160
- this.dataStore.set(requestId, data);
161
- const sub = this.subscriptions.get(requestId);
162
- if (sub?.resolve) {
163
- sub.resolve(data);
164
- sub.resolve = null;
165
- sub.reject = null;
166
- }
162
+ this.deliverOrQueue(requestId, data);
163
+ }
164
+ }
165
+ deliverOrQueue(requestId, data) {
166
+ const sub = this.subscriptions.get(requestId);
167
+ if (sub?.resolve) {
168
+ sub.resolve(data);
169
+ sub.resolve = null;
170
+ sub.reject = null;
171
+ return;
172
+ }
173
+ let queue = this.dataQueues.get(requestId);
174
+ if (!queue) {
175
+ queue = [];
176
+ this.dataQueues.set(requestId, queue);
177
+ }
178
+ queue.push(data);
179
+ if (queue.length > MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION) {
180
+ queue.splice(0, queue.length - MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION);
167
181
  }
168
182
  }
169
183
  // ------------------------------------------------------------------
@@ -233,8 +247,8 @@ export class SidecarWsClient {
233
247
  throw new PmxtError('[ws-client] Cannot send: WebSocket not connected');
234
248
  }
235
249
  this.ws.send(JSON.stringify(message));
236
- // Wait for first data event
237
- await this.waitForData(requestId, timeoutMs);
250
+ // Wait for first data event.
251
+ const firstData = await this.waitForData(requestId, timeoutMs);
238
252
  // Collect per-symbol data
239
253
  const result = {};
240
254
  for (const symbol of symbols) {
@@ -246,9 +260,8 @@ export class SidecarWsClient {
246
260
  }
247
261
  // If no per-symbol data, return the single data event as-is
248
262
  if (Object.keys(result).length === 0) {
249
- const data = this.dataStore.get(requestId);
250
- if (data && typeof data === "object") {
251
- return data;
263
+ if (firstData && typeof firstData === "object") {
264
+ return firstData;
252
265
  }
253
266
  }
254
267
  return result;
@@ -264,6 +277,8 @@ export class SidecarWsClient {
264
277
  }
265
278
  this.ws = null;
266
279
  }
280
+ this.dataQueues.clear();
281
+ this.dataStore.clear();
267
282
  }
268
283
  get connected() {
269
284
  return this.ws !== null && !this.closed;
@@ -272,11 +287,14 @@ export class SidecarWsClient {
272
287
  // Internal
273
288
  // ------------------------------------------------------------------
274
289
  waitForData(requestId, timeoutMs) {
275
- // Check if data is already available
276
- const existing = this.dataStore.get(requestId);
277
- if (existing !== undefined) {
278
- this.dataStore.delete(requestId);
279
- return Promise.resolve(existing);
290
+ // Check if queued data is already available.
291
+ const queue = this.dataQueues.get(requestId);
292
+ if (queue && queue.length > 0) {
293
+ const next = queue.shift();
294
+ if (queue.length === 0) {
295
+ this.dataQueues.delete(requestId);
296
+ }
297
+ return Promise.resolve(next);
280
298
  }
281
299
  return new Promise((resolve, reject) => {
282
300
  const sub = this.subscriptions.get(requestId);
@@ -57,6 +57,7 @@ export interface ExchangeOptions {
57
57
  * prediction market platforms (Polymarket, Kalshi, etc.).
58
58
  */
59
59
  export declare abstract class Exchange {
60
+ private static readonly OBDATA_WATCH_ALL_SOURCES;
60
61
  protected exchangeName: string;
61
62
  protected apiKey?: string;
62
63
  protected privateKey?: string;
@@ -116,6 +117,7 @@ export declare abstract class Exchange {
116
117
  private watchViaWs;
117
118
  private wsTransportUnavailableError;
118
119
  private isWsTransportUnavailableError;
120
+ private defaultWatchAllOrderBookVenues;
119
121
  private getWsInternals;
120
122
  private wsSubscriptionKey;
121
123
  private getWsSubscriptionId;
@@ -282,7 +284,8 @@ export declare abstract class Exchange {
282
284
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
283
285
  * Requires hosted mode (`pmxtApiKey` set).
284
286
  *
285
- * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
287
+ * @param venues - Optional venue filter. Defaults to this exchange's venue
288
+ * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues.
286
289
  * @returns Next event with source, symbol, and orderbook
287
290
  *
288
291
  * @example
@@ -129,6 +129,12 @@ function convertSubscriptionSnapshot(raw) {
129
129
  * prediction market platforms (Polymarket, Kalshi, etc.).
130
130
  */
131
131
  class Exchange {
132
+ static OBDATA_WATCH_ALL_SOURCES = new Set([
133
+ "polymarket",
134
+ "limitless",
135
+ "kalshi",
136
+ "opinion",
137
+ ]);
132
138
  exchangeName;
133
139
  apiKey;
134
140
  privateKey;
@@ -361,6 +367,12 @@ class Exchange {
361
367
  return error instanceof errors_js_1.PmxtError
362
368
  && /connection failed|no websocket|websocket.*not connected/i.test(error.message);
363
369
  }
370
+ defaultWatchAllOrderBookVenues() {
371
+ if (Exchange.OBDATA_WATCH_ALL_SOURCES.has(this.exchangeName)) {
372
+ return [this.exchangeName];
373
+ }
374
+ return undefined;
375
+ }
364
376
  getWsInternals(ws) {
365
377
  return ws;
366
378
  }
@@ -387,6 +399,7 @@ class Exchange {
387
399
  }
388
400
  internals.activeSubs.delete(subKey);
389
401
  internals.subscriptions.delete(requestId);
402
+ internals.dataQueues.delete(requestId);
390
403
  internals.dataStore.delete(requestId);
391
404
  const firstArg = args[0] ?? "";
392
405
  const symbols = Array.isArray(firstArg)
@@ -1566,7 +1579,8 @@ class Exchange {
1566
1579
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1567
1580
  * Requires hosted mode (`pmxtApiKey` set).
1568
1581
  *
1569
- * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1582
+ * @param venues - Optional venue filter. Defaults to this exchange's venue
1583
+ * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues.
1570
1584
  * @returns Next event with source, symbol, and orderbook
1571
1585
  *
1572
1586
  * @example
@@ -1583,7 +1597,8 @@ class Exchange {
1583
1597
  if (!this.isHosted) {
1584
1598
  throw new errors_js_1.PmxtError("watchAllOrderBooks() requires hosted mode (set pmxtApiKey)");
1585
1599
  }
1586
- const args = venues ? [venues] : [];
1600
+ const effectiveVenues = venues ?? this.defaultWatchAllOrderBookVenues();
1601
+ const args = effectiveVenues?.length ? [effectiveVenues] : [];
1587
1602
  const wsData = await this.watchViaWs("watchAllOrderBooks", args);
1588
1603
  if (wsData !== null) {
1589
1604
  return {
@@ -18,7 +18,9 @@ export declare class SidecarWsClient {
18
18
  private accessToken;
19
19
  private authParamName;
20
20
  private closed;
21
- /** requestId -> latest data payload */
21
+ /** requestId -> queued data payloads for single-event watch methods */
22
+ private dataQueues;
23
+ /** requestId[:symbol] -> latest data payload for batch snapshots */
22
24
  private dataStore;
23
25
  /** requestId -> subscription metadata */
24
26
  private subscriptions;
@@ -30,6 +32,7 @@ export declare class SidecarWsClient {
30
32
  private connect;
31
33
  private getWebSocketConstructor;
32
34
  private dispatch;
35
+ private deliverOrQueue;
33
36
  subscribe(exchange: string, method: string, args: any[], credentials?: Record<string, any>, timeoutMs?: number): Promise<any>;
34
37
  subscribeBatch(exchange: string, method: string, args: any[], credentials?: Record<string, any>, timeoutMs?: number): Promise<Record<string, any>>;
35
38
  close(): void;
@@ -10,6 +10,7 @@
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.SidecarWsClient = void 0;
12
12
  const errors_js_1 = require("./errors.js");
13
+ const MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION = 100_000;
13
14
  /**
14
15
  * Multiplexed WebSocket client for the pmxt sidecar.
15
16
  *
@@ -22,7 +23,9 @@ class SidecarWsClient {
22
23
  accessToken;
23
24
  authParamName;
24
25
  closed = false;
25
- /** requestId -> latest data payload */
26
+ /** requestId -> queued data payloads for single-event watch methods */
27
+ dataQueues = new Map();
28
+ /** requestId[:symbol] -> latest data payload for batch snapshots */
26
29
  dataStore = new Map();
27
30
  /** requestId -> subscription metadata */
28
31
  subscriptions = new Map();
@@ -157,16 +160,27 @@ class SidecarWsClient {
157
160
  if (eventType === "data" && requestId) {
158
161
  const symbol = msg.symbol || "";
159
162
  const data = msg.data || {};
160
- // Store by (requestId:symbol) for batch methods
163
+ // Store by (requestId:symbol) for batch methods.
161
164
  this.dataStore.set(`${requestId}:${symbol}`, data);
162
- // Store by requestId alone for single-symbol methods
163
- this.dataStore.set(requestId, data);
164
- const sub = this.subscriptions.get(requestId);
165
- if (sub?.resolve) {
166
- sub.resolve(data);
167
- sub.resolve = null;
168
- sub.reject = null;
169
- }
165
+ this.deliverOrQueue(requestId, data);
166
+ }
167
+ }
168
+ deliverOrQueue(requestId, data) {
169
+ const sub = this.subscriptions.get(requestId);
170
+ if (sub?.resolve) {
171
+ sub.resolve(data);
172
+ sub.resolve = null;
173
+ sub.reject = null;
174
+ return;
175
+ }
176
+ let queue = this.dataQueues.get(requestId);
177
+ if (!queue) {
178
+ queue = [];
179
+ this.dataQueues.set(requestId, queue);
180
+ }
181
+ queue.push(data);
182
+ if (queue.length > MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION) {
183
+ queue.splice(0, queue.length - MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION);
170
184
  }
171
185
  }
172
186
  // ------------------------------------------------------------------
@@ -236,8 +250,8 @@ class SidecarWsClient {
236
250
  throw new errors_js_1.PmxtError('[ws-client] Cannot send: WebSocket not connected');
237
251
  }
238
252
  this.ws.send(JSON.stringify(message));
239
- // Wait for first data event
240
- await this.waitForData(requestId, timeoutMs);
253
+ // Wait for first data event.
254
+ const firstData = await this.waitForData(requestId, timeoutMs);
241
255
  // Collect per-symbol data
242
256
  const result = {};
243
257
  for (const symbol of symbols) {
@@ -249,9 +263,8 @@ class SidecarWsClient {
249
263
  }
250
264
  // If no per-symbol data, return the single data event as-is
251
265
  if (Object.keys(result).length === 0) {
252
- const data = this.dataStore.get(requestId);
253
- if (data && typeof data === "object") {
254
- return data;
266
+ if (firstData && typeof firstData === "object") {
267
+ return firstData;
255
268
  }
256
269
  }
257
270
  return result;
@@ -267,6 +280,8 @@ class SidecarWsClient {
267
280
  }
268
281
  this.ws = null;
269
282
  }
283
+ this.dataQueues.clear();
284
+ this.dataStore.clear();
270
285
  }
271
286
  get connected() {
272
287
  return this.ws !== null && !this.closed;
@@ -275,11 +290,14 @@ class SidecarWsClient {
275
290
  // Internal
276
291
  // ------------------------------------------------------------------
277
292
  waitForData(requestId, timeoutMs) {
278
- // Check if data is already available
279
- const existing = this.dataStore.get(requestId);
280
- if (existing !== undefined) {
281
- this.dataStore.delete(requestId);
282
- return Promise.resolve(existing);
293
+ // Check if queued data is already available.
294
+ const queue = this.dataQueues.get(requestId);
295
+ if (queue && queue.length > 0) {
296
+ const next = queue.shift();
297
+ if (queue.length === 0) {
298
+ this.dataQueues.delete(requestId);
299
+ }
300
+ return Promise.resolve(next);
283
301
  }
284
302
  return new Promise((resolve, reject) => {
285
303
  const sub = this.subscriptions.get(requestId);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.46.3",
3
+ "version": "2.46.5",
4
4
  "description": "OpenAPI client for pmxtjs",
5
5
  "author": "OpenAPI-Generator",
6
6
  "repository": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.46.3",
3
+ "version": "2.46.5",
4
4
  "description": "Unified prediction market data API - The ccxt for prediction markets",
5
5
  "author": "PMXT Contributors",
6
6
  "repository": {
@@ -43,7 +43,7 @@
43
43
  "unified"
44
44
  ],
45
45
  "dependencies": {
46
- "pmxt-core": "2.46.3",
46
+ "pmxt-core": "2.46.5",
47
47
  "ws": "^8.18.0"
48
48
  },
49
49
  "devDependencies": {
package/pmxt/client.ts CHANGED
@@ -61,6 +61,7 @@ interface SidecarWsClientInternals {
61
61
  ws: RawWebSocketLike | null;
62
62
  activeSubs: Map<string, string>;
63
63
  subscriptions: Map<string, { reject: ((error: Error) => void) | null }>;
64
+ dataQueues: Map<string, any[]>;
64
65
  dataStore: Map<string, any>;
65
66
  }
66
67
 
@@ -238,6 +239,13 @@ export interface ExchangeOptions {
238
239
  * prediction market platforms (Polymarket, Kalshi, etc.).
239
240
  */
240
241
  export abstract class Exchange {
242
+ private static readonly OBDATA_WATCH_ALL_SOURCES = new Set([
243
+ "polymarket",
244
+ "limitless",
245
+ "kalshi",
246
+ "opinion",
247
+ ]);
248
+
241
249
  protected exchangeName: string;
242
250
  protected apiKey?: string;
243
251
  protected privateKey?: string;
@@ -508,6 +516,13 @@ export abstract class Exchange {
508
516
  && /connection failed|no websocket|websocket.*not connected/i.test(error.message);
509
517
  }
510
518
 
519
+ private defaultWatchAllOrderBookVenues(): string[] | undefined {
520
+ if (Exchange.OBDATA_WATCH_ALL_SOURCES.has(this.exchangeName)) {
521
+ return [this.exchangeName];
522
+ }
523
+ return undefined;
524
+ }
525
+
511
526
  private getWsInternals(ws: SidecarWsClient): SidecarWsClientInternals {
512
527
  return ws as unknown as SidecarWsClientInternals;
513
528
  }
@@ -538,6 +553,7 @@ export abstract class Exchange {
538
553
 
539
554
  internals.activeSubs.delete(subKey);
540
555
  internals.subscriptions.delete(requestId);
556
+ internals.dataQueues.delete(requestId);
541
557
  internals.dataStore.delete(requestId);
542
558
 
543
559
  const firstArg = args[0] ?? "";
@@ -1718,7 +1734,8 @@ export abstract class Exchange {
1718
1734
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1719
1735
  * Requires hosted mode (`pmxtApiKey` set).
1720
1736
  *
1721
- * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1737
+ * @param venues - Optional venue filter. Defaults to this exchange's venue
1738
+ * for venue clients (e.g. Kalshi -> ["kalshi"]); Router defaults to all venues.
1722
1739
  * @returns Next event with source, symbol, and orderbook
1723
1740
  *
1724
1741
  * @example
@@ -1737,7 +1754,8 @@ export abstract class Exchange {
1737
1754
  throw new PmxtError("watchAllOrderBooks() requires hosted mode (set pmxtApiKey)");
1738
1755
  }
1739
1756
 
1740
- const args: any[] = venues ? [venues] : [];
1757
+ const effectiveVenues = venues ?? this.defaultWatchAllOrderBookVenues();
1758
+ const args: any[] = effectiveVenues?.length ? [effectiveVenues] : [];
1741
1759
  const wsData = await this.watchViaWs("watchAllOrderBooks", args);
1742
1760
  if (wsData !== null) {
1743
1761
  return {
package/pmxt/ws-client.ts CHANGED
@@ -9,6 +9,8 @@
9
9
 
10
10
  import { PmxtError } from "./errors.js";
11
11
 
12
+ const MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION = 100_000;
13
+
12
14
  interface WsSubscription {
13
15
  readonly requestId: string;
14
16
  readonly method: string;
@@ -39,7 +41,9 @@ export class SidecarWsClient {
39
41
  private authParamName: string;
40
42
  private closed = false;
41
43
 
42
- /** requestId -> latest data payload */
44
+ /** requestId -> queued data payloads for single-event watch methods */
45
+ private dataQueues: Map<string, any[]> = new Map();
46
+ /** requestId[:symbol] -> latest data payload for batch snapshots */
43
47
  private dataStore: Map<string, any> = new Map();
44
48
  /** requestId -> subscription metadata */
45
49
  private subscriptions: Map<string, WsSubscription> = new Map();
@@ -189,17 +193,30 @@ export class SidecarWsClient {
189
193
  const symbol = msg.symbol || "";
190
194
  const data = msg.data || {};
191
195
 
192
- // Store by (requestId:symbol) for batch methods
196
+ // Store by (requestId:symbol) for batch methods.
193
197
  this.dataStore.set(`${requestId}:${symbol}`, data);
194
- // Store by requestId alone for single-symbol methods
195
- this.dataStore.set(requestId, data);
198
+ this.deliverOrQueue(requestId, data);
199
+ }
200
+ }
196
201
 
197
- const sub = this.subscriptions.get(requestId);
198
- if (sub?.resolve) {
199
- sub.resolve(data);
200
- sub.resolve = null;
201
- sub.reject = null;
202
- }
202
+ private deliverOrQueue(requestId: string, data: any): void {
203
+ const sub = this.subscriptions.get(requestId);
204
+ if (sub?.resolve) {
205
+ sub.resolve(data);
206
+ sub.resolve = null;
207
+ sub.reject = null;
208
+ return;
209
+ }
210
+
211
+ let queue = this.dataQueues.get(requestId);
212
+ if (!queue) {
213
+ queue = [];
214
+ this.dataQueues.set(requestId, queue);
215
+ }
216
+ queue.push(data);
217
+
218
+ if (queue.length > MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION) {
219
+ queue.splice(0, queue.length - MAX_QUEUED_MESSAGES_PER_SUBSCRIPTION);
203
220
  }
204
221
  }
205
222
 
@@ -297,8 +314,8 @@ export class SidecarWsClient {
297
314
  }
298
315
  this.ws.send(JSON.stringify(message));
299
316
 
300
- // Wait for first data event
301
- await this.waitForData(requestId, timeoutMs);
317
+ // Wait for first data event.
318
+ const firstData = await this.waitForData(requestId, timeoutMs);
302
319
 
303
320
  // Collect per-symbol data
304
321
  const result: Record<string, any> = {};
@@ -312,9 +329,8 @@ export class SidecarWsClient {
312
329
 
313
330
  // If no per-symbol data, return the single data event as-is
314
331
  if (Object.keys(result).length === 0) {
315
- const data = this.dataStore.get(requestId);
316
- if (data && typeof data === "object") {
317
- return data;
332
+ if (firstData && typeof firstData === "object") {
333
+ return firstData;
318
334
  }
319
335
  }
320
336
 
@@ -331,6 +347,8 @@ export class SidecarWsClient {
331
347
  }
332
348
  this.ws = null;
333
349
  }
350
+ this.dataQueues.clear();
351
+ this.dataStore.clear();
334
352
  }
335
353
 
336
354
  get connected(): boolean {
@@ -342,11 +360,14 @@ export class SidecarWsClient {
342
360
  // ------------------------------------------------------------------
343
361
 
344
362
  private waitForData(requestId: string, timeoutMs: number): Promise<any> {
345
- // Check if data is already available
346
- const existing = this.dataStore.get(requestId);
347
- if (existing !== undefined) {
348
- this.dataStore.delete(requestId);
349
- return Promise.resolve(existing);
363
+ // Check if queued data is already available.
364
+ const queue = this.dataQueues.get(requestId);
365
+ if (queue && queue.length > 0) {
366
+ const next = queue.shift();
367
+ if (queue.length === 0) {
368
+ this.dataQueues.delete(requestId);
369
+ }
370
+ return Promise.resolve(next);
350
371
  }
351
372
 
352
373
  return new Promise<any>((resolve, reject) => {