use sqlx::{PgPool, postgres::PgListener};
use tokio::sync::broadcast;
use tokio_util::sync::CancellationToken;
use uuid::Uuid;
pub const PG_CHANNEL: &str = "bus_events";
#[derive(Clone, Debug)]
pub struct BusEvent(pub serde_json::Value);
impl BusEvent {
pub fn kind(&self) -> &str {
self.0.get("kind").and_then(|v| v.as_str()).unwrap_or("")
}
fn uuid_field(&self, key: &str) -> Option<Uuid> {
self.0
.get(key)
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
}
pub fn team_id(&self) -> Option<Uuid> {
self.uuid_field("team_id")
}
pub fn recipient_agent_id(&self) -> Option<Uuid> {
self.uuid_field("recipient_agent_id")
}
pub fn sender_agent_id(&self) -> Option<Uuid> {
self.uuid_field("sender_agent_id")
}
pub fn channel_id(&self) -> Option<Uuid> {
self.uuid_field("channel_id")
}
pub fn message_id(&self) -> Option<i64> {
self.0.get("id").and_then(|v| v.as_i64())
}
pub fn is_direct_message(&self) -> bool {
self.kind() == "message" && self.recipient_agent_id().is_some()
}
pub fn visible_to(&self, team_id: Uuid, agent_id: Uuid) -> bool {
if self.team_id() != Some(team_id) {
return false;
}
if self.is_direct_message() {
return self.recipient_agent_id() == Some(agent_id)
|| self.sender_agent_id() == Some(agent_id);
}
true
}
}
#[derive(Clone)]
pub struct EventHub {
tx: broadcast::Sender<BusEvent>,
}
impl Default for EventHub {
fn default() -> Self {
Self::new()
}
}
impl EventHub {
pub fn new() -> Self {
let (tx, _) = broadcast::channel(256);
Self { tx }
}
pub fn subscribe(&self) -> broadcast::Receiver<BusEvent> {
self.tx.subscribe()
}
pub fn publish(&self, event: BusEvent) {
let _ = self.tx.send(event);
}
}
pub async fn run_pg_listener(pool: PgPool, hub: EventHub, ct: CancellationToken) {
loop {
if ct.is_cancelled() {
return;
}
match PgListener::connect_with(&pool).await {
Ok(mut listener) => {
if let Err(e) = listener.listen(PG_CHANNEL).await {
tracing::warn!(error = %e, "LISTEN failed; retrying");
} else {
tracing::info!("event listener attached to '{PG_CHANNEL}'");
loop {
tokio::select! {
_ = ct.cancelled() => return,
recv = listener.try_recv() => match recv {
Ok(Some(notification)) => {
match serde_json::from_str(notification.payload()) {
Ok(value) => hub.publish(BusEvent(value)),
Err(e) => tracing::warn!(
error = %e,
payload = notification.payload(),
"unparseable bus event"
),
}
}
Ok(None) => tracing::debug!("event listener reconnected"),
Err(e) => {
tracing::warn!(error = %e, "event listener error");
break;
}
}
}
}
}
}
Err(e) => {
tracing::warn!(error = %e, "could not attach event listener; retrying");
}
}
tokio::select! {
_ = ct.cancelled() => return,
_ = tokio::time::sleep(std::time::Duration::from_secs(2)) => {}
}
}
}