1use std::fmt::Display;
15use std::ops::Deref;
16use std::sync::Arc;
17
18use cratestack_core::{CratestackError, SubscriptionBackplane, TopicHandle};
19use serde::Serialize;
20use serde::de::DeserializeOwned;
21
22use crate::SqlxRuntime;
23use crate::backplane::notify_payload::{NOTIFY_CHANNEL, encode};
24use crate::bound::{BoundTx, nested_in_bound};
25use crate::error::cratestack_error_from_sqlx;
26use crate::sqlx;
27
28pub struct Topic<T> {
32 handle: TopicHandle<T>,
33 attempt: Option<Arc<BoundTx>>,
35}
36
37impl<T> Deref for Topic<T> {
38 type Target = TopicHandle<T>;
39
40 fn deref(&self) -> &Self::Target {
41 &self.handle
42 }
43}
44
45impl<T> Topic<T>
46where
47 T: Serialize + DeserializeOwned + Send + 'static,
48{
49 pub fn new(backplane: Arc<dyn SubscriptionBackplane>, topic: &str, key: impl Display) -> Self {
51 Self {
52 handle: TopicHandle::new(backplane, topic, key),
53 attempt: None,
54 }
55 }
56
57 #[doc(hidden)]
60 pub fn in_attempt(mut self, runtime: Option<&SqlxRuntime>) -> Self {
61 self.attempt = runtime.and_then(|runtime| runtime.bound.clone());
62 self
63 }
64
65 pub async fn publish(&self, message: &T) -> Result<(), CratestackError> {
72 let Some(attempt) = &self.attempt else {
73 return self.handle.publish(message).await;
74 };
75 let payload = self.encode(message)?;
76 if !self.backplane().publishes_model_events_in_tx() {
77 attempt.defer_publish(self.backplane().clone(), self.channel().to_owned(), payload);
78 return Ok(());
79 }
80 attempt.lock().map(drop).map_err(|_| {
81 CratestackError::Internal(
82 "a topic publish is refused while `db.transaction(..)` runs on this handle: \
83 call `publish_in_tx(tx, &message)` with its `tx`"
84 .to_owned(),
85 )
86 })?;
87 nested_in_bound(attempt, async |tx| self.notify(tx, &payload).await).await
88 }
89
90 pub async fn publish_in_tx(
97 &self,
98 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
99 message: &T,
100 ) -> Result<(), CratestackError> {
101 if !self.backplane().publishes_model_events_in_tx() {
102 if let Some(attempt) = &self.attempt {
103 let payload = self.encode(message)?;
104 attempt.defer_publish(self.backplane().clone(), self.channel().to_owned(), payload);
105 return Ok(());
106 }
107 return Err(CratestackError::Internal(
108 "`publish_in_tx` needs a backplane the transaction notifies (PgNotifyBackplane); \
109 with this one, `publish` after the commit"
110 .to_owned(),
111 ));
112 }
113 self.notify(tx, &self.encode(message)?).await
114 }
115
116 async fn notify(
118 &self,
119 tx: &mut sqlx::Transaction<'_, sqlx::Postgres>,
120 payload: &str,
121 ) -> Result<(), CratestackError> {
122 let notification = encode(self.channel(), payload, None).ok_or_else(|| {
123 CratestackError::BadRequest(
124 "a topic message must stay under Postgres' 8000-byte NOTIFY limit".to_owned(),
125 )
126 })?;
127 sqlx::query("SELECT pg_notify($1, $2)")
128 .bind(NOTIFY_CHANNEL)
129 .bind(notification)
130 .execute(&mut **tx)
131 .await
132 .map_err(cratestack_error_from_sqlx)?;
133 Ok(())
134 }
135}