Skip to main content

ferrijs_std/abort/
abort_signal.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3use std::sync::{Arc, RwLock};
4
5use crate::events::{Emitter, EventEmitter, EventList};
6use crate::exceptions::{DOMException, DOMExceptionName};
7use crate::utils::mc_oneshot;
8use rquickjs::{
9    atom::PredefinedAtom,
10    class::{Trace, Tracer},
11    function::OnceFn,
12    prelude::{Opt, This},
13    Array, Class, Ctx, Error, Exception, Function, JsLifetime, Result, Undefined, Value,
14};
15
16#[derive(Clone)]
17#[rquickjs::class]
18pub struct AbortSignal<'js> {
19    emitter: EventEmitter<'js>,
20    pub aborted: bool,
21    reason: Option<Value<'js>>,
22    pub sender: mc_oneshot::Sender<Value<'js>>,
23}
24
25unsafe impl<'js> JsLifetime<'js> for AbortSignal<'js> {
26    type Changed<'to> = AbortSignal<'to>;
27}
28
29impl<'js> Trace<'js> for AbortSignal<'js> {
30    fn trace<'a>(&self, tracer: Tracer<'a, 'js>) {
31        if let Some(reason) = &self.reason {
32            tracer.mark(reason);
33        }
34        self.emitter.trace(tracer);
35        self.sender.trace(tracer);
36    }
37}
38
39impl<'js> Emitter<'js> for AbortSignal<'js> {
40    fn get_event_list(&self) -> Arc<RwLock<EventList<'js>>> {
41        self.emitter.get_event_list()
42    }
43}
44
45#[rquickjs::methods(rename_all = "camelCase")]
46impl<'js> AbortSignal<'js> {
47    #[qjs(constructor)]
48    pub fn new() -> Self {
49        let (sender, _) = mc_oneshot::channel::<Value<'js>>();
50        Self {
51            emitter: EventEmitter::new(),
52            aborted: false,
53            reason: None,
54            sender,
55        }
56    }
57
58    #[qjs(get, rename = "onabort")]
59    pub fn get_on_abort(&self) -> Option<Function<'js>> {
60        Self::get_listeners_str(self, "abort").first().cloned()
61    }
62
63    #[qjs(set, rename = "onabort")]
64    pub fn set_on_abort(
65        this: This<Class<'js, Self>>,
66        ctx: Ctx<'js>,
67        listener: Function<'js>,
68    ) -> Result<()> {
69        Self::add_event_listener_str(this.0, &ctx, "abort", listener, false, false)?;
70        Ok(())
71    }
72
73    pub fn remove_on_abort(
74        this: This<Class<'js, Self>>,
75        ctx: Ctx<'js>,
76        listener: Function<'js>,
77    ) -> Result<()> {
78        Self::remove_event_listener_str(this.0, &ctx, "abort", listener)?;
79        Ok(())
80    }
81
82    pub fn throw_if_aborted(&self, ctx: Ctx<'js>) -> Result<()> {
83        if self.aborted {
84            return Err(ctx.throw(
85                self.reason
86                    .clone()
87                    .unwrap_or_else(|| Undefined.into_value(ctx.clone())),
88            ));
89        }
90        Ok(())
91    }
92
93    #[qjs(static)]
94    pub fn any(ctx: Ctx<'js>, signals: Array<'js>) -> Result<Class<'js, Self>> {
95        let mut new_signal = AbortSignal::new();
96
97        let mut signal_instances = Vec::with_capacity(signals.len());
98
99        for signal in signals.iter() {
100            let signal: Value = signal?;
101            let signal: Class<AbortSignal> = Class::from_value(&signal)
102                .map_err(|_| Exception::throw_type(&ctx, "Value is not an AbortSignal instance"))?;
103            let signal_borrow = signal.borrow();
104            if signal_borrow.aborted {
105                new_signal.aborted = true;
106                new_signal.reason.clone_from(&signal_borrow.reason);
107                let new_signal = Class::instance(ctx, new_signal)?;
108                return Ok(new_signal);
109            } else {
110                drop(signal_borrow);
111                signal_instances.push(signal);
112            }
113        }
114
115        let new_signal_instance = Class::instance(ctx.clone(), new_signal)?;
116        for signal in signal_instances {
117            let signal_instance_2 = new_signal_instance.clone();
118            Self::add_event_listener_str(
119                signal,
120                &ctx,
121                "abort",
122                Function::new(
123                    ctx.clone(),
124                    OnceFn::from(|ctx, signal| {
125                        struct Args<'js>(Ctx<'js>, This<Class<'js, AbortSignal<'js>>>);
126                        let Args(ctx, signal) = Args(ctx, signal);
127                        let mut borrow = signal_instance_2.borrow_mut();
128                        borrow.aborted = true;
129                        borrow.reason.clone_from(&signal.borrow().reason);
130                        drop(borrow);
131                        Self::send_aborted(This(signal_instance_2), ctx)
132                    }),
133                )?,
134                false,
135                true,
136            )?;
137        }
138
139        Ok(new_signal_instance)
140    }
141
142    #[qjs(get)]
143    pub fn aborted(&self) -> bool {
144        self.aborted
145    }
146
147    #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)]
148    pub fn to_string_tag() -> &'static str {
149        stringify!(AbortSignal)
150    }
151
152    #[qjs(get)]
153    pub fn reason(&self) -> Option<Value<'js>> {
154        self.reason.clone()
155    }
156
157    #[qjs(set, rename = "reason")]
158    pub fn set_reason(&mut self, reason: Opt<Value<'js>>) {
159        match reason.0 {
160            Some(new_reason) if !new_reason.is_undefined() => self.reason.replace(new_reason),
161            _ => self.reason.take(),
162        };
163    }
164
165    #[qjs(skip)]
166    pub fn send_aborted(this: This<Class<'js, Self>>, ctx: Ctx<'js>) -> Result<()> {
167        let mut borrow = this.borrow_mut();
168        borrow.aborted = true;
169        let reason = get_reason_or_dom_exception(
170            &ctx,
171            borrow.reason.as_ref(),
172            DOMExceptionName::AbortError,
173        )?;
174        borrow.reason = Some(reason.clone());
175        borrow.sender.send(reason);
176        drop(borrow);
177        Self::emit_str(this.0, &ctx, "abort", vec![], false)?;
178        Ok(())
179    }
180
181    #[qjs(static)]
182    pub fn abort(ctx: Ctx<'js>, reason: Opt<Value<'js>>) -> Result<Class<'js, Self>> {
183        let mut signal = Self::new();
184        signal.set_reason(reason);
185        let instance = Class::instance(ctx.clone(), signal)?;
186        Self::send_aborted(This(instance.clone()), ctx)?;
187        Ok(instance)
188    }
189
190    #[qjs(static)]
191    pub fn timeout(ctx: Ctx<'js>, milliseconds: u64) -> Result<Class<'js, Self>> {
192        let timeout_error =
193            get_reason_or_dom_exception(&ctx, None, DOMExceptionName::TimeoutError)?;
194
195        let signal = Self::new();
196        let signal_instance = Class::instance(ctx.clone(), signal)?;
197        let signal_instance2 = signal_instance.clone();
198
199        let cb = Function::new(
200            ctx.clone(),
201            OnceFn::from(move |ctx| {
202                let mut borrow = signal_instance.borrow_mut();
203                borrow.set_reason(Opt(Some(timeout_error)));
204                drop(borrow);
205                Self::send_aborted(This(signal_instance), ctx)?;
206                Ok::<_, Error>(())
207            }),
208        )?;
209
210        #[cfg(feature = "sleep-timers")]
211        {
212            crate::timers::set_timeout_interval(
213                &ctx,
214                cb,
215                milliseconds,
216                crate::utils::provider::ProviderType::Timeout,
217            )?;
218        }
219        #[cfg(all(not(feature = "sleep-timers"), feature = "sleep-tokio"))]
220        {
221            use crate::context::CtxExtension;
222            ctx.clone().spawn_exit_simple(async move {
223                tokio::time::sleep(std::time::Duration::from_millis(milliseconds)).await;
224                cb.call::<_, ()>(())?;
225                Ok(())
226            });
227        }
228        #[cfg(all(not(feature = "sleep-tokio"), not(feature = "sleep-timers")))]
229        {
230            compile_error!("Either the `sleep-tokio` or `sleep-timers` feature must be enabled")
231        }
232
233        Ok(signal_instance2)
234    }
235}
236
237fn get_reason_or_dom_exception<'js>(
238    ctx: &Ctx<'js>,
239    reason: Option<&Value<'js>>,
240    name: DOMExceptionName,
241) -> Result<Value<'js>> {
242    let reason = if let Some(reason) = reason {
243        reason.clone()
244    } else {
245        let ex = DOMException::new_with_name(ctx, name, String::new())?;
246        Class::instance(ctx.clone(), ex)?.into_value()
247    };
248    Ok(reason)
249}
250
251#[cfg(test)]
252mod tests {
253    use std::time::Duration;
254
255    use crate::test::test_async_with;
256
257    use super::*;
258
259    #[tokio::test]
260    async fn test_abort_signal() {
261        test_async_with(|ctx| {
262            crate::abort::init(&ctx).unwrap();
263            #[cfg(feature = "sleep-timers")]
264            crate::timers::init(&ctx).unwrap();
265            Box::pin(async move {
266                let signal = AbortSignal::timeout(ctx, 5).unwrap();
267
268                assert!(!signal.borrow().aborted());
269
270                tokio::time::sleep(Duration::from_millis(50)).await;
271
272                assert!(signal.borrow().aborted());
273                let reason = signal.borrow().reason().unwrap();
274                let reason = Class::<DOMException>::from_value(&reason).unwrap();
275                assert_eq!(reason.borrow().name(), "TimeoutError");
276            })
277        })
278        .await;
279    }
280}