pmxtjs 2.44.5 → 2.45.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/pmxt/client.ts CHANGED
@@ -52,6 +52,18 @@ import { PmxtError, fromServerError } from "./errors.js";
52
52
  import { LOCAL_URL, resolvePmxtBaseUrl } from "./constants.js";
53
53
  import { SidecarWsClient } from "./ws-client.js";
54
54
 
55
+ interface RawWebSocketLike {
56
+ send(data: string): void;
57
+ }
58
+
59
+ interface SidecarWsClientInternals {
60
+ ensureConnected(): Promise<void>;
61
+ ws: RawWebSocketLike | null;
62
+ activeSubs: Map<string, string>;
63
+ subscriptions: Map<string, { reject: ((error: Error) => void) | null }>;
64
+ dataStore: Map<string, any>;
65
+ }
66
+
55
67
  /**
56
68
  * Resolve a MarketOutcome shorthand to a plain outcome ID string.
57
69
  * Accepts either a raw string ID or a MarketOutcome object.
@@ -418,7 +430,7 @@ export abstract class Exchange {
418
430
  * Return the shared WebSocket client, creating it on first use.
419
431
  *
420
432
  * Returns `null` if the sidecar /ws endpoint was previously found
421
- * to be unavailable, letting callers fall back to HTTP.
433
+ * to be unavailable.
422
434
  */
423
435
  private async getOrCreateWs(): Promise<SidecarWsClient | null> {
424
436
  if (this._wsUnsupported) return null;
@@ -480,14 +492,79 @@ export abstract class Exchange {
480
492
  this.getCredentials() as Record<string, any> | undefined,
481
493
  );
482
494
  } catch (error) {
483
- // Only fall back to HTTP for transport-level failures
484
- if (error instanceof PmxtError && /connection failed|no websocket/i.test(error.message)) {
495
+ if (this.isWsTransportUnavailableError(error)) {
485
496
  return null;
486
497
  }
487
498
  throw error;
488
499
  }
489
500
  }
490
501
 
502
+ private wsTransportUnavailableError(method: string): PmxtError {
503
+ return new PmxtError(`${method}() requires WebSocket transport — connection failed`);
504
+ }
505
+
506
+ private isWsTransportUnavailableError(error: unknown): boolean {
507
+ return error instanceof PmxtError
508
+ && /connection failed|no websocket|websocket.*not connected/i.test(error.message);
509
+ }
510
+
511
+ private getWsInternals(ws: SidecarWsClient): SidecarWsClientInternals {
512
+ return ws as unknown as SidecarWsClientInternals;
513
+ }
514
+
515
+ private wsSubscriptionKey(method: string, args: any[]): string {
516
+ const firstArg = args[0] ?? "";
517
+ return Array.isArray(firstArg)
518
+ ? `${method}:${[...firstArg].sort().join(",")}`
519
+ : `${method}:${firstArg}`;
520
+ }
521
+
522
+ private getWsSubscriptionId(ws: SidecarWsClient, method: string, args: any[]): string | undefined {
523
+ const internals = this.getWsInternals(ws);
524
+ const subKey = this.wsSubscriptionKey(method, args);
525
+ return internals.activeSubs.get(subKey);
526
+ }
527
+
528
+ private clearWsSubscription(ws: SidecarWsClient, method: string, args: any[]): void {
529
+ const internals = this.getWsInternals(ws);
530
+ const subKey = this.wsSubscriptionKey(method, args);
531
+ const requestId = internals.activeSubs.get(subKey);
532
+ if (!requestId) return;
533
+
534
+ const sub = internals.subscriptions.get(requestId);
535
+ if (sub?.reject) {
536
+ sub.reject(new PmxtError(`${method} subscription cancelled`));
537
+ }
538
+
539
+ internals.activeSubs.delete(subKey);
540
+ internals.subscriptions.delete(requestId);
541
+ internals.dataStore.delete(requestId);
542
+
543
+ const firstArg = args[0] ?? "";
544
+ const symbols = Array.isArray(firstArg)
545
+ ? firstArg.map(String)
546
+ : firstArg
547
+ ? [String(firstArg)]
548
+ : [];
549
+ for (const symbol of symbols) {
550
+ internals.dataStore.delete(`${requestId}:${symbol}`);
551
+ }
552
+ }
553
+
554
+ private async sendWsMessage(
555
+ ws: SidecarWsClient,
556
+ message: Record<string, any>,
557
+ ): Promise<void> {
558
+ const internals = this.getWsInternals(ws);
559
+ await internals.ensureConnected();
560
+
561
+ const socket = internals.ws;
562
+ if (!socket) {
563
+ throw new PmxtError('[ws-client] Cannot send: WebSocket not connected');
564
+ }
565
+ socket.send(JSON.stringify(message));
566
+ }
567
+
491
568
  // Low-Level API Access
