Skip to content

Support duplex additional child-process file descriptors - #8905

Merged
tim-smart merged 8 commits into
v4/next-minorfrom
agent/effect-bot-architect/e7bb04282397
Oct 8, 2026
Merged

tim-smart merged 8 commits into
v4/next-minorfrom
agent/effect-bot-architect/e7bb04282397

Conversation

@effect-bot

@effect-bot effect-bot commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Adds a duplex variant to ChildProcess additionalFds, so a sidecar can run a request/response protocol over one inherited descriptor.

const handle = yield* ChildProcess.make("sidecar", [], {
  additionalFds: { fd3: { type: "duplex" } }
})
// write with handle.getInputFd(3), read with handle.getOutputFd(3)

Changes

  • AdditionalFdConfig gets { type: "duplex", stream?, sink? }. ChildProcessHandle keeps its shape: getInputFd and getOutputFd both work on a duplex fd, so other spawner implementations are unaffected. DenoChildProcessSpawner already rejects additionalFds.
  • NodeChildProcessSpawner wires the read stream and the write sink to the same child.stdio[fd] socket and sets allowHalfOpen = true on it, so the parent can still write after the child half-closes. When the input sink completes it calls end(), as input fds and stdin already do. On POSIX this is a real shutdown(SHUT_WR).
  • Additional-fd output adapters retain the first error from setup onward and check it when reading starts. A duplex write failure no longer emits an uncaught error, and a delayed reader receives a mapped PlatformError instead of hanging.
  • pipeTo(..., { to: "fdN" }) no longer replaces a duplex target with { type: "input" }. The target stays duplex and the piped stream becomes its input.
  • The JSDoc for additionalFds, PipeToOptions.to and the handle getters describes the platform support below.

Platform support (as documented)

  • Linux evidence: Effect's Node spawner is validated by the suite below. Separate raw node:child_process compatibility probes under Node.js and Deno verified same-fd request/response and half-close in both directions; those Deno probes did not validate Effect's spawner. Native DenoChildProcessSpawner still rejects additionalFds.
  • Expected, not verified: the same behavior on macOS, because it uses the same POSIX socket-pair path in libuv, Deno and Bun. Request/response on Windows is also expected, because libuv and Deno create a duplex named pipe.
  • Not supported: half-close on Windows, because named pipes have no write-side shutdown. Half-close on Bun is also unsupported, because Bun's end() closes the whole socket. The docs tell users to keep the input open until every response has been read.
  • Unverified: extra fds with Bun on Windows.
  • Child side: use new net.Socket({ fd }) in Node.js and Deno. Deno children can't use node:fs on the raw fd, and Bun children can't use net.Socket on it.

Tests

The final shared spawner suite adds two focused regressions:

  1. Same-fd request/response after pipeTo ends its input, covering duplex-target preservation and parent half-close on POSIX.
  2. A write failure before reading starts, checking the mapped write failure, absence of an uncaught exception, and mapped failure for a delayed reader rather than a hang.

Validation

At 1ba3ef7ef on Linux / Node 26.8.2:

  • pnpm vitest --run for packages/platform/node-shared/test, packages/effect/test/process and DenoChildProcessSpawner.test.ts: 202 passed. The native Deno spawner tests do not establish duplex support there.
  • pnpm lint and pnpm check: clean.
  • Both retained duplex tests passed in three repeated runs; the reviewer independently ran the full Node spawner file: 80/80 passed.

Earlier validation also passed pnpm jsdocs --check and the doctests for the two changed modules (8 passed). A separate manual check under Node and Bun confirmed that an unused duplex write side does not hold exitCode or the event loop open after the child exits. No macOS or Windows execution was performed.

Closes #8902
Closes EFF-1844

Covers same-fd request/response, POSIX half-close in both directions,
and pipeTo preserving a duplex fd target. Implementation follows.

Refs EFF-1844, #8902
@changeset-bot

