feat(streaming): Add streaming support: chunked stream transfers, file send/receive, stream events and helpers

This commit is contained in:
2025-08-30 23:02:49 +00:00
parent 7ba064584b
commit 994b1d20fb
9 changed files with 678 additions and 11 deletions

View File

@@ -122,25 +122,27 @@ export class IpcClient extends plugins.EventEmitter {
// If waitForReady is specified, wait for server socket to exist first
if (connectOptions.waitForReady) {
const waitTimeout = connectOptions.waitTimeout || 10000;
// For Unix domain sockets / named pipes: wait explicitly using helper that probes with clientOnly
if (this.options.socketPath) {
const { SmartIpc } = await import('./index.js');
await (SmartIpc as any).waitForServer({ socketPath: this.options.socketPath, timeoutMs: waitTimeout });
await attemptConnection();
return;
}
// Fallback (e.g., TCP): retry-connect loop
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')) {
if ((error as any).message?.includes('ECONNREFUSED')) {
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
@@ -233,6 +235,13 @@ export class IpcClient extends plugins.EventEmitter {
this.emit('reconnecting', info);
});
// Forward streaming events
// Emitted as ('stream', info, readable)
// info contains { streamId, meta, headers, clientId }
this.channel.on('stream', (info: any, readable: plugins.stream.Readable) => {
this.emit('stream', info, readable);
});
// Handle messages
this.channel.on('message', (message) => {
// Check if we have a handler for this message type
@@ -361,4 +370,40 @@ export class IpcClient extends plugins.EventEmitter {
public getStats(): any {
return this.channel.getStats();
}
/**
* Send a Node.js readable stream to the server
*/
public async sendStream(readable: plugins.stream.Readable | NodeJS.ReadableStream, options?: { headers?: Record<string, any>; chunkSize?: number; streamId?: string; meta?: Record<string, any> }): Promise<void> {
const headers = { ...(options?.headers || {}), clientId: this.clientId };
await (this as any).channel.sendStream(readable as any, { ...options, headers });
}
/**
* Send a file to the server via streaming
*/
public async sendFile(filePath: string, options?: { headers?: Record<string, any>; chunkSize?: number; streamId?: string; meta?: Record<string, any> }): Promise<void> {
const fs = plugins.fs;
const path = plugins.path;
const stat = fs.statSync(filePath);
const meta = {
...(options?.meta || {}),
type: 'file',
basename: path.basename(filePath),
size: stat.size,
mtimeMs: stat.mtimeMs
};
const rs = fs.createReadStream(filePath);
await this.sendStream(rs, { ...options, meta });
}
/** Cancel an outgoing stream by id */
public async cancelOutgoingStream(streamId: string): Promise<void> {
await (this as any).channel.cancelOutgoingStream(streamId, { clientId: this.clientId });
}
/** Cancel an incoming stream by id */
public async cancelIncomingStream(streamId: string): Promise<void> {
await (this as any).channel.cancelIncomingStream(streamId, { clientId: this.clientId });
}
}