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