servo/components/script/dom/underlyingsourcecontainer.rs
Taym Haddadi f3f4cc5500
Script implement TransformStream and TransformStreamDefaultController (#36739)
Part of https://github.com/servo/servo/issues/34676

---------

Signed-off-by: Taym Haddadi <haddadi.taym@gmail.com>
Signed-off-by: gterzian <2792687+gterzian@users.noreply.github.com>
Signed-off-by: Taym <haddadi.taym@gmail.com>
Co-authored-by: gterzian <2792687+gterzian@users.noreply.github.com>
2025-05-08 08:45:57 +00:00

303 lines
12 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 http://mozilla.org/MPL/2.0/. */
use std::ptr;
use std::rc::Rc;
use dom_struct::dom_struct;
use js::jsapi::{Heap, IsPromiseObject, JSObject};
use js::jsval::{JSVal, UndefinedValue};
use js::rust::{Handle as SafeHandle, HandleObject, HandleValue as SafeHandleValue, IntoHandle};
use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::codegen::Bindings::UnderlyingSourceBinding::UnderlyingSource as JsUnderlyingSource;
use crate::dom::bindings::codegen::UnionTypes::ReadableStreamDefaultControllerOrReadableByteStreamController as Controller;
use crate::dom::bindings::error::Error;
use crate::dom::bindings::reflector::{DomGlobal, Reflector, reflect_dom_object_with_proto};
use crate::dom::bindings::root::{Dom, DomRoot};
use crate::dom::defaultteeunderlyingsource::DefaultTeeUnderlyingSource;
use crate::dom::globalscope::GlobalScope;
use crate::dom::messageport::MessagePort;
use crate::dom::promise::Promise;
use crate::dom::transformstream::TransformStream;
use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
/// <https://streams.spec.whatwg.org/#underlying-source-api>
/// The `Js` variant corresponds to
/// the JavaScript object representing the underlying source.
/// The other variants are native sources in Rust.
#[derive(JSTraceable)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) enum UnderlyingSourceType {
/// Facilitate partial integration with sources
/// that are currently read into memory.
Memory(usize),
/// A blob as underlying source, with a known total size.
Blob(usize),
/// A fetch response as underlying source.
FetchResponse,
/// A struct representing a JS object as underlying source,
/// and the actual JS object for use as `thisArg` in callbacks.
Js(JsUnderlyingSource, Heap<*mut JSObject>),
/// Tee
Tee(Dom<DefaultTeeUnderlyingSource>),
/// Transfer, with the port used in some of the algorithms.
Transfer(Dom<MessagePort>),
/// A struct representing a JS object as underlying source,
/// and the actual JS object for use as `thisArg` in callbacks.
/// This is used for the `TransformStream` API.
Transform(Dom<TransformStream>, Rc<Promise>),
}
impl UnderlyingSourceType {
/// Is the source backed by a Rust native source?
pub(crate) fn is_native(&self) -> bool {
matches!(
self,
UnderlyingSourceType::Memory(_) |
UnderlyingSourceType::Blob(_) |
UnderlyingSourceType::FetchResponse |
UnderlyingSourceType::Transfer(_)
)
}
/// Does the source have all data in memory?
pub(crate) fn in_memory(&self) -> bool {
matches!(self, UnderlyingSourceType::Memory(_))
}
}
/// Wrapper around the underlying source.
#[dom_struct]
pub(crate) struct UnderlyingSourceContainer {
reflector_: Reflector,
#[ignore_malloc_size_of = "JsUnderlyingSource implemented in SM."]
underlying_source_type: UnderlyingSourceType,
}
impl UnderlyingSourceContainer {
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
fn new_inherited(underlying_source_type: UnderlyingSourceType) -> UnderlyingSourceContainer {
UnderlyingSourceContainer {
reflector_: Reflector::new(),
underlying_source_type,
}
}
#[cfg_attr(crown, allow(crown::unrooted_must_root))]
pub(crate) fn new(
global: &GlobalScope,
underlying_source_type: UnderlyingSourceType,
can_gc: CanGc,
) -> DomRoot<UnderlyingSourceContainer> {
// TODO: setting the underlying source dict as the prototype of the
// `UnderlyingSourceContainer`, as it is later used as the "this" in Call_.
// Is this a good idea?
reflect_dom_object_with_proto(
Box::new(UnderlyingSourceContainer::new_inherited(
underlying_source_type,
)),
global,
None,
can_gc,
)
}
/// Setting the JS object after the heap has settled down.
pub(crate) fn set_underlying_source_this_object(&self, object: HandleObject) {
if let UnderlyingSourceType::Js(_source, this_obj) = &self.underlying_source_type {
this_obj.set(*object);
}
}
/// <https://streams.spec.whatwg.org/#dom-underlyingsource-cancel>
#[allow(unsafe_code)]
pub(crate) fn call_cancel_algorithm(
&self,
cx: SafeJSContext,
global: &GlobalScope,
reason: SafeHandleValue,
can_gc: CanGc,
) -> Option<Result<Rc<Promise>, Error>> {
match &self.underlying_source_type {
UnderlyingSourceType::Js(source, this_obj) => {
if let Some(algo) = &source.cancel {
let result = unsafe {
algo.Call_(
&SafeHandle::from_raw(this_obj.handle()),
Some(reason),
ExceptionHandling::Rethrow,
can_gc,
)
};
return Some(result);
}
None
},
UnderlyingSourceType::Tee(tee_underlying_source) => {
// Call the cancel algorithm for the appropriate branch.
tee_underlying_source.cancel_algorithm(cx, global, reason, can_gc)
},
UnderlyingSourceType::Transform(stream, _) => {
// Return ! TransformStreamDefaultSourceCancelAlgorithm(stream, reason).
Some(stream.transform_stream_default_source_cancel(cx, global, reason, can_gc))
},
UnderlyingSourceType::Transfer(port) => {
// Let cancelAlgorithm be the following steps, taking a reason argument:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
// Let result be PackAndPostMessageHandlingError(port, "error", reason).
let result = port.pack_and_post_message_handling_error("error", reason, can_gc);
// Disentangle port.
self.global().disentangle_port(port, can_gc);
let promise = Promise::new(&self.global(), can_gc);
// If result is an abrupt completion,
if let Err(error) = result {
// Return a promise rejected with result.[[Value]].
promise.reject_error(error, can_gc);
} else {
// Otherwise, return a promise resolved with undefined.
promise.resolve_native(&(), can_gc);
}
Some(Ok(promise))
},
_ => None,
}
}
/// <https://streams.spec.whatwg.org/#dom-underlyingsource-pull>
#[allow(unsafe_code)]
pub(crate) fn call_pull_algorithm(
&self,
controller: Controller,
_global: &GlobalScope,
can_gc: CanGc,
) -> Option<Result<Rc<Promise>, Error>> {
match &self.underlying_source_type {
UnderlyingSourceType::Js(source, this_obj) => {
if let Some(algo) = &source.pull {
let result = unsafe {
algo.Call_(
&SafeHandle::from_raw(this_obj.handle()),
controller,
ExceptionHandling::Rethrow,
can_gc,
)
};
return Some(result);
}
None
},
UnderlyingSourceType::Tee(tee_underlying_source) => {
// Call the pull algorithm for the appropriate branch.
Some(Ok(tee_underlying_source.pull_algorithm(can_gc)))
},
UnderlyingSourceType::Transfer(port) => {
// Let pullAlgorithm be the following steps:
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
let cx = GlobalScope::get_cx();
// Perform ! PackAndPostMessage(port, "pull", undefined).
rooted!(in(*cx) let mut value = UndefinedValue());
port.pack_and_post_message("pull", value.handle(), can_gc)
.expect("Sending pull should not fail.");
// Return a promise resolved with undefined.
let promise = Promise::new(&self.global(), can_gc);
promise.resolve_native(&(), can_gc);
Some(Ok(promise))
},
// Note: other source type have no pull steps for now.
UnderlyingSourceType::Transform(stream, _) => {
// Return ! TransformStreamDefaultSourcePullAlgorithm(stream).
Some(stream.transform_stream_default_source_pull(&self.global(), can_gc))
},
_ => None,
}
}
/// <https://streams.spec.whatwg.org/#dom-underlyingsource-start>
///
/// Note: The algorithm can return any value, including a promise,
/// we always transform the result into a promise for convenience,
/// and it is also how to spec deals with the situation.
/// see "Let startPromise be a promise resolved with startResult."
/// at <https://streams.spec.whatwg.org/#set-up-readable-stream-default-controller>
#[allow(unsafe_code)]
pub(crate) fn call_start_algorithm(
&self,
controller: Controller,
can_gc: CanGc,
) -> Option<Result<Rc<Promise>, Error>> {
match &self.underlying_source_type {
UnderlyingSourceType::Js(source, this_obj) => {
if let Some(start) = &source.start {
let cx = GlobalScope::get_cx();
rooted!(in(*cx) let mut result_object = ptr::null_mut::<JSObject>());
rooted!(in(*cx) let mut result: JSVal);
unsafe {
if let Err(error) = start.Call_(
&SafeHandle::from_raw(this_obj.handle()),
controller,
result.handle_mut(),
ExceptionHandling::Rethrow,
can_gc,
) {
return Some(Err(error));
}
}
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 {
let promise = Promise::new(&self.global(), can_gc);
promise.resolve_native(&result.get(), can_gc);
promise
};
return Some(Ok(promise));
}
None
},
UnderlyingSourceType::Tee(_) => {
// Let startAlgorithm be an algorithm that returns undefined.
None
},
UnderlyingSourceType::Transfer(_) => {
// Let startAlgorithm be an algorithm that returns undefined.
// from <https://streams.spec.whatwg.org/#abstract-opdef-setupcrossrealmtransformreadable
None
},
UnderlyingSourceType::Transform(_, start_promise) => {
// Let startAlgorithm be an algorithm that returns startPromise.
Some(Ok(start_promise.clone()))
},
_ => None,
}
}
/// <https://streams.spec.whatwg.org/#dom-underlyingsource-autoallocatechunksize>
pub(crate) fn auto_allocate_chunk_size(&self) -> Option<u64> {
match &self.underlying_source_type {
UnderlyingSourceType::Js(source, _) => source.autoAllocateChunkSize,
_ => None,
}
}
/// Does the source have all data in memory?
pub(crate) fn in_memory(&self) -> bool {
self.underlying_source_type.in_memory()
}
}