pmxt-core 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.
Files changed (45) hide show
  1. package/dist/exchanges/kalshi/api.d.ts +1 -1
  2. package/dist/exchanges/kalshi/api.js +1 -1
  3. package/dist/exchanges/kalshi/fetcher.d.ts +11 -1
  4. package/dist/exchanges/kalshi/fetcher.js +49 -17
  5. package/dist/exchanges/kalshi/normalizer.d.ts +12 -0
  6. package/dist/exchanges/kalshi/normalizer.js +125 -1
  7. package/dist/exchanges/limitless/api.d.ts +1 -1
  8. package/dist/exchanges/limitless/api.js +1 -1
  9. package/dist/exchanges/mock/index.js +13 -2
  10. package/dist/exchanges/myriad/api.d.ts +1 -1
  11. package/dist/exchanges/myriad/api.js +1 -1
  12. package/dist/exchanges/opinion/api.d.ts +1 -1
  13. package/dist/exchanges/opinion/api.js +1 -1
  14. package/dist/exchanges/polymarket/api-clob.d.ts +1 -1
  15. package/dist/exchanges/polymarket/api-clob.js +1 -1
  16. package/dist/exchanges/polymarket/api-data.d.ts +1 -1
  17. package/dist/exchanges/polymarket/api-data.js +1 -1
  18. package/dist/exchanges/polymarket/api-gamma.d.ts +1 -1
  19. package/dist/exchanges/polymarket/api-gamma.js +1 -1
  20. package/dist/exchanges/polymarket/websocket.d.ts +12 -0
  21. package/dist/exchanges/polymarket/websocket.js +120 -14
  22. package/dist/exchanges/probable/api.d.ts +1 -1
  23. package/dist/exchanges/probable/api.js +1 -1
  24. package/dist/exchanges/suibets/api.d.ts +15 -0
  25. package/dist/exchanges/suibets/api.js +17 -0
  26. package/dist/exchanges/suibets/config.d.ts +16 -0
  27. package/dist/exchanges/suibets/config.js +34 -0
  28. package/dist/exchanges/suibets/errors.d.ts +16 -0
  29. package/dist/exchanges/suibets/errors.js +71 -0
  30. package/dist/exchanges/suibets/fetcher.d.ts +64 -0
  31. package/dist/exchanges/suibets/fetcher.js +128 -0
  32. package/dist/exchanges/suibets/index.d.ts +54 -0
  33. package/dist/exchanges/suibets/index.js +114 -0
  34. package/dist/exchanges/suibets/normalizer.d.ts +8 -0
  35. package/dist/exchanges/suibets/normalizer.js +102 -0
  36. package/dist/exchanges/suibets/utils.d.ts +63 -0
  37. package/dist/exchanges/suibets/utils.js +124 -0
  38. package/dist/index.d.ts +4 -0
  39. package/dist/index.js +5 -1
  40. package/dist/router/Router.js +12 -3
  41. package/dist/server/app.js +76 -1
  42. package/dist/server/exchange-factory.js +6 -0
  43. package/dist/server/openapi.yaml +7 -0
  44. package/dist/server/ws-handler.js +196 -23
  45. package/package.json +6 -6
@@ -4,6 +4,7 @@ exports.createWebSocketHandler = createWebSocketHandler;
4
4
  const ws_1 = require("ws");
5
5
  const errors_1 = require("../errors");
6
6
  const exchange_factory_1 = require("./exchange-factory");
7
+ const feed_factory_1 = require("./feed-factory");
7
8
  const logger_1 = require("../utils/logger");
8
9
  // ---------------------------------------------------------------------------
9
10
  // Streaming methods
@@ -14,6 +15,10 @@ const WATCH_METHODS = new Set([
14
15
  "watchOrderBooks",
15
16
  "watchTrades",
16
17
  ]);
18
+ /** Data-feed methods that produce streaming data. */
19
+ const FEED_WATCH_METHODS = new Set([
20
+ "watchTicker",
21
+ ]);
17
22
  /** Methods for unsubscribing. */
