pmxtjs 2.25.3 → 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.
@@ -28,6 +28,32 @@ export type * from "./pmxt/models.js";
28
28
  export * from "./pmxt/errors.js";
29
29
  declare function stopServer(): Promise<void>;
30
30
  declare function restartServer(): Promise<void>;
31
+ /**
32
+ * Namespaced server management API.
33
+ *
34
+ * Available commands:
35
+ * - status() Structured snapshot of the sidecar (running, pid, port, version, uptime)
36
+ * - health() True if the server responds to /health, false otherwise
37
+ * - start() Idempotently start the sidecar (no-op if already running)
38
+ * - stop() Stop the sidecar and clean up the lock file
39
+ * - restart() Stop and start the sidecar
40
+ * - logs(n) Return the last n log lines from the sidecar log file
41
+ */
42
+ export declare const server: {
43
+ readonly status: () => Promise<{
44
+ running: boolean;
45
+ pid: number | null;
46
+ port: number | null;
47
+ version: string | null;
48
+ uptimeSeconds: number | null;
49
+ lockFile: string;
50
+ }>;
51
+ readonly health: () => Promise<boolean>;
52
+ readonly start: () => Promise<void>;
53
+ readonly stop: () => Promise<void>;
54
+ readonly restart: () => Promise<void>;
55
+ readonly logs: (n?: number) => string[];
56
+ };
31
57
  declare const pmxt: {
32
58
  fromServerError(errorData: any): errors.PmxtError;
33
59
  PmxtError: typeof errors.PmxtError;
@@ -58,6 +84,21 @@ declare const pmxt: {
58
84
  Smarkets: typeof Smarkets;
59
85
  PolymarketUS: typeof PolymarketUS;
60
86
  ServerManager: typeof ServerManager;
87
+ server: {
88
+ readonly status: () => Promise<{
89
+ running: boolean;
90
+ pid: number | null;
91
+ port: number | null;
92
+ version: string | null;
93
+ uptimeSeconds: number | null;
94
+ lockFile: string;
95
+ }>;
96
+ readonly health: () => Promise<boolean>;
97
+ readonly start: () => Promise<void>;
98
+ readonly stop: () => Promise<void>;
99
+ readonly restart: () => Promise<void>;
100
+ readonly logs: (n?: number) => string[];
101
+ };
61
102
  stopServer: typeof stopServer;
62
103
  restartServer: typeof restartServer;
63
104
  };
package/dist/esm/index.js CHANGED
@@ -26,12 +26,34 @@ export { ServerManager } from "./pmxt/server-manager.js";
26
26
  export { MarketList } from "./pmxt/models.js";
27
27
  export * from "./pmxt/errors.js";
28
28
  const defaultManager = new ServerManager();
29
+ // Flat aliases for the namespaced server commands. Kept as permanent,
30
+ // fully-supported shorthand — `pmxt.server.stop()` and `pmxt.stopServer()`
31
+ // are equivalent and both are first-class API.
29
32
  async function stopServer() {
30
33
  await defaultManager.stop();
31
34
  }
32
35
  async function restartServer() {
33
36
  await defaultManager.restart();
34
37
  }
38
+ /**
39
+ * Namespaced server management API.
40
+ *
41
+ * Available commands:
42
+ * - status() Structured snapshot of the sidecar (running, pid, port, version, uptime)
43
+ * - health() True if the server responds to /health, false otherwise
44
+ * - start() Idempotently start the sidecar (no-op if already running)
45
+ * - stop() Stop the sidecar and clean up the lock file
46
+ * - restart() Stop and start the sidecar
47
+ * - logs(n) Return the last n log lines from the sidecar log file
48
+ */
49
+ export const server = {
50
+ status: () => defaultManager.status(),
51
+ health: () => defaultManager.health(),
52
+ start: () => defaultManager.start(),
53
+ stop: () => defaultManager.stop(),
54
+ restart: () => defaultManager.restart(),
55
+ logs: (n = 50) => defaultManager.logs(n),
56
+ };
35
57
  const pmxt = {
36
58
  Exchange,
37
59
  Polymarket,
@@ -46,6 +68,7 @@ const pmxt = {
46
68
  Smarkets,
47
69
  PolymarketUS,
48
70
  ServerManager,
71
+ server,
49
72
  stopServer,
50
73
  restartServer,
51
74
  ...models,
@@ -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) {
package/dist/index.d.ts CHANGED
@@ -28,6 +28,32 @@ export type * from "./pmxt/models.js";
28
28
  export * from "./pmxt/errors.js";
29
29
  declare function stopServer(): Promise<void>;
30
30
  declare function restartServer(): Promise<void>;
31
+ /**
32
+ * Namespaced server management API.
33
+ *
34
+ * Available commands:
35
+ * - status() Structured snapshot of the sidecar (running, pid, port, version, uptime)
36
+ * - health() True if the server responds to /health, false otherwise
37
+ * - start() Idempotently start the sidecar (no-op if already running)
38
+ * - stop() Stop the sidecar and clean up the lock file
39
+ * - restart() Stop and start the sidecar
40
+ * - logs(n) Return the last n log lines from the sidecar log file
41
+ */
42
+ export declare const server: {
43
+ readonly status: () => Promise<{
44
+ running: boolean;
45
+ pid: number | null;
46
+ port: number | null;
47
+ version: string | null;
48
+ uptimeSeconds: number | null;
49
+ lockFile: string;
50
+ }>;
51
+ readonly health: () => Promise<boolean>;
52
+ readonly start: () => Promise<void>;
53
+ readonly stop: () => Promise<void>;
54
+ readonly restart: () => Promise<void>;
55
+ readonly logs: (n?: number) => string[];
56
+ };
31
57
  declare const pmxt: {
32
58
  fromServerError(errorData: any): errors.PmxtError;
33
59
  PmxtError: typeof errors.PmxtError;
@@ -58,6 +84,21 @@ declare const pmxt: {
58
84
  Smarkets: typeof Smarkets;
59
85
  PolymarketUS: typeof PolymarketUS;
60
86
  ServerManager: typeof ServerManager;
87
+ server: {
88
+ readonly status: () => Promise<{
89
+ running: boolean;
90
+ pid: number | null;
91
+ port: number | null;
92
+ version: string | null;
93
+ uptimeSeconds: number | null;
94
+ lockFile: string;
95
+ }>;
96
+ readonly health: () => Promise<boolean>;
97
+ readonly start: () => Promise<void>;
98
+ readonly stop: () => Promise<void>;
99
+ readonly restart: () => Promise<void>;
100
+ readonly logs: (n?: number) => string[];
101
+ };
61
102
  stopServer: typeof stopServer;
62
103
  restartServer: typeof restartServer;
63
104
  };
package/dist/index.js CHANGED
@@ -55,7 +55,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
55
55
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
56
56
  };
57
57
  Object.defineProperty(exports, "__esModule", { value: true });
58
- exports.MarketList = exports.ServerManager = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.Limitless = exports.KalshiDemo = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
58
+ exports.server = exports.MarketList = exports.ServerManager = exports.PolymarketUS = exports.Smarkets = exports.Metaculus = exports.Opinion = exports.Baozi = exports.Probable = exports.Myriad = exports.Limitless = exports.KalshiDemo = exports.Kalshi = exports.Polymarket = exports.Exchange = void 0;
59
59
  const client_js_1 = require("./pmxt/client.js");
60
60
  const server_manager_js_1 = require("./pmxt/server-manager.js");
61
61
  const models = __importStar(require("./pmxt/models.js"));
@@ -79,12 +79,34 @@ var models_js_1 = require("./pmxt/models.js");
79
79
  Object.defineProperty(exports, "MarketList", { enumerable: true, get: function () { return models_js_1.MarketList; } });
80
80
  __exportStar(require("./pmxt/errors.js"), exports);
81
81
  const defaultManager = new server_manager_js_1.ServerManager();
82
+ // Flat aliases for the namespaced server commands. Kept as permanent,
83
+ // fully-supported shorthand — `pmxt.server.stop()` and `pmxt.stopServer()`
84
+ // are equivalent and both are first-class API.
82
85
  async function stopServer() {
83
86
  await defaultManager.stop();
84
87
  }
85
88
  async function restartServer() {
86
89
  await defaultManager.restart();
87
90
  }
91
+ /**
92
+ * Namespaced server management API.
93
+ *
94
+ * Available commands:
95
+ * - status() Structured snapshot of the sidecar (running, pid, port, version, uptime)
96
+ * - health() True if the server responds to /health, false otherwise
97
+ * - start() Idempotently start the sidecar (no-op if already running)
98
+ * - stop() Stop the sidecar and clean up the lock file
99
+ * - restart() Stop and start the sidecar
100
+ * - logs(n) Return the last n log lines from the sidecar log file
101
+ */
102
+ exports.server = {
103
+ status: () => defaultManager.status(),
104
+ health: () => defaultManager.health(),
105
+ start: () => defaultManager.start(),
106
+ stop: () => defaultManager.stop(),
107
+ restart: () => defaultManager.restart(),
108
+ logs: (n = 50) => defaultManager.logs(n),
109
+ };
88
110
  const pmxt = {
89
111
  Exchange: client_js_1.Exchange,
90
112
  Polymarket: client_js_1.Polymarket,
@@ -99,6 +121,7 @@ const pmxt = {
99
121
  Smarkets: client_js_1.Smarkets,
100
122
  PolymarketUS: client_js_1.PolymarketUS,
101
123
  ServerManager: server_manager_js_1.ServerManager,
124
+ server: exports.server,
102
125
  stopServer,
103
126
  restartServer,
104
127
  ...models,
@@ -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
  }
@@ -50,6 +50,28 @@ class ServerManager {
50
50
  api;
51
51
  lockPath;
52
52
  static DEFAULT_PORT = 3847;
53
+ // Process-wide coalescing of concurrent ensureServerRunning() calls.
54
+ //
55
+ // Each `Exchange` instance constructs its own ServerManager and each one
56
+ // kicks off ensureServerRunning() from its constructor. Without
57
+ // coalescing, N Exchange instances created in parallel all see "no
58
+ // server running", all spawn their own sidecar via pmxt-ensure-server,
59
+ // and the lock file ends up pointing at whichever spawn wrote last. Each
60
+ // Exchange instance has already captured its own basePath at
61
+ // construction time, so most of them end up talking to a sidecar whose
62
+ // access token does NOT match the token they later read from the lock
63
+ // file — every request returns 401 Unauthorized.
64
+ //
65
+ // The fix is process-wide: when a ensureServerRunning call is in
66
+ // flight, all subsequent callers await the same promise. After the
67
+ // in-flight call settles (success OR failure) the cache is cleared so
68
+ // later callers can re-check the sidecar state (e.g. if it was killed
69
+ // by the user between ticks).
70
+ //
71
+ // This is static on purpose — all ServerManager instances in the
72
+ // process share the same sidecar and the same lock file, so they must
73
+ // share the same in-flight promise.
74
+ static ensurePromise = null;
53
75
  constructor(options = {}) {
54
76
  this.baseUrl = options.baseUrl || `http://localhost:${ServerManager.DEFAULT_PORT}`;
55
77
  this.maxRetries = options.maxRetries || 30;
@@ -137,8 +159,21 @@ class ServerManager {
137
159
  }
138
160
  /**
139
161
  * Ensure the server is running, starting it if necessary.
162
+ *
163
+ * Concurrent calls across all ServerManager instances in the process
164
+ * are coalesced onto a single in-flight promise. See the comment on
165
+ * `ServerManager.ensurePromise` for why this matters.
140
166
  */
141
167
  async ensureServerRunning() {
168
+ if (ServerManager.ensurePromise) {
169
+ return ServerManager.ensurePromise;
170
+ }
171
+ ServerManager.ensurePromise = this.doEnsureServerRunning().finally(() => {
172
+ ServerManager.ensurePromise = null;
173
+ });
174
+ return ServerManager.ensurePromise;
175
+ }
176
+ async doEnsureServerRunning() {
142
177
  // Check for force restart
143
178
  if (process.env.PMXT_ALWAYS_RESTART === '1') {
144
179
  await this.killOldServer();
@@ -237,6 +272,70 @@ class ServerManager {
237
272
  await this.stop();
238
273
  await this.ensureServerRunning();
239
274
  }
275
+ /**
276
+ * Start the server if it is not already running.
277
+ *
278
+ * Idempotent: if the server is already running and healthy this returns
279
+ * immediately without restarting.
280
+ */
281
+ async start() {
282
+ await this.ensureServerRunning();
283
+ }
284
+ /**
285
+ * Get a structured snapshot of the sidecar server state.
286
+ *
287
+ * Returns a fresh object on every call (no shared mutable state).
288
+ */
289
+ async status() {
290
+ const info = this.getServerInfo();
291
+ const running = await this.isServerRunning();
292
+ let uptimeSeconds = null;
293
+ if (info && typeof info.timestamp === "number") {
294
+ const nowSeconds = Date.now() / 1000;
295
+ const tsSeconds = info.timestamp > 1e12 ? info.timestamp / 1000 : info.timestamp;
296
+ const delta = nowSeconds - tsSeconds;
297
+ if (delta >= 0)
298
+ uptimeSeconds = delta;
299
+ }
300
+ return {
301
+ running,
302
+ pid: info?.pid ?? null,
303
+ port: info?.port ?? null,
304
+ version: info?.version ?? null,
305
+ uptimeSeconds,
306
+ lockFile: this.lockPath,
307
+ };
308
+ }
309
+ /**
310
+ * Check whether the server's /health endpoint is currently responsive.
311
+ */
312
+ async health() {
313
+ return this.isServerRunning();
314
+ }
315
+ /**
316
+ * Return the last `n` lines from the sidecar server log file.
317
+ *
318
+ * The launcher writes server stdout/stderr to ~/.pmxt/server.log.
319
+ * Returns an empty array if no log file is present.
320
+ */
321
+ logs(n = 50) {
322
+ if (n <= 0)
323
+ return [];
324
+ const logPath = (0, path_1.join)((0, path_1.dirname)(this.lockPath), "server.log");
325
+ try {
326
+ if (!(0, fs_1.existsSync)(logPath))
327
+ return [];
328
+ const content = (0, fs_1.readFileSync)(logPath, "utf-8");
329
+ const lines = content.split(/\r?\n/);
330
+ // split on a trailing newline produces an empty final element; drop it
331
+ if (lines.length > 0 && lines[lines.length - 1] === "")
332
+ lines.pop();
333
+ return lines.length > n ? lines.slice(lines.length - n) : lines;
334
+ }
335
+ catch {
336
+ return [];
337
+ }
338
+ }
240
339
  async killOldServer() {
241
340
  const info = this.getServerInfo();
242
341
  if (info && info.pid) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.25.3",
3
+ "version": "2.26.0",
4
4
  "description": "OpenAPI client for pmxtjs",
5
5
  "author": "OpenAPI-Generator",
6
6
  "repository": {
package/index.ts CHANGED
@@ -33,6 +33,9 @@ export * from "./pmxt/errors.js";
33
33
 
34
34
  const defaultManager = new ServerManager();
35
35
 
36
+ // Flat aliases for the namespaced server commands. Kept as permanent,
37
+ // fully-supported shorthand — `pmxt.server.stop()` and `pmxt.stopServer()`
38
+ // are equivalent and both are first-class API.
36
39
  async function stopServer(): Promise<void> {
37
40
  await defaultManager.stop();
38
41
  }
@@ -41,6 +44,26 @@ async function restartServer(): Promise<void> {
41
44
  await defaultManager.restart();
42
45
  }
43
46
 
47
+ /**
48
+ * Namespaced server management API.
49
+ *
50
+ * Available commands:
51
+ * - status() Structured snapshot of the sidecar (running, pid, port, version, uptime)
52
+ * - health() True if the server responds to /health, false otherwise
53
+ * - start() Idempotently start the sidecar (no-op if already running)
54
+ * - stop() Stop the sidecar and clean up the lock file
55
+ * - restart() Stop and start the sidecar
56
+ * - logs(n) Return the last n log lines from the sidecar log file
57
+ */
58
+ export const server = {
59
+ status: () => defaultManager.status(),
60
+ health: () => defaultManager.health(),
61
+ start: () => defaultManager.start(),
62
+ stop: () => defaultManager.stop(),
63
+ restart: () => defaultManager.restart(),
64
+ logs: (n: number = 50) => defaultManager.logs(n),
65
+ } as const;
66
+
44
67
  const pmxt = {
45
68
  Exchange,
46
69
  Polymarket,
@@ -55,6 +78,7 @@ const pmxt = {
55
78
  Smarkets,
56
79
  PolymarketUS,
57
80
  ServerManager,
81
+ server,
58
82
  stopServer,
59
83
  restartServer,
60
84
  ...models,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pmxtjs",
3
- "version": "2.25.3",
3
+ "version": "2.26.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,7 @@
43
43
  "unified"
44
44
  ],
45
45
  "dependencies": {
46
- "pmxt-core": "2.25.3"
46
+ "pmxt-core": "2.26.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/jest": "^30.0.0",
@@ -31,6 +31,29 @@ export class ServerManager {
31
31
  private lockPath: string;
32
32
  private static readonly DEFAULT_PORT = 3847;
33
33
 
34
+ // Process-wide coalescing of concurrent ensureServerRunning() calls.
35
+ //
36
+ // Each `Exchange` instance constructs its own ServerManager and each one
37
+ // kicks off ensureServerRunning() from its constructor. Without
38
+ // coalescing, N Exchange instances created in parallel all see "no
39
+ // server running", all spawn their own sidecar via pmxt-ensure-server,
40
+ // and the lock file ends up pointing at whichever spawn wrote last. Each
41
+ // Exchange instance has already captured its own basePath at
42
+ // construction time, so most of them end up talking to a sidecar whose
43
+ // access token does NOT match the token they later read from the lock
44
+ // file — every request returns 401 Unauthorized.
45
+ //
46
+ // The fix is process-wide: when a ensureServerRunning call is in
47
+ // flight, all subsequent callers await the same promise. After the
48
+ // in-flight call settles (success OR failure) the cache is cleared so
49
+ // later callers can re-check the sidecar state (e.g. if it was killed
50
+ // by the user between ticks).
51
+ //
52
+ // This is static on purpose — all ServerManager instances in the
53
+ // process share the same sidecar and the same lock file, so they must
54
+ // share the same in-flight promise.
55
+ private static ensurePromise: Promise<void> | null = null;
56
+
34
57
  constructor(options: ServerManagerOptions = {}) {
35
58
  this.baseUrl = options.baseUrl || `http://localhost:${ServerManager.DEFAULT_PORT}`;
36
59
  this.maxRetries = options.maxRetries || 30;
@@ -124,8 +147,22 @@ export class ServerManager {
124
147
 
125
148
  /**
126
149
  * Ensure the server is running, starting it if necessary.
150
+ *
151
+ * Concurrent calls across all ServerManager instances in the process
152
+ * are coalesced onto a single in-flight promise. See the comment on
153
+ * `ServerManager.ensurePromise` for why this matters.
127
154
  */
128
155
  async ensureServerRunning(): Promise<void> {
156
+ if (ServerManager.ensurePromise) {
157
+ return ServerManager.ensurePromise;
158
+ }
159
+ ServerManager.ensurePromise = this.doEnsureServerRunning().finally(() => {
160
+ ServerManager.ensurePromise = null;
161
+ });
162
+ return ServerManager.ensurePromise;
163
+ }
164
+
165
+ private async doEnsureServerRunning(): Promise<void> {
129
166
  // Check for force restart
130
167
  if (process.env.PMXT_ALWAYS_RESTART === '1') {
131
168
  await this.killOldServer();
@@ -235,6 +272,78 @@ export class ServerManager {
235
272
  await this.ensureServerRunning();
236
273
  }
237
274
 
275
+ /**
276
+ * Start the server if it is not already running.
277
+ *
278
+ * Idempotent: if the server is already running and healthy this returns
279
+ * immediately without restarting.
280
+ */
281
+ async start(): Promise<void> {
282
+ await this.ensureServerRunning();
283
+ }
284
+
285
+ /**
286
+ * Get a structured snapshot of the sidecar server state.
287
+ *
288
+ * Returns a fresh object on every call (no shared mutable state).
289
+ */
290
+ async status(): Promise<{
291
+ running: boolean;
292
+ pid: number | null;
293
+ port: number | null;
294
+ version: string | null;
295
+ uptimeSeconds: number | null;
296
+ lockFile: string;
297
+ }> {
298
+ const info = this.getServerInfo();
299
+ const running = await this.isServerRunning();
300
+
301
+ let uptimeSeconds: number | null = null;
302
+ if (info && typeof info.timestamp === "number") {
303
+ const nowSeconds = Date.now() / 1000;
304
+ const tsSeconds = info.timestamp > 1e12 ? info.timestamp / 1000 : info.timestamp;
305
+ const delta = nowSeconds - tsSeconds;
306
+ if (delta >= 0) uptimeSeconds = delta;
307
+ }
308
+
309
+ return {
310
+ running,
311
+ pid: info?.pid ?? null,
312
+ port: info?.port ?? null,
313
+ version: info?.version ?? null,
314
+ uptimeSeconds,
315
+ lockFile: this.lockPath,
316
+ };
317
+ }
318
+
319
+ /**
320
+ * Check whether the server's /health endpoint is currently responsive.
321
+ */
322
+ async health(): Promise<boolean> {
323
+ return this.isServerRunning();
324
+ }
325
+
326
+ /**
327
+ * Return the last `n` lines from the sidecar server log file.
328
+ *
329
+ * The launcher writes server stdout/stderr to ~/.pmxt/server.log.
330
+ * Returns an empty array if no log file is present.
331
+ */
332
+ logs(n: number = 50): string[] {
333
+ if (n <= 0) return [];
334
+ const logPath = join(dirname(this.lockPath), "server.log");
335
+ try {
336
+ if (!existsSync(logPath)) return [];
337
+ const content = readFileSync(logPath, "utf-8");
338
+ const lines = content.split(/\r?\n/);
339
+ // split on a trailing newline produces an empty final element; drop it
340
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
341
+ return lines.length > n ? lines.slice(lines.length - n) : lines;
342
+ } catch {
343
+ return [];
344
+ }
345
+ }
346
+
238
347
  private async killOldServer(): Promise<void> {
239
348
  const info = this.getServerInfo();
240
349
  if (info && info.pid) {