Skip to content

fix(runtime): honor promisify.custom on stream.pipeline/finished (#6692) - #6694

Merged
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6692-promisify-pipeline-variadic
Jul 19, 2026
Merged

fix(runtime): honor promisify.custom on stream.pipeline/finished (#6692)#6694
proggeramlug merged 2 commits into
PerryTS:mainfrom
proggeramlug:fix/6692-promisify-pipeline-variadic

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #6692. Node defines stream.pipeline[util.promisify.custom] and stream.finished[util.promisify.custom], pointing at the promise-based node:stream/promises implementations. So promisify(stream.pipeline) in Node returns a (...streams) => Promise with no trailing callback.

Perry never installed those hooks, so promisify(stream.pipeline) fell back to the generic callback-appending wrapper. The bundled node-fetch that pi uses to download fd does:

const pipeline = promisify(Stream.pipeline);   // expects Node's promise impl
...
await pipeline(body, dest);

Routing that through the generic wrapper (instead of Node's dedicated promise-pipeline) is where the stream args got dropped under some dispatch paths, surfacing as TypeError: The "streams" argument must be specified (ERR_MISSING_ARGS).

Fix

Install the promisify.custom symbol on the stream.pipeline / stream.finished bound-native exports, pointing at the corresponding stream/promises callables. The existing custom_promisified_value path in util_promisify.rs then honors them, so promisify(stream.pipeline) returns the dedicated, well-tested promise-pipeline (thunk_streamP_pipeline, registered rest fixed_arity = 2) instead of the generic wrapper — matching Node exactly and sidestepping the fragile forwarding path entirely.

  • object/native_module/callable_exports.rs: wire the custom hook when minting the pipeline / finished exports (GC-rooted via a handle scope; no-ops if stream/promises is unavailable, leaving the generic fallback in place).
  • node_submodules/mod.rs: stream_promises_export_callable helper that installs the submodule registry entry (so the lookup succeeds even when the program never imported stream/promises) and returns its export value.

Behavior parity (Node 26.5.0)

expression before after / Node
typeof stream.pipeline[Symbol.for("nodejs.util.promisify.custom")] undefined function
typeof stream.finished[Symbol.for("nodejs.util.promisify.custom")] undefined function
promisify(pipeline)(src, dst) generic wrapper promise-pipeline; resolves undefined

Testing

  • New test-files/test_gap_promisify_pipeline_custom_6692.ts — byte-matches Node 26.5.0 (custom-symbol presence, promisified pipeline with a transform stage, promisified finished). Verified under both the fast dev build and the full auto-optimize / LTO (release) path.
  • Existing test_parity_stream_promises, test_issue_3070_stream_promises_input_validation, test_issue_1856_1857_exports_promisify still byte-match Node.
  • cargo test -p perry-runtime --lib stream_promises / native_module_stream suites pass.

Notes

  • Per maintainer instruction this PR carries no version bump / changelog entry.
  • Separate pre-existing gap (not addressed here): a web ReadableStream passed as a pipeline body isn't recognized by the stream/promises body validator — after this change it rejects with ERR_INVALID_ARG_TYPE (previously the generic path silently hung). Both diverge from Node, which accepts web streams; that's a distinct web-stream-support gap.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added promise-based custom behavior for util.promisify when promisifying stream.pipeline and stream.finished.
  • Bug Fixes

    • Ensures promisified pipeline/finished properly hook into the corresponding stream/promises implementations, with correct handling for unknown exports.
  • Tests

    • Added a test validating util.promisify.custom for stream.pipeline (including transform stages) and stream.finished promise resolution behavior.

…ryTS#6692)

Node defines `stream.pipeline[util.promisify.custom]` and
`stream.finished[util.promisify.custom]` pointing at the promise-based
`node:stream/promises` implementations, so `promisify(stream.pipeline)`
returns that impl (a `(...streams) => Promise` with no trailing callback).

Perry never installed those hooks, so `promisify(stream.pipeline)` fell
back to the generic callback-appending wrapper. Bundled node-fetch (pi's
fd downloader) does `const pipeline = promisify(Stream.pipeline); await
pipeline(body, dest)` expecting Node's custom promise impl; routing it
through the generic wrapper instead let the stream args be dropped under
some dispatch paths, surfacing as `ERR_MISSING_ARGS` ("The \"streams\"
argument must be specified").

Install the `promisify.custom` symbol on the `stream.pipeline` /
`stream.finished` bound-native exports, pointing at the corresponding
`stream/promises` callables. The existing `custom_promisified_value`
path in util_promisify.rs then honors them, so `promisify(...)` returns
the dedicated, well-tested promise-pipeline instead of the generic
wrapper — matching Node and sidestepping the fragile forwarding path.

- callable_exports.rs: wire the custom hook when minting the pipeline /
  finished exports (GC-rooted; no-ops if stream/promises is unavailable).
- node_submodules/mod.rs: `stream_promises_export_callable` helper that
  installs the submodule registry entry and returns its export value.

Adds test_gap_promisify_pipeline_custom_6692 (byte-matches Node 26.5.0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 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: Pro Plus

Run ID: 6742ee94-69a1-4989-b3bb-22736c89d422

📥 Commits

Reviewing files that changed from the base of the PR and between 8ec4a2b and db9f364.

📒 Files selected for processing (1)
  • crates/perry-runtime/src/object/native_module/callable_exports.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • crates/perry-runtime/src/object/native_module/callable_exports.rs

📝 Walkthrough

Walkthrough

The runtime adds util.promisify.custom hooks for stream.pipeline and stream.finished, resolving implementations from stream/promises. A regression test verifies hook exposure and promisified pipeline and finished behavior, including transformed streams.

Changes

Stream promisify integration

Layer / File(s) Summary
Attach stream promisify hooks
crates/perry-runtime/src/node_submodules/mod.rs, crates/perry-runtime/src/object/native_module/callable_exports.rs
Adds stream/promises export lookup and attaches the corresponding promise implementations to stream.pipeline and stream.finished through util.promisify.custom.
Validate promisified stream behavior
test-files/test_gap_promisify_pipeline_custom_6692.ts
Tests custom hook exposure, promisified pipelines with and without a transform, and promisified finished completion.

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

Sequence Diagram(s)

sequenceDiagram
  participant Test
  participant util_promisify
  participant stream_pipeline
  participant stream_promises
  Test->>util_promisify: promisify stream.pipeline
  util_promisify->>stream_pipeline: read util.promisify.custom
  stream_pipeline-->>util_promisify: return promise implementation
  Test->>util_promisify: invoke with streams
  util_promisify->>stream_promises: run promise-based pipeline
  stream_promises-->>Test: resolve undefined
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: honoring promisify.custom for stream.pipeline/finished.
Description check ✅ Passed The description covers the summary, fix, testing, and issue reference, and is mostly complete despite a few optional template sections being omitted.
Linked Issues check ✅ Passed The changes add Node-compatible promisify.custom hooks for stream.pipeline and stream.finished and include parity tests matching #6692's requirements.
Out of Scope Changes check ✅ Passed The diff stays focused on stream promisify behavior, supporting lookup, and tests, with no clear unrelated or extra-scope changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/perry-runtime/src/object/native_module/callable_exports.rs`:
- Around line 81-88: Propagate the relocated pointer from
attach_stream_promisify_custom: at
crates/perry-runtime/src/object/native_module/callable_exports.rs:81-88, shadow
value with the function’s returned f64; at
crates/perry-runtime/src/object/native_module/callable_exports.rs:228-260,
change attach_stream_promisify_custom to return f64 and return
target.get_nanbox_f64() after allocating operations so the caller receives the
updated pointer.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e1b51b46-8d2e-48bb-a577-08212353fadd

📥 Commits

Reviewing files that changed from the base of the PR and between a323f6d and 8ec4a2b.

📒 Files selected for processing (3)
  • crates/perry-runtime/src/node_submodules/mod.rs
  • crates/perry-runtime/src/object/native_module/callable_exports.rs
  • test-files/test_gap_promisify_pipeline_custom_6692.ts

Comment thread crates/perry-runtime/src/object/native_module/callable_exports.rs
…tom attach

CodeRabbit: attach_stream_promisify_custom roots the receiver in a handle
scope and then allocates (stream/promises export + custom symbol), which can
trigger a GC that evacuates the closure. Only the scope handle tracked the
move, so the caller's `value` local could go stale before it is stored in
NATIVE_CALLABLE_EXPORTS. Return the possibly-relocated pointer and have the
caller adopt it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@proggeramlug
proggeramlug merged commit 35a5a02 into PerryTS:main Jul 19, 2026
2 checks passed
@proggeramlug
proggeramlug deleted the fix/6692-promisify-pipeline-variadic branch July 19, 2026 17:22
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.

runtime: promisify(stream.pipeline) drops stream args → ERR_MISSING_ARGS (pi fd-download; promisify.custom / variadic forwarding)

1 participant