feat(ipcclient): Add clientOnly mode to prevent clients from auto-starting servers and improve registration/reconnect behavior
This commit is contained in:
@@ -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.'
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user