@relayfile/client 0.10.27-rc.0 → 0.10.27
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 +4 -0
- package/dist/client.js +93 -11
- package/dist/generated/control-plane.d.ts +5 -7
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export declare class RelayfileControlPlaneClient {
|
|
|
78
78
|
private readonly requestTimeoutMs;
|
|
79
79
|
private ready;
|
|
80
80
|
constructor(options?: RelayfileClientOptions);
|
|
81
|
+
private createHTTPRequest;
|
|
81
82
|
/** Low-level request over the unix socket. Throws RelayfileControlPlaneError. */
|
|
82
83
|
private rawRequest;
|
|
83
84
|
/** Connect (auto-starting the daemon if configured), version-negotiating once. */
|
|
@@ -86,6 +87,9 @@ export declare class RelayfileControlPlaneClient {
|
|
|
86
87
|
private spawnDaemon;
|
|
87
88
|
private startDaemonAndConnect;
|
|
88
89
|
private stopStaleDaemon;
|
|
90
|
+
private socketOwnerPids;
|
|
91
|
+
/** Resolve a Unix-socket owner without external tools on standard Linux hosts. */
|
|
92
|
+
private linuxSocketOwnerPids;
|
|
89
93
|
private versionMismatchError;
|
|
90
94
|
private versionRejectionError;
|
|
91
95
|
private restartCommand;
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { request } from 'node:http';
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, readlinkSync } from 'node:fs';
|
|
3
|
+
import { request, } from 'node:http';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
/**
|
|
@@ -82,7 +82,15 @@ export class RelayfileControlPlaneError extends Error {
|
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
85
|
-
const TRANSIENT_CONNECTION_CODES = new Set([
|
|
85
|
+
const TRANSIENT_CONNECTION_CODES = new Set([
|
|
86
|
+
'ENOENT',
|
|
87
|
+
'ECONNREFUSED',
|
|
88
|
+
'ETIMEDOUT',
|
|
89
|
+
// A daemon that binds the socket before it is ready to serve can accept and
|
|
90
|
+
// then drop the connection mid-request; both surface during startup polling.
|
|
91
|
+
'ECONNRESET',
|
|
92
|
+
'EPIPE',
|
|
93
|
+
]);
|
|
86
94
|
function isTransientConnectionCode(code) {
|
|
87
95
|
return code != null && TRANSIENT_CONNECTION_CODES.has(code);
|
|
88
96
|
}
|
|
@@ -106,6 +114,9 @@ export class RelayfileControlPlaneClient {
|
|
|
106
114
|
this.startTimeoutMs = options.startTimeoutMs ?? 5000;
|
|
107
115
|
this.requestTimeoutMs = options.requestTimeoutMs ?? 10000;
|
|
108
116
|
}
|
|
117
|
+
createHTTPRequest(options, onResponse) {
|
|
118
|
+
return request(options, onResponse);
|
|
119
|
+
}
|
|
109
120
|
/** Low-level request over the unix socket. Throws RelayfileControlPlaneError. */
|
|
110
121
|
rawRequest(opts) {
|
|
111
122
|
const query = opts.query
|
|
@@ -130,7 +141,7 @@ export class RelayfileControlPlaneClient {
|
|
|
130
141
|
settled = true;
|
|
131
142
|
resolve(value);
|
|
132
143
|
};
|
|
133
|
-
const req =
|
|
144
|
+
const req = this.createHTTPRequest({
|
|
134
145
|
socketPath: this.socketPath,
|
|
135
146
|
method: opts.method,
|
|
136
147
|
path,
|
|
@@ -300,13 +311,7 @@ export class RelayfileControlPlaneClient {
|
|
|
300
311
|
`(${lastErr instanceof Error ? lastErr.message : String(lastErr)}).`);
|
|
301
312
|
}
|
|
302
313
|
async stopStaleDaemon() {
|
|
303
|
-
const
|
|
304
|
-
const pids = [
|
|
305
|
-
...new Set((result?.code === 0 ? result.stdout : '')
|
|
306
|
-
.split(/\s+/)
|
|
307
|
-
.map((value) => Number.parseInt(value, 10))
|
|
308
|
-
.filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)),
|
|
309
|
-
];
|
|
314
|
+
const pids = await this.socketOwnerPids();
|
|
310
315
|
if (pids.length !== 1) {
|
|
311
316
|
throw new Error(pids.length === 0
|
|
312
317
|
? `could not identify the process serving ${this.socketPath}`
|
|
@@ -326,6 +331,83 @@ export class RelayfileControlPlaneClient {
|
|
|
326
331
|
throw new Error(`stale relayfile control-plane pid ${pids[0]} did not release ${this.socketPath} within ${this.startTimeoutMs}ms`);
|
|
327
332
|
}
|
|
328
333
|
}
|
|
334
|
+
async socketOwnerPids() {
|
|
335
|
+
if (process.platform === 'linux') {
|
|
336
|
+
const procPids = this.linuxSocketOwnerPids();
|
|
337
|
+
if (procPids.length > 0)
|
|
338
|
+
return procPids;
|
|
339
|
+
}
|
|
340
|
+
if (process.platform === 'win32') {
|
|
341
|
+
throw new Error('automatic stale-daemon replacement is not supported on Windows; stop the running relayfile control-plane manually');
|
|
342
|
+
}
|
|
343
|
+
const result = await this.runCommand('lsof', ['-t', '--', this.socketPath], 1000);
|
|
344
|
+
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); ` +
|
|
347
|
+
`install lsof or stop the stale relayfile control-plane manually`);
|
|
348
|
+
}
|
|
349
|
+
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()}` : ''})`);
|
|
351
|
+
}
|
|
352
|
+
return [
|
|
353
|
+
...new Set(result.stdout
|
|
354
|
+
.split(/\s+/)
|
|
355
|
+
.map((value) => Number.parseInt(value, 10))
|
|
356
|
+
.filter((pid) => Number.isSafeInteger(pid) && pid > 1 && pid !== process.pid)),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
/** Resolve a Unix-socket owner without external tools on standard Linux hosts. */
|
|
360
|
+
linuxSocketOwnerPids(procRoot = '/proc') {
|
|
361
|
+
let socketInodes;
|
|
362
|
+
try {
|
|
363
|
+
socketInodes = new Set(readFileSync(join(procRoot, 'net', 'unix'), 'utf8')
|
|
364
|
+
.split('\n')
|
|
365
|
+
.slice(1)
|
|
366
|
+
.map((line) => line.trim().split(/\s+/))
|
|
367
|
+
.filter((fields) => fields.length >= 8 && fields.slice(7).join(' ') === this.socketPath)
|
|
368
|
+
.map((fields) => fields[6]));
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return [];
|
|
372
|
+
}
|
|
373
|
+
if (socketInodes.size === 0)
|
|
374
|
+
return [];
|
|
375
|
+
const pids = new Set();
|
|
376
|
+
let processes;
|
|
377
|
+
try {
|
|
378
|
+
processes = readdirSync(procRoot).filter((name) => /^\d+$/.test(name));
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return [];
|
|
382
|
+
}
|
|
383
|
+
for (const processName of processes) {
|
|
384
|
+
const pid = Number.parseInt(processName, 10);
|
|
385
|
+
if (pid <= 1 || pid === process.pid)
|
|
386
|
+
continue;
|
|
387
|
+
const fdDir = join(procRoot, processName, 'fd');
|
|
388
|
+
let descriptors;
|
|
389
|
+
try {
|
|
390
|
+
descriptors = readdirSync(fdDir);
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
for (const descriptor of descriptors) {
|
|
396
|
+
try {
|
|
397
|
+
const target = readlinkSync(join(fdDir, descriptor));
|
|
398
|
+
const inode = target.match(/^socket:\[(\d+)\]$/)?.[1];
|
|
399
|
+
if (inode && socketInodes.has(inode)) {
|
|
400
|
+
pids.add(pid);
|
|
401
|
+
break;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
// File descriptors may disappear while /proc is scanned.
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return [...pids];
|
|
410
|
+
}
|
|
329
411
|
versionMismatchError(hello, installedVersion, detail) {
|
|
330
412
|
const supported = hello.supportedApiVersions?.length
|
|
331
413
|
? hello.supportedApiVersions.map((version) => `v${version}`).join(', ')
|
|
@@ -14,6 +14,9 @@ export interface paths {
|
|
|
14
14
|
* Negotiate control-plane API compatibility
|
|
15
15
|
* @description Unversioned discovery endpoint. The daemon always returns its current
|
|
16
16
|
* and supported API versions; any version header or query is ignored.
|
|
17
|
+
* No version parameters are declared here on purpose: discovery must stay
|
|
18
|
+
* callable by clients of any API version, including versions this contract
|
|
19
|
+
* does not know about.
|
|
17
20
|
*/
|
|
18
21
|
get: operations["hello"];
|
|
19
22
|
put?: never;
|
|
@@ -370,7 +373,6 @@ export interface components {
|
|
|
370
373
|
};
|
|
371
374
|
parameters: {
|
|
372
375
|
ApiVersionHeader: 3;
|
|
373
|
-
ApiVersionQuery: number;
|
|
374
376
|
};
|
|
375
377
|
requestBodies: never;
|
|
376
378
|
headers: never;
|
|
@@ -380,12 +382,8 @@ export type $defs = Record<string, never>;
|
|
|
380
382
|
export interface operations {
|
|
381
383
|
hello: {
|
|
382
384
|
parameters: {
|
|
383
|
-
query?:
|
|
384
|
-
|
|
385
|
-
};
|
|
386
|
-
header?: {
|
|
387
|
-
"X-Relayfile-API-Version"?: components["parameters"]["ApiVersionHeader"];
|
|
388
|
-
};
|
|
385
|
+
query?: never;
|
|
386
|
+
header?: never;
|
|
389
387
|
path?: never;
|
|
390
388
|
cookie?: never;
|
|
391
389
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@relayfile/client",
|
|
3
|
-
"version": "0.10.27
|
|
3
|
+
"version": "0.10.27",
|
|
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",
|