Skip to main content

fui/
fetch.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use crate::ffi;
6
7type ResponseCallback = Rc<dyn Fn(FetchResponse)>;
8type ErrorCallback = Rc<dyn Fn(FetchErrorEventArgs)>;
9
10thread_local! {
11    static NEXT_FETCH_ID: RefCell<u32> = const { RefCell::new(1) };
12    static ACTIVE_REQUESTS: RefCell<HashMap<u32, Rc<RefCell<FetchRequestInner>>>> = RefCell::new(HashMap::new());
13}
14
15fn encode_text_parts(values: &[String]) -> Vec<u8> {
16    let mut bytes = Vec::new();
17    bytes.extend_from_slice(&(values.len() as u32).to_le_bytes());
18    for value in values {
19        let encoded = value.as_bytes();
20        bytes.extend_from_slice(&(encoded.len() as u32).to_le_bytes());
21        bytes.extend_from_slice(encoded);
22    }
23    bytes
24}
25
26fn decode_text_parts(bytes: &[u8]) -> Vec<String> {
27    if bytes.len() < 4 {
28        return Vec::new();
29    }
30    let mut cursor = 0usize;
31    let count =
32        u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0, 0, 0, 0])) as usize;
33    cursor += 4;
34    let mut values = Vec::with_capacity(count);
35    for _ in 0..count {
36        if cursor + 4 > bytes.len() {
37            break;
38        }
39        let len = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0, 0, 0, 0]))
40            as usize;
41        cursor += 4;
42        if cursor + len > bytes.len() {
43            break;
44        }
45        values.push(String::from_utf8_lossy(&bytes[cursor..cursor + len]).into_owned());
46        cursor += len;
47    }
48    values
49}
50
51fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
52    let bytes = value.as_bytes();
53    callback(
54        if bytes.is_empty() {
55            0
56        } else {
57            bytes.as_ptr() as usize
58        },
59        bytes.len() as u32,
60    );
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
64pub struct FetchResponse {
65    pub ok: bool,
66    pub status: i32,
67    pub status_text: String,
68    pub url: String,
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
72pub struct FetchErrorEventArgs {
73    pub message: String,
74}
75
76struct FetchRequestInner {
77    method: String,
78    url: String,
79    headers: Vec<String>,
80    body: Vec<u8>,
81    on_complete: Option<ResponseCallback>,
82    on_error: Option<ErrorCallback>,
83    request_id: u32,
84    started: bool,
85    finished: bool,
86}
87
88pub struct FetchRequest {
89    inner: Rc<RefCell<FetchRequestInner>>,
90}
91
92impl FetchRequest {
93    fn new(url: impl Into<String>) -> Self {
94        Self {
95            inner: Rc::new(RefCell::new(FetchRequestInner {
96                method: "GET".to_string(),
97                url: url.into(),
98                headers: Vec::new(),
99                body: Vec::new(),
100                on_complete: None,
101                on_error: None,
102                request_id: 0,
103                started: false,
104                finished: false,
105            })),
106        }
107    }
108
109    pub fn method(self, value: impl Into<String>) -> Self {
110        self.inner.borrow_mut().method = value.into();
111        self
112    }
113
114    pub fn header(self, name: impl Into<String>, value: impl Into<String>) -> Self {
115        let mut inner = self.inner.borrow_mut();
116        inner.headers.push(name.into());
117        inner.headers.push(value.into());
118        drop(inner);
119        self
120    }
121
122    pub fn body_bytes(self, value: Vec<u8>) -> Self {
123        self.inner.borrow_mut().body = value;
124        self
125    }
126
127    pub fn body_text(self, value: impl Into<String>) -> Self {
128        self.body_bytes(value.into().into_bytes())
129    }
130
131    pub fn on_complete(self, handler: impl Fn(FetchResponse) + 'static) -> Self {
132        self.inner.borrow_mut().on_complete = Some(Rc::new(handler));
133        self
134    }
135
136    pub fn on_error(self, handler: impl Fn(FetchErrorEventArgs) + 'static) -> Self {
137        self.inner.borrow_mut().on_error = Some(Rc::new(handler));
138        self
139    }
140
141    pub fn start(self) -> Self {
142        let already_started = {
143            let inner = self.inner.borrow();
144            inner.started || inner.finished
145        };
146        if already_started {
147            return self;
148        }
149        let empty_url_error = {
150            let inner = self.inner.borrow();
151            if inner.url.is_empty() {
152                inner.on_error.clone()
153            } else {
154                None
155            }
156        };
157        if self.inner.borrow().url.is_empty() {
158            if let Some(callback) = empty_url_error {
159                callback(FetchErrorEventArgs {
160                    message: "FetchRequest.start: url must not be empty.".to_string(),
161                });
162            }
163            return self;
164        }
165        let (method, url, headers, body, request_id) = {
166            let mut inner = self.inner.borrow_mut();
167            let request_id = NEXT_FETCH_ID.with(|next| {
168                let mut slot = next.borrow_mut();
169                let id = *slot;
170                *slot += 1;
171                id
172            });
173            inner.request_id = request_id;
174            inner.started = true;
175            ACTIVE_REQUESTS.with(|requests| {
176                requests.borrow_mut().insert(request_id, self.inner.clone());
177            });
178            (
179                inner.method.clone(),
180                inner.url.clone(),
181                inner.headers.clone(),
182                inner.body.clone(),
183                request_id,
184            )
185        };
186        let header_bytes = encode_text_parts(&headers);
187        with_utf8(&method, |method_ptr, method_len| {
188            with_utf8(&url, |url_ptr, url_len| unsafe {
189                ffi::fui_fetch_start(
190                    request_id,
191                    method_ptr,
192                    method_len,
193                    url_ptr,
194                    url_len,
195                    if header_bytes.is_empty() {
196                        0
197                    } else {
198                        header_bytes.as_ptr() as usize
199                    },
200                    header_bytes.len() as u32,
201                    if body.is_empty() {
202                        0
203                    } else {
204                        body.as_ptr() as usize
205                    },
206                    body.len() as u32,
207                );
208            })
209        });
210        self
211    }
212
213    pub fn cancel(&self) {
214        let request_id = {
215            let inner = self.inner.borrow();
216            if !inner.started || inner.finished || inner.request_id == 0 {
217                return;
218            }
219            inner.request_id
220        };
221        unsafe { ffi::fui_fetch_cancel(request_id) };
222        finish_request(&self.inner);
223    }
224}
225
226impl Drop for FetchRequest {
227    fn drop(&mut self) {
228        self.cancel();
229    }
230}
231
232fn finish_request(request: &Rc<RefCell<FetchRequestInner>>) {
233    let request_id = {
234        let mut inner = request.borrow_mut();
235        if inner.finished {
236            return;
237        }
238        let request_id = inner.request_id;
239        inner.request_id = 0;
240        inner.finished = true;
241        inner.on_complete = None;
242        inner.on_error = None;
243        request_id
244    };
245    if request_id != 0 {
246        ACTIVE_REQUESTS.with(|requests| {
247            requests.borrow_mut().remove(&request_id);
248        });
249    }
250}
251
252pub struct Fetch;
253
254impl Fetch {
255    pub fn request(url: impl Into<String>) -> FetchRequest {
256        FetchRequest::new(url)
257    }
258}
259
260fn complete_request(request_id: u32, response: FetchResponse) {
261    let request = ACTIVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
262    let Some(request) = request else {
263        return;
264    };
265    let callback = {
266        let mut inner = request.borrow_mut();
267        if inner.finished {
268            return;
269        }
270        inner.finished = true;
271        inner.request_id = 0;
272        inner.on_error = None;
273        inner.on_complete.clone()
274    };
275    request.borrow_mut().on_complete = None;
276    if let Some(callback) = callback {
277        callback(response);
278    }
279}
280
281fn fail_request(request_id: u32, message: String) {
282    let request = ACTIVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
283    let Some(request) = request else {
284        return;
285    };
286    let callback = {
287        let mut inner = request.borrow_mut();
288        if inner.finished {
289            return;
290        }
291        inner.finished = true;
292        inner.request_id = 0;
293        inner.on_complete = None;
294        inner.on_error.clone()
295    };
296    request.borrow_mut().on_error = None;
297    if let Some(callback) = callback {
298        callback(FetchErrorEventArgs { message });
299    }
300}
301
302pub fn dispose_all_fetch_requests() {
303    let requests = ACTIVE_REQUESTS.with(|requests| {
304        requests
305            .borrow_mut()
306            .drain()
307            .map(|(_, request)| request)
308            .collect::<Vec<_>>()
309    });
310    for request in requests {
311        let request_id = {
312            let mut inner = request.borrow_mut();
313            if inner.finished || !inner.started || inner.request_id == 0 {
314                inner.finished = true;
315                inner.on_complete = None;
316                inner.on_error = None;
317                0
318            } else {
319                let request_id = inner.request_id;
320                inner.request_id = 0;
321                inner.finished = true;
322                inner.on_complete = None;
323                inner.on_error = None;
324                request_id
325            }
326        };
327        if request_id != 0 {
328            unsafe { ffi::fui_fetch_cancel(request_id) };
329        }
330    }
331}
332
333pub fn reset_fetch_runtime() {
334    dispose_all_fetch_requests();
335    NEXT_FETCH_ID.with(|next| {
336        *next.borrow_mut() = 1;
337    });
338}
339
340#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
341/// # Safety
342/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
343pub unsafe extern "C" fn __fui_on_fetch_complete(
344    request_id: u32,
345    ok: bool,
346    status: i32,
347    payload_ptr: *const u8,
348    payload_len: u32,
349) {
350    let parts = if payload_ptr.is_null() || payload_len == 0 {
351        Vec::new()
352    } else {
353        decode_text_parts(unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) })
354    };
355    complete_request(
356        request_id,
357        FetchResponse {
358            ok,
359            status,
360            status_text: parts.first().cloned().unwrap_or_default(),
361            url: parts.get(1).cloned().unwrap_or_default(),
362        },
363    );
364}
365
366#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
367/// # Safety
368/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
369pub unsafe extern "C" fn __fui_on_fetch_error(
370    request_id: u32,
371    payload_ptr: *const u8,
372    payload_len: u32,
373) {
374    let message = if payload_ptr.is_null() || payload_len == 0 {
375        "Fetch request failed.".to_string()
376    } else {
377        String::from_utf8_lossy(unsafe {
378            std::slice::from_raw_parts(payload_ptr, payload_len as usize)
379        })
380        .into_owned()
381    };
382    fail_request(request_id, message);
383}
384
385#[cfg(test)]
386mod tests {
387    use super::Fetch;
388    use crate::ffi::{self, Call};
389    use std::cell::RefCell;
390    use std::rc::Rc;
391
392    #[test]
393    fn fetch_request_emits_host_call() {
394        ffi::test::reset();
395        let request = Fetch::request("https://example.com")
396            .method("POST")
397            .header("Accept", "application/json")
398            .body_text("hello")
399            .start();
400        let calls = ffi::test::take_calls();
401        assert!(calls.iter().any(|call| matches!(call, Call::FetchStart { method, url, .. } if method == "POST" && url == "https://example.com")));
402        drop(request);
403    }
404
405    #[test]
406    fn fetch_callbacks_receive_result() {
407        ffi::test::reset();
408        let result = Rc::new(RefCell::new(String::new()));
409        let result_clone = result.clone();
410        let request = Fetch::request("https://example.com")
411            .on_complete(move |response| {
412                result_clone.replace(response.status_text);
413            })
414            .start();
415        let payload = {
416            let mut bytes = Vec::new();
417            bytes.extend_from_slice(&2u32.to_le_bytes());
418            for part in ["OK", "https://example.com"] {
419                bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
420                bytes.extend_from_slice(part.as_bytes());
421            }
422            bytes
423        };
424        unsafe {
425            super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
426        }
427        assert_eq!(&*result.borrow(), "OK");
428        drop(request);
429    }
430
431    #[test]
432    fn fetch_empty_url_reports_error_without_host_call() {
433        ffi::test::reset();
434        super::reset_fetch_runtime();
435        let result = Rc::new(RefCell::new(String::new()));
436        let result_clone = result.clone();
437        let request = Fetch::request("")
438            .on_error(move |event| {
439                result_clone.replace(event.message);
440            })
441            .start();
442        assert_eq!(
443            &*result.borrow(),
444            "FetchRequest.start: url must not be empty."
445        );
446        assert!(ffi::test::take_calls()
447            .iter()
448            .all(|call| !matches!(call, Call::FetchStart { .. })));
449        drop(request);
450    }
451
452    #[test]
453    fn fetch_cancel_finishes_and_suppresses_late_completion() {
454        ffi::test::reset();
455        super::reset_fetch_runtime();
456        let result = Rc::new(RefCell::new(String::new()));
457        let result_clone = result.clone();
458        let request = Fetch::request("https://example.com")
459            .on_complete(move |response| {
460                result_clone.replace(response.status_text);
461            })
462            .start();
463        request.cancel();
464        let calls = ffi::test::take_calls();
465        assert!(calls
466            .iter()
467            .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
468        let payload = {
469            let mut bytes = Vec::new();
470            bytes.extend_from_slice(&2u32.to_le_bytes());
471            for part in ["OK", "https://example.com"] {
472                bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
473                bytes.extend_from_slice(part.as_bytes());
474            }
475            bytes
476        };
477        unsafe {
478            super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
479        }
480        assert_eq!(&*result.borrow(), "");
481        drop(request);
482    }
483
484    #[test]
485    fn fetch_drop_cancels_active_request() {
486        ffi::test::reset();
487        super::reset_fetch_runtime();
488        {
489            let _request = Fetch::request("https://example.com").start();
490        }
491        let calls = ffi::test::take_calls();
492        assert!(calls
493            .iter()
494            .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
495    }
496
497    #[test]
498    fn fetch_error_callback_receives_default_message() {
499        ffi::test::reset();
500        super::reset_fetch_runtime();
501        let result = Rc::new(RefCell::new(String::new()));
502        let result_clone = result.clone();
503        let request = Fetch::request("https://example.com")
504            .on_error(move |event| {
505                result_clone.replace(event.message);
506            })
507            .start();
508        unsafe {
509            super::__fui_on_fetch_error(1, std::ptr::null(), 0);
510        }
511        assert_eq!(&*result.borrow(), "Fetch request failed.");
512        drop(request);
513    }
514}