posipaki 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/{define-actor-D7c9vn6S.js → define-actor-DR1P1PA5.js} +3 -3
  2. package/dist/define-actor-DR1P1PA5.js.map +1 -0
  3. package/dist/hooks-CgXhmY4r.js.map +1 -1
  4. package/dist/{hooks-D1QaP6Vv.d.ts → hooks-Dpbe39oa.d.ts} +5 -2
  5. package/dist/hooks-Dpbe39oa.d.ts.map +1 -0
  6. package/dist/hooks.d.ts +1 -1
  7. package/dist/index.d.ts +4 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -3
  10. package/dist/plugins/debug-logger.d.ts +7 -5
  11. package/dist/plugins/debug-logger.d.ts.map +1 -1
  12. package/dist/plugins/debug-logger.js +71 -16
  13. package/dist/plugins/debug-logger.js.map +1 -1
  14. package/dist/plugins/tree-introspection.d.ts +2 -2
  15. package/dist/plugins/tree-introspection.js +1 -1
  16. package/dist/{process-BVwU7TQo.d.ts → process-CoEKoY5f.d.ts} +2 -2
  17. package/dist/{process-BVwU7TQo.d.ts.map → process-CoEKoY5f.d.ts.map} +1 -1
  18. package/dist/{process.async-C_-NpMFX.js → process.async-B1FqZyD1.js} +11 -2
  19. package/dist/process.async-B1FqZyD1.js.map +1 -0
  20. package/dist/remote/gateway-cli.js +30 -11
  21. package/dist/remote/gateway-cli.js.map +1 -1
  22. package/dist/remote/index.d.ts +5 -5
  23. package/dist/remote/index.d.ts.map +1 -1
  24. package/dist/remote/index.js +15 -13
  25. package/dist/remote/index.js.map +1 -1
  26. package/dist/remote/node.d.ts +112 -5
  27. package/dist/remote/node.d.ts.map +1 -1
  28. package/dist/remote/node.js +126 -87
  29. package/dist/remote/node.js.map +1 -1
  30. package/dist/testing/index.d.ts +2 -2
  31. package/dist/{types-DYd-DDFB.d.ts → types-CuabnerI.d.ts} +35 -3
  32. package/dist/types-CuabnerI.d.ts.map +1 -0
  33. package/dist/{version-B97iGJJk.js → version-B6eNJRZD.js} +2 -2
  34. package/dist/{version-B97iGJJk.js.map → version-B6eNJRZD.js.map} +1 -1
  35. package/dist/{websocket-Dqd8Hu6G.js → websocket-DecgMfb3.js} +105 -26
  36. package/dist/websocket-DecgMfb3.js.map +1 -0
  37. package/dist/{websocket-Dyfw3WDD.d.ts → websocket-sGQcEDbm.d.ts} +111 -124
  38. package/dist/websocket-sGQcEDbm.d.ts.map +1 -0
  39. package/dist/xfetch.d.ts +2 -2
  40. package/package.json +1 -1
  41. package/dist/define-actor-D7c9vn6S.js.map +0 -1
  42. package/dist/hooks-D1QaP6Vv.d.ts.map +0 -1
  43. package/dist/process.async-C_-NpMFX.js.map +0 -1
  44. package/dist/types-DYd-DDFB.d.ts.map +0 -1
  45. package/dist/websocket-Dqd8Hu6G.js.map +0 -1
  46. package/dist/websocket-Dyfw3WDD.d.ts.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"process.async-B1FqZyD1.js","names":[],"sources":["../src/adapters.ts","../src/process.async.ts"],"sourcesContent":["import type { ProcessFn, AsyncProcessFn, Message } from \"./types.js\";\n\nexport function asyncify<A, S, IM extends Message, OM extends Message>(\n fn: ProcessFn<A, S, IM, OM>,\n): AsyncProcessFn<A, S, IM, OM> {\n return async function* (ctx, args) {\n yield* fn(ctx, args) as any;\n };\n}\n","import { defer, makeWaiter, debugLog, withTimeout } from \"./util.js\";\nimport type { DeferredCall, Waiter } from \"./util.js\";\nimport type {\n Message,\n WithSender,\n SenderInfo,\n ExitMessage,\n StopMessage,\n FrameChannel,\n ProcessCtx,\n AsyncProcessFn,\n ProcessFn,\n} from \"./types.js\";\nimport { asyncify } from \"./adapters.js\";\n\n// ---- types ------------------------------------------------------------------\n\n/** An async iterator over process state. Receives `WithSender<InMessage | StopMessage>`. */\ntype AsyncProcessGenerator<ProcessState, InMessage extends Message> = AsyncGenerator<\n ProcessState | null,\n void,\n WithSender<InMessage | StopMessage>\n>;\n\ntype NotifyFn = () => void;\n\n/**\n * Framework-internal traffic: a message no actor ever sees. Used to resume the\n * generator without delivering anything — `start()` uses it to move past the\n * initial yield, and `wake()` to give a parked dispatch loop one chance to\n * notice that it is done. Actors skip it before any hook or handler runs.\n */\nexport const INTERNAL_ADVANCE = \"__ADVANCE__\";\n\n/** Sender stamp for the messages above. */\nconst internalSender = (): SenderInfo => ({\n fromName: \"__internal__\",\n fromId: Symbol(\"__internal__\"),\n});\n\n// ---- runDispatchAsync -------------------------------------------------------\n\ntype AsyncReducer<M> = (msg: M) => Promise<void>;\ntype ReadyFn = () => boolean;\n\n/**\n * Async equivalent of `runDispatch`. Loops, yielding `null` and feeding\n * each incoming message to an `async` reducer. Exits when `readyFn()`\n * returns true.\n */\nexport async function* runDispatchAsync<M>(\n name: string,\n fn: AsyncReducer<M>,\n readyFn: ReadyFn = () => false,\n debugLevel = false,\n): AsyncGenerator<null, void, M> {\n let msg: M;\n while (!readyFn()) {\n msg = yield null;\n debugLog(debugLevel, \"msg\", name, \" <- \", msg);\n await fn(msg);\n }\n}\n\n// ---- AsyncProcess -----------------------------------------------------------\n\n/** A subscriber to this process's message channel: receives the message and\n * its sender separately (not a `WithSender` tuple). */\ntype MessageCallback<M extends Message> = (msg: M, from: SenderInfo) => void;\n\n/** How long a parent waits for a child to stop before continuing shutdown. */\nconst CHILD_STOP_TIMEOUT_MS = 1_000;\n\n/**\n * A process driven by an async generator. Functionally identical to\n * {@link Process} but supports `await` inside reducers.\n *\n * Messages are processed **one at a time** — if a tick is already\n * in-flight, new messages are buffered and processed when the current\n * tick completes.\n */\nexport class AsyncProcess<\n Args,\n State,\n InMessage extends Message,\n OutMessage extends Message,\n ReflectionMethods extends object,\n> {\n /** Reflection methods. Empty by default — defineActor fills in configured\n * methods, each wrapped so calling one answers with a promise whatever the\n * method was written as. */\n $reflection: ReflectionMethods = {} as ReflectionMethods;\n pgenerator: AsyncProcessFn<Args, State, InMessage, OutMessage>;\n pname: string;\n /**\n * The connection this process holds, or null. Its own fact, like `pname`: read through\n * `ctx.connection` and set where the connection is made — at spawn (`{ connection }`),\n * or by the process itself when it opens one.\n */\n connection: FrameChannel | null = null;\n /** Subscribers to this process's message channel (`ctx.toParent`). */\n private messageSubscribers: Array<MessageCallback<OutMessage>> = [];\n id: symbol;\n state: State | null;\n\n private current: AsyncProcessGenerator<State, InMessage> | null = null;\n /** Every buffered message carries sender provenance. */\n private buffer: Array<WithSender<InMessage | StopMessage>> = [];\n private nextTick: DeferredCall | null = null;\n /** Child processes forked from this one (any fork method). */\n children: Array<AsyncProcess<unknown, unknown, Message, Message, {}>> = [];\n /** Children inherited from a child that exited (see ctx-orphans proposal). */\n orphans: Array<AsyncProcess<unknown, unknown, Message, Message, {}>> = [];\n private stateSubscribers: Array<NotifyFn> = [];\n /** Unsubscribe handles for outgoing message subscriptions (monitor only). */\n private pvtOutgoingSubscriptions: Array<() => void> = [];\n /** Unsubscribe handles for adopt (ownership) subscriptions, keyed by child id. */\n private pvtAdoptUnsubs: Map<symbol, () => void> = new Map();\n /** Live buffer while this process is an unowned orphan mid-handoff. */\n private pvtPending: Array<WithSender<Message>> | null = null;\n /** Unsubscribe for the collector currently buffering into pvtPending. */\n private pvtCollectorUnsub: (() => void) | null = null;\n private exitWaiter: Waiter;\n private pvtIsPaused: boolean = false;\n private pvtDead: boolean = false;\n /**\n * Set once the initial state exists. An actor whose start failed never got\n * one: it is dead on arrival, and its end-of-life hooks must not run against\n * a state that never arrived (see `pvtStartFailed`).\n */\n private pvtStarted: boolean = false;\n private pvtTickInProgress: boolean = false;\n /** True once the initial state has been delivered and `start()` has advanced\n * the generator into its dispatch loop. Until then no tick may run: the\n * generator is still suspended at the initial yield, which belongs to\n * `start()` and would swallow a message handed to it (see `start()`). */\n private pvtDispatchLive: boolean = false;\n private pvtExitReject: ((e: unknown) => void) | null = null;\n private pvtReady!: Waiter;\n private pvtResolveReady!: () => void;\n\n constructor(\n fn: AsyncProcessFn<Args, State, InMessage, OutMessage>,\n pname: string,\n toParent?: MessageCallback<OutMessage>,\n ) {\n this.pgenerator = fn;\n this.pname = pname;\n if (toParent) this.messageSubscribers.push(toParent);\n this.id = Symbol(pname);\n this.state = null;\n this.exitWaiter = makeWaiter();\n this.pvtReady = makeWaiter();\n this.pvtResolveReady = this.pvtReady.resolve;\n // `ready()` carries a failed start to whoever awaited it. A caller that\n // never awaited it must not turn that into an unhandled rejection.\n void this.pvtReady.promise.catch(() => {});\n }\n\n /** Compact handle for `util.inspect` — actor state often holds process\n * references, and dumping the internals would be unreadable. */\n [Symbol.for(\"nodejs.util.inspect.custom\")](): string {\n return `AsyncProcess<${this.pname}>`;\n }\n\n /** Promise that resolves once the initial state is available. */\n ready(): Promise<void> {\n return this.pvtReady.promise;\n }\n\n // ---- lifecycle ------------------------------------------------------------\n\n /**\n * Kick off the async generator. The first `yield` sets the initial\n * state; for async generators this happens in a microtask.\n */\n start(arg0: Args, parentName?: string | null, parentId?: symbol | null) {\n const selfCtx: SenderInfo = { fromName: this.pname, fromId: this.id };\n\n const proc = this;\n const ctx: ProcessCtx<Args, State, InMessage, OutMessage> = {\n pname: this.pname,\n id: this.id,\n self: this,\n parentName: parentName ?? null,\n parentId: parentId ?? null,\n get connection() {\n return proc.connection;\n },\n set connection(value: FrameChannel | null) {\n proc.connection = value;\n },\n fork: this.fork.bind(this),\n forkSync: this.forkSync.bind(this),\n children: this.children,\n orphans: this.orphans,\n adopt: this.adopt.bind(this),\n monitor: this.monitor.bind(this),\n sendSelf: (msg) => {\n this.send([msg, selfCtx]);\n },\n wake: () => {\n this.pvtWake();\n },\n toParent: (msg) => {\n this.messageSubscribers.forEach((cb) => cb(msg as OutMessage, selfCtx));\n },\n notify: () => {\n this.notify();\n },\n };\n\n this.current = this.pvtWatchExit(ctx, arg0);\n void this.current.next().then(\n (ret: IteratorResult<State | null, void>) => {\n this.state = ret.value ?? null;\n this.pvtStarted = true;\n this.pvtResolveReady();\n if (ret.done) {\n this.exitWaiter.resolve();\n return;\n }\n // Advance past the initial yield so the pvtWatchExit generator\n // runs its finally block (EXIT/STOP) and the inner generator\n // enters its dispatch loop.\n const advance: WithSender<InMessage | StopMessage> = [\n { type: INTERNAL_ADVANCE } as InMessage,\n internalSender(),\n ];\n // Only now may a tick feed the generator. Anything that arrived while\n // we were still setting up waited in the buffer: no tick is pending for\n // it (they were all held back), so schedule one.\n this.pvtDispatchLive = true;\n this.pvtEatResult(this.current!.next(advance));\n if (this.buffer.length > 0) this.pvtScheduleTick();\n },\n // The generator threw before its first yield: setup() — or a hook that\n // runs before it — failed, so there is no state and nothing to dispatch.\n (e: unknown) => this.pvtStartFailed(e),\n );\n return this;\n }\n\n /**\n * The process never started: its first `next()` rejected before any state\n * existed. The failure *is* the outcome, so `ready()` carries it — `spawn()`\n * waits on `ready()`, which is what turns a spawner that fails into a spawn\n * that fails, with its own error intact. Whoever is already parked in\n * `wait()` is told as well; a later `wait()` sees the process as ended.\n */\n private pvtStartFailed(e: unknown): void {\n this.pvtDead = true;\n // Abandon the generator: it is already finished (it threw), but nothing\n // may tick it again.\n this.current = null;\n this.buffer.length = 0;\n this.nextTick?.cancel();\n this.nextTick = null;\n this.pvtUnsubscribeAll();\n\n this.pvtReady.reject(e);\n this.pvtExitReject?.(e);\n this.pvtExitReject = null;\n this.exitWaiter.resolve();\n }\n\n /** Wrap the user's generator so EXIT/STOP logic fires on completion. */\n private async *pvtWatchExit(\n ctx: ProcessCtx<Args, State, InMessage, OutMessage>,\n arg0: Args,\n ): AsyncProcessGenerator<State, InMessage> {\n try {\n yield* this.pgenerator(ctx, arg0);\n } finally {\n // Cascade: stop every child (nice) and collect the refusers as orphans.\n const orphans: Array<AnyProcess> = [...this.orphans];\n const parentSender: SenderInfo = { fromName: this.pname, fromId: this.id };\n const stopPromises = this.children.map(async (child) => {\n const stopped = await child.stop({ from: parentSender });\n if (!stopped) {\n orphans.push(child);\n console.warn(\n `posipaki: child \"${child.pname}\" did not stop within ${CHILD_STOP_TIMEOUT_MS}ms; continuing shutdown`,\n );\n }\n });\n\n await Promise.all(stopPromises);\n // Hand surviving children and inherited orphans up to the parent for adoption (see ctx-orphans proposal).\n // In-process only.\n this.pvtPrepareHandoff(orphans);\n // Why it ended, when it said: a stop that was agreed to is not the news a wire that went\n // away is (`CHANNEL_LOST`), and neither is a child that finished what it was for.\n ctx.toParent({\n type: \"EXIT\",\n ...(ctx.exitReason === undefined ? {} : { reason: ctx.exitReason }),\n orphans,\n });\n // Unsubscribe from children/monitored processes so a dead parent\n // doesn't keep receiving their messages (handoff collectors stay on the orphans).\n this.pvtUnsubscribeAll();\n // An actor that never got a state never started, and `afterEnd` is handed\n // a context whose state is typed as present: running it there means every\n // implementation that reads `this.state` trips over an undefined one —\n // and swaps the caller's error for its own TypeError.\n if (ctx.afterExit && this.pvtStarted) await ctx.afterExit();\n }\n }\n\n // ---- fork -----------------------------------------------------------------\n\n fork<ChildArgs, ChildState, ChildIM extends Message, ChildOM extends Message>(\n fn: AsyncProcessFn<ChildArgs, ChildState, ChildIM, ChildOM>,\n pname: string,\n ): (args: ChildArgs) => AsyncProcess<ChildArgs, ChildState, ChildIM, ChildOM, {}> {\n return (args: ChildArgs) => {\n const child = new AsyncProcess<ChildArgs, ChildState, ChildIM, ChildOM, {}>(fn, pname);\n this.adopt(child);\n child.start(args, this.pname, this.id);\n return child;\n };\n }\n\n forkSync<ChildArgs, ChildState, ChildIM extends Message, ChildOM extends InMessage>(\n fn: ProcessFn<ChildArgs, ChildState, ChildIM, ChildOM>,\n pname: string,\n ): (args: ChildArgs) => AsyncProcess<ChildArgs, ChildState, ChildIM, ChildOM, {}> {\n return this.fork(asyncify(fn), pname);\n }\n\n // ---- message processing ---------------------------------------------------\n\n /**\n * Give a parked dispatch loop one chance to notice a flag that was set from\n * outside it. Clearing `done` is not enough on its own: an idle process is\n * suspended at its dispatch yield, and nothing re-reads the flag until\n * something resumes it. A stop that arrives as a message (STOP) resumes the\n * loop by itself; this is for the paths that do not.\n */\n pvtWake(): void {\n if (this.pvtDead || !this.pvtDispatchLive) return;\n // The loop is running right now: it re-checks the flag as soon as the\n // reducer returns, so a wake-up message would only sit in the buffer.\n if (this.pvtTickInProgress) return;\n this.send([{ type: INTERNAL_ADVANCE } as InMessage, internalSender()]);\n }\n\n protected async pvtTick(): Promise<void> {\n // A message that arrives before the initial state is delivered is kept in\n // the buffer, not fed to the generator: the generator is still suspended at\n // its initial yield, and the value sent to that yield is discarded — the\n // message would be lost and `start()`'s advance would take its place.\n if (!this.current || !this.pvtDispatchLive || this.pvtTickInProgress) return;\n\n this.pvtTickInProgress = true;\n try {\n let msgAndSender: WithSender<InMessage | StopMessage> | undefined;\n let ret: IteratorResult<State | null, void> | null = null;\n while ((msgAndSender = this.buffer.shift()) !== undefined) {\n ret = await this.pvtSafeNext(msgAndSender);\n if (!ret || ret.done) break;\n }\n this.notify();\n this.pvtEatResult(ret);\n } catch (e) {\n this.pvtExitReject?.(e);\n this.pvtExitReject = null;\n } finally {\n this.pvtTickInProgress = false;\n }\n }\n\n /** Call `.next()` and redirect unhandled rejections. */\n private async pvtSafeNext(\n msgAndSender: WithSender<InMessage | StopMessage>,\n ): Promise<IteratorResult<State | null, void> | null> {\n try {\n return await this.current!.next(msgAndSender);\n } catch (e) {\n this.pvtExitReject?.(e);\n this.pvtExitReject = null;\n return { done: true, value: undefined };\n }\n }\n\n private pvtEatResult(\n ret: IteratorResult<State | null, void> | Promise<IteratorResult<State | null, void>> | null,\n ): void {\n if (!ret) return;\n Promise.resolve(ret).then((r) => {\n if (r.done) {\n this.exitWaiter.resolve();\n }\n });\n }\n\n /** Broadcast a message to all children. */\n toAllChildren(msg: Message): void {\n const stamp: SenderInfo = { fromName: this.pname, fromId: this.id };\n this.children.forEach((p) => p.send([msg, stamp] as WithSender<Message>));\n }\n\n /** Stamp and enqueue a message from a named sender (external API). */\n send(msg: InMessage | StopMessage, from?: SenderInfo): void;\n /** Enqueue a pre-stamped message (internal: fromChild, toAllChildren). */\n send(msgAndSender: WithSender<InMessage | StopMessage>): void;\n send(\n msgOrTuple: InMessage | StopMessage | WithSender<InMessage | StopMessage>,\n from?: SenderInfo,\n ): void {\n if (this.pvtDead) return;\n if (\"type\" in msgOrTuple) {\n this.buffer.push([msgOrTuple as InMessage | StopMessage, from as SenderInfo]);\n } else {\n this.buffer.push(msgOrTuple as WithSender<InMessage | StopMessage>);\n }\n this.pvtScheduleTick();\n }\n\n /**\n * Synchronously flush the buffer. For sync processes use {@link Process.tick};\n * for async processes this is **not guaranteed** to process everything\n * immediately if reducers contain `await`. Prefer `send()` + `await proc.wait()`.\n */\n tick(): void {\n this.nextTick?.flush();\n this.nextTick = null;\n }\n\n private pvtScheduleTick(): void {\n if (this.pvtIsPaused || this.pvtDead) return;\n\n this.nextTick?.cancel();\n this.nextTick = defer(() => {\n this.nextTick = null;\n void this.pvtTick();\n });\n }\n\n // ---- subscribers ----------------------------------------------------------\n\n notify(): void {\n this.stateSubscribers.forEach((f) => f());\n }\n\n get isListenedTo(): boolean {\n return this.stateSubscribers.length > 0;\n }\n\n subscribe(channel: \"message\", cb: MessageCallback<OutMessage>): () => void;\n subscribe(channel: \"state\", cb: NotifyFn): () => void;\n /** @deprecated Use `subscribe(\"state\", cb)`. */\n subscribe(cb: NotifyFn): () => void;\n subscribe(\n channelOrCb: \"message\" | \"state\" | NotifyFn,\n cb?: MessageCallback<OutMessage> | NotifyFn,\n ): () => void {\n if (typeof channelOrCb === \"function\") {\n return this.subscribe(\"state\", channelOrCb);\n }\n if (channelOrCb === \"message\") {\n const fn = cb as MessageCallback<OutMessage>;\n this.messageSubscribers.push(fn);\n return () => this.pvtUnsubscribe(this.messageSubscribers, fn);\n }\n const fn = cb as NotifyFn;\n this.stateSubscribers.push(fn);\n return () => this.pvtUnsubscribe(this.stateSubscribers, fn);\n }\n\n private pvtUnsubscribe<T>(list: Array<T>, fn: T): void {\n const idx = list.indexOf(fn);\n if (idx >= 0) list.splice(idx, 1);\n }\n\n // ---- pause / resume -------------------------------------------------------\n\n /**\n * Freeze dispatch: no further ticks are scheduled, so the process stops\n * *processing* incoming messages. This is half-working as a \"quiesce\"\n * primitive — it does not stop the process from *emitting* via async\n * callbacks (timers, fetch) that bypass the dispatch loop.\n */\n pause(): void {\n this.nextTick?.cancel();\n this.nextTick = null;\n this.pvtIsPaused = true;\n }\n\n resume(): void {\n this.pvtIsPaused = false;\n this.pvtScheduleTick();\n }\n\n /**\n * Hard-kill this process: abandon the generator without running its\n * `finally` (no EXIT, no STOP cascade), drop the inbox and observers,\n * unsubscribe from children/monitors, and settle `wait()`/`ready()`.\n *\n * There is nothing to observe afterward — callers remove the process from\n * their `children`/`orphans` lists imperatively. Does not cascade to this\n * process's own children; they are abandoned (force-stop them separately if\n * owned).\n *\n * Best-effort: an idle generator (suspended at `yield`) is abandoned\n * cleanly; a generator mid-`await` completes its `finally` when that await\n * settles, because JS offers no way to cancel a pending promise.\n */\n forceStop(opts?: { cascade?: boolean }): void {\n if (this.pvtDead) return;\n this.pvtDead = true;\n\n // Abandon the generator so its finally block never runs.\n this.current = null;\n\n // Drop the inbox and stop scheduling.\n this.buffer.length = 0;\n this.nextTick?.cancel();\n this.nextTick = null;\n\n // Release incoming observers and outgoing subscriptions.\n this.messageSubscribers.length = 0;\n this.stateSubscribers.length = 0;\n this.pvtUnsubscribeAll();\n // Drop any handoff state this process carried as an unowned orphan.\n this.unparent();\n\n // Settle waiters.\n this.pvtResolveReady();\n this.exitWaiter.resolve();\n this.pvtExitReject = null;\n\n if (opts?.cascade) {\n // Stop the children too, starting with the nice stop (fire-and-forget:\n // the hard kill itself does not block on their graceful exit).\n const selfSender: SenderInfo = { fromName: this.pname, fromId: this.id };\n for (const child of this.children) {\n void child.stop({ force: true, from: selfSender }).catch(() => {});\n }\n }\n }\n\n /**\n * Stop this process. Sends STOP (the nice way — the generator cooperates\n * and its `finally` runs) and awaits on a best-effort basis.\n *\n * With `{ force: true }`, when the process refuses (does not exit within the\n * timeout) it escalates: fires `generator.return()` *without awaiting* (throw\n * the generator away for GC to find), cascades to children with the same\n * ladder, then hard-kills itself.\n *\n * Resolves `true` if the process stopped (gracefully or forced), `false` if\n * it refused and `force` was not requested.\n */\n async stop(opts?: { force?: boolean; from?: SenderInfo }): Promise<boolean> {\n if (this.pvtDead) return true;\n\n this.send({ type: \"STOP\" } as StopMessage, opts?.from);\n\n try {\n await withTimeout(this.wait(), CHILD_STOP_TIMEOUT_MS, \"stop\");\n return true;\n } catch (e) {\n if ((e as Error)?.message !== \"Timeout:stop\") throw e;\n // refused to stop within the timeout\n }\n\n if (!opts?.force) return false;\n\n // Escalate: give the finally a chance to run (fire-and-forget), but do not\n // block on it — the process may be stuck awaiting a dead promise.\n void this.current?.return(undefined);\n\n // Cascade to children with the same ladder, stamped from this process.\n const selfSender: SenderInfo = { fromName: this.pname, fromId: this.id };\n await Promise.all(\n [...this.children].map((child) => child.stop({ force: true, from: selfSender })),\n );\n\n this.forceStop();\n return true;\n }\n\n // ---- waiting --------------------------------------------------------------\n\n /**\n * Returns a promise that resolves when the generator completes, or\n * rejects if an unhandled error occurs during message processing.\n */\n wait(): Promise<void> {\n return new Promise<void>((resolve, reject) => {\n this.pvtExitReject = reject;\n\n this.exitWaiter.promise.then(\n () => {\n this.pvtExitReject = null;\n resolve();\n },\n (e) => {\n this.pvtExitReject = null;\n reject(e);\n },\n );\n });\n }\n\n // ---- child messages -------------------------------------------------------\n\n /**\n * Tap another process's message channel: every message it emits is fed\n * into this process's incoming queue. No ownership — the sender's EXIT\n * does not touch `children`/`orphans`.\n */\n monitor<ChildArgs, ChildState, ChildIM extends Message, ChildOM extends Message>(\n child: AsyncProcess<ChildArgs, ChildState, ChildIM, ChildOM, {}>,\n ): () => void {\n const unsub = child.subscribe(\"message\", (msg, from) => {\n this.send([msg, from] as WithSender<InMessage | StopMessage>);\n });\n this.pvtOutgoingSubscriptions.push(unsub);\n return unsub;\n }\n\n /**\n * Claim another process as a child: its messages are routed here and its\n * EXIT removes it from `children` (collecting its orphans). Generalizes\n * what `fork` does — used for both spawning and orphan adoption.\n */\n adopt<ChildArgs, ChildState, ChildIM extends Message, ChildOM extends Message>(\n child: AsyncProcess<ChildArgs, ChildState, ChildIM, ChildOM, {}>,\n ): () => void {\n this.children.push(child as unknown as AnyProcess);\n const unsub = child.subscribe(\"message\", (msg, from) => {\n this.pvtChildMessage(msg, from);\n });\n this.pvtAdoptUnsubs.set(child.id, unsub);\n // Lossless handoff: drain a pending buffer left by the previous parent.\n this.pvtDrainPending(child as unknown as AnyProcess);\n return unsub;\n }\n\n /** Handles a child's emitted message: EXIT cleanup + forward to the queue. */\n private pvtChildMessage(msg: Message, sender: SenderInfo): void {\n if (msg.type === \"EXIT\") {\n // sender.fromId === child's id (stamped by the child's ctx.toParent)\n // Mutate in place so ctx.children (a live reference) stays consistent.\n const idx = this.children.findIndex((p) => p.id === sender.fromId);\n if (idx >= 0) this.children.splice(idx, 1);\n const orphans = (msg as ExitMessage).orphans;\n if (orphans && orphans.length > 0) {\n this.orphans.push(...orphans);\n }\n }\n this.send([msg, sender] as WithSender<InMessage | StopMessage>);\n }\n\n // ---- orphan handoff -------------------------------------------------------\n\n /** Unsubscribe from all outgoing subscriptions (monitors + adopt). */\n private pvtUnsubscribeAll(): void {\n for (const unsub of this.pvtOutgoingSubscriptions) unsub();\n this.pvtOutgoingSubscriptions = [];\n for (const unsub of this.pvtAdoptUnsubs.values()) unsub();\n this.pvtAdoptUnsubs.clear();\n }\n\n /** Prepare surviving children and inherited orphans for a lossless handoff:\n * recover undrained messages and install a collector on each orphan. */\n private pvtPrepareHandoff(orphans: Array<AnyProcess>): void {\n for (const orphan of orphans) {\n this.pvtBackFeed(orphan);\n if (!orphan.pvtCollectorUnsub) {\n this.pvtSwapToCollector(orphan);\n }\n }\n }\n\n /** Recover messages the orphan emitted into my inbox that I never drained. */\n private pvtBackFeed(orphan: AnyProcess): void {\n const fromOrphan: Array<WithSender<Message>> = [];\n this.buffer = this.buffer.filter((entry) => {\n if (entry[1].fromId === orphan.id) {\n fromOrphan.push(entry);\n return false;\n }\n return true;\n });\n if (fromOrphan.length === 0) return;\n if (!orphan.pvtPending) orphan.pvtPending = [];\n orphan.pvtPending.unshift(...fromOrphan);\n }\n\n /** Replace my ownership subscription to `orphan` with a handoff collector. */\n private pvtSwapToCollector(orphan: AnyProcess): void {\n const mine = this.pvtAdoptUnsubs.get(orphan.id);\n if (mine) {\n mine();\n this.pvtAdoptUnsubs.delete(orphan.id);\n }\n if (!orphan.pvtPending) orphan.pvtPending = [];\n const pending = orphan.pvtPending;\n orphan.pvtCollectorUnsub = orphan.subscribe(\"message\", (msg, from) => {\n pending.push([msg, from]);\n });\n }\n\n /** Drain a handed-off orphan's pending buffer into my inbox (lossless adopt). */\n private pvtDrainPending(orphan: AnyProcess): void {\n if (orphan.pvtCollectorUnsub) {\n orphan.pvtCollectorUnsub();\n orphan.pvtCollectorUnsub = null;\n }\n const pending = orphan.pvtPending;\n if (!pending) return;\n orphan.pvtPending = null;\n for (const [msg, from] of pending) {\n this.pvtChildMessage(msg, from);\n }\n }\n\n /**\n * Drop this process's handoff state (collector + pending buffer) without\n * stopping it. Used by `onOrphan`'s \"unparent\" decision: the orphan keeps\n * running and stays in the orphans list, but its in-flight buffer is gone.\n */\n unparent(): void {\n if (this.pvtCollectorUnsub) {\n this.pvtCollectorUnsub();\n this.pvtCollectorUnsub = null;\n }\n this.pvtPending = null;\n }\n}\nexport type AnyProcess = AsyncProcess<unknown, unknown, Message, Message, {}>;\n\n/**\n * Whether a value is a process, judged by what it holds rather than by which\n * class built it. `instanceof` would answer about the bundle, not the value: a\n * build that inlines its own copy of this class per entry point (one dist file\n * importing the core, another doing the same) has two constructors for one kind\n * of thing, and a process from one is not an instance of the other.\n */\nexport function isProcess(value: unknown): value is AnyProcess {\n if (typeof value !== \"object\" || value === null) return false;\n const candidate = value as Partial<AnyProcess>;\n return (\n typeof candidate.pname === \"string\" &&\n typeof candidate.id === \"symbol\" &&\n typeof candidate.send === \"function\" &&\n typeof candidate.fork === \"function\"\n );\n}\n// ---- spawnAsync -------------------------------------------------------------\n\n/**\n * Spawn a new async process. Accepts both sync and async process\n * functions — sync ones are automatically wrapped with {@link asyncify}.\n */\nexport function spawnAsync<\n Args,\n State,\n InMessage extends Message = Message,\n OutMessage extends Message = ExitMessage,\n>(\n fn: AsyncProcessFn<Args, State, InMessage, OutMessage>,\n pname: string,\n toParent?: MessageCallback<OutMessage>,\n parentName?: string | null,\n parentId?: symbol | null,\n connection?: FrameChannel | null,\n): (args: Args) => AsyncProcess<Args, State, InMessage, OutMessage, {}> {\n return (args: Args) => {\n const proc = new AsyncProcess<Args, State, InMessage, OutMessage, {}>(fn, pname, toParent);\n proc.start(args, parentName, parentId);\n proc.connection = connection ?? null;\n return proc;\n };\n}\n"],"mappings":"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"}
