@relayfile/client 0.10.53 → 0.10.55

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/dist/client.d.ts CHANGED
@@ -69,6 +69,19 @@ export interface RelayfileClientOptions {
69
69
  startTimeoutMs?: number;
70
70
  /** Per-request timeout. A hung socket rejects instead of blocking forever. */
71
71
  requestTimeoutMs?: number;
72
+ /**
73
+ * Maximum time allowed to discover a stale daemon through `lsof`.
74
+ * Clamped to 5–10 seconds so loaded hosts get enough time without an
75
+ * unbounded control-plane replacement wait.
76
+ */
77
+ staleDaemonDiscoveryTimeoutMs?: number;
78
+ /**
79
+ * Maximum time allowed for a stale daemon to release the socket and exit
80
+ * after SIGTERM. Clamped to 5–10 seconds and never inherited from
81
+ * `startTimeoutMs`, so an oversized or non-finite start budget cannot make
82
+ * stale-daemon replacement wait forever.
83
+ */
84
+ staleDaemonTerminationTimeoutMs?: number;
72
85
  }
73
86
  export declare class RelayfileControlPlaneClient {
74
87
  private readonly socketPath;
@@ -76,6 +89,8 @@ export declare class RelayfileControlPlaneClient {
76
89
  private readonly autoStart;
77
90
  private readonly startTimeoutMs;
78
91
  private readonly requestTimeoutMs;
92
+ private readonly staleDaemonDiscoveryTimeoutMs;
93
+ private readonly staleDaemonTerminationTimeoutMs;
79
94
  private ready;
80
95
  constructor(options?: RelayfileClientOptions);
81
96
  private createHTTPRequest;
@@ -87,6 +102,7 @@ export declare class RelayfileControlPlaneClient {
87
102
  private spawnDaemon;
88
103
  private startDaemonAndConnect;
89
104
  private stopStaleDaemon;
105
+ private isProcessAlive;
90
106
  private socketOwnerPids;
91
107
  /** Resolve a Unix-socket owner without external tools on standard Linux hosts. */
92
108
  private linuxSocketOwnerPids;
package/dist/client.js CHANGED
@@ -23,6 +23,10 @@ export const RELAYFILE_API_VERSION = 3;
23
23
  export const MIN_RELAYFILE_VERSION = '0.10.17';
24
24
  /** First published relayfile binary that can replace a stale daemon for API v3. */
25
25
  const MIN_RELAYFILE_VERSION_FOR_CURRENT_API = '0.10.21';
26
+ const DEFAULT_STALE_DAEMON_DISCOVERY_TIMEOUT_MS = 5000;
27
+ const MAX_STALE_DAEMON_DISCOVERY_TIMEOUT_MS = 10000;
28
+ const DEFAULT_STALE_DAEMON_TERMINATION_TIMEOUT_MS = 5000;
29
+ const MAX_STALE_DAEMON_TERMINATION_TIMEOUT_MS = 10000;
26
30
  const SEMVER_RE = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
27
31
  /** First semver-looking token in a string (tolerates a leading `v` and surrounding words). */
28
32
  export function firstSemver(value) {
@@ -106,6 +110,8 @@ export class RelayfileControlPlaneClient {
106
110
  autoStart;
107
111
  startTimeoutMs;
108
112
  requestTimeoutMs;
113
+ staleDaemonDiscoveryTimeoutMs;
114
+ staleDaemonTerminationTimeoutMs;
109
115
  ready;
110
116
  constructor(options = {}) {
111
117
  this.socketPath = options.socketPath ?? defaultRelayfileSocketPath();
@@ -113,6 +119,14 @@ export class RelayfileControlPlaneClient {
113
119
  this.autoStart = options.autoStart ?? process.env.RELAYFILE_REQUIRE_DAEMON !== '1';
114
120
  this.startTimeoutMs = options.startTimeoutMs ?? 5000;
115
121
  this.requestTimeoutMs = options.requestTimeoutMs ?? 10000;
122
+ const requestedStaleDaemonDiscoveryTimeoutMs = options.staleDaemonDiscoveryTimeoutMs;
123
+ this.staleDaemonDiscoveryTimeoutMs = Number.isFinite(requestedStaleDaemonDiscoveryTimeoutMs)
124
+ ? Math.min(MAX_STALE_DAEMON_DISCOVERY_TIMEOUT_MS, Math.max(DEFAULT_STALE_DAEMON_DISCOVERY_TIMEOUT_MS, requestedStaleDaemonDiscoveryTimeoutMs))
125
+ : DEFAULT_STALE_DAEMON_DISCOVERY_TIMEOUT_MS;
126
+ const requestedStaleDaemonTerminationTimeoutMs = options.staleDaemonTerminationTimeoutMs;
127
+ this.staleDaemonTerminationTimeoutMs = Number.isFinite(requestedStaleDaemonTerminationTimeoutMs)
128
+ ? Math.min(MAX_STALE_DAEMON_TERMINATION_TIMEOUT_MS, Math.max(DEFAULT_STALE_DAEMON_TERMINATION_TIMEOUT_MS, requestedStaleDaemonTerminationTimeoutMs))
129
+ : DEFAULT_STALE_DAEMON_TERMINATION_TIMEOUT_MS;
116
130
  }
117
131
  createHTTPRequest(options, onResponse) {
118
132
  return request(options, onResponse);
@@ -245,8 +259,15 @@ export class RelayfileControlPlaneClient {
245
259
  hello = await this.startDaemonAndConnect(installedVersion);
246
260
  }
247
261
  catch (err) {
248
- if (err instanceof RelayfileControlPlaneError && err.code === 'VERSION_INCOMPATIBLE') {
249
- throw err;
262
+ if (err instanceof RelayfileControlPlaneError) {
263
+ // VERSION_INCOMPATIBLE from the replacement handshake and the typed
264
+ // stale-daemon discovery/termination codes are actionable as-is;
265
+ // wrapping them would hide the diagnosis ensureReady callers need.
266
+ if (err.code === 'VERSION_INCOMPATIBLE' ||
267
+ err.code === 'STALE_DAEMON_DISCOVERY_FAILED' ||
268
+ err.code === 'STALE_DAEMON_STOP_FAILED') {
269
+ throw err;
270
+ }
250
271
  }
251
272
  throw this.versionMismatchError(hello, installedVersion, `Automatic stale-daemon replacement failed: ${err instanceof Error ? err.message : String(err)}`);
252
273
  }
@@ -321,14 +342,25 @@ export class RelayfileControlPlaneClient {
321
342
  process.kill(pids[0], 'SIGTERM');
322
343
  }
323
344
  catch (err) {
324
- throw new Error(`could not stop stale relayfile control-plane pid ${pids[0]}: ${err instanceof Error ? err.message : String(err)}`);
345
+ throw new RelayfileControlPlaneError('STALE_DAEMON_STOP_FAILED', `could not stop stale relayfile control-plane pid ${pids[0]}: ${err instanceof Error ? err.message : String(err)}`);
325
346
  }
326
- const deadline = Date.now() + this.startTimeoutMs;
327
- while (existsSync(this.socketPath) && Date.now() < deadline) {
347
+ const deadline = Date.now() + this.staleDaemonTerminationTimeoutMs;
348
+ while ((existsSync(this.socketPath) || this.isProcessAlive(pids[0])) &&
349
+ Date.now() < deadline) {
328
350
  await sleep(50);
329
351
  }
330
- if (existsSync(this.socketPath)) {
331
- throw new Error(`stale relayfile control-plane pid ${pids[0]} did not release ${this.socketPath} within ${this.startTimeoutMs}ms`);
352
+ if (existsSync(this.socketPath) || this.isProcessAlive(pids[0])) {
353
+ throw new RelayfileControlPlaneError('STALE_DAEMON_STOP_FAILED', `stale relayfile control-plane pid ${pids[0]} did not release ${this.socketPath} ` +
354
+ `and exit within ${this.staleDaemonTerminationTimeoutMs}ms`);
355
+ }
356
+ }
357
+ isProcessAlive(pid) {
358
+ try {
359
+ process.kill(pid, 0);
360
+ return true;
361
+ }
362
+ catch (err) {
363
+ return err.code !== 'ESRCH';
332
364
  }
333
365
  }
334
366
  async socketOwnerPids() {
@@ -340,14 +372,22 @@ export class RelayfileControlPlaneClient {
340
372
  if (process.platform === 'win32') {
341
373
  throw new Error('automatic stale-daemon replacement is not supported on Windows; stop the running relayfile control-plane manually');
342
374
  }
343
- const result = await this.runCommand('lsof', ['-t', '--', this.socketPath], 1000);
375
+ const result = await this.runCommand('lsof', ['-t', '--', this.socketPath], this.staleDaemonDiscoveryTimeoutMs);
344
376
  if (!result) {
345
- throw new Error(`could not run \`lsof\` to identify the process serving ${this.socketPath} ` +
346
- `(it is not installed, not on PATH, or did not exit within 1000ms); ` +
377
+ throw new RelayfileControlPlaneError('STALE_DAEMON_DISCOVERY_FAILED', `could not run \`lsof\` to identify the process serving ${this.socketPath} ` +
378
+ `(it is not installed or not on PATH); ` +
347
379
  `install lsof or stop the stale relayfile control-plane manually`);
348
380
  }
381
+ if (result.timedOut) {
382
+ throw new RelayfileControlPlaneError('STALE_DAEMON_DISCOVERY_FAILED', `\`lsof\` timed out after ${this.staleDaemonDiscoveryTimeoutMs}ms while identifying ` +
383
+ `the process serving ${this.socketPath}${result.stderr.trim() ? ` (stderr: ${result.stderr.trim()})` : ''}`);
384
+ }
385
+ if (result.commandError) {
386
+ throw new RelayfileControlPlaneError('STALE_DAEMON_DISCOVERY_FAILED', `could not run \`lsof\` to identify the process serving ${this.socketPath}: ` +
387
+ `${result.commandError}${result.stderr.trim() ? ` (stderr: ${result.stderr.trim()})` : ''}`);
388
+ }
349
389
  if (result.code !== 0) {
350
- throw new Error(`lsof could not identify the process serving ${this.socketPath} (exit ${result.code}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ''})`);
390
+ throw new RelayfileControlPlaneError('STALE_DAEMON_DISCOVERY_FAILED', `lsof could not identify the process serving ${this.socketPath} (exit ${result.code}${result.stderr.trim() ? `: ${result.stderr.trim()}` : ''})`);
351
391
  }
352
392
  return [
353
393
  ...new Set(result.stdout
@@ -466,11 +506,11 @@ export class RelayfileControlPlaneClient {
466
506
  child.stderr?.on('data', (chunk) => {
467
507
  stderr += String(chunk);
468
508
  });
469
- child.once('error', () => finish());
509
+ child.once('error', (err) => finish({ stdout, stderr, code: null, commandError: err instanceof Error ? err.message : String(err) }));
470
510
  child.once('close', (code) => finish({ stdout, stderr, code }));
471
511
  timeout = setTimeout(() => {
472
512
  child.kill();
473
- finish();
513
+ finish({ stdout, stderr, code: null, timedOut: true });
474
514
  }, timeoutMs);
475
515
  timeout.unref?.();
476
516
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relayfile/client",
3
- "version": "0.10.53",
3
+ "version": "0.10.55",
4
4
  "description": "Typed client for the relayfile local control-plane (unix-socket HTTP/JSON), generated from the authoritative OpenAPI contract",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",