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, Vec<ClockJob>>>,
875    generic_jobs: RefCell<VecDeque<GenericJob>>,
876    stop: Arc<AtomicBool>,
877}
878
879impl SimpleJobExecutor {
880    fn clear(&self) {
881        self.promise_jobs.borrow_mut().clear();
882        self.async_jobs.borrow_mut().clear();
883        self.clock_jobs.borrow_mut().clear();
884        self.generic_jobs.borrow_mut().clear();
885    }
886}
887
888impl Debug for SimpleJobExecutor {
889    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890        f.debug_struct("SimpleJobExecutor").finish_non_exhaustive()
891    }
892}
893
894impl SimpleJobExecutor {
895    /// Creates a new `SimpleJobExecutor`.
896    #[must_use]
897    pub fn new() -> Self {
898        Self::default()
899    }
900
901    /// Gets the cancellation token for this executor.
902    ///
903    /// Setting the signal to `true` will exit the inner event loop and
904    /// stop executing any pending jobs.
905    pub fn get_cancellation_token(&self) -> Arc<AtomicBool> {
906        Arc::clone(&self.stop)
907    }
908
909    pub(crate) fn is_empty(&self) -> bool {
910        self.promise_jobs.borrow().is_empty()
911            && self.async_jobs.borrow().is_empty()
912            && self.generic_jobs.borrow().is_empty()
913            && self.clock_jobs.borrow().is_empty()
914    }
915}
916
917impl JobExecutor for SimpleJobExecutor {
918    fn enqueue_job(self: Rc<Self>, job: Job, context: &mut Context) {
919        match job {
920            Job::PromiseJob(p) => self.promise_jobs.borrow_mut().push_back(p),
921            Job::AsyncJob(a) => self.async_jobs.borrow_mut().push_back(a),
922            Job::TimeoutJob(t) => {
923                let now = context.clock().now();
924                self.clock_jobs
925                    .borrow_mut()
926                    .entry(now + t.timeout())
927                    .or_default()
928                    .push(ClockJob::Timeout(t));
929            }
930            Job::IntervalJob(i) => {
931                let now = context.clock().now();
932                self.clock_jobs
933                    .borrow_mut()
934                    .entry(now + i.interval())
935                    .or_default()
936                    .push(ClockJob::Interval(i));
937            }
938            Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g),
939            Job::FinalizationRegistryCleanupJob(fr) => {
940                self.finalization_registry_jobs.borrow_mut().push_back(fr);
941            }
942        }
943    }
944
945    fn run_jobs(self: Rc<Self>, context: &mut Context) -> JsResult<()> {
946        future::block_on(self.run_jobs_async(&RefCell::new(context)))
947    }
948
949    async fn run_jobs_async(self: Rc<Self>, context: &RefCell<&mut Context>) -> JsResult<()>
950    where
951        Self: Sized,
952    {
953        let mut group = FutureGroup::new();
954        let mut fr_group = FutureGroup::new();
955        loop {
956            if self.stop.load(Ordering::Relaxed) {
957                self.stop.store(false, Ordering::Relaxed);
958                self.clear();
959                return Ok(());
960            }
961
962            for job in mem::take(&mut *self.async_jobs.borrow_mut()) {
963                group.insert(job.call(context));
964            }
965
966            for job in mem::take(&mut *self.finalization_registry_jobs.borrow_mut()) {
967                fr_group.insert(job.call(context));
968            }
969
970            // Dispatch all past-due timeout jobs before the termination check.
971            {
972                let now = context.borrow().clock().now();
973                let jobs_to_run = {
974                    let mut timeout_jobs = self.clock_jobs.borrow_mut();
975                    let mut jobs_to_keep = timeout_jobs.split_off(&now);
976                    jobs_to_keep.retain(|_, jobs| {
977                        jobs.retain(|job| !job.cancelled());
978                        !jobs.is_empty()
979                    });
980                    mem::replace(&mut *timeout_jobs, jobs_to_keep)
981                };
982
983                for jobs in jobs_to_run.into_values() {
984                    for job in jobs {
985                        if !job.cancelled() {
986                            match job {
987                                ClockJob::Timeout(job) => {
988                                    if let Err(err) = job.call(&mut context.borrow_mut()) {
989                                        self.clear();
990                                        return Err(err);
991                                    }
992                                }
993                                ClockJob::Interval(job) => {
994                                    let context = &mut context.borrow_mut();
995                                    let now = context.clock().now();
996                                    if let Err(err) = job.call(context) {
997                                        self.clear();
998                                        return Err(err);
999                                    }
1000                                    self.clock_jobs
1001                                        .borrow_mut()
1002                                        .entry(now + job.interval())
1003                                        .or_default()
1004                                        .push(ClockJob::Interval(job));
1005                                }
1006                            }
1007                        }
1008                    }
1009                }
1010            }
1011
1012            if self.is_empty() && group.is_empty() {
1013                match future::poll_once(fr_group.next()).await.flatten() {
1014                    Some(Err(err)) => {
1015                        self.clear();
1016                        return Err(err);
1017                    }
1018                    _ if !self.is_empty() => {}
1019                    _ => break,
1020                }
1021            }
1022
1023            if let Some(Err(err)) = future::poll_once(group.next()).await.flatten() {
1024                self.clear();
1025                return Err(err);
1026            }
1027
1028            let jobs = mem::take(&mut *self.promise_jobs.borrow_mut());
1029            for job in jobs {
1030                if let Err(err) = job.call(&mut context.borrow_mut()) {
1031                    self.clear();
1032                    return Err(err);
1033                }
1034            }
1035
1036            let jobs = mem::take(&mut *self.generic_jobs.borrow_mut());
1037            for job in jobs {
1038                if let Err(err) = job.call(&mut context.borrow_mut()) {
1039                    self.clear();
1040                    return Err(err);
1041                }
1042            }
1043            context.borrow_mut().clear_kept_objects();
1044            future::yield_now().await;
1045        }
1046
1047        Ok(())
1048    }
1049}