pmxtjs 2.40.5 → 2.41.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.
@@ -5,7 +5,7 @@
5
5
  * OpenAPI client, matching the Python API exactly.
6
6
  */
7
7
  import { Configuration, DefaultApi, ExchangeCredentials } from "../generated/src/index.js";
8
- import { Balance, BuiltOrder, CreateOrderParams, EventFetchParams, EventFilterCriteria, EventFilterFunction, ExecutionPriceResult, MarketFetchParams, MarketFilterCriteria, MarketFilterFunction, MarketOutcome, MyTradesParams, Order, OrderBook, OrderHistoryParams, PaginatedMarketsResult, Position, PriceCandle, SubscribedAddressSnapshot, SubscriptionOption, Trade, UnifiedEvent, UnifiedMarket, UserTrade } from "./models.js";
8
+ import { Balance, BuiltOrder, CreateOrderParams, EventFetchParams, EventFilterCriteria, EventFilterFunction, ExecutionPriceResult, MarketFetchParams, MarketFilterCriteria, MarketFilterFunction, MarketOutcome, MyTradesParams, Order, OrderBook, OrderHistoryParams, PaginatedMarketsResult, Position, PriceCandle, SubscribedAddressSnapshot, SubscriptionOption, Trade, UnifiedEvent, UnifiedMarket, UserTrade, FirehoseEvent } from "./models.js";
9
9
  import { ServerManager } from "./server-manager.js";
10
10
  /**
11
11
  * Base exchange client options.
@@ -252,6 +252,26 @@ export declare abstract class Exchange {
252
252
  * ```
253
253
  */
254
254
  watchOrderBooks(outcomeIds: (string | MarketOutcome)[], limit?: number): Promise<Record<string, OrderBook>>;
