pmxtjs 2.35.30 → 2.35.32

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.
Files changed (42) hide show
  1. package/dist/esm/generated/src/apis/DefaultApi.d.ts +33 -1
  2. package/dist/esm/generated/src/apis/DefaultApi.js +48 -1
  3. package/dist/esm/generated/src/models/WatchOrderBooks200Response.d.ts +48 -0
  4. package/dist/esm/generated/src/models/WatchOrderBooks200Response.js +48 -0
  5. package/dist/esm/generated/src/models/WatchOrderBooksRequest.d.ts +40 -0
  6. package/dist/esm/generated/src/models/WatchOrderBooksRequest.js +47 -0
  7. package/dist/esm/generated/src/models/WatchOrderBooksRequestArgsInner.d.ts +21 -0
  8. package/dist/esm/generated/src/models/WatchOrderBooksRequestArgsInner.js +47 -0
  9. package/dist/esm/generated/src/models/index.d.ts +3 -0
  10. package/dist/esm/generated/src/models/index.js +3 -0
  11. package/dist/esm/pmxt/client.d.ts +42 -0
  12. package/dist/esm/pmxt/client.js +149 -4
  13. package/dist/esm/pmxt/ws-client.d.ts +37 -0
  14. package/dist/esm/pmxt/ws-client.js +272 -0
  15. package/dist/generated/src/apis/DefaultApi.d.ts +33 -1
  16. package/dist/generated/src/apis/DefaultApi.js +48 -1
  17. package/dist/generated/src/models/WatchOrderBooks200Response.d.ts +48 -0
  18. package/dist/generated/src/models/WatchOrderBooks200Response.js +55 -0
  19. package/dist/generated/src/models/WatchOrderBooksRequest.d.ts +40 -0
  20. package/dist/generated/src/models/WatchOrderBooksRequest.js +54 -0
  21. package/dist/generated/src/models/WatchOrderBooksRequestArgsInner.d.ts +21 -0
  22. package/dist/generated/src/models/WatchOrderBooksRequestArgsInner.js +53 -0
  23. package/dist/generated/src/models/index.d.ts +3 -0
  24. package/dist/generated/src/models/index.js +3 -0
  25. package/dist/pmxt/client.d.ts +42 -0
  26. package/dist/pmxt/client.js +149 -4
  27. package/dist/pmxt/ws-client.d.ts +37 -0
  28. package/dist/pmxt/ws-client.js +276 -0
  29. package/generated/.openapi-generator/FILES +6 -0
  30. package/generated/docs/DefaultApi.md +71 -0
  31. package/generated/docs/WatchOrderBooks200Response.md +38 -0
  32. package/generated/docs/WatchOrderBooksRequest.md +36 -0
  33. package/generated/docs/WatchOrderBooksRequestArgsInner.md +32 -0
  34. package/generated/package.json +1 -1
  35. package/generated/src/apis/DefaultApi.ts +71 -0
  36. package/generated/src/models/WatchOrderBooks200Response.ts +96 -0
  37. package/generated/src/models/WatchOrderBooksRequest.ts +89 -0
  38. package/generated/src/models/WatchOrderBooksRequestArgsInner.ts +59 -0
  39. package/generated/src/models/index.ts +3 -0
  40. package/package.json +2 -2
  41. package/pmxt/client.ts +181 -8
  42. package/pmxt/ws-client.ts +347 -0
package/pmxt/client.ts CHANGED
@@ -44,6 +44,7 @@ import { ServerManager } from "./server-manager.js";
44
44
  import { buildArgsWithOptionalOptions } from "./args.js";
45
45
  import { PmxtError, fromServerError } from "./errors.js";