492
569
 
493
570
  /**
@@ -1122,24 +1199,32 @@ export abstract class Exchange {
1122
1199
 
1123
1200
  async unwatchOrderBook(outcomeId: string | MarketOutcome): Promise<void> {
1124
1201
  await this.initPromise;
1202
+ const resolvedOutcomeId = resolveOutcomeId(outcomeId);
1203
+ const args: any[] = [resolvedOutcomeId];
1125
1204
  try {
1126
- const args: any[] = [];
1127
- args.push(resolveOutcomeId(outcomeId));
1128
- const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/unwatchOrderBook`, {
1129
- method: 'POST',
1130
- headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
1131
- body: JSON.stringify({ args, credentials: this.getCredentials() }),
1132
- });
1133
- if (!response.ok) {
1134
- const body = await response.json().catch(() => ({}));
1135
- if (body.error && typeof body.error === "object") {
1136
- throw fromServerError(body.error);
1137
- }
1138
- throw new PmxtError(body.error?.message || response.statusText);
1205
+ const ws = await this.getOrCreateWs();
1206
+ if (!ws) {
1207
+ throw this.wsTransportUnavailableError("unwatchOrderBook");
1139
1208
  }
1140
- const json = await response.json();
1141
- this.handleResponse(json);
1209
+
1210
+ const requestId = this.getWsSubscriptionId(ws, "watchOrderBook", args)
1211
+ ?? `req-${Math.random().toString(36).slice(2, 14)}`;
1212
+
1213
+ await this.sendWsMessage(
1214
+ ws,
1215
+ {
1216
+ id: requestId,
1217
+ action: "unsubscribe",
1218
+ exchange: this.exchangeName,
1219
+ method: "unwatchOrderBook",
1220
+ args,
1221
+ },
1222
+ );
1223
+ this.clearWsSubscription(ws, "watchOrderBook", args);
1142
1224
  } catch (error) {
1225
+ if (this.isWsTransportUnavailableError(error)) {
1226
+ throw this.wsTransportUnavailableError("unwatchOrderBook");
1227
+ }
1143
1228
  if (error instanceof PmxtError) throw error;
1144
1229
  throw new PmxtError(`Failed to unwatchOrderBook: ${error}`);
1145
1230
  }
@@ -1545,33 +1630,12 @@ export abstract class Exchange {
1545
1630
  args.push(params);
1546
1631
  }
1547
1632
 
1548
- // Try WebSocket transport first
1549
1633
  const wsData = await this.watchViaWs("watchOrderBook", args);
1550
1634
  if (wsData !== null) {
1551
1635
  return convertOrderBook(wsData);
1552
1636
  }
1553
1637
 
1554
- // HTTP fallback
1555
- try {
1556
- const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/watchOrderBook`, {
1557
- method: 'POST',
1558
- headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
1559
- body: JSON.stringify({ args, credentials: this.getCredentials() }),
1560
- });
1561
- if (!response.ok) {
1562
- const body = await response.json().catch(() => ({}));
1563
- if (body.error && typeof body.error === "object") {
1564
- throw fromServerError(body.error);
1565
- }
1566
- throw new PmxtError(body.error?.message || response.statusText);
1567
- }
1568
- const json = await response.json();
1569
- const data = this.handleResponse(json);
1570
- return convertOrderBook(data);
1571
- } catch (error) {
1572
- if (error instanceof PmxtError) throw error;
1573
- throw new PmxtError(`Failed to watch order book: ${error}`);
1574
- }
1638
+ throw this.wsTransportUnavailableError("watchOrderBook");
1575
1639
  }
1576
1640
 
1577
1641
  /**
@@ -1581,9 +1645,6 @@ export abstract class Exchange {
1581
1645
  * order book snapshot. Call repeatedly in a loop to stream updates
1582
1646
  * (CCXT Pro pattern).
1583
1647
  *
1584
- * Prefers the sidecar WebSocket transport when available, falling
1585
- * back to HTTP POST for older sidecars.
1586
- *
1587
1648
  * @param outcomeIds - Array of outcome IDs (or MarketOutcome objects)
1588
1649
  * @param limit - Optional depth limit for each order book
1589
1650
  * @param params - Optional exchange-specific parameters
@@ -1618,61 +1679,33 @@ export abstract class Exchange {
1618
1679
  args.push(params);
1619
1680
  }
1620
1681
 
1621
- // Try WebSocket transport first
1622
- const ws = await this.getOrCreateWs();
1623
- if (ws) {
1624
- try {
1625
- const rawResult = await ws.subscribeBatch(
1626
- this.exchangeName,
1627
- "watchOrderBooks",
1628
- args,
1629
- this.getCredentials() as Record<string, any> | undefined,
1630
- );
1631
- if (rawResult && typeof rawResult === "object") {
1632
- const result: Record<string, OrderBook> = {};
1633
- for (const [k, v] of Object.entries(rawResult)) {
1634
- if (v && typeof v === "object") {
1635
- result[k] = convertOrderBook(v);
1636
- }
1637
- }
1638
- return result;
1639
- }
1640
- } catch (error) {
1641
- // Only fall through to HTTP for transport-level WS failures
1642
- if (!(error instanceof PmxtError) || !/connection failed|no websocket/i.test(error.message)) {
1643
- throw error;
1644
- }
1682
+ try {
1683
+ const ws = await this.getOrCreateWs();
1684
+ if (!ws) {
1685
+ throw this.wsTransportUnavailableError("watchOrderBooks");
1645
1686
  }
1646
- }
1647
1687
 
1648
- // HTTP fallback
1649
- try {
1650
- const response = await this.fetchWithRetry(
1651
- `${this.resolveBaseUrl()}/api/${this.exchangeName}/watchOrderBooks`,
1652
- {
1653
- method: 'POST',
1654
- headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
1655
- body: JSON.stringify({ args, credentials: this.getCredentials() }),
1656
- },
1688
+ const rawResult = await ws.subscribeBatch(
1689
+ this.exchangeName,
1690
+ "watchOrderBooks",
1691
+ args,
1692
+ this.getCredentials() as Record<string, any> | undefined,
1657
1693
  );
1658
- if (!response.ok) {
1659
- const body = await response.json().catch(() => ({}));
1660
- if (body.error && typeof body.error === "object") {
1661
- throw fromServerError(body.error);
1662
- }
1663
- throw new PmxtError(body.error?.message || response.statusText);
1664
- }
1665
- const json = await response.json();
1666
- const data = this.handleResponse(json);
1667
- if (data && typeof data === "object") {
1694
+ if (rawResult && typeof rawResult === "object") {
1668
1695
  const result: Record<string, OrderBook> = {};
1669
- for (const [k, v] of Object.entries(data as Record<string, any>)) {
1670
- result[k] = convertOrderBook(v);
1696
+ for (const [k, v] of Object.entries(rawResult)) {
1697
+ if (v && typeof v === "object") {
1698
+ result[k] = convertOrderBook(v);
1699
+ }
1671
1700
  }
1672
1701
  return result;
1673
1702
  }
1703
+
1674
1704
  throw new PmxtError("watchOrderBooks: unexpected response shape from server");
1675
1705
  } catch (error) {
1706
+ if (this.isWsTransportUnavailableError(error)) {
1707
+ throw this.wsTransportUnavailableError("watchOrderBooks");
1708
+ }
1676
1709
  if (error instanceof PmxtError) throw error;
1677
1710
  throw new PmxtError(`Failed to watch order books: ${error}`);
1678
1711
  }
@@ -1714,7 +1747,7 @@ export abstract class Exchange {
1714
1747
  };
1715
1748
  }
1716
1749
 
1717
- throw new PmxtError("watchAllOrderBooks() requires WebSocket transport — connection failed");
1750
+ throw this.wsTransportUnavailableError("watchAllOrderBooks");
1718
1751
  }
1719
1752
 
1720
1753
  /** @deprecated Use {@link watchAllOrderBooks} instead. */