changeset-bot Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f1ec011

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 32 packages
Name Type
effect Patch
@effect/platform-node-shared Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@effect-bot
effect-bot changed the base branch from main to v4/next-minor October 8, 2026 06:50
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 38.79 KB 38.79 KB 0.00 KB (0.00%)
basic.ts 7.06 KB 7.06 KB 0.00 KB (0.00%)
batching.ts 10.14 KB 10.14 KB 0.00 KB (0.00%)
brand.ts 6.73 KB 6.73 KB 0.00 KB (0.00%)
cache.ts 10.89 KB 10.89 KB 0.00 KB (0.00%)
config.ts 22.27 KB 22.27 KB 0.00 KB (0.00%)
differ.ts 21.34 KB 21.34 KB 0.00 KB (0.00%)
http-client.ts 22.67 KB 22.67 KB 0.00 KB (0.00%)
http-router.ts 34.05 KB 34.05 KB 0.00 KB (0.00%)
logger.ts 11.22 KB 11.22 KB 0.00 KB (0.00%)
metric.ts 9.03 KB 9.03 KB 0.00 KB (0.00%)
optic.ts 6.96 KB 6.96 KB 0.00 KB (0.00%)
pubsub.ts 15.18 KB 15.18 KB 0.00 KB (0.00%)
queue.ts 12.13 KB 12.13 KB 0.00 KB (0.00%)
schedule.ts 11.22 KB 11.22 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.76 KB 13.76 KB 0.00 KB (0.00%)
schema-binary.ts 40.00 KB 40.00 KB 0.00 KB (0.00%)
schema-class.ts 21.09 KB 21.09 KB 0.00 KB (0.00%)
schema-fromJsonSchemaDocument.ts 32.10 KB 32.10 KB 0.00 KB (0.00%)
schema-representation-roundtrip.ts 27.31 KB 27.31 KB 0.00 KB (0.00%)
schema-string-transformation.ts 14.49 KB 14.49 KB 0.00 KB (0.00%)
schema-string.ts 12.02 KB 12.02 KB 0.00 KB (0.00%)
schema-template-literal.ts 16.09 KB 16.09 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.32 KB 38.32 KB 0.00 KB (0.00%)
schema-toCodeDocument.ts 25.30 KB 25.30 KB 0.00 KB (0.00%)
schema-toCodecJson.ts 20.26 KB 20.26 KB 0.00 KB (0.00%)
schema-toEquivalence.ts 20.45 KB 20.45 KB 0.00 KB (0.00%)
schema-toFormatter.ts 20.57 KB 20.57 KB 0.00 KB (0.00%)
schema-toJsonSchemaDocument.ts 25.03 KB 25.03 KB 0.00 KB (0.00%)
schema-toRepresentation.ts 20.55 KB 20.55 KB 0.00 KB (0.00%)
schema.ts 20.31 KB 20.31 KB 0.00 KB (0.00%)
stm.ts 13.24 KB 13.24 KB 0.00 KB (0.00%)
stream.ts 9.99 KB 9.99 KB 0.00 KB (0.00%)

const inputSinks = new Map<number, Sink.Sink<void, Uint8Array, never, PlatformError.PlatformError>>()
const outputStreams = new Map<number, Stream.Stream<Uint8Array, PlatformError.PlatformError>>()

const setupInput = Effect.fnUntraced(function*(

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.

This could use Effect.suspend instead

@tim-smart
tim-smart enabled auto-merge (squash) October 8, 2026 08:02
@tim-smart
tim-smart merged commit b8ff68b into v4/next-minor Oct 8, 2026
10 checks passed
@tim-smart
tim-smart deleted the agent/effect-bot-architect/e7bb04282397 branch October 8, 2026 08:10
@effect-bot effect-bot mentioned this pull request Oct 8, 2026
@effect-bot effect-bot mentioned this pull request Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ChildProcess: support bidirectional (duplex) additional file descriptors

2 participants