Skip to content

fix(stream): wait for both finished sides - #9906

Closed
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9202-stream-finished-duplex
Closed

fix(stream): wait for both finished sides#9906
proggeramlug wants to merge 1 commit into
PerryTS:mainfrom
proggeramlug:fix/9202-stream-finished-duplex

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Calling callback-form stream.finished() on an unread PassThrough fired as soon as the writable side emitted finish. Node waits for the readable side too, so the minimal fixture exits without invoking the callback; Perry printed err: true.

This gives the default callback form a completion listener that tracks the stream's actual directional terminal events. Readable-only and writable-only streams still complete on their one side, duplex streams wait for both end and finish, and existing error/close behavior remains intact. Option-specific paths such as { readable: false } keep their existing listener.

Refs #9202.

Validation:

  • focused unit: finished_waits_for_both_passthrough_sides
  • stream/finished/*: 7/7 parity fixtures passed against Node 26.5.1
  • stream/promises/finished-*: 12/12 parity fixtures passed
  • full perry-runtime: 3,243 passed, 4 ignored; doc tests 8 ignored
  • ./scripts/run_lint_gates.sh: all 64 gates passed, 2 CI-only skipped

Summary by CodeRabbit

  • Bug Fixes
    • Fixed callback-form stream.finished() so it waits for both readable and writable sides of duplex streams to complete.
    • Unread PassThrough streams now correctly wait for their readable end event before invoking the completion callback.
    • Completion callbacks are invoked once the stream finishes, closes, or encounters an error.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 94655f11-d446-4062-bbf9-21bb001f6304

📥 Commits

Reviewing files that changed from the base of the PR and between 504e180 and 23f0e50.

📒 Files selected for processing (5)
  • changelog.d/9906-stream-finished-duplex.md
  • crates/perry-runtime/src/node_stream.rs
  • crates/perry-runtime/src/node_stream_constructors/pipeline.rs
  • crates/perry-runtime/src/node_stream_dispatch.rs
  • crates/perry-runtime/src/node_stream_tests.rs

Included review availability: Your plan provides up to 8 included reviews per hour; 5 remain after this review.


📝 Walkthrough

Walkthrough

Callback-form stream.finished() now waits for readable and writable completion on duplex streams. The runtime registers a new completion callback, and a PassThrough test verifies that the callback runs only after resumption.

Changes

Duplex stream completion

Layer / File(s) Summary
Completion logic and wiring
crates/perry-runtime/src/node_stream.rs, crates/perry-runtime/src/node_stream_constructors/pipeline.rs, crates/perry-runtime/src/node_stream_dispatch.rs
The default completion callback checks both stream sides, closed state, and readable errors before invoking the callback. The callback is registered and used by the default stream.finished() path.
Regression coverage and changelog
crates/perry-runtime/src/node_stream_tests.rs, changelog.d/9906-stream-finished-duplex.md
The PassThrough test verifies that completion waits after end("done") until resume. The changelog documents the behavior.

Estimated code review effort: 3 (Moderate) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 23f0e

Callback-form stream.finished() now completes after both sides of a duplex stream finish, preventing premature callbacks for unread PassThrough streams. The implementation and regression coverage align with the intended behavior, with no current merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 4 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description clearly explains the bug, the implementation, preserved behavior, related issue, and validation results. It does not reproduce every template heading or checklist item, but it contains…
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main change: stream.finished now waits for both terminal sides.
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.
Full details: Docstring Coverage

Explanation

Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 4 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

proggeramlug pushed a commit that referenced this pull request Sep 6, 2026
…he new roots

Two files went over 2000 lines:

- regex.rs -> regex/compile_cache.rs takes the program-compilation and
  cache group (size limit, std/fancy builders, eviction, the checked
  compile-and-cache entry). `js_regexp_new_impl` was the obvious bigger
  extraction and is deliberately NOT the one taken: it carries the two
  #7341 raw-handle debt sites, and raw_handle_debt.py's --no-raise-vs arm
  refuses a ceiling on a file absent at the merge base, so moving it would
  have forced either surgery on the allocation path two perf PRs are
  tuning, or a with_const_ptr(|p| p) that games the ratchet without
  scoping anything.
- regex/tests.rs -> regex/tests_part2.rs, split at a test boundary. All
  70 #[test] items are accounted for across the two files, and part2
  carries the same cfg(all(test, feature)) gate as its sibling.

#9893's new roots are classified: REGEXP_PROTOTYPE_PTR_SLOT and
REGEXP_PROTOTYPE_TEST_CLOSURE_SLOT are covered_elsewhere, naming
scan_object_cache_roots_mut, which really does visit them (as an i64 slot
and a nanbox word respectively) and is registered via reg_scanner!. The
test index and the walk counter are not_a_gc_pointer.

NEVER_MATCH moved with the compile-cache group, so its inventory entry is
retargeted. #9906's new test thread_local is recorded cold — it is a test
file, and the other 23 there are recorded the same way.
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Landed on main via merge train #9922. Validated as a tree: 64/64 lint gates, and perry-runtime/codegen/hir/stdlib all green (5,962 tests, 0 failures). Thanks!

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.

1 participant