use std::sync::Arc;
use std::time::Duration;
use tokio::sync::{broadcast, Mutex};
use crate::error::PgError;
const NOTIFICATION_CAPACITY: usize = 64;
#[derive(Debug, Clone)]
pub struct PgNotification {
pub channel: String,
pub payload: String,
pub process_id: i32,
}
impl From<tokio_postgres::Notification> for PgNotification {
fn from(n: tokio_postgres::Notification) -> Self {
PgNotification {
process_id: n.process_id(),
channel: n.channel().to_string(),
payload: n.payload().to_string(),
}
}
}
pub struct NotificationStream {
pub(crate) rx: broadcast::Receiver<PgNotification>,
pub(crate) channel: String,
pub(crate) inner: Arc<Mutex<tokio_postgres::Client>>,
}
impl NotificationStream {
pub async fn recv_timeout(&mut self, timeout: Duration) -> Option<PgNotification> {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
if remaining.is_zero() {
return None;
}
match tokio::time::timeout(remaining, self.rx.recv()).await {
Err(_elapsed) => return None,
Ok(Err(broadcast::error::RecvError::Lagged(_))) => continue,
Ok(Err(broadcast::error::RecvError::Closed)) => return None,
Ok(Ok(n)) if n.channel == self.channel => return Some(n),
Ok(Ok(_other)) => continue,
}
}
}
pub async fn unlisten(self) -> Result<(), PgError> {
validate_channel_name(&self.channel)?;
let client = self.inner.lock().await;
client
.batch_execute(&format!("UNLISTEN {}", self.channel))
.await
.map_err(|e| PgError::Notify(e.to_string()))
}
}
pub(crate) fn spawn_connection_driver<S, T>(
mut connection: tokio_postgres::Connection<S, T>,
) -> broadcast::Sender<PgNotification>
where
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
{
let (tx, _rx) = broadcast::channel::<PgNotification>(NOTIFICATION_CAPACITY);
let tx_clone = tx.clone();
tokio::spawn(async move {
loop {
match futures::future::poll_fn(|cx| connection.poll_message(cx)).await {
None => break,
Some(Err(_e)) => break,
Some(Ok(tokio_postgres::AsyncMessage::Notification(n))) => {
let _ = tx_clone.send(PgNotification::from(n));
}
Some(Ok(_)) => {
}
}
}
});
tx
}
pub(crate) async fn notify(
inner: &Arc<Mutex<tokio_postgres::Client>>,
channel: &str,
payload: &str,
) -> Result<(), PgError> {
validate_channel_name(channel)?;
let escaped_payload = payload.replace('\'', "''");
let sql = format!("NOTIFY {channel}, '{escaped_payload}'");
let client = inner.lock().await;
client
.batch_execute(&sql)
.await
.map_err(|e| PgError::Notify(e.to_string()))
}
pub(crate) async fn listen(
inner: &Arc<Mutex<tokio_postgres::Client>>,
notif_tx: &Option<broadcast::Sender<PgNotification>>,
channel: &str,
) -> Result<NotificationStream, PgError> {
validate_channel_name(channel)?;
let tx = notif_tx.as_ref().ok_or_else(|| {
PgError::Notify(
"LISTEN requires a connection created via PgConnection::connect; \
connections created via from_client cannot receive notifications"
.to_string(),
)
})?;
let sql = format!("LISTEN {channel}");
{
let client = inner.lock().await;
client
.batch_execute(&sql)
.await
.map_err(|e| PgError::Notify(e.to_string()))?;
}
Ok(NotificationStream {
rx: tx.subscribe(),
channel: channel.to_string(),
inner: Arc::clone(inner),
})
}
pub(crate) fn validate_channel_name(name: &str) -> Result<(), PgError> {
if name.is_empty() {
return Err(PgError::Notify(
"channel name must not be empty".to_string(),
));
}
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
return Err(PgError::Notify(format!(
"invalid channel name {name:?}: only ASCII alphanumeric characters and underscores are allowed"
)));
}
Ok(())
}