diff --git a/Cargo.lock b/Cargo.lock index a3e7b199bb0..72b424627ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -638,6 +638,7 @@ dependencies = [ "boa_engine", "futures-lite", "indoc", + "rustc-hash 2.1.3", "textwrap", ] diff --git a/core/runtime/src/clone/mod.rs b/core/runtime/src/clone/mod.rs deleted file mode 100644 index c98654183e1..00000000000 --- a/core/runtime/src/clone/mod.rs +++ /dev/null @@ -1,53 +0,0 @@ -//! Module containing all types and functions to implement `structuredClone`. -//! -//! See . -#![allow(clippy::needless_pass_by_value)] - -use boa_engine::realm::Realm; -use boa_engine::value::TryFromJs; -use boa_engine::{Context, JsResult, JsValue, boa_module}; - -/// Options used by `structuredClone`. This is currently unused. -#[derive(Debug, Clone, TryFromJs)] -pub struct StructuredCloneOptions { - transfer: Option>, -} - -/// JavaScript module containing the `structuredClone` types and functions. -#[boa_module] -pub mod js_module { - use super::StructuredCloneOptions; - use crate::store::JsValueStore; - use boa_engine::value::TryIntoJs; - use boa_engine::{Context, JsResult, JsValue}; - - /// The [`structuredClone()`][mdn] method of the Window interface creates a - /// deep clone of a given value using the [structured clone algorithm][sca]. - /// - /// # Errors - /// Will return an error if the context cannot create objects or copy bytes, or - /// if any unhandled case by the structured clone algorithm. - /// - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone - /// [sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm - pub fn structured_clone( - value: JsValue, - options: Option, - context: &mut Context, - ) -> JsResult { - let v = JsValueStore::try_from_js( - &value, - context, - options.and_then(|o| o.transfer).unwrap_or_default(), - )?; - v.try_into_js(context) - } -} - -/// Register the `structuredClone` function in the global context. -/// -/// # Errors -/// Return an error if the function is already registered. -pub fn register(realm: Option, context: &mut Context) -> JsResult<()> { - js_module::boa_register(realm, context) -} diff --git a/core/runtime/src/extensions.rs b/core/runtime/src/extensions.rs index f271bd0afe7..16d10467105 100644 --- a/core/runtime/src/extensions.rs +++ b/core/runtime/src/extensions.rs @@ -23,7 +23,7 @@ pub struct TimeoutExtension; impl RuntimeExtension for TimeoutExtension { fn register(self, _realm: Option, context: &mut Context) -> JsResult<()> { - crate::interval::register(context) + boa_wintertc::timers::register(context) } } @@ -33,7 +33,7 @@ pub struct MicrotaskExtension; impl RuntimeExtension for MicrotaskExtension { fn register(self, realm: Option, context: &mut Context) -> JsResult<()> { - crate::microtask::register(realm, context) + boa_wintertc::microtask::register(realm, context) } } @@ -54,7 +54,7 @@ pub struct StructuredCloneExtension; impl RuntimeExtension for StructuredCloneExtension { fn register(self, realm: Option, context: &mut Context) -> JsResult<()> { - crate::clone::register(realm, context) + boa_wintertc::clone::register(realm, context) } } diff --git a/core/runtime/src/interval.rs b/core/runtime/src/interval.rs deleted file mode 100644 index 247aab04a37..00000000000 --- a/core/runtime/src/interval.rs +++ /dev/null @@ -1,235 +0,0 @@ -//! A module that declares any functions for dealing with intervals or -//! timeouts. - -use boa_engine::interop::JsRest; -use boa_engine::job::{CancellationToken, IntervalJob, NativeJobFn}; -use boa_engine::job::{NativeJob, TimeoutJob}; -use boa_engine::object::builtins::JsFunction; - -use boa_engine::{Context, IntoJsFunctionCopied, JsResult, JsValue, js_error, js_string}; -use std::collections::HashMap; -use std::num::NonZeroU32; - -#[cfg(test)] -mod tests; - -/// The internal state of the interval module. The value is whether the interval -/// function is still active. -struct IntervalInnerState { - active_map: HashMap, - id: NonZeroU32, -} - -impl Default for IntervalInnerState { - fn default() -> Self { - Self { - active_map: HashMap::new(), - id: NonZeroU32::MIN, - } - } -} - -impl IntervalInnerState { - /// Get the interval handler map from the context, or add it to the context if not - /// present. - fn from_context(context: &mut Context) -> &mut Self { - if !context.has_data::() { - context.insert_data(Self::default()); - } - - context - .host_defined_mut() - .get_mut::() - .expect("Should have inserted.") - } - - /// Create an interval ID. - fn next_id(&mut self) -> JsResult { - self.active_map.retain(|_, v| !v.revoked()); - let id = self.id; - self.id = id - .checked_add(1) - .ok_or_else(|| js_error!(Error: "Interval ID overflow"))?; - Ok(id) - } - - /// Delete an interval ID from the active map. - fn clear_interval(&mut self, id: u32) -> Option { - self.active_map.retain(|_, v| !v.revoked()); - let id = NonZeroU32::new(id)?; - self.active_map.remove(&id) - } - - /// Drains and returns every active timer/interval token. - fn drain_tokens(&mut self) -> Vec { - std::mem::take(&mut self.active_map).into_values().collect() - } -} - -/// Set a timeout to call the given function after the given delay. -/// The `code` version of this function is not supported at the moment. -/// -/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout). -/// -/// # Errors -/// Any errors when trying to read the context, converting the arguments or -/// enqueuing the job. -pub fn set_timeout( - function_ref: Option, - delay_in_msec: Option, - rest: JsRest<'_>, - context: &mut Context, -) -> JsResult { - let Some(function_ref) = function_ref else { - return Ok(0); - }; - - // The spec converts the delay to a WebIDL `long`, which maps to `i32`. - // Negative values are clamped to 0. - let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?; - let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0)); - - let state = IntervalInnerState::from_context(context); - let id = state.next_id()?; - - // Get ownership of rest arguments. - let rest = rest.to_vec(); - - let job = TimeoutJob::new( - NativeJob::new(move |context| { - let result = function_ref.call(&JsValue::undefined(), &rest, context); - let state = IntervalInnerState::from_context(context); - state.active_map.remove(&id); - result - }), - delay, - ); - let token = job.cancellation_token().clone(); - - token.push_callback(move |context| { - let state = IntervalInnerState::from_context(context); - state.active_map.remove(&id); - }); - - state.active_map.insert(id, token); - - context.enqueue_job(job.into()); - - Ok(id.get()) -} - -/// Call a given function on an interval with the given delay. -/// The `code` version of this function is not supported at the moment. -/// -/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval). -/// -/// # Errors -/// Any errors when trying to read the context, converting the arguments or -/// enqueuing the job. -pub fn set_interval( - function_ref: Option, - delay_in_msec: Option, - rest: JsRest<'_>, - context: &mut Context, -) -> JsResult { - let Some(function_ref) = function_ref else { - return Ok(0); - }; - - // The spec converts the delay to a WebIDL `long`, which maps to `i32`. - // Negative values are clamped to 0. - let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?; - let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0)); - - let state = IntervalInnerState::from_context(context); - let id = state.next_id()?; - - // Get ownership of rest arguments. - let rest = rest.to_vec(); - - let job = IntervalJob::new( - NativeJobFn::new(move |context| function_ref.call(&JsValue::undefined(), &rest, context)), - delay, - ); - let token = job.cancellation_token().clone(); - - token.push_callback(move |context| { - let state = IntervalInnerState::from_context(context); - state.active_map.remove(&id); - }); - - state.active_map.insert(id, token); - - context.enqueue_job(job.into()); - - Ok(id.get()) -} - -/// Clears a timeout or interval currently running. -/// -/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout). -/// -/// Please note that this is the same exact method as `clearInterval`, as both can be -/// used interchangeably. Invalid, zero, or negative IDs are silently ignored, -/// matching browser behavior. -/// -/// # Errors -/// Returns an error if the `id` argument cannot be converted to an `i32`. -pub fn clear_timeout(id: Option, context: &mut Context) -> JsResult { - let id = id.unwrap_or_default().to_i32(context)?; - if id > 0 { - let handler_map = IntervalInnerState::from_context(context); - if let Some(token) = handler_map.clear_interval(id.cast_unsigned()) { - token.cancel(context); - } - } - Ok(JsValue::undefined()) -} - -/// Cancels every currently active timer and interval registered through -/// this module's `setTimeout` / `setInterval`. -/// -/// Intended for test teardown and graceful shutdown: after this call, any -/// pending [`boa_engine::job::TimeoutJob`] / [`boa_engine::job::IntervalJob`] -/// in the executor's queue will be skipped on its next tick, allowing -/// `run_jobs_async` to exit naturally without disturbing unrelated -/// `PromiseJob`, `NativeAsyncJob`, or `GenericJob` work. -/// -/// Note: timers scheduled *after* this call returns are not affected. -pub fn clear_all(context: &mut Context) { - let state = IntervalInnerState::from_context(context); - // Drain first to avoid re-entrant mutation: each token's cancel callback - // tries to remove its own id from `active_map`. - let tokens = state.drain_tokens(); - for token in tokens { - token.cancel(context); - } -} - -/// Register the interval module into the given context. -/// -/// # Errors -/// Any error returned by the context when registering the global functions. -pub fn register(context: &mut Context) -> JsResult<()> { - register_functions(context) -} - -/// Register the interval module without any clock. This still needs the proper -/// typing for the clock, even if it is not registered to the context. -/// -/// # Errors -/// Any error returned by the context when registering the global functions. -pub fn register_functions(context: &mut Context) -> JsResult<()> { - let set_timeout_ = set_timeout.into_js_function_copied(context); - context.register_global_callable(js_string!("setTimeout"), 1, set_timeout_)?; - - let set_interval_ = set_interval.into_js_function_copied(context); - context.register_global_callable(js_string!("setInterval"), 1, set_interval_)?; - - // These two methods are identical, just under different names in JavaScript. - let clear_timeout_ = clear_timeout.into_js_function_copied(context); - context.register_global_callable(js_string!("clearTimeout"), 1, clear_timeout_.clone())?; - context.register_global_callable(js_string!("clearInterval"), 1, clear_timeout_)?; - - Ok(()) -} diff --git a/core/runtime/src/lib.rs b/core/runtime/src/lib.rs index e1e1ec2869e..1888169424f 100644 --- a/core/runtime/src/lib.rs +++ b/core/runtime/src/lib.rs @@ -119,16 +119,43 @@ pub use console::{Console, ConsoleState, DefaultLogger, Logger, NullLogger}; #[cfg(feature = "fetch")] pub mod abort; -pub mod clone; + +/// `structuredClone`, re-exported from [`boa_wintertc`]. +/// +/// This API is part of the `WinterTC` (TC55) Minimum Common Web API and is implemented in +/// `boa_wintertc`. It is re-exported here so `boa_runtime` users keep a single import path. +#[doc(inline)] +pub use boa_wintertc::clone; + pub mod extensions; #[cfg(feature = "fetch")] pub mod fetch; -pub mod interval; +/// Timer APIs (`setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`), +/// re-exported from [`boa_wintertc`]. +/// +/// These APIs are part of the `WinterTC` (TC55) Minimum Common Web API and are implemented in +/// `boa_wintertc::timers`. The module is re-exported under its historical `interval` name so +/// `boa_runtime` users keep a single, unchanged import path. +#[doc(inline)] +pub use boa_wintertc::timers as interval; pub mod message; -pub mod microtask; + +/// `queueMicrotask`, re-exported from [`boa_wintertc`]. +/// +/// This API is part of the `WinterTC` (TC55) Minimum Common Web API and is implemented in +/// `boa_wintertc`. It is re-exported here so `boa_runtime` users keep a single import path. +#[doc(inline)] +pub use boa_wintertc::microtask; #[cfg(feature = "process")] pub mod process; -pub mod store; +/// [`JsValueStore`](boa_wintertc::store::JsValueStore) and related structured-data types, +/// re-exported from [`boa_wintertc`]. +/// +/// This is the serialization core backing `structuredClone` (and, in the future, messaging). +/// It lives in `boa_wintertc` and is re-exported here so `boa_runtime` internals (e.g. `message`) +/// keep a single import path. +#[doc(inline)] +pub use boa_wintertc::store; /// Support for the `$262` test262 harness object. #[cfg(feature = "test262")] pub mod test262; @@ -264,6 +291,7 @@ pub(crate) mod test { } /// Executes `op` with the currently active context in an async environment. + #[allow(unused)] pub(crate) fn inspect_context_async(op: impl AsyncFnOnce(&mut Context) + 'static) -> Self { Self(Inner::InspectContextAsync { op: Box::new(move |ctx| Box::pin(op(ctx))), diff --git a/core/runtime/src/microtask/mod.rs b/core/runtime/src/microtask/mod.rs deleted file mode 100644 index a9156a1283c..00000000000 --- a/core/runtime/src/microtask/mod.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Microtask-related functions and types. -use boa_engine::realm::Realm; -use boa_engine::{Context, JsResult, boa_module}; - -#[cfg(test)] -mod tests; - -/// JavaScript module containing the `queueMicrotask` function. -#[boa_module] -pub mod js_module { - use boa_engine::job::{Job, PromiseJob}; - use boa_engine::object::builtins::JsFunction; - use boa_engine::{Context, JsValue}; - - /// The [`queueMicrotask()`][mdn] method of the `Window` interface queues a - /// microtask to be executed at a safe time prior to control returning to - /// the browser's event loop. - /// - /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask - pub fn queue_microtask(callback: JsFunction, context: &mut Context) { - context.enqueue_job(Job::from(PromiseJob::new(move |context| { - callback.call(&JsValue::undefined(), &[], context) - }))); - } -} - -/// Register the `queueMicrotask` function to the realm or context. -/// -/// # Errors -/// Returns an error if the microtask extension cannot be registered. -pub fn register(realm: Option, context: &mut Context) -> JsResult<()> { - js_module::boa_register(realm, context) -} diff --git a/core/runtime/src/microtask/tests.rs b/core/runtime/src/microtask/tests.rs deleted file mode 100644 index ba7bcef9a28..00000000000 --- a/core/runtime/src/microtask/tests.rs +++ /dev/null @@ -1,55 +0,0 @@ -use crate::RuntimeExtension; -use crate::console::tests::RecordingLogger; -use crate::test::{TestAction, run_test_actions_with}; -use boa_engine::Context; -use indoc::indoc; - -#[test] -fn queue_microtask() { - let context = &mut Context::default(); - crate::microtask::register(None, context).unwrap(); - let logger = RecordingLogger::default(); - crate::extensions::ConsoleExtension(logger.clone()) - .register(None, context) - .unwrap(); - - run_test_actions_with( - [ - TestAction::run(indoc! {r#" - console.log(1); - queueMicrotask(() => console.log(2)); - console.log(3); - queueMicrotask(() => { - console.log(4); - queueMicrotask(() => { - console.log(5); - queueMicrotask(() => console.log(6)); - console.log(7); - }); - console.log(8); - }); - console.log(9); - "#}), - TestAction::inspect_context(|context| { - context.run_jobs().unwrap(); - }), - ], - context, - ); - - let logs = logger.log.borrow().clone(); - assert_eq!( - logs, - indoc! { r#" - 1 - 3 - 9 - 2 - 4 - 8 - 5 - 7 - 6 - "# } - ); -} diff --git a/core/wintertc/Cargo.toml b/core/wintertc/Cargo.toml index b661013a61c..fc5dc42419d 100644 --- a/core/wintertc/Cargo.toml +++ b/core/wintertc/Cargo.toml @@ -13,6 +13,7 @@ rust-version.workspace = true [dependencies] boa_engine.workspace = true base64.workspace = true +rustc-hash = { workspace = true, features = ["std"] } [dev-dependencies] indoc.workspace = true diff --git a/core/wintertc/src/clone/mod.rs b/core/wintertc/src/clone/mod.rs index 8bcd5daa32a..c98654183e1 100644 --- a/core/wintertc/src/clone/mod.rs +++ b/core/wintertc/src/clone/mod.rs @@ -1,23 +1,53 @@ -//! TC55 `structuredClone` implementation. +//! Module containing all types and functions to implement `structuredClone`. //! -//! Spec: -//! -//! # TC55 Status -//! -//! `structuredClone` is required in the `WinterTC` TC55 Minimum Common Web API. -//! -//! # TODO -//! -//! - Migrate `structuredClone` from `boa_runtime::clone`. +//! See . +#![allow(clippy::needless_pass_by_value)] + +use boa_engine::realm::Realm; +use boa_engine::value::TryFromJs; +use boa_engine::{Context, JsResult, JsValue, boa_module}; -/// Register `structuredClone` into the given context. +/// Options used by `structuredClone`. This is currently unused. +#[derive(Debug, Clone, TryFromJs)] +pub struct StructuredCloneOptions { + transfer: Option>, +} + +/// JavaScript module containing the `structuredClone` types and functions. +#[boa_module] +pub mod js_module { + use super::StructuredCloneOptions; + use crate::store::JsValueStore; + use boa_engine::value::TryIntoJs; + use boa_engine::{Context, JsResult, JsValue}; + + /// The [`structuredClone()`][mdn] method of the Window interface creates a + /// deep clone of a given value using the [structured clone algorithm][sca]. + /// + /// # Errors + /// Will return an error if the context cannot create objects or copy bytes, or + /// if any unhandled case by the structured clone algorithm. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window/structuredClone + /// [sca]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm + pub fn structured_clone( + value: JsValue, + options: Option, + context: &mut Context, + ) -> JsResult { + let v = JsValueStore::try_from_js( + &value, + context, + options.and_then(|o| o.transfer).unwrap_or_default(), + )?; + v.try_into_js(context) + } +} + +/// Register the `structuredClone` function in the global context. /// /// # Errors -/// -/// Returns a [`boa_engine::JsError`] if registration fails. -pub fn register( - _realm: Option, - _ctx: &mut boa_engine::Context, -) -> boa_engine::JsResult<()> { - Ok(()) +/// Return an error if the function is already registered. +pub fn register(realm: Option, context: &mut Context) -> JsResult<()> { + js_module::boa_register(realm, context) } diff --git a/core/wintertc/src/lib.rs b/core/wintertc/src/lib.rs index 17c86bb625b..2a2168ddbbb 100644 --- a/core/wintertc/src/lib.rs +++ b/core/wintertc/src/lib.rs @@ -29,6 +29,7 @@ html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo_black.svg" )] #![cfg_attr(not(test), forbid(clippy::unwrap_used))] +#![cfg_attr(test, allow(clippy::needless_raw_string_hashes))] // Makes strings a bit more copy-pastable #![allow( clippy::module_name_repetitions, clippy::redundant_pub_crate, @@ -44,6 +45,7 @@ pub mod events; #[cfg(feature = "fetch")] pub mod fetch; pub mod microtask; +pub mod store; pub mod timers; #[cfg(feature = "url")] pub mod url; @@ -63,7 +65,9 @@ pub fn register( ctx: &mut boa_engine::Context, ) -> boa_engine::JsResult<()> { console::register(realm.clone(), ctx)?; - timers::register(realm.clone(), ctx)?; + // `timers` mirrors `boa_runtime::interval::register`, which does not take a + // realm (timer globals are registered directly on the context). + timers::register(ctx)?; encoding::register(realm.clone(), ctx)?; microtask::register(realm.clone(), ctx)?; clone::register(realm.clone(), ctx)?; diff --git a/core/wintertc/src/microtask/mod.rs b/core/wintertc/src/microtask/mod.rs index bae48303b4e..a9156a1283c 100644 --- a/core/wintertc/src/microtask/mod.rs +++ b/core/wintertc/src/microtask/mod.rs @@ -1,23 +1,33 @@ -//! TC55 `queueMicrotask` implementation. -//! -//! Spec: -//! -//! # TC55 Status -//! -//! `queueMicrotask` is required in the `WinterTC` TC55 Minimum Common Web API. -//! -//! # TODO -//! -//! - Migrate `queueMicrotask` from `boa_runtime::microtask`. +//! Microtask-related functions and types. +use boa_engine::realm::Realm; +use boa_engine::{Context, JsResult, boa_module}; -/// Register `queueMicrotask` into the given context. +#[cfg(test)] +mod tests; + +/// JavaScript module containing the `queueMicrotask` function. +#[boa_module] +pub mod js_module { + use boa_engine::job::{Job, PromiseJob}; + use boa_engine::object::builtins::JsFunction; + use boa_engine::{Context, JsValue}; + + /// The [`queueMicrotask()`][mdn] method of the `Window` interface queues a + /// microtask to be executed at a safe time prior to control returning to + /// the browser's event loop. + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Window/queueMicrotask + pub fn queue_microtask(callback: JsFunction, context: &mut Context) { + context.enqueue_job(Job::from(PromiseJob::new(move |context| { + callback.call(&JsValue::undefined(), &[], context) + }))); + } +} + +/// Register the `queueMicrotask` function to the realm or context. /// /// # Errors -/// -/// Returns a [`boa_engine::JsError`] if registration fails. -pub fn register( - _realm: Option, - _ctx: &mut boa_engine::Context, -) -> boa_engine::JsResult<()> { - Ok(()) +/// Returns an error if the microtask extension cannot be registered. +pub fn register(realm: Option, context: &mut Context) -> JsResult<()> { + js_module::boa_register(realm, context) } diff --git a/core/wintertc/src/microtask/tests.rs b/core/wintertc/src/microtask/tests.rs new file mode 100644 index 00000000000..72d31f7b90b --- /dev/null +++ b/core/wintertc/src/microtask/tests.rs @@ -0,0 +1,57 @@ +use crate::test::{TestAction, run_test_actions_with}; +use boa_engine::{Context, js_str}; +use indoc::indoc; + +#[test] +fn queue_microtask() { + let context = &mut Context::default(); + crate::microtask::register(None, context).unwrap(); + + run_test_actions_with( + [ + // Record execution order in a global array instead of relying on + // `console`, which is not part of this crate's test surface. + TestAction::run(indoc! {r#" + order = []; + order.push(1); + queueMicrotask(() => order.push(2)); + order.push(3); + queueMicrotask(() => { + order.push(4); + queueMicrotask(() => { + order.push(5); + queueMicrotask(() => order.push(6)); + order.push(7); + }); + order.push(8); + }); + order.push(9); + "#}), + TestAction::inspect_context(|ctx| { + ctx.run_jobs().unwrap(); + }), + TestAction::inspect_context(|ctx| { + let order = ctx.global_object().get(js_str!("order"), ctx).unwrap(); + let order = order.as_object().unwrap(); + + assert_eq!( + order.get(js_str!("length"), ctx).unwrap().as_i32(), + Some(9), + "all nine pushes must run", + ); + + // Synchronous pushes run first (1, 3, 9); the queued microtasks + // then drain in FIFO order, with nested microtasks appended as + // they are enqueued. + for (i, expected) in [1, 3, 9, 2, 4, 8, 5, 7, 6].into_iter().enumerate() { + assert_eq!( + order.get(i, ctx).unwrap().as_i32(), + Some(expected), + "order[{i}] mismatch", + ); + } + }), + ], + context, + ); +} diff --git a/core/runtime/src/store/from.rs b/core/wintertc/src/store/from.rs similarity index 100% rename from core/runtime/src/store/from.rs rename to core/wintertc/src/store/from.rs diff --git a/core/runtime/src/store/mod.rs b/core/wintertc/src/store/mod.rs similarity index 100% rename from core/runtime/src/store/mod.rs rename to core/wintertc/src/store/mod.rs diff --git a/core/runtime/src/store/to.rs b/core/wintertc/src/store/to.rs similarity index 100% rename from core/runtime/src/store/to.rs rename to core/wintertc/src/store/to.rs diff --git a/core/wintertc/src/timers/mod.rs b/core/wintertc/src/timers/mod.rs index b3d06adae8a..9290417df12 100644 --- a/core/wintertc/src/timers/mod.rs +++ b/core/wintertc/src/timers/mod.rs @@ -5,20 +5,236 @@ //! # TC55 Status //! //! Timer APIs are required in the `WinterTC` TC55 Minimum Common Web API. -//! -//! # TODO -//! -//! - Migrate `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval` from `boa_runtime::interval`. -/// Register timer globals (`setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`) -/// into the given context. +use boa_engine::interop::JsRest; +use boa_engine::job::{CancellationToken, IntervalJob, NativeJobFn}; +use boa_engine::job::{NativeJob, TimeoutJob}; +use boa_engine::object::builtins::JsFunction; + +use boa_engine::{Context, IntoJsFunctionCopied, JsResult, JsValue, js_error, js_string}; +use std::collections::HashMap; +use std::num::NonZeroU32; + +#[cfg(test)] +mod tests; + +/// The internal state of the interval module. The value is whether the interval +/// function is still active. +struct IntervalInnerState { + active_map: HashMap, + id: NonZeroU32, +} + +impl Default for IntervalInnerState { + fn default() -> Self { + Self { + active_map: HashMap::new(), + id: NonZeroU32::MIN, + } + } +} + +impl IntervalInnerState { + /// Get the interval handler map from the context, or add it to the context if not + /// present. + fn from_context(context: &mut Context) -> &mut Self { + if !context.has_data::() { + context.insert_data(Self::default()); + } + + context + .host_defined_mut() + .get_mut::() + .expect("Should have inserted.") + } + + /// Create an interval ID. + fn next_id(&mut self) -> JsResult { + self.active_map.retain(|_, v| !v.revoked()); + let id = self.id; + self.id = id + .checked_add(1) + .ok_or_else(|| js_error!(Error: "Interval ID overflow"))?; + Ok(id) + } + + /// Delete an interval ID from the active map. + fn clear_interval(&mut self, id: u32) -> Option { + self.active_map.retain(|_, v| !v.revoked()); + let id = NonZeroU32::new(id)?; + self.active_map.remove(&id) + } + + /// Drains and returns every active timer/interval token. + fn drain_tokens(&mut self) -> Vec { + std::mem::take(&mut self.active_map).into_values().collect() + } +} + +/// Set a timeout to call the given function after the given delay. +/// The `code` version of this function is not supported at the moment. +/// +/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout). /// /// # Errors +/// Any errors when trying to read the context, converting the arguments or +/// enqueuing the job. +pub fn set_timeout( + function_ref: Option, + delay_in_msec: Option, + rest: JsRest<'_>, + context: &mut Context, +) -> JsResult { + let Some(function_ref) = function_ref else { + return Ok(0); + }; + + // The spec converts the delay to a WebIDL `long`, which maps to `i32`. + // Negative values are clamped to 0. + let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?; + let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0)); + + let state = IntervalInnerState::from_context(context); + let id = state.next_id()?; + + // Get ownership of rest arguments. + let rest = rest.to_vec(); + + let job = TimeoutJob::new( + NativeJob::new(move |context| { + let result = function_ref.call(&JsValue::undefined(), &rest, context); + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + result + }), + delay, + ); + let token = job.cancellation_token().clone(); + + token.push_callback(move |context| { + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + }); + + state.active_map.insert(id, token); + + context.enqueue_job(job.into()); + + Ok(id.get()) +} + +/// Call a given function on an interval with the given delay. +/// The `code` version of this function is not supported at the moment. /// -/// Returns a [`boa_engine::JsError`] if registration fails. -pub fn register( - _realm: Option, - _ctx: &mut boa_engine::Context, -) -> boa_engine::JsResult<()> { +/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval). +/// +/// # Errors +/// Any errors when trying to read the context, converting the arguments or +/// enqueuing the job. +pub fn set_interval( + function_ref: Option, + delay_in_msec: Option, + rest: JsRest<'_>, + context: &mut Context, +) -> JsResult { + let Some(function_ref) = function_ref else { + return Ok(0); + }; + + // The spec converts the delay to a WebIDL `long`, which maps to `i32`. + // Negative values are clamped to 0. + let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?; + let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0)); + + let state = IntervalInnerState::from_context(context); + let id = state.next_id()?; + + // Get ownership of rest arguments. + let rest = rest.to_vec(); + + let job = IntervalJob::new( + NativeJobFn::new(move |context| function_ref.call(&JsValue::undefined(), &rest, context)), + delay, + ); + let token = job.cancellation_token().clone(); + + token.push_callback(move |context| { + let state = IntervalInnerState::from_context(context); + state.active_map.remove(&id); + }); + + state.active_map.insert(id, token); + + context.enqueue_job(job.into()); + + Ok(id.get()) +} + +/// Clears a timeout or interval currently running. +/// +/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout). +/// +/// Please note that this is the same exact method as `clearInterval`, as both can be +/// used interchangeably. Invalid, zero, or negative IDs are silently ignored, +/// matching browser behavior. +/// +/// # Errors +/// Returns an error if the `id` argument cannot be converted to an `i32`. +pub fn clear_timeout(id: Option, context: &mut Context) -> JsResult { + let id = id.unwrap_or_default().to_i32(context)?; + if id > 0 { + let handler_map = IntervalInnerState::from_context(context); + if let Some(token) = handler_map.clear_interval(id.cast_unsigned()) { + token.cancel(context); + } + } + Ok(JsValue::undefined()) +} + +/// Cancels every currently active timer and interval registered through +/// this module's `setTimeout` / `setInterval`. +/// +/// Intended for test teardown and graceful shutdown: after this call, any +/// pending [`boa_engine::job::TimeoutJob`] / [`boa_engine::job::IntervalJob`] +/// in the executor's queue will be skipped on its next tick, allowing +/// `run_jobs_async` to exit naturally without disturbing unrelated +/// `PromiseJob`, `NativeAsyncJob`, or `GenericJob` work. +/// +/// Note: timers scheduled *after* this call returns are not affected. +pub fn clear_all(context: &mut Context) { + let state = IntervalInnerState::from_context(context); + // Drain first to avoid re-entrant mutation: each token's cancel callback + // tries to remove its own id from `active_map`. + let tokens = state.drain_tokens(); + for token in tokens { + token.cancel(context); + } +} + +/// Register the interval module into the given context. +/// +/// # Errors +/// Any error returned by the context when registering the global functions. +pub fn register(context: &mut Context) -> JsResult<()> { + register_functions(context) +} + +/// Register the interval module without any clock. This still needs the proper +/// typing for the clock, even if it is not registered to the context. +/// +/// # Errors +/// Any error returned by the context when registering the global functions. +pub fn register_functions(context: &mut Context) -> JsResult<()> { + let set_timeout_ = set_timeout.into_js_function_copied(context); + context.register_global_callable(js_string!("setTimeout"), 1, set_timeout_)?; + + let set_interval_ = set_interval.into_js_function_copied(context); + context.register_global_callable(js_string!("setInterval"), 1, set_interval_)?; + + // These two methods are identical, just under different names in JavaScript. + let clear_timeout_ = clear_timeout.into_js_function_copied(context); + context.register_global_callable(js_string!("clearTimeout"), 1, clear_timeout_.clone())?; + context.register_global_callable(js_string!("clearInterval"), 1, clear_timeout_)?; + Ok(()) } diff --git a/core/runtime/src/interval/tests.rs b/core/wintertc/src/timers/tests.rs similarity index 99% rename from core/runtime/src/interval/tests.rs rename to core/wintertc/src/timers/tests.rs index e3925da74f3..3d8aa9a39c2 100644 --- a/core/runtime/src/interval/tests.rs +++ b/core/wintertc/src/timers/tests.rs @@ -1,5 +1,5 @@ -use crate::interval; use crate::test::{TestAction, run_test_actions_with}; +use crate::timers as interval; use boa_engine::context::time::FixedClock; use boa_engine::context::{Clock, ContextBuilder}; use boa_engine::job::{JobExecutor, SimpleJobExecutor};