18
23
  const UNWATCH_METHODS = {
19
24
  unwatchOrderBook: "watchOrderBook",
@@ -30,7 +35,10 @@ function sendError(ws, id, message, code) {
30
35
  * Build a unique key for a subscription so we can cancel it.
31
36
  */
32
37
  function subscriptionKey(msg) {
33
- return `${msg.exchange}:${msg.method}:${JSON.stringify(msg.args)}`;
38
+ const target = msg.targetKind === "feed"
39
+ ? `feed:${msg.feed}`
40
+ : `exchange:${msg.exchange}`;
41
+ return `${target}:${msg.method}:${JSON.stringify(msg.args)}`;
34
42
  }
35
43
  /**
36
44
  * Start a streaming loop for a single-ticker watch method
@@ -101,6 +109,78 @@ async function streamBatch(exchange, method, args, id, ws, signal) {
101
109
  }
102
110
  }
103
111
  }
112
+ /**
113
+ * Start a streaming loop for feed watch methods.
114
+ *
115
+ * Data feeds expose callback-driven watch methods rather than the exchange
116
+ * layer's await-next-update methods, so lifecycle is tied to the returned
117
+ * unsubscribe callback and the client's AbortSignal.
118
+ */
119
+ async function streamFeedSingle(feed, feedName, method, args, id, ws, signal) {
120
+ const symbol = typeof args[0] === "string" ? args[0] : String(args[0] ?? "");
121
+ let unsubscribe;
122
+ let settled = false;
123
+ let resolveDone = () => { };
124
+ const done = new Promise((resolve) => {
125
+ resolveDone = resolve;
126
+ });
127
+ const cleanup = () => {
128
+ if (settled)
129
+ return;
130
+ settled = true;
131
+ signal.removeEventListener("abort", cleanup);
132
+ ws.removeListener("close", cleanup);
133
+ if (unsubscribe) {
134
+ try {
135
+ unsubscribe();
136
+ }
137
+ catch (err) {
138
+ logger_1.logger.warn('ws-handler: feed unsubscribe failed', {
139
+ feed: feedName,
140
+ method,
141
+ id,
142
+ error: err instanceof Error ? err.message : String(err),
143
+ });
144
+ }
145
+ }
146
+ resolveDone();
147
+ };
148
+ signal.addEventListener("abort", cleanup, { once: true });
149
+ ws.once("close", cleanup);
150
+ try {
151
+ if (typeof feed.connect === "function") {
152
+ await feed.connect();
153
+ }
154
+ if (signal.aborted || ws.readyState !== ws_1.WebSocket.OPEN) {
155
+ cleanup();
156
+ return;
157
+ }
158
+ if (method !== "watchTicker" || typeof feed.watchTicker !== "function") {
159
+ sendError(ws, id, `Method '${method}' not found on ${feedName}`);
160
+ cleanup();
161
+ return;
162
+ }
163
+ unsubscribe = feed.watchTicker(symbol, (data) => {
164
+ if (!signal.aborted && ws.readyState === ws_1.WebSocket.OPEN) {
165
+ send(ws, { id, event: "data", method, symbol, source: feedName, data });
166
+ }
167
+ });
168
+ send(ws, { event: "subscribed", id });
169
+ await done;
170
+ }
171
+ catch (err) {
172
+ if (!signal.aborted) {
173
+ const message = err instanceof errors_1.BaseError
174
+ ? err.message
175
+ : err instanceof Error
176
+ ? err.message
177
+ : "Unknown streaming error";
178
+ const code = err instanceof errors_1.BaseError ? err.code : undefined;
179
+ sendError(ws, id, message, code);
180
+ }
181
+ cleanup();
182
+ }
183
+ }
104
184
  // ---------------------------------------------------------------------------
105
185
  // Public API
106
186
  // ---------------------------------------------------------------------------
@@ -158,32 +238,75 @@ function createWebSocketHandler(options = {}) {
158
238
  const id = parsed.id;
159
239
  const action = parsed.action;
160
240
  const exchange = parsed.exchange;
241
+ const feed = parsed.feed;
161
242
  const method = parsed.method;
162
- if (!id || !action || !exchange || !method) {
163
- sendError(ws, id, "Missing required fields: id, action, exchange, method");
243
+ if (!id || !action) {
244
+ sendError(ws, id, "Missing required fields: id, action");
245
+ return;
246
+ }
247
+ if (action === "unsubscribe" && !method && !exchange && !feed) {
248
+ handleUnsubscribeById(ws, state, id);
249
+ return;
250
+ }
251
+ if (!method) {
252
+ sendError(ws, id, "Missing required field: method");
253
+ return;
254
+ }
255
+ if ((exchange && feed) || (!exchange && !feed)) {
256
+ sendError(ws, id, "Missing required target: provide exactly one of exchange or feed");
164
257
  return;
165
258
  }
166
- const exchangeName = exchange.toLowerCase();
167
259
  if (action === "subscribe") {
168
- const msg = {
169
- id,
170
- action: "subscribe",
171
- exchange: exchangeName,
172
- method,
173
- args: parsed.args || [],
174
- credentials: parsed.credentials,
175
- };
176
- handleSubscribe(ws, state, msg, exchangeName);
260
+ if (feed) {
261
+ const feedName = feed.toLowerCase();
262
+ const msg = {
263
+ id,
264
+ action: "subscribe",
265
+ targetKind: "feed",
266
+ feed: feedName,
267
+ method,
268
+ args: parsed.args || [],
269
+ };
270
+ handleFeedSubscribe(ws, state, msg, feedName);
271
+ }
272
+ else {
273
+ const exchangeName = exchange.toLowerCase();
274
+ const msg = {
275
+ id,
276
+ action: "subscribe",
277
+ targetKind: "exchange",
278
+ exchange: exchangeName,
279
+ method,
280
+ args: parsed.args || [],
281
+ credentials: parsed.credentials,
282
+ };
283
+ handleSubscribe(ws, state, msg, exchangeName);
284
+ }
177
285
  }
178
286
  else if (action === "unsubscribe") {
179
- const msg = {
180
- id,
181
- action: "unsubscribe",
182
- exchange: exchangeName,
183
- method,
184
- args: parsed.args || [],
185
- };
186
- handleUnsubscribe(ws, state, msg, exchangeName);
287
+ if (feed) {
288
+ const msg = {
289
+ id,
290
+ action: "unsubscribe",
291
+ targetKind: "feed",
292
+ feed: feed.toLowerCase(),
293
+ method,
294
+ args: parsed.args || [],
295
+ };
296
+ handleUnsubscribe(ws, state, msg);
297
+ }
298
+ else {
299
+ const exchangeName = exchange.toLowerCase();
300
+ const msg = {
301
+ id,
302
+ action: "unsubscribe",
303
+ targetKind: "exchange",
304
+ exchange: exchangeName,
305
+ method,
306
+ args: parsed.args || [],
307
+ };
308
+ handleUnsubscribe(ws, state, msg);
309
+ }
187
310
  }
188
311
  else {
189
312
  sendError(ws, id, `Unknown action: ${action}`);
@@ -257,7 +380,7 @@ function handleSubscribe(ws, state, msg, exchangeName) {
257
380
  return;
258
381
  }
259
382
  const abortController = new AbortController();
260
- state.subscriptions.set(key, { abortController });
383
+ state.subscriptions.set(key, { id, abortController });
261
384
  const streamFn = method === "watchOrderBooks" ? streamBatch : streamSingle;
262
385
  // Fire and forget -- the loop runs until aborted or WS closes
263
386
  streamFn(exchange, method, args || [], id, ws, abortController.signal).catch((err) => {
@@ -270,10 +393,45 @@ function handleSubscribe(ws, state, msg, exchangeName) {
270
393
  error: err instanceof Error ? err.message : String(err),
271
394
  });
272
395
  sendError(ws, id, err instanceof Error ? err.message : 'Streaming error');
396
+ }).finally(() => {
273
397
  state.subscriptions.delete(key);
274
398
  });
275
399
  }
276
- function handleUnsubscribe(ws, state, msg, exchangeName) {
400
+ function handleFeedSubscribe(ws, state, msg, feedName) {
401
+ const { id, method, args } = msg;
402
+ if (!FEED_WATCH_METHODS.has(method)) {
403
+ sendError(ws, id, `Method '${method}' is not a feed streaming method. Use the HTTP API for non-streaming calls.`);
404
+ return;
405
+ }
406
+ const key = subscriptionKey(msg);
407
+ if (state.subscriptions.has(key)) {
408
+ sendError(ws, id, `Already subscribed to ${key}`);
409
+ return;
410
+ }
411
+ let feed;
412
+ try {
413
+ feed = (0, feed_factory_1.getFeed)(feedName);
414
+ }
415
+ catch (err) {
416
+ const message = err instanceof Error ? err.message : "Failed to create feed";
417
+ sendError(ws, id, message);
418
+ return;
419
+ }
420
+ const abortController = new AbortController();
421
+ state.subscriptions.set(key, { id, abortController });
422
+ streamFeedSingle(feed, feedName, method, args || [], id, ws, abortController.signal).catch((err) => {
423
+ logger_1.logger.warn('ws-handler: feed stream ended with unexpected error', {
424
+ feed: feedName,
425
+ method,
426
+ id,
427
+ error: err instanceof Error ? err.message : String(err),
428
+ });
429
+ sendError(ws, id, err instanceof Error ? err.message : 'Streaming error');
430
+ }).finally(() => {
431
+ state.subscriptions.delete(key);
432
+ });
433
+ }
434
+ function handleUnsubscribe(ws, state, msg) {
277
435
  const { id } = msg;
278
436
  // Build the key of the subscription to cancel.
279
437
  // For unsubscribe actions, the corresponding subscribe key uses the watch method.
@@ -289,3 +447,18 @@ function handleUnsubscribe(ws, state, msg, exchangeName) {
289
447
  state.subscriptions.delete(key);
290
448
  send(ws, { event: "subscribed", id }); // Acknowledge unsubscribe
291
449
  }
450
+ function handleUnsubscribeById(ws, state, id) {
451
+ let cancelled = false;
452
+ for (const [key, sub] of state.subscriptions) {
453
+ if (sub.id === id) {
454
+ sub.abortController.abort();
455
+ state.subscriptions.delete(key);
456
+ cancelled = true;
457
+ }
458
+ }
459
+ if (!cancelled) {
460
+ sendError(ws, id, `No active subscription for id ${id}`);
461
+ return;
462
+ }
463
+ send(ws, { event: "subscribed", id });
464
+ }
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "pmxt-core",
3
- "version": "2.44.5",
3
+ "version": "2.45.0",
4
4
  "description": "pmxt is a unified prediction market data API. The ccxt for prediction markets.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/pmxt-dev/pmxt.git"
9
+ "url": "git+https://github.com/pmxt-dev/pmxt.git"
10
10
  },
11
11
  "bin": {
12
- "pmxt-server": "./dist/server/index.js",
13
- "pmxt-ensure-server": "./bin/pmxt-ensure-server"
12
+ "pmxt-server": "dist/server/index.js",
13
+ "pmxt-ensure-server": "bin/pmxt-ensure-server"
14
14
  },
15
15
  "files": [
16
16
  "dist",
@@ -29,8 +29,8 @@
29
29
  "test": "jest -c jest.config.js",
30
30
  "server": "tsx watch src/server/index.ts",
31
31
  "server:prod": "node dist/server/index.js",
32
- "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.44.5,library=urllib3",
33
- "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.44.5,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
32
+ "generate:sdk:python": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g python -o ../sdks/python/generated --package-name pmxt_internal --additional-properties=projectName=pmxt-internal,packageVersion=2.45.0,library=urllib3",
33
+ "generate:sdk:typescript": "npx @openapitools/openapi-generator-cli generate -i src/server/openapi.yaml -g typescript-fetch -o ../sdks/typescript/generated --additional-properties=npmName=pmxtjs,npmVersion=2.45.0,supportsES6=true,typescriptThreePlus=true && node ../sdks/typescript/scripts/fix-generated.js",
34
34
  "fetch:openapi": "node scripts/fetch-openapi-specs.js",
35
35
  "extract:jsdoc": "node ../scripts/extract-jsdoc.js",
36
36
  "generate:docs": "npm run extract:jsdoc && node ../scripts/generate-api-docs.js",