Skip to main content

ruststream_sqlx/outbox/
wrap.rs

1//! A publisher with the publish middleware's tracking, for publishes outside the handlers.
2
3use std::fmt;
4use std::sync::Arc;
5
6use ruststream::{OutgoingFor, OutgoingMessage, Publisher};
7use sqlx::Database;
8
9use super::OUTBOX_ID_HEADER;
10use super::error::TrackedPublishError;
11use super::registry::RecordList;
12use super::switch::enabled;
13use crate::outbox::store::Store;
14
15/// A publisher that records what it publishes under a registered name, from
16/// [`wrap`](super::Outbox::wrap).
17///
18/// It publishes as the publisher it wraps does, with the same payload form and options. A message
19/// under a registered name is recorded first and carries [`OUTBOX_ID_HEADER`]; one under any
20/// other name goes out as it is.
21///
22/// # Examples
23///
24/// ```
25/// # #[cfg(all(feature = "outbox", feature = "postgres"))]
26/// # mod demo {
27/// # use ruststream::OutgoingMessage;
28/// # use ruststream::memory::prelude::*;
29/// # use ruststream_sqlx::{Outbox, outbox};
30/// # use serde::{Deserialize, Serialize};
31/// # use sqlx::postgres::{PgConnection, PgPool, Postgres};
32/// # #[derive(Outbox, sqlx::FromRow)]
33/// # #[outbox(table = "outbox")]
34/// # pub struct OrderOutbox { #[field(id)] id: i64, #[field(name)] name: String, #[field(payload)] payload: Vec<u8> }
35/// # impl outbox::Publish<Postgres> for OrderOutbox {
36/// #     async fn publish(conn: &mut PgConnection, msg: &OutgoingMessage<'_>) -> sqlx::Result<i64> {
37/// #         sqlx::query_scalar("INSERT INTO outbox (name, payload) VALUES ($1, $2) RETURNING id")
38/// #             .bind(msg.name()).bind(msg.payload()).fetch_one(conn).await
39/// #     }
40/// # }
41/// # #[derive(Deserialize)] pub struct PlaceOrder { id: u64 }
42/// # #[derive(Serialize, Deserialize, Outgoing)] #[outgoing(name = "orders")] pub struct OrderPlaced { id: u64 }
43/// # #[subscriber("checkout", reply)] async fn place(cmd: &PlaceOrder) -> OrderPlaced { OrderPlaced { id: cmd.id } }
44/// # #[subscriber("orders")] async fn fulfil(_: &OrderPlaced) -> HandlerOutcome { HandlerOutcome::ack() }
45/// pub async fn serve(pool: PgPool) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
46///     let tracking = outbox! { pool: pool, "orders" => OrderOutbox };
47///     let broker = MemoryBroker::new().bindable();
48///     let egress = broker.bind(Publish);
49///     let running = RustStream::new(AppInfo::new("orders", "0.1.0"))
50///         .with_broker(broker, |b| {
51///             b.include(fulfil);
52///         })
53///         .start()
54///         .await?;
55///
56///     let publisher = tracking.wrap(running.publisher(egress).await?);
57///     publisher.message(&OrderPlaced { id: 7 }).publish().await?;
58///
59///     running.shutdown().await?;
60///     Ok(())
61/// }
62/// # }
63/// # fn main() {}
64/// ```
65pub struct TrackedPublisher<Live, DB: Database, Records> {
66    live: Live,
67    store: Arc<Store<DB>>,
68    records: Records,
69}
70
71impl<Live, DB: Database, Records> TrackedPublisher<Live, DB, Records> {
72    pub(super) const fn new(live: Live, store: Arc<Store<DB>>, records: Records) -> Self {
73        Self {
74            live,
75            store,
76            records,
77        }
78    }
79}
80
81impl<Live: Clone, DB: Database, Records: Copy> Clone for TrackedPublisher<Live, DB, Records> {
82    fn clone(&self) -> Self {
83        Self {
84            live: self.live.clone(),
85            store: Arc::clone(&self.store),
86            records: self.records,
87        }
88    }
89}
90
91impl<Live: fmt::Debug, DB: Database, Records: fmt::Debug> fmt::Debug
92    for TrackedPublisher<Live, DB, Records>
93{
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        f.debug_struct("TrackedPublisher")
96            .field("live", &self.live)
97            .field("records", &self.records)
98            .finish_non_exhaustive()
99    }
100}
101
102impl<Live, DB, Records> Publisher for TrackedPublisher<Live, DB, Records>
103where
104    Live: Publisher,
105    DB: Database,
106    Records: RecordList<DB>,
107{
108    type Payload = Live::Payload;
109    type Error = TrackedPublishError<Live::Error>;
110    type Options = Live::Options;
111
112    async fn publish(
113        &self,
114        msg: OutgoingFor<'_, Self::Payload>,
115        options: Option<&Self::Options>,
116    ) -> Result<(), Self::Error> {
117        let msg = if enabled() && self.records.contains(msg.name()) {
118            let (name, payload, headers) = msg.into_parts();
119            // The record reads a view that borrows the payload in whatever form the publisher
120            // takes it, so the payload is neither copied nor converted.
121            let view = OutgoingMessage::with_payload(name, payload.as_ref()).with_headers(headers);
122            let recorded = self.records.record(&self.store, &view).await;
123            let (_, _, mut headers) = view.into_parts();
124            if let Some(id) = recorded {
125                headers.insert(OUTBOX_ID_HEADER, id.map_err(TrackedPublishError::Outbox)?);
126            }
127            OutgoingMessage::with_payload(name, payload).with_headers(headers)
128        } else {
129            msg
130        };
131        self.live
132            .publish(msg, options)
133            .await
134            .map_err(TrackedPublishError::Publish)
135    }
136}