✨ feat(node): add Stdio context API for host process stdio#214
✨ feat(node): add Stdio context API for host process stdio#214
Conversation
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.
📝 WalkthroughWalkthroughAdds a Stdio context API for Node.js standard streams with operations for Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 5 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (5)
node/mod.tsnode/package.jsonnode/stdio.test.tsnode/stdio.tsnode/tsconfig.json
commit: |
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.
|
do we have an explicit test case for when multiple consumers are reading off of |
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.
|
@cowboyd just added a test for this and it passes |
There was a problem hiding this comment.
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 | 🟡 MinorUpdate 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
📒 Files selected for processing (3)
node/README.mdnode/stdio.test.tsnode/stdio.ts
|
I don't think this can be tested by mocking stdin. |
Motivation
Node-facing code in this repo has no programmable seam for the host process's
stdin,stdout, andstderr. 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/processalready ships aStdiocontext 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
Stdiocontext API that lives in@effectionx/nodeand is solely about the host process's stdio.Approach
Stdiocontext API at@effectionx/nodewith scope"@effectionx/node/stdio", exposing three operations:stdin()— returns aStream<Uint8Array, void>sourced fromprocess.stdinvia the existingfromReadablehelper.stdout(bytes)— writes toprocess.stdout.stderr(bytes)— writes toprocess.stderr.stdin,stdout, andstderrare destructured fromStdio.operationsand re-exported so callers canyield* stdout(bytes)directly, matching the ergonomics of@effectionx/fs.Stdio.around({ *stdout(args, next) { ... return yield* next(...args); } })per the repo's(args, next)/next(...args)contract.Stdioin@effectionx/process(scope@effectionx/process:io) is unchanged. The two APIs coexist — different packages, different scope names, no context-registry collision.@effectionx/context-apiis added as a runtimedependencyof@effectionx/node.node/tsconfig.jsongains a reference to../context-api.0.2.4 → 0.3.0for the new public API surface.Usage
Tests
node/stdio.test.tscovers: 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
Tests
Documentation
Chores