pmxtjs 2.25.2 → 2.26.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.
@@ -1619,3 +1619,73 @@ export class Baozi extends Exchange {
1619
1619
  super("baozi", options);
1620
1620
  }
1621
1621
  }
1622
+ /**
1623
+ * Opinion exchange client.
1624
+ *
1625
+ * Polygon-based CLOB exchange. Public catalog endpoints work without
1626
+ * credentials; trading requires `apiKey` (proxy address) and `privateKey`.
1627
+ *
1628
+ * @example
1629
+ * ```typescript
1630
+ * const opinion = new Opinion();
1631
+ * const events = await opinion.fetchEvents();
1632
+ * ```
1633
+ */
1634
+ export class Opinion extends Exchange {
1635
+ constructor(options = {}) {
1636
+ super("opinion", options);
1637
+ }
1638
+ }
1639
+ /**
1640
+ * Metaculus exchange client.
1641
+ *
1642
+ * Forecasting platform. Public read-only access works without credentials;
1643
+ * authenticated calls accept a bearer token via `apiKey`.
1644
+ *
1645
+ * @example
1646
+ * ```typescript
1647
+ * const metaculus = new Metaculus();
1648
+ * const events = await metaculus.fetchEvents();
1649
+ * ```
1650
+ */
1651
+ export class Metaculus extends Exchange {
1652
+ constructor(options = {}) {
1653
+ super("metaculus", options);
1654
+ }
1655
+ }
1656
+ /**
1657
+ * Smarkets exchange client.
1658
+ *
1659
+ * UK-based betting exchange. Public catalog endpoints work without
1660
+ * credentials; trading requires Smarkets account email (`apiKey`) and
1661
+ * password (`privateKey`).
1662
+ *
1663
+ * @example
1664
+ * ```typescript
1665
+ * const smarkets = new Smarkets();
1666
+ * const events = await smarkets.fetchEvents();
1667
+ * ```
1668
+ */
1669
+ export class Smarkets extends Exchange {
1670
+ constructor(options = {}) {
1671
+ super("smarkets", options);
1672
+ }
1673
+ }
1674
+ /**
1675
+ * Polymarket US exchange client.
1676
+ *
1677
+ * US-regulated Polymarket venue. Public catalog endpoints work without
1678
+ * credentials; trading requires `apiKey` (keyId) and `privateKey`
1679
+ * (secretKey) issued by Polymarket US.
1680
+ *
1681
+ * @example
1682
+ * ```typescript
1683
+ * const polyUs = new PolymarketUS();
1684
+ * const events = await polyUs.fetchEvents();
1685
+ * ```
1686
+ */
1687
+ export class PolymarketUS extends Exchange {
1688
+ constructor(options = {}) {
1689
+ super("polymarket_us", options);
1690
+ }
1691
+ }
@@ -15,6 +15,7 @@ export declare class ServerManager {
15
15
  private api;
16
16
  private lockPath;
17
17
  private static readonly DEFAULT_PORT;
18
+ private static ensurePromise;
18
19
  constructor(options?: ServerManagerOptions);
19
20
  /**
20
21
  * Read server information from lock file.
@@ -43,8 +44,13 @@ export declare class ServerManager {
43
44
  private waitForServer;
44
45
  /**
45
46
  * Ensure the server is running, starting it if necessary.
47
+ *
48
+ * Concurrent calls across all ServerManager instances in the process
49
+ * are coalesced onto a single in-flight promise. See the comment on
50
+ * `ServerManager.ensurePromise` for why this matters.
46
51
  */
47
52
  ensureServerRunning(): Promise<void>;
53
+ private doEnsureServerRunning;
48
54
  private isVersionMismatch;
49
55
  /**
50
56
  * Stop the currently running server.
@@ -54,5 +60,36 @@ export declare class ServerManager {
54
60
  * Restart the server.
55
61
  */
56
62
  restart(): Promise<void>;
63
+ /**
64
+ * Start the server if it is not already running.
65
+ *
66
+ * Idempotent: if the server is already running and healthy this returns
67
+ * immediately without restarting.
68
+ */
69
+ start(): Promise<void>;
70
+ /**
71
+ * Get a structured snapshot of the sidecar server state.
72
+ *
73
+ * Returns a fresh object on every call (no shared mutable state).
74
+ */
75
+ status(): Promise<{
76
+ running: boolean;
77
+ pid: number | null;
78
+ port: number | null;
79
+ version: string | null;
80
+ uptimeSeconds: number | null;
81
+ lockFile: string;
82
+ }>;
83
+ /**
84
+ * Check whether the server's /health endpoint is currently responsive.
85
+ */
86
+ health(): Promise<boolean>;
87
+ /**
88
+ * Return the last `n` lines from the sidecar server log file.
89
+ *
90
+ * The launcher writes server stdout/stderr to ~/.pmxt/server.log.
91
+ * Returns an empty array if no log file is present.
92
+ */
93
+ logs(n?: number): string[];
57
94
  private killOldServer;
58
95
  }
