Streams: Implement stream pipe-to (#35650)

* implement PipeTo, stub pipe_to

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* define a data structure to manage the piping

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement propagation of errors forward and backward, stub shutdown and shutdown with action

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* adding more fine-grain shutdown variants to state

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement progagate closing backward and forward

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement shutdown and actions

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement reading and writing

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement shutdown continuation and finalize

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix typo

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* add can_gc arguments

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* implement writer close with error propagation

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* move and document wait on pending write

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* more docs

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* write pending reads as part of shutdown

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* turn on piping test suite

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* add comment about using Rust api
improve comment on result
add comment on backpressure

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix multiple propagations

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix writing of chunks

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix error and close propagation
update test expectations

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix warnings

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* improve docs
remove redundant logic in pending writes

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix clippy

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* remove unnecessary expansion of visibility of enqueued value to_jsval

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* remove unnecessary conditional accessing of streams when propagating states

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* improve docs

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* remove unused result var

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fix typo

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* remove redundant logic dealing with closed sources with pending writes

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* add doc links for shutdown actions

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* add comments on the need to return early when shutting down before checking close and error states

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* fmt

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>

* Update test expectations

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>

* fix can_gc

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>

---------

Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>
Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
Co-authored-by: Taym Haddadi <haddadi.taym@gmail.com>
This commit is contained in:
Gregory Terzian 2025-03-18 19:13:09 +08:00 committed by GitHub
parent 67a5f285ed
commit 8d39d7706a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1319 additions and 29 deletions

View file

@ -3,13 +3,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::ptr::{self};
use std::rc::Rc;
use dom_struct::dom_struct;
use js::conversions::ToJSValConvertible;
use js::jsapi::{Heap, JSObject};
use js::jsval::{JSVal, ObjectValue, UndefinedValue};
use js::jsval::{JSVal, NullValue, ObjectValue, UndefinedValue};
use js::rust::{
HandleObject as SafeHandleObject, HandleValue as SafeHandleValue,
MutableHandleValue as SafeMutableHandleValue,
@ -18,19 +19,22 @@ use js::typedarray::ArrayBufferViewU8;
use crate::dom::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategy;
use crate::dom::bindings::codegen::Bindings::ReadableStreamBinding::{
ReadableStreamGetReaderOptions, ReadableStreamMethods, ReadableStreamReaderMode,
ReadableStreamGetReaderOptions, ReadableStreamMethods, ReadableStreamReaderMode, StreamPipeOptions
};
use crate::dom::bindings::codegen::Bindings::ReadableStreamDefaultReaderBinding::ReadableStreamDefaultReaderMethods;
use crate::dom::bindings::codegen::Bindings::ReadableStreamDefaultControllerBinding::ReadableStreamDefaultController_Binding::ReadableStreamDefaultControllerMethods;
use crate::dom::bindings::codegen::Bindings::UnderlyingSourceBinding::UnderlyingSource as JsUnderlyingSource;
use crate::dom::bindings::conversions::{ConversionBehavior, ConversionResult};
use crate::dom::bindings::error::{Error, ErrorToJsval, Fallible};
use crate::dom::bindings::codegen::GenericBindings::WritableStreamDefaultWriterBinding::WritableStreamDefaultWriter_Binding::WritableStreamDefaultWriterMethods;
use crate::dom::writablestream::WritableStream;
use crate::dom::bindings::codegen::UnionTypes::ReadableStreamDefaultReaderOrReadableStreamBYOBReader as ReadableStreamReader;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::{DomRoot, MutNullableDom, Dom};
use crate::dom::bindings::trace::RootedTraceableBox;
use crate::dom::bindings::utils::get_dictionary_property;
use crate::dom::countqueuingstrategy::{extract_high_water_mark, extract_size_algorithm};
use crate::dom::readablestreamgenericreader::ReadableStreamGenericReader;
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::readablebytestreamcontroller::ReadableByteStreamController;
@ -40,6 +44,7 @@ use crate::dom::readablestreamdefaultreader::{ReadRequest, ReadableStreamDefault
use crate::dom::defaultteeunderlyingsource::TeeCancelAlgorithm;
use crate::dom::types::DefaultTeeUnderlyingSource;
use crate::dom::underlyingsourcecontainer::UnderlyingSourceType;
use crate::dom::writablestreamdefaultwriter::WritableStreamDefaultWriter;
use crate::js::conversions::FromJSValConvertible;
use crate::realms::{enter_realm, InRealm};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
@ -49,6 +54,619 @@ use super::bindings::buffer_source::HeapBufferSource;
use super::bindings::codegen::Bindings::ReadableStreamBYOBReaderBinding::ReadableStreamBYOBReaderReadOptions;
use super::readablestreambyobreader::ReadIntoRequest;
/// State Machine for `PipeTo`.
#[derive(Clone, Debug, Default, PartialEq)]
enum PipeToState {
/// The starting state
#[default]
Starting,
/// Waiting for the writer to be ready
PendingReady,
/// Waiting for a read to resolve.
PendingRead,
/// Waiting for all pending writes to finish,
/// as part of shutting down with an optional action.
ShuttingDownWithPendingWrites(Option<ShutdownAction>),
/// When shutting down with an action,
/// waiting for the action to complete,
/// at which point we can `finalize`.
ShuttingDownPendingAction,
/// The pipe has been finalized,
/// no further actions should be performed.
Finalized,
}
/// <https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action>
#[derive(Clone, Debug, PartialEq)]
enum ShutdownAction {
/// <https://streams.spec.whatwg.org/#writable-stream-abort>
WritableStreamAbort,
/// <https://streams.spec.whatwg.org/#readable-stream-cancel>
ReadableStreamCancel,
/// <https://streams.spec.whatwg.org/#writable-stream-default-writer-close-with-error-propagation>
WritableStreamDefaultWriterCloseWithErrorPropagation,
}
impl js::gc::Rootable for PipeTo {}
/// The "in parallel, but not really" part of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
///
/// Note: the spec is flexible about how this is done, but requires the following constraints to apply:
/// - Public API must not be used: we'll only use Rust.
/// - Backpressure must be enforced: we'll only read from source when dest is ready.
/// - Shutdown must stop activity: we'll do this together with the below.
/// - Error and close states must be propagated: we'll do this by checking these states at every step.
#[derive(Clone, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct PipeTo {
/// <https://streams.spec.whatwg.org/#ref-for-readablestream%E2%91%A7%E2%91%A0>
reader: Dom<ReadableStreamDefaultReader>,
/// <https://streams.spec.whatwg.org/#ref-for-acquire-writable-stream-default-writer>
writer: Dom<WritableStreamDefaultWriter>,
/// Pending writes are needed when shutting down(with an action),
/// because we can only finalize when all writes are finished.
#[ignore_malloc_size_of = "Rc are hard"]
pending_writes: Rc<RefCell<VecDeque<Rc<Promise>>>>,
/// The state machine.
#[ignore_malloc_size_of = "Rc are hard"]
#[no_trace]
state: Rc<RefCell<PipeToState>>,
/// <https://streams.spec.whatwg.org/#readablestream-pipe-to-preventabort>
prevent_abort: bool,
/// <https://streams.spec.whatwg.org/#readablestream-pipe-to-preventcancel>
prevent_cancel: bool,
/// <https://streams.spec.whatwg.org/#readablestream-pipe-to-preventclose>
prevent_close: bool,
/// The `shuttingDown` variable of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
#[ignore_malloc_size_of = "Rc are hard"]
shutting_down: Rc<Cell<bool>>,
/// The error potentially passed to shutdown,
/// stored here because we must keep it across a microtask.
#[ignore_malloc_size_of = "mozjs"]
shutdown_error: Rc<Heap<JSVal>>,
/// The promise returned by a shutdown action.
/// We keep it to only continue when it is not pending anymore.
#[ignore_malloc_size_of = "Rc are hard"]
shutdown_action_promise: Rc<RefCell<Option<Rc<Promise>>>>,
/// The promise resolved or rejected at
/// <https://streams.spec.whatwg.org/#rs-pipeTo-finalize>
#[ignore_malloc_size_of = "Rc are hard"]
result_promise: Rc<Promise>,
}
impl Callback for PipeTo {
/// The pipe makes progress one microtask at a time.
/// Note: we use one struct as the callback for all promises,
/// and for both of their reactions.
///
/// The context of the callback is determined from:
/// - the current state.
/// - the type of `result`.
/// - the state of a stored promise(in some cases).
fn callback(&self, cx: SafeJSContext, result: SafeHandleValue, realm: InRealm, can_gc: CanGc) {
let global = self.reader.global();
// Note: we only care about the result of writes when they are rejected,
// and the error is accessed not through handlers,
// but directly using `dest.get_stored_error`.
// So we must mark rejected promises as handled
// to prevent unhandled rejection errors.
self.pending_writes.borrow_mut().retain(|p| {
let pending = p.is_pending();
if !pending {
p.set_promise_is_handled();
}
pending
});
// Note: cloning to prevent re-borrow in methods called below.
let state_before_checks = self.state.borrow().clone();
// Note: if we are in a `PendingRead` state,
// and the source is closed,
// we try to write chunks before doing any shutdown,
// which is necessary to implement the
// "If any chunks have been read but not yet written, write them to dest."
// part of shutdown.
if state_before_checks == PipeToState::PendingRead {
let source = self.reader.get_stream().expect("Source stream must be set");
if source.is_closed() {
let dest = self
.writer
.get_stream()
.expect("Destination stream must be set");
// If dest.[[state]] is "writable",
// and ! WritableStreamCloseQueuedOrInFlight(dest) is false,
if dest.is_writable() && !dest.close_queued_or_in_flight() {
let has_done = {
if !result.is_object() {
false
} else {
rooted!(in(*cx) let object = result.to_object());
rooted!(in(*cx) let mut done = UndefinedValue());
get_dictionary_property(*cx, object.handle(), "done", done.handle_mut())
.unwrap()
}
};
// If any chunks have been read but not yet written, write them to dest.
let contained_bytes = self.write_chunk(cx, &global, result, can_gc);
if !contained_bytes && !has_done {
// This is the case that the microtask ran in reaction
// to the closed promise of the reader,
// so we should wait for subsequent chunks,
// and skip the shutdown below
// (reader is closed, but there are still pending reads).
// Shutdown will happen when the last chunk has been received.
return;
}
}
}
}
self.check_and_propagate_errors_forward(cx, &global, realm, can_gc);
self.check_and_propagate_errors_backward(cx, &global, realm, can_gc);
self.check_and_propagate_closing_forward(cx, &global, realm, can_gc);
self.check_and_propagate_closing_backward(cx, &global, realm, can_gc);
// Note: cloning to prevent re-borrow in methods called below.
let state = self.state.borrow().clone();
// If we switched to a shutdown state,
// return.
// Progress will be made at the next tick.
if state != state_before_checks {
return;
}
match state {
PipeToState::Starting => unreachable!("PipeTo should not be in the Starting state."),
PipeToState::PendingReady => {
// Read a chunk.
self.read_chunk(&global, realm, can_gc);
},
PipeToState::PendingRead => {
// Write the chunk.
self.write_chunk(cx, &global, result, can_gc);
// Wait for the writer to be ready again.
self.wait_for_writer_ready(&global, realm, can_gc);
},
PipeToState::ShuttingDownWithPendingWrites(action) => {
// Wait until every chunk that has been read has been written
// (i.e. the corresponding promises have settled).
if let Some(write) = self.pending_writes.borrow_mut().front().cloned() {
self.wait_on_pending_write(&global, write, realm, can_gc);
return;
}
// Note: error is stored in `self.shutdown_error`.
if let Some(action) = action {
// Let p be the result of performing action.
self.perform_action(cx, &global, action, realm, can_gc);
} else {
// Finalize, passing along error if it was given.
self.finalize(cx, &global, can_gc);
}
},
PipeToState::ShuttingDownPendingAction => {
let Some(ref promise) = *self.shutdown_action_promise.borrow() else {
unreachable!();
};
if promise.is_pending() {
// While waiting for the action to complete,
// we may get callbacks for other promises(closed, ready),
// and we should ignore those.
return;
}
// Finalize, passing along error if it was given.
if !result.is_undefined() {
// All actions either resolve with undefined,
// or reject with an error,
// and the error should be used when finalizing.
self.shutdown_error.set(result.get());
}
self.finalize(cx, &global, can_gc);
},
PipeToState::Finalized => {},
}
}
}
impl PipeTo {
/// Wait for the writer to be ready,
/// which implements the constraint that backpressure must be enforced.
fn wait_for_writer_ready(&self, global: &GlobalScope, realm: InRealm, can_gc: CanGc) {
{
let mut state = self.state.borrow_mut();
*state = PipeToState::PendingReady;
}
let ready_promise = self.writer.Ready();
if ready_promise.is_fulfilled() {
self.read_chunk(global, realm, can_gc);
} else {
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(self.clone())),
Some(Box::new(self.clone())),
can_gc,
);
ready_promise.append_native_handler(&handler, realm, can_gc);
// Note: if the writer is not ready,
// in order to ensure progress we must
// also react to the closure of the source(because source may close empty).
let closed_promise = self.reader.Closed();
closed_promise.append_native_handler(&handler, realm, can_gc);
}
}
/// Read a chunk
fn read_chunk(&self, global: &GlobalScope, realm: InRealm, can_gc: CanGc) {
*self.state.borrow_mut() = PipeToState::PendingRead;
let chunk_promise = self.reader.Read(can_gc);
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(self.clone())),
Some(Box::new(self.clone())),
can_gc,
);
chunk_promise.append_native_handler(&handler, realm, can_gc);
// Note: in order to ensure progress we must
// also react to the closure of the destination.
let ready_promise = self.writer.Closed();
ready_promise.append_native_handler(&handler, realm, can_gc);
}
/// Try to write a chunk using the jsval, and returns wether it succeeded
// It will fail if it is the last `done` chunk, or if it is not a chunk at all.
fn write_chunk(
&self,
cx: SafeJSContext,
global: &GlobalScope,
chunk: SafeHandleValue,
can_gc: CanGc,
) -> bool {
if chunk.is_object() {
rooted!(in(*cx) let object = chunk.to_object());
rooted!(in(*cx) let mut bytes = UndefinedValue());
let has_value =
get_dictionary_property(*cx, object.handle(), "value", bytes.handle_mut())
.expect("Chunk should have a value.");
if !bytes.is_undefined() && has_value {
// Write the chunk.
let write_promise = self.writer.write(cx, global, bytes.handle(), can_gc);
self.pending_writes.borrow_mut().push_back(write_promise);
return true;
}
}
false
}
/// Only as part of shutting-down do we wait on pending writes
/// (backpressure is communicated not through pending writes
/// but through the readiness of the writer).
fn wait_on_pending_write(
&self,
global: &GlobalScope,
promise: Rc<Promise>,
realm: InRealm,
can_gc: CanGc,
) {
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(self.clone())),
Some(Box::new(self.clone())),
can_gc,
);
promise.append_native_handler(&handler, realm, can_gc);
}
/// Errors must be propagated forward part of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
fn check_and_propagate_errors_forward(
&self,
cx: SafeJSContext,
global: &GlobalScope,
realm: InRealm,
can_gc: CanGc,
) {
// An early return is necessary if we are shutting down,
// because in that case the source can already have been set to none.
if self.shutting_down.get() {
return;
}
// if source.[[state]] is or becomes "errored", then
let source = self
.reader
.get_stream()
.expect("Reader should still have a stream");
if source.is_errored() {
rooted!(in(*cx) let mut source_error = UndefinedValue());
source.get_stored_error(source_error.handle_mut());
self.shutdown_error.set(source_error.get());
// If preventAbort is false,
if !self.prevent_abort {
// shutdown with an action of ! WritableStreamAbort(dest, source.[[storedError]])
// and with source.[[storedError]].
self.shutdown(
cx,
global,
Some(ShutdownAction::WritableStreamAbort),
realm,
can_gc,
)
} else {
// Otherwise, shutdown with source.[[storedError]].
self.shutdown(cx, global, None, realm, can_gc);
}
}
}
/// Errors must be propagated backward part of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
fn check_and_propagate_errors_backward(
&self,
cx: SafeJSContext,
global: &GlobalScope,
realm: InRealm,
can_gc: CanGc,
) {
// An early return is necessary if we are shutting down,
// because in that case the destination can already have been set to none.
if self.shutting_down.get() {
return;
}
// if dest.[[state]] is or becomes "errored", then
let dest = self
.writer
.get_stream()
.expect("Writer should still have a stream");
if dest.is_errored() {
rooted!(in(*cx) let mut dest_error = UndefinedValue());
dest.get_stored_error(dest_error.handle_mut());
self.shutdown_error.set(dest_error.get());
// If preventCancel is false,
if !self.prevent_cancel {
// shutdown with an action of ! ReadableStreamCancel(source, dest.[[storedError]])
// and with dest.[[storedError]].
self.shutdown(
cx,
global,
Some(ShutdownAction::ReadableStreamCancel),
realm,
can_gc,
)
} else {
// Otherwise, shutdown with dest.[[storedError]].
self.shutdown(cx, global, None, realm, can_gc);
}
}
}
/// Closing must be propagated forward part of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
fn check_and_propagate_closing_forward(
&self,
cx: SafeJSContext,
global: &GlobalScope,
realm: InRealm,
can_gc: CanGc,
) {
// An early return is necessary if we are shutting down,
// because in that case the source can already have been set to none.
if self.shutting_down.get() {
return;
}
// if source.[[state]] is or becomes "closed", then
let source = self
.reader
.get_stream()
.expect("Reader should still have a stream");
if source.is_closed() {
// If preventClose is false,
if !self.prevent_close {
// shutdown with an action of ! WritableStreamAbort(dest, source.[[storedError]])
// and with source.[[storedError]].
self.shutdown(
cx,
global,
Some(ShutdownAction::WritableStreamDefaultWriterCloseWithErrorPropagation),
realm,
can_gc,
)
} else {
// Otherwise, shutdown.
self.shutdown(cx, global, None, realm, can_gc);
}
}
}
/// Closing must be propagated backward part of
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
fn check_and_propagate_closing_backward(
&self,
cx: SafeJSContext,
global: &GlobalScope,
realm: InRealm,
can_gc: CanGc,
) {
// An early return is necessary if we are shutting down,
// because in that case the destination can already have been set to none.
if self.shutting_down.get() {
return;
}
// if ! WritableStreamCloseQueuedOrInFlight(dest) is true
// or dest.[[state]] is "closed"
let dest = self
.writer
.get_stream()
.expect("Writer should still have a stream");
if dest.close_queued_or_in_flight() || dest.is_closed() {
// Assert: no chunks have been read or written.
// Note: unclear how to perform this assertion.
// Let destClosed be a new TypeError.
rooted!(in(*cx) let mut dest_closed = UndefinedValue());
let error =
Error::Type("Destination is closed or has closed queued or in flight".to_string());
error.to_jsval(cx, global, dest_closed.handle_mut(), can_gc);
self.shutdown_error.set(dest_closed.get());
// If preventCancel is false,
if !self.prevent_cancel {
// shutdown with an action of ! ReadableStreamCancel(source, destClosed)
// and with destClosed.
self.shutdown(
cx,
global,
Some(ShutdownAction::ReadableStreamCancel),
realm,
can_gc,
)
} else {
// Otherwise, shutdown with destClosed.
self.shutdown(cx, global, None, realm, can_gc);
}
}
}
/// <https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action>
/// <https://streams.spec.whatwg.org/#rs-pipeTo-shutdown>
/// Combined into one method with an optional action.
fn shutdown(
&self,
cx: SafeJSContext,
global: &GlobalScope,
action: Option<ShutdownAction>,
realm: InRealm,
can_gc: CanGc,
) {
// If shuttingDown is true, abort these substeps.
// Set shuttingDown to true.
if !self.shutting_down.replace(true) {
let dest = self.writer.get_stream().expect("Stream must be set");
// If dest.[[state]] is "writable",
// and ! WritableStreamCloseQueuedOrInFlight(dest) is false,
if dest.is_writable() && !dest.close_queued_or_in_flight() {
// If any chunks have been read but not yet written, write them to dest.
// Done at the top of `Callback`.
// Wait until every chunk that has been read has been written
// (i.e. the corresponding promises have settled).
if let Some(write) = self.pending_writes.borrow_mut().front() {
*self.state.borrow_mut() = PipeToState::ShuttingDownWithPendingWrites(action);
self.wait_on_pending_write(global, write.clone(), realm, can_gc);
return;
}
}
// Note: error is stored in `self.shutdown_error`.
if let Some(action) = action {
// Let p be the result of performing action.
self.perform_action(cx, global, action, realm, can_gc);
} else {
// Finalize, passing along error if it was given.
self.finalize(cx, global, can_gc);
}
}
}
/// The perform action part of
/// <https://streams.spec.whatwg.org/#rs-pipeTo-shutdown-with-action>
fn perform_action(
&self,
cx: SafeJSContext,
global: &GlobalScope,
action: ShutdownAction,
realm: InRealm,
can_gc: CanGc,
) {
rooted!(in(*cx) let mut error = self.shutdown_error.get());
*self.state.borrow_mut() = PipeToState::ShuttingDownPendingAction;
// Let p be the result of performing action.
let promise = match action {
ShutdownAction::WritableStreamAbort => {
let dest = self.writer.get_stream().expect("Stream must be set");
dest.abort(cx, global, error.handle(), can_gc)
},
ShutdownAction::ReadableStreamCancel => {
let source = self
.reader
.get_stream()
.expect("Reader should have a stream.");
source.cancel(error.handle(), can_gc)
},
ShutdownAction::WritableStreamDefaultWriterCloseWithErrorPropagation => {
self.writer.close_with_error_propagation(cx, global, can_gc)
},
};
// Upon fulfillment of p, finalize, passing along originalError if it was given.
// Upon rejection of p with reason newError, finalize with newError.
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(self.clone())),
Some(Box::new(self.clone())),
can_gc,
);
promise.append_native_handler(&handler, realm, can_gc);
*self.shutdown_action_promise.borrow_mut() = Some(promise);
}
/// <https://streams.spec.whatwg.org/#rs-pipeTo-finalize>
fn finalize(&self, cx: SafeJSContext, global: &GlobalScope, can_gc: CanGc) {
*self.state.borrow_mut() = PipeToState::Finalized;
// Perform ! WritableStreamDefaultWriterRelease(writer).
self.writer.release(cx, global, can_gc);
// If reader implements ReadableStreamBYOBReader,
// perform ! ReadableStreamBYOBReaderRelease(reader).
// TODO.
// Otherwise, perform ! ReadableStreamDefaultReaderRelease(reader).
self.reader
.release(can_gc)
.expect("Releasing the reader should not fail");
// If signal is not undefined, remove abortAlgorithm from signal.
// TODO: implement AbortSignal.
rooted!(in(*cx) let mut error = self.shutdown_error.get());
if !error.is_null() {
// If error was given, reject promise with error.
self.result_promise.reject_native(&error.handle(), can_gc);
} else {
// Otherwise, resolve promise with undefined.
self.result_promise.resolve_native(&(), can_gc);
}
}
}
/// The fulfillment handler for the reacting to sourceCancelPromise part of
/// <https://streams.spec.whatwg.org/#readable-stream-cancel>.
#[derive(Clone, JSTraceable, MallocSizeOf)]
@ -977,6 +1595,98 @@ impl ReadableStream {
Ok(vec![branch_1, branch_2])
}
/// <https://streams.spec.whatwg.org/#readable-stream-pipe-to>
#[allow(clippy::too_many_arguments)]
fn pipe_to(
&self,
cx: SafeJSContext,
global: &GlobalScope,
dest: &WritableStream,
prevent_abort: bool,
prevent_cancel: bool,
prevent_close: bool,
realm: InRealm,
can_gc: CanGc,
) -> Rc<Promise> {
// Assert: source implements ReadableStream.
// Assert: dest implements WritableStream.
// Assert: prevent_close, prevent_abort, and prevent_cancel are all booleans.
// Done with method signature types.
// If signal was not given, let signal be undefined.
// Assert: either signal is undefined, or signal implements AbortSignal.
// TODO: implement AbortSignal.
// Assert: ! IsReadableStreamLocked(source) is false.
assert!(!self.is_locked());
// Assert: ! IsWritableStreamLocked(dest) is false.
assert!(!dest.is_locked());
// If source.[[controller]] implements ReadableByteStreamController,
// let reader be either ! AcquireReadableStreamBYOBReader(source)
// or ! AcquireReadableStreamDefaultReader(source),
// at the user agents discretion.
// Note: for now only using default readers.
// Otherwise, let reader be ! AcquireReadableStreamDefaultReader(source).
let reader = self
.acquire_default_reader(can_gc)
.expect("Acquiring a default reader for pipe_to cannot fail");
// Let writer be ! AcquireWritableStreamDefaultWriter(dest).
let writer = dest
.aquire_default_writer(cx, global, can_gc)
.expect("Acquiring a default writer for pipe_to cannot fail");
// Set source.[[disturbed]] to true.
self.disturbed.set(true);
// Let shuttingDown be false.
// Done below with default.
// Let promise be a new promise.
let promise = Promise::new(global, can_gc);
// If signal is not undefined,
// TODO: implement AbortSignal.
// In parallel, but not really, using reader and writer, read all chunks from source and write them to dest.
rooted!(in(*cx) let pipe_to = PipeTo {
reader: Dom::from_ref(&reader),
writer: Dom::from_ref(&writer),
pending_writes: Default::default(),
state: Default::default(),
prevent_abort,
prevent_cancel,
prevent_close,
shutting_down: Default::default(),
shutdown_error: Default::default(),
shutdown_action_promise: Default::default(),
result_promise: promise.clone(),
});
// Note: set the shutdown error to null,
// to distinguish it from cases
// where the error is set to undefined.
pipe_to.shutdown_error.set(NullValue());
// Note: perfom checks now, since streams can start as closed or errored.
pipe_to.check_and_propagate_errors_forward(cx, global, realm, can_gc);
pipe_to.check_and_propagate_errors_backward(cx, global, realm, can_gc);
pipe_to.check_and_propagate_closing_forward(cx, global, realm, can_gc);
pipe_to.check_and_propagate_closing_backward(cx, global, realm, can_gc);
// If we are not closed or errored,
if *pipe_to.state.borrow() == PipeToState::Starting {
// Start the pipe, by waiting on the writer being ready for a chunk.
pipe_to.wait_for_writer_ready(global, realm, can_gc);
}
// Return promise.
promise
}
/// <https://streams.spec.whatwg.org/#readable-stream-tee>
fn tee(
&self,
@ -1172,6 +1882,52 @@ impl ReadableStreamMethods<crate::DomTypeHolder> for ReadableStream {
// Return ? ReadableStreamTee(this, false).
self.tee(false, can_gc)
}
/// <https://streams.spec.whatwg.org/#rs-pipe-to>
fn PipeTo(
&self,
destination: &WritableStream,
options: &StreamPipeOptions,
realm: InRealm,
can_gc: CanGc,
) -> Rc<Promise> {
let cx = GlobalScope::get_cx();
let global = self.global();
// If ! IsReadableStreamLocked(this) is true,
if self.is_locked() {
// return a promise rejected with a TypeError exception.
let promise = Promise::new(&global, can_gc);
promise.reject_error(Error::Type("Source stream is locked".to_owned()), can_gc);
return promise;
}
// If ! IsWritableStreamLocked(destination) is true,
if destination.is_locked() {
// return a promise rejected with a TypeError exception.
let promise = Promise::new(&global, can_gc);
promise.reject_error(
Error::Type("Destination stream is locked".to_owned()),
can_gc,
);
return promise;
}
// Let signal be options["signal"] if it exists, or undefined otherwise.
// TODO: implement AbortSignal.
// Return ! ReadableStreamPipeTo.
self.pipe_to(
cx,
&global,
destination,
options.preventAbort,
options.preventCancel,
options.preventClose,
realm,
can_gc,
)
}
}
#[allow(unsafe_code)]