255
+ /**
256
+ * Stream all orderbook updates across venues via the hosted WebSocket API.
257
+ *
258
+ * Returns a promise that resolves with the next book event.
259
+ * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
260
+ * Requires hosted mode (`pmxtApiKey` set).
261
+ *
262
+ * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
263
+ * @returns Next firehose event with source, symbol, and orderbook
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" });
268
+ * while (true) {
269
+ * const event = await poly.firehose();
270
+ * console.log(event.source, event.symbol, event.orderbook.bids[0]);
271
+ * }
272
+ * ```
273
+ */
274
+ firehose(venues?: string[]): Promise<FirehoseEvent>;
255
275
  /**
256
276
  * Watch real-time trade updates via WebSocket.
257
277
  *
@@ -188,9 +188,14 @@ export class Exchange {
188
188
  this.api = new DefaultApi(this.config);
189
189
  }
190
190
  catch (error) {
191
- throw new PmxtError(`Failed to start PMXT server: ${error}\n\n` +
191
+ const msg = `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
192
192
  `Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n` +
193
- `Or start the server manually: pmxt-server`);
193
+ `Or start the server manually: pmxt-server`;
194
+ const pmxtError = new PmxtError(msg);
195
+ if (error instanceof Error) {
196
+ pmxtError.cause = error;
197
+ }
198
+ throw pmxtError;
194
199
  }
195
200
  }
196
201
  }
@@ -289,8 +294,11 @@ export class Exchange {
289
294
  if (this._wsClient?.connected)
290
295
  return this._wsClient;
291
296
  const host = this.resolveBaseUrl();
292
- const accessToken = this.serverManager.getAccessToken();
293
- const client = new SidecarWsClient(host, accessToken || undefined);
297
+ const accessToken = this.isHosted
298
+ ? this.pmxtApiKey
299
+ : this.serverManager.getAccessToken();
300
+ const authParamName = this.isHosted ? "apiKey" : "token";
301
+ const client = new SidecarWsClient(host, accessToken || undefined, authParamName);
294
302
  try {
295
303
  // Trigger connection to validate the endpoint exists.
296
304
  // subscribe() calls ensureConnected internally, but we want
@@ -325,8 +333,12 @@ export class Exchange {
325
333
  try {
326
334
  return await ws.subscribe(this.exchangeName, method, args, this.getCredentials());
327
335
  }
328
- catch {
329
- return null;
336
+ catch (error) {
337
+ // Only fall back to HTTP for transport-level failures
338
+ if (error instanceof PmxtError && /connection failed|no websocket/i.test(error.message)) {
339
+ return null;
340
+ }
341
+ throw error;
330
342
  }
331
343
  }
332
344
  // Low-Level API Access
@@ -1423,8 +1435,11 @@ export class Exchange {
1423
1435
  return result;
1424
1436
  }
1425
1437
  }
1426
- catch {
1427
- // fall through to HTTP
1438
+ catch (error) {
1439
+ // Only fall through to HTTP for transport-level WS failures
1440
+ if (!(error instanceof PmxtError) || !/connection failed|no websocket/i.test(error.message)) {
1441
+ throw error;
1442
+ }
1428
1443
  }
1429
1444
  }
1430
1445
  // HTTP fallback
@@ -1450,7 +1465,7 @@ export class Exchange {
1450
1465
  }
1451
1466
  return result;
1452
1467
  }
1453
- return {};
1468
+ throw new PmxtError("watchOrderBooks: unexpected response shape from server");
1454
1469
  }
1455
1470
  catch (error) {
1456
1471
  if (error instanceof PmxtError)
@@ -1458,6 +1473,41 @@ export class Exchange {
1458
1473
  throw new PmxtError(`Failed to watch order books: ${error}`);
1459
1474
  }
1460
1475
  }
1476
+ /**
1477
+ * Stream all orderbook updates across venues via the hosted WebSocket API.
1478
+ *
1479
+ * Returns a promise that resolves with the next book event.
1480
+ * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1481
+ * Requires hosted mode (`pmxtApiKey` set).
1482
+ *
1483
+ * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1484
+ * @returns Next firehose event with source, symbol, and orderbook
1485
+ *
1486
+ * @example
1487
+ * ```typescript
1488
+ * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" });
1489
+ * while (true) {
1490
+ * const event = await poly.firehose();
1491
+ * console.log(event.source, event.symbol, event.orderbook.bids[0]);
1492
+ * }
1493
+ * ```
1494
+ */
1495
+ async firehose(venues) {
1496
+ await this.initPromise;
1497
+ if (!this.isHosted) {
1498
+ throw new PmxtError("firehose() requires hosted mode (set pmxtApiKey)");
1499
+ }
1500
+ const args = venues ? [venues] : [];
1501
+ const wsData = await this.watchViaWs("firehose", args);
1502
+ if (wsData !== null) {
1503
+ return {
1504
+ source: wsData._source || "",
1505
+ symbol: wsData._symbol || "",
1506
+ orderbook: convertOrderBook(wsData),
1507
+ };
1508
+ }
1509
+ throw new PmxtError("firehose() requires WebSocket transport — connection failed");
1510
+ }
1461
1511
  /**
1462
1512
  * Watch real-time trade updates via WebSocket.
1463
1513
  *
@@ -112,6 +112,17 @@ export interface OrderBook {
112
112
  /** Unix timestamp (milliseconds) */
113
113
  timestamp?: number;
114
114
  }
115
+ /**
116
+ * A single event from the firehose stream.
117
+ */
118
+ export interface FirehoseEvent {
119
+ /** The venue this event originated from (e.g. "polymarket", "limitless") */
120
+ source: string;
121
+ /** The outcome token id / asset id */
122
+ symbol: string;
123
+ /** The order book snapshot */
124
+ orderbook: OrderBook;
125
+ }
115
126
  /**
116
127
  * Result of an execution price calculation.
117
128
  */
