From bd391ab3ce2c4e1ded6db9ab07de82f0d9651d15 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 14 Aug 2026 20:30:08 +0000 Subject: [PATCH] bun-types: declare Subprocess.connected The Subprocess prototype has had a connected getter since IPC landed (BunObject.classes.ts, getConnected), and node:child_process builds ChildProcess#connected on top of it, but interface Subprocess in bun.d.ts never declared it, so proc.connected is a TS2339 error for users. Declare it as readonly connected: boolean, document it in the child process docs, and assert on it in the bun-types spawn fixture. The fixture is now also type-checked on its own with tsc in bun-types.test.ts, since the in-process type-check cases are skipped on debug builds. --- docs/runtime/child-process.mdx | 9 ++++++ packages/bun-types/bun.d.ts | 25 +++++++++++++++++ test/integration/bun-types/bun-types.test.ts | 29 ++++++++++++++++++++ test/integration/bun-types/fixture/spawn.ts | 20 ++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/docs/runtime/child-process.mdx b/docs/runtime/child-process.mdx index a5c489152c68..ea20302e2c8a 100644 --- a/docs/runtime/child-process.mdx +++ b/docs/runtime/child-process.mdx @@ -304,6 +304,14 @@ The `serialization` option controls the underlying communication format between - `advanced`: (default) Messages are serialized using the JSC `serialize` API, which supports cloning [everything `structuredClone` supports](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm). This does not support transferring ownership of objects. - `json`: Messages are serialized using `JSON.stringify` and `JSON.parse`, which does not support as many object types as `advanced` does. +`childProc.connected` reports whether the IPC channel is still open. It is `true` from the time the process is spawned with `ipc` until either side disconnects or the child exits, and always `false` for a process spawned without `ipc`. Calling `.send()` once it is `false` throws `ERR_IPC_CHANNEL_CLOSED`, so check it before sending to a child that may have gone away: + +```ts +if (childProc.connected) { + childProc.send("still there?"); +} +``` + To disconnect the IPC channel from the parent process, call: ```ts @@ -580,6 +588,7 @@ interface Subprocess extends AsyncDisposable { readonly exitCode: number | null; readonly signalCode: NodeJS.Signals | null; readonly killed: boolean; + readonly connected: boolean; kill(exitCode?: number | NodeJS.Signals): void; ref(): void; diff --git a/packages/bun-types/bun.d.ts b/packages/bun-types/bun.d.ts index 175f4b6f4b4e..aad66c4c9c07 100644 --- a/packages/bun-types/bun.d.ts +++ b/packages/bun-types/bun.d.ts @@ -7518,6 +7518,31 @@ declare module "bun" { */ readonly killed: boolean; + /** + * Whether the IPC channel to the subprocess is open. + * + * `true` from the moment a process is spawned with the `ipc` option until the + * channel closes. It becomes `false` synchronously when {@link disconnect} is + * called (even if queued messages are still being flushed), and once the + * subprocess has called `process.disconnect()` or exited. It is always `false` + * for a process spawned without the `ipc` option. + * + * Once this is `false`, {@link send} fails with `ERR_IPC_CHANNEL_CLOSED`. To be + * notified when the channel closes, pass an `onDisconnect` callback to {@link Bun.spawn}. + * + * This is the same value `child_process.ChildProcess` exposes as `subprocess.connected`. + * + * @example + * ```ts + * const child = Bun.spawn(["bun", "child.ts"], { ipc(message) {} }); + * child.connected; // true + * + * child.disconnect(); + * child.connected; // false + * ``` + */ + readonly connected: boolean; + /** * Kill the process * @param exitCode Exit code or signal to send to the process diff --git a/test/integration/bun-types/bun-types.test.ts b/test/integration/bun-types/bun-types.test.ts index 05fa801f45e5..e1fca224640e 100644 --- a/test/integration/bun-types/bun-types.test.ts +++ b/test/integration/bun-types/bun-types.test.ts @@ -400,6 +400,35 @@ describe("@types/bun integration test", () => { }); }); + // Also runs on debug builds, where the in-process typeTest cases (which cover the whole + // fixture directory) are skipped: checks fixture/spawn.ts alone against the packed bun-types. + describe("Bun.spawn", () => { + test("fixture/spawn.ts type-checks", async () => { + const checkDir = join(TEMP_DIR, "spawn-fixture-check"); + const tsconfig = structuredClone(sourceTsconfig); + tsconfig.files = [join(BASE_FIXTURE_DIR, "spawn.ts")]; + tsconfig.compilerOptions.typeRoots = [join(BASE_FIXTURE_DIR, "node_modules", "@types")]; + await mkdir(checkDir, { recursive: true }); + await makeTree(checkDir, { + "tsconfig.json": JSON.stringify(tsconfig, null, 2), + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), join(BASE_FIXTURE_DIR, "node_modules", "typescript", "bin", "tsc"), "-p", "."], + env: bunEnv, + cwd: checkDir, + stdout: "pipe", + stderr: "pipe", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr.trim()).toBe(""); + expect(stdout.trim()).toBe(""); + expect(exitCode).toBe(0); + }); + }); + describe("Test Globals", () => { const code = ` const test_shouldBeAFunction: Function = test; diff --git a/test/integration/bun-types/fixture/spawn.ts b/test/integration/bun-types/fixture/spawn.ts index 2036413158a0..51fbfd762329 100644 --- a/test/integration/bun-types/fixture/spawn.ts +++ b/test/integration/bun-types/fixture/spawn.ts @@ -126,6 +126,26 @@ function depromise(_promise: Promise): T { proc.unref(); } +{ + const proc = Bun.spawn(["bun", "child.ts"], { + ipc(message, subprocess) { + tsd.expectType(subprocess.connected).is(); + }, + }); + + tsd.expectType(proc.connected).is(); + if (proc.connected) proc.disconnect(); + + // @ts-expect-error connected is a read-only getter + proc.connected = false; +} + +{ + // connected exists whether or not the process was spawned with `ipc` (it is false without it). + const proc = Bun.spawn(["echo", "hello"]); + tsd.expectType(proc.connected).is(); +} + { const proc = Bun.spawn(["echo", "hello"], { stdio: ["pipe", "pipe", "pipe"],