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)]
341pub 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)]
367pub 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}