@@ -16,6 +16,7 @@ export declare class SidecarWsClient {
16
16
  private ws;
17
17
  private host;
18
18
  private accessToken;
19
+ private authParamName;
19
20
  private closed;
20
21
  /** requestId -> latest data payload */
21
22
  private dataStore;
@@ -24,7 +25,7 @@ export declare class SidecarWsClient {
24
25
  /** (method:symbolKey) -> requestId -- avoids duplicate subscribes */
25
26
  private activeSubs;
26
27
  private connectPromise;
27
- constructor(host: string, accessToken?: string);
28
+ constructor(host: string, accessToken?: string, authParamName?: string);
28
29
  private ensureConnected;
29
30
  private connect;
30
31
  private getWebSocketConstructor;
@@ -17,6 +17,7 @@ export class SidecarWsClient {
17
17
  ws = null;
18
18
  host;
19
19
  accessToken;
20
+ authParamName;
20
21
  closed = false;
21
22
  /** requestId -> latest data payload */
22
23
  dataStore = new Map();
@@ -25,9 +26,10 @@ export class SidecarWsClient {
25
26
  /** (method:symbolKey) -> requestId -- avoids duplicate subscribes */
26
27
  activeSubs = new Map();
27
28
  connectPromise = null;
28
- constructor(host, accessToken) {
29
+ constructor(host, accessToken, authParamName = "token") {
29
30
  this.host = host;
30
31
  this.accessToken = accessToken;
32
+ this.authParamName = authParamName;
31
33
  }
32
34
  // ------------------------------------------------------------------
33
35
  // Connection lifecycle
@@ -58,7 +60,7 @@ export class SidecarWsClient {
58
60
  }
59
61
  let url = `${scheme}://${hostPart}/ws`;
60
62
  if (this.accessToken) {
61
- url = `${url}?token=${this.accessToken}`;
63
+ url = `${url}?${this.authParamName}=${this.accessToken}`;
62
64
  }
63
65
  // Use the ws package in Node.js, native WebSocket in browsers
64
66
  const WsConstructor = this.getWebSocketConstructor();
@@ -77,22 +79,30 @@ export class SidecarWsClient {
77
79
  // Connection failed during handshake
78
80
  reject(new PmxtError(`WebSocket connection failed: ${err.message || err}`));
79
81
  }
82
+ else {
83
+ // Post-handshake error — propagate to all pending subscribers
84
+ const error = new PmxtError(`WebSocket error: ${err.message || err}`);
85
+ for (const sub of this.subscriptions.values()) {
86
+ if (sub.reject) {
87
+ sub.reject(error);
88
+ sub.reject = null;
89
+ sub.resolve = null;
90
+ }
91
+ }
92
+ this.closed = true;
93
+ this.ws = null;
94
+ }
80
95
  };
81
96
  ws.onclose = () => {
82
97
  this.closed = true;
83
98
  this.ws = null;
84
99
  };
85
100
  ws.onmessage = (event) => {
86
- try {
87
- const data = typeof event.data === "string"
88
- ? event.data
89
- : event.data.toString();
90
- const msg = JSON.parse(data);
91
- this.dispatch(msg);
92
- }
93
- catch {
94
- // Ignore unparseable frames
95
- }
101
+ const raw = typeof event.data === "string"
102
+ ? event.data
103
+ : event.data.toString();
104
+ const msg = JSON.parse(raw);
105
+ this.dispatch(msg);
96
106
  };
97
107
  });
98
108
  }
@@ -101,14 +111,15 @@ export class SidecarWsClient {
101
111
  if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
102
112
  return globalThis.WebSocket;
103
113
  }
104
- // Node.js -- try to require ws
114
+ // Node.js -- require ws
105
115
  try {
106
116
  // Dynamic require to avoid bundler issues
107
117
  const wsModule = require("ws");
108
118
  return wsModule.default || wsModule;
109
119
  }
110
120
  catch {
111
- return null;
121
+ throw new PmxtError("WebSocket support in Node.js requires the 'ws' package. " +
122
+ "Install it with: npm install ws");
112
123
  }
113
124
  }
