@relayfile/client 0.10.52 → 0.10.54
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 +16 -0
- package/dist/client.js +53 -13
- package/package.json +1 -1
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
|
|
249
|
-
|
|
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
|
|
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.
|
|
327
|
-
while (existsSync(this.socketPath)
|
|
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
|
|
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],
|
|
375
|
+
const result = await this.runCommand('lsof', ['-t', '--', this.socketPath], this.staleDaemonDiscoveryTimeoutMs);
|
|
344
376
|
if (!result) {
|
|
345
|
-
throw new
|
|
346
|
-
`(it is not installed
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.10.54",
|
|
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",
|