Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions crates/js-component-bindgen/src/intrinsics/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1775,6 +1775,32 @@ mod tests {
}
}

#[test]
fn stream_drop_rejects_busy_ends_before_table_removal() {
for (intrinsic, error) in [
(
AsyncStreamIntrinsic::StreamDropReadable,
"cannot remove busy stream",
),
(
AsyncStreamIntrinsic::StreamDropWritable,
"cannot drop busy stream",
),
] {
let source = render_intrinsic_body(Intrinsic::AsyncStream(intrinsic));
let busy_check = source
.find("if (streamEnd.isCopying() || streamEnd.hasPendingEvent()) {")
.expect("stream drop should reject an active or undelivered copy");
let removal = source
.find("const removedStreamEnd = deleteStreamEnd(")
.expect("stream drop should remove a validated idle end");

assert!(source.contains(&format!("throw new Error('{error}');")));
assert!(busy_check < removal, "busy validation must precede removal");
assert!(source.contains("if (removedStreamEnd !== streamEnd) {"));
}
}

#[test]
fn future_ends_track_own_and_peer_drop_state_separately() {
let mut intrinsics = BTreeSet::from([Intrinsic::AsyncFuture(
Expand Down
19 changes: 18 additions & 1 deletion crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2320,6 +2320,7 @@ impl AsyncStreamIntrinsic {
Self::StreamDropReadable | Self::StreamDropWritable => {
let debug_log_fn = render_args.require_intrinsic(Intrinsic::DebugLog);
let stream_drop_fn = self.name();
let get_stream_end_fn = render_args.require_intrinsic(Self::GetStreamEnd);
let delete_stream_end_fn = render_args.require_intrinsic(Self::DeleteStreamEnd);
let current_task_get_fn = render_args
.require_intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask));
Expand All @@ -2329,6 +2330,11 @@ impl AsyncStreamIntrinsic {
} else {
render_args.require_intrinsic(Self::StreamReadableEndClass)
};
let busy_error = if is_write {
"cannot drop busy stream"
} else {
"cannot remove busy stream"
};
let get_or_create_async_state_fn = render_args.require_intrinsic(
Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState),
);
Expand All @@ -2343,7 +2349,7 @@ impl AsyncStreamIntrinsic {
const cstate = {get_or_create_async_state_fn}(componentIdx);
if (!cstate) {{ throw new Error(`missing component state for component idx [${{componentIdx}}]`); }}

const streamEnd = {delete_stream_end_fn}({{ tableIdx: streamTableIdx, streamEndWaitableIdx }});
const streamEnd = {get_stream_end_fn}({{ tableIdx: streamTableIdx, streamEndWaitableIdx }});
if (!streamEnd) {{
throw new Error(`missing stream (waitable [${{streamEndWaitableIdx}}], table [${{streamTableIdx}}], component [${{componentIdx}}])`);
}}
Expand All @@ -2352,6 +2358,17 @@ impl AsyncStreamIntrinsic {
throw new Error('invalid stream end class, expected [{stream_end_class}]');
}}

// Copy completion is not observable until the guest consumes its
// pending event, so both an active copy and an undelivered event
// keep the table-local stream handle busy.
if (streamEnd.isCopying() || streamEnd.hasPendingEvent()) {{
throw new Error('{busy_error}');
}}

const removedStreamEnd = {delete_stream_end_fn}({{ tableIdx: streamTableIdx, streamEndWaitableIdx }});
if (removedStreamEnd !== streamEnd) {{
throw new Error('removed stream end does not match validated stream end');
}}
streamEnd.drop();
}}
"#));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const WAST_TESTS: readonly WastTest[] = [
{ relPath: 'async/sync-streams.wast' },
{ relPath: 'async/dont-block-start.wast' },
{ relPath: 'async/closed-stream.wast' },
{ relPath: 'async/drop-stream.wast' },

// Skipped tests
{ relPath: 'async/trap-if-block-and-sync.wast', skip: true },
Expand All @@ -56,7 +57,6 @@ const WAST_TESTS: readonly WastTest[] = [
{ relPath: 'async/cross-abi-calls.wast', skip: true },
{ relPath: 'async/drop-cross-task-borrow.wast', skip: true },
{ relPath: 'async/trap-if-done.wast', skip: true },
{ relPath: 'async/drop-stream.wast', skip: true },
];

// These tests are ported from the component-model repo
Expand Down
Loading