feat(ipcclient): Add clientOnly mode to prevent clients from auto-starting servers and improve registration/reconnect behavior

This commit is contained in:
2025-08-29 08:48:38 +00:00
parent fd3fc7518b
commit fa53dcfc4f
8 changed files with 178 additions and 40 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@push.rocks/smartipc',
version: '2.1.3',
version: '2.2.0',
description: 'A library for node inter process communication, providing an easy-to-use API for IPC.'
}

View File

@@ -45,6 +45,7 @@ export class IpcClient extends plugins.EventEmitter {
private messageHandlers = new Map<string, (payload: any) => any | Promise<any>>();
private isConnected = false;
private clientId: string;
private didRegisterOnce = false;
constructor(options: IIpcClientOptions) {
super();
@@ -66,30 +67,7 @@ export class IpcClient extends plugins.EventEmitter {
// Helper function to attempt registration
const attemptRegistration = async (): Promise<void> => {
const registerTimeoutMs = this.options.registerTimeoutMs || 5000;
try {
const response = await this.channel.request<any, any>(
'__register__',
{
clientId: this.clientId,
metadata: this.options.metadata
},
{
timeout: registerTimeoutMs,
headers: { clientId: this.clientId } // Include clientId in headers for proper routing
}
);
if (!response.success) {
throw new Error(response.error || 'Registration failed');
}
this.isConnected = true;
this.emit('connect');
} catch (error) {
throw new Error(`Failed to register with server: ${error.message}`);
}
await this.attemptRegistrationInternal();
};
// Helper function to attempt connection with retry
@@ -170,6 +148,38 @@ export class IpcClient extends plugins.EventEmitter {
}
}
/**
* Attempt to register this client over the current channel connection.
* Sets connection flags and emits 'connect' on success.
*/
private async attemptRegistrationInternal(): Promise<void> {
const registerTimeoutMs = this.options.registerTimeoutMs || 5000;
try {
const response = await this.channel.request<any, any>(
'__register__',
{
clientId: this.clientId,
metadata: this.options.metadata
},
{
timeout: registerTimeoutMs,
headers: { clientId: this.clientId }
}
);
if (!response.success) {
throw new Error(response.error || 'Registration failed');
}
this.isConnected = true;
this.didRegisterOnce = true;
this.emit('connect');
} catch (error: any) {
throw new Error(`Failed to register with server: ${error.message}`);
}
}
/**
* Disconnect from the server
*/
@@ -188,8 +198,16 @@ export class IpcClient extends plugins.EventEmitter {
*/
private setupChannelHandlers(): void {
// Forward channel events
this.channel.on('connect', () => {
// Don't emit connect here, wait for successful registration
this.channel.on('connect', async () => {
// On reconnects, re-register automatically when we had connected before
if (this.didRegisterOnce && !this.isConnected) {
try {
await this.attemptRegistrationInternal();
} catch (error) {
this.emit('error', error);
}
}
// For initial connect(), registration is handled explicitly there
});
this.channel.on('disconnect', (reason) => {
@@ -343,4 +361,4 @@ export class IpcClient extends plugins.EventEmitter {
public getStats(): any {
return this.channel.getStats();
}
}
}

View File

@@ -18,6 +18,12 @@ export interface IIpcMessageEnvelope<T = any> {
export interface IIpcTransportOptions {
/** Unique identifier for this transport */
id: string;
/**
* When true, a client transport will NOT auto-start a server when connect()
* encounters ECONNREFUSED/ENOENT. Useful for strict client/daemon setups.
* Default: false. Can also be overridden by env SMARTIPC_CLIENT_ONLY=1.
*/
clientOnly?: boolean;
/** Socket path for Unix domain sockets or pipe name for Windows */
socketPath?: string;
/** TCP host for network transport */
@@ -195,7 +201,21 @@ export class UnixSocketTransport extends IpcTransport {
this.socket.on('error', (error: any) => {
if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') {
// No server exists, we should become the server
// Determine if we must NOT auto-start server
const envVal = process.env.SMARTIPC_CLIENT_ONLY;
const envClientOnly = !!envVal && (envVal === '1' || envVal === 'true' || envVal === 'TRUE');
const clientOnly = this.options.clientOnly === true || envClientOnly;
if (clientOnly) {
// Reject instead of starting a server to avoid races
const reason = error.code || 'UNKNOWN';
const err = new Error(`Server not available (${reason}); clientOnly prevents auto-start`);
(err as any).code = reason;
reject(err);
return;
}
// No server exists and clientOnly is false: become the server (back-compat)
this.socket = null;
this.startServer(socketPath).then(resolve).catch(reject);
} else {
@@ -718,4 +738,4 @@ export function createTransport(options: IIpcTransportOptions): IpcTransport {
} else {
return new UnixSocketTransport(options);
}
}
}

View File

@@ -35,6 +35,7 @@ export class SmartIpc {
socketPath: options.socketPath,
clientId: `probe-${process.pid}-${Date.now()}`,
heartbeat: false,
clientOnly: true,
connectRetry: {
enabled: false // Don't retry, we're handling retries here
},
@@ -127,4 +128,4 @@ export class SmartIpc {
}
// Export the main class as default
export default SmartIpc;
export default SmartIpc;