use fred::prelude::*;
use fred::types::Message;
use std::sync::{Arc, Mutex, PoisonError};
fn quit_in_background(client: Client) {
if let Ok(runtime) = tokio::runtime::Handle::try_current() {
runtime.spawn(async move {
let _ = client.quit().await;
});
}
}
struct QuitOnDrop(Option<Client>);
impl Drop for QuitOnDrop {
fn drop(&mut self) {
if let Some(client) = self.0.take() {
quit_in_background(client);
}
}
}
#[derive(Default)]
struct State {
watchers: usize,
client: Option<Client>,
}
pub(super) struct SharedSubscriber {
state: Mutex<State>,
connecting: tokio::sync::Mutex<()>,
}
struct Hold(Arc<SharedSubscriber>);
impl Drop for Hold {
fn drop(&mut self) {
let last = {
let mut state = self.0.state.lock().unwrap_or_else(PoisonError::into_inner);
state.watchers = state.watchers.saturating_sub(1);
if state.watchers == 0 {
state.client.take()
} else {
None
}
};
if let Some(client) = last {
quit_in_background(client);
}
}
}
pub(super) struct Watch {
_hold: Hold,
pub(super) messages: tokio::sync::broadcast::Receiver<Message>,
}
fn escape_glob(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
if matches!(c, '*' | '?' | '[' | ']' | '\\') {
out.push('\\');
}
out.push(c);
}
out
}
impl SharedSubscriber {
pub(super) fn new() -> Arc<Self> {
Arc::new(Self {
state: Mutex::new(State::default()),
connecting: tokio::sync::Mutex::new(()),
})
}
#[cfg(test)]
pub(super) fn watcher_count(&self) -> usize {
self.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.watchers
}
pub(super) async fn join(self: &Arc<Self>, base: &Client, prefix: &str) -> Option<Watch> {
let _connecting = self.connecting.lock().await;
let hold = {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
state.watchers += 1;
let hold = Hold(self.clone());
if let Some(client) = &state.client {
return Some(Watch {
_hold: hold,
messages: client.message_rx(),
});
}
hold
};
let subscriber = base.clone_new();
let mut pending = QuitOnDrop(Some(subscriber.clone()));
subscriber.init().await.ok()?;
let escaped = escape_glob(prefix);
let patterns = vec![
format!("{escaped}:revoked-session:*"),
format!("{escaped}:revoked-user:*"),
];
subscriber.psubscribe(patterns).await.ok()?;
let messages = subscriber.message_rx();
pending.0 = None;
self.state
.lock()
.unwrap_or_else(PoisonError::into_inner)
.client = Some(subscriber);
Some(Watch {
_hold: hold,
messages,
})
}
}