@@ -3,6 +3,7 @@ import { mkdtemp, open, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { basename, join } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { Writable } from "node:stream";
6
7
  import * as readline from "node:readline";
7
8
  //#region src/remote/channel.ts
8
9
  const ERROR_KEY = "$error";
@@ -10,9 +11,33 @@ const ERROR_KEY = "$error";
10
11
  function errorFrame$1(reason) {
11
12
  return { [ERROR_KEY]: reason };
12
13
  }
14
+ /** A frame of the far end's own output: `data` on the stream `fd`. */
15
+ function outputFrame(fd, data) {
16
+ return {
17
+ $fd: fd,
18
+ data
19
+ };
20
+ }
21
+ //#endregion
22
+ //#region src/remote/protocols/json1.ts
23
+ function encode(frame) {
24
+ return JSON.stringify(frame);
25
+ }
26
+ //#endregion
27
+ //#region src/remote/streams.ts
28
+ /**
29
+ * A writable that writes what is written to it as output frames of one fd: one chunk is one frame.
30
+ *
31
+ * The shape of an output frame lives here rather than at each caller — the gateway relays a payload's
32
+ * own output and holds no channel — and a send that fails is the stream's error, not a lost line.
33
+ */
34
+ function fdWriter(send, fd) {
35
+ return new Writable({ write(chunk, _encoding, done) {
36
+ Promise.resolve(send(outputFrame(fd, String(chunk)))).then(() => done(), (error) => done(error));
37
+ } });
38
+ }
13
39
  //#endregion
14
40
  //#region src/remote/stdio.ts
15
- const OUTPUT_KEY = "$fd";
16
41
  /** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */
17
42
  var StdioWireError = class extends Error {
18
43
  constructor(message) {
@@ -20,12 +45,6 @@ var StdioWireError = class extends Error {
20
45
  this.name = "StdioWireError";
21
46
  }
22
47
  };
23
- function outputFrame(fd, data) {
24
- return JSON.stringify({
25
- [OUTPUT_KEY]: fd,
26
- data
27
- });
28
- }
29
48
  /** An `$error` frame as a line. The word and its shape are the shared vocabulary's
30
49
  * (channel.ts) — one word for a wire that cannot be spoken over, whether the failure was at
31
50
  * the start of a session or in the middle of one; what this file owns is its line form. */
@@ -306,12 +325,12 @@ async function fail(wire, reason) {
306
325
  await wire.send(errorFrame(reason));
307
326
  } catch {}
308
327
  }
309
- /** Copy everything the payload prints onto the wire as output frames. */
328
+ /** Copy everything the payload prints to the client, as output frames of the fd it came in on. */
310
329
  function forwardOutput(stream, wire, fd) {
311
330
  stream.setEncoding("utf-8");
312
- stream.on("data", (chunk) => {
313
- wire.send(outputFrame(fd, chunk)).catch(() => {});
314
- });
331
+ const out = fdWriter((frame) => wire.send(encode(frame)), fd);
332
+ out.on("error", () => {});
333
+ stream.pipe(out, { end: false });
315
334
  }
316
335
  /**
317
336
  * Serve one environment over stdin/stdout. Returns the exit code; the caller
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-cli.js","names":["errorFrame","wireErrorFrame","errorText","Lines"],"sources":["../../src/remote/channel.ts","../../src/remote/stdio.ts","../../src/remote/transports/fifo.ts","../../src/remote/gateway.ts","../../src/remote/gateway-cli.ts"],"sourcesContent":["// ── Frame channel ──────────────────────────────────────────────────────────\n//\n// The seam's shared vocabulary. A StringTransport moves encoded frames\n// (strings); a Channel moves decoded frame objects. The frame guards narrow a\n// decoded frame to one of the `$`-key shapes. server.ts / client.ts speak only\n// in Channels and these guards — they never see a specific protocol or\n// transport.\n\nimport type { Message } from \"../types.js\";\nimport {\n decodeProcessRefs,\n encodeProcessRefs,\n type ProcessTable,\n type RefResolver,\n} from \"./process-ref.js\";\n\nexport interface StringTransport {\n send(frame: string): void | Promise<void>;\n onMessage(handler: (frame: string) => void): void;\n removeHandler(): void;\n /** Fires when the peer disconnects (transport closes from the other side). */\n onClose(handler: () => void): void;\n close(): Promise<void>;\n}\n\nexport interface Channel {\n send(frame: Record<string, unknown>): void | Promise<void>;\n onMessage(handler: (frame: Record<string, unknown>) => void): void;\n removeHandler(): void;\n /** Fires when the peer disconnects (transport closes from the other side). */\n onClose(handler: () => void): void;\n close(): Promise<void>;\n}\n\n// ── the address on a frame ─────────────────────────────────────────────────\n//\n// A frame that is about a process carries that process's id as `to`, at the top level\n// of the frame rather than inside its body: the frame is the envelope, and the address\n// says who it is for whatever kind it turns out to be. Every frame that is about a\n// process names it, the root of the connection included, since both ends number their\n// own root and neither has to be guessed at.\n\n/** The id a frame addresses, or null when it names no process. */\nexport function frameTo(frame: Record<string, unknown>): number | null {\n const to = frame.to;\n return typeof to === \"number\" ? to : null;\n}\n\n/**\n * A frame on its way out: every process in it becomes the reference this\n * connection uses for that process, and the process the frame addresses is\n * written down as `to`.\n */\nexport function encodeFrame(\n frame: Record<string, unknown>,\n table: ProcessTable,\n to: number,\n): Record<string, unknown> {\n const encoded = encodeProcessRefs(frame, table) as Record<string, unknown>;\n return { ...encoded, to };\n}\n\n/**\n * A frame just read: every reference in it becomes a handle on that process — or\n * the handle this side already made for it, since the same process handed over\n * twice is one process.\n */\nexport function decodeFrame(\n frame: Record<string, unknown>,\n resolve: RefResolver,\n): Record<string, unknown> {\n return decodeProcessRefs(frame, resolve) as Record<string, unknown>;\n}\n\n// ── frame guards ──────────────────────────────────────────────────────────\n\nexport function isProto(msg: Record<string, unknown>): msg is { $proto: string } {\n return \"$proto\" in msg;\n}\nexport function isInit(msg: Record<string, unknown>): msg is { $init: Record<string, unknown> } {\n return \"$init\" in msg;\n}\nexport function isState(msg: Record<string, unknown>): msg is { $state: Record<string, unknown> } {\n return \"$state\" in msg;\n}\nexport function isMsg(\n msg: Record<string, unknown>,\n): msg is { $msg: { fromName: string; body: Message } } {\n return \"$msg\" in msg;\n}\nexport function isExit(\n msg: Record<string, unknown>,\n): msg is { $exit: { code: number; state: unknown } } {\n return \"$exit\" in msg;\n}\n\n// ── a wire that could not be read ─────────────────────────────────────────\n//\n// Two frames are about the wire itself and not about a process: what a codec could\n// not read, and what a side says when the wire is over. Neither is addressed to\n// anything, and both are decided before a frame is looked up as news or as an ask —\n// a frame about the wire is about the ground the other frames stand on.\n//\n// {\"$bad\": {\"reason\": \"…\", \"raw\": \"…\"}} this side could not read that frame\n// {\"$error\": \"…\"} the wire cannot be spoken over, and why\n//\n// They are frames rather than raised errors on purpose. A frame is read inside the\n// transport's own callback — a socket's `message` event, a stream's line reader —\n// where a throw belongs to nobody: it arrives at the host as an uncaught exception,\n// and one unreadable frame from one peer takes the process down, every other\n// connection in it included. An unreadable frame is the connection's business, not\n// the process's, so it is carried as a frame the connection can answer: `$bad` is how\n// a reader says it inward, and `$error` is how a side says it outward before ending\n// that connection.\n//\n// `$error` is not a refused call — a call that is refused says so about itself, in\n// `$r.result.<name>` (see below). This one is about the wire the calls travel on.\n\nconst BAD_KEY = \"$bad\";\n\n/** How much of an unreadable line is carried: enough to see what it was, never enough to be it.\n * A line that failed to parse is the one line nobody vouched for, so quoting it whole would move\n * whatever it holds — a credential in a paste accident is how that happens — into every log,\n * transcript and context window that later reads the frame. Why, how long, and a short head of\n * it. (`reason` carries a little of the line on its own: a JSON parser says where it gave up and\n * quotes what it was looking at. That is the parser's business, not a promise this file makes.) */\nexport const BAD_PREFIX_LIMIT = 32;\n\nexport function isBad(msg: Record<string, unknown>): msg is { $bad: { reason: string } } {\n return BAD_KEY in msg;\n}\n\n/** The frame that says a frame could not be read: why, how long it was, and the head of it. */\nexport function badFrame(reason: string, raw: string): Record<string, unknown> {\n const prefix = raw.length > BAD_PREFIX_LIMIT ? `${raw.slice(0, BAD_PREFIX_LIMIT)}…` : raw;\n return { [BAD_KEY]: { reason, length: raw.length, prefix } };\n}\n\n/** Why a frame could not be read, or null when it is not a frame that says so. */\nexport function badReason(msg: Record<string, unknown>): string | null {\n const body = msg[BAD_KEY];\n if (typeof body !== \"object\" || body === null) return null;\n const reason = (body as { reason?: unknown }).reason;\n return typeof reason === \"string\" ? reason : null;\n}\n\nconst ERROR_KEY = \"$error\";\n\nexport function isError(msg: Record<string, unknown>): msg is { $error: string } {\n return ERROR_KEY in msg;\n}\n\n/** The frame that says the wire is over: the reason, and the connection ends with it. */\nexport function errorFrame(reason: string): Record<string, unknown> {\n return { [ERROR_KEY]: reason };\n}\n\n/** The reason a wire error carries, or null when it is not one. */\nexport function errorReason(msg: Record<string, unknown>): string | null {\n const reason = msg[ERROR_KEY];\n return typeof reason === \"string\" ? reason : null;\n}\n\n// ── what is said about a crossed process ────────────────────────────────────\n//\n// A process that crossed can be asked for less than everything, or for nothing at\n// all: what crosses about it is three categories, and only the ones asked for are\n// sent. Silence is the empty list, so a handle that asks for nothing hears\n// nothing — not its state, not its messages, and not that it is gone.\n//\n// {\"$tune\": {\"streams\": [\"state\"]}, \"to\": 3} this much about it, and no more\n\nexport type StreamKind = \"message\" | \"state\" | \"exit\";\n\n/** The three, in the one order they are ever named in — the order a tune frame\n * carries them in, so two ways of asking for the same thing look the same. */\nexport const STREAM_KINDS: StreamKind[] = [\"message\", \"state\", \"exit\"];\n\nexport function isStreamKind(value: unknown): value is StreamKind {\n return typeof value === \"string\" && (STREAM_KINDS as string[]).includes(value);\n}\n\n// ── control frames ─────────────────────────────────────────────────────────\n//\n// What the holder of a handle can ask of a process on the far side, beside\n// sending it a message. Each is a frame of its own rather than a message, so\n// nothing an actor reads is ever a control signal by accident:\n//\n// {\"$stop\": {}, \"to\": 3} stop it; it ends, and its `$exit` crosses back\n// {\"$pause\": {}, \"to\": 3} stop feeding it messages\n// {\"$resume\": {}, \"to\": 3} feed it again\n// {\"$release\": {}, \"to\": 3} let it go: forget this id, and stop telling me\n// about the process behind it\n// {\"$tune\": {\"streams\": [...]}, \"to\": 3}\n// say this much about it and no more: a subset of\n// message, state and exit, or the empty list for\n// silence\n//\n// They address the process they are about, so they are answered by whoever holds\n// that process — the side that can act on it. Stopping the *root* of a connection\n// is not one of these: that stays the STOP message it always was, since the root\n// is not a handle but the process this side is talking through.\n\nexport function isStop(msg: Record<string, unknown>): msg is { $stop: Record<string, unknown> } {\n return \"$stop\" in msg;\n}\nexport function isPause(msg: Record<string, unknown>): msg is { $pause: Record<string, unknown> } {\n return \"$pause\" in msg;\n}\nexport function isResume(msg: Record<string, unknown>): msg is { $resume: Record<string, unknown> } {\n return \"$resume\" in msg;\n}\nexport function isRelease(msg: Record<string, unknown>): msg is { $release: Record<string, unknown> } {\n return \"$release\" in msg;\n}\n\nexport function isTune(msg: Record<string, unknown>): msg is { $tune: Record<string, unknown> } {\n return \"$tune\" in msg;\n}\n\n/**\n * The categories a tune frame asks for, in the order they are named in — or null\n * when the frame says something this vocabulary has no word for, which is a frame\n * to ignore rather than to guess at.\n */\nexport function tuneFrame(frame: { $tune: unknown }): StreamKind[] | null {\n const body = frame.$tune;\n if (typeof body !== \"object\" || body === null) return null;\n const streams = (body as { streams?: unknown }).streams;\n if (!Array.isArray(streams)) return null;\n if (!streams.every(isStreamKind)) return null;\n return STREAM_KINDS.filter((kind) => (streams as StreamKind[]).includes(kind));\n}\n\n// ── reflection frames ──────────────────────────────────────────────────────\n//\n// A reflection call crosses the seam as one frame and its answer as another, and\n// both go both ways: whoever holds a process is the one that answers for it.\n//\n// {\"$r.methods\": [\"inspect.getTree\", …]} what a process can answer, once, when\n// it crosses (and, for the root, once\n// when the connection opens)\n// {\"$r.call.<name>\": {seq, args}} asking, addressed to the process\n// {\"$r.result.<name>\": {seq, value}} answered\n// {\"$r.result.<name>\": {seq, error}} refused\n//\n// The method name is in the frame key, so a frame says what it is without a\n// table, and `seq` — one connection's, never reused — tells two calls to the\n// same method apart while both are in flight. An answer is about the call and not\n// about a process, so it carries no address: it names the seq it settles.\n\nexport const REFLECT_METHODS = \"$r.methods\";\nexport const REFLECT_CALL = \"$r.call.\";\nexport const REFLECT_RESULT = \"$r.result.\";\n\nexport interface ReflectionCall {\n name: string;\n seq: number;\n args: unknown[];\n}\n\nexport interface ReflectionResult {\n name: string;\n seq: number;\n value?: unknown;\n error?: string;\n}\n\nexport function isReflectionMethods(\n msg: Record<string, unknown>,\n): msg is { \"$r.methods\": string[] } {\n const names = msg[REFLECT_METHODS];\n return Array.isArray(names) && names.every((name) => typeof name === \"string\");\n}\n\nfunction reflectionBody(\n frame: Record<string, unknown>,\n prefix: string,\n): { name: string; body: Record<string, unknown> } | null {\n for (const key of Object.keys(frame)) {\n if (!key.startsWith(prefix) || key.length === prefix.length) continue;\n const body = frame[key];\n if (typeof body !== \"object\" || body === null) return null;\n return { name: key.slice(prefix.length), body: body as Record<string, unknown> };\n }\n return null;\n}\n\n/** A call frame, or null — a malformed one is not worth an answer. */\nexport function asReflectionCall(frame: Record<string, unknown>): ReflectionCall | null {\n const parsed = reflectionBody(frame, REFLECT_CALL);\n if (!parsed || typeof parsed.body.seq !== \"number\") return null;\n const args = parsed.body.args;\n return {\n name: parsed.name,\n seq: parsed.body.seq,\n args: Array.isArray(args) ? args : [],\n };\n}\n\n/** An answer frame, or null. `error` present means the call was refused. */\nexport function asReflectionResult(frame: Record<string, unknown>): ReflectionResult | null {\n const parsed = reflectionBody(frame, REFLECT_RESULT);\n if (!parsed || typeof parsed.body.seq !== \"number\") return null;\n const { seq, value, error } = parsed.body;\n return typeof error === \"string\"\n ? { name: parsed.name, seq, error }\n : { name: parsed.name, seq, value };\n}\n\n/**\n * Why a value cannot cross a frame, or null when it can. Only JSON data\n * crosses: a function, a symbol or a bigint cannot be written down at all, and\n * a value with a cycle or a bigint inside it makes the encoder throw. What\n * JSON itself would quietly drop — a function stored inside an object — is the\n * caller's business, not something to invent a policy for here.\n */\nexport function jsonProblem(value: unknown): string | null {\n const kind = typeof value;\n if (kind === \"function\" || kind === \"symbol\" || kind === \"bigint\") return `a ${kind}`;\n if (kind !== \"object\" || value === null) return null;\n try {\n JSON.stringify(value);\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n return null;\n}\n","// ── stdio wire ─────────────────────────────────────────────────────────────\n//\n// A newline-delimited JSON channel over a pair of streams: the one wire every\n// way into a foreign execution context can carry. Two callers rely on it, and\n// they are two halves of the same thing:\n//\n// * the client of a spawn we make ourselves, on fds handed to the child (see\n// `fdStreams`) — private fds, so the child's own stdout stays free;\n// * a first-stage command on its own stdin/stdout, over ssh, under `sudo`, in\n// a container — where stdio is the only channel there is.\n//\n// Frames are one JSON object per line. Whatever the far end prints is carried\n// as an *output frame* (`$fd`), so it can never be mistaken for a frame of the\n// protocol itself; a failed start is carried as an `$error` frame.\n//\n// Why the client filters output frames before handing the transport to the\n// seam: `json1Channel` decodes every line it sees, and the first frame of a\n// session is the peer's `$proto`. Stripping output frames in the transport\n// keeps the protocol view clean without the seam knowing anything about them.\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport {\n errorFrame as wireErrorFrame,\n errorReason as wireErrorReason,\n isProto,\n type Channel,\n type StringTransport,\n} from \"./channel.js\";\nimport { VERSION, json1Channel } from \"./protocols/json1.js\";\n\n/** The stream pair a line transport talks over. */\nexport interface LineStreams {\n read: Readable;\n write: Writable;\n}\n\n/** A stream of the far end's process: its stdout or its stderr. */\nexport type OutputFd = 1 | 2;\n\n/** Where the far end's own output goes. */\nexport type OutputSink = (fd: OutputFd, data: string) => void;\n\n/** How long to wait for the far end's first protocol frame. */\nexport const HANDSHAKE_TIMEOUT_MS = 15_000;\n\nconst OUTPUT_KEY = \"$fd\";\n\n/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */\nexport class StdioWireError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"StdioWireError\";\n }\n}\n\n// ── output frames ──────────────────────────────────────────────────────────\n\n/** One frame of the far end's own output. */\nexport interface OutputFrame {\n fd: OutputFd;\n data: string;\n}\n\nexport function outputFrame(fd: OutputFd, data: string): string {\n return JSON.stringify({ [OUTPUT_KEY]: fd, data });\n}\n\n/** The output frame a line carries, or null when it carries something else. */\nexport function parseOutputFrame(line: string): OutputFrame | null {\n // Cheap reject first: almost every line of a session is protocol traffic.\n if (!line.startsWith('{\"$fd\"')) return null;\n let parsed: unknown;\n try {\n parsed = JSON.parse(line);\n } catch {\n return null;\n }\n if (typeof parsed !== \"object\" || parsed === null) return null;\n const frame = parsed as Record<string, unknown>;\n const fd = frame[OUTPUT_KEY];\n const data = frame.data;\n if (fd !== 1 && fd !== 2) return null;\n return { fd, data: typeof data === \"string\" ? data : JSON.stringify(data) };\n}\n\n/** An `$error` frame as a line. The word and its shape are the shared vocabulary's\n * (channel.ts) — one word for a wire that cannot be spoken over, whether the failure was at\n * the start of a session or in the middle of one; what this file owns is its line form. */\nexport function errorFrame(reason: string): string {\n return JSON.stringify(wireErrorFrame(reason));\n}\n\n/** The reason an `$error` frame carries, or null when it is not one. */\nexport function errorReason(frame: Record<string, unknown>): string | null {\n return wireErrorReason(frame);\n}\n\n// ── transports ─────────────────────────────────────────────────────────────\n\n/** Is this write error just a peer that already left? */\nfunction peerGone(code: unknown): boolean {\n return (\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_DESTROYED\" ||\n code === \"ERR_STREAM_WRITE_AFTER_END\" ||\n code === \"ERR_STREAM_ALREADY_FINISHED\"\n );\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A `StringTransport` over a read/write stream pair, one JSON frame per line.\n *\n * `close()` is idempotent and never throws: teardown races are normal here —\n * the peer exits, the read side ends, and a write arrives a moment later.\n */\nexport class LineTransport implements StringTransport {\n private readonly streams: LineStreams;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private pvtClosed = false;\n private pvtClosing: Promise<void> | null = null;\n\n constructor(streams: LineStreams) {\n this.streams = streams;\n const lines = readline.createInterface({ input: streams.read });\n lines.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);\n });\n lines.on(\"close\", () => {\n this.pvtOnClose?.();\n void this.close();\n });\n // A stream error is a peer that went away, not a reason to take the process\n // down with an unhandled 'error' event.\n streams.read.on(\"error\", () => {\n void this.close();\n });\n streams.write.on(\"error\", () => {\n void this.close();\n });\n }\n\n get closed(): boolean {\n return this.pvtClosed;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.pvtClosed) throw new StdioWireError(\"stdio transport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new StdioWireError(\"stdio transport: handler already set — call removeHandler()\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const previous = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return previous;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n send(frame: string): Promise<void> {\n if (this.pvtClosed) {\n return Promise.resolve();\n }\n const line = frame.endsWith(\"\\n\") ? frame : `${frame}\\n`;\n return new Promise<void>((resolve, reject) => {\n try {\n this.streams.write.write(line, (err?: Error | null) => {\n if (!err) return resolve();\n const code = (err as { code?: unknown }).code;\n // A peer that has hung up cannot be told anything; the write that\n // races its exit is not an error in this transport.\n if (peerGone(code)) {\n return resolve();\n }\n reject(new StdioWireError(`stdio transport: ${err.message}`));\n });\n } catch (err) {\n reject(new StdioWireError(`stdio transport: ${errorText(err)}`));\n }\n });\n }\n\n async close(): Promise<void> {\n this.pvtClosing ??= this.pvtClose();\n await this.pvtClosing;\n }\n\n private async pvtClose(): Promise<void> {\n await Promise.resolve();\n\n this.pvtClosed = true;\n // Best effort: a half-open stream (the peer is already gone) must not turn\n // teardown into an exception.\n try {\n this.streams.read.destroy?.();\n } catch {\n /* already gone */\n }\n try {\n if (!this.streams.write.writableEnded) this.streams.write.end();\n } catch {\n /* already gone */\n }\n }\n}\n\n/** Streams over two fds of *our own* process — the child side of an fd wire. */\nexport function fdStreams(readFd: number, writeFd: number): LineStreams {\n const read = createReadStream(\"\", { fd: readFd, encoding: \"utf-8\", autoClose: false });\n const write = createWriteStream(\"\", { fd: writeFd, encoding: \"utf-8\", autoClose: false });\n return { read, write };\n}\n\n/**\n * A transport that lifts output frames out of the stream: everything else is\n * passed through untouched, so the protocol sees frames and only frames.\n */\nexport class OutputFilter implements StringTransport {\n private readonly inner: StringTransport;\n private readonly sink: OutputSink;\n\n constructor(inner: StringTransport, sink: OutputSink) {\n this.inner = inner;\n this.sink = sink;\n }\n\n send(frame: string): void | Promise<void> {\n return this.inner.send(frame);\n }\n\n onMessage(handler: (frame: string) => void): void {\n this.inner.onMessage((line) => {\n const output = parseOutputFrame(line);\n if (output) this.sink(output.fd, output.data);\n else handler(line);\n });\n }\n\n removeHandler(): void {\n this.inner.removeHandler();\n }\n\n onClose(handler: () => void): void {\n this.inner.onClose(handler);\n }\n\n close(): Promise<void> {\n return this.inner.close();\n }\n}\n\n// ── sinks ──────────────────────────────────────────────────────────────────\n\n/** Write the far end's output to our stderr, tagged with its name. */\nexport function stderrSink(name = \"\"): OutputSink {\n const tag = name ? `[${name}] ` : \"\";\n return (fd, data) => {\n const lead = fd === 1 ? tag : `${tag}err: `;\n for (const line of data.split(\"\\n\")) {\n if (line.length > 0) process.stderr.write(`${lead}${line}\\n`);\n }\n };\n}\n\n// ── handshakes ─────────────────────────────────────────────────────────────\n\n/** The next frame that arrives, or a timeout. */\nasync function nextFrame(channel: Channel, timeoutMs: number): Promise<Record<string, unknown>> {\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new StdioWireError(`no protocol frame within ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref?.();\n channel.onMessage((frame) => {\n clearTimeout(timer);\n resolve(frame);\n });\n });\n}\n\nexport interface ClientChannelOptions {\n /** Where the far end's own output goes. Defaults to our stderr. */\n onOutput?: OutputSink;\n /** How long to wait for the peer's protocol frame. Defaults to {@link HANDSHAKE_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\n/**\n * The client end of the wire: hand the streams to the seam (minus output\n * frames) and wait for the peer's `$proto`. A first stage reports a failed\n * start as an `$error` frame instead.\n */\nexport async function clientChannel(\n streams: LineStreams,\n opts: ClientChannelOptions = {},\n): Promise<Channel> {\n const transport = new LineTransport(streams);\n const channel = json1Channel(new OutputFilter(transport, opts.onOutput ?? stderrSink()));\n const frame = await nextFrame(channel, opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS);\n channel.removeHandler();\n\n const reason = errorReason(frame);\n if (reason) throw new StdioWireError(reason);\n if (!isProto(frame)) {\n throw new StdioWireError(`unexpected first frame: ${JSON.stringify(frame).slice(0, 120)}`);\n }\n if (frame.$proto !== VERSION) {\n throw new StdioWireError(`unsupported protocol ${String(frame.$proto)}, expected ${VERSION}`);\n }\n return channel;\n}\n\n/** The server end of the wire — a payload speaking stdio itself. */\nexport async function serverChannel(streams: LineStreams): Promise<Channel> {\n const channel = json1Channel(new LineTransport(streams));\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── FIFO transport ─────────────────────────────────────────────────────────\n//\n// Newline-delimited UTF-8 over named fifos. Strict handler lifecycle:\n// onMessage() throws if a handler is already set; call removeHandler() first.\n\nimport { open, type FileHandle } from \"node:fs/promises\";\nimport type { ReadStream, WriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { StringTransport } from \"../channel.js\";\n\nexport class FifoUtf8NlineTransport implements StringTransport {\n private readFd: FileHandle | null;\n private writeFd: FileHandle | null;\n private rl: readline.Interface | null;\n private rs: ReadStream | null;\n private ws: WriteStream | null;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n private closingPromise: Promise<void> | null = null;\n private pvtError: Error | null = null;\n\n private constructor(opts: { readFd?: FileHandle; writeFd?: FileHandle }) {\n this.readFd = opts.readFd ?? null;\n this.writeFd = opts.writeFd ?? null;\n\n if (this.readFd) {\n this.rs = this.readFd.createReadStream({ encoding: \"utf-8\" });\n this.rl = readline.createInterface({ input: this.rs });\n\n this.rl.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);\n });\n\n this.rl.on(\"close\", () => {\n this.pvtOnClose?.();\n this.close();\n });\n\n this.rs.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.rs = null;\n this.rl = null;\n }\n\n if (this.writeFd) {\n this.ws = this.writeFd.createWriteStream({ encoding: \"utf-8\" });\n this.ws.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.ws = null;\n }\n }\n\n // ── factories ──────────────────────────────────────────────────────────\n\n static openReaderFd(readFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd });\n }\n\n static openWriterFd(writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ writeFd });\n }\n\n static fromFds(readFd: FileHandle, writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd, writeFd });\n }\n\n // ── bidirectional connection ───────────────────────────────────────────\n\n /**\n * Start opening a bidirectional connection. Opens readPath for reading\n * in the background, returns a promise. The caller should do whatever\n * setup is needed to unblock the read (e.g. spawn a process that opens\n * readPath for writing), then await the returned transport.\n *\n * The transport itself is built only once both directions are open. A read\n * stream created while no writer exists on its fifo is handed an immediate\n * end-of-stream by some runtimes (bun), which closes a channel that has not\n * carried a byte yet — so the writes side is opened before the reader exists.\n */\n static beginConnect(\n readPath: string,\n writePath: string,\n ): { transport: Promise<FifoUtf8NlineTransport> } {\n const readFdPromise = open(readPath, \"r\");\n\n const transport = readFdPromise.then(async (readFd) => {\n const writeFd = await open(writePath, \"w\");\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n });\n\n return { transport };\n }\n\n /**\n * Open a bidirectional connection. Opens writePath for writing first\n * (unblocking the peer's read), starts reading readPath in the\n * background, then awaits both.\n *\n * Use this when you are the side that responds to the peer's beginConnect\n * (i.e. you don't need to interleave any setup between the read and write\n * opens). Built like `beginConnect`: the transport exists only after both\n * directions do.\n */\n static async connect(readPath: string, writePath: string): Promise<FifoUtf8NlineTransport> {\n const readFdPromise = open(readPath, \"r\");\n const writeFd = await open(writePath, \"w\");\n const readFd = await readFdPromise;\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n }\n\n // ── public API ─────────────────────────────────────────────────────────\n\n get canSend(): boolean {\n return this.writeFd !== null && !this.closed;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.readFd === null) throw new Error(\"FifoUtf8NlineTransport: not a reader\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"FifoUtf8NlineTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n get lastError(): Error | null {\n return this.pvtError;\n }\n\n async send(line: string): Promise<void> {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.writeFd === null) throw new Error(\"FifoUtf8NlineTransport: not a writer\");\n if (!line.endsWith(\"\\n\")) line += \"\\n\";\n await this.ws?.write(line);\n }\n\n async close(): Promise<void> {\n this.closingPromise = this.closingPromise || this.pvtClose();\n await this.closingPromise;\n this.closed = true;\n }\n\n private async pvtClose(): Promise<void> {\n // tranfser control back to caller\n // so they can capture the promise reference\n // and prevent reentry\n await Promise.resolve();\n\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n if (this.rs) {\n this.rs.destroy();\n this.rs = null;\n }\n if (this.ws) {\n await new Promise<void>((resolve) => {\n try {\n this.ws?.end(\"\", resolve);\n } catch {\n resolve();\n }\n });\n this.ws = null;\n }\n\n if (this.readFd) {\n try {\n await this.readFd.close();\n } catch {}\n this.readFd = null;\n }\n if (this.writeFd) {\n try {\n await this.writeFd.close();\n } catch {}\n this.writeFd = null;\n }\n }\n}\n","// ── Gateway ────────────────────────────────────────────────────────────────\n//\n// The first-stage command. Run this *inside* a foreign environment — directly,\n// under `sudo`, over ssh, in a container — and it:\n//\n// 1. creates the channel to the payload (fifo paths it makes itself, in a\n// private temp dir, so they belong to the uid that will open them),\n// 2. starts the payload — the first argument, required — hands it everything else it was\n// given (the host version among it) and relays frames between stdin/stdout,\n// 3. carries whatever the payload prints to the client as output frames\n// (`$fd`), so the payload's own stdout can never be mistaken for protocol.\n//\n// Why a gateway at all: a fifo path created on the client side is useless when the\n// payload runs as another uid (it cannot open it) or on another host (the path is\n// not there). stdio is the one channel every way into an environment gives us, so\n// the wire is stdio and the fifo stays inside the environment.\n//\n// The gateway reads nothing out of what it carries and owns no flag: the payload's path and\n// every argument after it go through verbatim, in order. The payload is the one who refuses a\n// host version it is not, and a relay that judged would be a second place for that decision.\n//\n// Directories and processes are cleaned up on every exit path, which is also why\n// the client never sees a transport being closed twice.\n//\n// It imports nothing but node builtins and this package, so the payload behind it\n// can be anything the runtime there can start.\n\nimport { execFile, spawn } from \"node:child_process\";\nimport type { ChildProcess } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { errorFrame, outputFrame } from \"./stdio.js\";\nimport type { LineTransport, OutputFd } from \"./stdio.js\";\nimport { LineTransport as Lines } from \"./stdio.js\";\nimport { FifoUtf8NlineTransport } from \"./transports/fifo.js\";\n\nconst run = promisify(execFile);\n\n/** Exit code for \"this environment could not be set up\". */\nexport const GATEWAY_FAILED = 1;\n\nexport interface GatewayBoot {\n /**\n * The program to start for the payload: the first word of the command the client composed —\n * a runtime, or a program that runs itself. Required, and first, and the gateway has no\n * opinion of its own about which it is.\n */\n worker: string;\n /**\n * Everything after the payload, carried to it **as is** and in order: the consumer's own\n * arguments, and posipaki's own facts among them (the host version travels this way). The\n * gateway reads nothing by name and owns no flag, so nothing of the caller's is interpreted,\n * eaten, re-ordered or re-spelled — and the next fact added to that line needs no new\n * vocabulary here.\n */\n passthrough: string[];\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A label for the gateway's own temp directory: the program this environment was made for.\n * It is the gateway's business alone — the payload is not told which label it is.\n */\nfunction label(worker: string): string {\n const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, \"-\").slice(0, 60);\n return name === \"\" ? \"payload\" : name;\n}\n\n/**\n * The argv that boots this gateway — built by the client, read by\n * {@link gatewayBoot}, so the two halves cannot drift apart.\n *\n * The payload comes first and its arguments after it, so a positional argument of the\n * payload's own can never be mistaken for the payload.\n */\nexport function gatewayArgs(boot: GatewayBoot): string[] {\n return [boot.worker, ...boot.passthrough];\n}\n\n/** The boot a gateway was started with, or the reason it cannot start. */\nexport function gatewayBoot(argv: string[]): GatewayBoot {\n const [worker, ...passthrough] = argv;\n if (worker === undefined || worker.startsWith(\"--\")) {\n throw new Error(\"no <payload>: the gateway relays to a payload, named as its first argument\");\n }\n return { passthrough, worker };\n}\n\n/**\n * End a start that failed. The reason has already gone out on the wire, and\n * returning is not enough: the `open()` on a fifo no payload will ever write sits\n * in the threadpool forever, so the loop never drains and the gateway would linger\n * with a dead payload on its books.\n */\nfunction abandon(): never {\n process.exit(GATEWAY_FAILED);\n}\n\n/** Carry a failed start to the client, then let the caller exit non-zero. */\nasync function fail(wire: LineTransport, reason: string): Promise<void> {\n process.stderr.write(`gateway: ${reason}\\n`);\n try {\n await wire.send(errorFrame(reason));\n } catch {\n /* the client is gone too — the exit code still says what happened */\n }\n}\n\n/** Copy everything the payload prints onto the wire as output frames. */\nfunction forwardOutput(stream: NodeJS.ReadableStream, wire: LineTransport, fd: OutputFd): void {\n stream.setEncoding(\"utf-8\");\n stream.on(\"data\", (chunk: string) => {\n void wire.send(outputFrame(fd, chunk)).catch(() => {\n /* the client hung up: the payload's exit is what matters now */\n });\n });\n}\n\n/**\n * Serve one environment over stdin/stdout. Returns the exit code; the caller\n * turns it into `process.exitCode`.\n */\nexport async function runGateway(argv: string[] = process.argv.slice(2)): Promise<number> {\n const wire = new Lines({ read: process.stdin, write: process.stdout });\n\n let booted: GatewayBoot;\n try {\n booted = gatewayBoot(argv);\n } catch (err) {\n // Nothing has been started yet, so there is nothing to tear down — but the\n // client still deserves the reason rather than a channel that closes on it,\n // and the loop can outlive us: an open stdin nobody will close keeps the\n // process alive, so this is an exit, not a return.\n await fail(wire, errorText(err));\n abandon();\n }\n const { passthrough, worker } = booted;\n\n const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));\n const fifoIn = join(dir, \"in\"); // the payload writes, we read\n const fifoOut = join(dir, \"out\"); // we write, the payload reads\n\n let child: ChildProcess | null = null;\n let fifo: FifoUtf8NlineTransport | null = null;\n\n /**\n * Everything this gateway owns, undone. All of it is idempotent, because the\n * exit path and the signal path can both arrive.\n */\n const teardown = async (): Promise<void> => {\n child?.kill();\n await fifo?.close().catch(() => {});\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n /**\n * A client that gives up stops us with a signal, and a gateway that died on the\n * spot would leave its payload behind in the environment — a process holding a\n * fifo nobody will ever write, which is a leak the environment cannot clean up\n * for us. So the signals do what the exit path does.\n */\n const onTerminate = () => {\n void teardown()\n .catch(() => {})\n .then(() => process.exit(GATEWAY_FAILED));\n };\n for (const signal of [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const) process.on(signal, onTerminate);\n\n try {\n await run(\"mkfifo\", [\"-m\", \"600\", fifoIn, fifoOut]);\n } catch (err) {\n await fail(wire, `cannot create the environment's fifos: ${errorText(err)}`);\n await teardown();\n abandon();\n }\n\n // Open the read side in the background, then start the payload that unblocks\n // it — the order the fifo handshake requires.\n const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);\n const workerArgs = [worker, ...passthrough, `--fifo-in=${fifoIn}`, `--fifo-out=${fifoOut}`];\n // Whatever the client said starts the payload, run as it said: a runtime with a script of\n // ours, or a program of the caller's own. The gateway does not know which it is, and does\n // not choose a runtime on the far end's behalf.\n const workerProc = spawn(workerArgs[0]!, workerArgs.slice(1), {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child = workerProc;\n forwardOutput(workerProc.stdout, wire, 1);\n forwardOutput(workerProc.stderr, wire, 2);\n\n const exited = new Promise<number>((settle) => {\n // A payload killed by a signal (the client hung up, and we killed it) has no\n // code — for us that is a clean end, not a failure.\n workerProc.once(\"exit\", (code) => settle(code ?? 0));\n });\n\n try {\n fifo = await Promise.race([\n connection.transport,\n exited.then((code) => {\n throw new Error(`the payload exited with code ${code} before it opened its channel`);\n }),\n ]);\n } catch (err) {\n await fail(wire, errorText(err));\n await teardown();\n abandon();\n }\n\n // Relay: the protocol is the client's and the payload's business, not ours.\n const relay = fifo;\n relay.onMessage((line) => {\n void wire.send(line).catch(() => {\n /* client gone */\n });\n });\n wire.onMessage((line) => {\n void relay.send(line).catch(() => {\n /* the payload is gone; its exit path cleans up */\n });\n });\n wire.onClose(() => {\n child?.kill();\n });\n\n const code = await exited;\n await teardown();\n await wire.close();\n return code;\n}\n","// ── The gateway, as a program ──────────────────────────────────────────────\n//\n// A gateway is something a client runs: the first-stage command that makes a fifo\n// inside a foreign environment and relays frames between its own stdin/stdout and\n// the payload. That is this file's whole job — it always serves, and it exits with\n// the code the run ended in.\n//\n// Serving lives in ./gateway.js and starts nothing by itself. A file cannot decide\n// \"am I the program?\" from where it is: a consumer that bundles the node surface\n// into its own program carries this code along, and there the bundle *is* the\n// program — a payload would find a gateway booting on its wire. So the decision is\n// in the layout instead: importing a module never starts anything, and running this\n// one always does.\n//\n// The client names it either way: `posipaki/remote/gateway-cli.js` is a published\n// specifier, so `import.meta.resolve` answers with the built file, and a consumer\n// with a program of its own stages that instead. Nothing here works out its own\n// location — a path derived from `import.meta.url` is a guess about a layout the\n// bundler owns, and the guess is wrong the moment the module moves into a chunk.\n\nimport { GATEWAY_FAILED, runGateway } from \"./gateway.js\";\n\nvoid (async () => {\n try {\n process.exitCode = await runGateway();\n } catch (err) {\n process.stderr.write(`gateway: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = GATEWAY_FAILED;\n }\n})();\n"],"mappings":"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"}
1
+ {"version":3,"file":"gateway-cli.js","names":["errorFrame","wireErrorFrame","errorText","Lines"],"sources":["../../src/remote/channel.ts","../../src/remote/protocols/json1.ts","../../src/remote/streams.ts","../../src/remote/stdio.ts","../../src/remote/transports/fifo.ts","../../src/remote/gateway.ts","../../src/remote/gateway-cli.ts"],"sourcesContent":["// ── Frame channel ──────────────────────────────────────────────────────────\n//\n// The seam's shared vocabulary. A StringTransport moves encoded frames\n// (strings); a Channel moves decoded frame objects. The frame guards narrow a\n// decoded frame to one of the `$`-key shapes. server.ts / client.ts speak only\n// in Channels and these guards — they never see a specific protocol or\n// transport.\n\nimport type { FrameChannel, Message } from \"../types.js\";\nimport {\n decodeProcessRefs,\n encodeProcessRefs,\n type ProcessTable,\n type RefResolver,\n} from \"./process-ref.js\";\n\nexport interface StringTransport {\n send(frame: string): void | Promise<void>;\n onMessage(handler: (frame: string) => void): void;\n removeHandler(): void;\n /** Fires when the peer disconnects (transport closes from the other side). */\n onClose(handler: () => void): void;\n close(): Promise<void>;\n}\n\n/** The frames of one connection, as the remote layer speaks them: the process's own `FrameChannel`,\n * where a reader has the unsubscribe `onMessage` returned rather than a handler to take back. */\nexport type Channel = FrameChannel;\n\n/**\n * One handler on the wire, however many readers on it. A transport holds one handler — and reports a\n * second one as a mistake — so the layer above keeps the readers and installs a single handler; the\n * subscribe this returns is what a reader calls to leave. The handler goes on with the first reader and\n * stays for the life of the connection: taking it off between readers would drop whatever arrives in\n * between, and a connection is not paused by nobody reading it.\n */\nexport function frameReaders(\n install: (deliver: (frame: Record<string, unknown>) => void) => void,\n): (handler: (frame: Record<string, unknown>) => void) => () => void {\n const readers = new Set<(frame: Record<string, unknown>) => void>();\n let installed = false;\n return (handler) => {\n if (!installed) {\n installed = true;\n install((frame) => {\n // Over a copy: a reader that leaves as it is handed a frame must not cut the rest short.\n for (const reader of [...readers]) reader(frame);\n });\n }\n readers.add(handler);\n return () => {\n readers.delete(handler);\n };\n };\n}\n\n// ── the address on a frame ─────────────────────────────────────────────────\n//\n// A frame that is about a process carries that process's id as `to`, at the top level\n// of the frame rather than inside its body: the frame is the envelope, and the address\n// says who it is for whatever kind it turns out to be. Every frame that is about a\n// process names it, the root of the connection included, since both ends number their\n// own root and neither has to be guessed at.\n\n/** The id a frame addresses, or null when it names no process. */\nexport function frameTo(frame: Record<string, unknown>): number | null {\n const to = frame.to;\n return typeof to === \"number\" ? to : null;\n}\n\n/**\n * A frame on its way out: every process in it becomes the reference this\n * connection uses for that process, and the process the frame addresses is\n * written down as `to`.\n */\nexport function encodeFrame(\n frame: Record<string, unknown>,\n table: ProcessTable,\n to: number,\n): Record<string, unknown> {\n const encoded = encodeProcessRefs(frame, table) as Record<string, unknown>;\n return { ...encoded, to };\n}\n\n/**\n * A frame just read: every reference in it becomes a handle on that process — or\n * the handle this side already made for it, since the same process handed over\n * twice is one process.\n */\nexport function decodeFrame(\n frame: Record<string, unknown>,\n resolve: RefResolver,\n): Record<string, unknown> {\n return decodeProcessRefs(frame, resolve) as Record<string, unknown>;\n}\n\n// ── frame guards ──────────────────────────────────────────────────────────\n//\n// Each guard narrows a decoded frame to the frame it is, and each of those types is\n// named, so the whole set can be written down once (see `Frame`, at the foot of this\n// file). A guard asks only whether the key is there: the body under it belongs to\n// whoever wrote it.\n\nexport type ProtoFrame = {\n $proto: string;\n};\nexport type InitFrame = {\n $init: Record<string, unknown>;\n};\nexport type StateFrame = {\n $state: Record<string, unknown>;\n};\nexport type MsgFrame = {\n $msg: { fromName: string; body: Message };\n};\nexport type ExitFrame = {\n $exit: { code: number; state: unknown };\n};\n\nexport function isProto(msg: Record<string, unknown>): msg is ProtoFrame {\n return \"$proto\" in msg;\n}\nexport function isInit(msg: Record<string, unknown>): msg is InitFrame {\n return \"$init\" in msg;\n}\nexport function isState(msg: Record<string, unknown>): msg is StateFrame {\n return \"$state\" in msg;\n}\nexport function isMsg(msg: Record<string, unknown>): msg is MsgFrame {\n return \"$msg\" in msg;\n}\nexport function isExit(msg: Record<string, unknown>): msg is ExitFrame {\n return \"$exit\" in msg;\n}\n\n// ── a wire that could not be read ─────────────────────────────────────────\n//\n// Two frames are about the wire itself and not about a process: what a codec could\n// not read, and what a side says when the wire is over. Neither is addressed to\n// anything, and both are decided before a frame is looked up as news or as an ask —\n// a frame about the wire is about the ground the other frames stand on.\n//\n// {\"$bad\": {\"reason\": \"…\", \"raw\": \"…\"}} this side could not read that frame\n// {\"$error\": \"…\"} the wire cannot be spoken over, and why\n//\n// They are frames rather than raised errors on purpose. A frame is read inside the\n// transport's own callback — a socket's `message` event, a stream's line reader —\n// where a throw belongs to nobody: it arrives at the host as an uncaught exception,\n// and one unreadable frame from one peer takes the process down, every other\n// connection in it included. An unreadable frame is the connection's business, not\n// the process's, so it is carried as a frame the connection can answer: `$bad` is how\n// a reader says it inward, and `$error` is how a side says it outward before ending\n// that connection.\n//\n// `$error` is not a refused call — a call that is refused says so about itself, in\n// `$r.result.<name>` (see below). This one is about the wire the calls travel on.\n\nconst BAD_KEY = \"$bad\";\n\n/** How much of an unreadable line is carried: enough to see what it was, never enough to be it.\n * A line that failed to parse is the one line nobody vouched for, so quoting it whole would move\n * whatever it holds — a credential in a paste accident is how that happens — into every log,\n * transcript and context window that later reads the frame. Why, how long, and a short head of\n * it. (`reason` carries a little of the line on its own: a JSON parser says where it gave up and\n * quotes what it was looking at. That is the parser's business, not a promise this file makes.) */\nexport const BAD_PREFIX_LIMIT = 32;\n\nexport type BadFrame = {\n $bad: { reason: string };\n};\n\nexport function isBad(msg: Record<string, unknown>): msg is BadFrame {\n return BAD_KEY in msg;\n}\n\n/** The frame that says a frame could not be read: why, how long it was, and the head of it. */\nexport function badFrame(reason: string, raw: string): Record<string, unknown> {\n const prefix = raw.length > BAD_PREFIX_LIMIT ? `${raw.slice(0, BAD_PREFIX_LIMIT)}…` : raw;\n return { [BAD_KEY]: { reason, length: raw.length, prefix } };\n}\n\n/** Why a frame could not be read, or null when it is not a frame that says so. */\nexport function badReason(msg: Record<string, unknown>): string | null {\n const body = msg[BAD_KEY];\n if (typeof body !== \"object\" || body === null) return null;\n const reason = (body as { reason?: unknown }).reason;\n return typeof reason === \"string\" ? reason : null;\n}\n\nconst ERROR_KEY = \"$error\";\n\nexport type ErrorFrame = {\n $error: string;\n};\n\nexport function isError(msg: Record<string, unknown>): msg is ErrorFrame {\n return ERROR_KEY in msg;\n}\n\n/** The frame that says the wire is over: the reason, and the connection ends with it. */\nexport function errorFrame(reason: string): Record<string, unknown> {\n return { [ERROR_KEY]: reason };\n}\n\n/** The reason a wire error carries, or null when it is not one. */\nexport function errorReason(msg: Record<string, unknown>): string | null {\n const reason = msg[ERROR_KEY];\n return typeof reason === \"string\" ? reason : null;\n}\n\n// ── the far end's own output ───────────────────────────────────────────────\n//\n// What one end's root says on one of its own streams is a frame like any other, read and written\n// above the protocol step: the number is the stream a program knows, and what is said there is a\n// string. A print is fd 1, an error fd 2, and what the other end may read is fd 0.\n//\n// {\"$fd\": 0 | 1 | 2, \"data\": \"…\"}\n\nexport type OutputFd = 0 | 1 | 2;\n\n/** An output frame as a type: what `outputFrame` writes and `outputOf` reads. */\nexport type OutputFrame = {\n $fd: OutputFd;\n data: string;\n};\n\n/** A frame of the far end's own output: `data` on the stream `fd`. */\nexport function outputFrame(fd: OutputFd, data: string): Record<string, unknown> {\n return { $fd: fd, data };\n}\n\n/** The output a frame carries, or null when it carries something else. */\nexport function outputOf(frame: Record<string, unknown>): { fd: OutputFd; data: string } | null {\n const fd = frame.$fd;\n if (fd !== 0 && fd !== 1 && fd !== 2) return null;\n const data = frame.data;\n return { fd, data: typeof data === \"string\" ? data : JSON.stringify(data) };\n}\n\n// ── what is said about a crossed process ────────────────────────────────────\n//\n// A process that crossed can be asked for less than everything, or for nothing at\n// all: what crosses about it is three categories, and only the ones asked for are\n// sent. Silence is the empty list, so a handle that asks for nothing hears\n// nothing — not its state, not its messages, and not that it is gone.\n//\n// {\"$tune\": {\"streams\": [\"state\"]}, \"to\": 3} this much about it, and no more\n\nexport type StreamKind = \"message\" | \"state\" | \"exit\";\n\n/** The three, in the one order they are ever named in — the order a tune frame\n * carries them in, so two ways of asking for the same thing look the same. */\nexport const STREAM_KINDS: StreamKind[] = [\"message\", \"state\", \"exit\"];\n\nexport function isStreamKind(value: unknown): value is StreamKind {\n return typeof value === \"string\" && (STREAM_KINDS as string[]).includes(value);\n}\n\n// ── control frames ─────────────────────────────────────────────────────────\n//\n// What the holder of a handle can ask of a process on the far side, beside\n// sending it a message. Each is a frame of its own rather than a message, so\n// nothing an actor reads is ever a control signal by accident:\n//\n// {\"$stop\": {}, \"to\": 3} stop it; it ends, and its `$exit` crosses back\n// {\"$pause\": {}, \"to\": 3} stop feeding it messages\n// {\"$resume\": {}, \"to\": 3} feed it again\n// {\"$release\": {}, \"to\": 3} let it go: forget this id, and stop telling me\n// about the process behind it\n// {\"$tune\": {\"streams\": [...]}, \"to\": 3}\n// say this much about it and no more: a subset of\n// message, state and exit, or the empty list for\n// silence\n//\n// They address the process they are about, so they are answered by whoever holds\n// that process — the side that can act on it. Stopping the *root* of a connection\n// is not one of these: that stays the STOP message it always was, since the root\n// is not a handle but the process this side is talking through.\n\nexport type StopFrame = {\n $stop: Record<string, unknown>;\n};\nexport type PauseFrame = {\n $pause: Record<string, unknown>;\n};\nexport type ResumeFrame = {\n $resume: Record<string, unknown>;\n};\nexport type ReleaseFrame = {\n $release: Record<string, unknown>;\n};\nexport type TuneFrame = {\n $tune: Record<string, unknown>;\n};\n\nexport function isStop(msg: Record<string, unknown>): msg is StopFrame {\n return \"$stop\" in msg;\n}\nexport function isPause(msg: Record<string, unknown>): msg is PauseFrame {\n return \"$pause\" in msg;\n}\nexport function isResume(msg: Record<string, unknown>): msg is ResumeFrame {\n return \"$resume\" in msg;\n}\nexport function isRelease(msg: Record<string, unknown>): msg is ReleaseFrame {\n return \"$release\" in msg;\n}\nexport function isTune(msg: Record<string, unknown>): msg is TuneFrame {\n return \"$tune\" in msg;\n}\n\n/**\n * The categories a tune frame asks for, in the order they are named in — or null\n * when the frame says something this vocabulary has no word for, which is a frame\n * to ignore rather than to guess at.\n */\nexport function tuneFrame(frame: { $tune: unknown }): StreamKind[] | null {\n const body = frame.$tune;\n if (typeof body !== \"object\" || body === null) return null;\n const streams = (body as { streams?: unknown }).streams;\n if (!Array.isArray(streams)) return null;\n if (!streams.every(isStreamKind)) return null;\n return STREAM_KINDS.filter((kind) => (streams as StreamKind[]).includes(kind));\n}\n\n// ── reflection frames ──────────────────────────────────────────────────────\n//\n// A reflection call crosses the seam as one frame and its answer as another, and\n// both go both ways: whoever holds a process is the one that answers for it.\n//\n// {\"$r.methods\": [\"inspect.getTree\", …]} what a process can answer, once, when\n// it crosses (and, for the root, once\n// when the connection opens)\n// {\"$r.call.<name>\": {seq, args}} asking, addressed to the process\n// {\"$r.result.<name>\": {seq, value}} answered\n// {\"$r.result.<name>\": {seq, error}} refused\n//\n// The method name is in the frame key, so a frame says what it is without a\n// table, and `seq` — one connection's, never reused — tells two calls to the\n// same method apart while both are in flight. An answer is about the call and not\n// about a process, so it carries no address: it names the seq it settles.\n\nexport const REFLECT_METHODS = \"$r.methods\";\nexport const REFLECT_CALL = \"$r.call.\";\nexport const REFLECT_RESULT = \"$r.result.\";\n\nexport interface ReflectionCall {\n name: string;\n seq: number;\n args: unknown[];\n}\n\nexport interface ReflectionResult {\n name: string;\n seq: number;\n value?: unknown;\n error?: string;\n}\n\nexport type ReflectionMethodsFrame = {\n \"$r.methods\": string[];\n};\n\n/** A call frame: the method in the key, the call in the body (`$r.call.<name>`). */\nexport type ReflectionCallFrame = {\n [key: `$r.call.${string}`]: { seq: number; args?: unknown[] };\n};\n\n/** An answer frame: `value` or `error`, naming the seq it settles. */\nexport type ReflectionResultFrame = {\n [key: `$r.result.${string}`]: { seq: number; value?: unknown; error?: string };\n};\n\nexport function isReflectionMethods(msg: Record<string, unknown>): msg is ReflectionMethodsFrame {\n const names = msg[REFLECT_METHODS];\n return Array.isArray(names) && names.every((name) => typeof name === \"string\");\n}\n\nfunction reflectionBody(\n frame: Record<string, unknown>,\n prefix: string,\n): { name: string; body: Record<string, unknown> } | null {\n for (const key of Object.keys(frame)) {\n if (!key.startsWith(prefix) || key.length === prefix.length) continue;\n const body = frame[key];\n if (typeof body !== \"object\" || body === null) return null;\n return { name: key.slice(prefix.length), body: body as Record<string, unknown> };\n }\n return null;\n}\n\n/** A call frame, or null — a malformed one is not worth an answer. */\nexport function asReflectionCall(frame: Record<string, unknown>): ReflectionCall | null {\n const parsed = reflectionBody(frame, REFLECT_CALL);\n if (!parsed || typeof parsed.body.seq !== \"number\") return null;\n const args = parsed.body.args;\n return {\n name: parsed.name,\n seq: parsed.body.seq,\n args: Array.isArray(args) ? args : [],\n };\n}\n\n/** An answer frame, or null. `error` present means the call was refused. */\nexport function asReflectionResult(frame: Record<string, unknown>): ReflectionResult | null {\n const parsed = reflectionBody(frame, REFLECT_RESULT);\n if (!parsed || typeof parsed.body.seq !== \"number\") return null;\n const { seq, value, error } = parsed.body;\n return typeof error === \"string\"\n ? { name: parsed.name, seq, error }\n : { name: parsed.name, seq, value };\n}\n\n/**\n * Why a value cannot cross a frame, or null when it can. Only JSON data\n * crosses: a function, a symbol or a bigint cannot be written down at all, and\n * a value with a cycle or a bigint inside it makes the encoder throw. What\n * JSON itself would quietly drop — a function stored inside an object — is the\n * caller's business, not something to invent a policy for here.\n */\nexport function jsonProblem(value: unknown): string | null {\n const kind = typeof value;\n if (kind === \"function\" || kind === \"symbol\" || kind === \"bigint\") return `a ${kind}`;\n if (kind !== \"object\" || value === null) return null;\n try {\n JSON.stringify(value);\n } catch (err) {\n return err instanceof Error ? err.message : String(err);\n }\n return null;\n}\n\n// ── the frames, as one type ────────────────────────────────────────────────\n//\n// The guards above say what a frame is one at a time; this is the same set as one type.\n// Neither direction nor protocol is in it — who may send which is the two ends' business,\n// and the handshake names the protocol that writes them. The address a frame carries\n// (`to`, `frameTo`) is the envelope the connection writes around it, not the frame.\n\nexport type Frame =\n | ProtoFrame\n | InitFrame\n | StateFrame\n | MsgFrame\n | ExitFrame\n | BadFrame\n | ErrorFrame\n | OutputFrame\n | StopFrame\n | PauseFrame\n | ResumeFrame\n | ReleaseFrame\n | TuneFrame\n | ReflectionMethodsFrame\n | ReflectionCallFrame\n | ReflectionResultFrame;\n","// ── json1 wire protocol ────────────────────────────────────────────────────\n//\n// One JSON object per frame: `{\"$key\": value}`. encode/decode turn a decoded\n// frame object into a string and back. json1Channel wraps a StringTransport so\n// it moves frame objects instead of strings.\n//\n// This is the boundary where the wire's bytes become a frame, and it is the one\n// place a bad peer can throw into a callback that belongs to nobody but the host:\n// the handler runs where the transport called it — a socket's `message` event, a\n// line reader — so a `SyntaxError` here is an uncaught exception and takes the\n// process down. So a line that will not parse is not thrown at the caller: it\n// becomes the frame that says so (`$bad`, channel.ts) and travels inward like any\n// other, where the connection it came from can answer for it.\n//\n// One frame is one JSON *object*: a line that parses to a number, a string, an\n// array or `null` is not a frame either, and reading one as a frame (`\"$init\" in 7`)\n// throws just as surely as a line that never parsed.\n\nimport { badFrame, frameReaders, type Channel, type StringTransport } from \"../channel.js\";\n\nexport const VERSION = \"json.v1\";\n\n/** The frames this protocol carries. The shapes and the union are in channel.ts; a protocol\n * owns that the connection speaks them, and how they are written down. */\nexport type { Frame } from \"../channel.js\";\n\nexport function encode(frame: Record<string, unknown>): string {\n return JSON.stringify(frame);\n}\n\n/** The frame a line carries, throwing on a line that is not JSON. `readFrame` is how the\n * seam reads a line; this is the primitive underneath it. */\nexport function decode(str: string): Record<string, unknown> {\n return JSON.parse(str);\n}\n\n/** A line read as a frame: the frame itself, or the frame that says this one could not be\n * read. Never throws — see the note at the top of this file. */\nexport function readFrame(str: string): Record<string, unknown> {\n let parsed: unknown;\n try {\n parsed = decode(str);\n } catch (err) {\n return badFrame(err instanceof Error ? err.message : String(err), str);\n }\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n return badFrame(`a frame is one JSON object, and this was ${kindOf(parsed)}`, str);\n }\n return parsed as Record<string, unknown>;\n}\n\nfunction kindOf(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n return `a ${typeof value}`;\n}\n\nexport function json1Channel(transport: StringTransport): Channel {\n // One handler on the transport, however many readers on the channel: the fan-out is `frameReaders`.\n const subscribe = frameReaders((deliver) => transport.onMessage((str) => deliver(readFrame(str))));\n return {\n send(frame) {\n return transport.send(encode(frame));\n },\n onMessage: subscribe,\n onClose(handler) {\n transport.onClose(handler);\n },\n close() {\n return transport.close();\n },\n };\n}\n","// ── the connection as byte streams ─────────────────────────────────────────\n//\n// A frame with `$fd` is what one end's root says on one of its own streams (`outputFrame`,\n// channel.ts): the number is the stream a program knows, and both directions of a stream live\n// under it. This is the layer that turns those frames into the streams a program holds, so a\n// logger, a terminal or a child's own stdio plugs into a connection with a pipe.\n//\n// channel ──{$fd: 1, data}──▶ stdout.read what the other root printed\n// channel ◀─{$fd: 1, data}──── stdout.write what this root prints\n//\n// Both directions on one fd, because both ends have a root and each reads what the other prints.\n// Nothing is buffered beyond the streams themselves: a `read` nobody drains grows, and a `write`\n// is one frame per chunk.\n\nimport { PassThrough, Writable, type Readable } from \"node:stream\";\n\nimport { outputFrame, outputOf, type Channel, type OutputFd } from \"./channel.js\";\n\n/**\n * What a log line needs: a stream says `info` or `warn`, and nothing here cares which logger it is.\n * The seam states the two methods rather than importing one, so a debug logger, a console or a test's\n * own collector all fit without the remote layer depending on a plugin.\n */\nexport interface LineLog {\n info(message: string, ...args: unknown[]): void;\n warn(message: string, ...args: unknown[]): void;\n}\n\n/** One stream of a connection: what the other end's root says on it, and what this end's root says. */\nexport interface FdStream {\n /** The other end's root, printing on this stream of its own. */\n readonly read: Readable;\n /** This end's root, printing on this stream; the other end reads it. */\n readonly write: Writable;\n}\n\n/** The three streams of a connection, and the one way to let them go. */\nexport interface FdSet {\n readonly stdin: FdStream;\n readonly stdout: FdStream;\n readonly stderr: FdStream;\n /** Stop reading the connection, and end every stream of it. */\n close(): void;\n}\n\n/** Where a frame goes: a connection's own `send`, or a transport's, encoded. */\nexport type OutputSender = (frame: Record<string, unknown>) => void | Promise<void>;\n\n/**\n * A writable that writes what is written to it as output frames of one fd: one chunk is one frame.\n *\n * The shape of an output frame lives here rather than at each caller — the gateway relays a payload's\n * own output and holds no channel — and a send that fails is the stream's error, not a lost line.\n */\nexport function fdWriter(send: OutputSender, fd: OutputFd): Writable {\n return new Writable({\n write(chunk: Buffer | string, _encoding, done): void {\n // `send` may answer nothing (a transport that writes synchronously): either way the chunk is\n // taken once it is out, and a failure is the stream's to report.\n void Promise.resolve(send(outputFrame(fd, String(chunk)))).then(\n () => done(),\n (error: unknown) => done(error as Error),\n );\n },\n });\n}\n\n/**\n * The byte streams of a connection: what each end's root prints, and what it reads.\n *\n * The frames are the connection's, so anyone may also read them directly (`Channel.onMessage`); what\n * this adds is the shape a program outside the seam already speaks.\n */\nexport function connectionFds(channel: Channel): FdSet {\n const sides = new Map<OutputFd, { stream: FdStream; read: PassThrough }>();\n\n const side = (fd: OutputFd): FdStream => {\n const read = new PassThrough();\n const stream: FdStream = { read, write: fdWriter((frame) => channel.send(frame), fd) };\n sides.set(fd, { stream, read });\n return stream;\n };\n\n const stdin = side(0);\n const stdout = side(1);\n const stderr = side(2);\n\n const off = channel.onMessage((frame) => {\n const output = outputOf(frame);\n if (!output) return;\n sides.get(output.fd)?.read.push(output.data);\n });\n\n return {\n stdin,\n stdout,\n stderr,\n close(): void {\n off();\n for (const { stream, read } of sides.values()) {\n read.end();\n stream.write.end();\n }\n },\n };\n}\n\n/**\n * A writable that turns what is written to it into log lines: fd 1 arrives as `info` and fd 2 as\n * `warn`, and the tail is held until a newline (or the end) so a chunk boundary is not a line\n * boundary.\n */\nexport function logStream(log: LineLog, opts: { fd?: OutputFd } = {}): Writable {\n const warn = opts.fd === 2;\n const say = (line: string): void => {\n if (warn) log.warn(line);\n else log.info(line);\n };\n\n let tail = \"\";\n /** Everything up to the last newline goes out; what follows one waits to be finished. */\n const drain = (): void => {\n const lines = tail.split(\"\\n\");\n tail = lines.pop() ?? \"\";\n for (const line of lines) {\n if (line.length > 0) say(line);\n }\n };\n\n return new Writable({\n write(chunk: Buffer | string, _encoding, done): void {\n tail += String(chunk);\n drain();\n done();\n },\n final(done): void {\n drain();\n if (tail.length > 0) say(tail);\n done();\n },\n });\n}\n","// ── stdio wire ─────────────────────────────────────────────────────────────\n//\n// A newline-delimited JSON channel over a pair of streams: the one wire every\n// way into a foreign execution context can carry. Two callers rely on it, and\n// they are two halves of the same thing:\n//\n// * the client of a spawn we make ourselves, on fds handed to the child (see\n// `fdStreams`) — private fds, so the child's own stdout stays free;\n// * a first-stage command on its own stdin/stdout, over ssh, under `sudo`, in\n// a container — where stdio is the only channel there is.\n//\n// Frames are one JSON object per line. Whatever the far end prints is carried as an *output\n// frame* (`$fd`), so it can never be mistaken for a frame of the protocol itself; a failed start\n// is carried as an `$error` frame.\n//\n// Output frames are routed above the protocol step, by whoever holds the connection (output.ts):\n// every line decodes to a frame, and the ones that are the far end's own print go to that\n// holder's sink instead of to the seam. The handshake below reads the first frame that is the\n// protocol's, since the far end's own print may come first.\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport {\n errorFrame as wireErrorFrame,\n errorReason as wireErrorReason,\n isProto,\n outputOf,\n type Channel,\n type OutputFd,\n type StringTransport,\n} from \"./channel.js\";\nimport { VERSION, json1Channel } from \"./protocols/json1.js\";\nimport { stderrSink, withOutput, type OutputSink } from \"./output.js\";\n\n/** The stream pair a line transport talks over. */\nexport interface LineStreams {\n read: Readable;\n write: Writable;\n}\n\n/** How long to wait for the far end's first protocol frame. */\nexport const HANDSHAKE_TIMEOUT_MS = 15_000;\n\n/** Raised when the wire itself fails: no frame, the wrong protocol, a peer that went away. */\nexport class StdioWireError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"StdioWireError\";\n }\n}\n\n/** An `$error` frame as a line. The word and its shape are the shared vocabulary's\n * (channel.ts) — one word for a wire that cannot be spoken over, whether the failure was at\n * the start of a session or in the middle of one; what this file owns is its line form. */\nexport function errorFrame(reason: string): string {\n return JSON.stringify(wireErrorFrame(reason));\n}\n\n/** The reason an `$error` frame carries, or null when it is not one. */\nexport function errorReason(frame: Record<string, unknown>): string | null {\n return wireErrorReason(frame);\n}\n\n// ── transports ─────────────────────────────────────────────────────────────\n\n/** Is this write error just a peer that already left? */\nfunction peerGone(code: unknown): boolean {\n return (\n code === \"EPIPE\" ||\n code === \"ERR_STREAM_DESTROYED\" ||\n code === \"ERR_STREAM_WRITE_AFTER_END\" ||\n code === \"ERR_STREAM_ALREADY_FINISHED\"\n );\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A `StringTransport` over a read/write stream pair, one JSON frame per line.\n *\n * `close()` is idempotent and never throws: teardown races are normal here —\n * the peer exits, the read side ends, and a write arrives a moment later.\n */\nexport class LineTransport implements StringTransport {\n private readonly streams: LineStreams;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private pvtClosed = false;\n private pvtClosing: Promise<void> | null = null;\n\n constructor(streams: LineStreams) {\n this.streams = streams;\n const lines = readline.createInterface({ input: streams.read });\n lines.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.pvtClosed) this.pvtOnMessage(line);\n });\n lines.on(\"close\", () => {\n this.pvtOnClose?.();\n void this.close();\n });\n // A stream error is a peer that went away, not a reason to take the process\n // down with an unhandled 'error' event.\n streams.read.on(\"error\", () => {\n void this.close();\n });\n streams.write.on(\"error\", () => {\n void this.close();\n });\n }\n\n get closed(): boolean {\n return this.pvtClosed;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.pvtClosed) throw new StdioWireError(\"stdio transport: closed\");\n if (this.pvtOnMessage !== null) {\n throw new StdioWireError(\"stdio transport: handler already set — call removeHandler()\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const previous = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return previous;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n send(frame: string): Promise<void> {\n if (this.pvtClosed) {\n return Promise.resolve();\n }\n const line = frame.endsWith(\"\\n\") ? frame : `${frame}\\n`;\n return new Promise<void>((resolve, reject) => {\n try {\n this.streams.write.write(line, (err?: Error | null) => {\n if (!err) return resolve();\n const code = (err as { code?: unknown }).code;\n // A peer that has hung up cannot be told anything; the write that\n // races its exit is not an error in this transport.\n if (peerGone(code)) {\n return resolve();\n }\n reject(new StdioWireError(`stdio transport: ${err.message}`));\n });\n } catch (err) {\n reject(new StdioWireError(`stdio transport: ${errorText(err)}`));\n }\n });\n }\n\n async close(): Promise<void> {\n this.pvtClosing ??= this.pvtClose();\n await this.pvtClosing;\n }\n\n private async pvtClose(): Promise<void> {\n await Promise.resolve();\n\n this.pvtClosed = true;\n // Best effort: a half-open stream (the peer is already gone) must not turn\n // teardown into an exception.\n try {\n this.streams.read.destroy?.();\n } catch {\n /* already gone */\n }\n try {\n if (!this.streams.write.writableEnded) this.streams.write.end();\n } catch {\n /* already gone */\n }\n }\n}\n\n/** Streams over two fds of *our own* process — the child side of an fd wire. */\nexport function fdStreams(readFd: number, writeFd: number): LineStreams {\n const read = createReadStream(\"\", { fd: readFd, encoding: \"utf-8\", autoClose: false });\n const write = createWriteStream(\"\", { fd: writeFd, encoding: \"utf-8\", autoClose: false });\n return { read, write };\n}\n\n// ── handshakes ─────────────────────────────────────────────────────────────\n\n/** The next frame that is the protocol's, or a timeout. The far end's own output is not the\n * protocol's: it goes to `sink` while this waits, and the wait carries on. */\nasync function nextFrame(\n channel: Channel,\n timeoutMs: number,\n sink: OutputSink,\n): Promise<Record<string, unknown>> {\n return await new Promise((resolve, reject) => {\n const timer = setTimeout(\n () => reject(new StdioWireError(`no protocol frame within ${timeoutMs}ms`)),\n timeoutMs,\n );\n timer.unref?.();\n const off = channel.onMessage((frame) => {\n const output = outputOf(frame);\n if (output) {\n sink(output.fd, output.data);\n return;\n }\n off();\n clearTimeout(timer);\n resolve(frame);\n });\n });\n}\n\nexport interface ClientChannelOptions {\n /** Where the far end's own output goes. Defaults to our stderr. */\n onOutput?: OutputSink;\n /** How long to wait for the peer's protocol frame. Defaults to {@link HANDSHAKE_TIMEOUT_MS}. */\n timeoutMs?: number;\n}\n\n/**\n * The client end of the connection: hand the streams to the seam and wait for the peer's `$proto`,\n * with the far end's own output going to `onOutput` (our stderr by default) as it arrives. A\n * first stage reports a failed start as an `$error` frame instead.\n */\nexport async function clientChannel(\n streams: LineStreams,\n opts: ClientChannelOptions = {},\n): Promise<Channel> {\n const channel = json1Channel(new LineTransport(streams));\n const sink = opts.onOutput ?? stderrSink();\n const frame = await nextFrame(channel, opts.timeoutMs ?? HANDSHAKE_TIMEOUT_MS, sink);\n\n const reason = errorReason(frame);\n if (reason) throw new StdioWireError(reason);\n if (!isProto(frame)) {\n throw new StdioWireError(`unexpected first frame: ${JSON.stringify(frame).slice(0, 120)}`);\n }\n if (frame.$proto !== VERSION) {\n throw new StdioWireError(`unsupported protocol ${String(frame.$proto)}, expected ${VERSION}`);\n }\n // From here on the far end's own output is this connection's business and not the transport's:\n // every frame reaches the seam, and the ones that are its print are taken off on the way in.\n return withOutput(channel, sink);\n}\n\n/** The server end of the wire — a payload speaking stdio itself. */\nexport async function serverChannel(streams: LineStreams): Promise<Channel> {\n const channel = json1Channel(new LineTransport(streams));\n await channel.send({ $proto: VERSION });\n return channel;\n}\n","// ── FIFO transport ─────────────────────────────────────────────────────────\n//\n// Newline-delimited UTF-8 over named fifos. Strict handler lifecycle:\n// onMessage() throws if a handler is already set; call removeHandler() first.\n\nimport { open, type FileHandle } from \"node:fs/promises\";\nimport type { ReadStream, WriteStream } from \"node:fs\";\nimport * as readline from \"node:readline\";\nimport type { StringTransport } from \"../channel.js\";\n\nexport class FifoUtf8NlineTransport implements StringTransport {\n private readFd: FileHandle | null;\n private writeFd: FileHandle | null;\n private rl: readline.Interface | null;\n private rs: ReadStream | null;\n private ws: WriteStream | null;\n private pvtOnMessage: ((line: string) => void) | null = null;\n private pvtOnClose: (() => void) | null = null;\n private closed = false;\n private closingPromise: Promise<void> | null = null;\n private pvtError: Error | null = null;\n\n private constructor(opts: { readFd?: FileHandle; writeFd?: FileHandle }) {\n this.readFd = opts.readFd ?? null;\n this.writeFd = opts.writeFd ?? null;\n\n if (this.readFd) {\n this.rs = this.readFd.createReadStream({ encoding: \"utf-8\" });\n this.rl = readline.createInterface({ input: this.rs });\n\n this.rl.on(\"line\", (line) => {\n if (this.pvtOnMessage && !this.closed) this.pvtOnMessage(line);\n });\n\n this.rl.on(\"close\", () => {\n this.pvtOnClose?.();\n this.close();\n });\n\n this.rs.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.rs = null;\n this.rl = null;\n }\n\n if (this.writeFd) {\n this.ws = this.writeFd.createWriteStream({ encoding: \"utf-8\" });\n this.ws.on(\"error\", (err: Error) => {\n this.pvtError = err;\n this.close();\n });\n } else {\n this.ws = null;\n }\n }\n\n // ── factories ──────────────────────────────────────────────────────────\n\n static openReaderFd(readFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd });\n }\n\n static openWriterFd(writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ writeFd });\n }\n\n static fromFds(readFd: FileHandle, writeFd: FileHandle): FifoUtf8NlineTransport {\n return new FifoUtf8NlineTransport({ readFd, writeFd });\n }\n\n // ── bidirectional connection ───────────────────────────────────────────\n\n /**\n * Start opening a bidirectional connection. Opens readPath for reading\n * in the background, returns a promise. The caller should do whatever\n * setup is needed to unblock the read (e.g. spawn a process that opens\n * readPath for writing), then await the returned transport.\n *\n * The transport itself is built only once both directions are open. A read\n * stream created while no writer exists on its fifo is handed an immediate\n * end-of-stream by some runtimes (bun), which closes a channel that has not\n * carried a byte yet — so the writes side is opened before the reader exists.\n */\n static beginConnect(\n readPath: string,\n writePath: string,\n ): { transport: Promise<FifoUtf8NlineTransport> } {\n const readFdPromise = open(readPath, \"r\");\n\n const transport = readFdPromise.then(async (readFd) => {\n const writeFd = await open(writePath, \"w\");\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n });\n\n return { transport };\n }\n\n /**\n * Open a bidirectional connection. Opens writePath for writing first\n * (unblocking the peer's read), starts reading readPath in the\n * background, then awaits both.\n *\n * Use this when you are the side that responds to the peer's beginConnect\n * (i.e. you don't need to interleave any setup between the read and write\n * opens). Built like `beginConnect`: the transport exists only after both\n * directions do.\n */\n static async connect(readPath: string, writePath: string): Promise<FifoUtf8NlineTransport> {\n const readFdPromise = open(readPath, \"r\");\n const writeFd = await open(writePath, \"w\");\n const readFd = await readFdPromise;\n return FifoUtf8NlineTransport.fromFds(readFd, writeFd);\n }\n\n // ── public API ─────────────────────────────────────────────────────────\n\n get canSend(): boolean {\n return this.writeFd !== null && !this.closed;\n }\n\n onMessage(handler: (line: string) => void): void {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.readFd === null) throw new Error(\"FifoUtf8NlineTransport: not a reader\");\n if (this.pvtOnMessage !== null) {\n throw new Error(\"FifoUtf8NlineTransport: handler already set — call removeHandler() first\");\n }\n this.pvtOnMessage = handler;\n }\n\n removeHandler(): ((line: string) => void) | null {\n const prev = this.pvtOnMessage;\n this.pvtOnMessage = null;\n return prev;\n }\n\n onClose(handler: () => void): void {\n this.pvtOnClose = handler;\n }\n\n get hasHandler(): boolean {\n return this.pvtOnMessage !== null;\n }\n\n get lastError(): Error | null {\n return this.pvtError;\n }\n\n async send(line: string): Promise<void> {\n if (this.closed) throw new Error(\"FifoUtf8NlineTransport: closed\");\n if (this.writeFd === null) throw new Error(\"FifoUtf8NlineTransport: not a writer\");\n if (!line.endsWith(\"\\n\")) line += \"\\n\";\n await this.ws?.write(line);\n }\n\n async close(): Promise<void> {\n this.closingPromise = this.closingPromise || this.pvtClose();\n await this.closingPromise;\n this.closed = true;\n }\n\n private async pvtClose(): Promise<void> {\n // tranfser control back to caller\n // so they can capture the promise reference\n // and prevent reentry\n await Promise.resolve();\n\n if (this.rl) {\n this.rl.close();\n this.rl = null;\n }\n if (this.rs) {\n this.rs.destroy();\n this.rs = null;\n }\n if (this.ws) {\n await new Promise<void>((resolve) => {\n try {\n this.ws?.end(\"\", resolve);\n } catch {\n resolve();\n }\n });\n this.ws = null;\n }\n\n if (this.readFd) {\n try {\n await this.readFd.close();\n } catch {}\n this.readFd = null;\n }\n if (this.writeFd) {\n try {\n await this.writeFd.close();\n } catch {}\n this.writeFd = null;\n }\n }\n}\n","// ── Gateway ────────────────────────────────────────────────────────────────\n//\n// The first-stage command. Run this *inside* a foreign environment — directly,\n// under `sudo`, over ssh, in a container — and it:\n//\n// 1. creates the channel to the payload (fifo paths it makes itself, in a\n// private temp dir, so they belong to the uid that will open them),\n// 2. starts the payload — the first argument, required — hands it everything else it was\n// given (the host version among it) and relays frames between stdin/stdout,\n// 3. carries whatever the payload prints to the client as output frames\n// (`$fd`), so the payload's own stdout can never be mistaken for protocol.\n//\n// Why a gateway at all: a fifo path created on the client side is useless when the\n// payload runs as another uid (it cannot open it) or on another host (the path is\n// not there). stdio is the one channel every way into an environment gives us, so\n// the wire is stdio and the fifo stays inside the environment.\n//\n// The gateway reads nothing out of what it carries and owns no flag: the payload's path and\n// every argument after it go through verbatim, in order. The payload is the one who refuses a\n// host version it is not, and a relay that judged would be a second place for that decision.\n//\n// Directories and processes are cleaned up on every exit path, which is also why\n// the client never sees a transport being closed twice.\n//\n// It imports nothing but node builtins and this package, so the payload behind it\n// can be anything the runtime there can start.\n\nimport { execFile, spawn, type ChildProcess } from \"node:child_process\";\nimport { mkdtemp, rm } from \"node:fs/promises\";\nimport { tmpdir } from \"node:os\";\nimport { basename, join } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport type { OutputFd } from \"./channel.js\";\nimport { encode } from \"./protocols/json1.js\";\nimport { fdWriter } from \"./streams.js\";\nimport { errorFrame } from \"./stdio.js\";\nimport type { LineTransport } from \"./stdio.js\";\nimport { LineTransport as Lines } from \"./stdio.js\";\nimport { FifoUtf8NlineTransport } from \"./transports/fifo.js\";\n\nconst run = promisify(execFile);\n\n/** Exit code for \"this environment could not be set up\". */\nexport const GATEWAY_FAILED = 1;\n\nexport interface GatewayBoot {\n /**\n * The program to start for the payload: the first word of the command the client composed —\n * a runtime, or a program that runs itself. Required, and first, and the gateway has no\n * opinion of its own about which it is.\n */\n worker: string;\n /**\n * Everything after the payload, carried to it **as is** and in order: the consumer's own\n * arguments, and posipaki's own facts among them (the host version travels this way). The\n * gateway reads nothing by name and owns no flag, so nothing of the caller's is interpreted,\n * eaten, re-ordered or re-spelled — and the next fact added to that line needs no new\n * vocabulary here.\n */\n passthrough: string[];\n}\n\nfunction errorText(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n}\n\n/**\n * A label for the gateway's own temp directory: the program this environment was made for.\n * It is the gateway's business alone — the payload is not told which label it is.\n */\nfunction label(worker: string): string {\n const name = basename(worker).replace(/[^A-Za-z0-9.+@_-]/g, \"-\").slice(0, 60);\n return name === \"\" ? \"payload\" : name;\n}\n\n/**\n * The argv that boots this gateway — built by the client, read by\n * {@link gatewayBoot}, so the two halves cannot drift apart.\n *\n * The payload comes first and its arguments after it, so a positional argument of the\n * payload's own can never be mistaken for the payload.\n */\nexport function gatewayArgs(boot: GatewayBoot): string[] {\n return [boot.worker, ...boot.passthrough];\n}\n\n/** The boot a gateway was started with, or the reason it cannot start. */\nexport function gatewayBoot(argv: string[]): GatewayBoot {\n const [worker, ...passthrough] = argv;\n if (worker === undefined || worker.startsWith(\"--\")) {\n throw new Error(\"no <payload>: the gateway relays to a payload, named as its first argument\");\n }\n return { passthrough, worker };\n}\n\n/**\n * End a start that failed. The reason has already gone out on the wire, and\n * returning is not enough: the `open()` on a fifo no payload will ever write sits\n * in the threadpool forever, so the loop never drains and the gateway would linger\n * with a dead payload on its books.\n */\nfunction abandon(): never {\n process.exit(GATEWAY_FAILED);\n}\n\n/** Carry a failed start to the client, then let the caller exit non-zero. */\nasync function fail(wire: LineTransport, reason: string): Promise<void> {\n process.stderr.write(`gateway: ${reason}\\n`);\n try {\n await wire.send(errorFrame(reason));\n } catch {\n /* the client is gone too — the exit code still says what happened */\n }\n}\n\n/** Copy everything the payload prints to the client, as output frames of the fd it came in on. */\nfunction forwardOutput(stream: NodeJS.ReadableStream, wire: LineTransport, fd: OutputFd): void {\n stream.setEncoding(\"utf-8\");\n const out = fdWriter((frame) => wire.send(encode(frame)), fd);\n // The client hung up: the payload's exit is what matters now, and a frame that could not be\n // delivered is not something anyone here can act on.\n out.on(\"error\", () => {});\n stream.pipe(out, { end: false });\n}\n\n/**\n * Serve one environment over stdin/stdout. Returns the exit code; the caller\n * turns it into `process.exitCode`.\n */\nexport async function runGateway(argv: string[] = process.argv.slice(2)): Promise<number> {\n const wire = new Lines({ read: process.stdin, write: process.stdout });\n\n let booted: GatewayBoot;\n try {\n booted = gatewayBoot(argv);\n } catch (err) {\n // Nothing has been started yet, so there is nothing to tear down — but the\n // client still deserves the reason rather than a channel that closes on it,\n // and the loop can outlive us: an open stdin nobody will close keeps the\n // process alive, so this is an exit, not a return.\n await fail(wire, errorText(err));\n abandon();\n }\n const { passthrough, worker } = booted;\n\n const dir = await mkdtemp(join(tmpdir(), `posipaki-${label(worker)}-`));\n const fifoIn = join(dir, \"in\"); // the payload writes, we read\n const fifoOut = join(dir, \"out\"); // we write, the payload reads\n\n let child: ChildProcess | null = null;\n let fifo: FifoUtf8NlineTransport | null = null;\n\n /**\n * Everything this gateway owns, undone. All of it is idempotent, because the\n * exit path and the signal path can both arrive.\n */\n const teardown = async (): Promise<void> => {\n child?.kill();\n await fifo?.close().catch(() => {});\n await rm(dir, { recursive: true, force: true }).catch(() => {});\n };\n\n /**\n * A client that gives up stops us with a signal, and a gateway that died on the\n * spot would leave its payload behind in the environment — a process holding a\n * fifo nobody will ever write, which is a leak the environment cannot clean up\n * for us. So the signals do what the exit path does.\n */\n const onTerminate = () => {\n void teardown()\n .catch(() => {})\n .then(() => process.exit(GATEWAY_FAILED));\n };\n for (const signal of [\"SIGTERM\", \"SIGINT\", \"SIGHUP\"] as const) process.on(signal, onTerminate);\n\n try {\n await run(\"mkfifo\", [\"-m\", \"600\", fifoIn, fifoOut]);\n } catch (err) {\n await fail(wire, `cannot create the environment's fifos: ${errorText(err)}`);\n await teardown();\n abandon();\n }\n\n // Open the read side in the background, then start the payload that unblocks\n // it — the order the fifo handshake requires.\n const connection = FifoUtf8NlineTransport.beginConnect(fifoIn, fifoOut);\n const workerArgs = [worker, ...passthrough, `--fifo-in=${fifoIn}`, `--fifo-out=${fifoOut}`];\n // Whatever the client said starts the payload, run as it said: a runtime with a script of\n // ours, or a program of the caller's own. The gateway does not know which it is, and does\n // not choose a runtime on the far end's behalf.\n const workerProc = spawn(workerArgs[0]!, workerArgs.slice(1), {\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child = workerProc;\n forwardOutput(workerProc.stdout, wire, 1);\n forwardOutput(workerProc.stderr, wire, 2);\n\n const exited = new Promise<number>((settle) => {\n // A payload killed by a signal (the client hung up, and we killed it) has no\n // code — for us that is a clean end, not a failure.\n workerProc.once(\"exit\", (code) => settle(code ?? 0));\n });\n\n try {\n fifo = await Promise.race([\n connection.transport,\n exited.then((code) => {\n throw new Error(`the payload exited with code ${code} before it opened its channel`);\n }),\n ]);\n } catch (err) {\n await fail(wire, errorText(err));\n await teardown();\n abandon();\n }\n\n // Relay: the protocol is the client's and the payload's business, not ours.\n const relay = fifo;\n relay.onMessage((line) => {\n void wire.send(line).catch(() => {\n /* client gone */\n });\n });\n wire.onMessage((line) => {\n void relay.send(line).catch(() => {\n /* the payload is gone; its exit path cleans up */\n });\n });\n wire.onClose(() => {\n child?.kill();\n });\n\n const code = await exited;\n await teardown();\n await wire.close();\n return code;\n}\n","// ── The gateway, as a program ──────────────────────────────────────────────\n//\n// A gateway is something a client runs: the first-stage command that makes a fifo\n// inside a foreign environment and relays frames between its own stdin/stdout and\n// the payload. That is this file's whole job — it always serves, and it exits with\n// the code the run ended in.\n//\n// Serving lives in ./gateway.js and starts nothing by itself. A file cannot decide\n// \"am I the program?\" from where it is: a consumer that bundles the node surface\n// into its own program carries this code along, and there the bundle *is* the\n// program — a payload would find a gateway booting on its wire. So the decision is\n// in the layout instead: importing a module never starts anything, and running this\n// one always does.\n//\n// The client names it either way: `posipaki/remote/gateway-cli.js` is a published\n// specifier, so `import.meta.resolve` answers with the built file, and a consumer\n// with a program of its own stages that instead. Nothing here works out its own\n// location — a path derived from `import.meta.url` is a guess about a layout the\n// bundler owns, and the guess is wrong the moment the module moves into a chunk.\n\nimport { GATEWAY_FAILED, runGateway } from \"./gateway.js\";\n\nvoid (async () => {\n try {\n process.exitCode = await runGateway();\n } catch (err) {\n process.stderr.write(`gateway: ${err instanceof Error ? err.message : String(err)}\\n`);\n process.exitCode = GATEWAY_FAILED;\n }\n})();\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { i as Message, s as SenderInfo } from "../types-DYd-DDFB.js";
2
- import { $ as isProto, B as Channel, G as errorFrame, H as StringTransport, J as isError, K as errorReason, Q as isPause, U as badFrame, V as StreamKind, W as badReason, X as isInit, Y as isExit, Z as isMsg, _ as serveRemoteActor, a as ClientSpawner, b as ServedProcess, c as Connection, d as ReactorOptions, et as isRelease, f as ServedConnection, g as reactor, h as oneConnection, i as CHANNEL_LOST, l as Listener, m as SessionFactory, n as WebSocketLike, nt as isState, o as remoteClient, p as Session, q as isBad, r as WebSocketTransport, rt as isStop, s as AnyActorDefinition, t as WS_OPEN, tt as isResume, u as Reactor, v as Init, x as Spawner, y as Served, z as BAD_PREFIX_LIMIT } from "../websocket-Dyfw3WDD.js";
1
+ import { a as Message, c as SenderInfo } from "../types-CuabnerI.js";
2
+ import { A as OutputFd, B as isInit, C as Init, D as BAD_PREFIX_LIMIT, E as Spawner, F as errorFrame, G as isResume, H as isPause, I as errorReason, J as outputFrame, K as isState, L as isBad, M as StringTransport, N as badFrame, O as Channel, P as badReason, R as isError, S as serveRemoteActor, T as ServedProcess, U as isProto, V as isMsg, W as isRelease, Y as outputOf, _ as ServedConnection, b as oneConnection, c as ClientContext, d as remoteClient, f as AnyActorDefinition, g as ReactorOptions, h as Reactor, i as OutputSink, j as StreamKind, k as Frame, l as ClientSpawner, m as Listener, n as WebSocketLike, o as withOutput, p as Connection, q as isStop, r as WebSocketTransport, s as CHANNEL_LOST, t as WS_OPEN, u as RemoteClientOptions, v as Session, w as Served, x as reactor, y as SessionFactory, z as isExit } from "../websocket-sGQcEDbm.js";
3
3
  //#region src/remote/remote-process.d.ts
4
4
  interface RemoteExit {
5
5
  code: number;
@@ -261,12 +261,12 @@ interface WorkerLike {
261
261
  declare class WorkerTransport implements Channel {
262
262
  private worker;
263
263
  private pvtOnMessage;
264
+ private pvtSubscribe;
264
265
  private pvtOnClose;
265
266
  private closed;
266
267
  constructor(worker: WorkerLike);
267
268
  get canSend(): boolean;
268
- onMessage(handler: (frame: Record<string, unknown>) => void): void;
269
- removeHandler(): ((frame: Record<string, unknown>) => void) | null;
269
+ onMessage(handler: (frame: Record<string, unknown>) => void): () => void;
270
270
  onClose(handler: () => void): void;
271
271
  get hasHandler(): boolean;
272
272
  send(frame: Record<string, unknown>): Promise<void>;
@@ -342,5 +342,5 @@ declare function workerClientSpawner<Args = unknown>(url: string | ((args: Args)
342
342
  //#region src/remote/spawners/worker-self.d.ts
343
343
  declare function workerSelfSpawner(selfScope?: WorkerLike): Spawner;
344
344
  //#endregion
345
- export { type AnyActorDefinition, BAD_PREFIX_LIMIT, type BunServerWebSocketLike, CHANNEL_LOST, type Channel, type ClientSpawner, type Connection, type FrameSink, type Init, type Listener, type OpenedWebSocket, PROCESS_REF, type ProcessRef, type Reactor, type ReactorOptions, type RemoteMessage, RemoteProcess, type Served, type ServedConnection, type ServedProcess, type Session, type SessionFactory, type Spawner, type StringTransport, VERSION, WORKER_VERSION, WS_OPEN, type WebSocketClientOptions, type WebSocketCtor, type WebSocketLike, type WebSocketOpener, WebSocketTransport, type WorkerCtor, type WorkerLike, WorkerTransport, badFrame, badReason, bunServerWebSocket, decode, encode, errorFrame, errorReason, isBad, isError, isExit, isInit, isMsg, isPause, isProto, isRelease, isRemoteProcess, isResume, isState, isStop, json1Channel, makeSender, oneConnection, reactor, readFrame, remoteClient, rootIdFor, serveRemoteActor, workerClientSpawner, workerSelfSpawner, wsClientSpawner, wsServerSpawner };
345
+ export { type AnyActorDefinition, BAD_PREFIX_LIMIT, type BunServerWebSocketLike, CHANNEL_LOST, type Channel, type ClientContext, type ClientSpawner, type Connection, type Frame, type FrameSink, type Init, type Listener, type OpenedWebSocket, type OutputFd, type OutputSink, PROCESS_REF, type ProcessRef, type Reactor, type ReactorOptions, type RemoteClientOptions, type RemoteMessage, RemoteProcess, type Served, type ServedConnection, type ServedProcess, type Session, type SessionFactory, type Spawner, type StringTransport, VERSION, WORKER_VERSION, WS_OPEN, type WebSocketClientOptions, type WebSocketCtor, type WebSocketLike, type WebSocketOpener, WebSocketTransport, type WorkerCtor, type WorkerLike, WorkerTransport, badFrame, badReason, bunServerWebSocket, decode, encode, errorFrame, errorReason, isBad, isError, isExit, isInit, isMsg, isPause, isProto, isRelease, isRemoteProcess, isResume, isState, isStop, json1Channel, makeSender, oneConnection, outputFrame, outputOf, reactor, readFrame, remoteClient, rootIdFor, serveRemoteActor, withOutput, workerClientSpawner, workerSelfSpawner, wsClientSpawner, wsServerSpawner };
346
346
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/remote/remote-process.ts","../../src/remote/process-ref.ts","../../src/remote/sender.ts","../../src/remote/protocols/json1.ts","../../src/remote/transports/worker.ts","../../src/remote/spawners/ws-client.ts","../../src/remote/spawners/ws-server.ts","../../src/remote/spawners/worker-client.ts","../../src/remote/spawners/worker-self.ts"],"mappings":";;;UAyDiB;EACf;EACA;;;;;KAMU,aAAa,OAAO,yBAAyB,sBAAsB;;;KAInE,eAAe;;KAGf,cAAc,eAAe,UAAU,YAAY,KAAK,QAAQ;cAE/D,cAAc,cAAc,UAAU,SAAS,eAAe,UAAU;;WAE1E;;;WAGA,KAAK;;;WAGL;;EAET,OAAO;;WAEE,aAAa,mBAAmB,oBAAoB;UAErD;UACA;UACA;;;;UAIA;UACA;;UAEA;UACA;UACA;UACA;;UAEA;;;UAGA;;;UAGA;;;;UAIA;;;;;UAKA;EAER,YAAY,KAAK,YAAY,MAAM,WAAW,kBAAkB,QAAQ;;;;;;;;;;;EAmBxE;;;;;;EASA;;EAUA;;;;UAOQ;UAOA;;;;;;EAUR;;;;;;;EAWA;;;UAgBQ;;EAQR;;;;;;;EAkBA;;;;;;EAUA,cAAc;;UAUN;;;;;;UAUA;;;;UAYA;;;;;;;;;;EAiBR,SAAS;;UAQD;;;;;;;;;EAcR,QAAQ,QAAQ;;;;;;EAgBhB,QAAQ;;;;;;;EAgBR;;EAqBA;;EAMA;;;;;;;;;;;EAeA,KAAK,OAAO;;;;UASJ;;;;UAYA;;EAOR,KAAK,KAAK;;;;;EASV,UAAU,oBAAoB,IAAI,cAAc;EAChD,UAAU,kBAAkB;UAepB;;EAUR,aAAa,OAAO;;EASpB,eAAe,KAAK,QAAQ;;;;EAU5B,YAAY,MAAM;;;;EAYlB,eAAe,iBAAiB,OAAO,cAAc,oBAAoB;;;;;cCvc9D;;;;;;;iBAQG,UAAU,QAAQ;;UAWjB;EACf;EACA;;;;KAYU;;;;iBAoJI,gBAAgB,iBAAiB,SAAS;;;iBCvM1C,WACd,kBACA,2BACA,0BACC;;;cCMU;iBAEG,OAAO,OAAO;;;iBAMd,OAAO,cAAc;;;iBAMrB,UAAU,cAAc;iBAmBxB,aAAa,WAAW,kBAAkB;;;;cC1C7C;;;;;;UAOI;EACf,YAAY;EACZ,aAAa;IAAS;;EACtB,YAAY;EACZ;;cAGW,2BAA2B;UAC9B;UACA;UACA;UACA;EAER,YAAY,QAAQ;MAgBhB;EAIJ,UAAU,UAAU,OAAO;EAQ3B,mBAAmB,OAAO;EAM1B,QAAQ;MAIJ;EAIE,KAAK,OAAO,0BAA0B;EAKtC,SAAS;;;;;KC5DL;WACD;EACT,KAAK;EACL,MAAM,eAAe;EACrB;EACA,WAAW;EACX,aAAa;IAAS;;EACtB;;KAGU,qBAAqB,gBAAgB;;;;;;;KAQrC,gBAAgB,mBAAmB,aAAa,MAAM,SAAS;UAE1D,uBAAuB;;EAEtC,OAAO,gBAAgB;;iBA6BT,gBAAgB,gBAC9B,gBAAgB,MAAM,kBACtB,gBAAe,eACf,OAAM,uBAAuB,QAC5B,cAAc;;;iBC/DD,gBAAgB,IAAI,gBAAgB;;;;;;;UAcnC;EACf,KAAK;EACL,MAAM,eAAe;WACZ;EACT;;;;;;;;;;;;;;;;;;;;;iBAsBc,mBAAmB,IAAI,yBAAyB;;;KCxCpD,kBAAkB,gBAAgB;iBAI9B,oBAAoB,gBAClC,gBAAgB,MAAM,kBACtB,aAAY,aACX,cAAc;;;iBCRD,kBAAkB,YAAW,aAAmD"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/remote/remote-process.ts","../../src/remote/process-ref.ts","../../src/remote/sender.ts","../../src/remote/protocols/json1.ts","../../src/remote/transports/worker.ts","../../src/remote/spawners/ws-client.ts","../../src/remote/spawners/ws-server.ts","../../src/remote/spawners/worker-client.ts","../../src/remote/spawners/worker-self.ts"],"mappings":";;;UAyDiB;EACf;EACA;;;;;KAMU,aAAa,OAAO,yBAAyB,sBAAsB;;;KAInE,eAAe;;KAGf,cAAc,eAAe,UAAU,YAAY,KAAK,QAAQ;cAE/D,cAAc,cAAc,UAAU,SAAS,eAAe,UAAU;;WAE1E;;;WAGA,KAAK;;;WAGL;;EAET,OAAO;;WAEE,aAAa,mBAAmB,oBAAoB;UAErD;UACA;UACA;;;;UAIA;UACA;;UAEA;UACA;UACA;UACA;;UAEA;;;UAGA;;;UAGA;;;;UAIA;;;;;UAKA;EAER,YAAY,KAAK,YAAY,MAAM,WAAW,kBAAkB,QAAQ;;;;;;;;;;;EAmBxE;;;;;;EASA;;EAUA;;;;UAOQ;UAOA;;;;;;EAUR;;;;;;;EAWA;;;UAgBQ;;EAQR;;;;;;;EAkBA;;;;;;EAUA,cAAc;;UAUN;;;;;;UAUA;;;;UAYA;;;;;;;;;;EAiBR,SAAS;;UAQD;;;;;;;;;EAcR,QAAQ,QAAQ;;;;;;EAgBhB,QAAQ;;;;;;;EAgBR;;EAqBA;;EAMA;;;;;;;;;;;EAeA,KAAK,OAAO;;;;UASJ;;;;UAYA;;EAOR,KAAK,KAAK;;;;;EASV,UAAU,oBAAoB,IAAI,cAAc;EAChD,UAAU,kBAAkB;UAepB;;EAUR,aAAa,OAAO;;EASpB,eAAe,KAAK,QAAQ;;;;EAU5B,YAAY,MAAM;;;;EAYlB,eAAe,iBAAiB,OAAO,cAAc,oBAAoB;;;;;cCvc9D;;;;;;;iBAQG,UAAU,QAAQ;;UAWjB;EACf;EACA;;;;KAYU;;;;iBAoJI,gBAAgB,iBAAiB,SAAS;;;iBCvM1C,WACd,kBACA,2BACA,0BACC;;;cCMU;iBAMG,OAAO,OAAO;;;iBAMd,OAAO,cAAc;;;iBAMrB,UAAU,cAAc;iBAmBxB,aAAa,WAAW,kBAAkB;;;;cC9C7C;;;;;;UAOI;EACf,YAAY;EACZ,aAAa;IAAS;;EACtB,YAAY;EACZ;;cAGW,2BAA2B;UAC9B;UACA;UACA;UACA;UACA;EAER,YAAY,QAAQ;MAgBhB;EAIJ,UAAU,UAAU,OAAO;EAU3B,QAAQ;MAIJ;EAIE,KAAK,OAAO,0BAA0B;EAKtC,SAAS;;;;;KCzDL;WACD;EACT,KAAK;EACL,MAAM,eAAe;EACrB;EACA,WAAW;EACX,aAAa;IAAS;;EACtB;;KAGU,qBAAqB,gBAAgB;;;;;;;KAQrC,gBAAgB,mBAAmB,aAAa,MAAM,SAAS;UAE1D,uBAAuB;;EAEtC,OAAO,gBAAgB;;iBA6BT,gBAAgB,gBAC9B,gBAAgB,MAAM,kBACtB,gBAAe,eACf,OAAM,uBAAuB,QAC5B,cAAc;;;iBC/DD,gBAAgB,IAAI,gBAAgB;;;;;;;UAcnC;EACf,KAAK;EACL,MAAM,eAAe;WACZ;EACT;;;;;;;;;;;;;;;;;;;;;iBAsBc,mBAAmB,IAAI,yBAAyB;;;KCxCpD,kBAAkB,gBAAgB;iBAI9B,oBAAoB,gBAClC,gBAAgB,MAAM,kBACtB,aAAY,aACX,cAAc;;;iBCRD,kBAAkB,YAAW,aAAmD"}
@@ -1,10 +1,11 @@
1
- import { A as isStop, C as isInit, D as isRelease, E as isProto, M as isRemoteProcess, N as rootIdFor, O as isResume, S as isExit, T as isPause, _ as badReason, a as oneConnection, b as isBad, c as makeSender, d as decode, f as encode, g as badFrame, h as BAD_PREFIX_LIMIT, i as remoteClient, j as PROCESS_REF, k as isState, l as RemoteProcess, m as readFrame, n as WebSocketTransport, o as reactor, p as json1Channel, r as CHANNEL_LOST, s as serveRemoteActor, t as WS_OPEN, u as VERSION, v as errorFrame, w as isMsg, x as isError, y as errorReason } from "../websocket-Dqd8Hu6G.js";
1
+ import { A as isRelease, C as isBad, D as isMsg, E as isInit, F as outputOf, I as PROCESS_REF, L as isRemoteProcess, M as isState, N as isStop, O as isPause, P as outputFrame, R as rootIdFor, S as frameReaders, T as isExit, _ as BAD_PREFIX_LIMIT, a as CHANNEL_LOST, b as errorFrame, c as reactor, d as RemoteProcess, f as VERSION, g as readFrame, h as json1Channel, i as withOutput, j as isResume, k as isProto, l as serveRemoteActor, m as encode, n as WebSocketTransport, o as remoteClient, p as decode, s as oneConnection, t as WS_OPEN, u as makeSender, v as badFrame, w as isError, x as errorReason, y as badReason } from "../websocket-DecgMfb3.js";
2
2
  //#region src/remote/transports/worker.ts
3
3
  /** The worker encoding: native structured clone (no JSON). */
4
4
  const WORKER_VERSION = "clone.v1";
5
5
  var WorkerTransport = class {
6
6
  constructor(worker) {
7
7
  this.pvtOnMessage = null;
8
+ this.pvtSubscribe = null;
8
9
  this.pvtOnClose = null;
9
10
  this.closed = false;
10
11
  this.worker = worker;
@@ -21,13 +22,10 @@ var WorkerTransport = class {
21
22
  }
22
23
  onMessage(handler) {
23
24
  if (this.closed) throw new Error("WorkerTransport: closed");
24
- if (this.pvtOnMessage !== null) throw new Error("WorkerTransport: handler already set — call removeHandler() first");
25
- this.pvtOnMessage = handler;
26
- }
27
- removeHandler() {
28
- const prev = this.pvtOnMessage;
29
- this.pvtOnMessage = null;
30
- return prev;
25
+ this.pvtSubscribe ??= frameReaders((deliver) => {
26
+ this.pvtOnMessage = deliver;
27
+ });
28
+ return this.pvtSubscribe(handler);
31
29
  }
32
30
  onClose(handler) {
33
31
  this.pvtOnClose = handler;
@@ -80,11 +78,13 @@ function wsClientSpawner(url, WebSocketImpl = defaultWebSocket, opts = {}) {
80
78
  const transport = new WebSocketTransport(ws);
81
79
  const channel = json1Channel(transport);
82
80
  const protoPromise = new Promise((resolve) => {
83
- channel.onMessage((frame) => resolve(frame));
81
+ const off = channel.onMessage((frame) => {
82
+ off();
83
+ resolve(frame);
84
+ });
84
85
  });
85
86
  await openWebSocket(ws);
86
87
  const protoFrame = await protoPromise;
87
- channel.removeHandler();
88
88
  if (!isProto(protoFrame) || protoFrame.$proto !== "json.v1") throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);
89
89
  return channel;
90
90
  };
@@ -133,9 +133,11 @@ function workerClientSpawner(url, WorkerImpl = defaultWorker) {
133
133
  return async (args) => {
134
134
  const channel = new WorkerTransport(new WorkerImpl(typeof url === "function" ? url(args) : url));
135
135
  const protoFrame = await new Promise((resolve) => {
136
- channel.onMessage((frame) => resolve(frame));
136
+ const off = channel.onMessage((frame) => {
137
+ off();
138
+ resolve(frame);
139
+ });
137
140
  });
138
- channel.removeHandler();
139
141
  if (!isProto(protoFrame) || protoFrame.$proto !== "clone.v1") throw new Error(`unsupported protocol: ${JSON.stringify(protoFrame).slice(0, 50)}`);
140
142
  return channel;
141
143
  };
@@ -150,6 +152,6 @@ function workerSelfSpawner(selfScope = globalThis) {
150
152
  };
151
153
  }
152
154
  //#endregion
153
- export { BAD_PREFIX_LIMIT, CHANNEL_LOST, PROCESS_REF, RemoteProcess, VERSION, WORKER_VERSION, WS_OPEN, WebSocketTransport, WorkerTransport, badFrame, badReason, bunServerWebSocket, decode, encode, errorFrame, errorReason, isBad, isError, isExit, isInit, isMsg, isPause, isProto, isRelease, isRemoteProcess, isResume, isState, isStop, json1Channel, makeSender, oneConnection, reactor, readFrame, remoteClient, rootIdFor, serveRemoteActor, workerClientSpawner, workerSelfSpawner, wsClientSpawner, wsServerSpawner };
155
+ export { BAD_PREFIX_LIMIT, CHANNEL_LOST, PROCESS_REF, RemoteProcess, VERSION, WORKER_VERSION, WS_OPEN, WebSocketTransport, WorkerTransport, badFrame, badReason, bunServerWebSocket, decode, encode, errorFrame, errorReason, isBad, isError, isExit, isInit, isMsg, isPause, isProto, isRelease, isRemoteProcess, isResume, isState, isStop, json1Channel, makeSender, oneConnection, outputFrame, outputOf, reactor, readFrame, remoteClient, rootIdFor, serveRemoteActor, withOutput, workerClientSpawner, workerSelfSpawner, wsClientSpawner, wsServerSpawner };
154
156
 
155
157
  //# sourceMappingURL=index.js.map