use std::{error::Error, fmt, future::Future};
use chrono::{DateTime, Utc};
use crate::{
Categorized, Category, Events, Failure, Permanent, Revision, Subject, Transient, UpcastEvent,
Upcasters, revision::id_of, subject::subject_of,
};
mod run;
#[cfg(test)]
pub(crate) mod tests;
pub use run::{
CaughtUpError, Phase, ProjectionOptions, ProjectionRun, ProjectionStatus, Reconnect,
wait_caught_up,
};
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct Envelope<E> {
pub id: Revision,
pub time: DateTime<Utc>,
pub source: String,
pub subject: Subject,
pub event_type: String,
pub trace_parent: Option<String>,
pub trace_state: Option<String>,
pub part: usize,
pub data: E,
}
pub(crate) const MADE_UP_SOURCE: &str = "https://architecturekit.test";
impl<E: Events> Envelope<E> {
#[must_use]
pub fn new(subject: Subject, id: impl Into<Revision>, data: E) -> Self {
Self {
id: id.into(),
time: DateTime::UNIX_EPOCH,
source: MADE_UP_SOURCE.to_owned(),
subject,
event_type: data.event_type().to_owned(),
trace_parent: None,
trace_state: None,
part: 0,
data,
}
}
}
mod sealed {
pub trait Sealed {}
impl<E: crate::Events + Send + 'static> Sealed for E {}
impl Sealed for eventsourcingdb::Event {}
}
pub trait Projected: sealed::Sealed + Sized + Send + 'static {
#[doc(hidden)]
fn project(
stored: &eventsourcingdb::Event,
upcasters: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent>;
}
impl<E: Events + Send + 'static> Projected for E {
fn project(
stored: &eventsourcingdb::Event,
upcasters: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent> {
let upcast = UpcastEvent::from_stored(stored)?;
let parts = match upcasters {
Some(upcasters) => upcasters.apply(upcast)?,
None => vec![upcast],
};
let mut envelopes = Vec::new();
for (part, event) in parts.into_iter().enumerate() {
let (id, time, source, subject, event_type) = (
event.id(),
*event.time(),
event.source().to_owned(),
event.subject().clone(),
event.event_type().to_owned(),
);
let data = match E::from_data(&event_type, event.into_data()) {
None => continue,
Some(Ok(data)) => data,
Some(Err(cause)) => {
return Err(Permanent::Other(Failure::new(
format!("decoding event {id} of type {event_type:?}"),
Some(Box::new(cause)),
)));
}
};
envelopes.push(Envelope {
id,
time,
source,
subject,
event_type,
trace_parent: stored.traceparent().map(ToOwned::to_owned),
trace_state: stored.tracestate().map(ToOwned::to_owned),
part,
data,
});
}
Ok(envelopes)
}
}
impl Projected for eventsourcingdb::Event {
fn project(
stored: &eventsourcingdb::Event,
_: Option<&Upcasters>,
) -> Result<Vec<Envelope<Self>>, Permanent> {
Ok(vec![Envelope {
id: id_of(stored)?,
time: *stored.time(),
source: stored.source().to_owned(),
subject: subject_of(stored)?,
event_type: stored.ty().to_owned(),
trace_parent: stored.traceparent().map(ToOwned::to_owned),
trace_state: stored.tracestate().map(ToOwned::to_owned),
part: 0,
data: stored.clone(),
}])
}
}
pub trait Projection: Send + Sync + 'static {
type Event: Projected;
type Error: Error + Categorized + Send + Sync + 'static;
fn apply(
&self,
event: Envelope<Self::Event>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
#[must_use]
fn upcasters() -> Option<&'static Upcasters> {
None
}
fn seen(&self, event_id: Revision) {
let _ = event_id;
}
}
pub trait ResumableProjection: Projection {
fn checkpoint(&self) -> impl Future<Output = Result<Option<Revision>, Self::Error>> + Send;
fn save_checkpoint(
&self,
event_id: Revision,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
#[must_use]
fn catch_up_batch_size(&self) -> usize {
1
}
}
pub trait TransactionalProjection: Send + Sync + 'static {
type Event: Projected;
type Error: Error + Categorized + Send + Sync + 'static;
type Transaction: Transaction<Event = Self::Event, Error = Self::Error>;
fn checkpoint(&self) -> impl Future<Output = Result<Option<Revision>, Self::Error>> + Send;
fn begin(&self) -> impl Future<Output = Result<Self::Transaction, Self::Error>> + Send;
#[must_use]
fn upcasters() -> Option<&'static Upcasters> {
None
}
#[must_use]
fn catch_up_batch_size(&self) -> usize {
1
}
}
pub trait Transaction: Send {
type Event;
type Error;
fn apply(
&mut self,
event: Envelope<Self::Event>,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn commit(
self,
last_event_id: Revision,
) -> impl Future<Output = Result<(), Self::Error>> + Send;
fn rollback(self) -> impl Future<Output = Result<(), Self::Error>> + Send;
}
#[derive(Debug)]
#[non_exhaustive]
pub enum ProjectionError {
Transient(Transient),
Permanent(Permanent),
#[non_exhaustive]
Panicked {
message: String,
},
#[non_exhaustive]
Projection {
category: Option<Category>,
error: Box<dyn Error + Send + Sync + 'static>,
},
#[non_exhaustive]
RollbackFailed {
cause: Box<ProjectionError>,
error: Box<ProjectionError>,
},
}
impl ProjectionError {
pub(crate) fn of<E: Error + Categorized + Send + Sync + 'static>(error: E) -> Self {
Self::Projection {
category: error.category(),
error: Box::new(error),
}
}
pub(crate) fn panicked(payload: &(dyn std::any::Any + Send)) -> Self {
let message = payload
.downcast_ref::<String>()
.map(String::as_str)
.or_else(|| payload.downcast_ref::<&str>().copied())
.unwrap_or("a value that is no text");
Self::Panicked {
message: message.to_owned(),
}
}
pub(crate) fn has_panicked(&self) -> bool {
match self {
Self::Panicked { .. } => true,
Self::RollbackFailed { cause, error } => cause.has_panicked() || error.has_panicked(),
Self::Transient(_) | Self::Permanent(_) | Self::Projection { .. } => false,
}
}
}
impl From<Permanent> for ProjectionError {
fn from(failure: Permanent) -> Self {
Self::Permanent(failure)
}
}
impl fmt::Display for ProjectionError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Transient(failure) => failure.fmt(formatter),
Self::Permanent(failure) => failure.fmt(formatter),
Self::Panicked { message } => write!(
formatter,
"{}: the projection panicked: {message}",
Category::Permanent
),
Self::Projection { error, .. } => error.fmt(formatter),
Self::RollbackFailed { cause, error } => {
write!(formatter, "{cause}, and rolling back failed: {error}")
}
}
}
}
impl Error for ProjectionError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::Transient(failure) => failure.source(),
Self::Permanent(failure) => failure.source(),
Self::Panicked { .. } => None,
Self::Projection { error, .. } => error.source(),
Self::RollbackFailed { cause, .. } => Some(cause.as_ref()),
}
}
}
impl Categorized for ProjectionError {
fn category(&self) -> Option<Category> {
match self {
Self::Transient(failure) => failure.category(),
Self::Permanent(failure) => failure.category(),
Self::Panicked { .. } => Some(Category::Permanent),
Self::Projection { category, .. } => *category,
Self::RollbackFailed { error, .. } if error.has_panicked() => error.category(),
Self::RollbackFailed { cause, .. } => cause.category(),
}
}
}