/** * WorkerProcess - Owns NDJSON pipe for a Bun child process * * Implements DD §8.1. stdout=protocol, stderr=fatal/log; exit-code table 0–5. * * @module packages/runtime/src/workers/WorkerProcess */ import type { ChildProcess } from 'child_process' import { WorkerProtocol, type WorkerMessage, type WorkerMessageType, type WorkerMessageDirection } from './WorkerProtocol.js' export type WorkerExitCode = | 0 // Normal exit | 1 // Error (unrecoverable) | 2 // Protocol error | 3 // Permission denied | 4 // Parent cancelled | 5 // Timeout interface ExitCodeInfo { semantic: string description: string } const EXIT_CODE_TABLE: Record = { 0: { semantic: 'normal', description: 'Worker completed successfully' }, 1: { semantic: 'error', description: 'Unrecoverable error occurred' }, 2: { semantic: 'protocol_error', description: 'Protocol violation or deserialization failure' }, 3: { semantic: 'permission_denied', description: 'Worker denied permission for operation' }, 4: { semantic: 'parent_cancelled', description: 'Parent process cancelled this worker' }, 5: { semantic: 'timeout', description: 'Worker exceeded time limit' } } export class WorkerProcess { private proc: ChildProcess | null = null private protocol: WorkerProtocol private message_handlers: Map void> = new Map() private buffer: string = '' constructor() { this.protocol = new WorkerProtocol() } /** * Set the child process. */ set_process(proc: ChildProcess): void { this.proc = proc this.setup_streams() } /** * Send a message to the worker process. */ send(message: WorkerMessage): void { if (!this.proc?.stdin?.writable) { throw new Error('Worker process stdin is not writable') } const line = this.protocol.encode(message) this.proc.stdin.write(line) } /** * Register a message handler. */ on_message(type: WorkerMessageType, handler: (msg: WorkerMessage) => void): void { this.message_handlers.set(type, handler) } /** * Get exit code info. */ get_exit_code_info(code: number): ExitCodeInfo | undefined { return EXIT_CODE_TABLE[code as WorkerExitCode] } /** * Check if process is alive. */ is_alive(): boolean { if (!this.proc) return false return this.proc.exitCode === null } /** * Kill the worker process. */ kill(signal: NodeJS.Signals = 'SIGTERM'): boolean { return this.proc?.kill(signal) || false } /** * Get the process ID. */ get_pid(): number | undefined { return this.proc?.pid } // ============================================================================ // Private // ============================================================================ private setup_streams(): void { if (!this.proc) return // stdout = protocol channel if (this.proc.stdout) { this.proc.stdout.on('data', (data: Buffer) => { this.buffer += data.toString() this.process_buffer() }) } // stderr = log/fatal if (this.proc.stderr) { this.proc.stderr.on('data', (data: Buffer) => { const message = data.toString().trim() if (message) { console.error('[Worker stderr]', message) } }) } // Exit handler this.proc.on('exit', (code, signal) => { const info = this.get_exit_code_info(code || 1) console.log(`[Worker] exited with code ${code} (${info?.semantic || 'unknown'}): ${info?.description || ''}`) }) } private process_buffer(): void { const lines = this.buffer.split('\n') this.buffer = lines.pop() || '' for (const line of lines) { const message = this.protocol.decode(line) if (!message) continue // Route to handler const handler = this.message_handlers.get(message.type) if (handler) { handler(message) } } } }