114
125
  dispatch(msg) {
@@ -5,7 +5,7 @@
5
5
  * OpenAPI client, matching the Python API exactly.
6
6
  */
7
7
  import { Configuration, DefaultApi, ExchangeCredentials } from "../generated/src/index.js";
8
- import { Balance, BuiltOrder, CreateOrderParams, EventFetchParams, EventFilterCriteria, EventFilterFunction, ExecutionPriceResult, MarketFetchParams, MarketFilterCriteria, MarketFilterFunction, MarketOutcome, MyTradesParams, Order, OrderBook, OrderHistoryParams, PaginatedMarketsResult, Position, PriceCandle, SubscribedAddressSnapshot, SubscriptionOption, Trade, UnifiedEvent, UnifiedMarket, UserTrade } from "./models.js";
8
+ import { Balance, BuiltOrder, CreateOrderParams, EventFetchParams, EventFilterCriteria, EventFilterFunction, ExecutionPriceResult, MarketFetchParams, MarketFilterCriteria, MarketFilterFunction, MarketOutcome, MyTradesParams, Order, OrderBook, OrderHistoryParams, PaginatedMarketsResult, Position, PriceCandle, SubscribedAddressSnapshot, SubscriptionOption, Trade, UnifiedEvent, UnifiedMarket, UserTrade, FirehoseEvent } from "./models.js";
9
9
  import { ServerManager } from "./server-manager.js";
10
10
  /**
11
11
  * Base exchange client options.
@@ -252,6 +252,26 @@ export declare abstract class Exchange {
252
252
  * ```
253
253
  */
254
254
  watchOrderBooks(outcomeIds: (string | MarketOutcome)[], limit?: number): Promise<Record<string, OrderBook>>;
255
+ /**
256
+ * Stream all orderbook updates across venues via the hosted WebSocket API.
257
+ *
258
+ * Returns a promise that resolves with the next book event.
259
+ * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
260
+ * Requires hosted mode (`pmxtApiKey` set).
261
+ *
262
+ * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
263
+ * @returns Next firehose event with source, symbol, and orderbook
264
+ *
265
+ * @example
266
+ * ```typescript
267
+ * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" });
268
+ * while (true) {
269
+ * const event = await poly.firehose();
270
+ * console.log(event.source, event.symbol, event.orderbook.bids[0]);
271
+ * }
272
+ * ```
273
+ */
274
+ firehose(venues?: string[]): Promise<FirehoseEvent>;
255
275
  /**
256
276
  * Watch real-time trade updates via WebSocket.
257
277
  *
@@ -191,9 +191,14 @@ class Exchange {
191
191
  this.api = new index_js_1.DefaultApi(this.config);
192
192
  }
193
193
  catch (error) {
194
- throw new errors_js_1.PmxtError(`Failed to start PMXT server: ${error}\n\n` +
194
+ const msg = `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
195
195
  `Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n` +
196
- `Or start the server manually: pmxt-server`);
196
+ `Or start the server manually: pmxt-server`;
197
+ const pmxtError = new errors_js_1.PmxtError(msg);
198
+ if (error instanceof Error) {
199
+ pmxtError.cause = error;
200
+ }
201
+ throw pmxtError;
197
202
  }
198
203
  }
199
204
  }
@@ -292,8 +297,11 @@ class Exchange {
292
297
  if (this._wsClient?.connected)
293
298
  return this._wsClient;
294
299
  const host = this.resolveBaseUrl();
295
- const accessToken = this.serverManager.getAccessToken();
296
- const client = new ws_client_js_1.SidecarWsClient(host, accessToken || undefined);
300
+ const accessToken = this.isHosted
301
+ ? this.pmxtApiKey
302
+ : this.serverManager.getAccessToken();
303
+ const authParamName = this.isHosted ? "apiKey" : "token";
304
+ const client = new ws_client_js_1.SidecarWsClient(host, accessToken || undefined, authParamName);
297
305
  try {
298
306
  // Trigger connection to validate the endpoint exists.
299
307
  // subscribe() calls ensureConnected internally, but we want
@@ -328,8 +336,12 @@ class Exchange {
328
336
  try {
329
337
  return await ws.subscribe(this.exchangeName, method, args, this.getCredentials());
330
338
  }
331
- catch {
332
- return null;
339
+ catch (error) {
340
+ // Only fall back to HTTP for transport-level failures
341
+ if (error instanceof errors_js_1.PmxtError && /connection failed|no websocket/i.test(error.message)) {
342
+ return null;
343
+ }
344
+ throw error;
333
345
  }
334
346
  }
335
347
  // Low-Level API Access
@@ -1426,8 +1438,11 @@ class Exchange {
1426
1438
  return result;
1427
1439
  }
1428
1440
  }
1429
- catch {
1430
- // fall through to HTTP
1441
+ catch (error) {
1442
+ // Only fall through to HTTP for transport-level WS failures
1443
+ if (!(error instanceof errors_js_1.PmxtError) || !/connection failed|no websocket/i.test(error.message)) {
1444
+ throw error;
1445
+ }
1431
1446
  }
1432
1447
  }
1433
1448
  // HTTP fallback
@@ -1453,7 +1468,7 @@ class Exchange {
1453
1468
  }
1454
1469
  return result;
1455
1470
  }
1456
- return {};
1471
+ throw new errors_js_1.PmxtError("watchOrderBooks: unexpected response shape from server");
1457
1472
  }
1458
1473
  catch (error) {
1459
1474
  if (error instanceof errors_js_1.PmxtError)
@@ -1461,6 +1476,41 @@ class Exchange {
1461
1476
  throw new errors_js_1.PmxtError(`Failed to watch order books: ${error}`);
1462
1477
  }
1463
1478
  }
1479
+ /**
1480
+ * Stream all orderbook updates across venues via the hosted WebSocket API.
1481
+ *
1482
+ * Returns a promise that resolves with the next book event.
1483
+ * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1484
+ * Requires hosted mode (`pmxtApiKey` set).
1485
+ *
1486
+ * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1487
+ * @returns Next firehose event with source, symbol, and orderbook
1488
+ *
1489
+ * @example
1490
+ * ```typescript
1491
+ * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" });
1492
+ * while (true) {
1493
+ * const event = await poly.firehose();
1494
+ * console.log(event.source, event.symbol, event.orderbook.bids[0]);
1495
+ * }
1496
+ * ```
1497
+ */
1498
+ async firehose(venues) {
1499
+ await this.initPromise;
1500
+ if (!this.isHosted) {
1501
+ throw new errors_js_1.PmxtError("firehose() requires hosted mode (set pmxtApiKey)");
1502
+ }
1503
+ const args = venues ? [venues] : [];
1504
+ const wsData = await this.watchViaWs("firehose", args);
1505
+ if (wsData !== null) {
1506
+ return {
1507
+ source: wsData._source || "",
1508
+ symbol: wsData._symbol || "",
1509
+ orderbook: convertOrderBook(wsData),
1510
+ };
1511
+ }
1512
+ throw new errors_js_1.PmxtError("firehose() requires WebSocket transport — connection failed");
1513
+ }
1464
1514
  /**
1465
1515
  * Watch real-time trade updates via WebSocket.
1466
1516
  *
@@ -112,6 +112,17 @@ export interface OrderBook {
112
112
  /** Unix timestamp (milliseconds) */
113
113
  timestamp?: number;
114
114
  }
