ferridriver_script/bindings/
abort.rs1use std::sync::Arc;
13use std::sync::atomic::{AtomicBool, Ordering};
14
15use rquickjs::class::Trace;
16use rquickjs::function::Opt;
17use rquickjs::{Class, Ctx, Function, Object, Value};
18
19pub struct AbortInner {
22 aborted: AtomicBool,
23 notify: tokio::sync::Notify,
24 message: std::sync::Mutex<Option<String>>,
27}
28
29impl AbortInner {
30 fn new() -> Arc<Self> {
31 Arc::new(Self {
32 aborted: AtomicBool::new(false),
33 notify: tokio::sync::Notify::new(),
34 message: std::sync::Mutex::new(None),
35 })
36 }
37
38 pub fn is_aborted(&self) -> bool {
39 self.aborted.load(Ordering::Acquire)
40 }
41
42 pub fn reason_message(&self) -> String {
45 self
46 .message
47 .lock()
48 .unwrap_or_else(std::sync::PoisonError::into_inner)
49 .clone()
50 .unwrap_or_else(|| "This operation was aborted".to_string())
51 }
52
53 fn mark(&self, message: Option<String>) {
54 *self.message.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = message;
55 self.aborted.store(true, Ordering::Release);
56 self.notify.notify_waiters();
57 }
58
59 pub async fn aborted(&self) {
63 self.notify.notified().await;
64 }
65}
66
67#[derive(Trace)]
68#[rquickjs::class(rename = "AbortSignal")]
69pub struct AbortSignalJs<'js> {
70 #[qjs(skip_trace)]
71 inner: Arc<AbortInner>,
72 #[qjs(skip_trace)]
73 aborted: bool,
74 reason: Option<Value<'js>>,
75 listeners: Vec<Function<'js>>,
76 onabort: Option<Function<'js>>,
77}
78
79#[allow(unsafe_code)]
80unsafe impl<'js> rquickjs::JsLifetime<'js> for AbortSignalJs<'js> {
81 type Changed<'to> = AbortSignalJs<'to>;
82}
83
84impl<'js> AbortSignalJs<'js> {
85 fn fresh() -> Self {
86 Self {
87 inner: AbortInner::new(),
88 aborted: false,
89 reason: None,
90 listeners: Vec::new(),
91 onabort: None,
92 }
93 }
94
95 pub fn inner_of(signal: &Class<'js, AbortSignalJs<'js>>) -> Arc<AbortInner> {
97 signal.borrow().inner.clone()
98 }
99
100 fn default_reason(ctx: &Ctx<'js>, name: &str, message: &str) -> rquickjs::Result<Value<'js>> {
104 let o = Object::new(ctx.clone())?;
105 o.set("name", name)?;
106 o.set("message", message)?;
107 Ok(o.into_value())
108 }
109
110 fn reason_to_message(reason: Option<&Value<'js>>) -> Option<String> {
111 let r = reason?;
112 if let Some(s) = r.as_string().and_then(|s| s.to_string().ok()) {
113 return Some(s);
114 }
115 r.as_object()
116 .and_then(|o| o.get::<_, String>("message").ok())
117 .or(Some("This operation was aborted".to_string()))
118 }
119
120 fn run_abort(this: &Class<'js, AbortSignalJs<'js>>, reason: Value<'js>) {
123 {
124 let mut b = this.borrow_mut();
125 if b.aborted {
126 return;
127 }
128 b.aborted = true;
129 b.reason = Some(reason.clone());
130 b.inner.mark(Self::reason_to_message(Some(&reason)));
131 }
132 let (onabort, listeners) = {
133 let b = this.borrow();
134 (b.onabort.clone(), b.listeners.clone())
135 };
136 if let Some(cb) = onabort {
137 let _ = cb.call::<_, ()>((reason.clone(),));
138 }
139 for cb in listeners {
140 let _ = cb.call::<_, ()>((reason.clone(),));
141 }
142 }
143}
144
145#[rquickjs::methods(rename_all = "camelCase")]
146impl<'js> AbortSignalJs<'js> {
147 #[qjs(constructor)]
152 pub fn new(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
153 Err(rquickjs::Exception::throw_type(&ctx, "Illegal constructor"))
154 }
155
156 #[qjs(get)]
157 pub fn aborted(&self) -> bool {
158 self.aborted
159 }
160
161 #[qjs(get)]
162 pub fn reason(&self) -> Option<Value<'js>> {
163 self.reason.clone()
164 }
165
166 #[qjs(rename = "throwIfAborted")]
167 pub fn throw_if_aborted(&self, ctx: Ctx<'js>) -> rquickjs::Result<()> {
168 if self.aborted {
169 let r = self.reason.clone().unwrap_or_else(|| Value::new_undefined(ctx.clone()));
170 return Err(ctx.throw(r));
171 }
172 Ok(())
173 }
174
175 #[qjs(get, rename = "onabort")]
176 pub fn get_onabort(&self) -> Option<Function<'js>> {
177 self.onabort.clone()
178 }
179
180 #[qjs(set, rename = "onabort")]
181 pub fn set_onabort(&mut self, cb: Opt<Function<'js>>) {
182 self.onabort = cb.0;
183 }
184
185 #[qjs(rename = "addEventListener")]
186 pub fn add_event_listener(&mut self, event: String, cb: Function<'js>) {
187 if event == "abort" {
188 self.listeners.push(cb);
189 }
190 }
191
192 #[qjs(rename = "removeEventListener")]
193 pub fn remove_event_listener(&mut self, event: String, cb: Function<'js>) {
194 if event == "abort" {
195 self.listeners.retain(|l| l != &cb);
196 }
197 }
198
199 #[qjs(static)]
201 pub fn abort(ctx: Ctx<'js>, reason: Opt<Value<'js>>) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
202 let inst = Class::instance(ctx.clone(), Self::fresh())?;
203 let r = match reason.0 {
204 Some(v) if !v.is_undefined() => v,
205 _ => Self::default_reason(&ctx, "AbortError", "This operation was aborted")?,
206 };
207 Self::run_abort(&inst, r);
208 Ok(inst)
209 }
210
211 #[qjs(static)]
214 pub fn timeout(ctx: Ctx<'js>, ms: u64) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
215 let inst = Class::instance(ctx.clone(), Self::fresh())?;
216 let inst2 = inst.clone();
217 let ctx2 = ctx.clone();
218 ctx.spawn(async move {
219 tokio::time::sleep(std::time::Duration::from_millis(ms)).await;
220 if let Ok(reason) = AbortSignalJs::default_reason(&ctx2, "TimeoutError", "The operation timed out") {
221 AbortSignalJs::run_abort(&inst2, reason);
222 }
223 });
224 Ok(inst)
225 }
226
227 #[qjs(static)]
230 pub fn any(
231 ctx: Ctx<'js>,
232 signals: Vec<Class<'js, AbortSignalJs<'js>>>,
233 ) -> rquickjs::Result<Class<'js, AbortSignalJs<'js>>> {
234 let combined = Class::instance(ctx.clone(), Self::fresh())?;
235 for s in &signals {
236 let (is_aborted, reason) = {
237 let b = s.borrow();
238 (b.aborted, b.reason.clone())
239 };
240 if is_aborted {
241 let r = reason.unwrap_or_else(|| {
242 Self::default_reason(&ctx, "AbortError", "This operation was aborted")
243 .unwrap_or_else(|_| Value::new_undefined(ctx.clone()))
244 });
245 Self::run_abort(&combined, r);
246 return Ok(combined);
247 }
248 }
249 for s in &signals {
250 let combined2 = combined.clone();
251 let cb = Function::new(ctx.clone(), move |reason: Value<'js>| {
252 AbortSignalJs::run_abort(&combined2, reason);
253 })?;
254 s.borrow_mut().listeners.push(cb);
255 }
256 Ok(combined)
257 }
258}
259
260#[derive(Trace)]
261#[rquickjs::class(rename = "AbortController")]
262pub struct AbortControllerJs<'js> {
263 signal: Class<'js, AbortSignalJs<'js>>,
264}
265
266#[allow(unsafe_code)]
267unsafe impl<'js> rquickjs::JsLifetime<'js> for AbortControllerJs<'js> {
268 type Changed<'to> = AbortControllerJs<'to>;
269}
270
271#[rquickjs::methods(rename_all = "camelCase")]
272impl<'js> AbortControllerJs<'js> {
273 #[qjs(constructor)]
274 pub fn new(ctx: Ctx<'js>) -> rquickjs::Result<Self> {
275 Ok(Self {
276 signal: Class::instance(ctx, AbortSignalJs::fresh())?,
277 })
278 }
279
280 #[qjs(get)]
281 pub fn signal(&self) -> Class<'js, AbortSignalJs<'js>> {
282 self.signal.clone()
283 }
284
285 #[qjs(rename = "abort")]
286 pub fn abort(&self, ctx: Ctx<'js>, reason: Opt<Value<'js>>) -> rquickjs::Result<()> {
287 let r = match reason.0 {
288 Some(v) if !v.is_undefined() => v,
289 _ => AbortSignalJs::default_reason(&ctx, "AbortError", "This operation was aborted")?,
290 };
291 AbortSignalJs::run_abort(&self.signal, r);
292 Ok(())
293 }
294}