Skip to main content

boa_engine/
job.rs

1//! Boa's API to create and customize `ECMAScript` jobs and job queues.
2//!
3//! [`Job`] is an ECMAScript [Job], or a closure that runs an `ECMAScript` computation when
4//! there's no other computation running. The module defines several type of jobs:
5//! - [`PromiseJob`] for Promise related jobs.
6//! - [`TimeoutJob`] for jobs that run after a certain amount of time.
7//! - [`NativeAsyncJob`] for jobs that support [`Future`].
8//! - [`NativeJob`] for generic jobs that aren't related to Promises.
9//!
10//! [`JobCallback`] is an ECMAScript [`JobCallback`] record, containing an `ECMAScript` function
11//! that is executed when a promise is either fulfilled or rejected.
12//!
13//! [`JobExecutor`] is a trait encompassing the required functionality for a job executor; this allows
14//! implementing custom event loops, custom handling of Jobs or other fun things.
15//! This trait is also accompanied by two implementors of the trait:
16//! - [`IdleJobExecutor`], which is an executor that does nothing, and the default executor if no executor is
17//!   provided. Useful for hosts that want to disable promises.
18//! - [`SimpleJobExecutor`], which is a simple FIFO queue that runs all jobs to completion, bailing
19//!   on the first error encountered. This simple executor will block on any async job queued.
20//!
21//! ## [`Trace`]?
22//!
23//! Most of the types defined in this module don't implement `Trace`. This is because most jobs can only
24//! be run once, and putting a `JobExecutor` on a garbage collected object is not allowed.
25//!
26//! In addition to that, not implementing `Trace` makes it so that the garbage collector can consider
27//! any captured variables inside jobs as roots, since you cannot store jobs within a [`Gc`].
28//!
29//! [Job]: https://tc39.es/ecma262/#sec-jobs
30//! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records
31//! [`Gc`]: boa_gc::Gc
32
33use crate::context::time::{JsDuration, JsInstant};
34use crate::sys::time;
35use crate::{
36    Context, JsResult, JsValue,
37    object::{JsFunction, NativeObject},
38    realm::Realm,
39};
40use boa_gc::{Finalize, Trace};
41use futures_concurrency::future::FutureGroup;
42use futures_lite::{StreamExt, future};
43use portable_atomic::AtomicBool;
44use std::any::Any;
45use std::cell::Cell;
46use std::collections::BTreeMap;
47use std::mem;
48use std::rc::Rc;
49use std::sync::Arc;
50use std::sync::atomic::Ordering;
51use std::{cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin};
52
53/// An ECMAScript [Job Abstract Closure].
54///
55/// This is basically a synchronous task that needs to be run to progress [`Promise`] objects,
56/// or unblock threads waiting on [`Atomics.waitAsync`].
57///
58/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs
59/// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
60/// [`Atomics.waitAsync`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync
61pub struct NativeJob {
62    #[allow(clippy::type_complexity)]
63    f: Box<dyn FnOnce(&mut Context) -> JsResult<JsValue>>,
64    realm: Option<Realm>,
65}
66
67impl Debug for NativeJob {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        f.debug_struct("NativeJob").finish_non_exhaustive()
70    }
71}
72
73impl NativeJob {
74    /// Creates a new `NativeJob` from a closure.
75    pub fn new<F>(f: F) -> Self
76    where
77        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
78    {
79        Self {
80            f: Box::new(f),
81            realm: None,
82        }
83    }
84
85    /// Creates a new `NativeJob` from a closure and an execution realm.
86    pub fn with_realm<F>(f: F, realm: Realm) -> Self
87    where
88        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
89    {
90        Self {
91            f: Box::new(f),
92            realm: Some(realm),
93        }
94    }
95
96    /// Gets a reference to the execution realm of the job.
97    #[must_use]
98    pub const fn realm(&self) -> Option<&Realm> {
99        self.realm.as_ref()
100    }
101
102    /// Calls the native job with the specified [`Context`].
103    ///
104    /// # Note
105    ///
106    /// If the native job has an execution realm defined, this sets the running execution
107    /// context to the realm's before calling the inner closure, and resets it after execution.
108    pub fn call(self, context: &mut Context) -> JsResult<JsValue> {
109        // If realm is not null, each time job is invoked the implementation must perform
110        // implementation-defined steps such that execution is prepared to evaluate ECMAScript
111        // code at the time of job's invocation.
112        if let Some(realm) = self.realm {
113            let old_realm = context.enter_realm(realm);
114
115            // Let scriptOrModule be GetActiveScriptOrModule() at the time HostEnqueuePromiseJob is
116            // invoked. If realm is not null, each time job is invoked the implementation must
117            // perform implementation-defined steps such that scriptOrModule is the active script or
118            // module at the time of job's invocation.
119            let result = (self.f)(context);
120
121            context.enter_realm(old_realm);
122
123            result
124        } else {
125            (self.f)(context)
126        }
127    }
128}
129
130/// An ECMAScript [Job Abstract Closure] that can be called multiple times.
131///
132/// This is basically a synchronous task that needs to be run to progress [`Promise`] objects,
133/// or unblock threads waiting on [`Atomics.waitAsync`].
134///
135/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs
136/// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
137/// [`Atomics.waitAsync`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/waitAsync
138pub struct NativeJobFn {
139    #[allow(clippy::type_complexity)]
140    f: Box<dyn Fn(&mut Context) -> JsResult<JsValue>>,
141    realm: Option<Realm>,
142}
143
144impl Debug for NativeJobFn {
145    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146        f.debug_struct("NativeJobFn").finish_non_exhaustive()
147    }
148}
149
150impl NativeJobFn {
151    /// Creates a new `NativeJobFn` from a closure.
152    pub fn new<F>(f: F) -> Self
153    where
154        F: Fn(&mut Context) -> JsResult<JsValue> + 'static,
155    {
156        Self {
157            f: Box::new(f),
158            realm: None,
159        }
160    }
161
162    /// Creates a new `NativeJob` from a closure and an execution realm.
163    pub fn with_realm<F>(f: F, realm: Realm) -> Self
164    where
165        F: Fn(&mut Context) -> JsResult<JsValue> + 'static,
166    {
167        Self {
168            f: Box::new(f),
169            realm: Some(realm),
170        }
171    }
172
173    /// Gets a reference to the execution realm of the job.
174    #[must_use]
175    pub const fn realm(&self) -> Option<&Realm> {
176        self.realm.as_ref()
177    }
178
179    /// Calls the native job with the specified [`Context`].
180    ///
181    /// # Note
182    ///
183    /// If the native job has an execution realm defined, this sets the running execution
184    /// context to the realm's before calling the inner closure, and resets it after execution.
185    pub fn call(&self, context: &mut Context) -> JsResult<JsValue> {
186        // If realm is not null, each time job is invoked the implementation must perform
187        // implementation-defined steps such that execution is prepared to evaluate ECMAScript
188        // code at the time of job's invocation.
189        if let Some(realm) = self.realm.clone() {
190            let old_realm = context.enter_realm(realm);
191
192            // Let scriptOrModule be GetActiveScriptOrModule() at the time HostEnqueuePromiseJob is
193            // invoked. If realm is not null, each time job is invoked the implementation must
194            // perform implementation-defined steps such that scriptOrModule is the active script or
195            // module at the time of job's invocation.
196            let result = (self.f)(context);
197
198            context.enter_realm(old_realm);
199
200            result
201        } else {
202            (self.f)(context)
203        }
204    }
205}
206
207type Callback = Box<dyn FnOnce(&mut Context)>;
208
209/// Token to cancel a [`TimeoutJob`] and [`IntervalJob`].
210#[derive(Clone)]
211pub struct CancellationToken(Rc<Cell<Vec<Callback>>>);
212
213impl Debug for CancellationToken {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        f.debug_struct("CancellationToken")
216            .field("cancelled", &self.revoked())
217            .finish_non_exhaustive()
218    }
219}
220
221impl CancellationToken {
222    /// Creates a new cancellation token.
223    pub(crate) fn new() -> Self {
224        Self(Rc::new(Cell::new(vec![Box::new(|_| {})])))
225    }
226
227    /// Sets a callback to run when the cancellation token gets used.
228    ///
229    /// On debug builds, this will panic if the cancellation token was already
230    /// used.
231    pub fn push_callback(&self, f: impl FnOnce(&mut Context) + 'static) {
232        let mut vec = self.0.take();
233        debug_assert!(
234            !vec.is_empty(),
235            "setting a callback on an already used cancellation token"
236        );
237        vec.push(Box::new(f));
238        self.0.set(vec);
239    }
240
241    /// Cancels the [`TimeoutJob`] or [`IntervalJob`] associated with this cancellation token.
242    pub fn cancel(&self, context: &mut Context) {
243        for job in self.0.take() {
244            job(context);
245        }
246    }
247
248    /// Revokes this cancellation token, making it unusable to cancel its associated job.
249    pub(crate) fn revoke(&self) {
250        self.0.take();
251    }
252
253    /// Returns `true` if this cancellation token has been revoked, either because
254    /// `cancel` was called or because its associated job has completed.
255    #[must_use]
256    pub fn revoked(&self) -> bool {
257        let callbacks = self.0.take();
258        let cancelled = callbacks.is_empty();
259        self.0.set(callbacks);
260        cancelled
261    }
262}
263
264/// An ECMAScript [Job] that runs after a certain amount of time.
265///
266/// This represents the [HostEnqueueTimeoutJob] operation from the specification.
267///
268/// [HostEnqueueTimeoutJob]: https://tc39.es/ecma262/#sec-hostenqueuetimeoutjob
269#[derive(Debug)]
270pub struct TimeoutJob {
271    /// The distance in milliseconds in the future when the job should run.
272    /// This will be added to the current time when the job is enqueued.
273    timeout: JsDuration,
274    /// The job to run after the specified timeout.
275    job: Option<NativeJob>,
276    /// Signals if the timeout job was cancelled.
277    cancellation_token: CancellationToken,
278}
279
280impl Drop for TimeoutJob {
281    fn drop(&mut self) {
282        self.cancellation_token.revoke();
283    }
284}
285
286impl TimeoutJob {
287    /// Create a new `TimeoutJob` with a timeout and a job.
288    #[must_use]
289    pub fn new(job: NativeJob, timeout_in_millis: u64) -> Self {
290        Self {
291            timeout: JsDuration::from_millis(timeout_in_millis),
292            job: Some(job),
293            cancellation_token: CancellationToken::new(),
294        }
295    }
296
297    /// Creates a new `TimeoutJob` from a closure and a timeout as [`std::time::Duration`].
298    #[must_use]
299    pub fn from_duration<F>(f: F, timeout: impl Into<JsDuration>) -> Self
300    where
301        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
302    {
303        Self::new(NativeJob::new(f), timeout.into().as_millis())
304    }
305
306    /// Creates a new `TimeoutJob` from a closure, a timeout, and an execution realm.
307    #[must_use]
308    pub fn with_realm<F>(f: F, realm: Realm, timeout: time::Duration) -> Self
309    where
310        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
311    {
312        Self::new(NativeJob::with_realm(f, realm), timeout.as_millis() as u64)
313    }
314
315    /// Calls the native job with the specified [`Context`].
316    ///
317    /// # Note
318    ///
319    /// If the native job has an execution realm defined, this sets the running execution
320    /// context to the realm's before calling the inner closure, and resets it after execution.
321    pub fn call(mut self, context: &mut Context) -> JsResult<JsValue> {
322        let result = self
323            .job
324            .take()
325            .map_or_else(|| Ok(JsValue::undefined()), |job| job.call(context));
326        self.cancellation_token.revoke();
327        result
328    }
329
330    /// Returns the timeout value in milliseconds since epoch.
331    #[inline]
332    #[must_use]
333    pub fn timeout(&self) -> JsDuration {
334        self.timeout
335    }
336
337    /// Returns `true` if the timeout was cancelled, and its execution can be skipped.
338    #[inline]
339    #[must_use]
340    pub fn cancelled(&self) -> bool {
341        self.cancellation_token.revoked()
342    }
343
344    /// Returns the [`CancellationToken`] for this timeout job.
345    #[must_use]
346    pub fn cancellation_token(&self) -> &CancellationToken {
347        &self.cancellation_token
348    }
349}
350
351/// An ECMAScript [Job] that runs at a certain interval of time.
352///
353/// This represents jobs enqueued by APIs such as [`setInterval`].
354///
355/// [`setInterval`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/setInterval
356#[derive(Debug)]
357pub struct IntervalJob {
358    /// The distance in milliseconds in the future when the job should run.
359    /// This will be added to the current time when the job is enqueued.
360    interval: JsDuration,
361    /// The job to run after every interval of time.
362    job: NativeJobFn,
363    /// Signals if the timeout job was cancelled.
364    cancellation_token: CancellationToken,
365}
366
367impl Drop for IntervalJob {
368    fn drop(&mut self) {
369        self.cancellation_token.revoke();
370    }
371}
372
373impl IntervalJob {
374    /// Create a new `IntervalJob` with an interval and a job.
375    #[must_use]
376    pub fn new(job: NativeJobFn, interval_in_millis: u64) -> Self {
377        Self {
378            interval: JsDuration::from_millis(interval_in_millis),
379            job,
380            cancellation_token: CancellationToken::new(),
381        }
382    }
383
384    /// Creates a new `IntervalJob` from a closure and an interval as [`std::time::Duration`].
385    #[must_use]
386    pub fn from_duration<F>(f: F, interval: impl Into<JsDuration>) -> Self
387    where
388        F: Fn(&mut Context) -> JsResult<JsValue> + 'static,
389    {
390        Self::new(NativeJobFn::new(f), interval.into().as_millis())
391    }
392
393    /// Creates a new `TimeoutJob` from a closure, an interval, and an execution realm.
394    #[must_use]
395    pub fn with_realm<F>(f: F, realm: Realm, interval: time::Duration) -> Self
396    where
397        F: Fn(&mut Context) -> JsResult<JsValue> + 'static,
398    {
399        Self::new(
400            NativeJobFn::with_realm(f, realm),
401            interval.as_millis() as u64,
402        )
403    }
404
405    /// Calls the interval job with the specified [`Context`].
406    ///
407    /// # Note
408    ///
409    /// If the interval job has an execution realm defined, this sets the running execution
410    /// context to the realm's before calling the inner closure, and resets it after execution.
411    pub fn call(&self, context: &mut Context) -> JsResult<JsValue> {
412        self.job.call(context)
413    }
414
415    /// Returns the interval value in milliseconds.
416    #[inline]
417    #[must_use]
418    pub fn interval(&self) -> JsDuration {
419        self.interval
420    }
421
422    /// Returns `true` if the interval job was cancelled, and its execution can be skipped.
423    #[inline]
424    #[must_use]
425    pub fn cancelled(&self) -> bool {
426        self.cancellation_token.revoked()
427    }
428
429    /// Returns the [`CancellationToken`] for this interval job.
430    #[must_use]
431    pub fn cancellation_token(&self) -> &CancellationToken {
432        &self.cancellation_token
433    }
434}
435
436/// An ECMAScript Generic [Job].
437///
438/// This represents the [HostEnqueueGenericJob] operation from the specification, which
439/// enqueues a job that is just like a [`PromiseJob`], but unconstrained in relation
440/// to priority and ordering.
441///
442/// [HostEnqueueGenericJob]: https://tc39.es/ecma262/#sec-hostenqueuegenericjob
443pub struct GenericJob(NativeJob);
444
445impl Debug for GenericJob {
446    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
447        f.debug_struct("GenericJob").finish_non_exhaustive()
448    }
449}
450
451impl GenericJob {
452    /// Creates a new `GenericJob` from a closure and an execution realm.
453    pub fn new<F>(f: F, realm: Realm) -> Self
454    where
455        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
456    {
457        Self(NativeJob::with_realm(f, realm))
458    }
459
460    /// Gets a reference to the execution realm of the job.
461    #[must_use]
462    pub const fn realm(&self) -> &Realm {
463        self.0
464            .realm
465            .as_ref()
466            .expect("all generic jobs must have an execution realm")
467    }
468
469    /// Calls the `GenericJob` with the specified [`Context`], setting the execution
470    /// context to the job's realm before calling the inner closure, and resets it after execution.
471    pub fn call(self, context: &mut Context) -> JsResult<JsValue> {
472        self.0.call(context)
473    }
474}
475
476/// The [`Future`] job returned by a [`NativeAsyncJob`] operation.
477pub type BoxedFuture<'a> = Pin<Box<dyn Future<Output = JsResult<JsValue>> + 'a>>;
478
479/// An ECMAScript [Job] that can be run asynchronously.
480///
481/// This is an additional type of job that is not defined by the specification, enabling running `Future` tasks
482/// created by ECMAScript code in an easier way.
483#[allow(clippy::type_complexity)]
484pub struct NativeAsyncJob {
485    f: Box<dyn for<'a> FnOnce(&'a RefCell<&mut Context>) -> BoxedFuture<'a>>,
486    realm: Option<Realm>,
487}
488
489impl Debug for NativeAsyncJob {
490    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491        f.debug_struct("NativeAsyncJob")
492            .field("f", &"Closure")
493            .finish()
494    }
495}
496
497impl NativeAsyncJob {
498    /// Creates a new `NativeAsyncJob` from an async closure.
499    pub fn new<F>(f: F) -> Self
500    where
501        F: AsyncFnOnce(&RefCell<&mut Context>) -> JsResult<JsValue> + 'static,
502    {
503        Self {
504            f: Box::new(move |ctx| Box::pin(async move { f(ctx).await })),
505            realm: None,
506        }
507    }
508
509    /// Creates a new `NativeAsyncJob` from an async closure and an execution realm.
510    pub fn with_realm<F>(f: F, realm: Realm) -> Self
511    where
512        F: AsyncFnOnce(&RefCell<&mut Context>) -> JsResult<JsValue> + 'static,
513    {
514        Self {
515            f: Box::new(move |ctx| Box::pin(async move { f(ctx).await })),
516            realm: Some(realm),
517        }
518    }
519
520    /// Gets a reference to the execution realm of the job.
521    #[must_use]
522    pub const fn realm(&self) -> Option<&Realm> {
523        self.realm.as_ref()
524    }
525
526    /// Calls the native async job with the specified [`Context`].
527    ///
528    /// # Note
529    ///
530    /// If the native async job has an execution realm defined, this sets the running execution
531    /// context to the realm's before calling the inner closure, and resets it after execution.
532    pub fn call<'a, 'b>(
533        self,
534        context: &'a RefCell<&'b mut Context>,
535        // We can make our users assume `Unpin` because `self.f` is already boxed, so we shouldn't
536        // need pin at all.
537    ) -> impl Future<Output = JsResult<JsValue>> + Unpin + use<'a, 'b> {
538        // If realm is not null, each time job is invoked the implementation must perform
539        // implementation-defined steps such that execution is prepared to evaluate ECMAScript
540        // code at the time of job's invocation.
541        let realm = self.realm;
542
543        let mut future = if let Some(realm) = &realm {
544            let old_realm = context.borrow_mut().enter_realm(realm.clone());
545
546            // Let scriptOrModule be GetActiveScriptOrModule() at the time HostEnqueuePromiseJob is
547            // invoked. If realm is not null, each time job is invoked the implementation must
548            // perform implementation-defined steps such that scriptOrModule is the active script or
549            // module at the time of job's invocation.
550            let result = (self.f)(context);
551
552            context.borrow_mut().enter_realm(old_realm);
553            result
554        } else {
555            (self.f)(context)
556        };
557
558        std::future::poll_fn(move |cx| {
559            // We need to do the same dance again since the inner code could assume we're still
560            // on the same realm.
561            if let Some(realm) = &realm {
562                let old_realm = context.borrow_mut().enter_realm(realm.clone());
563
564                let poll_result = future.as_mut().poll(cx);
565
566                context.borrow_mut().enter_realm(old_realm);
567                poll_result
568            } else {
569                future.as_mut().poll(cx)
570            }
571        })
572    }
573}
574
575/// An ECMAScript [Job Abstract Closure] executing code related to [`Promise`] objects.
576///
577/// This represents the [`HostEnqueuePromiseJob`] operation from the specification.
578///
579/// ### [Requirements]
580///
581/// - If realm is not null, each time job is invoked the implementation must perform implementation-defined
582///   steps such that execution is prepared to evaluate ECMAScript code at the time of job's invocation.
583/// - Let `scriptOrModule` be [`GetActiveScriptOrModule()`] at the time `HostEnqueuePromiseJob` is invoked.
584///   If realm is not null, each time job is invoked the implementation must perform implementation-defined steps
585///   such that `scriptOrModule` is the active script or module at the time of job's invocation.
586/// - Jobs must run in the same order as the `HostEnqueuePromiseJob` invocations that scheduled them.
587///
588/// Of all the requirements, Boa guarantees the first two by its internal implementation of `NativeJob`, meaning
589/// implementations of [`JobExecutor`] must only guarantee that jobs are run in the same order as they're enqueued.
590///
591/// [`Promise`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
592/// [`HostEnqueuePromiseJob`]: https://tc39.es/ecma262/#sec-hostenqueuepromisejob
593/// [Job Abstract Closure]: https://tc39.es/ecma262/#sec-jobs
594/// [Requirements]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-hostenqueuepromisejob
595/// [`GetActiveScriptOrModule()`]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-getactivescriptormodule
596pub struct PromiseJob(NativeJob);
597
598impl Debug for PromiseJob {
599    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
600        f.debug_struct("PromiseJob").finish_non_exhaustive()
601    }
602}
603
604impl PromiseJob {
605    /// Creates a new `PromiseJob` from a closure.
606    pub fn new<F>(f: F) -> Self
607    where
608        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
609    {
610        Self(NativeJob::new(f))
611    }
612
613    /// Creates a new `PromiseJob` from a closure and an execution realm.
614    pub fn with_realm<F>(f: F, realm: Realm) -> Self
615    where
616        F: FnOnce(&mut Context) -> JsResult<JsValue> + 'static,
617    {
618        Self(NativeJob::with_realm(f, realm))
619    }
620
621    /// Gets a reference to the execution realm of the `PromiseJob`.
622    #[must_use]
623    pub const fn realm(&self) -> Option<&Realm> {
624        self.0.realm()
625    }
626
627    /// Calls the `PromiseJob` with the specified [`Context`].
628    ///
629    /// # Note
630    ///
631    /// If the job has an execution realm defined, this sets the running execution
632    /// context to the realm's before calling the inner closure, and resets it after execution.
633    pub fn call(self, context: &mut Context) -> JsResult<JsValue> {
634        self.0.call(context)
635    }
636}
637
638/// [`JobCallback`][spec] records.
639///
640/// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records
641#[derive(Trace, Finalize)]
642pub struct JobCallback {
643    callback: JsFunction,
644    host_defined: Box<dyn NativeObject>,
645}
646
647impl Debug for JobCallback {
648    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
649        f.debug_struct("JobCallback")
650            .field("callback", &self.callback)
651            .field("host_defined", &"dyn NativeObject")
652            .finish()
653    }
654}
655
656impl JobCallback {
657    /// Creates a new `JobCallback`.
658    #[inline]
659    pub fn new<T: NativeObject>(callback: JsFunction, host_defined: T) -> Self {
660        Self {
661            callback,
662            host_defined: Box::new(host_defined),
663        }
664    }
665
666    /// Gets the inner callback of the job.
667    #[inline]
668    #[must_use]
669    pub const fn callback(&self) -> &JsFunction {
670        &self.callback
671    }
672
673    /// Gets a reference to the host defined additional field as an [`NativeObject`] trait object.
674    #[inline]
675    #[must_use]
676    pub fn host_defined(&self) -> &dyn NativeObject {
677        &*self.host_defined
678    }
679
680    /// Gets a mutable reference to the host defined additional field as an [`NativeObject`] trait object.
681    #[inline]
682    pub fn host_defined_mut(&mut self) -> &mut dyn NativeObject {
683        &mut *self.host_defined
684    }
685}
686
687/// A job that needs to be handled by a [`JobExecutor`].
688///
689/// # Requirements
690///
691/// The specification defines many types of jobs, but all of them must adhere to a set of requirements:
692///
693/// - At some future point in time, when there is no running execution context and the execution
694///   context stack is empty, the implementation must:
695///     - Perform any host-defined preparation steps.
696///     - Invoke the Job Abstract Closure.
697///     - Perform any host-defined cleanup steps, after which the execution context stack must be empty.
698/// - Only one Job may be actively undergoing evaluation at any point in time.
699/// - Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts.
700/// - The Abstract Closure must return a normal completion, implementing its own handling of errors.
701///
702/// Boa is a little bit flexible on the last requirement, since it allows jobs to return either
703/// values or errors, but the rest of the requirements must be followed for all conformant implementations.
704///
705/// Additionally, each job type can have additional requirements that must also be followed in addition
706/// to the previous ones.
707#[non_exhaustive]
708#[derive(Debug)]
709pub enum Job {
710    /// A `Promise`-related job.
711    ///
712    /// See [`PromiseJob`] for more information.
713    PromiseJob(PromiseJob),
714    /// A [`Future`]-related job.
715    ///
716    /// See [`NativeAsyncJob`] for more information.
717    AsyncJob(NativeAsyncJob),
718    /// A generic job that is to be executed after a number of milliseconds.
719    ///
720    /// See [`TimeoutJob`] for more information.
721    TimeoutJob(TimeoutJob),
722    /// A generic job that is to be executed after intervals of a number of milliseconds.
723    ///
724    /// See [`TimeoutJob`] for more information.
725    IntervalJob(IntervalJob),
726    /// A generic job.
727    ///
728    /// See [`GenericJob`] for more information.
729    GenericJob(GenericJob),
730    /// A job that will eventually cleanup a `FinalizationRegistry`.
731    ///
732    /// This job differs slightly from the [spec]; originally it's defined
733    /// as being enqueued exactly when a `FinalizationRegistry` needs to call
734    /// `FinalizationRegistry::cleanup`, but here it's defined as an async
735    /// job that suspends execution until it receives a signal from the engine
736    /// that the `FinalizationRegistry` needs to be cleaned up.
737    ///
738    /// # Execution
739    ///
740    /// As described on the [spec's section about execution][execution],
741    ///
742    /// > Because calling `HostEnqueueFinalizationRegistryCleanupJob` is optional,
743    /// > registered objects in a `FinalizationRegistry` do not necessarily hold
744    /// > that `FinalizationRegistry` live. Implementations may omit `FinalizationRegistry`
745    /// > callbacks for any reason, e.g., if the `FinalizationRegistry` itself becomes
746    /// > dead, or if the application is shutting down.
747    ///
748    /// For this reason, it is recommended to exclude `FinalizationRegistry` cleanup
749    /// jobs from any condition that exits from [`JobExecutor::run_jobs`].
750    ///
751    /// By the same token, it is recommended to execute `FinalizationRegistry` cleanup
752    /// jobs separately from all other enqueued [`NativeAsyncJob`]s, prioritizing the
753    /// execution of all other jobs if possible.
754    ///
755    /// [spec]: https://tc39.es/ecma262/#sec-weakref-host-hooks
756    /// [execution]: https://tc39.es/ecma262/#sec-weakref-execution
757    FinalizationRegistryCleanupJob(NativeAsyncJob),
758}
759
760impl From<NativeAsyncJob> for Job {
761    fn from(native_async_job: NativeAsyncJob) -> Self {
762        Job::AsyncJob(native_async_job)
763    }
764}
765
766impl From<PromiseJob> for Job {
767    fn from(promise_job: PromiseJob) -> Self {
768        Job::PromiseJob(promise_job)
769    }
770}
771
772impl From<TimeoutJob> for Job {
773    fn from(job: TimeoutJob) -> Self {
774        Job::TimeoutJob(job)
775    }
776}
777
778impl From<IntervalJob> for Job {
779    fn from(job: IntervalJob) -> Self {
780        Job::IntervalJob(job)
781    }
782}
783
784impl From<GenericJob> for Job {
785    fn from(job: GenericJob) -> Self {
786        Job::GenericJob(job)
787    }
788}
789
790/// An executor of `ECMAscript` [Jobs].
791///
792/// This is the main API that allows creating custom event loops.
793///
794/// [Jobs]: https://tc39.es/ecma262/#sec-jobs
795pub trait JobExecutor: Any {
796    /// Enqueues a `Job` on the executor.
797    ///
798    /// This method combines all the host-defined job enqueueing operations into a single method.
799    /// See the [spec] for more information on the requirements that each operation must follow.
800    ///
801    /// [spec]: https://tc39.es/ecma262/#sec-jobs
802    fn enqueue_job(self: Rc<Self>, job: Job, context: &mut Context);
803
804    /// Runs all jobs in the executor.
805    fn run_jobs(self: Rc<Self>, context: &mut Context) -> JsResult<()>;
806
807    /// Asynchronously runs all jobs in the executor.
808    ///
809    /// By default forwards to [`JobExecutor::run_jobs`]. Implementors using async should override this
810    /// with a proper algorithm to run jobs asynchronously.
811    #[expect(async_fn_in_trait, reason = "all our APIs are single-threaded")]
812    async fn run_jobs_async(self: Rc<Self>, context: &RefCell<&mut Context>) -> JsResult<()>
813    where
814        Self: Sized,
815    {
816        self.run_jobs(&mut context.borrow_mut())
817    }
818}
819
820/// A job executor that does nothing.
821///
822/// This executor is mostly useful if you want to disable the promise capabilities of the engine. This
823/// can be done by passing it to the [`ContextBuilder`]:
824///
825/// ```
826/// use boa_engine::{
827///     context::ContextBuilder,
828///     job::{IdleJobExecutor, JobExecutor},
829/// };
830/// use std::rc::Rc;
831///
832/// let executor = Rc::new(IdleJobExecutor);
833/// let context = ContextBuilder::new().job_executor(executor).build();
834/// ```
835///
836/// [`ContextBuilder`]: crate::context::ContextBuilder
837#[derive(Debug, Clone, Copy)]
838pub struct IdleJobExecutor;
839
840impl JobExecutor for IdleJobExecutor {
841    fn enqueue_job(self: Rc<Self>, _: Job, _: &mut Context) {}
842
843    fn run_jobs(self: Rc<Self>, _: &mut Context) -> JsResult<()> {
844        Ok(())
845    }
846}
847
848#[derive(Debug)]
849enum ClockJob {
850    Timeout(TimeoutJob),
851    Interval(IntervalJob),
852}
853
854impl ClockJob {
855    fn cancelled(&self) -> bool {
856        match self {
857            ClockJob::Timeout(t) => t.cancelled(),
858            ClockJob::Interval(i) => i.cancelled(),
859        }
860    }
861}
862
863/// A simple FIFO executor that bails on the first error.
864///
865/// This is the default job executor for the [`Context`], but it is mostly pretty limited
866/// for a custom event loop.
867///
868/// To disable running promise jobs on the engine, see [`IdleJobExecutor`].
869#[derive(Default)]
870pub struct SimpleJobExecutor {
871    promise_jobs: RefCell<VecDeque<PromiseJob>>,
872    async_jobs: RefCell<VecDeque<NativeAsyncJob>>,
873    finalization_registry_jobs: RefCell<VecDeque<NativeAsyncJob>>,
874    clock_jobs: RefCell<BTreeMap<JsInstant, VecDeque<ClockJob>>>,
875    generic_jobs: RefCell<VecDeque<GenericJob>>,
876    stop: Arc<AtomicBool>,
877    deadline: Cell<Option<time::Instant>>,
878    executed_jobs: Cell<u64>,
879}
880
881impl SimpleJobExecutor {
882    fn clear(&self) {
883        self.promise_jobs.borrow_mut().clear();
884        self.async_jobs.borrow_mut().clear();
885        self.clock_jobs.borrow_mut().clear();
886        self.generic_jobs.borrow_mut().clear();
887    }
888}
889
890impl Debug for SimpleJobExecutor {
891    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
892        f.debug_struct("SimpleJobExecutor").finish_non_exhaustive()
893    }
894}
895
896impl SimpleJobExecutor {
897    /// Creates a new `SimpleJobExecutor`.
898    #[must_use]
899    pub fn new() -> Self {
900        Self::default()
901    }
902
903    /// Gets the cancellation token for this executor.
904    ///
905    /// Setting the signal to `true` will exit the inner event loop and
906    /// stop executing any pending jobs.
907    pub fn get_cancellation_token(&self) -> Arc<AtomicBool> {
908        Arc::clone(&self.stop)
909    }
910
911    /// Runs jobs until the budget expires between jobs.
912    ///
913    /// Unstarted jobs remain queued in their original order. A single job
914    /// cannot be preempted. Native async jobs already being polled must finish
915    /// before this call returns: their futures borrow the context, so dropping
916    /// them at a slice boundary would cancel work. Promise jobs continue to
917    /// run while those futures are pending, retaining the ordinary executor's
918    /// ability to resolve promises awaited by native async jobs.
919    ///
920    /// Calling `run_jobs` without this method retains the unbounded drain.
921    pub fn run_jobs_with_budget(
922        self: Rc<Self>,
923        context: &mut Context,
924        budget: time::Duration,
925    ) -> JsResult<()> {
926        let deadline = time::Instant::now() + budget;
927        let previous = self.deadline.get();
928        self.deadline
929            .set(Some(previous.map_or(deadline, |outer| outer.min(deadline))));
930        let result = Rc::clone(&self).run_jobs(context);
931        self.deadline.set(previous);
932        result
933    }
934
935    /// Whether jobs can make progress immediately.
936    ///
937    /// Future clock jobs and finalization cleanup alone do not count.
938    #[must_use]
939    pub fn has_pending_jobs(&self, context: &Context) -> bool {
940        !self.promise_jobs.borrow().is_empty()
941            || !self.async_jobs.borrow().is_empty()
942            || !self.generic_jobs.borrow().is_empty()
943            || self
944                .clock_jobs
945                .borrow()
946                .range(..=context.clock().now())
947                .any(|(_, jobs)| jobs.iter().any(|job| !job.cancelled()))
948    }
949
950    /// Time until the next uncancelled timeout or interval.
951    #[must_use]
952    pub fn next_job_timeout(&self, context: &Context) -> Option<time::Duration> {
953        let now = context.clock().now();
954        self.clock_jobs
955            .borrow()
956            .iter()
957            .find(|(_, jobs)| jobs.iter().any(|job| !job.cancelled()))
958            .map(|(deadline, _)| (*deadline - now).into())
959    }
960
961    /// Number of jobs started, including native async jobs.
962    #[must_use]
963    pub fn executed_job_count(&self) -> u64 {
964        self.executed_jobs.get()
965    }
966
967    fn record_job(&self) {
968        self.executed_jobs
969            .set(self.executed_jobs.get().wrapping_add(1));
970    }
971
972    fn budget_exhausted(&self) -> bool {
973        self.deadline
974            .get()
975            .is_some_and(|deadline| time::Instant::now() >= deadline)
976    }
977
978    pub(crate) fn is_empty(&self) -> bool {
979        self.promise_jobs.borrow().is_empty()
980            && self.async_jobs.borrow().is_empty()
981            && self.generic_jobs.borrow().is_empty()
982            && self.clock_jobs.borrow().is_empty()
983    }
984}
985
986impl JobExecutor for SimpleJobExecutor {
987    fn enqueue_job(self: Rc<Self>, job: Job, context: &mut Context) {
988        match job {
989            Job::PromiseJob(p) => self.promise_jobs.borrow_mut().push_back(p),
990            Job::AsyncJob(a) => self.async_jobs.borrow_mut().push_back(a),
991            Job::TimeoutJob(t) => {
992                let now = context.clock().now();
993                self.clock_jobs
994                    .borrow_mut()
995                    .entry(now + t.timeout())
996                    .or_default()
997                    .push_back(ClockJob::Timeout(t));
998            }
999            Job::IntervalJob(i) => {
1000                let now = context.clock().now();
1001                self.clock_jobs
1002                    .borrow_mut()
1003                    .entry(now + i.interval())
1004                    .or_default()
1005                    .push_back(ClockJob::Interval(i));
1006            }
1007            Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g),
1008            Job::FinalizationRegistryCleanupJob(fr) => {
1009                self.finalization_registry_jobs.borrow_mut().push_back(fr);
1010            }
1011        }
1012    }
1013
1014    fn run_jobs(self: Rc<Self>, context: &mut Context) -> JsResult<()> {
1015        future::block_on(self.run_jobs_async(&RefCell::new(context)))
1016    }
1017
1018    async fn run_jobs_async(self: Rc<Self>, context: &RefCell<&mut Context>) -> JsResult<()>
1019    where
1020        Self: Sized,
1021    {
1022        let mut group = FutureGroup::new();
1023        let mut fr_group = FutureGroup::new();
1024        loop {
1025            if self.stop.load(Ordering::Relaxed) {
1026                self.stop.store(false, Ordering::Relaxed);
1027                self.clear();
1028                return Ok(());
1029            }
1030
1031            if group.is_empty() && self.budget_exhausted() {
1032                break;
1033            }
1034
1035            for job in mem::take(&mut *self.async_jobs.borrow_mut()) {
1036                self.record_job();
1037                group.insert(job.call(context));
1038            }
1039
1040            for job in mem::take(&mut *self.finalization_registry_jobs.borrow_mut()) {
1041                fr_group.insert(job.call(context));
1042            }
1043
1044            // Snapshot past-due jobs so a zero-length interval is rescheduled
1045            // for a later round, as in the unbounded executor.
1046            {
1047                let now = context.borrow().clock().now();
1048                let mut jobs_to_run = {
1049                    let mut timeout_jobs = self.clock_jobs.borrow_mut();
1050                    let mut jobs_to_keep = timeout_jobs.split_off(&now);
1051                    jobs_to_keep.retain(|_, jobs| {
1052                        jobs.retain(|job| !job.cancelled());
1053                        !jobs.is_empty()
1054                    });
1055                    mem::replace(&mut *timeout_jobs, jobs_to_keep)
1056                };
1057
1058                while let Some((deadline, mut jobs)) = jobs_to_run.pop_first() {
1059                    while !jobs.is_empty() {
1060                        if group.is_empty() && self.budget_exhausted() {
1061                            jobs_to_run.insert(deadline, jobs);
1062                            let mut queued = self.clock_jobs.borrow_mut();
1063                            for (deadline, mut remaining) in jobs_to_run {
1064                                if let Some(mut newer) = queued.remove(&deadline) {
1065                                    remaining.append(&mut newer);
1066                                }
1067                                queued.insert(deadline, remaining);
1068                            }
1069                            return Ok(());
1070                        }
1071                        let job = jobs.pop_front().expect("the clock batch is not empty");
1072                        if !job.cancelled() {
1073                            self.record_job();
1074                            match job {
1075                                ClockJob::Timeout(job) => {
1076                                    if let Err(err) = job.call(&mut context.borrow_mut()) {
1077                                        self.clear();
1078                                        return Err(err);
1079                                    }
1080                                }
1081                                ClockJob::Interval(job) => {
1082                                    let context = &mut context.borrow_mut();
1083                                    let now = context.clock().now();
1084                                    if let Err(err) = job.call(context) {
1085                                        self.clear();
1086                                        return Err(err);
1087                                    }
1088                                    self.clock_jobs
1089                                        .borrow_mut()
1090                                        .entry(now + job.interval())
1091                                        .or_default()
1092                                        .push_back(ClockJob::Interval(job));
1093                                }
1094                            }
1095                        }
1096                    }
1097                }
1098            }
1099
1100            if self.is_empty() && group.is_empty() {
1101                match future::poll_once(fr_group.next()).await.flatten() {
1102                    Some(Err(err)) => {
1103                        self.clear();
1104                        return Err(err);
1105                    }
1106                    _ if !self.is_empty() => {}
1107                    _ => break,
1108                }
1109            }
1110
1111            if let Some(Err(err)) = future::poll_once(group.next()).await.flatten() {
1112                self.clear();
1113                return Err(err);
1114            }
1115
1116            let mut jobs = mem::take(&mut *self.promise_jobs.borrow_mut());
1117            while !jobs.is_empty() {
1118                if group.is_empty() && self.budget_exhausted() {
1119                    let mut queued = self.promise_jobs.borrow_mut();
1120                    // The unfinished batch precedes jobs enqueued by the
1121                    // callbacks that already ran. Moving preserves FIFO and
1122                    // keeps every captured value rooted.
1123                    jobs.append(&mut *queued);
1124                    *queued = jobs;
1125                    return Ok(());
1126                }
1127                let job = jobs.pop_front().expect("the promise batch is not empty");
1128                self.record_job();
1129                if let Err(err) = job.call(&mut context.borrow_mut()) {
1130                    self.clear();
1131                    return Err(err);
1132                }
1133            }
1134
1135            let mut jobs = mem::take(&mut *self.generic_jobs.borrow_mut());
1136            while !jobs.is_empty() {
1137                if group.is_empty() && self.budget_exhausted() {
1138                    let mut queued = self.generic_jobs.borrow_mut();
1139                    jobs.append(&mut *queued);
1140                    *queued = jobs;
1141                    return Ok(());
1142                }
1143                let job = jobs.pop_front().expect("the generic batch is not empty");
1144                self.record_job();
1145                if let Err(err) = job.call(&mut context.borrow_mut()) {
1146                    self.clear();
1147                    return Err(err);
1148                }
1149            }
1150            context.borrow_mut().clear_kept_objects();
1151            future::yield_now().await;
1152        }
1153
1154        Ok(())
1155    }
1156}