import type { Readable, Writable } from "node:stream" import type { Json } from "atom.io/foundations/json" import { stringifyJson } from "atom.io/foundations/json" import { Subject } from "atom.io/foundations/subject" import type { UserKey } from "atom.io/realtime" import { UList } from "atom.io/transceivers/u-list" import type { StderrLog } from "./child-socket.ts" import type { EventPayload, Events } from "./custom-socket.ts" import { CustomSocket, isEventPayload } from "./custom-socket.ts" import { DelimitedJsonCodec, encodeJsonFrame } from "./delimited-json-codec.ts" export const PROOF_OF_LIFE_SIGNAL = `ALIVE` export class SubjectSocket< I extends Events, O extends Events, > extends CustomSocket { public in: Subject> public out: Subject> public id = `no_id_retrieved` public disposalEffects: (() => void)[] = [] public constructor(id: string) { super((...args) => { this.out.next(args as any) return this }) this.id = id this.in = new Subject() this.out = new Subject() this.in.subscribe(`socket`, (event) => { this.handleEvent(...event) }) } public dispose(): void { for (const dispose of this.disposalEffects.splice(0)) { dispose() } } } export type ParentProcess = { pid?: number | undefined stdin: Readable stdout: Writable stderr: Writable exit: (code?: number) => void } export class ParentSocket< I extends Events & { [user: UserKey]: [string, ...Json.Array[]] }, O extends Events & { [id in string as `user::${id}`]: [string, ...Json.Array[]] } & { /* eslint-disable quotes */ "user-joins": [key: UserKey] "user-leaves": [key: UserKey] /* eslint-enable quotes */ }, P extends ParentProcess = ParentProcess, > extends CustomSocket { protected relays: Map> protected initRelay: ( socket: SubjectSocket, userKey: UserKey, ) => (() => void) | void public proc: P public id = `#####` protected log(...args: StderrLog): void { this.proc.stderr.write( stringifyJson( args.map((arg) => arg instanceof UList ? `{ ${arg.toJSON().join(` | `)} }` : arg, ), ) + `\x03`, ) } public logger = { info: (...args: Json.Array): void => { this.log(`i`, ...args) }, warn: (...args: Json.Array): void => { this.log(`w`, ...args) }, error: (...args: Json.Array): void => { this.log(`e`, ...args) }, } public constructor(proc: P) { super((event, ...args) => { this.proc.stdout.write(encodeJsonFrame([event, ...args])) return this }) this.proc = proc this.proc.stdin.resume() this.relays = new Map() this.initRelay = () => { this.logger.info(`🔗`, `nothing to relay`) } const events = new DelimitedJsonCodec>({ onMalformed: (frame, error) => { this.logger.error( `received malformed data from parent process`, frame, String(error), ) }, onValue: (value) => { if (!isEventPayload(value)) { this.logger.error( `received invalid event payload from parent process`, value, ) return } this.logger.info(`🎰`, `received`, value) this.handleEvent(...value) }, }) this.proc.stdin.on(`data`, (buffer: Buffer) => { events.write(buffer) }) this.on(`exit`, () => { this.logger.info(`🔥`, this.id, `received "exit"`) this.proc.exit(0) }) if (this.proc.pid) { this.id = this.proc.pid?.toString() } this.on(`user-joins`, (userKey: UserKey) => { this.logger.info(`👤`, userKey, `joined`) const existingRelay = this.relays.get(userKey) if (existingRelay) { this.logger.info(`🔗`, `reattaching relay services for`, userKey) this.off(userKey) existingRelay.dispose() const cleanupRelay = this.initRelay(existingRelay, userKey) if (cleanupRelay) { existingRelay.disposalEffects.push(cleanupRelay) } this.on(userKey, (...data) => { existingRelay.in.next(data) }) existingRelay.disposalEffects.push( existingRelay.out.subscribe(`socket`, (data) => { this.emit(userKey, ...(data as any)) }), ) return } const relay = new SubjectSocket(userKey) this.relays.set(userKey, relay) this.logger.info(`🔗`, `attaching relay services for`, userKey) const cleanupRelay = this.initRelay(relay, userKey) if (cleanupRelay) { relay.disposalEffects.push(cleanupRelay) } this.on(userKey, (...data) => { relay.in.next(data) }) relay.disposalEffects.push( relay.out.subscribe(`socket`, (data) => { this.emit(userKey, ...(data as any)) }), ) }) this.on(`user-leaves`, (userKey: UserKey) => { const relay = this.relays.get(userKey) this.off(userKey) if (relay) { relay.dispose() this.relays.delete(userKey) } }) this.proc.stdout.write(encodeJsonFrame(PROOF_OF_LIFE_SIGNAL)) } public receiveRelay( attachServices: ( socket: SubjectSocket, userKey: UserKey, ) => (() => void) | void, ): void { this.initRelay = attachServices this.logger.info(`🔗`, `ready to relay`) } }