feat(core): Add heartbeat grace/timeout options, client retry/wait-for-ready, server readiness and socket cleanup, transport socket options, helper utilities, and tests
This commit is contained in:
@@ -5,11 +5,35 @@ import type { IIpcChannelOptions } from './classes.ipcchannel.js';
|
||||
/**
|
||||
* Options for IPC Client
|
||||
*/
|
||||
export interface IConnectRetryConfig {
|
||||
/** Enable connection retry */
|
||||
enabled: boolean;
|
||||
/** Initial delay before first retry in ms */
|
||||
initialDelay?: number;
|
||||
/** Maximum delay between retries in ms */
|
||||
maxDelay?: number;
|
||||
/** Maximum number of attempts */
|
||||
maxAttempts?: number;
|
||||
/** Total timeout for all retry attempts in ms */
|
||||
totalTimeout?: number;
|
||||
}
|
||||
|
||||
export interface IClientConnectOptions {
|
||||
/** Wait for server to be ready before attempting connection */
|
||||
waitForReady?: boolean;
|
||||
/** Timeout for waiting for server readiness in ms */
|
||||
waitTimeout?: number;
|
||||
}
|
||||
|
||||
export interface IIpcClientOptions extends IIpcChannelOptions {
|
||||
/** Client identifier */
|
||||
clientId?: string;
|
||||
/** Client metadata */
|
||||
metadata?: Record<string, any>;
|
||||
/** Connection retry configuration */
|
||||
connectRetry?: IConnectRetryConfig;
|
||||
/** Registration timeout in ms (default: 5000) */
|
||||
registerTimeoutMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,34 +59,111 @@ export class IpcClient extends plugins.EventEmitter {
|
||||
/**
|
||||
* Connect to the server
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
public async connect(connectOptions: IClientConnectOptions = {}): Promise<void> {
|
||||
if (this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect the channel
|
||||
await this.channel.connect();
|
||||
// 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 }
|
||||
);
|
||||
|
||||
// Register with the server
|
||||
try {
|
||||
const response = await this.channel.request<any, any>(
|
||||
'__register__',
|
||||
{
|
||||
clientId: this.clientId,
|
||||
metadata: this.options.metadata
|
||||
},
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || 'Registration failed');
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to attempt connection with retry
|
||||
const attemptConnection = async (): Promise<void> => {
|
||||
const retryConfig = this.options.connectRetry;
|
||||
const maxAttempts = retryConfig?.maxAttempts || 1;
|
||||
const initialDelay = retryConfig?.initialDelay || 100;
|
||||
const maxDelay = retryConfig?.maxDelay || 1500;
|
||||
const totalTimeout = retryConfig?.totalTimeout || 15000;
|
||||
|
||||
const startTime = Date.now();
|
||||
let lastError: Error | undefined;
|
||||
let delay = initialDelay;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
// Check total timeout
|
||||
if (totalTimeout && Date.now() - startTime > totalTimeout) {
|
||||
throw new Error(`Connection timeout after ${totalTimeout}ms: ${lastError?.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Connect the channel
|
||||
await this.channel.connect();
|
||||
|
||||
// Attempt registration
|
||||
await attemptRegistration();
|
||||
return; // Success!
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
|
||||
// Disconnect channel for retry
|
||||
await this.channel.disconnect().catch(() => {});
|
||||
|
||||
// If this isn't the last attempt and retry is enabled, wait before retrying
|
||||
if (attempt < maxAttempts && retryConfig?.enabled) {
|
||||
// Check if we have time for another attempt
|
||||
if (totalTimeout && Date.now() - startTime + delay > totalTimeout) {
|
||||
break; // Will timeout, don't wait
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
// Exponential backoff with max limit
|
||||
delay = Math.min(delay * 2, maxDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isConnected = true;
|
||||
this.emit('connect');
|
||||
} catch (error) {
|
||||
await this.channel.disconnect();
|
||||
throw new Error(`Failed to register with server: ${error.message}`);
|
||||
// All attempts failed
|
||||
throw lastError || new Error('Failed to connect to server');
|
||||
};
|
||||
|
||||
// If waitForReady is specified, wait for server socket to exist first
|
||||
if (connectOptions.waitForReady) {
|
||||
const waitTimeout = connectOptions.waitTimeout || 10000;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < waitTimeout) {
|
||||
try {
|
||||
// Try to connect
|
||||
await attemptConnection();
|
||||
return; // Success!
|
||||
} catch (error) {
|
||||
// If it's a connection refused error, server might not be ready yet
|
||||
if ((error as any).message?.includes('ECONNREFUSED') ||
|
||||
(error as any).message?.includes('ENOENT')) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
continue;
|
||||
}
|
||||
// Other errors should be thrown
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Server not ready after ${waitTimeout}ms`);
|
||||
} else {
|
||||
// Normal connection attempt
|
||||
await attemptConnection();
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user