View file

@ -263,7 +263,7 @@ impl WritableStreamDefaultWriter {
}
/// <https://streams.spec.whatwg.org/#writable-stream-default-writer-write>
fn write(
pub(crate) fn write(
&self,
cx: SafeJSContext,
global: &GlobalScope,
@ -377,6 +377,52 @@ impl WritableStreamDefaultWriter {
// Set this.[[stream]] to undefined.
self.stream.set(None);
}
/// <https://streams.spec.whatwg.org/#writable-stream-default-writer-close-with-error-propagation>
pub(crate) fn close_with_error_propagation(
&self,
cx: SafeJSContext,
global: &GlobalScope,
can_gc: CanGc,
) -> Rc<Promise> {
// Let stream be writer.[[stream]].
let Some(stream) = self.stream.get() else {
// Assert: stream is not undefined.
unreachable!("Stream should be set.");
};
// Let state be stream.[[state]].
// Used via stream method calls.
// If ! WritableStreamCloseQueuedOrInFlight(stream) is true
// or state is "closed",
if stream.close_queued_or_in_flight() || stream.is_closed() {
// return a promise resolved with undefined.
let promise = Promise::new(global, can_gc);
promise.resolve_native(&(), can_gc);
return promise;
}
// If state is "errored",
if stream.is_errored() {
// return a promise rejected with stream.[[storedError]].
rooted!(in(*cx) let mut error = UndefinedValue());
stream.get_stored_error(error.handle_mut());
let promise = Promise::new(global, can_gc);
promise.reject_native(&error.handle(), can_gc);
return promise;
}
// Assert: state is "writable" or "erroring".
assert!(stream.is_writable() || stream.is_erroring());
// Return ! WritableStreamDefaultWriterClose(writer).
self.close(cx, global, can_gc)
}
pub(crate) fn get_stream(&self) -> Option<DomRoot<WritableStream>> {
self.stream.get()
}
}
impl WritableStreamDefaultWriterMethods<crate::DomTypeHolder> for WritableStreamDefaultWriter {

View file

@ -632,7 +632,8 @@ DOMInterfaces = {
},
'ReadableStream': {
'canGc': ['GetReader', 'Cancel', 'Tee'],
'canGc': ['GetReader', 'Cancel', 'PipeTo', 'Tee'],
'inRealms': ['PipeTo'],
},
"ReadableStreamDefaultController": {

View file

@ -23,8 +23,8 @@ interface _ReadableStream {
// [Throws]
// ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {});
// [NewObject]
// Promise<undefined> pipeTo(WritableStream destination, optional StreamPipeOptions options = {});
[NewObject]
Promise<undefined> pipeTo(WritableStream destination, optional StreamPipeOptions options = {});
[Throws]
sequence<ReadableStream> tee();

View file

@ -241,6 +241,8 @@ skip: true
skip: false
[streams]
skip: true
[piping]
skip: false
[readable-streams]
skip: false
[readable-byte-streams]

View file

@ -1,3 +0,0 @@
[response-body-read-task-handling.html]
[piping from a body stream to a JS-written WritableStream should occur in a microtask scope]
expected: FAIL

View file

@ -2,17 +2,11 @@
[using pipeThrough on Response body should disturb it synchronously]
expected: FAIL
[using pipeTo on Response body should disturb it synchronously]
expected: FAIL
[response-stream-disturbed-by-pipe.any.html]
[using pipeThrough on Response body should disturb it synchronously]
expected: FAIL
[using pipeTo on Response body should disturb it synchronously]
expected: FAIL
[response-stream-disturbed-by-pipe.any.serviceworker.html]
expected: ERROR

View file

@ -1,5 +1,4 @@
[response-stream-with-broken-then.any.worker.html]
expected: TIMEOUT
[Attempt to inject {done: false, value: bye} via Object.prototype.then.]
expected: FAIL
@ -23,7 +22,6 @@
expected: ERROR
[response-stream-with-broken-then.any.html]
expected: CRASH
[Attempt to inject {done: false, value: bye} via Object.prototype.then.]
expected: FAIL

View file

@ -0,0 +1,29 @@
[abort.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[abort.any.shadowrealm-in-window.html]
expected: ERROR
[abort.any.html]
expected: ERROR
[abort.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[abort.any.sharedworker.html]
expected: ERROR
[abort.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[abort.any.serviceworker.html]
expected: ERROR
[abort.any.worker.html]
expected: ERROR
[abort.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[abort.any.shadowrealm-in-shadowrealm.html]
expected: ERROR

View file

@ -0,0 +1,27 @@
[close-propagation-backward.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[close-propagation-backward.any.serviceworker.html]
expected: ERROR
[close-propagation-backward.any.html]
[close-propagation-backward.any.sharedworker.html]
expected: ERROR
[close-propagation-backward.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[close-propagation-backward.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[close-propagation-backward.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[close-propagation-backward.any.shadowrealm-in-window.html]
expected: ERROR
[close-propagation-backward.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[close-propagation-backward.any.worker.html]

View file

@ -0,0 +1,27 @@
[close-propagation-forward.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[close-propagation-forward.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[close-propagation-forward.any.shadowrealm-in-window.html]
expected: ERROR
[close-propagation-forward.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[close-propagation-forward.any.worker.html]
[close-propagation-forward.any.serviceworker.html]
expected: ERROR
[close-propagation-forward.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[close-propagation-forward.any.sharedworker.html]
expected: ERROR
[close-propagation-forward.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[close-propagation-forward.any.html]

View file

@ -0,0 +1,27 @@
[error-propagation-backward.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[error-propagation-backward.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[error-propagation-backward.any.shadowrealm-in-window.html]
expected: ERROR
[error-propagation-backward.any.worker.html]
[error-propagation-backward.any.html]
[error-propagation-backward.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[error-propagation-backward.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[error-propagation-backward.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[error-propagation-backward.any.sharedworker.html]
expected: ERROR
[error-propagation-backward.any.serviceworker.html]
expected: ERROR

View file

@ -0,0 +1,27 @@
[error-propagation-forward.any.html]
[error-propagation-forward.any.serviceworker.html]
expected: ERROR
[error-propagation-forward.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[error-propagation-forward.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[error-propagation-forward.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[error-propagation-forward.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[error-propagation-forward.any.sharedworker.html]
expected: ERROR
[error-propagation-forward.any.worker.html]
[error-propagation-forward.any.shadowrealm-in-window.html]
expected: ERROR
[error-propagation-forward.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR

View file

@ -0,0 +1,27 @@
[flow-control.any.html]
[flow-control.any.serviceworker.html]
expected: ERROR
[flow-control.any.worker.html]
[flow-control.any.sharedworker.html]
expected: ERROR
[flow-control.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[flow-control.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[flow-control.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[flow-control.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[flow-control.any.shadowrealm-in-window.html]
expected: ERROR
[flow-control.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR

View file

@ -0,0 +1,27 @@
[general-addition.any.sharedworker.html]
expected: ERROR
[general-addition.any.html]
[general-addition.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[general-addition.any.shadowrealm-in-window.html]
expected: ERROR
[general-addition.any.serviceworker.html]
expected: ERROR
[general-addition.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[general-addition.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[general-addition.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[general-addition.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[general-addition.any.worker.html]

View file

@ -0,0 +1,27 @@
[general.any.sharedworker.html]
expected: ERROR
[general.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[general.any.worker.html]
[general.any.serviceworker.html]
expected: ERROR
[general.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[general.any.shadowrealm-in-window.html]
expected: ERROR
[general.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[general.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[general.any.html]
[general.any.shadowrealm-in-shadowrealm.html]
expected: ERROR

View file

@ -0,0 +1,27 @@
[multiple-propagation.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[multiple-propagation.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[multiple-propagation.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[multiple-propagation.any.serviceworker.html]
expected: ERROR
[multiple-propagation.any.shadowrealm-in-window.html]
expected: ERROR
[multiple-propagation.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[multiple-propagation.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[multiple-propagation.any.worker.html]
[multiple-propagation.any.html]
[multiple-propagation.any.sharedworker.html]
expected: ERROR

View file

@ -0,0 +1,142 @@
[pipe-through.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[pipe-through.any.serviceworker.html]
expected: ERROR
[pipe-through.any.worker.html]
expected: ERROR
[Piping through a duck-typed pass-through transform stream should work]
expected: FAIL
[Piping through a transform errored on the writable end does not cause an unhandled promise rejection]
expected: FAIL
[pipeThrough should not call pipeTo on this]
expected: FAIL
[pipeThrough should not call pipeTo on the ReadableStream prototype]
expected: FAIL
[pipeThrough should brand-check this and not allow 'null']
expected: FAIL
[pipeThrough should brand-check this and not allow 'undefined']
expected: FAIL
[pipeThrough should brand-check this and not allow '0']
expected: FAIL
[pipeThrough should brand-check this and not allow 'NaN']
expected: FAIL
[pipeThrough should brand-check this and not allow 'true']
expected: FAIL
[pipeThrough should brand-check this and not allow 'ReadableStream']
expected: FAIL
[pipeThrough should brand-check this and not allow '[object ReadableStream\]']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'null']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'undefined']
expected: FAIL
[pipeThrough should brand-check writable and not allow '0']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'NaN']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'true']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'WritableStream']
expected: FAIL
[pipeThrough should brand-check writable and not allow '[object WritableStream\]']
expected: FAIL
[pipeThrough should rethrow errors from accessing readable or writable]
expected: FAIL
[pipe-through.any.sharedworker.html]
expected: ERROR
[pipe-through.any.shadowrealm-in-window.html]
expected: ERROR
[pipe-through.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[pipe-through.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[pipe-through.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[pipe-through.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[pipe-through.any.html]
expected: ERROR
[Piping through a duck-typed pass-through transform stream should work]
expected: FAIL
[Piping through a transform errored on the writable end does not cause an unhandled promise rejection]
expected: FAIL
[pipeThrough should not call pipeTo on this]
expected: FAIL
[pipeThrough should not call pipeTo on the ReadableStream prototype]
expected: FAIL
[pipeThrough should brand-check this and not allow 'null']
expected: FAIL
[pipeThrough should brand-check this and not allow 'undefined']
expected: FAIL
[pipeThrough should brand-check this and not allow '0']
expected: FAIL
[pipeThrough should brand-check this and not allow 'NaN']
expected: FAIL
[pipeThrough should brand-check this and not allow 'true']
expected: FAIL
[pipeThrough should brand-check this and not allow 'ReadableStream']
expected: FAIL
[pipeThrough should brand-check this and not allow '[object ReadableStream\]']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'null']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'undefined']
expected: FAIL
[pipeThrough should brand-check writable and not allow '0']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'NaN']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'true']
expected: FAIL
[pipeThrough should brand-check writable and not allow 'WritableStream']
expected: FAIL
[pipeThrough should brand-check writable and not allow '[object WritableStream\]']
expected: FAIL
[pipeThrough should rethrow errors from accessing readable or writable]
expected: FAIL

View file

@ -0,0 +1,29 @@
[then-interception.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[then-interception.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[then-interception.any.serviceworker.html]
expected: ERROR
[then-interception.any.worker.html]
expected: CRASH
[then-interception.any.html]
expected: CRASH
[then-interception.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[then-interception.any.shadowrealm-in-window.html]
expected: ERROR
[then-interception.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[then-interception.any.sharedworker.html]
expected: ERROR
[then-interception.any.shadowrealm-in-sharedworker.html]
expected: ERROR

View file

@ -0,0 +1,59 @@
[throwing-options.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[throwing-options.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR
[throwing-options.any.serviceworker.html]
expected: ERROR
[throwing-options.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[throwing-options.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[throwing-options.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[throwing-options.any.shadowrealm-in-window.html]
expected: ERROR
[throwing-options.any.html]
expected: TIMEOUT
[pipeThrough should stop after getting preventAbort throws]
expected: FAIL
[pipeThrough should stop after getting preventCancel throws]
expected: FAIL
[pipeThrough should stop after getting preventClose throws]
expected: FAIL
[pipeTo should stop after getting signal throws]
expected: TIMEOUT
[pipeThrough should stop after getting signal throws]
expected: FAIL
[throwing-options.any.worker.html]
expected: TIMEOUT
[pipeThrough should stop after getting preventAbort throws]
expected: FAIL
[pipeThrough should stop after getting preventCancel throws]
expected: FAIL
[pipeThrough should stop after getting preventClose throws]
expected: FAIL
[pipeTo should stop after getting signal throws]
expected: TIMEOUT
[pipeThrough should stop after getting signal throws]
expected: FAIL
[throwing-options.any.sharedworker.html]
expected: ERROR

View file

@ -0,0 +1,33 @@
[transform-streams.any.shadowrealm-in-dedicatedworker.html]
expected: ERROR
[transform-streams.https.any.shadowrealm-in-serviceworker.html]
expected: ERROR
[transform-streams.any.sharedworker.html]
expected: ERROR
[transform-streams.any.shadowrealm-in-sharedworker.html]
expected: ERROR
[transform-streams.any.worker.html]
[Piping through an identity transform stream should close the destination when the source closes]
expected: FAIL
[transform-streams.any.shadowrealm-in-shadowrealm.html]
expected: ERROR
[transform-streams.any.html]
[Piping through an identity transform stream should close the destination when the source closes]
expected: FAIL
[transform-streams.any.shadowrealm-in-window.html]
expected: ERROR
[transform-streams.any.serviceworker.html]
expected: ERROR
[transform-streams.https.any.shadowrealm-in-audioworklet.html]
expected: ERROR

View file

@ -5,17 +5,11 @@
expected: ERROR
[patched-global.any.html]
[pipeTo() should not call Promise.prototype.then()]
expected: FAIL
[ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader methods]
expected: FAIL
[patched-global.any.worker.html]
[pipeTo() should not call Promise.prototype.then()]
expected: FAIL
[ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader methods]
expected: FAIL

View file

@ -5,14 +5,8 @@
expected: ERROR
[reentrant-strategies.any.html]
[pipeTo() inside size() should behave as expected]
expected: FAIL
[reentrant-strategies.any.worker.html]
[pipeTo() inside size() should behave as expected]
expected: FAIL
[reentrant-strategies.any.shadowrealm.html]
expected: TIMEOUT