Skip to main content

df_hyperliquid_core/client/web/
mod.rs

1//! HTTP and websocket for the browser and Workers: [`Web`] sends requests
2//! through the global `fetch`, [`clock`] is `Date.now()`, and a [`Feed`] keeps
3//! the websocket open over the global `WebSocket`. Hand the first two to
4//! [`crate::client::Api`]:
5//!
6//! ```ignore
7//! let api = df_hyperliquid_core::client::Api::new(df_hyperliquid_core::client::web::Web, df_hyperliquid_core::client::Network::Mainnet, df_hyperliquid_core::client::web::clock);
8//! ```
9//!
10//! and an [`crate::live::Session`] to the feed, then read what it hands back:
11//!
12//! ```ignore
13//! let mut feed = df_hyperliquid_core::client::web::Feed::start(df_hyperliquid_core::live::Session::new(df_hyperliquid_core::client::Network::Mainnet));
14//! feed.subscribe(df_hyperliquid_core::live::Subscription::AllMids { dex: String::new() }).await?;
15//! while let Some(event) = feed.next().await {
16//!     // Opened, Frame, Unreadable, or Closed with the delay before the next dial.
17//! }
18//! ```
19//!
20//! `fetch`, `WebSocket` and `setTimeout` are read off the global object, so
21//! this runs in a page, a Web Worker, a Cloudflare Worker and Node 22. The API
22//! answers CORS with `*`. Requests are never retried.
23
24pub mod socket;
25
26use crate::client::{Http, Method, Request, Response, TransportError};
27use js_sys::{Array, Function, Promise, Reflect, Uint8Array};
28use wasm_bindgen::{JsCast, JsValue};
29use wasm_bindgen_futures::JsFuture;
30use web_sys::{Headers, RequestInit};
31
32pub use socket::{Event, Feed, FeedError};
33
34/// The global `fetch`.
35#[derive(Debug, Clone, Copy, Default)]
36pub struct Web;
37
38impl Http for Web {
39    async fn send(&self, request: Request) -> Result<Response, TransportError> {
40        let global = js_sys::global();
41        let fetch: Function =
42            Reflect::get(&global, &JsValue::from_str("fetch")).map_err(unreachable)?.dyn_into().map_err(unreachable)?;
43        let headers = Headers::new().map_err(unreachable)?;
44        for (name, value) in &request.headers {
45            headers.append(name, value).map_err(unreachable)?;
46        }
47        let init = RequestInit::new();
48        init.set_method(match request.method {
49            Method::Get => "GET",
50            Method::Post => "POST",
51        });
52        init.set_headers(&headers);
53        if !request.body.is_empty() {
54            init.set_body(&Uint8Array::from(request.body.as_slice()));
55        }
56        let promise: Promise = fetch
57            .call2(&global, &JsValue::from_str(&request.url), &init)
58            .map_err(unreachable)?
59            .dyn_into()
60            .map_err(unreachable)?;
61        let answer: web_sys::Response =
62            JsFuture::from(promise).await.map_err(unreachable)?.dyn_into().map_err(unreachable)?;
63        let status = answer.status();
64        let headers = headers_of(&answer.headers());
65        let buffer = JsFuture::from(answer.array_buffer().map_err(unreachable)?).await.map_err(unreachable)?;
66        Ok(Response { status, headers, body: Uint8Array::new(&buffer).to_vec() })
67    }
68}
69
70/// Every header as a name and a value. Fetch gives names in lower case.
71fn headers_of(headers: &Headers) -> Vec<(String, String)> {
72    let Ok(Some(entries)) = js_sys::try_iter(headers) else { return Vec::new() };
73    entries
74        .filter_map(Result::ok)
75        .filter_map(|entry| {
76            let pair: Array = entry.dyn_into().ok()?;
77            Some((pair.get(0).as_string()?, pair.get(1).as_string()?))
78        })
79        .collect()
80}
81
82/// No answer: fetch refused before the venue said anything. For `/exchange`
83/// that means unknown, never failed.
84fn unreachable(error: JsValue) -> TransportError {
85    let message = Reflect::get(&error, &JsValue::from_str("message")).ok().and_then(|message| message.as_string());
86    TransportError::Unreachable(message.unwrap_or_else(|| "the request did not complete".to_owned()))
87}
88
89/// Unix milliseconds from `Date.now()`: the [`crate::client::Clock`] in the
90/// browser and Workers, where std has no clock.
91#[must_use]
92pub fn clock() -> u64 {
93    // Whole milliseconds below 2^53, so the cast is exact.
94    js_sys::Date::now() as u64
95}
96
97#[cfg(test)]
98#[cfg(target_arch = "wasm32")]
99#[path = "tests/fetch.rs"]
100mod tests;