diff --git a/crates/jstz_api/src/idl.rs b/crates/jstz_api/src/idl.rs index 9f8a65bbf..c366de120 100644 --- a/crates/jstz_api/src/idl.rs +++ b/crates/jstz_api/src/idl.rs @@ -182,3 +182,20 @@ impl ArrayBufferLike for JsBufferSource { } } } + +// https://webidl.spec.whatwg.org/#idl-types + +pub type Any = JsValue; +pub type Bytes = i8; +pub type Octet = u8; +pub type Short = i16; +pub type UnsignedShort = u16; +pub type Long = i32; +pub type UnsignedLong = u32; +pub type LongLong = i64; +pub type UnsignedLongLong = u64; +pub type UnrestrictedFloat = f32; +pub type UnrestrictedDouble = f64; + +pub type PositiveInteger = UnsignedLongLong; +pub type Number = f64; diff --git a/crates/jstz_api/src/lib.rs b/crates/jstz_api/src/lib.rs index 85f84806d..bed936b10 100644 --- a/crates/jstz_api/src/lib.rs +++ b/crates/jstz_api/src/lib.rs @@ -4,6 +4,8 @@ mod kv; pub mod encoding; pub mod http; pub mod idl; +pub mod stream; +pub mod todo; pub mod url; pub mod urlpattern; pub use console::{ConsoleApi, LogRecord, LOG_PREFIX}; diff --git a/crates/jstz_api/src/stream/mod.rs b/crates/jstz_api/src/stream/mod.rs new file mode 100644 index 000000000..85b88cab1 --- /dev/null +++ b/crates/jstz_api/src/stream/mod.rs @@ -0,0 +1,14 @@ +use boa_engine::Context; + +use self::readable::ReadableStreamApi; + +pub mod readable; +mod tmp; + +pub struct StreamApi; + +impl jstz_core::Api for StreamApi { + fn init(self, context: &mut Context<'_>) { + ReadableStreamApi.init(context); + } +} diff --git a/crates/jstz_api/src/stream/readable/mod.rs b/crates/jstz_api/src/stream/readable/mod.rs new file mode 100644 index 000000000..a569adc41 --- /dev/null +++ b/crates/jstz_api/src/stream/readable/mod.rs @@ -0,0 +1,58 @@ +use boa_engine::{value::TryFromJs, Context, JsArgs, JsResult}; +use boa_gc::{custom_trace, Finalize, Trace}; +use jstz_core::native::{ + register_global_class, ClassBuilder, JsNativeObject, NativeClass, +}; + +use crate::stream::readable::underlying_source::{ + UnderlyingSource, UnderlyingSourceTrait, +}; + +pub mod underlying_source; + +pub struct ReadableStream { + // TODO +} + +impl Finalize for ReadableStream { + fn finalize(&self) { + todo!() + } +} + +unsafe impl Trace for ReadableStream { + custom_trace!(this, todo!()); +} + +pub struct ReadableStreamClass; + +impl NativeClass for ReadableStreamClass { + type Instance = ReadableStream; + + const NAME: &'static str = "ReadableStream"; + + fn constructor( + _this: &JsNativeObject, + args: &[boa_engine::JsValue], + context: &mut Context<'_>, + ) -> JsResult { + let underlying_source = + Option::::try_from_js(args.get_or_undefined(0), context)?; + todo!() + } + + fn init(class: &mut ClassBuilder<'_, '_>) -> JsResult<()> { + // TODO + Ok(()) + } +} + +pub struct ReadableStreamApi; + +impl jstz_core::Api for ReadableStreamApi { + fn init(self, context: &mut Context<'_>) { + register_global_class::(context) + .expect("The `ReadableStream` class shouldn't exist yet") + // TODO + } +} diff --git a/crates/jstz_api/src/stream/readable/underlying_source.rs b/crates/jstz_api/src/stream/readable/underlying_source.rs new file mode 100644 index 000000000..8202ac982 --- /dev/null +++ b/crates/jstz_api/src/stream/readable/underlying_source.rs @@ -0,0 +1,411 @@ +//! https://streams.spec.whatwg.org/#underlying-source-api + +use boa_engine::{ + js_string, object::builtins::JsPromise, property::PropertyKey, value::TryFromJs, + Context, JsNativeError, JsObject, JsResult, JsValue, +}; +use boa_gc::{custom_trace, Finalize, Trace}; +use jstz_core::{ + impl_into_js_from_into, js_fn::JsFn, native::JsNativeObject, value::IntoJs, +}; +use std::str::FromStr; + +use crate::idl; + +use crate::stream::tmp::*; + +/// dictionary [UnderlyingSource][spec] { +/// UnderlyingSourceStartCallback start; +/// UnderlyingSourcePullCallback pull; +/// UnderlyingSourceCancelCallback cancel; +/// ReadableStreamType type; +/// \[EnforceRange\] unsigned long long autoAllocateChunkSize; +/// }; +/// +/// [Note][spec2]: We cannot declare the underlyingSource argument as having the UnderlyingSource type directly, because doing so would lose the reference to the original object. We need to retain the object so we can invoke the various methods on it. +/// +/// [spec]: https://streams.spec.whatwg.org/#dictdef-underlyingsource +/// [spec2]: https://streams.spec.whatwg.org/#rs-constructor +#[derive(Debug)] +pub struct UnderlyingSource { + /// TODO + pub this: JsObject, + + /// **[start][spec](controller), of type UnderlyingSourceStartCallback** + /// + /// A function that is called immediately during creation of the ReadableStream. + /// + /// Typically this is used to adapt a push source by setting up relevant event listeners, as in the example of § 10.1 A readable stream with an underlying push source (no backpressure support), or to acquire access to a pull source, as in § 10.4 A readable stream with an underlying pull source. + /// + /// If this setup process is asynchronous, it can return a promise to signal success or failure; a rejected promise will error the stream. Any thrown exceptions will be re-thrown by the ReadableStream() constructor. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-start + pub start: Option, + + /// **[pull][spec](controller), of type UnderlyingSourcePullCallback** + /// + /// A function that is called whenever the stream’s internal queue of chunks becomes not full, i.e. whenever the queue’s desired size becomes positive. Generally, it will be called repeatedly until the queue reaches its high water mark (i.e. until the desired size becomes non-positive). + /// + /// For push sources, this can be used to resume a paused flow, as in § 10.2 A readable stream with an underlying push source and backpressure support. For pull sources, it is used to acquire new chunks to enqueue into the stream, as in § 10.4 A readable stream with an underlying pull source. + /// + /// This function will not be called until start() successfully completes. Additionally, it will only be called repeatedly if it enqueues at least one chunk or fulfills a BYOB request; a no-op pull() implementation will not be continually called. + /// + /// If the function returns a promise, then it will not be called again until that promise fulfills. (If the promise rejects, the stream will become errored.) This is mainly used in the case of pull sources, where the promise returned represents the process of acquiring a new chunk. Throwing an exception is treated the same as returning a rejected promise. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-pull + pub pull: Option, + + /// **cancel(reason), of type UnderlyingSourceCancelCallback** + /// A function that is called whenever the consumer cancels the stream, via stream.cancel() or reader.cancel(). It takes as its argument the same value as was passed to those methods by the consumer. + /// + /// Readable streams can additionally be canceled under certain conditions during piping; see the definition of the pipeTo() method for more details. + /// + // For all streams, this is generally used to release access to the underlying resource; see for example § 10.1 A readable stream with an underlying push source (no backpressure support). + /// + /// If the shutdown process is asynchronous, it can return a promise to signal success or failure; the result will be communicated via the return value of the cancel() method that was called. Throwing an exception is treated the same as returning a rejected promise. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-cancel + /// + /// *Even if the cancelation process fails, the stream will still close; it will not be put into an errored state. This is because a failure in the cancelation process doesn’t matter to the consumer’s view of the stream, once they’ve expressed disinterest in it by canceling. The failure is only communicated to the immediate caller of the corresponding method.* + /// + /// *This is different from the behavior of the close and abort options of a WritableStream's underlying sink, which upon failure put the corresponding WritableStream into an errored state. Those correspond to specific actions the producer is requesting and, if those actions fail, they indicate something more persistently wrong.* + pub cancel: Option, + + /// **[type][spec] (byte streams only), of type ReadableStreamType** + /// + /// Can be set to "bytes" to signal that the constructed ReadableStream is a readable byte stream. This ensures that the resulting ReadableStream will successfully be able to vend BYOB readers via its getReader() method. It also affects the controller argument passed to the start() and pull() methods; see below. + /// + /// For an example of how to set up a readable byte stream, including using the different controller interface, see § 10.3 A readable byte stream with an underlying push source (no backpressure support). + /// + /// Setting any value other than "bytes" or undefined will cause the ReadableStream() constructor to throw an exception. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-type + pub r#type: Option, + + /// **[autoAllocateChunkSize][spec] (byte streams only), of type unsigned long long** + /// + /// Can be set to a positive integer to cause the implementation to automatically allocate buffers for the underlying source code to write into. In this case, when a consumer is using a default reader, the stream implementation will automatically allocate an ArrayBuffer of the given size, so that controller.byobRequest is always present, as if the consumer was using a BYOB reader. + /// + /// This is generally used to cut down on the amount of code needed to handle consumers that use default readers, as can be seen by comparing § 10.3 A readable byte stream with an underlying push source (no backpressure support) without auto-allocation to § 10.5 A readable byte stream with an underlying pull source with auto-allocation. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-autoallocatechunksize + pub auto_allocate_chunk_size: Option, // TODO [EnforceRange] +} + +impl Finalize for UnderlyingSource { + fn finalize(&self) {} +} + +unsafe impl Trace for UnderlyingSource { + custom_trace!(this, { + mark(&this.this); + mark(&this.start); + mark(&this.pull); + mark(&this.cancel); + }); +} + +// TODO derive this implementation with a macro? +impl TryFromJs for UnderlyingSource { + fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult { + // TODO check that this function works as intended in all cases, + // and move it either to a new derive macro for TryFromJs, or to JsObject + #[allow(non_snake_case)] + pub fn get_JsObject_property( + obj: &JsObject, + name: &str, + context: &mut Context<'_>, + ) -> JsResult { + let key = PropertyKey::from(js_string!(name)); + let key2 = key.clone(); + let has_prop = obj.has_property(key, context)?; + if has_prop { + obj.get(key2, context) + } else { + Ok(JsValue::Undefined) + } + } + + let this = value.to_object(context)?; + let start: Option = + get_JsObject_property(&this, "start", context)?.try_js_into(context)?; + let pull: Option = + get_JsObject_property(&this, "pull", context)?.try_js_into(context)?; + let cancel: Option = + get_JsObject_property(&this, "cancel", context)?.try_js_into(context)?; + let r#type = + get_JsObject_property(&this, "type", context)?.try_js_into(context)?; + let auto_allocate_chunk_size = + get_JsObject_property(&this, "autoAllocateChunkSize", context)? + .try_js_into(context)?; + Ok(UnderlyingSource { + this, + start, + pull, + cancel, + r#type, + auto_allocate_chunk_size, + }) + } +} + +impl Into for UnderlyingSource { + fn into(self) -> JsValue { + self.this.into() + } +} + +impl_into_js_from_into!(UnderlyingSource); + +pub trait UnderlyingSourceTrait { + /// **[start][spec](controller), of type UnderlyingSourceStartCallback** + /// + /// A function that is called immediately during creation of the ReadableStream. + /// + /// Typically this is used to adapt a push source by setting up relevant event listeners, as in the example of § 10.1 A readable stream with an underlying push source (no backpressure support), or to acquire access to a pull source, as in § 10.4 A readable stream with an underlying pull source. + /// + /// If this setup process is asynchronous, it can return a promise to signal success or failure; a rejected promise will error the stream. Any thrown exceptions will be re-thrown by the ReadableStream() constructor. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-start + fn start( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult; + + /// **[pull][spec](controller), of type UnderlyingSourcePullCallback** + /// + /// A function that is called whenever the stream’s internal queue of chunks becomes not full, i.e. whenever the queue’s desired size becomes positive. Generally, it will be called repeatedly until the queue reaches its high water mark (i.e. until the desired size becomes non-positive). + /// + /// For push sources, this can be used to resume a paused flow, as in § 10.2 A readable stream with an underlying push source and backpressure support. For pull sources, it is used to acquire new chunks to enqueue into the stream, as in § 10.4 A readable stream with an underlying pull source. + /// + /// This function will not be called until start() successfully completes. Additionally, it will only be called repeatedly if it enqueues at least one chunk or fulfills a BYOB request; a no-op pull() implementation will not be continually called. + /// + /// If the function returns a promise, then it will not be called again until that promise fulfills. (If the promise rejects, the stream will become errored.) This is mainly used in the case of pull sources, where the promise returned represents the process of acquiring a new chunk. Throwing an exception is treated the same as returning a rejected promise. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-pull + fn pull( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult>; + + /// **cancel(reason), of type UnderlyingSourceCancelCallback** + /// A function that is called whenever the consumer cancels the stream, via stream.cancel() or reader.cancel(). It takes as its argument the same value as was passed to those methods by the consumer. + /// + /// Readable streams can additionally be canceled under certain conditions during piping; see the definition of the pipeTo() method for more details. + /// + // For all streams, this is generally used to release access to the underlying resource; see for example § 10.1 A readable stream with an underlying push source (no backpressure support). + /// + /// If the shutdown process is asynchronous, it can return a promise to signal success or failure; the result will be communicated via the return value of the cancel() method that was called. Throwing an exception is treated the same as returning a rejected promise. + /// + /// [spec]: https://streams.spec.whatwg.org/#dom-underlyingsource-cancel + /// + /// *Even if the cancelation process fails, the stream will still close; it will not be put into an errored state. This is because a failure in the cancelation process doesn’t matter to the consumer’s view of the stream, once they’ve expressed disinterest in it by canceling. The failure is only communicated to the immediate caller of the corresponding method.* + /// + /// *This is different from the behavior of the close and abort options of a WritableStream's underlying sink, which upon failure put the corresponding WritableStream into an errored state. Those correspond to specific actions the producer is requesting and, if those actions fail, they indicate something more persistently wrong.* + fn cancel( + &self, + reason: Option, + context: &mut Context, + ) -> JsResult>; +} + +#[derive(Default)] +pub struct UndefinedUnderlyingSource {} + +impl UnderlyingSourceTrait for UndefinedUnderlyingSource { + fn start( + &self, + _controller: JsNativeObject, + _context: &mut Context, + ) -> JsResult { + Ok(JsValue::Undefined) // TODO spec link + } + + fn pull( + &self, + _controller: JsNativeObject, + context: &mut Context, + ) -> JsResult> { + JsPromise::resolve(JsValue::Undefined, context).map(Option::Some) // TODO spec link + } + + fn cancel( + &self, + _reason: Option, + context: &mut Context, + ) -> JsResult> { + JsPromise::resolve(JsValue::Undefined, context).map(Option::Some) // TODO spec link + } +} + +impl UnderlyingSourceTrait for UnderlyingSource { + fn start( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult { + if let Some(ref start) = self.start { + start.call( + self.this.clone(), // TODO remove clone? https://tezos-dev.slack.com/archives/C061SSDBN69/p1701192316869399 + (controller,), + context, + ) + } else { + UndefinedUnderlyingSource::default().start(controller, context) + } + } + + fn pull( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult> { + if let Some(ref pull) = self.pull { + pull.call( + self.this.clone(), // TODO remove clone? https://tezos-dev.slack.com/archives/C061SSDBN69/p1701192316869399 + (controller,), + context, + ) + } else { + UndefinedUnderlyingSource::default().pull(controller, context) + } + } + + fn cancel( + &self, + reason: Option, + context: &mut Context, + ) -> JsResult> { + if let Some(ref cancel) = self.cancel { + cancel.call( + self.this.clone(), // TODO remove clone? https://tezos-dev.slack.com/archives/C061SSDBN69/p1701192316869399 + (reason.unwrap_or(JsValue::Undefined),), + context, + ) + } else { + UndefinedUnderlyingSource::default().cancel(reason, context) + } + } +} + +impl UnderlyingSourceTrait for Option { + fn start( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult { + match self { + Some(underlying_source) => underlying_source.start(controller, context), + None => UndefinedUnderlyingSource::default().start(controller, context), + } + } + + fn pull( + &self, + controller: JsNativeObject, + context: &mut Context, + ) -> JsResult> { + match self { + Some(underlying_source) => underlying_source.pull(controller, context), + None => UndefinedUnderlyingSource::default().pull(controller, context), + } + } + + fn cancel( + &self, + reason: Option, + context: &mut Context, + ) -> JsResult> { + match self { + Some(underlying_source) => underlying_source.cancel(reason, context), + None => UndefinedUnderlyingSource::default().cancel(reason, context), + } + } +} + +/// typedef (ReadableStreamDefaultController or ReadableByteStreamController) [ReadableStreamController][spec]; +/// +/// [spec]: https://streams.spec.whatwg.org/#typedefdef-readablestreamcontroller +#[derive(Debug)] +pub enum ReadableStreamController { + DefaultController(ReadableStreamDefaultController), + ByteController(ReadableByteStreamController), +} + +impl Finalize for ReadableStreamController { + fn finalize(&self) {} +} + +unsafe impl Trace for ReadableStreamController { + custom_trace!(this, { + match this { + ReadableStreamController::DefaultController(value) => mark(value), + ReadableStreamController::ByteController(value) => mark(value), + } + }); +} + +/// callback [UnderlyingSourceStartCallback][spec] = any (ReadableStreamController controller); +/// +/// [spec]: https://streams.spec.whatwg.org/#callbackdef-underlyingsourcestartcallback +pub type UnderlyingSourceStartCallback = + JsFn,), idl::Any>; + +/// callback [UnderlyingSourcePullCallback][spec] = Promise (ReadableStreamController controller); +/// +/// [spec]: https://streams.spec.whatwg.org/#callbackdef-underlyingsourcepullcallback +pub type UnderlyingSourcePullCallback = + JsFn,), Option>; + +/// callback [UnderlyingSourceCancelCallback][spec] = Promise (optional any reason); +/// +/// [spec]: https://streams.spec.whatwg.org/#callbackdef-underlyingsourcecancelcallback +pub type UnderlyingSourceCancelCallback = + JsFn>; + +#[derive(Debug, PartialEq)] +pub enum ReadableStreamType { + Bytes, +} + +impl Into<&str> for ReadableStreamType { + fn into(self) -> &'static str { + match self { + ReadableStreamType::Bytes => "bytes", + } + } +} + +impl FromStr for ReadableStreamType { + type Err = (); + + fn from_str(s: &str) -> Result { + match s { + "bytes" => Ok(ReadableStreamType::Bytes), + _ => Err(()), + } + } +} + +impl IntoJs for ReadableStreamType { + fn into_js(self, context: &mut Context<'_>) -> JsValue { + let str: &str = self.into(); + String::from(str).into_js(context) + } +} + +impl TryFromJs for ReadableStreamType { + fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult { + let str = String::try_from_js(value, context)?; + ReadableStreamType::from_str(&str).map_err(|()| { + JsNativeError::typ() + .with_message(format!( + "{} is not a valid value for enumeration ReadableStreamType.", + str + )) + .into() + }) + } +} diff --git a/crates/jstz_api/src/stream/tmp.rs b/crates/jstz_api/src/stream/tmp.rs new file mode 100644 index 000000000..4b8e0d772 --- /dev/null +++ b/crates/jstz_api/src/stream/tmp.rs @@ -0,0 +1,6 @@ +//! Temporary definitions to allow compiling before defining all types + +use crate::todo::Todo; + +pub type ReadableStreamDefaultController = Todo; +pub type ReadableByteStreamController = Todo; diff --git a/crates/jstz_api/src/todo.rs b/crates/jstz_api/src/todo.rs new file mode 100644 index 000000000..37d487585 --- /dev/null +++ b/crates/jstz_api/src/todo.rs @@ -0,0 +1,40 @@ +use boa_engine::value::TryFromJs; +use boa_gc::{custom_trace, Finalize, Trace}; +use jstz_core::value::IntoJs; + +/// A placeholder for types that have yet to be defined +#[derive(Debug)] +pub enum Todo { + Todo, +} + +impl Finalize for Todo { + fn finalize(&self) { + todo!() + } +} + +#[allow(unused_variables)] +unsafe impl Trace for Todo { + custom_trace!(this, todo!()); +} + +#[allow(unused_variables)] +impl IntoJs for Todo { + fn into_js( + self, + context: &mut boa_engine::prelude::Context<'_>, + ) -> boa_engine::prelude::JsValue { + todo!() + } +} + +#[allow(unused_variables)] +impl TryFromJs for Todo { + fn try_from_js( + value: &boa_engine::prelude::JsValue, + context: &mut boa_engine::prelude::Context<'_>, + ) -> boa_engine::prelude::JsResult { + todo!() + } +} diff --git a/crates/jstz_cli/src/repl.rs b/crates/jstz_cli/src/repl.rs index 132108d69..cca46b629 100644 --- a/crates/jstz_cli/src/repl.rs +++ b/crates/jstz_cli/src/repl.rs @@ -2,7 +2,7 @@ use anyhow::Result; use boa_engine::{js_string, JsResult, JsValue, Source}; use jstz_api::{ encoding::EncodingApi, http::HttpApi, url::UrlApi, urlpattern::UrlPatternApi, - ConsoleApi, KvApi, + ConsoleApi, KvApi, stream::StreamApi }; use jstz_core::host::HostRuntime; use jstz_core::{ @@ -48,6 +48,7 @@ pub fn exec(self_address: Option, cfg: &Config) -> Result<()> { rt.context(), ); realm_clone.register_api(EncodingApi, rt.context()); + realm_clone.register_api(StreamApi, rt.context()); realm_clone.register_api(UrlApi, rt.context()); realm_clone.register_api(UrlPatternApi, rt.context()); realm_clone.register_api(HttpApi, rt.context()); diff --git a/crates/jstz_core/src/js_fn.rs b/crates/jstz_core/src/js_fn.rs new file mode 100644 index 000000000..7f7c4247d --- /dev/null +++ b/crates/jstz_core/src/js_fn.rs @@ -0,0 +1,136 @@ +use std::{marker::PhantomData, ops::Deref}; + +use boa_engine::{ + object::builtins::JsFunction, value::TryFromJs, Context, JsResult, JsValue, +}; +use boa_gc::{custom_trace, Finalize, Trace}; + +use crate::value::IntoJs; + +pub trait IntoJsArgs { + fn into_js_args(self, context: &mut Context<'_>) -> [JsValue; N]; +} + +impl IntoJsArgs<0> for () { + fn into_js_args(self, _context: &mut Context<'_>) -> [JsValue; 0] { + [] + } +} + +impl IntoJsArgs<1> for (T0,) { + fn into_js_args(self, context: &mut Context<'_>) -> [JsValue; 1] { + [self.0.into_js(context)] + } +} + +impl IntoJsArgs<2> for (T0, T1) { + fn into_js_args(self, context: &mut Context<'_>) -> [JsValue; 2] { + [self.0.into_js(context), self.1.into_js(context)] + } +} + +impl IntoJsArgs<3> for (T0, T1, T2) { + fn into_js_args(self, context: &mut Context<'_>) -> [JsValue; 3] { + [ + self.0.into_js(context), + self.1.into_js(context), + self.2.into_js(context), + ] + } +} + +/// A `JsFn` is a `JsFunction` tagged with some Rust types used to handle the `TryFromJs` and `IntoJs` conversions automatically: +/// - `T` is the type of the `this` parameter; +/// - `N` is the arity; +/// - `I` is a tuple `(I1, ..., IN)` that contains the types of the parameters; +/// - `O` is the type of the output. +#[derive(Debug)] +pub struct JsFn, O: TryFromJs> { + function: JsFunction, + _this_type: PhantomData, + _inputs_type: PhantomData, + _output_type: PhantomData, +} + +impl, O: TryFromJs> Finalize + for JsFn +{ + fn finalize(&self) {} +} + +unsafe impl, O: TryFromJs> Trace + for JsFn +{ + custom_trace!(this, { + mark(&this.function); + }); +} + +impl, O: TryFromJs> Deref + for JsFn +{ + type Target = JsFunction; + + fn deref(&self) -> &Self::Target { + &self.function + } +} + +impl, O: TryFromJs> Into + for JsFn +{ + fn into(self) -> JsFunction { + self.function + } +} + +impl, O: TryFromJs> From + for JsFn +{ + fn from(value: JsFunction) -> Self { + JsFn { + function: value, + _this_type: PhantomData, + _inputs_type: PhantomData, + _output_type: PhantomData, + } + } +} + +impl, O: TryFromJs> Into + for JsFn +{ + fn into(self) -> JsValue { + self.function.into() + } +} + +// impl, O: TryFromJs> TryFrom for JsFn +// This is implementable, but the right way to implement it would be to lift the implementation of `TryFromJs` for `JsFunction` (that does not use the context) to an implementation of `TryFrom` in boa +// (If it is eventually implemented, then the implementation of TryFromJs below should use it) + +impl, O: TryFromJs> IntoJs + for JsFn +{ + fn into_js(self, _context: &mut Context<'_>) -> JsValue { + self.function.into() + } +} + +impl, O: TryFromJs> TryFromJs + for JsFn +{ + fn try_from_js(value: &JsValue, context: &mut Context<'_>) -> JsResult { + JsFunction::try_from_js(value, context).map(JsFn::from) + } +} + +impl, O: TryFromJs> JsFn { + pub fn call(&self, this: T, inputs: I, context: &mut Context<'_>) -> JsResult { + let js_this = this.into_js(context); + let js_args = inputs.into_js_args(context); + self.deref() + .call(&js_this, &js_args, context) + .and_then(|output| O::try_from_js(&output, context)) + } +} diff --git a/crates/jstz_core/src/lib.rs b/crates/jstz_core/src/lib.rs index 99dcd70c4..bac9b0803 100644 --- a/crates/jstz_core/src/lib.rs +++ b/crates/jstz_core/src/lib.rs @@ -6,6 +6,7 @@ pub use error::{Error, Result}; pub mod future; pub mod host; pub mod iterators; +pub mod js_fn; pub mod kv; pub mod native; pub mod realm; diff --git a/crates/jstz_core/src/native.rs b/crates/jstz_core/src/native.rs index 0f9624159..37399194c 100644 --- a/crates/jstz_core/src/native.rs +++ b/crates/jstz_core/src/native.rs @@ -18,7 +18,7 @@ pub use boa_engine::{object::NativeObject, NativeFunction}; use crate::value::IntoJs; /// This struct permits Rust types to be passed around as JavaScript objects. -#[derive(Trace, Finalize)] +#[derive(Trace, Finalize, Debug)] pub struct JsNativeObject { inner: JsValue, _phantom: PhantomData, diff --git a/crates/jstz_core/src/value.rs b/crates/jstz_core/src/value.rs index b25069057..f9f9bb99f 100644 --- a/crates/jstz_core/src/value.rs +++ b/crates/jstz_core/src/value.rs @@ -15,6 +15,7 @@ pub trait IntoJs { fn into_js(self, context: &mut Context<'_>) -> JsValue; } +#[macro_export] macro_rules! impl_into_js_from_into { ($($T: ty), *) => { $(