@@ -1753,37 +1786,23 @@ export abstract class Exchange {
1753
1786
  ): Promise<Trade[]> {
1754
1787
  await this.initPromise;
1755
1788
  const resolvedOutcomeId = resolveOutcomeId(outcomeId);
1756
- try {
1757
- const args: any[] = [resolvedOutcomeId];
1758
- if (address !== undefined) {
1759
- args.push(address);
1760
- }
1761
- if (since !== undefined) {
1762
- args.push(since);
1763
- }
1764
- if (limit !== undefined) {
1765
- args.push(limit);
1766
- }
1789
+ const args: any[] = [resolvedOutcomeId];
1790
+ if (address !== undefined) {
1791
+ args.push(address);
1792
+ }
1793
+ if (since !== undefined) {
1794
+ args.push(since);
1795
+ }
1796
+ if (limit !== undefined) {
1797
+ args.push(limit);
1798
+ }
1767
1799
 
1768
- const response = await this.fetchWithRetry(`${this.resolveBaseUrl()}/api/${this.exchangeName}/watchTrades`, {
1769
- method: 'POST',
1770
- headers: { 'Content-Type': 'application/json', ...this.getAuthHeaders() },
1771
- body: JSON.stringify({ args, credentials: this.getCredentials() }),
1772
- });
1773
- if (!response.ok) {
1774
- const body = await response.json().catch(() => ({}));
1775
- if (body.error && typeof body.error === "object") {
1776
- throw fromServerError(body.error);
1777
- }
1778
- throw new PmxtError(body.error?.message || response.statusText);
1779
- }
1780
- const json = await response.json();
1781
- const data = this.handleResponse(json);
1782
- return data.map(convertTrade);
1783
- } catch (error) {
1784
- if (error instanceof PmxtError) throw error;
1785
- throw new PmxtError(`Failed to watch trades: ${error}`);
1800
+ const wsData = await this.watchViaWs("watchTrades", args);
1801
+ if (wsData !== null) {
1802
+ return wsData.map(convertTrade);
1786
1803
  }
1804
+
1805
+ throw this.wsTransportUnavailableError("watchTrades");
1787
1806
  }
1788
1807
 
1789
1808
  /**
@@ -2760,6 +2779,21 @@ export class Hyperliquid extends Exchange {
2760
2779
  }
2761
2780
  }
2762
2781
 
2782
+ /**
2783
+ * SuiBets exchange client.
2784
+ *
2785
+ * @example
2786
+ * ```typescript
2787
+ * const suibets = new SuiBets();
2788
+ * const markets = await suibets.fetchMarkets();
2789
+ * ```
2790
+ */
2791
+ export class SuiBets extends Exchange {
2792
+ constructor(options: ExchangeOptions = {}) {
2793
+ super("suibets", options);
2794
+ }
2795
+ }
2796
+
2763
2797
  /**
2764
2798
  * Mock exchange client.
2765
2799
  *
package/pmxt/router.ts CHANGED
@@ -242,9 +242,9 @@ export class Router extends Exchange {
242
242
  const params = 'title' in marketOrParams ? { market: marketOrParams as UnifiedMarket } : marketOrParams;
243
243
  await this.initPromise;
244
244
  const query: Record<string, unknown> = {};
245
- const marketId = params.marketId ?? params.market?.marketId;
245
+ const marketId = params.marketId ?? (!params.market?.slug ? params.market?.marketId : undefined);
246
246
  if (marketId) query.marketId = marketId;
247
- if (params.slug) query.slug = params.slug;
247
+ if (params.slug ?? params.market?.slug) query.slug = params.slug ?? params.market?.slug;
248
248
  if (params.url) query.url = params.url;
249
249
  if (params.relation) query.relation = params.relation;
250
250
  if (params.minConfidence !== undefined) query.minConfidence = params.minConfidence;
@@ -255,6 +255,9 @@ export class Router extends Exchange {
255
255
  const json = await this.sidecarReadRequest('fetchMarketMatches', query, [query]);
256
256
  const data = this.handleResponse(json);
257
257
  if (!data) return [];
258
+ if (!Array.isArray(data)) {
259
+ throw new Error('fetchMarketMatches returned an unexpected response shape: expected an array');
260
+ }
258
261
  return (data as any[]).map(parseMatchResult);
259
262
  } catch (error) {
260
263
  if (error instanceof Error) throw error;
@@ -317,9 +320,9 @@ export class Router extends Exchange {
317
320
  const params = 'title' in eventOrParams && 'markets' in eventOrParams ? { event: eventOrParams as UnifiedEvent } : eventOrParams;
318
321
  await this.initPromise;
319
322
  const query: Record<string, unknown> = {};
320
- const eventId = params.eventId ?? params.event?.id;
323
+ const eventId = params.eventId ?? (!params.event?.slug ? params.event?.id : undefined);
321
324
  if (eventId) query.eventId = eventId;
322
- if (params.slug) query.slug = params.slug;
325
+ if (params.slug ?? params.event?.slug) query.slug = params.slug ?? params.event?.slug;
323
326
  if (params.relation) query.relation = params.relation;
324
327
  if (params.minConfidence !== undefined) query.minConfidence = params.minConfidence;
325
328
  if (params.limit !== undefined) query.limit = params.limit;
@@ -329,6 +332,9 @@ export class Router extends Exchange {
329
332
  const json = await this.sidecarReadRequest('fetchEventMatches', query, [query]);
330
333
  const data = this.handleResponse(json);
331
334
  if (!data) return [];
335
+ if (!Array.isArray(data)) {
336
+ throw new Error('fetchEventMatches returned an unexpected response shape: expected an array');
337
+ }
332
338
  return (data as any[]).map((entry) => {
333
339
  const event = convertEvent(entry.event || {});
334
340
  return {