115
+ /**
116
+ * A single event from the firehose stream.
117
+ */
118
+ export interface FirehoseEvent {
119
+ /** The venue this event originated from (e.g. "polymarket", "limitless") */
120
+ source: string;
121
+ /** The outcome token id / asset id */
122
+ symbol: string;
123
+ /** The order book snapshot */
124
+ orderbook: OrderBook;
125
+ }
115
126
  /**
116
127
  * Result of an execution price calculation.
117
128
  */
@@ -16,6 +16,7 @@ export declare class SidecarWsClient {
16
16
  private ws;
17
17
  private host;
18
18
  private accessToken;
19
+ private authParamName;
19
20
  private closed;
20
21
  /** requestId -> latest data payload */
21
22
  private dataStore;
@@ -24,7 +25,7 @@ export declare class SidecarWsClient {
24
25
  /** (method:symbolKey) -> requestId -- avoids duplicate subscribes */
25
26
  private activeSubs;
26
27
  private connectPromise;
27
- constructor(host: string, accessToken?: string);
28
+ constructor(host: string, accessToken?: string, authParamName?: string);
28
29
  private ensureConnected;
29
30
  private connect;
30
31
  private getWebSocketConstructor;
@@ -20,6 +20,7 @@ class SidecarWsClient {
20
20
  ws = null;
21
21
  host;
22
22
  accessToken;
23
+ authParamName;
23
24
  closed = false;
24
25
  /** requestId -> latest data payload */
25
26
  dataStore = new Map();
@@ -28,9 +29,10 @@ class SidecarWsClient {
28
29
  /** (method:symbolKey) -> requestId -- avoids duplicate subscribes */
29
30
  activeSubs = new Map();
30
31
  connectPromise = null;
31
- constructor(host, accessToken) {
32
+ constructor(host, accessToken, authParamName = "token") {
32
33
  this.host = host;
33
34
  this.accessToken = accessToken;
35
+ this.authParamName = authParamName;
34
36
  }
35
37
  // ------------------------------------------------------------------
36
38
  // Connection lifecycle
@@ -61,7 +63,7 @@ class SidecarWsClient {
61
63
  }
62
64
  let url = `${scheme}://${hostPart}/ws`;
63
65
  if (this.accessToken) {
64
- url = `${url}?token=${this.accessToken}`;
66
+ url = `${url}?${this.authParamName}=${this.accessToken}`;
65
67
  }
66
68
  // Use the ws package in Node.js, native WebSocket in browsers
67
69
  const WsConstructor = this.getWebSocketConstructor();
@@ -80,22 +82,30 @@ class SidecarWsClient {
80
82
  // Connection failed during handshake
81
83
  reject(new errors_js_1.PmxtError(`WebSocket connection failed: ${err.message || err}`));
82
84
  }
85
+ else {
86
+ // Post-handshake error — propagate to all pending subscribers
87
+ const error = new errors_js_1.PmxtError(`WebSocket error: ${err.message || err}`);
88
+ for (const sub of this.subscriptions.values()) {
89
+ if (sub.reject) {
90
+ sub.reject(error);
91
+ sub.reject = null;
92
+ sub.resolve = null;
93
+ }
94
+ }
95
+ this.closed = true;
96
+ this.ws = null;
97
+ }
83
98
  };
84
99
  ws.onclose = () => {
85
100
  this.closed = true;
86
101
  this.ws = null;
87
102
  };
88
103
  ws.onmessage = (event) => {
89
- try {
90
- const data = typeof event.data === "string"
91
- ? event.data
92
- : event.data.toString();
93
- const msg = JSON.parse(data);
94
- this.dispatch(msg);
95
- }
96
- catch {
97
- // Ignore unparseable frames
98
- }
104
+ const raw = typeof event.data === "string"
105
+ ? event.data
106
+ : event.data.toString();
107
+ const msg = JSON.parse(raw);
108
+ this.dispatch(msg);
99
109
  };
100
110
  });
101
111
  }
@@ -104,14 +114,15 @@ class SidecarWsClient {
104
114
  if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
105
115
  return globalThis.WebSocket;
106
116
  }
107
- // Node.js -- try to require ws
117
+ // Node.js -- require ws
108
118
  try {
109
119
  // Dynamic require to avoid bundler issues
110
120
  const wsModule = require("ws");
111
121
  return wsModule.default || wsModule;
112
122
  }
113
123
  catch {
114
- return null;
124
+ throw new errors_js_1.PmxtError("WebSocket support in Node.js requires the 'ws' package. " +
125
+ "Install it with: npm install ws");
115
126
  }
116
127
  }
