UNPKG

@akala/core

Version:
234 lines (213 loc) 9.1 kB
import { ChildProcess } from "child_process"; import type { AllEventKeys } from "../events/event-bus.js"; import { EventEmitter, type AllEvents } from "../events/event-emitter.js"; import type { EventListener, EventOptions } from "../events/shared.js"; import { IsomorphicBuffer } from "../helpers.js"; import { SocketAdapterAkalaEventMap, SocketAdapter } from "./shared.js"; import { StatefulSubscription, type Subscription } from "../teardown-manager.js"; import { Deferred } from "../promiseHelpers.js"; export class ProcessStdioAdapter extends EventEmitter<SocketAdapterAkalaEventMap> implements SocketAdapter { messageEvent: (args_0: string | IsomorphicBuffer) => void; openEvent: (args_0: Event) => void; closeEvent: (args_0: CloseEvent) => void; errorEvent: (args_0: Event) => void; get open(): boolean { return !!this.process.pid; } pipe(socket: SocketAdapter) { this.on('message', (message) => socket.send(message)); this.on('close', () => socket.close()); } async close() { this.off('message'); this.off('error'); this.closeEvent(null); } send(data: string | IsomorphicBuffer): Promise<void> { if (typeof data === 'string') return new Promise<void>((resolve, reject) => this.process.stdout.write(data, err => err ? reject(err) : resolve())); return new Promise<void>((resolve, reject) => this.process.stdout.write(data.toArray(), err => err ? reject(err) : resolve())); } private readonly messageListeners: [(ev: unknown) => void, (ev: unknown) => void][] = []; public off<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler?: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): boolean { switch (event) { case 'message': { let listeners = this.messageListeners; if (handler) listeners = listeners.filter(f => f[0] == handler); var result = false; for (const listener of listeners) { this.process.stdin.off('data', listener[1]); result = !!this.messageListeners.splice(this.messageListeners.indexOf(listener), 1)?.length || result; } } break; case 'open': return false; case 'close': case 'error': //eslint-disable-next-line @typescript-eslint/no-explicit-any this.process.stdout.off(event, handler as any); break; default: throw new Error(`Unsupported event ${String(event)}`); } return true; } public on<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>, options?: EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': { const x = function (data) { return (handler as EventListener<SocketAdapterAkalaEventMap['message']>).call(this, typeof data === 'string' ? data : IsomorphicBuffer.fromBuffer(data)); }; this.messageListeners.push([handler, x]); if (options?.once) this.process.stdin.once('data', x); else this.process.stdin.on('data', x); return new StatefulSubscription(() => { this.messageListeners.splice(this.messageListeners.findIndex(x => x[0] === handler), 1); this.process.stdin.off('message', x); }).unsubscribe; } case 'open': handler(null); break; case 'close': case 'error': if (options?.once) this.process.stdin.once(event, handler); else this.process.stdin.on(event, handler); return new StatefulSubscription(() => { this.process.stdin.off(event, handler); }).unsubscribe; case Symbol.dispose: return super.on(event, handler, options); default: throw new Error(`Unsupported event ${String(event)}`); } } public once<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': return this.on(event, handler, { once: true } as EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>); case 'close': case 'error': case 'open': case Symbol.dispose: return this.on(event, handler, { once: true } as EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>); default: let x: never = event; throw new Error(`Unsupported event ${x}`); } } constructor(private process: NodeJS.Process, abort: AbortSignal) { super(); abort.addEventListener('abort', () => { this.close(); }); this.messageEvent = this.getOrCreate('message').emit.bind(this.get('message')); this.closeEvent = this.getOrCreate('close').emit.bind(this.get('close')); this.errorEvent = this.getOrCreate('error').emit.bind(this.get('error')); } } export class ChildProcessStdioAdapter extends EventEmitter<SocketAdapterAkalaEventMap> implements SocketAdapter { messageEvent: (args_0: string) => void; openEvent: (args_0: Event) => void; closeEvent: (args_0: CloseEvent) => void; get open(): boolean { return !!this.process.pid; } pipe(socket: SocketAdapter) { this.on('message', (message) => socket.send(message)); this.on('close', () => socket.close()); } close() { const deferred = new Deferred<void>(); this.process.on('disconnect', () => deferred.resolve()); this.process.disconnect(); return deferred; } send(data: string) { return new Promise<void>((resolve, reject) => this.process.stdin.write(data + '\n', err => err ? reject(err) : resolve())); } off<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>(event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>): boolean { const result = super.off(event, handler); if (!super.hasListener(event)) switch (event) { case 'message': this.process.stdout.off('data', this.messageEvent); break; case 'close': this.process.stdout.off('end', this.closeEvent); break; } return result; } public on<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>, options?: EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]> ): Subscription { if (!this.events[event].hasListeners) switch (event) { case 'message': if (options?.once) this.process.stdout.once('data', this.messageEvent); else this.process.stdout.on('data', this.messageEvent); break; case 'open': handler(null); return () => false; case 'close': if (options?.once) this.process.stdout.once('close', this.closeEvent); else this.process.stdout.on('close', this.closeEvent); break; } return super.on(event, handler, options); } once<const TEvent extends AllEventKeys<SocketAdapterAkalaEventMap>>(event: TEvent, handler: EventListener<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>, options?: Omit<EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>, 'once'>): Subscription { return this.on(event, handler, { ...options, once: true } as EventOptions<AllEvents<SocketAdapterAkalaEventMap>[TEvent]>) } constructor(private process: ChildProcess, abort: AbortSignal) { super(); abort.addEventListener('abort', () => this[Symbol.dispose]()); this.messageEvent = this.getOrCreate('message').emit.bind(this.get('message')); this.closeEvent = this.getOrCreate('close').emit.bind(this.get('close')); } }