1use 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}