Skip to content

bun-types: declare Subprocess.connected - #38677

Open
robobun wants to merge 1 commit into
mainfrom
farm/e3bb2b05/subprocess-connected-type
Open

bun-types: declare Subprocess.connected#38677
robobun wants to merge 1 commit into
mainfrom
farm/e3bb2b05/subprocess-connected-type

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • Bun.spawn(cmd, { ipc() {} }).connected fails to type-check: error TS2339: Property 'connected' does not exist on type 'Subprocess<"ignore", "pipe", "inherit">'.
  • The property exists at runtime: src/runtime/api/BunObject.classes.ts:103 puts a connected getter on the Subprocess prototype, implemented by get_connected in src/runtime/api/bun/subprocess.rs:860, and node:child_process implements ChildProcess#connected by reading it (src/js/node/child_process.ts:1343).
  • interface Subprocess in packages/bun-types/bun.d.ts never declared it, so TypeScript users have to cast to reach it.

Fix

  • Declares readonly connected: boolean on interface Subprocess, with JSDoc describing when it is true and false.
  • The declaration mirrors the runtime: the getter returns a plain boolean (true while the IPC channel is open, false after disconnect(), after the child disconnected or exited, and always false without the ipc option), and it has no setter. Every statement in the JSDoc and the docs paragraph was checked by running it, see the transcript below; the send() claim comes from do_send in src/runtime/ipc_host.rs:117, which gates on the same is_connected() predicate as the getter and throws ERR_IPC_CHANNEL_CLOSED when it is false.
  • Documents the property in docs/runtime/child-process.mdx (IPC section and the Reference block).
  • Test: test/integration/bun-types/fixture/spawn.ts asserts proc.connected is a boolean on a process spawned with and without ipc, on the ipc callback's subprocess argument, and that assigning to it is an error. test/integration/bun-types/bun-types.test.ts gets a Bun.spawn > fixture/spawn.ts type-checks case that runs tsc over that fixture alone; it fails without the bun.d.ts change (4 x TS2339, on both release and debug builds) and passes with it.
  • Verified: bun bd test test/integration/bun-types/bun-types.test.ts (4 pass, 12 skipped as debug-only skips), and the same file under a release build, where the in-process cases also run (16 pass, including the lib.dom.d.ts variant and tsgo).
  • Not touched: the runtime Subprocess.prototype also has an undeclared writable getter (alias of stdin, returning a FileSink). It has been left out of bun-types since the package's first commit, presumably because it is not a WritableStream, so this PR leaves it alone. send()'s signature is being extended separately in bun-types: type Subprocess.send(message, handle, options, callback) and the ipc callback's handle #38662; this PR does not touch it.

Background

  • Bun.spawn with the ipc option opens a message channel (a socket pair) between the parent and a child bun/node process. subprocess.send() writes to it, the ipc callback receives from it, and either side can close it with disconnect(); it also closes when the child exits. connected is the parent's view of whether that channel is still open.
  • packages/bun-types is the published bun-types package; interface Subprocess in bun.d.ts is the type of the object Bun.spawn returns, so a runtime property missing from it is invisible to TypeScript users.
  • test/integration/bun-types/ packs bun-types and type-checks the fixture/ directory against it. Those in-process checks are skipped on debug builds (they drive the TypeScript language service and are very slow there), which is why this PR adds a case that spawns tsc on the one fixture file instead, following the existing Bun.mmap case in the same file.
Runtime behavior the declaration and docs describe (release build)
const noIpc = Bun.spawn([process.execPath, "-e", "0"]);
console.log("no ipc:", noIpc.connected, typeof noIpc.connected);

const p = Bun.spawn([process.execPath, "-e", "process.on('disconnect', () => process.exit(0)); setInterval(() => {}, 1000)"], { ipc() {} });
console.log("connected:", p.connected);
p.disconnect();
console.log("connected after disconnect():", p.connected);
try { p.send("x"); } catch (e) { console.log("send after disconnect:", e.code, "-", e.message); }
await p.exited;
console.log("connected after exit:", p.connected);
no ipc: false boolean
connected: true
connected after disconnect(): false
send after disconnect: ERR_IPC_CHANNEL_CLOSED - Subprocess.send() can only be used if an IPC channel is open.
connected after exit: false

