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        request.refuse_if_offline()?;
49        let global = js_sys::global();
50        let fetch: Function =
51            Reflect::get(&global, &JsValue::from_str("fetch")).map_err(unreachable)?.dyn_into().map_err(unreachable)?;
52        let headers = Headers::new().map_err(unreachable)?;
53        for (name, value) in &request.headers {
54            headers.append(name, value).map_err(unreachable)?;
55        }
56        let init = RequestInit::new();
57        init.set_method(match request.method {
58            Method::Get => "GET",
59            Method::Post => "POST",
60        });
61        init.set_headers(&headers);
62        if !request.body.is_empty() {
63            init.set_body(&Uint8Array::from(request.body.as_slice()));
64        }
65        let promise: Promise = fetch
66            .call2(&global, &JsValue::from_str(&request.url), &init)
67            .map_err(unreachable)?
68            .dyn_into()
69            .map_err(unreachable)?;
70        let answer: web_sys::Response =
71            JsFuture::from(promise).await.map_err(unreachable)?.dyn_into().map_err(unreachable)?;
72        let status = answer.status();
73        let headers = headers_of(&answer.headers());
74        let buffer = JsFuture::from(answer.array_buffer().map_err(unreachable)?).await.map_err(unreachable)?;
75        Ok(Response { status, headers, body: Uint8Array::new(&buffer).to_vec() })
76    }
77}
78
79/// Every header as a name and a value. Fetch gives names in lower case.
80fn headers_of(headers: &Headers) -> Vec<(String, String)> {
81    let Ok(Some(entries)) = js_sys::try_iter(headers) else { return Vec::new() };
82    entries
83        .filter_map(Result::ok)
84        .filter_map(|entry| {
85            let pair: Array = entry.dyn_into().ok()?;
86            Some((pair.get(0).as_string()?, pair.get(1).as_string()?))
87        })
88        .collect()
89}
90
91/// No answer: fetch refused before the venue said anything. For `/exchange`
92/// that means unknown, never failed.
93fn unreachable(error: JsValue) -> TransportError {
94    let message = Reflect::get(&error, &JsValue::from_str("message")).ok().and_then(|message| message.as_string());
95    TransportError::Unreachable(message.unwrap_or_else(|| "the request did not complete".to_owned()))
96}
97
98/// Unix milliseconds from `Date.now()`: the [`crate::client::Clock`] in the
99/// browser and Workers, where std has no clock.
100#[must_use]
101pub fn clock() -> u64 {
102    // Whole milliseconds below 2^53, so the cast is exact.
103    js_sys::Date::now() as u64
104}
105
106#[cfg(test)]
107#[cfg(target_arch = "wasm32")]
108#[path = "tests/fetch.rs"]
109mod tests;