Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

setTimeout, setInterval and clearInterval (and the same clearTimeout) implementations #4130

Merged
merged 16 commits into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions core/engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ boa_ast.workspace = true
boa_parser.workspace = true
boa_string.workspace = true
cow-utils.workspace = true
futures-lite.workspace = true
serde = { workspace = true, features = ["derive", "rc"] }
serde_json.workspace = true
rand.workspace = true
Expand All @@ -94,7 +95,6 @@ sptr.workspace = true
thiserror.workspace = true
dashmap.workspace = true
num_enum.workspace = true
pollster.workspace = true
thin-vec.workspace = true
itertools = { workspace = true, default-features = false }
icu_normalizer = { workspace = true, features = ["compiled_data"] }
Expand Down Expand Up @@ -139,7 +139,6 @@ criterion.workspace = true
float-cmp.workspace = true
indoc.workspace = true
textwrap.workspace = true
futures-lite.workspace = true
test-case.workspace = true

[target.x86_64-unknown-linux-gnu.dev-dependencies]
Expand Down
2 changes: 1 addition & 1 deletion core/engine/src/context/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ pub trait HostHooks {
None
}

/// Gets the current UTC time of the host.
/// Gets the current UTC time of the host, in milliseconds since epoch.
///
/// Defaults to using [`OffsetDateTime::now_utc`] on all targets,
/// which can cause panics if the target doesn't support [`SystemTime::now`][time].
Expand Down
133 changes: 121 additions & 12 deletions core/engine/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! [`Job`] is an ECMAScript [Job], or a closure that runs an `ECMAScript` computation when
//! there's no other computation running. The module defines several type of jobs:
//! - [`PromiseJob`] for Promise related jobs.
//! - [`TimeoutJob`] for jobs that run after a certain amount of time.
//! - [`NativeAsyncJob`] for jobs that support [`Future`].
//! - [`NativeJob`] for generic jobs that aren't related to Promises.
//!
Expand All @@ -15,7 +16,7 @@
//! - [`IdleJobExecutor`], which is an executor that does nothing, and the default executor if no executor is
//! provided. Useful for hosts that want to disable promises.
//! - [`SimpleJobExecutor`], which is a simple FIFO queue that runs all jobs to completion, bailing
//! on the first error encountered.
//! on the first error encountered. This simple executor will block on any async job queued.
//!
//! ## [`Trace`]?
//!
Expand All @@ -29,14 +30,15 @@
//! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records
//! [`Gc`]: boa_gc::Gc

use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin};

use crate::context::time::{JsDuration, JsInstant};
use crate::{
object::{JsFunction, NativeObject},
realm::Realm,
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};
use std::collections::BTreeMap;
use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin};

/// An ECMAScript [Job Abstract Closure].
///
Expand Down Expand Up @@ -115,6 +117,82 @@ impl NativeJob {
}
}

/// An ECMAScript [Job] that runs after a certain amount of time.
///
/// This represents the [HostEnqueueTimeoutJob] operation from the specification.
///
/// [HostEnqueueTimeoutJob]: https://tc39.es/ecma262/#sec-hostenqueuetimeoutjob
pub struct TimeoutJob {
/// The distance in milliseconds in the future when the job should run.
/// This will be added to the current time when the job is enqueued.
timeout: JsDuration,
/// The job to run after the time has passed.
job: NativeJob,
}

impl Debug for TimeoutJob {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TimeoutJob")
.field("timeout", &self.timeout)
.field("job", &self.job)
.finish()
}
}

impl TimeoutJob {
/// Create a new `TimeoutJob` with a timeout and a job.
#[must_use]
pub fn new(job: NativeJob, timeout_in_millis: u64) -> Self {
Self {
timeout: JsDuration::from_millis(timeout_in_millis),
job,
}
}

/// Creates a new `TimeoutJob` from a closure and a timeout as [`std::time::Duration`].
#[must_use]
pub fn from_duration<F>(f: F, timeout: impl Into<JsDuration>) -> Self
where
F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
{
Self::new(NativeJob::new(f), timeout.into().as_millis())
}

/// Creates a new `TimeoutJob` from a closure, a timeout, and an execution realm.
#[must_use]
pub fn with_realm<F>(
f: F,
realm: Realm,
timeout: std::time::Duration,
context: &mut Context,
) -> Self
where
F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
{
Self::new(
NativeJob::with_realm(f, realm, context),
timeout.as_millis() as u64,
)
}

/// Calls the native job with the specified [`Context`].
///
/// # Note
///
/// If the native job has an execution realm defined, this sets the running execution
/// context to the realm's before calling the inner closure, and resets it after execution.
pub fn call(self, context: &mut Context) -> JsResult<JsValue> {
self.job.call(context)
}

/// Returns the timeout value in milliseconds since epoch.
#[inline]
#[must_use]
pub fn timeout(&self) -> JsDuration {
self.timeout
}
}

