yew_event_source/
lib.rs

1//! Event stream handling for the yew framework
2use std::fmt;
3
4use gloo_events::EventListener;
5use std::borrow::Cow;
6use wasm_bindgen::JsCast;
7use web_sys::{Event, EventSource, MessageEvent};
8use yew::callback::Callback;
9use yew::format::{FormatError, Text};
10use yew::services::Task;
11
12/// A status of an event source connection. Used for status notification.
13#[derive(PartialEq, Debug)]
14pub enum EventSourceStatus {
15    /// Fired when an event source connection was opened.
16    Open,
17    /// Fired when an event source connection had an error.
18    Error,
19}
20
21/// Ready state of an event source
22///
23/// [Documented at MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource/readyState)
24#[derive(PartialEq, Debug)]
25pub enum ReadyState {
26    /// The event source connection is connecting.
27    Connecting,
28    /// The event source connection is open.
29    Open,
30    /// The event source connection is closed.
31    Closed,
32}
33
34/// A handle to control current event source connection. Implements `Task` and could be canceled.
35pub struct EventSourceTask {
36    event_source: EventSource,
37    // We need to keep this else it is cleaned up on drop.
38    _notification: Callback<EventSourceStatus>,
39    listeners: Vec<EventListener>,
40}
41
42impl EventSourceTask {
43    #![allow(clippy::unnecessary_wraps)]
44    fn new(
45        event_source: EventSource,
46        notification: Callback<EventSourceStatus>,
47    ) -> Result<EventSourceTask, &'static str> {
48        Ok(EventSourceTask {
49            event_source,
50            _notification: notification,
51            listeners: vec![],
52        })
53    }
54
55    fn add_unwrapped_event_listener<S, F>(&mut self, event_type: S, callback: F)
56    where
57        S: Into<Cow<'static, str>>,
58        F: FnMut(&Event) + 'static,
59    {
60        self.listeners
61            .push(EventListener::new(&self.event_source, event_type, callback));
62    }
63
64    /// Register a callback for events of a given type
65    ///
66    /// This registers an event listener, which will fire `callback` when an
67    /// event of `event_type` occurs.
68    pub fn add_event_listener<S, OUT: 'static>(&mut self, event_type: S, callback: Callback<OUT>)
69    where
70        S: Into<Cow<'static, str>>,
71        OUT: From<Text>,
72    {
73        // This will convert from a generic `Event` into a `MessageEvent` taking
74        // text, as is required by an event source.
75        let wrapped_callback = move |event: &Event| {
76            let event = event.dyn_ref::<MessageEvent>().unwrap();
77            let text = event.data().as_string();
78
79            let data = if let Some(text) = text {
80                Ok(text)
81            } else {
82                Err(FormatError::ReceivedBinaryForText.into())
83            };
84
85            let out = OUT::from(data);
86            callback.emit(out);
87        };
88        self.add_unwrapped_event_listener(event_type, wrapped_callback);
89    }
90
91    /// Query the ready state of the event source.
92    pub fn ready_state(&self) -> ReadyState {
93        match self.event_source.ready_state() {
94            web_sys::EventSource::CONNECTING => ReadyState::Connecting,
95            web_sys::EventSource::OPEN => ReadyState::Open,
96            web_sys::EventSource::CLOSED => ReadyState::Closed,
97            _ => panic!("unexpected ready state"),
98        }
99    }
100}
101
102impl fmt::Debug for EventSourceTask {
103    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104        f.write_str("EventSourceTask")
105    }
106}
107
108/// An event source service attached to a user context.
109#[derive(Default, Debug)]
110pub struct EventSourceService {}
111
112impl EventSourceService {
113    /// Creates a new service instance.
114    pub fn new() -> Self {
115        Self {}
116    }
117
118    /// Connects to a server at `url` by an event source connection.
119    ///
120    /// The `notification` callback is fired when either an open or error event
121    /// happens.
122    pub fn connect(
123        &mut self,
124        url: &str,
125        notification: Callback<EventSourceStatus>,
126    ) -> Result<EventSourceTask, &str> {
127        let event_source = EventSource::new(url);
128        if event_source.is_err() {
129            return Err("Failed to created event source with given URL");
130        }
131
132        let event_source = event_source.map_err(|_| "failed to build event source")?;
133
134        let notify = notification.clone();
135        let listener_open = move |_: &Event| {
136            notify.emit(EventSourceStatus::Open);
137        };
138        let notify = notification.clone();
139        let listener_error = move |_: &Event| {
140            notify.emit(EventSourceStatus::Error);
141        };
142
143        let mut result = EventSourceTask::new(event_source, notification)?;
144        result.add_unwrapped_event_listener("open", listener_open);
145        result.add_unwrapped_event_listener("error", listener_error);
146        Ok(result)
147    }
148}
149
150impl Task for EventSourceTask {
151    fn is_active(&self) -> bool {
152        self.ready_state() == ReadyState::Open
153    }
154}
155
156impl Drop for EventSourceTask {
157    fn drop(&mut self) {
158        if self.is_active() {
159            self.event_source.close()
160        }
161    }
162}