1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4#![warn(rustdoc::broken_intra_doc_links)]
5
6#[cfg(target_arch = "wasm32")]
7mod web {
8 use std::{
9 cell::{Cell, RefCell},
10 collections::{HashMap, VecDeque},
11 pin::Pin,
12 rc::Rc,
13 time::Duration,
14 };
15
16 use base64::{
17 Engine as _,
18 engine::general_purpose::{STANDARD, STANDARD_NO_PAD},
19 };
20 use futures::{FutureExt, Sink, SinkExt, StreamExt, channel::mpsc, future::poll_fn};
21 use gloo_net::http::Request;
22 use gloo_net::websocket::{Message, State, WebSocketError, futures::WebSocket};
23 use gloo_timers::future::TimeoutFuture;
24 use phoenix_channel_client::{
25 ConnectContext, Connector, Endpoint, ResolvedEndpoint, Socket, SocketStatus, Timer,
26 };
27 use phoenix_channel_runtime::{
28 Transport, TransportClose, TransportCloseRequest, TransportError, TransportErrorKind,
29 TransportEvent, WireMessage,
30 };
31 use serde::Deserialize;
32 use wasm_bindgen::{JsCast, JsValue, closure::Closure};
33 use web_sys::{AbortController, Event, VisibilityState, Window};
34
35 const AUTH_TOKEN_PREFIX: &str = "base64url.bearer.phx.";
36 const DEFAULT_LONG_POLL_TIMEOUT: Duration = Duration::from_secs(20);
37 const HEALTH_CHECK_REFERENCE: &str = "phoenix-channel-runtime-health";
38
39 pub struct WebTransport {
41 inner: Option<WebSocket>,
42 queued: VecDeque<TransportEvent>,
43 }
44
45 pub struct LongPollTransport {
47 endpoint: url::Url,
48 token: Rc<RefCell<Option<String>>>,
49 auth_token: Option<String>,
50 events: mpsc::UnboundedReceiver<Result<TransportEvent, TransportError>>,
51 closed: Rc<Cell<bool>>,
52 request_timeout: Duration,
53 requests: Rc<RefCell<HashMap<u64, AbortController>>>,
54 next_request_id: Rc<Cell<u64>>,
55 }
56
57 pub struct WebLifecycle {
62 window: Window,
63 pagehide: Closure<dyn FnMut(Event)>,
64 pageshow: Closure<dyn FnMut(Event)>,
65 visibilitychange: Closure<dyn FnMut(Event)>,
66 offline: Closure<dyn FnMut(Event)>,
67 online: Closure<dyn FnMut(Event)>,
68 }
69
70 impl WebLifecycle {
71 pub fn attach(socket: Socket) -> Result<Self, JsValue> {
73 let window =
74 web_sys::window().ok_or_else(|| JsValue::from_str("window unavailable"))?;
75 let resume_after_page_show = Rc::new(Cell::new(false));
76 let resume_after_online = Rc::new(Cell::new(false));
77
78 let pagehide = {
79 let socket = socket.clone();
80 let resume = resume_after_page_show.clone();
81 Closure::wrap(Box::new(move |_event: Event| {
82 if is_active(socket.status()) {
83 resume.set(true);
84 let socket = socket.clone();
85 wasm_bindgen_futures::spawn_local(async move {
86 let _ = socket.disconnect().await;
87 });
88 }
89 }) as Box<dyn FnMut(Event)>)
90 };
91 let pageshow = {
92 let socket = socket.clone();
93 let resume = resume_after_page_show;
94 Closure::wrap(Box::new(move |_event: Event| {
95 if resume.replace(false) {
96 let socket = socket.clone();
97 wasm_bindgen_futures::spawn_local(async move {
98 let _ = socket.connect().await;
99 });
100 }
101 }) as Box<dyn FnMut(Event)>)
102 };
103 let visibilitychange = {
104 let socket = socket.clone();
105 let window = window.clone();
106 Closure::wrap(Box::new(move |_event: Event| {
107 let visible = window.document().is_some_and(|document| {
108 document.visibility_state() == VisibilityState::Visible
109 });
110 if visible && socket.status() == SocketStatus::WaitingToReconnect {
111 let socket = socket.clone();
112 wasm_bindgen_futures::spawn_local(async move {
113 let _ = socket.reconnect().await;
114 });
115 }
116 }) as Box<dyn FnMut(Event)>)
117 };
118 let offline = {
119 let socket = socket.clone();
120 let resume = resume_after_online.clone();
121 Closure::wrap(Box::new(move |_event: Event| {
122 if is_active(socket.status()) {
123 resume.set(true);
124 let socket = socket.clone();
125 wasm_bindgen_futures::spawn_local(async move {
126 let _ = socket.disconnect().await;
127 });
128 }
129 }) as Box<dyn FnMut(Event)>)
130 };
131 let online = {
132 let socket = socket.clone();
133 let resume = resume_after_online;
134 Closure::wrap(Box::new(move |_event: Event| {
135 if resume.replace(false) {
136 let socket = socket.clone();
137 wasm_bindgen_futures::spawn_local(async move {
138 let _ = socket.connect().await;
139 });
140 }
141 }) as Box<dyn FnMut(Event)>)
142 };
143
144 add_listener(&window, "pagehide", &pagehide)?;
145 add_listener(&window, "pageshow", &pageshow)?;
146 add_listener(&window, "visibilitychange", &visibilitychange)?;
147 add_listener(&window, "offline", &offline)?;
148 add_listener(&window, "online", &online)?;
149 Ok(Self {
150 window,
151 pagehide,
152 pageshow,
153 visibilitychange,
154 offline,
155 online,
156 })
157 }
158 }
159
160 impl Drop for WebLifecycle {
161 fn drop(&mut self) {
162 remove_listener(&self.window, "pagehide", &self.pagehide);
163 remove_listener(&self.window, "pageshow", &self.pageshow);
164 remove_listener(&self.window, "visibilitychange", &self.visibilitychange);
165 remove_listener(&self.window, "offline", &self.offline);
166 remove_listener(&self.window, "online", &self.online);
167 }
168 }
169
170 fn is_active(status: SocketStatus) -> bool {
171 matches!(
172 status,
173 SocketStatus::Connecting | SocketStatus::Connected | SocketStatus::WaitingToReconnect
174 )
175 }
176
177 fn add_listener(
178 window: &Window,
179 name: &str,
180 callback: &Closure<dyn FnMut(Event)>,
181 ) -> Result<(), JsValue> {
182 window.add_event_listener_with_callback(name, callback.as_ref().unchecked_ref())
183 }
184
185 fn remove_listener(window: &Window, name: &str, callback: &Closure<dyn FnMut(Event)>) {
186 let _ = window.remove_event_listener_with_callback(name, callback.as_ref().unchecked_ref());
187 }
188
189 #[derive(Clone)]
191 pub struct WebConnector {
192 endpoint: WebEndpoint,
193 long_poll_fallback: Option<Duration>,
194 long_poll_timeout: Duration,
195 }
196
197 #[derive(Clone)]
199 pub struct LongPollConnector {
200 endpoint: WebEndpoint,
201 request_timeout: Duration,
202 }
203
204 #[derive(Clone)]
205 enum WebEndpoint {
206 Url(String),
207 Phoenix(Endpoint),
208 }
209
210 impl WebConnector {
211 pub fn new(url: impl Into<String>) -> Self {
213 Self {
214 endpoint: WebEndpoint::Url(url.into()),
215 long_poll_fallback: None,
216 long_poll_timeout: DEFAULT_LONG_POLL_TIMEOUT,
217 }
218 }
219
220 pub fn from_endpoint(endpoint: Endpoint) -> Self {
222 Self {
223 endpoint: WebEndpoint::Phoenix(endpoint),
224 long_poll_fallback: None,
225 long_poll_timeout: DEFAULT_LONG_POLL_TIMEOUT,
226 }
227 }
228
229 pub fn long_poll_fallback(mut self, after: Duration) -> Self {
231 self.long_poll_fallback = Some(after);
232 self
233 }
234
235 pub fn long_poll_timeout(mut self, timeout: Duration) -> Self {
237 self.long_poll_timeout = timeout;
238 self
239 }
240 }
241
242 impl LongPollConnector {
243 pub fn new(url: impl Into<String>) -> Self {
245 Self {
246 endpoint: WebEndpoint::Url(url.into()),
247 request_timeout: DEFAULT_LONG_POLL_TIMEOUT,
248 }
249 }
250
251 pub fn from_endpoint(endpoint: Endpoint) -> Self {
253 Self {
254 endpoint: WebEndpoint::Phoenix(endpoint),
255 request_timeout: DEFAULT_LONG_POLL_TIMEOUT,
256 }
257 }
258
259 pub fn request_timeout(mut self, timeout: Duration) -> Self {
261 self.request_timeout = timeout;
262 self
263 }
264 }
265
266 async fn resolve_endpoint(
267 endpoint: WebEndpoint,
268 context: ConnectContext,
269 ) -> Result<ResolvedEndpoint, TransportError> {
270 match endpoint {
271 WebEndpoint::Url(url) => Ok(ResolvedEndpoint {
272 url,
273 protocols: Vec::new(),
274 }),
275 WebEndpoint::Phoenix(endpoint) => endpoint.resolve(context).await.map_err(|error| {
276 TransportError::with_kind(TransportErrorKind::Connect, error.to_string())
277 }),
278 }
279 }
280
281 impl Connector for WebConnector {
282 fn connect(
283 &self,
284 context: ConnectContext,
285 ) -> futures::future::LocalBoxFuture<'static, Result<Box<dyn Transport>, TransportError>>
286 {
287 let endpoint = self.endpoint.clone();
288 let fallback = self.long_poll_fallback;
289 let long_poll_timeout = self.long_poll_timeout;
290 Box::pin(async move {
291 let endpoint = resolve_endpoint(endpoint, context).await?;
292 if let Some(after) = fallback {
293 if let Ok(transport) =
294 WebTransport::connect_with_health_check(endpoint.clone(), after).await
295 {
296 return Ok(Box::new(transport) as Box<dyn Transport>);
297 }
298 let transport =
299 LongPollTransport::connect_resolved(endpoint, long_poll_timeout).await?;
300 Ok(Box::new(transport) as Box<dyn Transport>)
301 } else {
302 let transport = WebTransport::connect_resolved(endpoint).await?;
303 Ok(Box::new(transport) as Box<dyn Transport>)
304 }
305 })
306 }
307 }
308
309 impl Connector for LongPollConnector {
310 fn connect(
311 &self,
312 context: ConnectContext,
313 ) -> futures::future::LocalBoxFuture<'static, Result<Box<dyn Transport>, TransportError>>
314 {
315 let endpoint = self.endpoint.clone();
316 let request_timeout = self.request_timeout;
317 Box::pin(async move {
318 let endpoint = resolve_endpoint(endpoint, context).await?;
319 let transport =
320 LongPollTransport::connect_resolved(endpoint, request_timeout).await?;
321 Ok(Box::new(transport) as Box<dyn Transport>)
322 })
323 }
324 }
325
326 #[derive(Clone, Copy, Debug, Default)]
328 pub struct WebTimer;
329
330 impl Timer for WebTimer {
331 fn sleep(&self, duration: Duration) -> futures::future::LocalBoxFuture<'static, ()> {
332 let milliseconds = duration.as_millis().min(u128::from(u32::MAX)) as u32;
333 Box::pin(TimeoutFuture::new(milliseconds))
334 }
335
336 fn now(&self) -> Duration {
337 web_sys::window()
338 .and_then(|window| window.performance())
339 .map(|performance| Duration::from_secs_f64(performance.now() / 1000.0))
340 .unwrap_or_default()
341 }
342 }
343
344 impl WebTransport {
345 pub fn connect(url: &str) -> Result<Self, TransportError> {
347 let inner = WebSocket::open(url).map_err(|error| {
348 TransportError::with_kind(TransportErrorKind::Connect, format!("{error:?}"))
349 })?;
350 Ok(Self {
351 inner: Some(inner),
352 queued: VecDeque::new(),
353 })
354 }
355
356 async fn connect_with_health_check(
357 endpoint: ResolvedEndpoint,
358 threshold: Duration,
359 ) -> Result<Self, TransportError> {
360 let connect = Self::connect_resolved(endpoint).fuse();
361 let open_timeout = timeout(threshold).fuse();
362 futures::pin_mut!(connect, open_timeout);
363 let mut transport = futures::select! {
364 result = connect => result?,
365 () = open_timeout => return Err(connect_error("WebSocket open timed out")),
366 };
367
368 {
369 let health_check = transport.health_check().fuse();
370 let health_timeout = timeout(threshold).fuse();
371 futures::pin_mut!(health_check, health_timeout);
372 futures::select! {
373 result = health_check => result?,
374 () = health_timeout => return Err(connect_error("WebSocket health check timed out")),
375 }
376 }
377 Ok(transport)
378 }
379
380 async fn connect_resolved(endpoint: ResolvedEndpoint) -> Result<Self, TransportError> {
381 let mut inner = if endpoint.protocols.is_empty() {
382 WebSocket::open(&endpoint.url)
383 } else {
384 WebSocket::open_with_protocols(&endpoint.url, &endpoint.protocols)
385 }
386 .map_err(|error| {
387 TransportError::with_kind(TransportErrorKind::Connect, format!("{error:?}"))
388 })?;
389 poll_fn(|context| Pin::new(&mut inner).poll_ready(context))
390 .await
391 .map_err(|error| {
392 TransportError::with_kind(TransportErrorKind::Connect, error.to_string())
393 })?;
394 if !matches!(inner.state(), State::Open) {
395 return Err(TransportError::with_kind(
396 TransportErrorKind::Connect,
397 "WebSocket closed before the open event",
398 ));
399 }
400 Ok(Self {
401 inner: Some(inner),
402 queued: VecDeque::new(),
403 })
404 }
405
406 async fn health_check(&mut self) -> Result<(), TransportError> {
407 let message =
408 serde_json::json!([null, HEALTH_CHECK_REFERENCE, "phoenix", "heartbeat", {}])
409 .to_string();
410 let inner = self.inner.as_mut().ok_or_else(closed_error)?;
411 inner.send(Message::Text(message)).await.map_err(|error| {
412 TransportError::with_kind(TransportErrorKind::Send, format!("{error:?}"))
413 })?;
414
415 loop {
416 let Some(message) = inner.next().await else {
417 return Err(connect_error("WebSocket ended during health check"));
418 };
419 let event = websocket_event(message)?;
420 if let TransportEvent::Message(WireMessage::Text(text)) = &event {
421 if is_health_check_ack(text) {
422 return Ok(());
423 }
424 }
425 if matches!(event, TransportEvent::Closed(_)) {
426 return Err(connect_error("WebSocket closed during health check"));
427 }
428 self.queued.push_back(event);
429 }
430 }
431 }
432
433 fn is_health_check_ack(text: &str) -> bool {
434 let Ok(frame) = serde_json::from_str::<serde_json::Value>(text) else {
435 return false;
436 };
437 let Some(frame) = frame.as_array() else {
438 return false;
439 };
440 frame.get(1).and_then(serde_json::Value::as_str) == Some(HEALTH_CHECK_REFERENCE)
441 && frame.get(2).and_then(serde_json::Value::as_str) == Some("phoenix")
442 && frame.get(3).and_then(serde_json::Value::as_str) == Some("phx_reply")
443 && frame
444 .get(4)
445 .and_then(|payload| payload.get("status"))
446 .and_then(serde_json::Value::as_str)
447 == Some("ok")
448 }
449
450 #[derive(Deserialize)]
451 struct PollResponse {
452 status: u16,
453 #[serde(default)]
454 token: Option<String>,
455 #[serde(default)]
456 messages: Vec<String>,
457 }
458
459 impl LongPollTransport {
460 async fn connect_resolved(
461 endpoint: ResolvedEndpoint,
462 request_timeout: Duration,
463 ) -> Result<Self, TransportError> {
464 let mut url = url::Url::parse(&endpoint.url).map_err(connect_error)?;
465 let scheme = match url.scheme() {
466 "ws" => "http".to_owned(),
467 "wss" => "https".to_owned(),
468 "http" | "https" => url.scheme().to_owned(),
469 scheme => {
470 return Err(connect_error(format!(
471 "unsupported LongPoll URL scheme {scheme}"
472 )));
473 }
474 };
475 url.set_scheme(&scheme)
476 .map_err(|_| connect_error("failed to set LongPoll URL scheme"))?;
477 let path = url.path().trim_end_matches('/');
478 let path = path.strip_suffix("/websocket").map_or_else(
479 || format!("{path}/longpoll"),
480 |base| format!("{base}/longpoll"),
481 );
482 url.set_path(&path);
483
484 let auth_token = endpoint
485 .protocols
486 .get(1)
487 .and_then(|protocol| protocol.strip_prefix(AUTH_TOKEN_PREFIX))
488 .and_then(|encoded| STANDARD_NO_PAD.decode(encoded).ok())
489 .and_then(|bytes| String::from_utf8(bytes).ok());
490 let requests = Rc::new(RefCell::new(HashMap::new()));
491 let next_request_id = Rc::new(Cell::new(0));
492 let response = long_poll_request(
493 &url,
494 None,
495 auth_token.as_deref(),
496 None,
497 request_timeout,
498 &requests,
499 &next_request_id,
500 TransportErrorKind::Connect,
501 )
502 .await?;
503 match response.status {
504 200 | 204 | 410 => {
505 let Some(initial_token) = response.token else {
506 return Err(connect_error("LongPoll handshake did not return a token"));
507 };
508 let token = Rc::new(RefCell::new(Some(initial_token)));
509 let closed = Rc::new(Cell::new(false));
510 let (events_tx, events) = mpsc::unbounded();
511 for message in response.messages {
512 let _ = events_tx.unbounded_send(Ok(TransportEvent::Message(
513 WireMessage::Text(message),
514 )));
515 }
516 let poll_endpoint = url.clone();
517 let poll_token = token.clone();
518 let poll_auth_token = auth_token.clone();
519 let poll_closed = closed.clone();
520 let poll_requests = requests.clone();
521 let poll_next_request_id = next_request_id.clone();
522 wasm_bindgen_futures::spawn_local(async move {
523 while !poll_closed.get() {
524 let current_token = poll_token.borrow().clone();
525 let response = long_poll_request(
526 &poll_endpoint,
527 current_token.as_deref(),
528 poll_auth_token.as_deref(),
529 None,
530 request_timeout,
531 &poll_requests,
532 &poll_next_request_id,
533 TransportErrorKind::Receive,
534 )
535 .await;
536 let response = match response {
537 Ok(response) => response,
538 Err(error) => {
539 let _ = events_tx.unbounded_send(Err(error));
540 break;
541 }
542 };
543 match response.status {
544 200 => {
545 if let Some(next_token) = response.token {
546 poll_token.replace(Some(next_token));
547 }
548 for message in response.messages {
549 if events_tx
550 .unbounded_send(Ok(TransportEvent::Message(
551 WireMessage::Text(message),
552 )))
553 .is_err()
554 {
555 return;
556 }
557 }
558 }
559 204 => {}
560 410 => {
561 poll_closed.set(true);
562 let _ = events_tx.unbounded_send(Ok(TransportEvent::Closed(
563 TransportClose::new(
564 Some(3410),
565 "LongPoll session is gone",
566 false,
567 ),
568 )));
569 }
570 403 => {
571 poll_closed.set(true);
572 let _ = events_tx.unbounded_send(Ok(TransportEvent::Closed(
573 TransportClose::new(
574 Some(1008),
575 "LongPoll request was forbidden",
576 false,
577 ),
578 )));
579 }
580 status => {
581 let _ =
582 events_tx.unbounded_send(Err(TransportError::with_kind(
583 TransportErrorKind::Receive,
584 format!("LongPoll GET returned status {status}"),
585 )));
586 break;
587 }
588 }
589 }
590 });
591 Ok(Self {
592 endpoint: url,
593 token,
594 auth_token,
595 events,
596 closed,
597 request_timeout,
598 requests,
599 next_request_id,
600 })
601 }
602 403 => Err(connect_error("LongPoll handshake was forbidden")),
603 status => Err(connect_error(format!(
604 "LongPoll handshake returned status {status}"
605 ))),
606 }
607 }
608
609 async fn post(&self, body: String) -> Result<(), TransportError> {
610 let token = self.token.borrow().clone();
611 let response = long_poll_request(
612 &self.endpoint,
613 token.as_deref(),
614 self.auth_token.as_deref(),
615 Some(body),
616 self.request_timeout,
617 &self.requests,
618 &self.next_request_id,
619 TransportErrorKind::Send,
620 )
621 .await?;
622 validate_post_status(response.status)
623 }
624 }
625
626 fn validate_post_status(status: u16) -> Result<(), TransportError> {
627 match status {
628 200 => Ok(()),
629 403 => Err(TransportError::with_kind(
630 TransportErrorKind::Send,
631 "LongPoll POST was forbidden",
632 )),
633 408 => Err(TransportError::with_kind(
634 TransportErrorKind::Send,
635 "LongPoll POST dispatch timed out",
636 )),
637 410 => Err(TransportError::with_kind(
638 TransportErrorKind::Send,
639 "LongPoll POST session is gone",
640 )),
641 status => Err(TransportError::with_kind(
642 TransportErrorKind::Send,
643 format!("LongPoll POST returned status {status}"),
644 )),
645 }
646 }
647
648 fn endpoint_url(endpoint: &url::Url, token: Option<&str>) -> String {
649 let mut endpoint = endpoint.clone();
650 if let Some(token) = token {
651 endpoint.query_pairs_mut().append_pair("token", token);
652 }
653 endpoint.into()
654 }
655
656 async fn long_poll_request(
657 endpoint: &url::Url,
658 token: Option<&str>,
659 auth_token: Option<&str>,
660 body: Option<String>,
661 request_timeout: Duration,
662 requests: &Rc<RefCell<HashMap<u64, AbortController>>>,
663 next_request_id: &Rc<Cell<u64>>,
664 error_kind: TransportErrorKind,
665 ) -> Result<PollResponse, TransportError> {
666 let controller = AbortController::new()
667 .map_err(|error| TransportError::with_kind(error_kind, format!("{error:?}")))?;
668 let mut request_id = next_request_id.get().wrapping_add(1);
669 if request_id == 0 {
670 request_id = 1;
671 }
672 next_request_id.set(request_id);
673 requests.borrow_mut().insert(request_id, controller.clone());
674
675 let endpoint = endpoint_url(endpoint, token);
676 let mut request = if body.is_some() {
677 Request::post(&endpoint).header("Content-Type", "application/x-ndjson")
678 } else {
679 Request::get(&endpoint).header("Accept", "application/json")
680 }
681 .abort_signal(Some(&controller.signal()));
682 if let Some(token) = auth_token {
683 request = request.header("X-Phoenix-AuthToken", token);
684 }
685 let request = match match body {
686 Some(body) => request.body(body),
687 None => request.build(),
688 } {
689 Ok(request) => request,
690 Err(error) => {
691 requests.borrow_mut().remove(&request_id);
692 return Err(TransportError::with_kind(error_kind, error.to_string()));
693 }
694 };
695 let response = async move {
696 let response = request
697 .send()
698 .await
699 .map_err(|error| TransportError::with_kind(error_kind, error.to_string()))?;
700 let http_status = response.status();
701 let text = response
702 .text()
703 .await
704 .map_err(|error| TransportError::with_kind(error_kind, error.to_string()))?;
705 if text.is_empty() {
706 return Ok(PollResponse {
707 status: http_status,
708 token: token.map(str::to_owned),
709 messages: Vec::new(),
710 });
711 }
712 serde_json::from_str(&text).map_err(|error| {
713 TransportError::with_kind(error_kind, format!("invalid LongPoll response: {error}"))
714 })
715 }
716 .fuse();
717 let timeout = timeout(request_timeout).fuse();
718 futures::pin_mut!(response, timeout);
719 let result = futures::select! {
720 result = response => result,
721 () = timeout => {
722 controller.abort();
723 Err(TransportError::with_kind(error_kind, format!(
724 "LongPoll request timed out after {request_timeout:?}"
725 )))
726 }
727 };
728 requests.borrow_mut().remove(&request_id);
729 result
730 }
731
732 fn timeout(duration: Duration) -> TimeoutFuture {
733 TimeoutFuture::new(duration.as_millis().min(u128::from(u32::MAX)) as u32)
734 }
735
736 fn connect_error(error: impl std::fmt::Display) -> TransportError {
737 TransportError::with_kind(TransportErrorKind::Connect, error.to_string())
738 }
739
740 impl Transport for WebTransport {
741 fn send<'a>(
742 &'a mut self,
743 message: WireMessage,
744 ) -> futures::future::LocalBoxFuture<'a, Result<(), TransportError>> {
745 Box::pin(async move {
746 let message = match message {
747 WireMessage::Text(text) => Message::Text(text),
748 WireMessage::Binary(bytes) => Message::Bytes(bytes),
749 };
750 self.inner
751 .as_mut()
752 .ok_or_else(closed_error)?
753 .send(message)
754 .await
755 .map_err(|error| {
756 TransportError::with_kind(TransportErrorKind::Send, format!("{error:?}"))
757 })
758 })
759 }
760
761 fn receive<'a>(
762 &'a mut self,
763 ) -> futures::future::LocalBoxFuture<'a, Result<TransportEvent, TransportError>> {
764 Box::pin(async move {
765 if let Some(event) = self.queued.pop_front() {
766 return Ok(event);
767 }
768 let Some(message) = self.inner.as_mut().ok_or_else(closed_error)?.next().await
769 else {
770 return Ok(TransportEvent::Closed(TransportClose::connection_ended()));
771 };
772 websocket_event(message)
773 })
774 }
775
776 fn close<'a>(
777 &'a mut self,
778 ) -> futures::future::LocalBoxFuture<'a, Result<(), TransportError>> {
779 Box::pin(async move {
780 let Some(inner) = self.inner.take() else {
781 return Ok(());
782 };
783 inner.close(None, None).map_err(|error| {
784 TransportError::with_kind(TransportErrorKind::Close, format!("{error:?}"))
785 })
786 })
787 }
788
789 fn close_with<'a>(
790 &'a mut self,
791 request: TransportCloseRequest,
792 ) -> futures::future::LocalBoxFuture<'a, Result<(), TransportError>> {
793 Box::pin(async move {
794 let Some(inner) = self.inner.take() else {
795 return Ok(());
796 };
797 inner
798 .close(Some(request.code), Some(&request.reason))
799 .map_err(|error| {
800 TransportError::with_kind(TransportErrorKind::Close, format!("{error:?}"))
801 })
802 })
803 }
804 }
805
806 fn websocket_event(
807 message: Result<Message, WebSocketError>,
808 ) -> Result<TransportEvent, TransportError> {
809 match message {
810 Ok(Message::Text(text)) => Ok(TransportEvent::Message(WireMessage::Text(text))),
811 Ok(Message::Bytes(bytes)) => Ok(TransportEvent::Message(WireMessage::Binary(bytes))),
812 Err(WebSocketError::ConnectionClose(close)) => Ok(TransportEvent::Closed(
813 TransportClose::new(Some(close.code), close.reason, close.was_clean),
814 )),
815 Err(error) => Err(TransportError::with_kind(
816 TransportErrorKind::Receive,
817 error.to_string(),
818 )),
819 }
820 }
821
822 impl Transport for LongPollTransport {
823 fn supports_binary(&self) -> bool {
824 false
825 }
826
827 fn send<'a>(
828 &'a mut self,
829 message: WireMessage,
830 ) -> futures::future::LocalBoxFuture<'a, Result<(), TransportError>> {
831 Box::pin(async move {
832 if self.closed.get() {
833 return Err(closed_error());
834 }
835 let body = match message {
836 WireMessage::Text(text) => text,
837 WireMessage::Binary(bytes) => STANDARD.encode(bytes),
838 };
839 self.post(body).await
840 })
841 }
842
843 fn receive<'a>(
844 &'a mut self,
845 ) -> futures::future::LocalBoxFuture<'a, Result<TransportEvent, TransportError>> {
846 Box::pin(async move {
847 if self.closed.get() {
848 return Ok(TransportEvent::Closed(TransportClose::new(
849 Some(1000),
850 "LongPoll transport closed",
851 true,
852 )));
853 }
854 self.events.next().await.unwrap_or_else(|| {
855 Ok(TransportEvent::Closed(TransportClose::connection_ended()))
856 })
857 })
858 }
859
860 fn close<'a>(
861 &'a mut self,
862 ) -> futures::future::LocalBoxFuture<'a, Result<(), TransportError>> {
863 Box::pin(async move {
864 self.closed.set(true);
865 for (_, controller) in self.requests.borrow_mut().drain() {
866 controller.abort();
867 }
868 self.events.close();
869 Ok(())
870 })
871 }
872 }
873
874 fn closed_error() -> TransportError {
875 TransportError::with_kind(TransportErrorKind::Other, "WebSocket is closed")
876 }
877
878 #[cfg(test)]
879 mod tests {
880 use super::*;
881 use wasm_bindgen_test::wasm_bindgen_test;
882
883 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
884
885 #[wasm_bindgen_test]
886 fn classifies_long_poll_post_statuses() {
887 assert!(validate_post_status(200).is_ok());
888 for (status, message) in [
889 (403, "forbidden"),
890 (408, "timed out"),
891 (410, "session is gone"),
892 ] {
893 let error = validate_post_status(status).unwrap_err();
894 assert_eq!(error.kind(), TransportErrorKind::Send);
895 assert!(error.message().contains(message));
896 }
897 }
898 }
899}
900
901#[cfg(target_arch = "wasm32")]
902pub use web::{
903 LongPollConnector, LongPollTransport, WebConnector, WebLifecycle, WebTimer, WebTransport,
904};