117
128
  dispatch(msg) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.40.5",
3
+ "version": "2.41.0",
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.40.5",
3
+ "version": "2.41.0",
4
4
  "description": "Unified prediction market data API - The ccxt for prediction markets",
5
5
  "author": "PMXT Contributors",
6
6
  "repository": {
@@ -43,7 +43,8 @@
43
43
  "unified"
44
44
  ],
45
45
  "dependencies": {
46
- "pmxt-core": "2.40.5"
46
+ "pmxt-core": "2.41.0",
47
+ "ws": "^8.18.0"
47
48
  },
48
49
  "devDependencies": {
49
50
  "@types/jest": "^30.0.0",
package/pmxt/client.ts CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  UnifiedEvent,
43
43
  UnifiedMarket,
44
44
  UserTrade,
45
+ FirehoseEvent,
45
46
  } from "./models.js";
46
47
 
47
48
  import { ServerManager } from "./server-manager.js";
@@ -296,11 +297,15 @@ export abstract class Exchange {
296
297
  });
297
298
  this.api = new DefaultApi(this.config);
298
299
  } catch (error) {
299
- throw new PmxtError(
300
- `Failed to start PMXT server: ${error}\n\n` +
300
+ const msg =
301
+ `Failed to start PMXT server: ${error instanceof Error ? error.message : error}\n\n` +
301
302
  `Please ensure 'pmxt-core' is installed: npm install -g pmxt-core\n` +
302
- `Or start the server manually: pmxt-server`
303
- );
303
+ `Or start the server manually: pmxt-server`;
304
+ const pmxtError = new PmxtError(msg);
305
+ if (error instanceof Error) {
306
+ (pmxtError as any).cause = error;
307
+ }
308
+ throw pmxtError;
304
309
  }
