Skip to main content

ply_engine/net/
http.rs

1use std::sync::Arc;
2
3#[cfg(target_arch = "wasm32")]
4use sapp_jsutils::JsObject;
5
6/// Configuration builder passed to the HTTP request closure.
7pub struct HttpConfig {
8    pub(crate) headers: Vec<(String, String)>,
9    pub(crate) body: Option<Vec<u8>>,
10}
11
12impl HttpConfig {
13    pub(crate) fn new() -> Self {
14        Self {
15            headers: Vec::new(),
16            body: None,
17        }
18    }
19
20    /// Add a header to the request.
21    pub fn header(&mut self, key: &str, value: &str) -> &mut Self {
22        self.headers.push((key.to_owned(), value.to_owned()));
23        self
24    }
25
26    /// Set the request body as a string.
27    pub fn body(&mut self, body: &str) -> &mut Self {
28        self.body = Some(body.as_bytes().to_vec());
29        self
30    }
31
32    /// Set the request body as raw bytes.
33    pub fn body_bytes(&mut self, body: Vec<u8>) -> &mut Self {
34        self.body = Some(body);
35        self
36    }
37}
38
39/// A completed HTTP response.
40pub struct Response {
41    status: u16,
42    body: Vec<u8>,
43}
44
45impl Response {
46    pub(crate) fn new(status: u16, body: Vec<u8>) -> Self {
47        Self { status, body }
48    }
49
50    /// HTTP status code.
51    pub fn status(&self) -> u16 {
52        self.status
53    }
54
55    /// Response body as a UTF-8 string (lossy).
56    pub fn text(&self) -> &str {
57        std::str::from_utf8(&self.body).unwrap_or("")
58    }
59
60    /// Response body as raw bytes.
61    pub fn bytes(&self) -> &[u8] {
62        &self.body
63    }
64
65    /// Deserialize the response body as JSON.
66    #[cfg(feature = "net-json")]
67    pub fn json<T: serde::de::DeserializeOwned>(&self) -> Result<T, String> {
68        serde_json::from_slice(&self.body).map_err(|e| e.to_string())
69    }
70}
71
72/// A thin handle to an HTTP request tracked by the global manager.
73///
74/// Does not own the data — just a key for lookups.
75pub struct Request {
76    pub(crate) id: u64,
77}
78
79impl Request {
80    /// Check for a completed response.
81    ///
82    /// - `None`: still pending
83    /// - `Some(Ok(resp))`: completed successfully (`Arc<Response>`)
84    /// - `Some(Err(e))`: request failed
85    pub fn response(&self) -> Option<Result<Arc<Response>, String>> {
86        let mut mgr = super::NET_MANAGER.lock().unwrap();
87        let entry = mgr.http_requests.get_mut(&self.id)?;
88        entry.frames_not_accessed = 0;
89
90        // Try to transition Pending → Done/Error
91        match &mut entry.state {
92            HttpRequestState::Pending(pending) => {
93                if let Some(result) = pending.try_recv() {
94                    match result {
95                        Ok(resp) => {
96                            entry.state = HttpRequestState::Done(Arc::new(resp));
97                        }
98                        Err(e) => {
99                            entry.state = HttpRequestState::Error(e);
100                        }
101                    }
102                }
103            }
104            _ => {}
105        }
106
107        match &entry.state {
108            HttpRequestState::Pending(_) => None,
109            HttpRequestState::Done(resp) => Some(Ok(Arc::clone(resp))),
110            HttpRequestState::Error(e) => Some(Err(e.clone())),
111        }
112    }
113
114    /// Cancel and remove a pending request. Consumes the handle.
115    pub fn cancel(self) {
116        let mut mgr = super::NET_MANAGER.lock().unwrap();
117        mgr.http_requests.remove(&self.id);
118    }
119}
120
121/// Abstraction over the pending receive mechanism.
122pub(crate) struct PendingHttp {
123    #[cfg(not(target_arch = "wasm32"))]
124    rx: std::sync::mpsc::Receiver<Result<Response, String>>,
125    #[cfg(target_arch = "wasm32")]
126    cid: i32,
127}
128
129impl PendingHttp {
130    #[cfg(not(target_arch = "wasm32"))]
131    pub fn new(rx: std::sync::mpsc::Receiver<Result<Response, String>>) -> Self {
132        Self { rx }
133    }
134
135    #[cfg(target_arch = "wasm32")]
136    pub fn new(cid: i32) -> Self {
137        Self { cid }
138    }
139
140    /// Try to receive a completed response. Returns None if still pending.
141    pub fn try_recv(&mut self) -> Option<Result<Response, String>> {
142        #[cfg(not(target_arch = "wasm32"))]
143        {
144            self.rx.try_recv().ok()
145        }
146
147        #[cfg(target_arch = "wasm32")]
148        {
149            let js_obj = unsafe { ply_net_http_try_recv(self.cid) };
150            if js_obj.is_nil() {
151                return None;
152            }
153
154            // Check for error field
155            if js_obj.have_field("error") {
156                let mut error_str = String::new();
157                js_obj.field("error").to_string(&mut error_str);
158                if !error_str.is_empty() {
159                    return Some(Err(error_str));
160                }
161            }
162
163            let status = js_obj.field_u32("status") as u16;
164            let mut body = Vec::new();
165            js_obj.field("body").to_byte_buffer(&mut body);
166            Some(Ok(Response::new(status, body)))
167        }
168    }
169}
170
171/// Internal state for a tracked HTTP request.
172pub(crate) enum HttpRequestState {
173    /// In flight — response hasn't arrived yet.
174    Pending(PendingHttp),
175    /// Response arrived.
176    Done(Arc<Response>),
177    /// Request failed.
178    Error(String),
179}
180
181// WASM FFI
182#[cfg(target_arch = "wasm32")]
183extern "C" {
184    pub(crate) fn ply_net_http_make_request(
185        scheme: i32,
186        url: JsObject,
187        body: JsObject,
188        headers: JsObject,
189    ) -> i32;
190    fn ply_net_http_try_recv(cid: i32) -> JsObject;
191}