musli_web/macros.rs
1/// Defines the implementation of a `tokio-tungstenite` integration.
2///
3/// This is shared between the supported versions, which only differ in the
4/// crate they are linked against, the name of the generated implementation
5/// type, and the documented version.
6#[cfg(any(feature = "tungstenite029", feature = "tungstenite030"))]
7macro_rules! tungstenite_impl {
8 ($krate:ident, $impl_ty:ident, $version:literal) => {
9 use alloc::boxed::Box;
10 use alloc::string::{String, ToString};
11
12 use core::future::{Future, poll_fn};
13 use core::pin::Pin;
14 use core::task::{Context, Poll};
15
16 use std::io;
17
18 use bytes::Bytes;
19 use futures_core03::Stream;
20 use futures_sink03::Sink;
21 use tokio::io::{AsyncRead, AsyncWrite};
22 use tokio::net::TcpStream;
23 use $krate::tungstenite::client::IntoClientRequest;
24 use $krate::tungstenite::error::ProtocolError;
25 use $krate::tungstenite::handshake::client::Request;
26 use $krate::tungstenite::http::header::{HeaderMap, HeaderName, HeaderValue};
27 use $krate::tungstenite::protocol::Message as WsMessage;
28 use $krate::tungstenite::{Error, Utf8Bytes};
29 use $krate::{MaybeTlsStream, WebSocketStream, client_async, connect_async};
30
31 use crate::api::Mode;
32 use crate::client::{ClientImpl, EmptyCallback, Message, ServiceBuilder, SocketImpl};
33
34 /// A byte stream a websocket can be established over, see
35 /// [`connect_with()`].
36 ///
37 /// Implemented for anything which is [`AsyncRead`] and [`AsyncWrite`],
38 /// such as a `TcpStream`, a `UnixStream`, or one half of
39 /// `tokio::io::duplex`.
40 ///
41 /// [`AsyncRead`]: <https://docs.rs/tokio/1/tokio/io/trait.AsyncRead.html>
42 /// [`AsyncWrite`]: <https://docs.rs/tokio/1/tokio/io/trait.AsyncWrite.html>
43 pub trait ByteStream: 'static + Send + Unpin + AsyncRead + AsyncWrite {}
44
45 impl<S> ByteStream for S where S: 'static + Send + Unpin + AsyncRead + AsyncWrite {}
46
47 /// A boxed future opening a [`ByteStream`].
48 type OpenFuture = Pin<Box<dyn Future<Output = io::Result<Box<dyn ByteStream>>> + Send>>;
49
50 /// The socket type used by this implementation.
51 #[doc(hidden)]
52 pub enum Socket {
53 /// A socket connected to a url.
54 Url(Box<WebSocketStream<MaybeTlsStream<TcpStream>>>),
55 /// A socket established over a caller-supplied stream.
56 Stream(Box<WebSocketStream<Box<dyn ByteStream>>>),
57 }
58
59 /// A caller-supplied way of opening the stream a websocket is
60 /// established over, see [`connect_with()`].
61 #[doc(hidden)]
62 pub struct Connector {
63 url: String,
64 open: Box<dyn FnMut() -> OpenFuture + Send>,
65 }
66
67 #[doc = concat!("The public facing API for use with `tokio-tungstenite` `", $version, "`.")]
68 pub mod prelude {
69 pub mod ws {
70 //! Organization module prefixing all exported items with `ws` for
71 //! convenient namespacing.
72
73 pub use crate::api::ChannelId;
74 pub use crate::client::{
75 Channel, EmptyCallback, Error, Handle, Listener, Packet, RawPacket,
76 RequestBuilder, State, StateListener,
77 };
78
79 use core::future::Future;
80
81 use super::super::$impl_ty;
82
83 /// Implementation alias for [`connect`].
84 ///
85 /// [`connect`]: super::super::connect
86 #[inline]
87 pub fn connect(url: impl AsRef<str>) -> ServiceBuilder<EmptyCallback> {
88 super::super::connect(url)
89 }
90
91 /// Implementation alias for [`connect_with`].
92 ///
93 /// [`connect_with`]: super::super::connect_with
94 #[inline]
95 pub fn connect_with<F, O, S>(
96 url: impl AsRef<str>,
97 open: F,
98 ) -> ServiceBuilder<EmptyCallback>
99 where
100 F: 'static + Send + FnMut() -> O,
101 O: 'static + Send + Future<Output = std::io::Result<S>>,
102 S: super::super::ByteStream,
103 {
104 super::super::connect_with(url, open)
105 }
106
107 /// Implementation alias for [`Service`].
108 ///
109 /// [`Service`]: crate::client::Service
110 pub type Service = crate::client::Service<$impl_ty>;
111
112 /// Implementation alias for [`ServiceBuilder`].
113 ///
114 /// [`ServiceBuilder`]: crate::client::ServiceBuilder
115 pub type ServiceBuilder<C> = crate::client::ServiceBuilder<$impl_ty, C>;
116 }
117 }
118
119 #[doc = concat!("Client implementation for `tokio-tungstenite` `", $version, "`.")]
120 ///
121 /// See [`connect()`].
122 #[derive(Clone, Copy)]
123 pub enum $impl_ty {}
124
125 /// Construct a new [`ServiceBuilder`] which will connect to `url`.
126 ///
127 /// Note that no connection is established until [`Service::run`] or
128 /// [`Service::try_connect`] is called.
129 ///
130 /// [`Service::run`]: crate::client::Service::run
131 /// [`Service::try_connect`]: crate::client::Service::try_connect
132 #[inline]
133 pub fn connect(url: impl AsRef<str>) -> ServiceBuilder<$impl_ty, EmptyCallback> {
134 crate::client::connect(url)
135 }
136
137 /// Construct a new [`ServiceBuilder`] which establishes its websocket
138 /// over a stream opened by `open`, instead of connecting to `url`.
139 ///
140 /// `open` is called for every connection attempt, including
141 /// reconnects, so it can for example connect a `tokio::net::UnixStream`.
142 /// An error it returns is treated like any other failure to connect:
143 /// retried with a backoff by [`Service::run`], or returned by
144 /// [`Service::try_connect`].
145 ///
146 /// `url` is the request the websocket handshake is performed with, such
147 /// as `ws://localhost/ws`. It names the host and path the server sees,
148 /// but is never connected to.
149 ///
150 /// Note that no connection is established until [`Service::run`] or
151 /// [`Service::try_connect`] is called.
152 ///
153 /// [`Service::run`]: crate::client::Service::run
154 /// [`Service::try_connect`]: crate::client::Service::try_connect
155 pub fn connect_with<F, O, S>(
156 url: impl AsRef<str>,
157 mut open: F,
158 ) -> ServiceBuilder<$impl_ty, EmptyCallback>
159 where
160 F: 'static + Send + FnMut() -> O,
161 O: 'static + Send + Future<Output = io::Result<S>>,
162 S: ByteStream,
163 {
164 let url = url.as_ref();
165
166 let connector = Connector {
167 url: url.to_string(),
168 open: Box::new(move || {
169 let future = open();
170
171 Box::pin(async move {
172 let stream = future.await?;
173 Ok(Box::new(stream) as Box<dyn ByteStream>)
174 })
175 }),
176 };
177
178 crate::client::connect_with(url, connector)
179 }
180
181 impl crate::client::sealed_client::Sealed for $impl_ty {}
182
183 /// The headers which make up the websocket handshake itself, which
184 /// the client sets and which cannot be added through
185 /// [`ServiceBuilder::header`].
186 ///
187 /// [`ServiceBuilder::header`]: crate::client::ServiceBuilder::header
188 const HANDSHAKE_HEADERS: [&str; 5] = [
189 "host",
190 "connection",
191 "upgrade",
192 "sec-websocket-key",
193 "sec-websocket-version",
194 ];
195
196 /// Build the handshake request for `url` with `headers` added to it.
197 fn request(url: &str, headers: &HeaderMap) -> Result<Request, Error> {
198 let mut request = url.into_client_request()?;
199
200 for (name, value) in headers {
201 request.headers_mut().append(name, value.clone());
202 }
203
204 Ok(request)
205 }
206
207 impl ClientImpl for $impl_ty {
208 type Error = Error;
209 type Socket = Socket;
210 type Connector = Connector;
211 type Headers = HeaderMap;
212
213 fn header(headers: &mut HeaderMap, name: &str, value: &str) -> Result<(), Error> {
214 let name = HeaderName::from_bytes(name.as_bytes())?;
215
216 if HANDSHAKE_HEADERS.contains(&name.as_str()) {
217 return Err(Error::Protocol(ProtocolError::InvalidHeader(name.into())));
218 }
219
220 let value = HeaderValue::from_str(value)?;
221 headers.append(name, value);
222 Ok(())
223 }
224
225 #[inline]
226 async fn connect(url: &str, headers: &HeaderMap) -> Result<Self::Socket, Self::Error> {
227 let (socket, _) = connect_async(request(url, headers)?).await?;
228 Ok(Socket::Url(Box::new(socket)))
229 }
230
231 #[inline]
232 fn connect_with<'a>(
233 connector: &'a mut Self::Connector,
234 headers: &'a HeaderMap,
235 ) -> impl Future<Output = Result<Self::Socket, Self::Error>> + Send + 'a {
236 let open = (connector.open)();
237
238 async move {
239 let request = request(connector.url.as_str(), headers)?;
240 let stream = open.await.map_err(Error::Io)?;
241 let (socket, _) = client_async(request, stream).await?;
242 Ok(Socket::Stream(Box::new(socket)))
243 }
244 }
245 }
246
247 impl Socket {
248 #[inline]
249 fn poll_next_message(
250 &mut self,
251 cx: &mut Context<'_>,
252 ) -> Poll<Option<Result<WsMessage, Error>>> {
253 match self {
254 Socket::Url(socket) => Pin::new(&mut **socket).poll_next(cx),
255 Socket::Stream(socket) => Pin::new(&mut **socket).poll_next(cx),
256 }
257 }
258
259 #[inline]
260 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
261 match self {
262 Socket::Url(socket) => Pin::new(&mut **socket).poll_ready(cx),
263 Socket::Stream(socket) => Pin::new(&mut **socket).poll_ready(cx),
264 }
265 }
266
267 #[inline]
268 fn start_send(&mut self, message: WsMessage) -> Result<(), Error> {
269 match self {
270 Socket::Url(socket) => Pin::new(&mut **socket).start_send(message),
271 Socket::Stream(socket) => Pin::new(&mut **socket).start_send(message),
272 }
273 }
274
275 #[inline]
276 fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
277 match self {
278 Socket::Url(socket) => Pin::new(&mut **socket).poll_flush(cx),
279 Socket::Stream(socket) => Pin::new(&mut **socket).poll_flush(cx),
280 }
281 }
282
283 #[inline]
284 fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
285 match self {
286 Socket::Url(socket) => Pin::new(&mut **socket).poll_close(cx),
287 Socket::Stream(socket) => Pin::new(&mut **socket).poll_close(cx),
288 }
289 }
290 }
291
292 impl crate::client::sealed_socket::Sealed for Socket {}
293
294 impl SocketImpl for Socket {
295 type Error = Error;
296
297 #[inline]
298 fn recv(
299 &mut self,
300 ) -> impl Future<Output = Option<Result<Message, Self::Error>>> + Send + '_ {
301 poll_fn(move |cx| {
302 self.poll_next_message(cx)
303 .map(|message| message.map(|message| message.map(convert)))
304 })
305 }
306
307 #[inline]
308 fn send(
309 &mut self,
310 mode: Mode,
311 data: &[u8],
312 ) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
313 let message = match mode {
314 Mode::Binary => WsMessage::Binary(Bytes::copy_from_slice(data)),
315 // NB: A text frame is only ever written in a mode which guarantees
316 // that everything in it is valid UTF-8, so the lossy path is never
317 // taken.
318 Mode::Text => WsMessage::Text(Utf8Bytes::from(&*String::from_utf8_lossy(data))),
319 };
320
321 async move {
322 poll_fn(|cx| self.poll_ready(cx)).await?;
323 self.start_send(message)?;
324 poll_fn(|cx| self.poll_flush(cx)).await
325 }
326 }
327
328 #[inline]
329 fn close(&mut self) -> impl Future<Output = Result<(), Self::Error>> + Send + '_ {
330 poll_fn(move |cx| self.poll_close(cx))
331 }
332 }
333
334 /// Convert a tungstenite message into a message understood by the client.
335 #[inline]
336 fn convert(message: WsMessage) -> Message {
337 match message {
338 WsMessage::Binary(data) => Message::Binary(data),
339 WsMessage::Text(data) => Message::Text(Bytes::from(data)),
340 WsMessage::Ping(..) => Message::Ping,
341 WsMessage::Pong(..) => Message::Pong,
342 WsMessage::Close(..) => Message::Close,
343 // NB: Raw frames are never produced while reading, so this is a
344 // protocol violation which tears the connection down.
345 WsMessage::Frame(..) => Message::Unsupported,
346 }
347 }
348 };
349}
350
351/// Implements the integration for a version of `yew`.
352///
353/// This is shared between the supported versions, which only differ in the
354/// crate they are linked against.
355///
356/// NB: yew's own `#[hook]` and `#[derive(Properties)]` macros expand to paths
357/// through `::yew`, which this crate does not link under that name, so the
358/// hooks here implement `Hook` by hand.
359#[cfg(any(feature = "yew022", feature = "yew023"))]
360macro_rules! yew_impl {
361 ($krate:ident) => {
362 use alloc::rc::Rc;
363 use core::cell::{Cell, RefCell};
364 use core::fmt;
365 use core::mem;
366
367 use $krate::functional::{
368 Hook, HookContext, use_context, use_force_update, use_memo, use_mut_ref,
369 };
370 use $krate::{Callback, Component, Context, ContextProvider};
371
372 use crate::web03::prelude::ws::{
373 Channel, ChannelEvent, Handle, KeyedRequests, Request, Requests, State,
374 };
375
376 impl<I> crate::web::Callback<I> for Callback<I>
377 where
378 I: 'static,
379 {
380 #[inline]
381 fn call(&self, result: I) {
382 self.emit(result);
383 }
384 }
385
386 /// The message a missing [`HandleProvider`] panics with.
387 const MISSING_PROVIDER: &str = concat!(
388 "no ws::Handle in context: wrap the component tree in musli_web::",
389 stringify!($krate),
390 "::HandleProvider"
391 );
392
393 /// Provides a [`ws::Handle`] to every component below it.
394 ///
395 /// This is yew's [`ContextProvider`] for [`ws::Handle`], so it takes
396 /// the handle as its `context` property. Components below it get the
397 /// handle with [`use_handle`] in a function component, or
398 /// [`ContextExt::handle`] in a struct component.
399 ///
400 /// The handle is usually that of a [`ws::Service`] the root component
401 /// keeps for as long as the application runs, see the [module
402 /// documentation](self) for an example.
403 ///
404 /// [`ws::Handle`]: crate::web::Handle
405 /// [`ws::Service`]: crate::web::Service
406 pub type HandleProvider = ContextProvider<Handle>;
407
408 /// Get the [`ws::Handle`] provided by the closest [`HandleProvider`]
409 /// above the component.
410 ///
411 /// The component renders again if the provided handle changes.
412 ///
413 /// # Panics
414 ///
415 /// Panics if the component is not below a [`HandleProvider`]. Use
416 /// `use_context::<ws::Handle>()` to get an `Option` instead.
417 ///
418 /// [`ws::Handle`]: crate::web::Handle
419 ///
420 /// # Examples
421 ///
422 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
423 /// use yew::prelude::*;
424 #[doc = concat!("use musli_web::", stringify!($krate), "::use_handle;")]
425 ///
426 /// #[function_component(Status)]
427 /// fn status() -> Html {
428 /// let handle = use_handle();
429 /// html!(<p>{format!("Connected {} time(s)", handle.generation())}</p>)
430 /// }
431 /// ```
432 pub fn use_handle() -> impl Hook<Output = Handle> {
433 struct HookProvider;
434
435 impl Hook for HookProvider {
436 type Output = Handle;
437
438 fn run(self, ctx: &mut HookContext) -> Self::Output {
439 match use_context::<Handle>().run(ctx) {
440 Some(handle) => handle,
441 None => panic!("{MISSING_PROVIDER}"),
442 }
443 }
444 }
445
446 HookProvider
447 }
448
449 /// Keep a [`ws::Request`] for as long as the component is mounted.
450 ///
451 /// A request is cancelled when its [`ws::Request`] is dropped, so a
452 /// function component needs somewhere to keep it between renders.
453 /// Store a sent request with [`UseRequestHandle::set`], which cancels
454 /// the request stored before it. The stored request is cancelled when
455 /// the component is unmounted.
456 ///
457 /// [`ws::Request`]: crate::web::Request
458 ///
459 /// # Examples
460 ///
461 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
462 /// use yew::prelude::*;
463 /// use musli_web::web03::prelude::*;
464 #[doc = concat!("use musli_web::", stringify!($krate), "::{use_handle, use_request};")]
465 ///
466 /// mod api {
467 /// use musli::{Decode, Encode};
468 /// use musli_web::api;
469 ///
470 /// #[derive(Encode, Decode)]
471 /// pub struct HelloRequest<'de> {
472 /// pub message: &'de str,
473 /// }
474 ///
475 /// #[derive(Encode, Decode)]
476 /// pub struct HelloResponse<'de> {
477 /// pub message: &'de str,
478 /// }
479 ///
480 /// api::define! {
481 /// pub type Hello;
482 ///
483 /// impl Endpoint for Hello {
484 /// impl<'de> Request for HelloRequest<'de>;
485 /// type Response<'de> = HelloResponse<'de>;
486 /// }
487 /// }
488 /// }
489 ///
490 /// #[function_component(Hello)]
491 /// fn hello() -> Html {
492 /// let handle = use_handle();
493 /// let request = use_request();
494 /// let response = use_state(String::new);
495 ///
496 /// let onclick = {
497 /// let response = response.clone();
498 ///
499 /// move |_: MouseEvent| {
500 /// let response = response.clone();
501 ///
502 /// request.set(
503 /// handle
504 /// .request()
505 /// .body(api::HelloRequest { message: "Hello!" })
506 /// .on_packet(move |packet: Result<ws::Packet<api::Hello>, ws::Error>| {
507 /// if let Ok(r) = packet.and_then(|p| Ok(p.decode()?.message.to_owned())) {
508 /// response.set(r);
509 /// }
510 /// })
511 /// .send(),
512 /// );
513 /// }
514 /// };
515 ///
516 /// html! {
517 /// <>
518 /// <button {onclick}>{"Say hello"}</button>
519 /// <p>{(*response).clone()}</p>
520 /// </>
521 /// }
522 /// }
523 /// ```
524 pub fn use_request() -> impl Hook<Output = UseRequestHandle> {
525 struct HookProvider;
526
527 impl Hook for HookProvider {
528 type Output = UseRequestHandle;
529
530 fn run(self, ctx: &mut HookContext) -> Self::Output {
531 UseRequestHandle {
532 inner: use_mut_ref(Request::new).run(ctx),
533 }
534 }
535 }
536
537 HookProvider
538 }
539
540 /// The handle returned by [`use_request`].
541 ///
542 /// Cloning it is cheap and every clone refers to the same stored
543 /// request, so it can be moved into callbacks.
544 #[derive(Clone)]
545 pub struct UseRequestHandle {
546 inner: Rc<RefCell<Request>>,
547 }
548
549 impl UseRequestHandle {
550 /// Store `request`, cancelling the request stored before it.
551 pub fn set(&self, request: Request) {
552 let old = mem::replace(&mut *self.inner.borrow_mut(), request);
553 // NB: Dropped after the borrow ends, since cancelling the old
554 // request drops its callback.
555 drop(old);
556 }
557
558 /// Cancel the stored request, if any.
559 pub fn clear(&self) {
560 self.set(Request::new());
561 }
562
563 /// Whether the stored request is still waiting for its response.
564 ///
565 /// This does not make the component render again by itself; keep
566 /// track of what the response callback was told for that.
567 pub fn is_pending(&self) -> bool {
568 self.inner.borrow().is_pending()
569 }
570 }
571
572 /// Handles are equal if they refer to the same stored request.
573 impl PartialEq for UseRequestHandle {
574 #[inline]
575 fn eq(&self, other: &Self) -> bool {
576 Rc::ptr_eq(&self.inner, &other.inner)
577 }
578 }
579
580 impl fmt::Debug for UseRequestHandle {
581 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
582 f.debug_struct("UseRequestHandle")
583 .field("is_pending", &self.is_pending())
584 .finish()
585 }
586 }
587
588 /// Keep a set of [`ws::Requests`] for as long as the component is
589 /// mounted.
590 ///
591 /// This is [`use_request`] for a component which keeps several
592 /// requests in flight at once: storing a request with
593 /// [`UseRequestsHandle::push`] leaves the requests stored before it
594 /// alone, and each one is forgotten once it completes. Every request
595 /// still pending is cancelled when the component is unmounted.
596 ///
597 /// [`ws::Requests`]: crate::web::Requests
598 ///
599 /// # Examples
600 ///
601 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
602 /// use yew::prelude::*;
603 /// use musli_web::web03::prelude::*;
604 #[doc = concat!("use musli_web::", stringify!($krate), "::{use_handle, use_requests};")]
605 ///
606 /// mod api {
607 /// use musli::{Decode, Encode};
608 /// use musli_web::api;
609 ///
610 /// #[derive(Encode, Decode)]
611 /// pub struct LikeRequest {
612 /// pub id: u32,
613 /// }
614 ///
615 /// api::define! {
616 /// pub type Like;
617 ///
618 /// impl Endpoint for Like {
619 /// impl Request for LikeRequest;
620 /// type Response<'de> = ();
621 /// }
622 /// }
623 /// }
624 ///
625 /// #[function_component(Like)]
626 /// fn like() -> Html {
627 /// let handle = use_handle();
628 /// let requests = use_requests();
629 ///
630 /// let onclick = move |_: MouseEvent| {
631 /// // Clicking again while an earlier like is in flight sends
632 /// // another one, without cancelling the first.
633 /// requests.push(
634 /// handle
635 /// .request()
636 /// .body(api::LikeRequest { id: 42 })
637 /// .on_packet(|packet: Result<ws::Packet<api::Like>, ws::Error>| {
638 /// if let Err(error) = packet {
639 /// tracing::error!("Like failed: {error}");
640 /// }
641 /// })
642 /// .send(),
643 /// );
644 /// };
645 ///
646 /// html! {
647 /// <button {onclick}>{"Like"}</button>
648 /// }
649 /// }
650 /// ```
651 pub fn use_requests() -> impl Hook<Output = UseRequestsHandle> {
652 struct HookProvider;
653
654 impl Hook for HookProvider {
655 type Output = UseRequestsHandle;
656
657 fn run(self, ctx: &mut HookContext) -> Self::Output {
658 UseRequestsHandle {
659 inner: use_mut_ref(Requests::new).run(ctx),
660 }
661 }
662 }
663
664 HookProvider
665 }
666
667 /// The handle returned by [`use_requests`].
668 ///
669 /// Cloning it is cheap and every clone refers to the same set of
670 /// requests, so it can be moved into callbacks.
671 #[derive(Clone)]
672 pub struct UseRequestsHandle {
673 inner: Rc<RefCell<Requests>>,
674 }
675
676 impl UseRequestsHandle {
677 /// Keep `request` until it completes, see [`Requests::push`].
678 ///
679 /// [`Requests::push`]: crate::web::Requests::push
680 pub fn push(&self, request: Request) {
681 self.inner.borrow_mut().push(request);
682 }
683
684 /// Cancel every pending request.
685 pub fn clear(&self) {
686 let old = mem::take(&mut *self.inner.borrow_mut());
687 // NB: Dropped after the borrow ends, since cancelling the old
688 // requests drops their callbacks.
689 drop(old);
690 }
691
692 /// The number of requests which are still pending.
693 ///
694 /// This does not make the component render again by itself; keep
695 /// track of what the response callbacks were told for that.
696 pub fn len(&self) -> usize {
697 self.inner.borrow().len()
698 }
699
700 /// Whether no request is pending.
701 pub fn is_empty(&self) -> bool {
702 self.inner.borrow().is_empty()
703 }
704 }
705
706 /// Handles are equal if they refer to the same set of requests.
707 impl PartialEq for UseRequestsHandle {
708 #[inline]
709 fn eq(&self, other: &Self) -> bool {
710 Rc::ptr_eq(&self.inner, &other.inner)
711 }
712 }
713
714 impl fmt::Debug for UseRequestsHandle {
715 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
716 f.debug_struct("UseRequestsHandle")
717 .field("len", &self.len())
718 .finish()
719 }
720 }
721
722 /// Keep a set of [`ws::KeyedRequests`] for as long as the component
723 /// is mounted.
724 ///
725 /// This keeps at most one request in flight for each key: storing a
726 /// request with [`UseKeyedRequestsHandle::insert`] cancels the request
727 /// stored under the same key before it, and leaves the other keys
728 /// alone. Each request is forgotten once it completes, and every
729 /// request still pending is cancelled when the component is
730 /// unmounted.
731 ///
732 /// [`ws::KeyedRequests`]: crate::web::KeyedRequests
733 ///
734 /// # Examples
735 ///
736 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
737 /// # extern crate web_sys03 as web_sys;
738 /// use web_sys::HtmlInputElement;
739 /// use yew::prelude::*;
740 /// use musli_web::web03::prelude::*;
741 #[doc = concat!("use musli_web::", stringify!($krate), "::{use_handle, use_keyed_requests};")]
742 ///
743 /// mod api {
744 /// use musli::{Decode, Encode};
745 /// use musli_web::api;
746 ///
747 /// #[derive(Encode, Decode)]
748 /// pub struct ToggleRequest {
749 /// pub id: u32,
750 /// pub on: bool,
751 /// }
752 ///
753 /// api::define! {
754 /// pub type Toggle;
755 ///
756 /// impl Endpoint for Toggle {
757 /// impl Request for ToggleRequest;
758 /// type Response<'de> = ();
759 /// }
760 /// }
761 /// }
762 ///
763 /// #[derive(Properties, PartialEq)]
764 /// struct Props {
765 /// ids: Vec<u32>,
766 /// }
767 ///
768 /// #[function_component(Toggles)]
769 /// fn toggles(props: &Props) -> Html {
770 /// let handle = use_handle();
771 /// let requests = use_keyed_requests::<u32>();
772 ///
773 /// let toggle = move |id: u32, on: bool| {
774 /// // Toggling an id again cancels its earlier toggle, but a
775 /// // toggle of another id stays in flight.
776 /// requests.insert(
777 /// id,
778 /// handle
779 /// .request()
780 /// .body(api::ToggleRequest { id, on })
781 /// .on_packet(|packet: Result<ws::Packet<api::Toggle>, ws::Error>| {
782 /// if let Err(error) = packet {
783 /// tracing::error!("Toggle failed: {error}");
784 /// }
785 /// })
786 /// .send(),
787 /// );
788 /// };
789 ///
790 /// html! {
791 /// {for props.ids.iter().copied().map(|id| {
792 /// let toggle = toggle.clone();
793 ///
794 /// let onchange = move |e: Event| {
795 /// toggle(id, e.target_unchecked_into::<HtmlInputElement>().checked())
796 /// };
797 ///
798 /// html!(<input type="checkbox" {onchange} />)
799 /// })}
800 /// }
801 /// }
802 /// ```
803 pub fn use_keyed_requests<K>() -> impl Hook<Output = UseKeyedRequestsHandle<K>>
804 where
805 K: 'static,
806 {
807 struct HookProvider<K>(core::marker::PhantomData<K>);
808
809 impl<K> Hook for HookProvider<K>
810 where
811 K: 'static,
812 {
813 type Output = UseKeyedRequestsHandle<K>;
814
815 fn run(self, ctx: &mut HookContext) -> Self::Output {
816 UseKeyedRequestsHandle {
817 inner: use_mut_ref(KeyedRequests::new).run(ctx),
818 }
819 }
820 }
821
822 HookProvider(core::marker::PhantomData)
823 }
824
825 /// The handle returned by [`use_keyed_requests`].
826 ///
827 /// Cloning it is cheap and every clone refers to the same set of
828 /// requests, so it can be moved into callbacks.
829 pub struct UseKeyedRequestsHandle<K> {
830 inner: Rc<RefCell<KeyedRequests<K>>>,
831 }
832
833 impl<K> UseKeyedRequestsHandle<K>
834 where
835 K: Eq + core::hash::Hash,
836 {
837 /// Keep `request` under `key` until it completes, cancelling the
838 /// request stored under `key` before it, see
839 /// [`KeyedRequests::insert`].
840 ///
841 /// [`KeyedRequests::insert`]: crate::web::KeyedRequests::insert
842 pub fn insert(&self, key: K, request: Request) {
843 self.inner.borrow_mut().insert(key, request);
844 }
845
846 /// Cancel the request stored under `key`, returning whether it
847 /// was still pending.
848 pub fn remove<Q>(&self, key: &Q) -> bool
849 where
850 K: core::borrow::Borrow<Q>,
851 Q: ?Sized + Eq + core::hash::Hash,
852 {
853 self.inner.borrow_mut().remove(key)
854 }
855
856 /// Whether the request stored under `key` is still pending.
857 ///
858 /// This does not make the component render again by itself; keep
859 /// track of what the response callbacks were told for that.
860 pub fn is_pending<Q>(&self, key: &Q) -> bool
861 where
862 K: core::borrow::Borrow<Q>,
863 Q: ?Sized + Eq + core::hash::Hash,
864 {
865 self.inner.borrow().is_pending(key)
866 }
867 }
868
869 impl<K> UseKeyedRequestsHandle<K> {
870 /// Cancel every pending request.
871 pub fn clear(&self) {
872 let old = mem::take(&mut *self.inner.borrow_mut());
873 // NB: Dropped after the borrow ends, since cancelling the old
874 // requests drops their callbacks.
875 drop(old);
876 }
877
878 /// The number of keys whose request is still pending.
879 pub fn len(&self) -> usize {
880 self.inner.borrow().len()
881 }
882
883 /// Whether no request is pending.
884 pub fn is_empty(&self) -> bool {
885 self.inner.borrow().is_empty()
886 }
887 }
888
889 impl<K> Clone for UseKeyedRequestsHandle<K> {
890 #[inline]
891 fn clone(&self) -> Self {
892 Self {
893 inner: self.inner.clone(),
894 }
895 }
896 }
897
898 /// Handles are equal if they refer to the same set of requests.
899 impl<K> PartialEq for UseKeyedRequestsHandle<K> {
900 #[inline]
901 fn eq(&self, other: &Self) -> bool {
902 Rc::ptr_eq(&self.inner, &other.inner)
903 }
904 }
905
906 impl<K> fmt::Debug for UseKeyedRequestsHandle<K>
907 where
908 K: fmt::Debug,
909 {
910 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
911 f.debug_tuple("UseKeyedRequestsHandle")
912 .field(&*self.inner.borrow())
913 .finish()
914 }
915 }
916
917 /// Open a [`ws::Channel`] through the [`ws::Handle`] from
918 /// [`use_handle`], for as long as the component is mounted.
919 ///
920 /// The channel opens again by itself every time the connection does,
921 /// and the component renders again every time it opens, closes or
922 /// fails to open. Since channels [compare equal] by their current id, an
923 /// effect which depends on the channel runs again every time it opens,
924 /// which is the point at which to send the requests which need it:
925 ///
926 /// ```text
927 /// use_effect_with(channel.clone(), move |channel| {
928 /// if channel.is_open() {
929 /// request.set(channel.request()./* ... */.send());
930 /// }
931 /// });
932 /// ```
933 ///
934 /// A request made in response to the user can instead be queued with
935 /// [`Channel::when_open`], so that it is sent even if the channel
936 /// happens to be closed at the time.
937 ///
938 /// A new channel is opened if the provided handle changes. The channel
939 /// is closed when the component is unmounted and every clone of it has
940 /// been dropped. To be told why a channel failed to open, open it with
941 /// [`Handle::channel`] and [`ChannelBuilder::on_change`] instead.
942 ///
943 /// # Panics
944 ///
945 /// Panics if the component is not below a [`HandleProvider`].
946 ///
947 /// [`ws::Channel`]: crate::web::Channel
948 /// [`ws::Handle`]: crate::web::Handle
949 /// [compare equal]: crate::web::Channel#impl-PartialEq-for-Channel%3CH%3E
950 /// [`Channel::when_open`]: crate::web::Channel::when_open
951 /// [`Handle::channel`]: crate::web::Handle::channel
952 /// [`ChannelBuilder::on_change`]: crate::web::ChannelBuilder::on_change
953 ///
954 /// # Examples
955 ///
956 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
957 /// use yew::prelude::*;
958 /// use musli_web::web03::prelude::*;
959 #[doc = concat!("use musli_web::", stringify!($krate), "::{use_channel, use_request};")]
960 ///
961 /// mod api {
962 /// use musli::{Decode, Encode};
963 /// use musli_web::api;
964 ///
965 /// #[derive(Encode, Decode)]
966 /// pub struct Subscribe;
967 ///
968 /// #[derive(Encode, Decode)]
969 /// pub struct Subscribed {
970 /// pub count: u32,
971 /// }
972 ///
973 /// api::define! {
974 /// pub type Counter;
975 ///
976 /// impl Endpoint for Counter {
977 /// impl Request for Subscribe;
978 /// type Response<'de> = Subscribed;
979 /// }
980 /// }
981 /// }
982 ///
983 /// #[function_component(Counter)]
984 /// fn counter() -> Html {
985 /// let channel = use_channel();
986 /// let request = use_request();
987 /// let count = use_state(|| None::<u32>);
988 ///
989 /// {
990 /// let count = count.clone();
991 ///
992 /// use_effect_with(channel.clone(), move |channel| {
993 /// if channel.is_open() {
994 /// request.set(
995 /// channel
996 /// .request()
997 /// .body(api::Subscribe)
998 /// .on_packet(move |packet: Result<ws::Packet<api::Counter>, ws::Error>| {
999 /// if let Ok(response) = packet.and_then(|p| p.decode()) {
1000 /// count.set(Some(response.count));
1001 /// }
1002 /// })
1003 /// .send(),
1004 /// );
1005 /// }
1006 /// });
1007 /// }
1008 ///
1009 /// match (channel.is_open(), *count) {
1010 /// (false, _) => html!(<p>{"Connecting..."}</p>),
1011 /// (true, None) => html!(<p>{"Loading..."}</p>),
1012 /// (true, Some(count)) => html!(<p>{format!("Count: {count}")}</p>),
1013 /// }
1014 /// }
1015 /// ```
1016 pub fn use_channel() -> impl Hook<Output = Channel> {
1017 struct HookProvider;
1018
1019 impl Hook for HookProvider {
1020 type Output = Channel;
1021
1022 fn run(self, ctx: &mut HookContext) -> Self::Output {
1023 let handle = use_handle().run(ctx);
1024 let update = use_force_update().run(ctx);
1025
1026 let channel = use_memo(handle, move |handle| {
1027 handle
1028 .channel()
1029 .on_change(move |_: ChannelEvent| update.force_update())
1030 .open()
1031 })
1032 .run(ctx);
1033
1034 Channel::clone(&channel)
1035 }
1036 }
1037
1038 HookProvider
1039 }
1040
1041 /// Follow whether the connection of the [`ws::Handle`] from
1042 /// [`use_handle`] is open.
1043 ///
1044 /// This is `Some` with the connection's [generation] while it is open,
1045 /// and `None` while it is not. The component renders again when it
1046 /// changes, so an effect which depends on it runs again every time the
1047 /// connection opens, which is the point at which to fetch what the
1048 /// component shows again, see [refetching after a reconnect].
1049 ///
1050 /// # Panics
1051 ///
1052 /// Panics if the component is not below a [`HandleProvider`].
1053 ///
1054 /// [`ws::Handle`]: crate::web::Handle
1055 /// [generation]: crate::web::Handle::generation
1056 /// [refetching after a reconnect]: crate::web#refetching-after-a-reconnect
1057 ///
1058 /// # Examples
1059 ///
1060 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
1061 /// use yew::prelude::*;
1062 #[doc = concat!("use musli_web::", stringify!($krate), "::{use_handle, use_open, use_request};")]
1063 ///
1064 /// #[function_component(Status)]
1065 /// fn status() -> Html {
1066 /// let handle = use_handle();
1067 /// let open = use_open();
1068 /// let request = use_request();
1069 ///
1070 /// use_effect_with(open, move |open| {
1071 /// if open.is_some() {
1072 /// // Send the requests this component shows the result of
1073 /// // with `handle.request()` and store them in `request`.
1074 /// }
1075 /// });
1076 ///
1077 /// match open {
1078 /// Some(generation) => html!(<p>{format!("Connection #{generation}")}</p>),
1079 /// None => html!(<p>{"Disconnected"}</p>),
1080 /// }
1081 /// }
1082 /// ```
1083 pub fn use_open() -> impl Hook<Output = Option<u64>> {
1084 struct HookProvider;
1085
1086 impl Hook for HookProvider {
1087 type Output = Option<u64>;
1088
1089 fn run(self, ctx: &mut HookContext) -> Self::Output {
1090 let handle = use_handle().run(ctx);
1091 let update = use_force_update().run(ctx);
1092
1093 let state = use_memo(handle, move |handle| {
1094 let open = Rc::new(Cell::new(None));
1095
1096 let (state, listener) = handle.on_state_change({
1097 let open = open.clone();
1098 let handle = handle.clone();
1099
1100 move |state: State| {
1101 let new = state.is_open().then(|| handle.generation());
1102
1103 if open.replace(new) != new {
1104 update.force_update();
1105 }
1106 }
1107 });
1108
1109 open.set(state.is_open().then(|| handle.generation()));
1110 (open, listener)
1111 })
1112 .run(ctx);
1113
1114 state.0.get()
1115 }
1116 }
1117
1118 HookProvider
1119 }
1120
1121 mod sealed {
1122 pub trait Sealed {}
1123 impl<C> Sealed for super::Context<C> where C: super::Component {}
1124 }
1125
1126 /// Extension methods to get the [`ws::Handle`] provided by a
1127 /// [`HandleProvider`] in a struct component.
1128 ///
1129 /// These look the handle up when called and are not told if the
1130 /// provided handle changes, which suits the usual case of a handle
1131 /// which the root component keeps for as long as the application runs.
1132 /// To be told about changes, use [`Scope::context`] instead.
1133 ///
1134 /// [`ws::Handle`]: crate::web::Handle
1135 #[doc = concat!("[`Scope::context`]: ", stringify!($krate), "::html::Scope::context")]
1136 ///
1137 /// # Examples
1138 ///
1139 #[doc = concat!("```\n# extern crate ", stringify!($krate), " as yew;")]
1140 /// use yew::prelude::*;
1141 /// use musli_web::web03::prelude::*;
1142 #[doc = concat!("use musli_web::", stringify!($krate), "::ContextExt;")]
1143 ///
1144 /// struct Status {
1145 /// handle: ws::Handle,
1146 /// }
1147 ///
1148 /// impl Component for Status {
1149 /// type Message = ();
1150 /// type Properties = ();
1151 ///
1152 /// fn create(ctx: &Context<Self>) -> Self {
1153 /// Self {
1154 /// handle: ctx.handle(),
1155 /// }
1156 /// }
1157 ///
1158 /// fn view(&self, _: &Context<Self>) -> Html {
1159 /// html!(<p>{format!("Connected {} time(s)", self.handle.generation())}</p>)
1160 /// }
1161 /// }
1162 /// ```
1163 pub trait ContextExt: sealed::Sealed {
1164 /// Get the [`ws::Handle`] provided by the closest
1165 /// [`HandleProvider`] above the component.
1166 ///
1167 /// # Panics
1168 ///
1169 /// Panics if the component is not below a [`HandleProvider`]. Use
1170 /// [`ContextExt::try_handle`] to get an `Option` instead.
1171 ///
1172 /// [`ws::Handle`]: crate::web::Handle
1173 fn handle(&self) -> Handle;
1174
1175 /// Get the [`ws::Handle`] provided by the closest
1176 /// [`HandleProvider`] above the component, or `None` if there is
1177 /// none.
1178 ///
1179 /// [`ws::Handle`]: crate::web::Handle
1180 fn try_handle(&self) -> Option<Handle>;
1181 }
1182
1183 impl<C> ContextExt for Context<C>
1184 where
1185 C: Component,
1186 {
1187 #[inline]
1188 fn handle(&self) -> Handle {
1189 match self.try_handle() {
1190 Some(handle) => handle,
1191 None => panic!("{MISSING_PROVIDER}"),
1192 }
1193 }
1194
1195 #[inline]
1196 fn try_handle(&self) -> Option<Handle> {
1197 let (handle, _) = self.link().context::<Handle>(Callback::noop())?;
1198 Some(handle)
1199 }
1200 }
1201 };
1202}