/// The [`Future`] job returned by a [`NativeAsyncJob`] operation.
pub type BoxedFuture<'a> = Pin<Box<dyn Future<Output = JsResult<JsValue>> + 'a>>;

Expand Down Expand Up @@ -357,6 +435,10 @@ pub enum Job {
///
/// See [`NativeAsyncJob`] for more information.
AsyncJob(NativeAsyncJob),
/// A generic job that is to be executed after a number of milliseconds.
///
/// See [`TimeoutJob`] for more information.
TimeoutJob(TimeoutJob),
}

impl From<NativeAsyncJob> for Job {
Expand All @@ -371,6 +453,12 @@ impl From<PromiseJob> for Job {
}
}

impl From<TimeoutJob> for Job {
fn from(job: TimeoutJob) -> Self {
Job::TimeoutJob(job)
}
}

/// An executor of `ECMAscript` [Jobs].
///
/// This is the main API that allows creating custom event loops.
Expand Down Expand Up @@ -442,6 +530,7 @@ impl JobExecutor for IdleJobExecutor {
pub struct SimpleJobExecutor {
promise_jobs: RefCell<VecDeque<PromiseJob>>,
async_jobs: RefCell<VecDeque<NativeAsyncJob>>,
timeout_jobs: RefCell<BTreeMap<JsInstant, TimeoutJob>>,
}

impl Debug for SimpleJobExecutor {
Expand All @@ -459,28 +548,50 @@ impl SimpleJobExecutor {
}

impl JobExecutor for SimpleJobExecutor {
fn enqueue_job(&self, job: Job, _: &mut Context) {
fn enqueue_job(&self, job: Job, context: &mut Context) {
match job {
Job::PromiseJob(p) => self.promise_jobs.borrow_mut().push_back(p),
Job::AsyncJob(a) => self.async_jobs.borrow_mut().push_back(a),
Job::TimeoutJob(t) => {
let now = context.clock().now();
self.timeout_jobs.borrow_mut().insert(now + t.timeout(), t);
}
}
}

fn run_jobs(&self, context: &mut Context) -> JsResult<()> {
let now = context.clock().now();

{
let mut timeouts_borrow = self.timeout_jobs.borrow_mut();
// `split_off` returns the jobs after (or equal to) the key. So we need to add 1ms to
// the current time to get the jobs that are due, then swap with the inner timeout
// tree so that we get the jobs to actually run.
let jobs_to_keep = timeouts_borrow.split_off(&(now + JsDuration::from_millis(1)));
let jobs_to_run = std::mem::replace(&mut *timeouts_borrow, jobs_to_keep);
drop(timeouts_borrow);

for job in jobs_to_run.into_values() {
job.call(context)?;
}
}

let context = RefCell::new(context);
loop {
if self.promise_jobs.borrow().is_empty() && self.async_jobs.borrow().is_empty() {
break;
}

// Block on each async jobs running in the queue.
let mut next_job = self.async_jobs.borrow_mut().pop_front();
while let Some(job) = next_job {
if let Err(err) = pollster::block_on(job.call(&context)) {
if let Err(err) = futures_lite::future::block_on(job.call(&context)) {
self.async_jobs.borrow_mut().clear();
self.promise_jobs.borrow_mut().clear();
return Err(err);
};
next_job = self.async_jobs.borrow_mut().pop_front();
}

// Yeah, I have no idea why Rust extends the lifetime of a `RefCell` that should be immediately
// dropped after calling `pop_front`.
let mut next_job = self.promise_jobs.borrow_mut().pop_front();
while let Some(job) = next_job {
if let Err(err) = job.call(&mut context.borrow_mut()) {
Expand All @@ -490,10 +601,8 @@ impl JobExecutor for SimpleJobExecutor {
};
next_job = self.promise_jobs.borrow_mut().pop_front();
}

if self.async_jobs.borrow().is_empty() && self.promise_jobs.borrow().is_empty() {
return Ok(());
}
}

Ok(())
}
}
7 changes: 5 additions & 2 deletions core/engine/src/value/integer.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use num_traits::{AsPrimitive, FromPrimitive};
use std::cmp::Ordering;

/// Represents the result of the `ToIntegerOrInfinity` operation
Expand All @@ -21,10 +22,12 @@ impl IntegerOrInfinity {
///
/// Panics if `min > max`.
#[must_use]
pub fn clamp_finite(self, min: i64, max: i64) -> i64 {
pub fn clamp_finite<I: Ord + AsPrimitive<i64> + FromPrimitive>(self, min: I, max: I) -> I {
assert!(min <= max);
match self {
Self::Integer(i) => i.clamp(min, max),
Self::Integer(i) => {
I::from_i64(i.clamp(min.as_(), max.as_())).expect("`i` should already be clamped")
}
Self::PositiveInfinity => max,
Self::NegativeInfinity => min,
}
Expand Down
Loading
Loading