@@ -14,6 +14,28 @@ export class ServerManager {
14
14
  api;
15
15
  lockPath;
16
16
  static DEFAULT_PORT = 3847;
17
+ // Process-wide coalescing of concurrent ensureServerRunning() calls.
18
+ //
19
+ // Each `Exchange` instance constructs its own ServerManager and each one
20
+ // kicks off ensureServerRunning() from its constructor. Without
21
+ // coalescing, N Exchange instances created in parallel all see "no
22
+ // server running", all spawn their own sidecar via pmxt-ensure-server,
23
+ // and the lock file ends up pointing at whichever spawn wrote last. Each
24
+ // Exchange instance has already captured its own basePath at
25
+ // construction time, so most of them end up talking to a sidecar whose
26
+ // access token does NOT match the token they later read from the lock
27
+ // file — every request returns 401 Unauthorized.
28
+ //
29
+ // The fix is process-wide: when a ensureServerRunning call is in
30
+ // flight, all subsequent callers await the same promise. After the
31
+ // in-flight call settles (success OR failure) the cache is cleared so
32
+ // later callers can re-check the sidecar state (e.g. if it was killed
33
+ // by the user between ticks).
34
+ //
35
+ // This is static on purpose — all ServerManager instances in the
36
+ // process share the same sidecar and the same lock file, so they must
37
+ // share the same in-flight promise.
38
+ static ensurePromise = null;
17
39
  constructor(options = {}) {
18
40
  this.baseUrl = options.baseUrl || `http://localhost:${ServerManager.DEFAULT_PORT}`;
19
41
  this.maxRetries = options.maxRetries || 30;
@@ -101,8 +123,21 @@ export class ServerManager {
101
123
  }
102
124
  /**
103
125
  * Ensure the server is running, starting it if necessary.
126
+ *
127
+ * Concurrent calls across all ServerManager instances in the process
128
+ * are coalesced onto a single in-flight promise. See the comment on
129
+ * `ServerManager.ensurePromise` for why this matters.
104
130
  */
105
131
  async ensureServerRunning() {
132
+ if (ServerManager.ensurePromise) {
133
+ return ServerManager.ensurePromise;
134
+ }
135
+ ServerManager.ensurePromise = this.doEnsureServerRunning().finally(() => {
136
+ ServerManager.ensurePromise = null;
137
+ });
138
+ return ServerManager.ensurePromise;
139
+ }
140
+ async doEnsureServerRunning() {
106
141
  // Check for force restart
107
142
  if (process.env.PMXT_ALWAYS_RESTART === '1') {
108
143
  await this.killOldServer();
@@ -201,6 +236,70 @@ export class ServerManager {
201
236
  await this.stop();
202
237
  await this.ensureServerRunning();
203
238
  }
239
+ /**
240
+ * Start the server if it is not already running.
241
+ *
242
+ * Idempotent: if the server is already running and healthy this returns
243
+ * immediately without restarting.
244
+ */
245
+ async start() {
246
+ await this.ensureServerRunning();
247
+ }
248
+ /**
249
+ * Get a structured snapshot of the sidecar server state.
250
+ *
251
+ * Returns a fresh object on every call (no shared mutable state).
252
+ */
253
+ async status() {
254
+ const info = this.getServerInfo();
255
+ const running = await this.isServerRunning();
256
+ let uptimeSeconds = null;
257
+ if (info && typeof info.timestamp === "number") {
258
+ const nowSeconds = Date.now() / 1000;
259
+ const tsSeconds = info.timestamp > 1e12 ? info.timestamp / 1000 : info.timestamp;
260
+ const delta = nowSeconds - tsSeconds;
261
+ if (delta >= 0)
262
+ uptimeSeconds = delta;
263
+ }
264
+ return {
265
+ running,
266
+ pid: info?.pid ?? null,
267
+ port: info?.port ?? null,
268
+ version: info?.version ?? null,
269
+ uptimeSeconds,
270
+ lockFile: this.lockPath,
271
+ };
272
+ }
273
+ /**
274
+ * Check whether the server's /health endpoint is currently responsive.
275
+ */
276
+ async health() {
277
+ return this.isServerRunning();
278
+ }
279
+ /**
280
+ * Return the last `n` lines from the sidecar server log file.
281
+ *
282
+ * The launcher writes server stdout/stderr to ~/.pmxt/server.log.
283
+ * Returns an empty array if no log file is present.
284
+ */
285
+ logs(n = 50) {
286
+ if (n <= 0)
287
+ return [];
288
+ const logPath = join(dirname(this.lockPath), "server.log");
289
+ try {
290
+ if (!existsSync(logPath))
291
+ return [];
292
+ const content = readFileSync(logPath, "utf-8");
293
+ const lines = content.split(/\r?\n/);
294
+ // split on a trailing newline produces an empty final element; drop it
295
+ if (lines.length > 0 && lines[lines.length - 1] === "")
296
+ lines.pop();
297
+ return lines.length > n ? lines.slice(lines.length - n) : lines;
298
+ }
299
+ catch {
300
+ return [];
301
+ }
302
+ }
204
303
  async killOldServer() {
205
304
  const info = this.getServerInfo();
206
305
  if (info && info.pid) {