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}