1use std::{future::Future, sync::Arc, time::Duration};
4
5use futures_util::StreamExt;
6use polyoxide_rt::{Instant, Runtime};
7
8use crate::{tungstenite::Message, Connection, WsError};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Correlated<A> {
14 Answer(A),
16 Uncorrelated,
19 Push,
22}
23
24#[derive(Debug)]
26pub(crate) struct Requests {
27 next_id: u64,
28 min_interval: Option<Duration>,
29 answer_timeout: Option<Duration>,
30 last_request: Option<Instant>,
31}
32
33impl Default for Requests {
34 fn default() -> Self {
35 Self {
36 next_id: 1,
37 min_interval: None,
38 answer_timeout: None,
39 last_request: None,
40 }
41 }
42}
43
44impl Connection {
69 pub fn min_request_interval(mut self, interval: Duration) -> Self {
74 self.requests.min_interval = Some(interval);
75 self
76 }
77
78 pub fn answer_timeout(mut self, timeout: Duration) -> Self {
81 self.requests.answer_timeout = Some(timeout);
82 self
83 }
84
85 pub fn next_id(&self) -> u64 {
87 self.requests.next_id
88 }
89
90 pub fn take_id(&mut self) -> u64 {
94 let id = self.requests.next_id;
95 self.requests.next_id += 1;
96 id
97 }
98
99 pub fn take_paced_id(&mut self) -> Option<u64> {
111 let now = self.runtime.now();
112 let held_back = match (self.requests.last_request, self.requests.min_interval) {
113 (Some(last), Some(interval)) => last.checked_add(interval).is_some_and(|at| now < at),
114 _ => false,
115 };
116 if held_back {
117 return None;
118 }
119 self.requests.last_request = Some(now);
120 Some(self.take_id())
121 }
122
123 pub async fn request<A>(
126 &mut self,
127 build: impl FnOnce(u64) -> Message,
128 mut answer: impl FnMut(u64, &Message) -> Correlated<A>,
129 ) -> Result<A, WsError> {
130 if let Some(closed) = self.closed() {
133 return Err(closed);
134 }
135 if self.is_finished() {
136 return Err(already_closed());
137 }
138 let runtime = Arc::clone(&self.runtime);
139 if let (Some(last), Some(interval)) =
140 (self.requests.last_request, self.requests.min_interval)
141 {
142 if let Some(at) = last.checked_add(interval) {
144 runtime.sleep_until(at).await;
145 }
146 }
147 let id = self.take_id();
148 let timeout = self.requests.answer_timeout;
149 within(runtime.as_ref(), timeout, async {
150 self.send(build(id)).await?;
151 self.requests.last_request = Some(self.runtime.now());
152 self.read_until(&mut |message: &Message| answer(id, message))
153 .await
154 })
155 .await
156 }
157
158 pub async fn await_answer<A>(
164 &mut self,
165 mut answer: impl FnMut(&Message) -> Correlated<A>,
166 ) -> Result<A, WsError> {
167 let timeout = self.requests.answer_timeout;
168 let runtime = Arc::clone(&self.runtime);
169 within(runtime.as_ref(), timeout, self.read_until(&mut answer)).await
170 }
171
172 async fn read_until<A>(
173 &mut self,
174 answer: &mut impl FnMut(&Message) -> Correlated<A>,
175 ) -> Result<A, WsError> {
176 loop {
177 if let Some(closed) = self.closed() {
178 return Err(closed);
179 }
180 if self.is_finished() {
181 return Err(already_closed());
182 }
183 let message = match self.socket().next().await {
184 Some(Ok(message)) => message,
185 Some(Err(err)) => {
186 self.fail();
187 return Err(err.into());
188 }
189 None => {
190 self.record_close(None);
191 continue;
192 }
193 };
194 self.touch();
195 if let Message::Close(_) = message {
196 self.record_close(Some(&message));
197 self.keep(message);
198 self.flush_reply().await;
199 continue;
200 }
201 match answer(&message) {
202 Correlated::Answer(answer) => return Ok(answer),
203 Correlated::Uncorrelated => {
204 tracing::debug!(?message, "dropped an answer nobody awaits");
205 }
206 Correlated::Push => self.keep(message),
207 }
208 }
209 }
210}
211
212async fn within<A>(
214 runtime: &dyn Runtime,
215 timeout: Option<Duration>,
216 exchange: impl Future<Output = Result<A, WsError>>,
217) -> Result<A, WsError> {
218 match timeout {
219 Some(timeout) => polyoxide_rt::timeout(runtime, timeout, exchange)
220 .await
221 .ok_or_else(|| WsError::no_answer(timeout))?,
222 None => exchange.await,
223 }
224}
225
226fn already_closed() -> WsError {
228 WsError::from(crate::tungstenite::Error::AlreadyClosed)
229}
230
231#[cfg(test)]
232mod tests {
233 use serde_json::{json, Value};
234 use tokio_tungstenite::tungstenite::protocol::{frame::coding::CloseCode, CloseFrame};
235
236 use super::*;
237 use crate::{
238 connect,
239 test_server::{next_item, Script, ScriptedServer},
240 Kind, Mode, DEFAULT_CONNECT_TIMEOUT,
241 };
242
243 fn text(value: Value) -> Message {
244 Message::Text(value.to_string().into())
245 }
246
247 fn parse(message: &Message) -> Option<Value> {
248 match message {
249 Message::Text(text) => serde_json::from_str(text).ok(),
250 _ => None,
251 }
252 }
253
254 async fn answering<A>(answer: A) -> ScriptedServer
257 where
258 A: FnMut(&str) -> Vec<Message> + Clone + Send + 'static,
259 {
260 ScriptedServer::start_answering(vec![Script::hold()], move |_| answer.clone()).await
261 }
262
263 async fn open(server: &ScriptedServer) -> Connection {
264 let socket = connect(server.url.as_str(), DEFAULT_CONNECT_TIMEOUT)
265 .await
266 .expect("connect");
267 Connection::new(socket, Mode::Data)
268 }
269
270 async fn next_text(connection: &mut Connection) -> Value {
271 let message = tokio::time::timeout(Duration::from_secs(2), connection.next())
272 .await
273 .expect("a frame within 2 s")
274 .expect("the stream is open")
275 .expect("not an error");
276 parse(&message).expect("a JSON text frame")
277 }
278
279 mod perps {
282 use super::*;
283
284 pub(super) fn subscribe(channels: &'static [&'static str]) -> impl FnOnce(u64) -> Message {
285 move |id| text(json!({"id": id, "req": "sub", "chs": channels}))
286 }
287
288 pub(super) fn answer(id: u64, message: &Message) -> Correlated<Value> {
289 match parse(message) {
290 Some(frame) if frame.get("ch").is_some() => Correlated::Push,
291 Some(frame) if frame["id"] == id => Correlated::Answer(frame["data"].clone()),
292 Some(frame) if frame.get("id").is_some() => Correlated::Uncorrelated,
293 _ => Correlated::Push,
294 }
295 }
296
297 pub(super) fn push(sq: u64) -> Message {
298 text(json!({"ch": "bbo::1", "ts": 1, "sq": sq, "data": {}}))
299 }
300
301 pub(super) fn statuses(request: &str) -> Message {
302 let request: Value = serde_json::from_str(request).unwrap();
303 let ok: Vec<Value> = request["chs"]
304 .as_array()
305 .unwrap()
306 .iter()
307 .map(|_| json!({"status": "ok"}))
308 .collect();
309 text(json!({"id": request["id"], "data": ok}))
310 }
311 }
312
313 mod binance {
316 use super::*;
317
318 pub(super) fn subscribe(name: &'static str) -> impl FnOnce(u64) -> Message {
319 move |id| text(json!({"method": "SUBSCRIBE", "params": [name], "id": id}))
320 }
321
322 pub(super) fn answer(id: u64, message: &Message) -> Correlated<Result<Value, i64>> {
323 match parse(message) {
324 Some(frame) if frame.get("stream").is_some() => Correlated::Push,
325 Some(frame) if frame["id"] == id => Correlated::Answer(match frame.get("error") {
326 Some(error) => Err(error["code"].as_i64().unwrap()),
327 None => Ok(frame["result"].clone()),
328 }),
329 Some(_) => Correlated::Uncorrelated,
330 None => Correlated::Push,
331 }
332 }
333
334 pub(super) fn push(n: u64) -> Message {
335 text(json!({"stream": "btcusdt@aggTrade", "data": {"a": n}}))
336 }
337
338 pub(super) fn result(request: &str) -> Message {
339 let request: Value = serde_json::from_str(request).unwrap();
340 text(json!({"result": null, "id": request["id"]}))
341 }
342 }
343
344 #[tokio::test]
345 async fn ids_count_from_one() {
346 let server = ScriptedServer::start(vec![Script::hold()]).await;
347 let mut connection = open(&server).await;
348 assert_eq!(connection.next_id(), 1);
349 assert_eq!(connection.take_id(), 1);
350 assert_eq!(connection.take_id(), 2);
351 assert_eq!(connection.next_id(), 3);
352 }
353
354 #[tokio::test]
355 async fn a_request_after_a_failed_read_sends_nothing() {
356 let server = ScriptedServer::start(vec![Script::hold().then_cut()]).await;
357 let mut connection = open(&server).await;
358 assert!(next_item(&mut connection).await.is_err());
359 let err = connection
360 .request(binance::subscribe("a@aggTrade"), binance::answer)
361 .await
362 .expect_err("the connection is gone");
363 assert_eq!(err.kind(), Kind::AlreadyClosed);
364 assert_eq!(connection.next_id(), 1, "no id was taken");
365 }
366
367 #[tokio::test]
368 async fn an_interval_too_long_to_add_is_never_waited() {
369 let server = answering(|request: &str| vec![binance::result(request)]).await;
370 let mut connection = open(&server)
371 .await
372 .min_request_interval(Duration::MAX)
373 .answer_timeout(Duration::from_secs(2));
374 for name in ["a@aggTrade", "b@aggTrade"] {
375 let result = connection
376 .request(binance::subscribe(name), binance::answer)
377 .await
378 .expect("an answer, not a panic or a wait");
379 assert_eq!(result, Ok(Value::Null));
380 }
381 }
382
383 #[tokio::test]
384 async fn a_request_refreshes_last_inbound() {
385 let server = answering(|request: &str| vec![binance::result(request)]).await;
386 let mut connection = open(&server).await;
387 let before = connection.last_inbound();
388 tokio::time::sleep(Duration::from_millis(10)).await;
389 let result = connection
390 .request(binance::subscribe("a@aggTrade"), binance::answer)
391 .await
392 .expect("an answer");
393 assert_eq!(result, Ok(Value::Null));
394 assert!(
395 connection.last_inbound() > before,
396 "the answer, read by the request, must count as inbound"
397 );
398 }
399
400 #[tokio::test]
401 async fn all_mode_yields_a_close_read_while_awaiting() {
402 let server = answering(|_: &str| {
403 vec![Message::Close(Some(CloseFrame {
404 code: CloseCode::Policy,
405 reason: "Too many requests".into(),
406 }))]
407 })
408 .await;
409 let socket = connect(server.url.as_str(), DEFAULT_CONNECT_TIMEOUT)
410 .await
411 .expect("connect");
412 let mut connection = Connection::new(socket, Mode::All);
413 let err = connection
414 .request(binance::subscribe("a@aggTrade"), binance::answer)
415 .await
416 .expect_err("closed");
417 assert!(
418 matches!(
419 err.kind(),
420 Kind::Closed {
421 code: Some(1008),
422 ..
423 }
424 ),
425 "{err:?}"
426 );
427 assert!(matches!(
428 next_item(&mut connection).await,
429 Ok(Message::Close(Some(_)))
430 ));
431 assert!(connection.next().await.is_none());
432 }
433
434 #[tokio::test]
435 async fn perps_pushes_read_before_the_answer_are_kept_and_yielded_in_order() {
436 let server = answering(|request: &str| {
437 vec![perps::push(10), perps::push(11), perps::statuses(request)]
438 })
439 .await;
440 let mut connection = open(&server).await;
441 let data = connection
442 .request(perps::subscribe(&["bbo::1", "trades::1"]), perps::answer)
443 .await
444 .expect("an answer");
445 assert_eq!(data, json!([{"status": "ok"}, {"status": "ok"}]));
446 assert_eq!(next_text(&mut connection).await["sq"], 10);
447 assert_eq!(next_text(&mut connection).await["sq"], 11);
448 }
449
450 #[tokio::test]
451 async fn perps_an_answer_to_another_request_is_dropped() {
452 let server = answering(|request: &str| {
453 vec![
454 text(json!({"id": 99, "data": {"status": "ok"}})),
455 perps::statuses(request),
456 perps::push(12),
457 ]
458 })
459 .await;
460 let mut connection = open(&server).await;
461 connection
462 .request(perps::subscribe(&["bbo::1"]), perps::answer)
463 .await
464 .expect("its own answer");
465 assert_eq!(
466 next_text(&mut connection).await["sq"],
467 12,
468 "the stray answer must not be yielded"
469 );
470 }
471
472 #[tokio::test]
473 async fn binance_requests_are_paced() {
474 let interval = Duration::from_millis(50);
475 let server = answering(|request: &str| vec![binance::result(request)]).await;
476 let mut connection = open(&server).await.min_request_interval(interval);
477 let start = Instant::now();
478 for name in ["a@aggTrade", "b@aggTrade", "c@aggTrade"] {
479 let result = connection
480 .request(binance::subscribe(name), binance::answer)
481 .await
482 .expect("an answer");
483 assert_eq!(result, Ok(Value::Null));
484 }
485 assert!(
486 start.elapsed() >= interval * 2,
487 "three requests in {:?}",
488 start.elapsed()
489 );
490 }
491
492 #[tokio::test]
493 async fn binance_an_unanswered_request_is_no_answer_and_pushes_meanwhile_are_kept() {
494 let after = Duration::from_millis(200);
495 let server = answering(|_: &str| vec![binance::push(1), binance::push(2)]).await;
496 let mut connection = open(&server).await.answer_timeout(after);
497 let err = connection
498 .request(binance::subscribe("a@aggTrade"), binance::answer)
499 .await
500 .expect_err("nothing answers it");
501 assert_eq!(err.kind(), Kind::NoAnswer { after });
502 assert_eq!(next_text(&mut connection).await["data"]["a"], 1);
503 assert_eq!(next_text(&mut connection).await["data"]["a"], 2);
504 }
505
506 #[tokio::test]
507 async fn binance_a_late_answer_is_not_taken_for_the_next() {
508 let server = ScriptedServer::start_answering(vec![Script::hold()], |_| {
512 let mut held: Option<Message> = None;
513 move |request: &str| {
514 let value: Value = serde_json::from_str(request).unwrap();
515 match value["id"].as_u64() {
516 Some(2) => {
517 held = Some(text(
518 json!({"error": {"code": 2, "msg": "Invalid request"}, "id": 2}),
519 ));
520 Vec::new()
521 }
522 Some(3) => vec![
523 binance::push(7),
524 held.take().expect("request 2's answer"),
525 binance::result(request),
526 ],
527 _ => vec![binance::result(request)],
528 }
529 }
530 })
531 .await;
532 let mut connection = open(&server)
533 .await
534 .answer_timeout(Duration::from_millis(200));
535 for (name, expected) in [
536 ("a@aggTrade", Ok(Ok(Value::Null))),
537 ("b@aggTrade", Err(())),
538 ("c@aggTrade", Ok(Ok(Value::Null))),
539 ] {
540 let result = connection
541 .request(binance::subscribe(name), binance::answer)
542 .await
543 .map_err(|err| assert!(matches!(err.kind(), Kind::NoAnswer { .. }), "{err:?}"));
544 assert_eq!(result, expected, "{name}");
545 }
546 let push = tokio::time::timeout(Duration::from_secs(2), connection.next())
547 .await
548 .expect("the push read during request 3 was dropped");
549 assert_eq!(parse(&push.unwrap().unwrap()).unwrap()["data"]["a"], 7);
550 }
551
552 #[tokio::test]
553 async fn binance_an_error_answer_is_the_callers_to_read() {
554 let server = answering(|request: &str| {
555 let value: Value = serde_json::from_str(request).unwrap();
556 vec![text(
557 json!({"error": {"code": 2, "msg": "Invalid request"}, "id": value["id"]}),
558 )]
559 })
560 .await;
561 let mut connection = open(&server).await;
562 let result = connection
563 .request(binance::subscribe("a@aggTrade"), binance::answer)
564 .await
565 .expect("an answer");
566 assert_eq!(result, Err(2));
567 }
568
569 #[tokio::test]
570 async fn a_protocol_ping_awaits_its_pong_without_taking_an_id() {
571 let server = ScriptedServer::start(vec![Script::hold()]).await;
572 let mut connection = open(&server).await.answer_timeout(Duration::from_secs(2));
573 connection
574 .send(Message::Ping(Vec::new().into()))
575 .await
576 .expect("ping");
577 connection
578 .await_answer(|message| match message {
579 Message::Pong(_) => Correlated::Answer(()),
580 _ => Correlated::Push,
581 })
582 .await
583 .expect("a pong");
584 assert_eq!(connection.next_id(), 1);
585 }
586
587 #[tokio::test]
588 async fn a_close_while_awaiting_is_answered() {
589 let server = ScriptedServer::start(vec![Script::hold()
592 .after_first_frame()
593 .then_close()
594 .close_with(1008, "Too many requests")])
595 .await;
596 let mut connection = open(&server).await;
597 let err = connection
598 .request(binance::subscribe("a@aggTrade"), binance::answer)
599 .await
600 .expect_err("closed");
601 assert!(
602 matches!(
603 err.kind(),
604 Kind::Closed {
605 code: Some(1008),
606 ..
607 }
608 ),
609 "{err:?}"
610 );
611 server
612 .wait_for("the close reply", |s| s.close_reply_count() == 1)
613 .await;
614 }
615
616 #[tokio::test]
617 async fn a_close_while_awaiting_fails_the_request_and_every_later_one() {
618 let server = answering(|_: &str| {
619 vec![Message::Close(Some(CloseFrame {
620 code: CloseCode::Policy,
621 reason: "Too many requests".into(),
622 }))]
623 })
624 .await;
625 let mut connection = open(&server).await;
626 let closed = Kind::Closed {
627 code: Some(1008),
628 reason: "Too many requests",
629 };
630 let err = connection
631 .request(binance::subscribe("a@aggTrade"), binance::answer)
632 .await
633 .expect_err("closed");
634 assert_eq!(err.kind(), closed);
635 let again = connection
636 .request(binance::subscribe("b@aggTrade"), binance::answer)
637 .await
638 .expect_err("still closed");
639 assert_eq!(again.kind(), closed);
640 assert_eq!(connection.next_id(), 2, "nothing was sent after the close");
641 assert!(connection.next().await.is_none());
642 }
643}