df_hyperliquid_core/live/
session.rs1use crate::client::Network;
6use crate::live::frame::{BadFrame, Frame, decode};
7use crate::live::request::{Post, post, subscribe, unsubscribe};
8use crate::live::subscription::{BadSubscription, Subscription};
9
10pub const MAX_CONNECTIONS: usize = 10;
12pub const MAX_NEW_CONNECTIONS_PER_MINUTE: usize = 30;
14pub const MAX_SUBSCRIPTIONS: usize = 1_000;
16pub const MAX_USERS: usize = 10;
18pub const MAX_MESSAGES_PER_MINUTE: usize = 2_000;
20pub const MAX_INFLIGHT_POSTS: usize = 100;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub struct Limits {
27 pub subscriptions: usize,
28 pub users: usize,
29 pub posts: usize,
30}
31
32impl Limits {
33 pub const VENUE: Limits = Limits { subscriptions: MAX_SUBSCRIPTIONS, users: MAX_USERS, posts: MAX_INFLIGHT_POSTS };
34}
35
36impl Default for Limits {
37 fn default() -> Self {
38 Limits::VENUE
39 }
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct Timing {
45 pub ping_after_ms: u64,
48 pub silent_ms: u64,
51 pub steady_ms: u64,
54 pub first_retry_ms: u64,
56 pub last_retry_ms: u64,
58}
59
60impl Timing {
61 pub const DEFAULT: Timing = Timing {
64 ping_after_ms: 30_000,
65 silent_ms: 60_000,
66 steady_ms: 30_000,
67 first_retry_ms: 250,
68 last_retry_ms: 20_000,
69 };
70}
71
72impl Default for Timing {
73 fn default() -> Self {
74 Timing::DEFAULT
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
80pub enum Refused {
81 #[error("{limit} subscriptions is the most this session holds; the venue allows an IP 1000")]
82 Subscriptions { limit: usize },
83 #[error("{limit} users is the most this session follows; the venue allows an IP 10")]
84 Users { limit: usize },
85 #[error("{limit} posts are already waiting for an answer; the venue allows an IP 100")]
86 Posts { limit: usize },
87 #[error("the socket is not open, and a post is never held to send later")]
90 Closed,
91 #[error(transparent)]
92 Bad(#[from] BadSubscription),
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum Tick {
98 Wait,
99 Ping,
101 Reconnect,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct Retry {
109 pub after_ms: u64,
111 pub unanswered: Vec<u64>,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117enum Link {
118 Closed,
119 Open {
120 since: u64,
122 heard: u64,
124 talked: bool,
126 pinged: u64,
128 },
129}
130
131#[derive(Debug, Clone)]
133pub struct Session {
134 network: Network,
135 limits: Limits,
136 timing: Timing,
137 wanted: Vec<Subscription>,
138 link: Link,
139 failures: u32,
141 next_id: u64,
142 waiting: Vec<u64>,
143}
144
145impl Session {
146 #[must_use]
148 pub fn new(network: Network) -> Self {
149 Session {
150 network,
151 limits: Limits::VENUE,
152 timing: Timing::DEFAULT,
153 wanted: Vec::new(),
154 link: Link::Closed,
155 failures: 0,
156 next_id: 1,
157 waiting: Vec::new(),
158 }
159 }
160
161 #[must_use]
163 pub const fn with_limits(mut self, limits: Limits) -> Self {
164 self.limits = limits;
165 self
166 }
167
168 #[must_use]
169 pub const fn with_timing(mut self, timing: Timing) -> Self {
170 self.timing = timing;
171 self
172 }
173
174 #[must_use]
175 pub const fn network(&self) -> Network {
176 self.network
177 }
178
179 #[must_use]
181 pub const fn url(&self) -> &'static str {
182 self.network.ws_url()
183 }
184
185 #[must_use]
186 pub const fn is_open(&self) -> bool {
187 matches!(self.link, Link::Open { .. })
188 }
189
190 #[must_use]
192 pub fn wanted(&self) -> &[Subscription] {
193 &self.wanted
194 }
195
196 #[must_use]
198 pub fn users(&self) -> usize {
199 let mut users: Vec<_> = self.wanted.iter().filter_map(Subscription::user).collect();
200 users.sort_unstable();
201 users.dedup();
202 users.len()
203 }
204
205 #[must_use]
207 pub fn waiting(&self) -> &[u64] {
208 &self.waiting
209 }
210
211 pub fn subscribe(&mut self, subscription: Subscription) -> Result<Option<String>, Refused> {
215 if self.wanted.contains(&subscription) {
216 return Ok(None);
217 }
218 subscription.check()?;
219 if self.wanted.len() >= self.limits.subscriptions {
220 return Err(Refused::Subscriptions { limit: self.limits.subscriptions });
221 }
222 if let Some(user) = subscription.user()
223 && !self.wanted.iter().any(|wanted| wanted.user() == Some(user))
224 && self.users() >= self.limits.users
225 {
226 return Err(Refused::Users { limit: self.limits.users });
227 }
228 let frame = self.is_open().then(|| subscribe(&subscription));
229 self.wanted.push(subscription);
230 Ok(frame)
231 }
232
233 pub fn unsubscribe(&mut self, subscription: &Subscription) -> Option<String> {
236 let at = self.wanted.iter().position(|wanted| wanted == subscription)?;
237 let gone = self.wanted.remove(at);
238 self.is_open().then(|| unsubscribe(&gone))
239 }
240
241 pub fn post(&mut self, request: &Post) -> Result<(u64, String), Refused> {
244 if !self.is_open() {
245 return Err(Refused::Closed);
246 }
247 if self.waiting.len() >= self.limits.posts {
248 return Err(Refused::Posts { limit: self.limits.posts });
249 }
250 let id = self.next_id;
251 self.next_id = self.next_id.saturating_add(1);
252 self.waiting.push(id);
253 Ok((id, post(id, request)))
254 }
255
256 pub fn opened(&mut self, now: u64) -> Vec<String> {
259 self.link = Link::Open { since: now, heard: now, talked: false, pinged: now };
260 self.wanted.iter().map(subscribe).collect()
261 }
262
263 pub fn received(&mut self, now: u64, text: &str) -> Result<Frame, BadFrame> {
266 if let Link::Open { heard, talked, .. } = &mut self.link {
267 *heard = (*heard).max(now);
268 *talked = true;
269 }
270 let frame = decode(text);
271 let answered = match &frame {
272 Ok(Frame::Post(reply)) => Some(reply.id),
273 Ok(Frame::Error(refusal)) => refusal.post_id(),
274 _ => None,
275 };
276 if let Some(id) = answered {
277 self.waiting.retain(|waiting| *waiting != id);
278 }
279 frame
280 }
281
282 pub fn tick(&mut self, now: u64) -> Tick {
286 let Link::Open { heard, pinged, .. } = &mut self.link else { return Tick::Wait };
287 if now.saturating_sub(*heard) >= self.timing.silent_ms {
288 return Tick::Reconnect;
289 }
290 if now.saturating_sub((*heard).max(*pinged)) >= self.timing.ping_after_ms {
291 *pinged = now;
292 return Tick::Ping;
293 }
294 Tick::Wait
295 }
296
297 #[must_use]
299 pub fn due(&self) -> Option<u64> {
300 let Link::Open { heard, pinged, .. } = self.link else { return None };
301 let ping = heard.max(pinged).saturating_add(self.timing.ping_after_ms);
302 let silent = heard.saturating_add(self.timing.silent_ms);
303 Some(ping.min(silent))
304 }
305
306 pub fn closed(&mut self, now: u64, random: u64) -> Retry {
312 if let Link::Open { since, talked: true, .. } = self.link
313 && now.saturating_sub(since) >= self.timing.steady_ms
314 {
315 self.failures = 0;
316 }
317 self.link = Link::Closed;
318 let doubled = 1u64.checked_shl(self.failures).unwrap_or(u64::MAX);
319 let step = self.timing.first_retry_ms.saturating_mul(doubled).min(self.timing.last_retry_ms);
320 let jitter = random % (step / 4).saturating_add(1);
321 self.failures = self.failures.saturating_add(1);
322 Retry { after_ms: step.saturating_add(jitter), unanswered: std::mem::take(&mut self.waiting) }
323 }
324}
325
326#[cfg(test)]
327#[path = "tests/session.rs"]
328mod tests;