mod by_name;
mod database;
pub(crate) mod kinds;
mod row;
use std::fmt;
use std::pin::Pin;
use std::sync::Arc;
use std::time::Duration;
use futures::future::{BoxFuture, Either};
use futures::{Stream, StreamExt};
use ruststream::codec::CodecError;
use ruststream::{
AckError, BrokerMoves, HeaderMap, IncomingMessage, RetryDeclaration, Subscribe, Subscriber,
};
use sync_wrapper::SyncWrapper;
pub use by_name::ByName;
pub use database::RoleColumns;
pub use row::{NamedBytes, NamedId, NamedRow, NamedTime};
use super::broker::{ConnectedSqlxBroker, Shared};
use super::database::QueueDatabase;
use super::delivery::InboxDelivery;
use super::engine::Events;
use super::error::SqlxBrokerError;
use super::publish::table_of;
use super::queue::{Description, Timing, open};
use super::subscriber::InboxSubscriber;
use super::{BuiltIn, FormDialect, Plain};
use super::{InboxRow, PayloadRow};
pub(crate) trait Erased: fmt::Debug + Send + Sync {
fn payload(&self) -> &[u8];
fn headers(&self) -> &HeaderMap;
fn decode_error(&self) -> Option<&CodecError>;
fn partition_key(&self) -> Option<&[u8]>;
fn redelivery_count(&self) -> Option<u64>;
fn supports_nack_after(&self) -> bool;
fn ack(self: Box<Self>) -> BoxFuture<'static, Result<(), AckError>>;
fn nack(self: Box<Self>, requeue: bool) -> BoxFuture<'static, Result<(), AckError>>;
fn nack_after(self: Box<Self>, delay: Duration) -> BoxFuture<'static, Result<(), AckError>>;
}
impl<DB, Row> Erased for InboxDelivery<DB, Row>
where
DB: QueueDatabase,
Row: Events<DB> + PayloadRow,
{
fn payload(&self) -> &[u8] {
IncomingMessage::payload(self)
}
fn headers(&self) -> &HeaderMap {
IncomingMessage::headers(self)
}
fn decode_error(&self) -> Option<&CodecError> {
IncomingMessage::decode_error(self)
}
fn partition_key(&self) -> Option<&[u8]> {
IncomingMessage::partition_key(self)
}
fn redelivery_count(&self) -> Option<u64> {
IncomingMessage::redelivery_count(self)
}
fn supports_nack_after(&self) -> bool {
IncomingMessage::supports_nack_after(self)
}
fn ack(self: Box<Self>) -> BoxFuture<'static, Result<(), AckError>> {
Box::pin(IncomingMessage::ack(*self))
}
fn nack(self: Box<Self>, requeue: bool) -> BoxFuture<'static, Result<(), AckError>> {
Box::pin(IncomingMessage::nack(*self, requeue))
}
fn nack_after(self: Box<Self>, delay: Duration) -> BoxFuture<'static, Result<(), AckError>> {
Box::pin(IncomingMessage::nack_after(*self, delay))
}
}
pub struct NamedDelivery<DB, D = BuiltIn<DB>>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
delivered: Delivered<DB, D>,
}
#[expect(
clippy::large_enum_variant,
reason = "a by-name delivery carries its row inline; a box would allocate per delivery"
)]
enum Delivered<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
Described(InboxDelivery<DB, NamedRow<D>>),
Erased {
delivery: Box<dyn Erased>,
refused: bool,
},
}
impl<DB, D> NamedDelivery<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
const fn described(delivery: InboxDelivery<DB, NamedRow<D>>) -> Self {
Self {
delivered: Delivered::Described(delivery),
}
}
fn erased((delivery, refused): (Box<dyn Erased>, bool)) -> Self {
Self {
delivered: Delivered::Erased { delivery, refused },
}
}
}
impl<DB, D> fmt::Debug for NamedDelivery<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut tuple = f.debug_tuple("NamedDelivery");
match &self.delivered {
Delivered::Described(delivery) => tuple.field(delivery),
Delivered::Erased { delivery, .. } => tuple.field(delivery),
};
tuple.finish()
}
}
impl<DB, D> IncomingMessage for NamedDelivery<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
fn payload(&self) -> &[u8] {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::payload(delivery),
Delivered::Erased { delivery, .. } => delivery.payload(),
}
}
fn headers(&self) -> &HeaderMap {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::headers(delivery),
Delivered::Erased { delivery, .. } => delivery.headers(),
}
}
fn decode_error(&self) -> Option<&CodecError> {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::decode_error(delivery),
Delivered::Erased {
delivery,
refused: true,
} => delivery.decode_error(),
Delivered::Erased { refused: false, .. } => None,
}
}
fn partition_key(&self) -> Option<&[u8]> {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::partition_key(delivery),
Delivered::Erased { delivery, .. } => delivery.partition_key(),
}
}
fn redelivery_count(&self) -> Option<u64> {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::redelivery_count(delivery),
Delivered::Erased { delivery, .. } => delivery.redelivery_count(),
}
}
async fn ack(self) -> Result<(), AckError> {
match self.delivered {
Delivered::Described(delivery) => IncomingMessage::ack(delivery).await,
Delivered::Erased { delivery, .. } => delivery.ack().await,
}
}
async fn nack(self, requeue: bool) -> Result<(), AckError> {
match self.delivered {
Delivered::Described(delivery) => IncomingMessage::nack(delivery, requeue).await,
Delivered::Erased { delivery, .. } => delivery.nack(requeue).await,
}
}
fn supports_nack_after(&self) -> bool {
match &self.delivered {
Delivered::Described(delivery) => IncomingMessage::supports_nack_after(delivery),
Delivered::Erased { delivery, .. } => delivery.supports_nack_after(),
}
}
async fn nack_after(self, delay: Duration) -> Result<(), AckError> {
match self.delivered {
Delivered::Described(delivery) => IncomingMessage::nack_after(delivery, delay).await,
Delivered::Erased { delivery, .. } => delivery.nack_after(delay).await,
}
}
}
pub(crate) type ErasedStream =
Pin<Box<dyn Stream<Item = Result<(Box<dyn Erased>, bool), SqlxBrokerError>> + Send>>;
pub struct NamedSubscriber<DB, D = BuiltIn<DB>>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
opened: Opened<DB, D>,
}
enum Opened<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
Described(Box<InboxSubscriber<DB, NamedRow<D>>>),
Erased(SyncWrapper<ErasedStream>),
}
impl<DB, D> fmt::Debug for NamedSubscriber<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.opened {
Opened::Described(subscriber) => {
f.debug_tuple("NamedSubscriber").field(subscriber).finish()
}
Opened::Erased(_) => f.debug_struct("NamedSubscriber").finish_non_exhaustive(),
}
}
}
impl<DB, D> Subscriber for NamedSubscriber<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
type Message = NamedDelivery<DB, D>;
type Error = SqlxBrokerError;
fn stream(&mut self) -> impl Stream<Item = Result<Self::Message, Self::Error>> + Send + '_ {
match &mut self.opened {
Opened::Described(subscriber) => Either::Left(
subscriber
.stream()
.map(|delivery| delivery.map(NamedDelivery::described)),
),
Opened::Erased(stream) => Either::Right(
stream
.get_mut()
.as_mut()
.map(|delivery| delivery.map(NamedDelivery::erased)),
),
}
}
}
pub(crate) fn erased<'a, DB, Row>(
shared: &'a Arc<Shared<DB>>,
form: &'a FormDialect,
name: &'a str,
) -> BoxFuture<'a, Result<ErasedStream, SqlxBrokerError>>
where
DB: QueueDatabase,
Row: InboxRow + Events<DB> + PayloadRow,
{
Box::pin(async move {
let description = Description::of::<DB, Row>();
let subscriber = open::<DB, Row, Plain>(
shared,
form,
name,
Timing::default(),
&RetryDeclaration::new(),
&description,
)
.await?;
let stream = subscriber.into_stream().map(|delivery| {
delivery.map(|delivery| {
let refused = IncomingMessage::decode_error(&delivery).is_some();
let delivery: Box<dyn Erased> = Box::new(delivery);
(delivery, refused)
})
});
let stream: ErasedStream = Box::pin(stream);
Ok(stream)
})
}
impl<DB, D> Subscribe for ConnectedSqlxBroker<DB, D>
where
DB: QueueDatabase + RoleColumns,
D: ByName<DB> + 'static,
{
type Subscriber = NamedSubscriber<DB, D>;
type Copies = BrokerMoves;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, SqlxBrokerError> {
let Some((route, form)) = self.shared.routes.find(name) else {
return Err(SqlxBrokerError::NoRoute {
name: name.to_owned(),
});
};
let description = route.description();
if description.kinds.is_some() {
tracing::debug!(
target: "ruststream_sqlx",
path = "described",
subscription = name,
table = %table_of(&description.spec),
row = description.row,
"a by-name subscription reads its rows by role",
);
let subscriber = open::<DB, NamedRow<D>, Plain>(
&self.shared,
form,
name,
Timing::default(),
&RetryDeclaration::new(),
&description.by_role(),
)
.await?;
return Ok(NamedSubscriber {
opened: Opened::Described(Box::new(subscriber)),
});
}
tracing::debug!(
target: "ruststream_sqlx",
path = "erased",
subscription = name,
table = %table_of(&description.spec),
row = description.row,
"a by-name subscription runs its row's own code, a box per delivery and per settlement",
);
let stream = route.subscribe(&self.shared, form, name).await?;
Ok(NamedSubscriber {
opened: Opened::Erased(SyncWrapper::new(stream)),
})
}
}