Skip to content

✨ feat(node): add Stdio context API for host process stdio#214

Open
taras wants to merge 7 commits intomainfrom
feat/node-stdio-api
Open

✨ feat(node): add Stdio context API for host process stdio#214
taras wants to merge 7 commits intomainfrom
feat/node-stdio-api

Conversation

@taras
Copy link
Copy Markdown
Member

@taras taras commented Apr 23, 2026

Motivation

Node-facing code in this repo has no programmable seam for the host process's stdin, stdout, and stderr. Tests that want to assert what was written to stdout must reach for ad-hoc spies, and harnesses that want to feed synthesized stdin to code under test have no clean way to do so.

@effectionx/process already ships a Stdio context API, but that one governs child-process stdio — a different concern. Using it for host stdio conflated two scopes and was part of why an earlier attempt (in #159) at moving Stdio around had to be reverted.

This PR introduces a dedicated, middleware-capable Stdio context API that lives in @effectionx/node and is solely about the host process's stdio.

Approach

  • New Stdio context API at @effectionx/node with scope "@effectionx/node/stdio", exposing three operations:
    • stdin() — returns a Stream<Uint8Array, void> sourced from process.stdin via the existing fromReadable helper.
    • stdout(bytes) — writes to process.stdout.
    • stderr(bytes) — writes to process.stderr.
  • stdin, stdout, and stderr are destructured from Stdio.operations and re-exported so callers can yield* stdout(bytes) directly, matching the ergonomics of @effectionx/fs.
  • Middleware is installed with Stdio.around({ *stdout(args, next) { ... return yield* next(...args); } }) per the repo's (args, next) / next(...args) contract.
  • The child-process Stdio in @effectionx/process (scope @effectionx/process:io) is unchanged. The two APIs coexist — different packages, different scope names, no context-registry collision.
  • @effectionx/context-api is added as a runtime dependency of @effectionx/node.
  • node/tsconfig.json gains a reference to ../context-api.
  • Package version bumps 0.2.4 → 0.3.0 for the new public API surface.

Usage

import { main } from "effection";
import { Stdio, stdout } from "@effectionx/node";

await main(function* () {
  const captured: Uint8Array[] = [];

  yield* Stdio.around({
    *stdout(args, next) {
      captured.push(args[0]);
      return yield* next(...args);
    },
  });

  yield* stdout(new TextEncoder().encode("hello\n"));
  // bytes land in `captured` instead of the terminal
});

Tests

node/stdio.test.ts covers: stdout capture via middleware, stderr capture, stdin substitution with a synthetic stream, scope isolation (inner middleware does not leak to the outer scope), and a default-handler smoke test.

Summary by CodeRabbit

  • New Features

    • Added a Stdio context API for reading stdin and writing to stdout/stderr in Node.js
    • Public module exports expanded to expose stdio functionality
  • Tests

    • Added comprehensive tests covering Stdio middleware, stream operations, and scoping
  • Documentation

    • Updated docs with examples for host stdio usage, middleware interception, and stdin substitution
  • Chores

    • Package version bumped and manifest updated to include a new dependency

Introduces a middleware-capable context API at @effectionx/node that
wraps the host process's stdin / stdout / stderr. Enables tests and
harnesses to capture, redirect, or substitute host stdio without
touching the child-process Stdio in @effectionx/process.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a Stdio context API for Node.js standard streams with operations for stdin, stdout, and stderr. Exposes stdio via node/mod.ts, adds tests for middleware and defaults, bumps package version to 0.3.0, and adds a workspace dependency on @effectionx/context-api.

Changes

Cohort / File(s) Summary
Stdio API
node/stdio.ts
New context API Stdio and StdioApi interface: stdin(): Stream<Uint8Array>, stdout(bytes), stderr(bytes). Registers @effectionx/node/stdio and exports operations.
Tests
node/stdio.test.ts
New Vitest/EffectionX tests covering middleware interception, delegated writes, stdin stream replacement, scoped middleware behavior, and concurrent stdin consumers.
Module Re-exports
node/mod.ts
Re-exported ./stdio.ts to include stdio APIs in public module surface.
Package & TS Config
node/package.json, node/tsconfig.json
Package version bumped 0.2.4 → 0.3.0; added dependency @effectionx/context-api: "workspace:*"; added TS project reference to ../context-api.
Docs
node/README.md
Updated examples and new "Host Stdio" section documenting usage, middleware examples, and stdin/stdout/stderr operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • cowboyd
🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding a Stdio context API for host process stdio in the node package.
Description check ✅ Passed The description follows the required template with both Motivation and Approach sections fully completed, detailing the problem, solution design, usage examples, and testing approach.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Policy Compliance ✅ Passed Pull request complies with all applicable policies including commit message, package.json description/keywords, and appropriate version bump from 0.2.4 to 0.3.0.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/node-stdio-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@node/stdio.test.ts`:
- Around line 11-16: The test currently forwards captured bytes to the real
stdio by calling return yield* next(...args) inside the Stdio.around handlers
(stdout/stderr), which causes actual writes; change the handlers in the
Stdio.around call (the generator passed to Stdio.around, e.g., the *stdout and
*stderr functions that push into captured) to call return yield* next() without
forwarding the original args (or call next with an empty Buffer/string) so the
bytes are captured into the test variable but not written to the real
stdout/stderr; keep the captured.push(args[0]) logic and adjust both the stdout
and stderr handlers (the same pattern at lines 11-16 and 28-33) accordingly.

In `@node/stdio.ts`:
- Around line 39-48: The example shows middleware using Stdio.around and then
calling next(...args) so data is still written to the terminal; either stop
forwarding to next to truly capture output or update the trailing comment to
reflect that bytes are still written to the terminal. Modify the example around
handler (the stdout middleware passed to Stdio.around) so it does not call
next(...args) when you want to capture bytes (e.g., return without invoking
next), or change the final sentence after yield* stdout(...) to say the bytes
are captured but still forwarded to the terminal if next is called.
- Around line 57-62: The generator methods stdout and stderr currently call
process.stdout.write/process.stderr.write synchronously which swallows
backpressure and write errors; update the implementations (the *stdout and
*stderr generator functions) to wait for the write completion by using the write
callback or awaiting the 'drain' event when write returns false, and surface any
errors by rejecting/throwing them instead of ignoring them so callers observe
EPIPE/EACCES and other failures; ensure the logic honors the
Writable.write(callback) signature (or uses an Effection call to wrap the async
callback) and emits/drives backpressure correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 563004e6-dd70-490a-8e73-01bcf1a6752c

📥 Commits

Reviewing files that changed from the base of the PR and between 6982b1c and 1a70638.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (5)
  • node/mod.ts
  • node/package.json
  • node/stdio.test.ts
  • node/stdio.ts
  • node/tsconfig.json

Comment thread node/stdio.test.ts Outdated
Comment thread node/stdio.ts
Comment thread node/stdio.ts
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 23, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@effectionx/node@214

commit: 8fb5b68

taras added 5 commits April 23, 2026 11:03
Replace the placeholder smoke test with an end-to-end test that
redefines process.stdin as a PassThrough, feeds bytes through the
default Stdio.stdin handler, and asserts the bytes and end-of-stream
reach the subscription. Restores the original descriptor in finally
so the test stays hermetic.
Replace hook-observation tests with behavioral tests that swap
process.stdout / stderr / stdin for controllable fakes and assert both
that middleware observed the bytes and that delegation reached the
default handler. Strengthen the stdin substitution test to assert each
chunk and completion. Keep scope-isolation but route writes through a
fake so the test no longer prints to the real terminal.
Change the public stdin() contract from Operation<Stream<Uint8Array, void>>
to Stream<Uint8Array, void> so callers can do
`const sub = yield* stdin()` in one step. Since Stream already extends
Operation<Subscription>, the Operations-type mapping passes the
signature through unchanged, and the runtime wrapper unwraps the Stream
to a Subscription at the single yield* boundary — matching what the
type says.

- StdioApi.stdin is now `(): Stream<Uint8Array, void>`
- the default handler is a plain function, not a generator
- tests use the single-yield shape and the middleware substitution
  handler is a plain function returning the synthetic stream
Add a "Host Stdio" section covering: the default read/write path,
middleware via Stdio.around, delegation vs. redirection by calling or
omitting next(...args), and stdin substitution by returning a
replacement stream from a plain stdin middleware.
…ehavior

The example middleware calls `return yield* next(...args)`, so bytes
are captured AND forwarded. Update the trailing comment to reflect
that instead of claiming bytes are redirected away from the terminal.
@cowboyd
Copy link
Copy Markdown
Member

cowboyd commented Apr 23, 2026

do we have an explicit test case for when multiple consumers are reading off of stdin(). What has me asking is a situation I ran into recently where I needed to make only a single readable because it "steals" the bytes of every other subsequent readable. In other words, the very first ("data") listener on stdin saw all the bytes and all the others saw none.

Two subscriptions started before any bytes are written must both
observe the full chunk sequence and both complete on end-of-stream.
Locks in the multicast semantics so a future refactor that accidentally
shared state across stdin() calls would surface as a test failure.
@taras
Copy link
Copy Markdown
Member Author

taras commented Apr 23, 2026

@cowboyd just added a test for this and it passes

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
node/README.md (1)

17-23: ⚠️ Potential issue | 🟡 Minor

Update the module list to include stdio.

The README now documents Host Stdio, but the Modules section still says there are only two sub-modules.

📝 Proposed docs fix
-This package provides two sub-modules:
+This package provides three sub-modules:
 
 - `@effectionx/node/stream` - Stream utilities for Node.js
 - `@effectionx/node/events` - Event utilities for Node.js EventEmitters
+- `@effectionx/node/stdio` - Host process stdio context API
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@node/README.md` around lines 17 - 23, Update the Modules list in the README
to include the stdio sub-module by adding `@effectionx/node/stdio` to the bullet
list and mention that the `Stdio` context API is available (and can also be
imported from the main module), so replace the two-item list with three items
including `@effectionx/node/stdio` and ensure `Stdio` is referenced the same way
as `@effectionx/node/stream` and `@effectionx/node/events`.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@node/README.md`:
- Around line 17-23: Update the Modules list in the README to include the stdio
sub-module by adding `@effectionx/node/stdio` to the bullet list and mention
that the `Stdio` context API is available (and can also be imported from the
main module), so replace the two-item list with three items including
`@effectionx/node/stdio` and ensure `Stdio` is referenced the same way as
`@effectionx/node/stream` and `@effectionx/node/events`.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8a073476-ea93-4e54-99e0-daadafe78fd9

📥 Commits

Reviewing files that changed from the base of the PR and between 1a70638 and 8fb5b68.

📒 Files selected for processing (3)
  • node/README.md
  • node/stdio.test.ts
  • node/stdio.ts

@cowboyd
Copy link
Copy Markdown
Member

cowboyd commented Apr 23, 2026

I don't think this can be tested by mocking stdin.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants