servo/components/script/dom/transformstream.rs
Taym Haddadi 5b2305784a
Stream implement pipeThrough (#36977)
Part of https://github.com/servo/servo/issues/34676

https://github.com/servo/servo/pull/36905 needs to be merged first.

---------

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
2025-05-20 14:33:22 +00:00

1117 lines
45 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::Cell;
use std::collections::HashMap;
use std::ptr::{self};
use std::rc::Rc;
use base::id::{MessagePortId, MessagePortIndex};
use constellation_traits::TransformStreamData;
use dom_struct::dom_struct;
use js::jsapi::{Heap, IsPromiseObject, JSObject};
use js::jsval::{JSVal, ObjectValue, UndefinedValue};
use js::rust::{HandleObject as SafeHandleObject, HandleValue as SafeHandleValue, IntoHandle};
use script_bindings::callback::ExceptionHandling;
use script_bindings::realms::InRealm;
use super::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategySize;
use super::bindings::structuredclone::StructuredData;
use super::bindings::transferable::Transferable;
use super::messageport::MessagePort;
use super::promisenativehandler::Callback;
use super::types::{TransformStreamDefaultController, WritableStream};
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::QueuingStrategyBinding::QueuingStrategy;
use crate::dom::bindings::codegen::Bindings::TransformStreamBinding::TransformStreamMethods;
use crate::dom::bindings::codegen::Bindings::TransformerBinding::Transformer;
use crate::dom::bindings::conversions::ConversionResult;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::countqueuingstrategy::{extract_high_water_mark, extract_size_algorithm};
use crate::dom::globalscope::GlobalScope;
use crate::dom::promise::Promise;
use crate::dom::readablestream::{ReadableStream, create_readable_stream};
use crate::dom::types::PromiseNativeHandler;
use crate::dom::underlyingsourcecontainer::UnderlyingSourceType;
use crate::dom::writablestream::create_writable_stream;
use crate::dom::writablestreamdefaultcontroller::UnderlyingSinkType;
use crate::realms::enter_realm;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
impl js::gc::Rootable for TransformBackPressureChangePromiseFulfillment {}
/// Reacting to backpressureChangePromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct TransformBackPressureChangePromiseFulfillment {
/// The result of reacting to backpressureChangePromise.
#[ignore_malloc_size_of = "Rc is hard"]
result_promise: Rc<Promise>,
#[ignore_malloc_size_of = "mozjs"]
chunk: Box<Heap<JSVal>>,
/// The writable used in the fulfillment steps
writable: Dom<WritableStream>,
controller: Dom<TransformStreamDefaultController>,
}
impl Callback for TransformBackPressureChangePromiseFulfillment {
/// Reacting to backpressureChangePromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Let writable be stream.[[writable]].
// Let state be writable.[[state]].
// If state is "erroring", throw writable.[[storedError]].
if self.writable.is_erroring() {
rooted!(in(*cx) let mut error = UndefinedValue());
self.writable.get_stored_error(error.handle_mut());
self.result_promise.reject(cx, error.handle(), can_gc);
return;
}
// Assert: state is "writable".
assert!(self.writable.is_writable());
// Return ! TransformStreamDefaultControllerPerformTransform(controller, chunk).
rooted!(in(*cx) let mut chunk = UndefinedValue());
chunk.set(self.chunk.get());
let transform_result = self
.controller
.transform_stream_default_controller_perform_transform(
cx,
&self.writable.global(),
chunk.handle(),
can_gc,
)
.expect("perform transform failed");
// PerformTransformFulfillment and PerformTransformRejection do not need
// to be rooted because they only contain an Rc.
let handler = PromiseNativeHandler::new(
&self.writable.global(),
Some(Box::new(PerformTransformFulfillment {
result_promise: self.result_promise.clone(),
})),
Some(Box::new(PerformTransformRejection {
result_promise: self.result_promise.clone(),
})),
can_gc,
);
let realm = enter_realm(&*self.writable.global());
let comp = InRealm::Entered(&realm);
transform_result.append_native_handler(&handler, comp, can_gc);
}
}
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Reacting to fulfillment of performTransform as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm>
struct PerformTransformFulfillment {
#[ignore_malloc_size_of = "Rc is hard"]
result_promise: Rc<Promise>,
}
impl Callback for PerformTransformFulfillment {
fn callback(&self, _cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Fulfilled: resolve the outer promise
self.result_promise.resolve_native(&(), can_gc);
}
}
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Reacting to rejection of performTransform as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm>
struct PerformTransformRejection {
#[ignore_malloc_size_of = "Rc is hard"]
result_promise: Rc<Promise>,
}
impl Callback for PerformTransformRejection {
fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Stream already errored in perform_transform, just reject result_promise
self.result_promise.reject(cx, v, can_gc);
}
}
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
/// Reacting to rejection of backpressureChangePromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm>
struct BackpressureChangeRejection {
#[ignore_malloc_size_of = "Rc is hard"]
result_promise: Rc<Promise>,
}
impl Callback for BackpressureChangeRejection {
fn callback(&self, cx: SafeJSContext, reason: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
self.result_promise.reject(cx, reason, can_gc);
}
}
impl js::gc::Rootable for CancelPromiseFulfillment {}
/// Reacting to fulfillment of the cancelpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-abort-algorithm>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct CancelPromiseFulfillment {
readable: Dom<ReadableStream>,
controller: Dom<TransformStreamDefaultController>,
#[ignore_malloc_size_of = "mozjs"]
reason: Box<Heap<JSVal>>,
}
impl Callback for CancelPromiseFulfillment {
/// Reacting to backpressureChangePromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// If readable.[[state]] is "errored", reject controller.[[finishPromise]] with readable.[[storedError]].
if self.readable.is_errored() {
rooted!(in(*cx) let mut error = UndefinedValue());
self.readable.get_stored_error(error.handle_mut());
self.controller
.get_finish_promise()
.expect("finish promise is not set")
.reject_native(&error.handle(), can_gc);
} else {
// Otherwise:
// Perform ! ReadableStreamDefaultControllerError(readable.[[controller]], reason).
rooted!(in(*cx) let mut reason = UndefinedValue());
reason.set(self.reason.get());
self.readable
.get_default_controller()
.error(reason.handle(), can_gc);
// Resolve controller.[[finishPromise]] with undefined.
self.controller
.get_finish_promise()
.expect("finish promise is not set")
.resolve_native(&(), can_gc);
}
}
}
impl js::gc::Rootable for CancelPromiseRejection {}
/// Reacting to rejection of cancelpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-abort-algorithm>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct CancelPromiseRejection {
readable: Dom<ReadableStream>,
controller: Dom<TransformStreamDefaultController>,
}
impl Callback for CancelPromiseRejection {
/// Reacting to backpressureChangePromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Perform ! ReadableStreamDefaultControllerError(readable.[[controller]], r).
self.readable.get_default_controller().error(v, can_gc);
// Reject controller.[[finishPromise]] with r.
self.controller
.get_finish_promise()
.expect("finish promise is not set")
.reject(cx, v, can_gc);
}
}
impl js::gc::Rootable for SourceCancelPromiseFulfillment {}
/// Reacting to fulfillment of the cancelpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-source-cancel>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct SourceCancelPromiseFulfillment {
writeable: Dom<WritableStream>,
controller: Dom<TransformStreamDefaultController>,
stream: Dom<TransformStream>,
#[ignore_malloc_size_of = "mozjs"]
reason: Box<Heap<JSVal>>,
}
impl Callback for SourceCancelPromiseFulfillment {
/// Reacting to backpressureChangePromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// If cancelPromise was fulfilled, then:
let finish_promise = self
.controller
.get_finish_promise()
.expect("finish promise is not set");
let global = &self.writeable.global();
// If writable.[[state]] is "errored", reject controller.[[finishPromise]] with writable.[[storedError]].
if self.writeable.is_errored() {
rooted!(in(*cx) let mut error = UndefinedValue());
self.writeable.get_stored_error(error.handle_mut());
finish_promise.reject(cx, error.handle(), can_gc);
} else {
// Otherwise:
// Perform ! WritableStreamDefaultControllerErrorIfNeeded(writable.[[controller]], reason).
rooted!(in(*cx) let mut reason = UndefinedValue());
reason.set(self.reason.get());
self.writeable.get_default_controller().error_if_needed(
cx,
reason.handle(),
global,
can_gc,
);
// Perform ! TransformStreamUnblockWrite(stream).
self.stream.unblock_write(global, can_gc);
// Resolve controller.[[finishPromise]] with undefined.
finish_promise.resolve_native(&(), can_gc);
}
}
}
impl js::gc::Rootable for SourceCancelPromiseRejection {}
/// Reacting to rejection of cancelpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-source-cancel>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct SourceCancelPromiseRejection {
writeable: Dom<WritableStream>,
controller: Dom<TransformStreamDefaultController>,
stream: Dom<TransformStream>,
}
impl Callback for SourceCancelPromiseRejection {
/// Reacting to backpressureChangePromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// Perform ! WritableStreamDefaultControllerErrorIfNeeded(writable.[[controller]], r).
let global = &self.writeable.global();
self.writeable
.get_default_controller()
.error_if_needed(cx, v, global, can_gc);
// Perform ! TransformStreamUnblockWrite(stream).
self.stream.unblock_write(global, can_gc);
// Reject controller.[[finishPromise]] with r.
self.controller
.get_finish_promise()
.expect("finish promise is not set")
.reject(cx, v, can_gc);
}
}
impl js::gc::Rootable for FlushPromiseFulfillment {}
/// Reacting to fulfillment of the flushpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-close-algorithm>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct FlushPromiseFulfillment {
readable: Dom<ReadableStream>,
controller: Dom<TransformStreamDefaultController>,
}
impl Callback for FlushPromiseFulfillment {
/// Reacting to flushpromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, _v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// If flushPromise was fulfilled, then:
let finish_promise = self
.controller
.get_finish_promise()
.expect("finish promise is not set");
// If readable.[[state]] is "errored", reject controller.[[finishPromise]] with readable.[[storedError]].
if self.readable.is_errored() {
rooted!(in(*cx) let mut error = UndefinedValue());
self.readable.get_stored_error(error.handle_mut());
finish_promise.reject(cx, error.handle(), can_gc);
} else {
// Otherwise:
// Perform ! ReadableStreamDefaultControllerClose(readable.[[controller]]).
self.readable.get_default_controller().close(can_gc);
// Resolve controller.[[finishPromise]] with undefined.
finish_promise.resolve_native(&(), can_gc);
}
}
}
impl js::gc::Rootable for FlushPromiseRejection {}
/// Reacting to rejection of flushpromise as part of
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-close-algorithm>
#[derive(JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
struct FlushPromiseRejection {
readable: Dom<ReadableStream>,
controller: Dom<TransformStreamDefaultController>,
}
impl Callback for FlushPromiseRejection {
/// Reacting to flushpromise with the following fulfillment steps:
fn callback(&self, cx: SafeJSContext, v: SafeHandleValue, _realm: InRealm, can_gc: CanGc) {
// If flushPromise was rejected with reason r, then:
// Perform ! ReadableStreamDefaultControllerError(readable.[[controller]], r).
self.readable.get_default_controller().error(v, can_gc);
// Reject controller.[[finishPromise]] with r.
self.controller
.get_finish_promise()
.expect("finish promise is not set")
.reject(cx, v, can_gc);
}
}
/// <https://streams.spec.whatwg.org/#ts-class>
#[dom_struct]
pub struct TransformStream {
reflector_: Reflector,
/// <https://streams.spec.whatwg.org/#transformstream-backpressure>
backpressure: Cell<bool>,
/// <https://streams.spec.whatwg.org/#transformstream-backpressurechangepromise>
#[ignore_malloc_size_of = "Rc is hard"]
backpressure_change_promise: DomRefCell<Option<Rc<Promise>>>,
/// <https://streams.spec.whatwg.org/#transformstream-controller>
controller: MutNullableDom<TransformStreamDefaultController>,
/// <https://streams.spec.whatwg.org/#transformstream-detached>
detached: Cell<bool>,
/// <https://streams.spec.whatwg.org/#transformstream-readable>
readable: MutNullableDom<ReadableStream>,
/// <https://streams.spec.whatwg.org/#transformstream-writable>
writable: MutNullableDom<WritableStream>,
}
impl TransformStream {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
/// <https://streams.spec.whatwg.org/#initialize-transform-stream>
fn new_inherited() -> TransformStream {
TransformStream {
reflector_: Reflector::new(),
backpressure: Default::default(),
backpressure_change_promise: DomRefCell::new(None),
controller: MutNullableDom::new(None),
detached: Cell::new(false),
readable: MutNullableDom::new(None),
writable: MutNullableDom::new(None),
}
}
pub(crate) fn new_with_proto(
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
) -> DomRoot<TransformStream> {
reflect_dom_object_with_proto(
Box::new(TransformStream::new_inherited()),
global,
proto,
can_gc,
)
}
pub(crate) fn get_controller(&self) -> DomRoot<TransformStreamDefaultController> {
self.controller.get().expect("controller is not set")
}
pub(crate) fn get_writable(&self) -> DomRoot<WritableStream> {
self.writable.get().expect("writable stream is not set")
}
pub(crate) fn get_readable(&self) -> DomRoot<ReadableStream> {
self.readable.get().expect("readable stream is not set")
}
pub(crate) fn get_backpressure(&self) -> bool {
self.backpressure.get()
}
/// <https://streams.spec.whatwg.org/#initialize-transform-stream>
#[allow(clippy::too_many_arguments)]
fn initialize(
&self,
cx: SafeJSContext,
global: &GlobalScope,
start_promise: Rc<Promise>,
writable_high_water_mark: f64,
writable_size_algorithm: Rc<QueuingStrategySize>,
readable_high_water_mark: f64,
readable_size_algorithm: Rc<QueuingStrategySize>,
can_gc: CanGc,
) -> Fallible<()> {
// Let startAlgorithm be an algorithm that returns startPromise.
// Let writeAlgorithm be the following steps, taking a chunk argument:
// Return ! TransformStreamDefaultSinkWriteAlgorithm(stream, chunk).
// Let abortAlgorithm be the following steps, taking a reason argument:
// Return ! TransformStreamDefaultSinkAbortAlgorithm(stream, reason).
// Let closeAlgorithm be the following steps:
// Return ! TransformStreamDefaultSinkCloseAlgorithm(stream).
// Set stream.[[writable]] to ! CreateWritableStream(startAlgorithm, writeAlgorithm,
// closeAlgorithm, abortAlgorithm, writableHighWaterMark, writableSizeAlgorithm).
// Note: Those steps are implemented using UnderlyingSinkType::Transform.
let writable = create_writable_stream(
cx,
global,
writable_high_water_mark,
writable_size_algorithm,
UnderlyingSinkType::Transform(Dom::from_ref(self), start_promise.clone()),
can_gc,
)?;
self.writable.set(Some(&writable));
// Let pullAlgorithm be the following steps:
// Return ! TransformStreamDefaultSourcePullAlgorithm(stream).
// Let cancelAlgorithm be the following steps, taking a reason argument:
// Return ! TransformStreamDefaultSourceCancelAlgorithm(stream, reason).
// Set stream.[[readable]] to ! CreateReadableStream(startAlgorithm, pullAlgorithm,
// cancelAlgorithm, readableHighWaterMark, readableSizeAlgorithm).
let readable = create_readable_stream(
global,
UnderlyingSourceType::Transform(Dom::from_ref(self), start_promise.clone()),
Some(readable_size_algorithm),
Some(readable_high_water_mark),
can_gc,
);
self.readable.set(Some(&readable));
// Set stream.[[backpressure]] and stream.[[backpressureChangePromise]] to undefined.
// Note: This is done in the constructor.
// Perform ! TransformStreamSetBackpressure(stream, true).
self.set_backpressure(global, true, can_gc);
// Set stream.[[controller]] to undefined.
self.controller.set(None);
Ok(())
}
/// <https://streams.spec.whatwg.org/#transform-stream-set-backpressure>
pub(crate) fn set_backpressure(&self, global: &GlobalScope, backpressure: bool, can_gc: CanGc) {
// Assert: stream.[[backpressure]] is not backpressure.
assert!(self.backpressure.get() != backpressure);
// If stream.[[backpressureChangePromise]] is not undefined, resolve
// stream.[[backpressureChangePromise]] with undefined.
if let Some(promise) = self.backpressure_change_promise.borrow_mut().take() {
promise.resolve_native(&(), can_gc);
}
// Set stream.[[backpressureChangePromise]] to a new promise.;
*self.backpressure_change_promise.borrow_mut() = Some(Promise::new(global, can_gc));
// Set stream.[[backpressure]] to backpressure.
self.backpressure.set(backpressure);
}
/// <https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller>
fn set_up_transform_stream_default_controller(
&self,
controller: &TransformStreamDefaultController,
) {
// Assert: stream implements TransformStream.
// Note: this is checked with type.
// Assert: stream.[[controller]] is undefined.
assert!(self.controller.get().is_none());
// Set controller.[[stream]] to stream.
controller.set_stream(self);
// Set stream.[[controller]] to controller.
self.controller.set(Some(controller));
// Set controller.[[transformAlgorithm]] to transformAlgorithm.
// Set controller.[[flushAlgorithm]] to flushAlgorithm.
// Set controller.[[cancelAlgorithm]] to cancelAlgorithm.
// Note: These are set in the constructor.
}
/// <https://streams.spec.whatwg.org/#set-up-transform-stream-default-controller-from-transformer>
fn set_up_transform_stream_default_controller_from_transformer(
&self,
global: &GlobalScope,
transformer_obj: SafeHandleObject,
transformer: &Transformer,
can_gc: CanGc,
) {
// Let controller be a new TransformStreamDefaultController.
let controller = TransformStreamDefaultController::new(global, transformer, can_gc);
// Let transformAlgorithm be the following steps, taking a chunk argument:
// Let result be TransformStreamDefaultControllerEnqueue(controller, chunk).
// If result is an abrupt completion, return a promise rejected with result.[[Value]].
// Otherwise, return a promise resolved with undefined.
// Let flushAlgorithm be an algorithm which returns a promise resolved with undefined.
// Let cancelAlgorithm be an algorithm which returns a promise resolved with undefined.
// If transformerDict["transform"] exists, set transformAlgorithm to an algorithm which
// takes an argument
// chunk and returns the result of invoking transformerDict["transform"] with argument
// list « chunk, controller »
// and callback this value transformer.
// If transformerDict["flush"] exists, set flushAlgorithm to an algorithm which returns
// the result
// of invoking transformerDict["flush"] with argument list « controller » and callback
// this value transformer.
// If transformerDict["cancel"] exists, set cancelAlgorithm to an algorithm which takes an argument
// reason and returns the result of invoking transformerDict["cancel"] with argument list « reason »
// and callback this value transformer.
controller.set_transform_obj(transformer_obj);
// Perform ! SetUpTransformStreamDefaultController(stream, controller,
// transformAlgorithm, flushAlgorithm, cancelAlgorithm).
self.set_up_transform_stream_default_controller(&controller);
}
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-write-algorithm>
pub(crate) fn transform_stream_default_sink_write_algorithm(
&self,
cx: SafeJSContext,
global: &GlobalScope,
chunk: SafeHandleValue,
can_gc: CanGc,
) -> Fallible<Rc<Promise>> {
// Assert: stream.[[writable]].[[state]] is "writable".
assert!(self.writable.get().is_some());
// Let controller be stream.[[controller]].
let controller = self.controller.get().expect("controller is not set");
// If stream.[[backpressure]] is true,
if self.backpressure.get() {
// Let backpressureChangePromise be stream.[[backpressureChangePromise]].
let backpressure_change_promise = self.backpressure_change_promise.borrow();
// Assert: backpressureChangePromise is not undefined.
assert!(backpressure_change_promise.is_some());
// Return the result of reacting to backpressureChangePromise with the following fulfillment steps:
let result_promise = Promise::new(global, can_gc);
rooted!(in(*cx) let mut fulfillment_handler = Some(TransformBackPressureChangePromiseFulfillment {
controller: Dom::from_ref(&controller),
writable: Dom::from_ref(&self.writable.get().expect("writable stream")),
chunk: Heap::boxed(chunk.get()),
result_promise: result_promise.clone(),
}));
let handler = PromiseNativeHandler::new(
global,
fulfillment_handler.take().map(|h| Box::new(h) as Box<_>),
Some(Box::new(BackpressureChangeRejection {
result_promise: result_promise.clone(),
})),
can_gc,
);
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
backpressure_change_promise
.as_ref()
.expect("Promise must be some by now.")
.append_native_handler(&handler, comp, can_gc);
return Ok(result_promise);
}
// Return ! TransformStreamDefaultControllerPerformTransform(controller, chunk).
controller.transform_stream_default_controller_perform_transform(cx, global, chunk, can_gc)
}
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-abort-algorithm>
pub(crate) fn transform_stream_default_sink_abort_algorithm(
&self,
cx: SafeJSContext,
global: &GlobalScope,
reason: SafeHandleValue,
can_gc: CanGc,
) -> Fallible<Rc<Promise>> {
// Let controller be stream.[[controller]].
let controller = self.controller.get().expect("controller is not set");
// If controller.[[finishPromise]] is not undefined, return controller.[[finishPromise]].
if let Some(finish_promise) = controller.get_finish_promise() {
return Ok(finish_promise);
}
// Let readable be stream.[[readable]].
let readable = self.readable.get().expect("readable stream is not set");
// Let controller.[[finishPromise]] be a new promise.
controller.set_finish_promise(Promise::new(global, can_gc));
// Let cancelPromise be the result of performing controller.[[cancelAlgorithm]], passing reason.
let cancel_promise = controller.perform_cancel(cx, global, reason, can_gc)?;
// Perform ! TransformStreamDefaultControllerClearAlgorithms(controller).
controller.clear_algorithms();
// React to cancelPromise:
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(CancelPromiseFulfillment {
readable: Dom::from_ref(&readable),
controller: Dom::from_ref(&controller),
reason: Heap::boxed(reason.get()),
})),
Some(Box::new(CancelPromiseRejection {
readable: Dom::from_ref(&readable),
controller: Dom::from_ref(&controller),
})),
can_gc,
);
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
cancel_promise.append_native_handler(&handler, comp, can_gc);
// Return controller.[[finishPromise]].
let finish_promise = controller
.get_finish_promise()
.expect("finish promise is not set");
Ok(finish_promise)
}
/// <https://streams.spec.whatwg.org/#transform-stream-default-sink-close-algorithm>
pub(crate) fn transform_stream_default_sink_close_algorithm(
&self,
cx: SafeJSContext,
global: &GlobalScope,
can_gc: CanGc,
) -> Fallible<Rc<Promise>> {
// Let controller be stream.[[controller]].
let controller = self
.controller
.get()
.ok_or(Error::Type("controller is not set".to_string()))?;
// If controller.[[finishPromise]] is not undefined, return controller.[[finishPromise]].
if let Some(finish_promise) = controller.get_finish_promise() {
return Ok(finish_promise);
}
// Let readable be stream.[[readable]].
let readable = self
.readable
.get()
.ok_or(Error::Type("readable stream is not set".to_string()))?;
// Let controller.[[finishPromise]] be a new promise.
controller.set_finish_promise(Promise::new(global, can_gc));
// Let flushPromise be the result of performing controller.[[flushAlgorithm]].
let flush_promise = controller.perform_flush(cx, global, can_gc)?;
// Perform ! TransformStreamDefaultControllerClearAlgorithms(controller).
controller.clear_algorithms();
// React to flushPromise:
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(FlushPromiseFulfillment {
readable: Dom::from_ref(&readable),
controller: Dom::from_ref(&controller),
})),
Some(Box::new(FlushPromiseRejection {
readable: Dom::from_ref(&readable),
controller: Dom::from_ref(&controller),
})),
can_gc,
);
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
flush_promise.append_native_handler(&handler, comp, can_gc);
// Return controller.[[finishPromise]].
let finish_promise = controller
.get_finish_promise()
.expect("finish promise is not set");
Ok(finish_promise)
}
/// <https://streams.spec.whatwg.org/#transform-stream-default-source-cancel>
pub(crate) fn transform_stream_default_source_cancel(
&self,
cx: SafeJSContext,
global: &GlobalScope,
reason: SafeHandleValue,
can_gc: CanGc,
) -> Fallible<Rc<Promise>> {
// Let controller be stream.[[controller]].
let controller = self
.controller
.get()
.ok_or(Error::Type("controller is not set".to_string()))?;
// If controller.[[finishPromise]] is not undefined, return controller.[[finishPromise]].
if let Some(finish_promise) = controller.get_finish_promise() {
return Ok(finish_promise);
}
// Let writable be stream.[[writable]].
let writable = self
.writable
.get()
.ok_or(Error::Type("writable stream is not set".to_string()))?;
// Let controller.[[finishPromise]] be a new promise.
controller.set_finish_promise(Promise::new(global, can_gc));
// Let cancelPromise be the result of performing controller.[[cancelAlgorithm]], passing reason.
let cancel_promise = controller.perform_cancel(cx, global, reason, can_gc)?;
// Perform ! TransformStreamDefaultControllerClearAlgorithms(controller).
controller.clear_algorithms();
// React to cancelPromise:
let handler = PromiseNativeHandler::new(
global,
Some(Box::new(SourceCancelPromiseFulfillment {
writeable: Dom::from_ref(&writable),
controller: Dom::from_ref(&controller),
stream: Dom::from_ref(self),
reason: Heap::boxed(reason.get()),
})),
Some(Box::new(SourceCancelPromiseRejection {
writeable: Dom::from_ref(&writable),
controller: Dom::from_ref(&controller),
stream: Dom::from_ref(self),
})),
can_gc,
);
// Return controller.[[finishPromise]].
let finish_promise = controller
.get_finish_promise()
.expect("finish promise is not set");
let realm = enter_realm(global);
let comp = InRealm::Entered(&realm);
cancel_promise.append_native_handler(&handler, comp, can_gc);
Ok(finish_promise)
}
/// <https://streams.spec.whatwg.org/#transform-stream-default-source-pull>
pub(crate) fn transform_stream_default_source_pull(
&self,
global: &GlobalScope,
can_gc: CanGc,
) -> Fallible<Rc<Promise>> {
// Assert: stream.[[backpressure]] is true.
assert!(self.backpressure.get());
// Assert: stream.[[backpressureChangePromise]] is not undefined.
assert!(self.backpressure_change_promise.borrow().is_some());
// Perform ! TransformStreamSetBackpressure(stream, false).
self.set_backpressure(global, false, can_gc);
// Return stream.[[backpressureChangePromise]].
Ok(self
.backpressure_change_promise
.borrow()
.clone()
.expect("Promise must be some by now."))
}
/// <https://streams.spec.whatwg.org/#transform-stream-error-writable-and-unblock-write>
pub(crate) fn error_writable_and_unblock_write(
&self,
cx: SafeJSContext,
global: &GlobalScope,
error: SafeHandleValue,
can_gc: CanGc,
) {
// Perform ! TransformStreamDefaultControllerClearAlgorithms(stream.[[controller]]).
self.get_controller().clear_algorithms();
// Perform ! WritableStreamDefaultControllerErrorIfNeeded(stream.[[writable]].[[controller]], e).
self.get_writable()
.get_default_controller()
.error_if_needed(cx, error, global, can_gc);
// Perform ! TransformStreamUnblockWrite(stream).
self.unblock_write(global, can_gc)
}
/// <https://streams.spec.whatwg.org/#transform-stream-unblock-write>
pub(crate) fn unblock_write(&self, global: &GlobalScope, can_gc: CanGc) {
// If stream.[[backpressure]] is true, perform ! TransformStreamSetBackpressure(stream, false).
if self.backpressure.get() {
self.set_backpressure(global, false, can_gc);
}
}
/// <https://streams.spec.whatwg.org/#transform-stream-error>
pub(crate) fn error(
&self,
cx: SafeJSContext,
global: &GlobalScope,
error: SafeHandleValue,
can_gc: CanGc,
) {
// Perform ! ReadableStreamDefaultControllerError(stream.[[readable]].[[controller]], e).
self.get_readable()
.get_default_controller()
.error(error, can_gc);
// Perform ! TransformStreamErrorWritableAndUnblockWrite(stream, e).
self.error_writable_and_unblock_write(cx, global, error, can_gc);
}
}
impl TransformStreamMethods<crate::DomTypeHolder> for TransformStream {
/// <https://streams.spec.whatwg.org/#ts-constructor>
#[allow(unsafe_code)]
fn Constructor(
cx: SafeJSContext,
global: &GlobalScope,
proto: Option<SafeHandleObject>,
can_gc: CanGc,
transformer: Option<*mut JSObject>,
writable_strategy: &QueuingStrategy,
readable_strategy: &QueuingStrategy,
) -> Fallible<DomRoot<TransformStream>> {
// If transformer is missing, set it to null.
rooted!(in(*cx) let transformer_obj = transformer.unwrap_or(ptr::null_mut()));
// Let underlyingSinkDict be underlyingSink,
// converted to an IDL value of type UnderlyingSink.
let transformer_dict = if !transformer_obj.is_null() {
rooted!(in(*cx) let obj_val = ObjectValue(transformer_obj.get()));
match Transformer::new(cx, obj_val.handle()) {
Ok(ConversionResult::Success(val)) => val,
Ok(ConversionResult::Failure(error)) => return Err(Error::Type(error.to_string())),
_ => {
return Err(Error::JSFailed);
},
}
} else {
Transformer::empty()
};
// If transformerDict["readableType"] exists, throw a RangeError exception.
if !transformer_dict.readableType.handle().is_undefined() {
return Err(Error::Range("readableType is set".to_string()));
}
// If transformerDict["writableType"] exists, throw a RangeError exception.
if !transformer_dict.writableType.handle().is_undefined() {
return Err(Error::Range("writableType is set".to_string()));
}
// Let readableHighWaterMark be ? ExtractHighWaterMark(readableStrategy, 0).
let readable_high_water_mark = extract_high_water_mark(readable_strategy, 0.0)?;
// Let readableSizeAlgorithm be ! ExtractSizeAlgorithm(readableStrategy).
let readable_size_algorithm = extract_size_algorithm(readable_strategy, can_gc);
// Let writableHighWaterMark be ? ExtractHighWaterMark(writableStrategy, 1).
let writable_high_water_mark = extract_high_water_mark(writable_strategy, 1.0)?;
// Let writableSizeAlgorithm be ! ExtractSizeAlgorithm(writableStrategy).
let writable_size_algorithm = extract_size_algorithm(writable_strategy, can_gc);
// Let startPromise be a new promise.
let start_promise = Promise::new(global, can_gc);
// Perform ! InitializeTransformStream(this, startPromise, writableHighWaterMark,
// writableSizeAlgorithm, readableHighWaterMark, readableSizeAlgorithm).
let stream = TransformStream::new_with_proto(global, proto, can_gc);
stream.initialize(
cx,
global,
start_promise.clone(),
writable_high_water_mark,
writable_size_algorithm,
readable_high_water_mark,
readable_size_algorithm,
can_gc,
)?;
// Perform ? SetUpTransformStreamDefaultControllerFromTransformer(this, transformer, transformerDict).
stream.set_up_transform_stream_default_controller_from_transformer(
global,
transformer_obj.handle(),
&transformer_dict,
can_gc,
);
// If transformerDict["start"] exists, then resolve startPromise with the
// result of invoking transformerDict["start"]
// with argument list « this.[[controller]] » and callback this value transformer.
if let Some(start) = &transformer_dict.start {
rooted!(in(*cx) let mut result_object = ptr::null_mut::<JSObject>());
rooted!(in(*cx) let mut result: JSVal);
rooted!(in(*cx) let this_object = transformer_obj.get());
start.Call_(
&this_object.handle(),
&stream.get_controller(),
result.handle_mut(),
ExceptionHandling::Rethrow,
can_gc,
)?;
let is_promise = unsafe {
if result.is_object() {
result_object.set(result.to_object());
IsPromiseObject(result_object.handle().into_handle())
} else {
false
}
};
let promise = if is_promise {
let promise = Promise::new_with_js_promise(result_object.handle(), cx);
promise
} else {
Promise::new_resolved(global, cx, result.get(), can_gc)
};
start_promise.resolve_native(&promise, can_gc);
} else {
// Otherwise, resolve startPromise with undefined.
start_promise.resolve_native(&(), can_gc);
};
Ok(stream)
}
/// <https://streams.spec.whatwg.org/#ts-readable>
fn Readable(&self) -> DomRoot<ReadableStream> {
// Return this.[[readable]].
self.readable.get().expect("readable stream is not set")
}
/// <https://streams.spec.whatwg.org/#ts-writable>
fn Writable(&self) -> DomRoot<WritableStream> {
// Return this.[[writable]].
self.writable.get().expect("writable stream is not set")
}
}
/// <https://streams.spec.whatwg.org/#ts-transfer>
impl Transferable for TransformStream {
type Index = MessagePortIndex;
type Data = TransformStreamData;
fn transfer(&self) -> Result<(MessagePortId, TransformStreamData), ()> {
let global = self.global();
let realm = enter_realm(&*global);
let comp = InRealm::Entered(&realm);
let cx = GlobalScope::get_cx();
let can_gc = CanGc::note();
// Let readable be value.[[readable]].
let readable = self.get_readable();
// Let writable be value.[[writable]].
let writable = self.get_writable();
// If ! IsReadableStreamLocked(readable) is true, throw a "DataCloneError" DOMException.
// If ! IsWritableStreamLocked(writable) is true, throw a "DataCloneError" DOMException.
if readable.is_locked() || writable.is_locked() {
return Err(());
}
// First port pair (readable → proxy writable)
let port1 = MessagePort::new(&global, can_gc);
global.track_message_port(&port1, None);
let port1_peer = MessagePort::new(&global, can_gc);
global.track_message_port(&port1_peer, None);
global.entangle_ports(*port1.message_port_id(), *port1_peer.message_port_id());
let proxy_readable = ReadableStream::new_with_proto(&global, None, can_gc);
proxy_readable.setup_cross_realm_transform_readable(cx, &port1, can_gc);
proxy_readable
.pipe_to(cx, &global, &writable, false, false, false, comp, can_gc)
.set_promise_is_handled();
// Second port pair (proxy readable → writable)
let port2 = MessagePort::new(&global, can_gc);
global.track_message_port(&port2, None);
let port2_peer = MessagePort::new(&global, can_gc);
global.track_message_port(&port2_peer, None);
global.entangle_ports(*port2.message_port_id(), *port2_peer.message_port_id());
let proxy_writable = WritableStream::new_with_proto(&global, None, can_gc);
proxy_writable.setup_cross_realm_transform_writable(cx, &port2, can_gc);
// Pipe readable into the proxy writable (→ port_1)
readable
.pipe_to(
cx,
&global,
&proxy_writable,
false,
false,
false,
comp,
can_gc,
)
.set_promise_is_handled();
// Set dataHolder.[[readable]] to ! StructuredSerializeWithTransfer(readable, « readable »).
// Set dataHolder.[[writable]] to ! StructuredSerializeWithTransfer(writable, « writable »).
Ok((
*port1_peer.message_port_id(),
TransformStreamData {
readable: port1_peer.transfer()?,
writable: port2_peer.transfer()?,
},
))
}
fn transfer_receive(
owner: &GlobalScope,
_id: MessagePortId,
data: TransformStreamData,
) -> Result<DomRoot<Self>, ()> {
let can_gc = CanGc::note();
let cx = GlobalScope::get_cx();
let port1 = MessagePort::transfer_receive(owner, data.readable.0, data.readable.1)?;
let port2 = MessagePort::transfer_receive(owner, data.writable.0, data.writable.1)?;
// Let readableRecord be ! StructuredDeserializeWithTransfer(dataHolder.[[readable]], the current Realm).
// Set value.[[readable]] to readableRecord.[[Deserialized]].
// Let writableRecord be ! StructuredDeserializeWithTransfer(dataHolder.[[writable]], the current Realm).
let proxy_readable = ReadableStream::new_with_proto(owner, None, can_gc);
proxy_readable.setup_cross_realm_transform_readable(cx, &port2, can_gc);
let proxy_writable = WritableStream::new_with_proto(owner, None, can_gc);
proxy_writable.setup_cross_realm_transform_writable(cx, &port1, can_gc);
// Set value.[[readable]] to readableRecord.[[Deserialized]].
// Set value.[[writable]] to writableRecord.[[Deserialized]].
// Set value.[[backpressure]], value.[[backpressureChangePromise]], and value.[[controller]] to undefined.
let stream = TransformStream::new_with_proto(owner, None, can_gc);
stream.readable.set(Some(&proxy_readable));
stream.writable.set(Some(&proxy_writable));
Ok(stream)
}
fn serialized_storage<'a>(
data: StructuredData<'a, '_>,
) -> &'a mut Option<HashMap<MessagePortId, Self::Data>> {
match data {
StructuredData::Reader(r) => &mut r.transform_streams_port_impls,
StructuredData::Writer(w) => &mut w.transform_streams_port,
}
}
}