Skip to main content

actioncable/
subscription.rs

1use std::fmt;
2use std::pin::Pin;
3use std::sync::{Arc, Mutex, PoisonError, Weak};
4
5use serde::Serialize;
6use serde_json::{Map, Value};
7use tokio::sync::broadcast::error::RecvError;
8use tokio::sync::{broadcast, mpsc, oneshot, watch};
9
10use crate::client;
11use crate::error::Error;
12use crate::identifier::Identifier;
13use crate::message::Message;
14use crate::protocol::Command;
15
16/// How many connection events a subscription keeps for a handle that isn't reading them.
17/// One connection makes two, so this covers eight reconnects nobody looked at.
18const EVENT_BUFFER: usize = 16;
19
20/// What happened to a subscription's connection. Some channels only send what's new, so a
21/// reconnect can leave a gap, and only the client knows a reconnect happened.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum Event {
24    /// The server confirmed the subscription. `reconnected` is false the first time and true
25    /// every time after, which is when a channel that only streams what's new has a gap to
26    /// fill.
27    Connected {
28        /// False the first time, true every time after.
29        reconnected: bool,
30    },
31    /// The connection dropped. `will_reconnect` says whether the client is coming back or
32    /// has stopped for good.
33    Disconnected {
34        /// Whether the client is coming back or has stopped for good.
35        will_reconnect: bool,
36    },
37    /// The channel rejected the subscription. On the first subscribe that is also the
38    /// [`Error::Rejected`] the call returns, so a handle only ever reads this after a
39    /// reconnect. Nothing more arrives; the client has already let go of it.
40    Rejected,
41}
42
43/// One channel subscription on a client. Read what the channel sends with
44/// [`next`](Subscription::next), watch the connection with
45/// [`next_event`](Subscription::next_event), and talk back with
46/// [`perform`](Subscription::perform) or [`send`](Subscription::send).
47///
48/// Handles are cheap to clone and share one stream of messages: a message goes to whichever
49/// handle reads it first. The subscription outlives reconnects, resubscribed automatically,
50/// until [`unsubscribe`](Subscription::unsubscribe). Dropping the last handle unsubscribes
51/// too, when it happens on a tokio runtime; elsewhere it stops delivery and warns, and the
52/// server keeps the subscription until the connection ends.
53#[derive(Clone)]
54pub struct Subscription {
55    inner: Arc<Inner>,
56}
57
58struct Inner {
59    client: Weak<client::Inner>,
60    registration: Arc<Registration>,
61    messages: tokio::sync::Mutex<mpsc::Receiver<Message>>,
62    events: tokio::sync::Mutex<broadcast::Receiver<Event>>,
63    /// Flips when the callback task has run its last callback, so the message stream can
64    /// end behind it rather than in front of it.
65    callbacks: Option<Arc<watch::Sender<bool>>>,
66}
67
68impl Subscription {
69    pub(crate) fn new(
70        client: Weak<client::Inner>,
71        registration: Arc<Registration>,
72        receivers: Receivers,
73        callbacks: Option<Arc<watch::Sender<bool>>>,
74    ) -> Subscription {
75        Subscription {
76            inner: Arc::new(Inner {
77                client,
78                registration,
79                messages: tokio::sync::Mutex::new(receivers.messages),
80                events: tokio::sync::Mutex::new(receivers.events),
81                callbacks,
82            }),
83        }
84    }
85
86    /// The JSON identifier the server knows this subscription by, and echoes back on
87    /// everything it sends here.
88    pub fn key(&self) -> &str {
89        &self.inner.registration.key
90    }
91
92    /// The next message the channel broadcast or transmitted, or `None` once the
93    /// subscription is unsubscribed, rejected, or the client is closed —
94    /// [`err`](Subscription::err) says which. The last callback has returned by then, so a
95    /// loop that ends knows none is still running or about to.
96    ///
97    /// Read it promptly. A message that arrives with the buffer full is dropped and logged
98    /// rather than stalling the connection; [`ClientBuilder::message_buffer`] sizes the
99    /// buffer for a slow reader.
100    ///
101    /// [`ClientBuilder::message_buffer`]: crate::ClientBuilder::message_buffer
102    pub async fn next(&self) -> Option<Message> {
103        let message = self.inner.messages.lock().await.recv().await;
104
105        if message.is_none()
106            && let Some(callbacks) = &self.inner.callbacks
107        {
108            let mut finished = callbacks.subscribe();
109            let _last = finished.wait_for(|finished| *finished).await;
110        }
111        message
112    }
113
114    /// Why the subscription ended: [`Error::Unsubscribed`], [`Error::Rejected`], or
115    /// whatever stopped the client. `None` while it is live.
116    pub fn err(&self) -> Option<Error> {
117        self.inner.registration.reason()
118    }
119
120    /// The next connection event, in the order they happened, or `None` once the
121    /// subscription is over.
122    ///
123    /// A handle keeps the last sixteen events for a reader that isn't listening, and a
124    /// reader further behind than that skips the oldest and is told so in the log. Nothing
125    /// is lost while events are read as they come, and a handle that never reads them costs
126    /// nothing however long the client runs.
127    pub async fn next_event(&self) -> Option<Event> {
128        let mut events = self.inner.events.lock().await;
129        loop {
130            match events.recv().await {
131                Ok(event) => break Some(event),
132                Err(RecvError::Lagged(skipped)) => {
133                    tracing::warn!(key = %self.key(), skipped, "connection events went unread and were dropped");
134                }
135                Err(RecvError::Closed) => break None,
136            }
137        }
138    }
139
140    /// Invokes an action on the channel, the equivalent of the JavaScript client's
141    /// `perform`. `data` must encode to a JSON object, the only shape Rails routes to an
142    /// action, or to `null`, which sends the action alone; anything else is
143    /// [`Error::DataNotAnObject`].
144    pub async fn perform(&self, action: &str, data: impl Serialize) -> Result<(), Error> {
145        let payload = perform_payload(action, data)?;
146        self.client()?
147            .send(Command::message(self.key(), payload))
148            .await
149    }
150
151    /// Delivers `data` to the channel as-is, without naming an action. Rails routes it to
152    /// the channel's `receive` method.
153    pub async fn send(&self, data: impl Serialize) -> Result<(), Error> {
154        let payload = serde_json::to_string(&data)?;
155        self.client()?
156            .send(Command::message(self.key(), payload))
157            .await
158    }
159
160    /// Tells the server to drop the subscription and ends both streams. Other subscriptions
161    /// to the same identifier keep the server's; it only hears about the unsubscribe from
162    /// the last of them.
163    pub async fn unsubscribe(&self) -> Result<(), Error> {
164        let client = self.client()?;
165        if client.forget(&self.inner.registration, &Error::Unsubscribed) {
166            client.send(Command::unsubscribe(self.key())).await
167        } else {
168            Ok(())
169        }
170    }
171
172    fn client(&self) -> Result<Arc<client::Inner>, Error> {
173        self.inner.client.upgrade().ok_or(Error::Closed)
174    }
175}
176
177impl fmt::Debug for Subscription {
178    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
179        f.debug_struct("Subscription")
180            .field("key", &self.key())
181            .finish()
182    }
183}
184
185impl Drop for Inner {
186    fn drop(&mut self) {
187        if let Some(client) = self.client.upgrade()
188            && client.forget(&self.registration, &Error::Unsubscribed)
189        {
190            client.unsubscribe_later(&self.registration.key);
191        }
192    }
193}
194
195fn perform_payload(action: &str, data: impl Serialize) -> Result<String, Error> {
196    let mut fields = match serde_json::to_value(data)? {
197        Value::Object(fields) => Ok(fields),
198        Value::Null => Ok(Map::new()),
199        _ => Err(Error::DataNotAnObject {
200            action: action.to_string(),
201        }),
202    }?;
203    fields.insert("action".to_string(), Value::String(action.to_string()));
204    Ok(serde_json::to_string(&fields)?)
205}
206
207/// What to subscribe to, and what to do when its connection changes. The callbacks mirror
208/// the Go client's `OnConnected`, `OnDisconnected` and `OnRejected`; the same events also
209/// arrive on [`Subscription::next_event`], so use whichever reads better. A bare
210/// [`Identifier`] converts into a request with no callbacks, which is what
211/// [`Client::subscribe`](crate::Client::subscribe) takes most of the time.
212///
213/// ```no_run
214/// use actioncable::{Client, Identifier, SubscribeRequest};
215///
216/// # async fn run(client: Client) -> Result<(), actioncable::Error> {
217/// let room = client
218///     .subscribe(
219///         SubscribeRequest::new(Identifier::new("RoomChannel"))
220///             .on_connected(|reconnected| async move {
221///                 if reconnected {
222///                     println!("catch up");
223///                 }
224///             })
225///             .on_disconnected(|will_reconnect| async move {
226///                 println!("gone, back soon: {will_reconnect}");
227///             }),
228///     )
229///     .await?;
230/// # Ok(())
231/// # }
232/// ```
233///
234/// Callbacks are async: each returns a future, and `|x| async move { .. }` is how a plain
235/// closure becomes one. They run on their own tokio task, one at a time, in the order the
236/// events happened, never on the connection's task, and the next event waits for the
237/// current callback to finish. [`Client::close`](crate::Client::close),
238/// [`Client::subscribe`](crate::Client::subscribe) and
239/// [`Subscription::unsubscribe`] can all be awaited from inside one. They live as long as
240/// the [`Subscription`] does: dropping the last handle ends the callbacks along with
241/// delivery.
242pub struct SubscribeRequest {
243    identifier: Identifier,
244    callbacks: Callbacks,
245}
246
247impl SubscribeRequest {
248    /// A request for `identifier`, with no callbacks yet.
249    pub fn new(identifier: Identifier) -> SubscribeRequest {
250        SubscribeRequest {
251            identifier,
252            callbacks: Callbacks::default(),
253        }
254    }
255
256    /// Called every time the server confirms the subscription, including after a reconnect,
257    /// which is what `reconnected` reports.
258    pub fn on_connected<Fut>(
259        mut self,
260        callback: impl Fn(bool) -> Fut + Send + Sync + 'static,
261    ) -> SubscribeRequest
262    where
263        Fut: Future<Output = ()> + Send + 'static,
264    {
265        self.callbacks.connected =
266            Some(Box::new(move |reconnected| Box::pin(callback(reconnected))));
267        self
268    }
269
270    /// Called when the connection drops, with whether the client intends to dial again.
271    pub fn on_disconnected<Fut>(
272        mut self,
273        callback: impl Fn(bool) -> Fut + Send + Sync + 'static,
274    ) -> SubscribeRequest
275    where
276        Fut: Future<Output = ()> + Send + 'static,
277    {
278        self.callbacks.disconnected = Some(Box::new(move |will_reconnect| {
279            Box::pin(callback(will_reconnect))
280        }));
281        self
282    }
283
284    /// Called when the channel rejects the subscription.
285    pub fn on_rejected<Fut>(
286        mut self,
287        callback: impl Fn() -> Fut + Send + Sync + 'static,
288    ) -> SubscribeRequest
289    where
290        Fut: Future<Output = ()> + Send + 'static,
291    {
292        self.callbacks.rejected = Some(Box::new(move |()| Box::pin(callback())));
293        self
294    }
295
296    pub(crate) fn into_parts(self) -> (Identifier, Callbacks) {
297        (self.identifier, self.callbacks)
298    }
299}
300
301impl From<Identifier> for SubscribeRequest {
302    fn from(identifier: Identifier) -> SubscribeRequest {
303        SubscribeRequest::new(identifier)
304    }
305}
306
307type Callback<Arg> = Box<dyn Fn(Arg) -> Pin<Box<dyn Future<Output = ()> + Send>> + Send + Sync>;
308
309#[derive(Default)]
310pub(crate) struct Callbacks {
311    connected: Option<Callback<bool>>,
312    disconnected: Option<Callback<bool>>,
313    rejected: Option<Callback<()>>,
314}
315
316impl Callbacks {
317    fn is_empty(&self) -> bool {
318        self.connected.is_none() && self.disconnected.is_none() && self.rejected.is_none()
319    }
320
321    /// Starts the task that runs the callbacks, and hands back what flips when it has run
322    /// its last one. Nothing to call means no task and nothing to wait for.
323    pub(crate) fn spawn(
324        self,
325        events: broadcast::Receiver<Event>,
326    ) -> Option<Arc<watch::Sender<bool>>> {
327        if self.is_empty() {
328            return None;
329        }
330
331        let finished = Arc::new(watch::Sender::new(false));
332        let last = Arc::clone(&finished);
333        tokio::spawn(async move {
334            self.run(events).await;
335            last.send_replace(true);
336        });
337
338        Some(finished)
339    }
340
341    /// Runs the callbacks for every event as it arrives, one at a time, and ends with the
342    /// stream.
343    async fn run(self, mut events: broadcast::Receiver<Event>) {
344        loop {
345            match events.recv().await {
346                Ok(Event::Connected { reconnected }) => {
347                    if let Some(callback) = &self.connected {
348                        callback(reconnected).await;
349                    }
350                }
351                Ok(Event::Disconnected { will_reconnect }) => {
352                    if let Some(callback) = &self.disconnected {
353                        callback(will_reconnect).await;
354                    }
355                }
356                Ok(Event::Rejected) => {
357                    if let Some(callback) = &self.rejected {
358                        callback(()).await;
359                    }
360                }
361                Err(RecvError::Lagged(skipped)) => {
362                    tracing::warn!(
363                        skipped,
364                        "a callback fell behind and missed connection events"
365                    );
366                }
367                Err(RecvError::Closed) => break,
368            }
369        }
370    }
371}
372
373/// The server's verdict on a subscribe command.
374#[derive(Debug, Clone, Copy, PartialEq, Eq)]
375pub(crate) enum Decision {
376    Confirmed,
377    Rejected,
378}
379
380/// The client's side of one handle: what it needs to route frames to the handle and to
381/// tell it how the subscription is doing. Whether the server has confirmed the identifier
382/// is the client's to know, since every handle on one identifier shares that answer.
383pub(crate) struct Registration {
384    pub(crate) key: String,
385    decision: Mutex<Option<oneshot::Sender<Decision>>>,
386    messages: Mutex<Option<mpsc::Sender<Message>>>,
387    /// Every listener gets every event; none steals from another. Taken on close, so
388    /// nothing is announced to a subscription that is over.
389    events: Mutex<Option<broadcast::Sender<Event>>>,
390    /// Why the subscription ended, set once by whoever ended it.
391    reason: Mutex<Option<Error>>,
392}
393
394/// The handle's ends of a registration's channels.
395pub(crate) struct Receivers {
396    pub(crate) decision: oneshot::Receiver<Decision>,
397    pub(crate) messages: mpsc::Receiver<Message>,
398    pub(crate) events: broadcast::Receiver<Event>,
399}
400
401impl Registration {
402    pub(crate) fn register(key: String, buffer: usize) -> (Arc<Registration>, Receivers) {
403        let (decide, decision) = oneshot::channel();
404        let (deliver, messages) = mpsc::channel(buffer.max(1));
405        let (announce, events) = broadcast::channel(EVENT_BUFFER);
406        let registration = Arc::new(Registration {
407            key,
408            decision: Mutex::new(Some(decide)),
409            messages: Mutex::new(Some(deliver)),
410            events: Mutex::new(Some(announce)),
411            reason: Mutex::new(None),
412        });
413        (
414            registration,
415            Receivers {
416                decision,
417                messages,
418                events,
419            },
420        )
421    }
422
423    /// A second stream of the same events, for the callbacks. One asked for after the
424    /// registration closed is over before it starts.
425    pub(crate) fn watch_events(&self) -> broadcast::Receiver<Event> {
426        match self
427            .events
428            .lock()
429            .unwrap_or_else(PoisonError::into_inner)
430            .as_ref()
431        {
432            Some(announce) => announce.subscribe(),
433            None => broadcast::channel(1).1,
434        }
435    }
436
437    /// Passes the server's verdict on. The event goes out before the verdict: whoever is
438    /// waiting on the subscribe may unsubscribe the moment it wakes, and that must not get
439    /// ahead of the callback for the event that woke it.
440    pub(crate) fn confirm(&self, reconnected: bool) {
441        self.announce(Event::Connected { reconnected });
442        self.decide(Decision::Confirmed);
443    }
444
445    pub(crate) fn reject(&self) {
446        self.announce(Event::Rejected);
447        self.decide(Decision::Rejected);
448        self.close(&Error::Rejected {
449            key: self.key.clone(),
450        });
451    }
452
453    pub(crate) fn disconnect(&self, will_reconnect: bool) {
454        self.announce(Event::Disconnected { will_reconnect });
455    }
456
457    /// Hands a message to the reader, and reports whether there was room. A closed
458    /// subscription has nothing left to receive, and nothing to report.
459    pub(crate) fn deliver(&self, message: Message) -> bool {
460        match self
461            .messages
462            .lock()
463            .unwrap_or_else(PoisonError::into_inner)
464            .as_ref()
465        {
466            Some(deliver) => deliver.try_send(message).is_ok(),
467            None => true,
468        }
469    }
470
471    /// Ends both streams for the reason given, which the first one to end it wins. The
472    /// reader drains what was buffered and then sees `None`.
473    pub(crate) fn close(&self, reason: &Error) {
474        self.reason
475            .lock()
476            .unwrap_or_else(PoisonError::into_inner)
477            .get_or_insert_with(|| reason.clone());
478        self.messages
479            .lock()
480            .unwrap_or_else(PoisonError::into_inner)
481            .take();
482        self.events
483            .lock()
484            .unwrap_or_else(PoisonError::into_inner)
485            .take();
486    }
487
488    /// Why the subscription ended, `None` while it is live.
489    fn reason(&self) -> Option<Error> {
490        self.reason
491            .lock()
492            .unwrap_or_else(PoisonError::into_inner)
493            .clone()
494    }
495
496    fn decide(&self, decision: Decision) {
497        if let Some(decide) = self
498            .decision
499            .lock()
500            .unwrap_or_else(PoisonError::into_inner)
501            .take()
502            && decide.send(decision).is_err()
503        {
504            tracing::trace!(key = %self.key, "nobody is waiting on the subscribe");
505        }
506    }
507
508    fn announce(&self, event: Event) {
509        if let Some(announce) = self
510            .events
511            .lock()
512            .unwrap_or_else(PoisonError::into_inner)
513            .as_ref()
514            && announce.send(event).is_err()
515        {
516            tracing::trace!(key = %self.key, "nobody is listening for events");
517        }
518    }
519}
520
521#[cfg(test)]
522#[allow(
523    clippy::unwrap_used,
524    clippy::expect_used,
525    reason = "a test that can't have what it asked for has nothing left to assert"
526)]
527mod tests {
528    use std::collections::BTreeMap;
529
530    use serde_json::json;
531
532    use super::*;
533
534    #[test]
535    fn perform_payload_carries_the_action_alongside_the_data() {
536        assert_eq!(
537            r#"{"action":"speak","body":"Hello!"}"#,
538            perform_payload("speak", json!({ "body": "Hello!" })).unwrap()
539        );
540        assert_eq!(
541            r#"{"action":"speak"}"#,
542            perform_payload("speak", ()).unwrap()
543        );
544        assert!(matches!(
545            perform_payload("speak", vec!["nope"]),
546            Err(Error::DataNotAnObject { action }) if action == "speak"
547        ));
548        assert!(matches!(
549            perform_payload("speak", BTreeMap::from([(vec![1u8], 1u8)])),
550            Err(Error::Json(_))
551        ));
552    }
553}