@relayfile/client 0.10.27-rc.0 → 0.10.28

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
@@ -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(['ENOENT', 'ECONNREFUSED', 'ETIMEDOUT']);
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 = request({
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 result = await this.runCommand('lsof', ['-t', '--', this.socketPath], 1000);
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
- apiVersion?: components["parameters"]["ApiVersionQuery"];
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-rc.0",
3
+ "version": "0.10.28",
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",