305
310
  }
306
311
  }
@@ -408,9 +413,12 @@ export abstract class Exchange {
408
413
  if (this._wsClient?.connected) return this._wsClient;
409
414
 
410
415
  const host = this.resolveBaseUrl();
411
- const accessToken = this.serverManager.getAccessToken();
416
+ const accessToken = this.isHosted
417
+ ? this.pmxtApiKey
418
+ : this.serverManager.getAccessToken();
419
+ const authParamName = this.isHosted ? "apiKey" : "token";
412
420
 
413
- const client = new SidecarWsClient(host, accessToken || undefined);
421
+ const client = new SidecarWsClient(host, accessToken || undefined, authParamName);
414
422
  try {
415
423
  // Trigger connection to validate the endpoint exists.
416
424
  // subscribe() calls ensureConnected internally, but we want
@@ -459,8 +467,12 @@ export abstract class Exchange {
459
467
  args,
460
468
  this.getCredentials() as Record<string, any> | undefined,
461
469
  );
462
- } catch {
463
- return null;
470
+ } catch (error) {
471
+ // Only fall back to HTTP for transport-level failures
472
+ if (error instanceof PmxtError && /connection failed|no websocket/i.test(error.message)) {
473
+ return null;
474
+ }
475
+ throw error;
464
476
  }
465
477
  }
466
478
 
@@ -1540,8 +1552,11 @@ export abstract class Exchange {
1540
1552
  }
1541
1553
  return result;
1542
1554
  }
1543
- } catch {
1544
- // fall through to HTTP
1555
+ } catch (error) {
1556
+ // Only fall through to HTTP for transport-level WS failures
1557
+ if (!(error instanceof PmxtError) || !/connection failed|no websocket/i.test(error.message)) {
1558
+ throw error;
1559
+ }
1545
1560
  }
1546
1561
  }
1547
1562
 
@@ -1571,13 +1586,52 @@ export abstract class Exchange {
1571
1586
  }
1572
1587
  return result;
1573
1588
  }
1574
- return {};
1589
+ throw new PmxtError("watchOrderBooks: unexpected response shape from server");
1575
1590
  } catch (error) {
1576
1591
  if (error instanceof PmxtError) throw error;
1577
1592
  throw new PmxtError(`Failed to watch order books: ${error}`);
1578
1593
  }
1579
1594
  }
1580
1595
 
1596
+ /**
1597
+ * Stream all orderbook updates across venues via the hosted WebSocket API.
1598
+ *
1599
+ * Returns a promise that resolves with the next book event.
1600
+ * Call repeatedly in a loop to stream updates (CCXT Pro pattern).
1601
+ * Requires hosted mode (`pmxtApiKey` set).
1602
+ *
1603
+ * @param venues - Optional venue filter (e.g. ["polymarket", "limitless"])
1604
+ * @returns Next firehose event with source, symbol, and orderbook
1605
+ *
1606
+ * @example
1607
+ * ```typescript
1608
+ * const poly = new Polymarket({ pmxtApiKey: "pmxt_xxx" });
1609
+ * while (true) {
1610
+ * const event = await poly.firehose();
1611
+ * console.log(event.source, event.symbol, event.orderbook.bids[0]);
1612
+ * }
1613
+ * ```
1614
+ */
1615
+ async firehose(venues?: string[]): Promise<FirehoseEvent> {
1616
+ await this.initPromise;
1617
+
1618
+ if (!this.isHosted) {
1619
+ throw new PmxtError("firehose() requires hosted mode (set pmxtApiKey)");
1620
+ }
1621
+
1622
+ const args: any[] = venues ? [venues] : [];
1623
+ const wsData = await this.watchViaWs("firehose", args);
1624
+ if (wsData !== null) {
1625
+ return {
1626
+ source: (wsData as any)._source || "",
1627
+ symbol: (wsData as any)._symbol || "",
1628
+ orderbook: convertOrderBook(wsData),
1629
+ };
1630
+ }
1631
+
1632
+ throw new PmxtError("firehose() requires WebSocket transport — connection failed");
1633
+ }
1634
+
1581
1635
  /**
1582
1636
  * Watch real-time trade updates via WebSocket.
1583
1637
  *
package/pmxt/models.ts CHANGED
@@ -153,6 +153,20 @@ export interface OrderBook {
153
153
  timestamp?: number;
154
154
  }
155
155
 
156
+ /**
157
+ * A single event from the firehose stream.
158
+ */
159
+ export interface FirehoseEvent {
160
+ /** The venue this event originated from (e.g. "polymarket", "limitless") */
161
+ source: string;
162
+
163
+ /** The outcome token id / asset id */
164
+ symbol: string;
165
+
166
+ /** The order book snapshot */
167
+ orderbook: OrderBook;
168
+ }
169
+
156
170
  /**
157
171
  * Result of an execution price calculation.
158
172
  */