46
46
  import { LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js";
47
+ import { SidecarWsClient } from "./ws-client.js";
47
48
 
48
49
  /**
49
50
  * Resolve a MarketOutcome shorthand to a plain outcome ID string.
@@ -348,6 +349,11 @@ export abstract class Exchange {
348
349
  */
349
350
  private _getReadsUnsupported: boolean = false;
350
351
 
352
+ /** Shared WebSocket client for streaming methods (lazy). */
353
+ private _wsClient: SidecarWsClient | null = null;
354
+ /** Sticky flag: true if the sidecar /ws endpoint is unavailable. */
355
+ private _wsUnsupported: boolean = false;
356
+
351
357
  constructor(exchangeName: string, options: ExchangeOptions = {}) {
352
358
  this.exchangeName = exchangeName.toLowerCase();
353
359
  this.apiKey = options.apiKey;
@@ -502,6 +508,73 @@ export abstract class Exchange {
502
508
  throw lastError;
503
509
  }
504
510
 
511
+ /**
512
+ * Return the shared WebSocket client, creating it on first use.
513
+ *
514
+ * Returns `null` if the sidecar /ws endpoint was previously found
515
+ * to be unavailable, letting callers fall back to HTTP.
516
+ */
517
+ private async getOrCreateWs(): Promise<SidecarWsClient | null> {
518
+ if (this._wsUnsupported) return null;
519
+ if (this._wsClient?.connected) return this._wsClient;
520
+
521
+ const host = this.resolveBaseUrl();
522
+ const accessToken = this.serverManager.getAccessToken();
523
+
524
+ const client = new SidecarWsClient(host, accessToken || undefined);
525
+ try {
526
+ // Trigger connection to validate the endpoint exists.
527
+ // subscribe() calls ensureConnected internally, but we want
528
+ // to detect failure eagerly so we can set _wsUnsupported.
529
+ await client.subscribe(
530
+ this.exchangeName,
531
+ "_ping",
532
+ [],
533
+ undefined,
534
+ 3000,
535
+ ).catch(() => {
536
+ // Expected -- no _ping method. The connection itself
537
+ // succeeded if we got a WS error frame back. If the
538
+ // connection itself failed, we'll catch below.
539
+ });
540
+ // If we got here without the connect promise rejecting,
541
+ // the WS endpoint exists.
542
+ if (!client.connected) {
543
+ throw new Error("WS handshake failed");
544
+ }
545
+ } catch {
546
+ this._wsUnsupported = true;
547
+ client.close();
548
+ return null;
549
+ }
550
+
551
+ this._wsClient = client;
552
+ return this._wsClient;
553
+ }
554
+
555
+ /**
556
+ * Attempt to use the WS transport for a watch method.
557
+ * Returns the raw data on success, or `null` if WS is unavailable.
558
+ */
559
+ private async watchViaWs(
560
+ method: string,
561
+ args: any[],
562
+ ): Promise<any | null> {
563
+ const ws = await this.getOrCreateWs();
564
+ if (!ws) return null;
565
+
566
+ try {
567
+ return await ws.subscribe(
568
+ this.exchangeName,
569
+ method,
570
+ args,
571
+ this.getCredentials() as Record<string, any> | undefined,
572
+ );
573
+ } catch {
574
+ return null;
575
+ }
576
+ }
577
+
505
578
  // Low-Level API Access
506
579
 
507
580
  /**
@@ -1432,12 +1505,19 @@ export abstract class Exchange {
1432
1505
  async watchOrderBook(outcomeId: string | MarketOutcome, limit?: number): Promise<OrderBook> {
1433
1506
  await this.initPromise;
1434
1507
  const resolvedOutcomeId = resolveOutcomeId(outcomeId);
1435
- try {
1436
- const args: any[] = [resolvedOutcomeId];
1437
- if (limit !== undefined) {
1438
- args.push(limit);
1439
- }
1508
+ const args: any[] = [resolvedOutcomeId];
1509
+ if (limit !== undefined) {
1510
+ args.push(limit);
1511
+ }
1440
1512
 
1513
+ // Try WebSocket transport first
1514
+ const wsData = await this.watchViaWs("watchOrderBook", args);
1515
+ if (wsData !== null) {
1516
+ return convertOrderBook(wsData);
1517
+ }
1518
+
1519
+ // HTTP fallback
1520
+ try {
1441
1521
  const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/watchOrderBook`, {
1442
1522
  method: 'POST',
1443
1523
  headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
@@ -1459,18 +1539,111 @@ export abstract class Exchange {
1459
1539
  }
1460
1540
  }
1461
1541
 
1542
+ /**
1543
+ * Watch real-time order book updates for multiple outcomes at once.
1544
+ *
1545
+ * Returns a record mapping each outcome ID (ticker) to its latest
1546
+ * order book snapshot. Call repeatedly in a loop to stream updates
1547
+ * (CCXT Pro pattern).
1548
+ *
1549
+ * Prefers the sidecar WebSocket transport when available, falling
1550
+ * back to HTTP POST for older sidecars.
1551
+ *
1552
+ * @param outcomeIds - Array of outcome IDs (or MarketOutcome objects)
1553
+ * @param limit - Optional depth limit for each order book
1554
+ * @returns Record mapping ticker to OrderBook
1555
+ *
1556
+ * @example
1557
+ * ```typescript
1558
+ * const ids = markets.slice(0, 3).map(m => m.outcomes[0].outcomeId);
1559
+ * while (true) {
1560
+ * const books = await exchange.watchOrderBooks(ids);
1561
+ * for (const [ticker, ob] of Object.entries(books)) {
1562
+ * console.log(`${ticker}: bid=${ob.bids[0]?.price}`);
1563
+ * }
1564
+ * }
1565
+ * ```
1566
+ */
1567
+ async watchOrderBooks(
1568
+ outcomeIds: (string | MarketOutcome)[],
1569
+ limit?: number,
1570
+ ): Promise<Record<string, OrderBook>> {
1571
+ await this.initPromise;
1572
+ const resolvedIds = outcomeIds.map(resolveOutcomeId);
1573
+ const args: any[] = [resolvedIds];
1574
+ if (limit !== undefined) {
1575
+ args.push(limit);
1576
+ }
1577
+
1578
+ // Try WebSocket transport first
1579
+ const ws = await this.getOrCreateWs();
1580
+ if (ws) {
1581
+ try {
1582
+ const rawResult = await ws.subscribeBatch(
1583
+ this.exchangeName,
1584
+ "watchOrderBooks",
1585
+ args,
1586
+ this.getCredentials() as Record<string, any> | undefined,
1587
+ );
1588
+ if (rawResult && typeof rawResult === "object") {
1589
+ const result: Record<string, OrderBook> = {};
1590
+ for (const [k, v] of Object.entries(rawResult)) {
1591
+ if (v && typeof v === "object") {
1592
+ result[k] = convertOrderBook(v);
1593
+ }
1594
+ }
1595
+ return result;
1596
+ }
1597
+ } catch {
1598
+ // fall through to HTTP
1599
+ }
1600
+ }
1601
+
1602
+ // HTTP fallback
1603
+ try {
1604
+ const response = await this.fetchWithRetry(
1605
+ `${this.resolveBaseUrl()}/api/${this.exchangeName}/watchOrderBooks`,
1606
+ {
1607
+ method: 'POST',
1608
+ headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
1609
+ body: JSON.stringify({ args, credentials: this.getCredentials() }),
1610
+ },
1611
+ );
1612
+ if (!response.ok) {
1613
+ const body = await response.json().catch(() => ({}));
1614
+ if (body.error && typeof body.error === "object") {
1615
+ throw fromServerError(body.error);
1616
+ }
1617
+ throw new PmxtError(body.error?.message || response.statusText);
1618
+ }
1619
+ const json = await response.json();
1620
+ const data = this.handleResponse(json);
1621
+ if (data && typeof data === "object") {
1622
+ const result: Record<string, OrderBook> = {};
1623
+ for (const [k, v] of Object.entries(data as Record<string, any>)) {
1624
+ result[k] = convertOrderBook(v);
1625
+ }
1626
+ return result;
1627
+ }
1628
+ return {};
1629
+ } catch (error) {
1630
+ if (error instanceof PmxtError) throw error;
1631
+ throw new PmxtError(`Failed to watch order books: ${error}`);
1632
+ }
1633
+ }
1634
+
1462
1635
  /**
1463
1636
  * Watch real-time trade updates via WebSocket.
1464
- *
1637
+ *
1465
1638
  * Returns a promise that resolves with the next trade(s).
1466
1639
  * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1467
- *
1640
+ *
1468
1641
  * @param outcomeId - Outcome ID to watch
1469
1642
  * @param address - Public wallet to be watched
1470
1643
  * @param since - Optional timestamp to filter trades from
1471
1644
  * @param limit - Optional limit for number of trades
1472
1645
  * @returns Next trade update(s)
1473
- *
1646
+ *
1474
1647
  * @example
1475
1648
  * ```typescript
1476
1649
  * // Stream trade updates
@@ -0,0 +1,347 @@
1
+ /**
2
+ * WebSocket client for streaming methods.
3
+ *
4
+ * Provides a multiplexed WebSocket connection to the sidecar server,
5
+ * used by watchOrderBook and watchOrderBooks as an alternative to
6
+ * HTTP long-polling. Falls back to HTTP transparently when the sidecar
7
+ * does not support the /ws endpoint.
8
+ */
9
+
10
+ import { PmxtError } from "./errors.js";
11
+
12
+ interface WsSubscription {
13
+ readonly requestId: string;
14
+ readonly method: string;
15
+ readonly symbols: string[];
16
+ resolve: ((data: any) => void) | null;
17
+ reject: ((error: Error) => void) | null;
18
+ }
19
+
20
+ interface WsMessage {
21
+ id?: string;
22
+ event?: string;
23
+ method?: string;
24
+ symbol?: string;
25
+ data?: any;
26
+ error?: { message?: string; code?: string };
27
+ }
28
+
29
+ /**
30
+ * Multiplexed WebSocket client for the pmxt sidecar.
31
+ *
32
+ * Lazily connects to ws://{host}/ws?token={accessToken}. A single
33
+ * WebSocket connection is shared across all streaming subscriptions.
34
+ */
35
+ export class SidecarWsClient {
36
+ private ws: WebSocket | null = null;
37
+ private host: string;
38
+ private accessToken: string | undefined;
39
+ private closed = false;
40
+
41
+ /** requestId -> latest data payload */
42
+ private dataStore: Map<string, any> = new Map();
43
+ /** requestId -> subscription metadata */
44
+ private subscriptions: Map<string, WsSubscription> = new Map();
45
+ /** (method:symbolKey) -> requestId -- avoids duplicate subscribes */
46
+ private activeSubs: Map<string, string> = new Map();
47
+
48
+ private connectPromise: Promise<void> | null = null;
49
+
50
+ constructor(host: string, accessToken?: string) {
51
+ this.host = host;
52
+ this.accessToken = accessToken;
53
+ }
54
+
55
+ // ------------------------------------------------------------------
56
+ // Connection lifecycle
57
+ // ------------------------------------------------------------------
58
+
59
+ private async ensureConnected(): Promise<void> {
60
+ if (this.ws && !this.closed) return;
61
+ if (this.connectPromise) return this.connectPromise;
62
+
63
+ this.connectPromise = this.connect();
64
+ try {
65
+ await this.connectPromise;
66
+ } finally {
67
+ this.connectPromise = null;
68
+ }
69
+ }
70
+
71
+ private connect(): Promise<void> {
72
+ return new Promise<void>((resolve, reject) => {
73
+ let hostPart = this.host;
74
+ let scheme = "ws";
75
+ if (hostPart.startsWith("https://")) {
76
+ hostPart = hostPart.slice("https://".length);
77
+ scheme = "wss";
78
+ } else if (hostPart.startsWith("http://")) {
79
+ hostPart = hostPart.slice("http://".length);
80
+ }
81
+
82
+ let url = `${scheme}://${hostPart}/ws`;
83
+ if (this.accessToken) {
84
+ url = `${url}?token=${this.accessToken}`;
85
+ }
86
+
87
+ // Use the ws package in Node.js, native WebSocket in browsers
88
+ const WsConstructor = this.getWebSocketConstructor();
89
+ if (!WsConstructor) {
90
+ reject(new PmxtError("No WebSocket implementation available"));
91
+ return;
92
+ }
93
+
94
+ const ws = new WsConstructor(url);
95
+ this.closed = false;
96
+
97
+ ws.onopen = () => {
98
+ this.ws = ws;
99
+ resolve();
100
+ };
101
+
102
+ ws.onerror = (err: any) => {
103
+ if (!this.ws) {
104
+ // Connection failed during handshake
105
+ reject(new PmxtError(`WebSocket connection failed: ${err.message || err}`));
106
+ }
107
+ };
108
+
109
+ ws.onclose = () => {
110
+ this.closed = true;
111
+ this.ws = null;
112
+ };
113
+
114
+ ws.onmessage = (event: any) => {
115
+ try {
116
+ const data = typeof event.data === "string"
117
+ ? event.data
118
+ : event.data.toString();
119
+ const msg: WsMessage = JSON.parse(data);
120
+ this.dispatch(msg);
121
+ } catch {
122
+ // Ignore unparseable frames
123
+ }
124
+ };
125
+ });
126
+ }
127
+
128
+ private getWebSocketConstructor(): (new (url: string) => WebSocket) | null {
129
+ // Browser / Deno / Bun
130
+ if (typeof globalThis !== "undefined" && (globalThis as any).WebSocket) {
131
+ return (globalThis as any).WebSocket;
132
+ }
133
+ // Node.js -- try to require ws
134
+ try {
135
+ // Dynamic require to avoid bundler issues
136
+ const wsModule = require("ws");
137
+ return wsModule.default || wsModule;
138
+ } catch {
139
+ return null;
140
+ }
141
+ }
142
+
143
+ private dispatch(msg: WsMessage): void {
144
+ const eventType = msg.event;
145
+ const requestId = msg.id;
146
+
147
+ if (eventType === "error" && requestId) {
148
+ const sub = this.subscriptions.get(requestId);
149
+ if (sub?.reject) {
150
+ sub.reject(new PmxtError(
151
+ msg.error?.message || "WebSocket subscription error"
152
+ ));
153
+ sub.reject = null;
154
+ sub.resolve = null;
155
+ }
156
+ return;
157
+ }
158
+
159
+ if (eventType === "subscribed") {
160
+ // Acknowledgement -- nothing to do
161
+ return;
162
+ }
163
+
164
+ if (eventType === "data" && requestId) {
165
+ const symbol = msg.symbol || "";
166
+ const data = msg.data || {};
167
+
168
+ // Store by (requestId:symbol) for batch methods
169
+ this.dataStore.set(`${requestId}:${symbol}`, data);
170
+ // Store by requestId alone for single-symbol methods
171
+ this.dataStore.set(requestId, data);
172
+
173
+ const sub = this.subscriptions.get(requestId);
174
+ if (sub?.resolve) {
175
+ sub.resolve(data);
176
+ sub.resolve = null;
177
+ sub.reject = null;
178
+ }
179
+ }
180
+ }
181
+
182
+ // ------------------------------------------------------------------
183
+ // Public API
184
+ // ------------------------------------------------------------------
185
+
186
+ async subscribe(
187
+ exchange: string,
188
+ method: string,
189
+ args: any[],
190
+ credentials?: Record<string, any>,
191
+ timeoutMs = 30000,
192
+ ): Promise<any> {
193
+ const firstArg = args[0] ?? "";
194
+ const subKey = Array.isArray(firstArg)
195
+ ? `${method}:${[...firstArg].sort().join(",")}`
196
+ : `${method}:${firstArg}`;
197
+
198
+ // Reuse existing subscription
199
+ const existingId = this.activeSubs.get(subKey);
200
+ if (existingId && this.subscriptions.has(existingId)) {
201
+ return this.waitForData(existingId, timeoutMs);
202
+ }
203
+
204
+ await this.ensureConnected();
205
+
206
+ const requestId = `req-${Math.random().toString(36).slice(2, 14)}`;
207
+ const symbols = Array.isArray(firstArg) ? firstArg : firstArg ? [firstArg] : [];
208
+
209
+ const sub: WsSubscription = {
210
+ requestId,
211
+ method,
212
+ symbols,
213
+ resolve: null,
214
+ reject: null,
215
+ };
216
+ this.subscriptions.set(requestId, sub);
217
+ this.activeSubs.set(subKey, requestId);
218
+
219
+ const message: Record<string, any> = {
220
+ id: requestId,
221
+ action: "subscribe",
222
+ exchange,
223
+ method,
224
+ args,
225
+ };
226
+ if (credentials) {
227
+ message.credentials = credentials;
228
+ }
229
+
230
+ this.ws!.send(JSON.stringify(message));
231
+
232
+ return this.waitForData(requestId, timeoutMs);
233
+ }
234
+
235
+ async subscribeBatch(
236
+ exchange: string,
237
+ method: string,
238
+ args: any[],
239
+ credentials?: Record<string, any>,
240
+ timeoutMs = 30000,
241
+ ): Promise<Record<string, any>> {
242
+ const symbols: string[] = Array.isArray(args[0]) ? args[0] : [];
243
+
244
+ await this.ensureConnected();
245
+
246
+ const requestId = `req-${Math.random().toString(36).slice(2, 14)}`;
247
+
248
+ const sub: WsSubscription = {
249
+ requestId,
250
+ method,
251
+ symbols,
252
+ resolve: null,
253
+ reject: null,
254
+ };
255
+ this.subscriptions.set(requestId, sub);
256
+
257
+ const message: Record<string, any> = {
258
+ id: requestId,
259
+ action: "subscribe",
260
+ exchange,
261
+ method,
262
+ args,
263
+ };
264
+ if (credentials) {
265
+ message.credentials = credentials;
266
+ }
267
+
268
+ this.ws!.send(JSON.stringify(message));
269
+
270
+ // Wait for first data event
271
+ await this.waitForData(requestId, timeoutMs);
272
+
273
+ // Collect per-symbol data
274
+ const result: Record<string, any> = {};
275
+ for (const symbol of symbols) {
276
+ const storeKey = `${requestId}:${symbol}`;
277
+ const data = this.dataStore.get(storeKey);
278
+ if (data !== undefined) {
279
+ result[symbol] = data;
280
+ }
281
+ }
282
+
283
+ // If no per-symbol data, return the single data event as-is
284
+ if (Object.keys(result).length === 0) {
285
+ const data = this.dataStore.get(requestId);
286
+ if (data && typeof data === "object") {
287
+ return data;
288
+ }
289
+ }
290
+
291
+ return result;
292
+ }
293
+
294
+ close(): void {
295
+ this.closed = true;
296
+ if (this.ws) {
297
+ try {
298
+ this.ws.close();
299
+ } catch {
300
+ // ignore
301
+ }
302
+ this.ws = null;
303
+ }
304
+ }
305
+
306
+ get connected(): boolean {
307
+ return this.ws !== null && !this.closed;
308
+ }
309
+
310
+ // ------------------------------------------------------------------
311
+ // Internal
312
+ // ------------------------------------------------------------------
313
+
314
+ private waitForData(requestId: string, timeoutMs: number): Promise<any> {
315
+ // Check if data is already available
316
+ const existing = this.dataStore.get(requestId);
317
+ if (existing !== undefined) {
318
+ this.dataStore.delete(requestId);
319
+ return Promise.resolve(existing);
320
+ }
321
+
322
+ return new Promise<any>((resolve, reject) => {
323
+ const sub = this.subscriptions.get(requestId);
324
+ if (!sub) {
325
+ reject(new PmxtError("Subscription not found"));
326
+ return;
327
+ }
328
+
329
+ const timer = setTimeout(() => {
330
+ sub.resolve = null;
331
+ sub.reject = null;
332
+ reject(new PmxtError(
333
+ `Timeout waiting for WebSocket data (method=${sub.method})`
334
+ ));
335
+ }, timeoutMs);
336
+
337
+ sub.resolve = (data: any) => {
338
+ clearTimeout(timer);
339
+ resolve(data);
340
+ };
341
+ sub.reject = (err: Error) => {
342
+ clearTimeout(timer);
343
+ reject(err);
344
+ };
345
+ });
346
+ }
347
+ }