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(
341 any(not(feature = "worker-runtime"), feature = "native-runtime"),
342 no_mangle
343)]
344pub unsafe extern "C" fn __fui_on_fetch_complete(
347 request_id: u32,
348 ok: bool,
349 status: i32,
350 payload_ptr: *const u8,
351 payload_len: u32,
352) {
353 let parts = if payload_ptr.is_null() || payload_len == 0 {
354 Vec::new()
355 } else {
356 decode_text_parts(unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) })
357 };
358 complete_request(
359 request_id,
360 FetchResponse {
361 ok,
362 status,
363 status_text: parts.first().cloned().unwrap_or_default(),
364 url: parts.get(1).cloned().unwrap_or_default(),
365 },
366 );
367}
368
369#[cfg_attr(
370 any(not(feature = "worker-runtime"), feature = "native-runtime"),
371 no_mangle
372)]
373pub unsafe extern "C" fn __fui_on_fetch_error(
376 request_id: u32,
377 payload_ptr: *const u8,
378 payload_len: u32,
379) {
380 let message = if payload_ptr.is_null() || payload_len == 0 {
381 "Fetch request failed.".to_string()
382 } else {
383 String::from_utf8_lossy(unsafe {
384 std::slice::from_raw_parts(payload_ptr, payload_len as usize)
385 })
386 .into_owned()
387 };
388 fail_request(request_id, message);
389}
390
391#[cfg(test)]
392mod tests {
393 use super::Fetch;
394 use crate::ffi::{self, Call};
395 use std::cell::RefCell;
396 use std::rc::Rc;
397
398 #[test]
399 fn fetch_request_emits_host_call() {
400 ffi::test::reset();
401 let request = Fetch::request("https://example.com")
402 .method("POST")
403 .header("Accept", "application/json")
404 .body_text("hello")
405 .start();
406 let calls = ffi::test::take_calls();
407 assert!(calls.iter().any(|call| matches!(call, Call::FetchStart { method, url, .. } if method == "POST" && url == "https://example.com")));
408 drop(request);
409 }
410
411 #[test]
412 fn fetch_callbacks_receive_result() {
413 ffi::test::reset();
414 let result = Rc::new(RefCell::new(String::new()));
415 let result_clone = result.clone();
416 let request = Fetch::request("https://example.com")
417 .on_complete(move |response| {
418 result_clone.replace(response.status_text);
419 })
420 .start();
421 let payload = {
422 let mut bytes = Vec::new();
423 bytes.extend_from_slice(&2u32.to_le_bytes());
424 for part in ["OK", "https://example.com"] {
425 bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
426 bytes.extend_from_slice(part.as_bytes());
427 }
428 bytes
429 };
430 unsafe {
431 super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
432 }
433 assert_eq!(&*result.borrow(), "OK");
434 drop(request);
435 }
436
437 #[test]
438 fn fetch_empty_url_reports_error_without_host_call() {
439 ffi::test::reset();
440 super::reset_fetch_runtime();
441 let result = Rc::new(RefCell::new(String::new()));
442 let result_clone = result.clone();
443 let request = Fetch::request("")
444 .on_error(move |event| {
445 result_clone.replace(event.message);
446 })
447 .start();
448 assert_eq!(
449 &*result.borrow(),
450 "FetchRequest.start: url must not be empty."
451 );
452 assert!(ffi::test::take_calls()
453 .iter()
454 .all(|call| !matches!(call, Call::FetchStart { .. })));
455 drop(request);
456 }
457
458 #[test]
459 fn fetch_cancel_finishes_and_suppresses_late_completion() {
460 ffi::test::reset();
461 super::reset_fetch_runtime();
462 let result = Rc::new(RefCell::new(String::new()));
463 let result_clone = result.clone();
464 let request = Fetch::request("https://example.com")
465 .on_complete(move |response| {
466 result_clone.replace(response.status_text);
467 })
468 .start();
469 request.cancel();
470 let calls = ffi::test::take_calls();
471 assert!(calls
472 .iter()
473 .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
474 let payload = {
475 let mut bytes = Vec::new();
476 bytes.extend_from_slice(&2u32.to_le_bytes());
477 for part in ["OK", "https://example.com"] {
478 bytes.extend_from_slice(&(part.len() as u32).to_le_bytes());
479 bytes.extend_from_slice(part.as_bytes());
480 }
481 bytes
482 };
483 unsafe {
484 super::__fui_on_fetch_complete(1, true, 200, payload.as_ptr(), payload.len() as u32);
485 }
486 assert_eq!(&*result.borrow(), "");
487 drop(request);
488 }
489
490 #[test]
491 fn fetch_drop_cancels_active_request() {
492 ffi::test::reset();
493 super::reset_fetch_runtime();
494 {
495 let _request = Fetch::request("https://example.com").start();
496 }
497 let calls = ffi::test::take_calls();
498 assert!(calls
499 .iter()
500 .any(|call| matches!(call, Call::FetchCancel { request_id } if *request_id == 1)));
501 }
502
503 #[test]
504 fn fetch_error_callback_receives_default_message() {
505 ffi::test::reset();
506 super::reset_fetch_runtime();
507 let result = Rc::new(RefCell::new(String::new()));
508 let result_clone = result.clone();
509 let request = Fetch::request("https://example.com")
510 .on_error(move |event| {
511 result_clone.replace(event.message);
512 })
513 .start();
514 unsafe {
515 super::__fui_on_fetch_error(1, std::ptr::null(), 0);
516 }
517 assert_eq!(&*result.borrow(), "Fetch request failed.");
518 drop(request);
519 }
520}