use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use rquickjs::class::Trace;
use rquickjs::function::Opt;
use rquickjs::{Class, Ctx, Function, Object, Value};
pub struct AbortInner {
aborted: AtomicBool,
notify: tokio::sync::Notify,
message: std::sync::Mutex<Option<String>>,
}
impl AbortInner {
fn new() -> Arc<Self> {
Arc::new(Self {
aborted: AtomicBool::new(false),
notify: tokio::sync::Notify::new(),
message: std::sync::Mutex::new(None),
})
}
pub fn is_aborted(&self) -> bool {
self.aborted.load(Ordering::Acquire)
}
pub fn reason_message(&self) -> String {
self
.message
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_else(|| "This operation was aborted".to_string())
}
fn mark(&self, message: Option<String>) {
*self.message.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = message;
self.aborted.store(true, Ordering::Release);
self.notify.notify_waiters();
}
pub async fn aborted(&self) {
self.notify.notified().await;
}
}
#[derive(Trace)]
#[rquickjs::class(rename = "AbortSignal")]
pub struct AbortSignalJs<'js> {
#[qjs(skip_trace)]
inner: Arc<AbortInner>,
#[qjs(skip_trace)]
aborted: bool,
reason: Option<Value<'js>>,
listeners: Vec<Function<'js>>,
onabort: Option<Function<'js>>,
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for AbortSignalJs<'js> {
type Changed<'to> = AbortSignalJs<'to>;
}
impl<'js> AbortSignalJs<'js> {
fn fresh() -> Self {
Self {
inner: AbortInner::new(),
aborted: false,
reason: None,
listeners: Vec::new(),
onabort: None,
}
}
pub fn inner_of(signal: &Class<'js, AbortSignalJs<'js>>) -> Arc<AbortInner> {
signal.borrow().inner.clone()
}
fn default_reason(ctx: &Ctx<'js>, name: &str, message: &str) -> rquickjs::Result<Value<'js>> {
let o = Object::new(ctx.clone())?;
o.set("name", name)?;
o.set("message", message)?;
Ok(o.into_value())
}
fn reason_to_message(reason: Option<&Value<'js>>) -> Option<String> {
let r = reason?;
if let Some(s) = r.as_string().and_then(|s| s.to_string().ok()) {
return Some(s);
}
r.as_object()
.and_then(|o| o.get::<_, String>("message").ok())
.or(Some("This operation was aborted".to_string()))
}
fn run_abort(this: &Class<'js, AbortSignalJs<'js>>, reason: Value<'js>) {
{
let mut b = this.borrow_mut();
if b.aborted {
return;
}
b.aborted = true;
b.reason = Some(reason.clone());
b.inner.mark(Self::reason_to_message(Some(&reason)));
}
let (onabort, listeners) = {
let b = this.borrow();
(b.onabort.clone(), b.listeners.clone())
};
if let Some(cb) = onabort {
let _ = cb.call::<_, ()>((reason.clone(),));
}
for cb in listeners {
let _ = cb.call::<_, ()>((reason.clone(),));
}
}
}
#[rquickjs::methods(rename_all = "camelCase")]
impl<'js> AbortSignalJs<'js> {
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
Err(rquickjs::Exception::throw_type(&ctx, "Illegal constructor"))
}
#[qjs(get)]
pub fn aborted(&self) -> bool {
self.aborted
}
#[qjs(get)]
pub fn reason(&self) -> Option<Value<'js>> {
self.reason.clone()
}
#[qjs(rename = "throwIfAborted")]
pub fn throw_if_aborted(&self, ctx: Ctx<'js>) -> rquickjs::Result<()> {
if self.aborted {
let r = self.reason.clone().unwrap_or_else(|| Value::new_undefined(ctx.clone()));
return Err(ctx.throw(r));
}
Ok(())
}
#[qjs(get, rename = "onabort")]
pub fn get_onabort(&self) -> Option<Function<'js>> {
self.onabort.clone()
}
#[qjs(set, rename = "onabort")]
pub fn set_onabort(&mut self, cb: Opt<Function<'js>>) {
self.onabort = cb.0;
}
#[qjs(rename = "addEventListener")]
pub fn add_event_listener(&mut self, event: String, cb: Function<'js>) {
if event == "abort" {
self.listeners.push(cb);
}
}
#[qjs(rename = "removeEventListener")]
pub fn remove_event_listener(&mut self, event: String, cb: Function<'js>) {
if event == "abort" {
self.listeners.retain(|l| l != &cb);
}
}
#[qjs(static)]
pub fn abort(ctx: Ctx<'js>, reason: Opt<Value<'js>>) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
let inst = Class::instance(ctx.clone(), Self::fresh())?;
let r = match reason.0 {
Some(v) if !v.is_undefined() => v,
_ => Self::default_reason(&ctx, "AbortError", "This operation was aborted")?,
};
Self::run_abort(&inst, r);
Ok(inst)
}
#[qjs(static)]
pub fn timeout(ctx: Ctx<'js>, ms: u64) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
let inst = Class::instance(ctx.clone(), Self::fresh())?;
let inst2 = inst.clone();
let ctx2 = ctx.clone();
ctx.spawn(async move {
tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
if let Ok(reason) = AbortSignalJs::default_reason(&ctx2, "TimeoutError", "The operation timed out") {
AbortSignalJs::run_abort(&inst2, reason);
}
});
Ok(inst)
}
#[qjs(static)]
pub fn any(
ctx: Ctx<'js>,
signals: Vec<Class<'js, AbortSignalJs<'js>>>,
) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
let combined = Class::instance(ctx.clone(), Self::fresh())?;
for s in &signals {
let (is_aborted, reason) = {
let b = s.borrow();
(b.aborted, b.reason.clone())
};
if is_aborted {
let r = reason.unwrap_or_else(|| {
Self::default_reason(&ctx, "AbortError", "This operation was aborted")
.unwrap_or_else(|_| Value::new_undefined(ctx.clone()))
});
Self::run_abort(&combined, r);
return Ok(combined);
}
}
for s in &signals {
let combined2 = combined.clone();
let cb = Function::new(ctx.clone(), move |reason: Value<'js>| {
AbortSignalJs::run_abort(&combined2, reason);
})?;
s.borrow_mut().listeners.push(cb);
}
Ok(combined)
}
}
#[derive(Trace)]
#[rquickjs::class(rename = "AbortController")]
pub struct AbortControllerJs<'js> {
signal: Class<'js, AbortSignalJs<'js>>,
}
#[allow(unsafe_code)]
unsafe impl<'js> rquickjs::JsLifetime<'js> for AbortControllerJs<'js> {
type Changed<'to> = AbortControllerJs<'to>;
}
#[rquickjs::methods(rename_all = "camelCase")]
impl<'js> AbortControllerJs<'js> {
#[qjs(constructor)]
pub fn new(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
Ok(Self {
signal: Class::instance(ctx, AbortSignalJs::fresh())?,
})
}
#[qjs(get)]
pub fn signal(&self) -> Class<'js, AbortSignalJs<'js>> {
self.signal.clone()
}
#[qjs(rename = "abort")]
pub fn abort(&self, ctx: Ctx<'js>, reason: Opt<Value<'js>>) -> rquickjs::Result<()> {
let r = match reason.0 {
Some(v) if !v.is_undefined() => v,
_ => AbortSignalJs::default_reason(&ctx, "AbortError", "This operation was aborted")?,
};
AbortSignalJs::run_abort(&self.signal, r);
Ok(())
}
}