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:
2025-08-25 13:37:31 +00:00
parent e3c1d35895
commit dd25ffd3e4
9 changed files with 780 additions and 30 deletions

View File

@@ -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();
}
}