feat(streaming): Add streaming support: chunked stream transfers, file send/receive, stream events and helpers
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user