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}