BREAKING CHANGE(core): Refactor core IPC: replace node-ipc with native transports and add IpcChannel / IpcServer / IpcClient with heartbeat, reconnection, request/response and pub/sub. Update tests and documentation.
This commit is contained in:
232
ts/classes.ipcclient.ts
Normal file
232
ts/classes.ipcclient.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import * as plugins from './smartipc.plugins.js';
|
||||
import { IpcChannel } from './classes.ipcchannel.js';
|
||||
import type { IIpcChannelOptions } from './classes.ipcchannel.js';
|
||||
|
||||
/**
|
||||
* Options for IPC Client
|
||||
*/
|
||||
export interface IIpcClientOptions extends IIpcChannelOptions {
|
||||
/** Client identifier */
|
||||
clientId?: string;
|
||||
/** Client metadata */
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* IPC Client for connecting to an IPC server
|
||||
*/
|
||||
export class IpcClient extends plugins.EventEmitter {
|
||||
private options: IIpcClientOptions;
|
||||
private channel: IpcChannel;
|
||||
private messageHandlers = new Map<string, (payload: any) => any | Promise<any>>();
|
||||
private isConnected = false;
|
||||
private clientId: string;
|
||||
|
||||
constructor(options: IIpcClientOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.clientId = options.clientId || plugins.crypto.randomUUID();
|
||||
|
||||
// Create the channel
|
||||
this.channel = new IpcChannel(this.options);
|
||||
this.setupChannelHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the server
|
||||
*/
|
||||
public async connect(): Promise<void> {
|
||||
if (this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Connect the channel
|
||||
await this.channel.connect();
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
this.isConnected = true;
|
||||
this.emit('connect');
|
||||
} catch (error) {
|
||||
await this.channel.disconnect();
|
||||
throw new Error(`Failed to register with server: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the server
|
||||
*/
|
||||
public async disconnect(): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnected = false;
|
||||
await this.channel.disconnect();
|
||||
this.emit('disconnect');
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup channel event handlers
|
||||
*/
|
||||
private setupChannelHandlers(): void {
|
||||
// Forward channel events
|
||||
this.channel.on('connect', () => {
|
||||
// Don't emit connect here, wait for successful registration
|
||||
});
|
||||
|
||||
this.channel.on('disconnect', (reason) => {
|
||||
this.isConnected = false;
|
||||
this.emit('disconnect', reason);
|
||||
});
|
||||
|
||||
this.channel.on('error', (error) => {
|
||||
this.emit('error', error);
|
||||
});
|
||||
|
||||
this.channel.on('reconnecting', (info) => {
|
||||
this.emit('reconnecting', info);
|
||||
});
|
||||
|
||||
// Handle messages
|
||||
this.channel.on('message', (message) => {
|
||||
// Check if we have a handler for this message type
|
||||
if (this.messageHandlers.has(message.type)) {
|
||||
const handler = this.messageHandlers.get(message.type)!;
|
||||
|
||||
// If message expects a response
|
||||
if (message.headers?.requiresResponse && message.id) {
|
||||
Promise.resolve()
|
||||
.then(() => handler(message.payload))
|
||||
.then((result) => {
|
||||
return this.channel.sendMessage(
|
||||
`${message.type}_response`,
|
||||
result,
|
||||
{ correlationId: message.id }
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
return this.channel.sendMessage(
|
||||
`${message.type}_response`,
|
||||
null,
|
||||
{ correlationId: message.id, error: error.message }
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// Fire and forget
|
||||
handler(message.payload);
|
||||
}
|
||||
} else {
|
||||
// Emit unhandled message
|
||||
this.emit('message', message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a message handler
|
||||
*/
|
||||
public onMessage(type: string, handler: (payload: any) => any | Promise<any>): void {
|
||||
this.messageHandlers.set(type, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to the server
|
||||
*/
|
||||
public async sendMessage(type: string, payload: any, headers?: Record<string, any>): Promise<void> {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Client is not connected');
|
||||
}
|
||||
|
||||
// Always include clientId in headers
|
||||
await this.channel.sendMessage(type, payload, {
|
||||
...headers,
|
||||
clientId: this.clientId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the server and wait for response
|
||||
*/
|
||||
public async request<TReq = any, TRes = any>(
|
||||
type: string,
|
||||
payload: TReq,
|
||||
options?: { timeout?: number; headers?: Record<string, any> }
|
||||
): Promise<TRes> {
|
||||
if (!this.isConnected) {
|
||||
throw new Error('Client is not connected');
|
||||
}
|
||||
|
||||
// Always include clientId in headers
|
||||
return this.channel.request<TReq, TRes>(type, payload, {
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
clientId: this.clientId
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to a topic (pub/sub pattern)
|
||||
*/
|
||||
public async subscribe(topic: string, handler: (payload: any) => void): Promise<void> {
|
||||
// Register local handler
|
||||
this.messageHandlers.set(`topic:${topic}`, handler);
|
||||
|
||||
// Notify server about subscription
|
||||
await this.sendMessage('__subscribe__', { topic });
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from a topic
|
||||
*/
|
||||
public async unsubscribe(topic: string): Promise<void> {
|
||||
// Remove local handler
|
||||
this.messageHandlers.delete(`topic:${topic}`);
|
||||
|
||||
// Notify server about unsubscription
|
||||
await this.sendMessage('__unsubscribe__', { topic });
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish to a topic
|
||||
*/
|
||||
public async publish(topic: string, payload: any): Promise<void> {
|
||||
await this.sendMessage('__publish__', { topic, payload });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client ID
|
||||
*/
|
||||
public getClientId(): string {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if client is connected
|
||||
*/
|
||||
public getIsConnected(): boolean {
|
||||
return this.isConnected && this.channel.isConnected();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get client statistics
|
||||
*/
|
||||
public getStats(): any {
|
||||
return this.channel.getStats();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user