A child that calls process.disconnect() itself also flips the parent's connected to false (observed after onDisconnect fired).

Failure without the bun.d.ts change
(fail) @types/bun integration test > Bun.spawn > fixture/spawn.ts type-checks
../base-fixture/spawn.ts(132,33): error TS2339: Property 'connected' does not exist on type 'Subprocess<"ignore", "pipe", "inherit">'.
../base-fixture/spawn.ts(136,23): error TS2339: Property 'connected' does not exist on type 'Subprocess<"ignore", "pipe", "inherit">'.
../base-fixture/spawn.ts(137,12): error TS2339: Property 'connected' does not exist on type 'Subprocess<"ignore", "pipe", "inherit">'.
../base-fixture/spawn.ts(146,23): error TS2339: Property 'connected' does not exist on type 'Subprocess<"ignore", "pipe", "inherit">'.

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.
@robobun
robobun requested a review from alii as a code owner August 14, 2026 20:32
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6ad65881-7ed7-4b6c-8da6-6f54684ddb2d

📥 Commits

Reviewing files that changed from the base of the PR and between 9cff2a1 and bd391ab.

📒 Files selected for processing (4)
  • docs/runtime/child-process.mdx
  • packages/bun-types/bun.d.ts
  • test/integration/bun-types/bun-types.test.ts
  • test/integration/bun-types/fixture/spawn.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review.

Reproduced by adding proc.connected assertions to test/integration/bun-types/fixture/spawn.ts and type-checking it against the packed bun-types: 4 x TS2339: Property 'connected' does not exist on type 'Subprocess<...>' on main, clean with this branch (bun bd test test/integration/bun-types/bun-types.test.ts, and the full file including the in-process and lib.dom.d.ts cases under a release build).

The runtime already has the getter (BunObject.classes.ts connected -> getConnected); this PR only adds the declaration, docs, and the type test.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — declares the existing runtime Subprocess.connected getter in bun-types, with matching docs and a type-check test.

What was reviewed:

  • Confirmed the getter exists on the prototype (BunObject.classes.ts:103, no setter) and onDisconnect referenced in the JSDoc is a real spawn option.
  • New fixture blocks land after line 126, so the pinned spawn.ts:62/spawn.ts:107 diagnostics in the lib.dom case are unaffected.
  • New Bun.spawn > fixture/spawn.ts type-checks case mirrors the existing Bun.mmap tsc-spawn pattern in the same file.
Extended reasoning...

Overview

Adds readonly connected: boolean to interface Subprocess in packages/bun-types/bun.d.ts, documents it in docs/runtime/child-process.mdx (IPC prose + Reference block), adds fixture assertions in test/integration/bun-types/fixture/spawn.ts, and adds a spawned-tsc test case in bun-types.test.ts that runs on debug builds too. No runtime code is touched.

Security risks

None. This is a .d.ts declaration plus documentation; nothing executes differently at runtime.

Level of scrutiny

Low. Type declarations for an already-shipping getter are mechanical. I verified the runtime side: src/runtime/api/BunObject.classes.ts:103 declares connected: { getter: "getConnected" } with no setter, so readonly boolean is the correct shape. The JSDoc's onDisconnect reference resolves to a real option at bun.d.ts:7140.

Other factors

  • The fixture additions are inserted after the existing proc.unref() block (line 126+), so the hard-coded spawn.ts:62:38 and spawn.ts:107:38 diagnostic line numbers in the checks with lib.dom.d.ts case remain valid.
  • The new test case is a near-verbatim copy of the adjacent Bun.mmap case (same tsc binary path, same typeRoots wiring, same stdout/stderr/exitCode assertion order), so it inherits a known-working pattern.
  • The PR description shows the test failing without the .d.ts change (4× TS2339) and passing with it on both debug and release builds, satisfying the fails-for-the-right-reason requirement.

@robobun

robobun commented Aug 16, 2026

Copy link
Copy Markdown
Collaborator Author

Heads-up: #39279 adds test/internal/source-lints/subprocess-types.test.ts, which compares interface Subprocess with the Subprocess proto table in BunObject.classes.ts and currently lists connected in its pendingDeclarations table pointing at this PR. Whichever of the two lands second will see that lint fail on rebase with a message to delete the connected entry; that one-line deletion is the only interaction, the two branches otherwise merge cleanly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant