use boa_engine::class::Class;
use boa_engine::job::GenericJob;
use boa_engine::object::builtins::JsFunction;
use boa_engine::realm::Realm;
use boa_engine::{
Context, Finalize, JsData, JsError, JsNativeError, JsObject, JsResult, JsString, JsValue,
Trace, boa_class, boa_module, js_error, js_string,
};
use boa_gc::GcRefCell;
use std::cell::Cell;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct CancellationToken(Arc<AtomicBool>);
impl CancellationToken {
fn new() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
pub fn cancel(&self) {
self.0.store(true, Ordering::Release);
}
#[must_use]
pub fn is_cancelled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
}
fn make_abort_error(context: &mut Context) -> JsValue {
let obj = JsNativeError::error()
.with_message("signal is aborted without reason")
.into_opaque(context);
obj.set(js_string!("name"), js_string!("AbortError"), false, context)
.ok();
obj.into()
}
#[derive(Debug, Clone, JsData, Trace, Finalize)]
pub struct JsAbortSignal {
#[unsafe_ignore_trace]
aborted: Cell<bool>,
reason: GcRefCell<Option<JsValue>>,
listeners: GcRefCell<Vec<AbortEventListener>>,
#[unsafe_ignore_trace]
cancel_token: CancellationToken,
}
#[derive(Debug, Clone, Trace, Finalize)]
struct AbortEventListener {
event_type: JsString,
callback: JsFunction,
}
impl Default for JsAbortSignal {
fn default() -> Self {
Self {
aborted: Cell::new(false),
reason: GcRefCell::default(),
listeners: GcRefCell::default(),
cancel_token: CancellationToken::new(),
}
}
}
impl JsAbortSignal {
pub fn signal_abort(&self, reason: JsValue, context: &mut Context) -> JsResult<()> {
if self.aborted.get() {
return Ok(());
}
self.aborted.set(true);
*self.reason.borrow_mut() = Some(reason);
let abort = js_string!("abort");
let listeners: Vec<JsFunction> = self
.listeners
.borrow()
.iter()
.filter(|listener| listener.event_type == abort)
.map(|listener| listener.callback.clone())
.collect();
let realm = context.realm().clone();
for listener in listeners {
context.enqueue_job(
GenericJob::new(
move |context| {
listener.call(&JsValue::undefined(), &[], context)?;
Ok(JsValue::undefined())
},
realm.clone(),
)
.into(),
);
}
self.cancel_token.cancel();
Ok(())
}
#[must_use]
pub fn is_aborted(&self) -> bool {
self.aborted.get()
}
pub fn abort_reason(&self, context: &mut Context) -> JsValue {
if !self.aborted.get() {
return JsValue::undefined();
}
self.reason
.borrow()
.clone()
.unwrap_or_else(|| make_abort_error(context))
}
#[must_use]
pub fn cancellation_token(&self) -> CancellationToken {
self.cancel_token.clone()
}
}
#[boa_class(rename = "AbortSignal")]
#[boa(rename_all = "camelCase")]
impl JsAbortSignal {
#[boa(constructor)]
fn constructor() -> JsResult<Self> {
Err(JsNativeError::typ()
.with_message("Illegal constructor")
.into())
}
#[boa(getter)]
fn aborted(&self) -> bool {
self.aborted.get()
}
#[boa(getter)]
fn reason(&self, context: &mut Context) -> JsValue {
self.abort_reason(context)
}
fn throw_if_aborted(&self, context: &mut Context) -> JsResult<()> {
if self.aborted.get() {
Err(JsError::from_opaque(self.abort_reason(context)))
} else {
Ok(())
}
}
fn add_event_listener(
&self,
event_type: JsString,
callback: JsFunction,
_context: &mut Context,
) {
{
let listeners = self.listeners.borrow();
if listeners.iter().any(|listener| {
listener.event_type == event_type && JsObject::equals(&listener.callback, &callback)
}) {
return;
}
}
self.listeners.borrow_mut().push(AbortEventListener {
event_type,
callback,
});
}
fn remove_event_listener(&self, event_type: JsString, callback: JsFunction) {
self.listeners.borrow_mut().retain(|listener| {
listener.event_type != event_type || !JsObject::equals(&listener.callback, &callback)
});
}
}
#[derive(Debug, Clone, JsData, Trace, Finalize)]
pub struct JsAbortController {
signal: JsObject,
}
#[boa_class(rename = "AbortController")]
#[boa(rename_all = "camelCase")]
impl JsAbortController {
#[boa(constructor)]
fn constructor(context: &mut Context) -> JsResult<Self> {
let signal_obj = Class::from_data(JsAbortSignal::default(), context)?;
Ok(Self { signal: signal_obj })
}
#[boa(getter)]
fn signal(&self) -> JsObject {
self.signal.clone()
}
fn abort(&self, reason: Option<JsValue>, context: &mut Context) -> JsResult<()> {
let abort_reason = reason.unwrap_or_else(|| make_abort_error(context));
let Some(signal) = self.signal.downcast_ref::<JsAbortSignal>() else {
return Err(js_error!(TypeError: "AbortController: invalid signal object"));
};
signal.signal_abort(abort_reason, context)
}
}
#[boa_module]
pub mod js_module {
type JsAbortController = super::JsAbortController;
type JsAbortSignal = super::JsAbortSignal;
}
pub fn register(realm: Option<Realm>, context: &mut Context) -> JsResult<()> {
js_module::boa_register(realm, context)
}