package/pmxt/ws-client.ts CHANGED
@@ -36,6 +36,7 @@ export class SidecarWsClient {
36
36
  private ws: WebSocket | null = null;
37
37
  private host: string;
38
38
  private accessToken: string | undefined;
39
+ private authParamName: string;
39
40
  private closed = false;
40
41
 
41
42
  /** requestId -> latest data payload */
@@ -47,9 +48,10 @@ export class SidecarWsClient {
47
48
 
48
49
  private connectPromise: Promise<void> | null = null;
49
50
 
50
- constructor(host: string, accessToken?: string) {
51
+ constructor(host: string, accessToken?: string, authParamName: string = "token") {
51
52
  this.host = host;
52
53
  this.accessToken = accessToken;
54
+ this.authParamName = authParamName;
53
55
  }
54
56
 
55
57
  // ------------------------------------------------------------------
@@ -81,7 +83,7 @@ export class SidecarWsClient {
81
83
 
82
84
  let url = `${scheme}://${hostPart}/ws`;
83
85
  if (this.accessToken) {
84
- url = `${url}?token=${this.accessToken}`;
86
+ url = `${url}?${this.authParamName}=${this.accessToken}`;
85
87
  }
86
88
 
87
89
  // Use the ws package in Node.js, native WebSocket in browsers
@@ -103,6 +105,18 @@ export class SidecarWsClient {
103
105
  if (!this.ws) {
104
106
  // Connection failed during handshake
105
107
  reject(new PmxtError(`WebSocket connection failed: ${err.message || err}`));
108
+ } else {
109
+ // Post-handshake error — propagate to all pending subscribers
110
+ const error = new PmxtError(`WebSocket error: ${err.message || err}`);
111
+ for (const sub of this.subscriptions.values()) {
112
+ if (sub.reject) {
113
+ sub.reject(error);
114
+ sub.reject = null;
115
+ sub.resolve = null;
116
+ }
117
+ }
118
+ this.closed = true;
119
+ this.ws = null;
106
120
  }
107
121
  };
108
122
 
@@ -112,15 +126,11 @@ export class SidecarWsClient {
112
126
  };
113
127
 
114
128
  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
- }
129
+ const raw = typeof event.data === "string"
130
+ ? event.data
131
+ : event.data.toString();
132
+ const msg: WsMessage = JSON.parse(raw);
133
+ this.dispatch(msg);
124
134
  };
125
135
  });
126
136
  }
@@ -130,13 +140,16 @@ export class SidecarWsClient {
130
140
  if (typeof globalThis !== "undefined" && (globalThis as any).WebSocket) {
131
141
  return (globalThis as any).WebSocket;
132
142
  }
133
- // Node.js -- try to require ws
143
+ // Node.js -- require ws
134
144
  try {
135
145
  // Dynamic require to avoid bundler issues
136
146
  const wsModule = require("ws");
137
147
  return wsModule.default || wsModule;
138
148
  } catch {
139
- return null;
149
+ throw new PmxtError(
150
+ "WebSocket support in Node.js requires the 'ws' package. " +
151
+ "Install it with: npm install ws"
152
+ );
140
153
  }
141
154
  }
142
155