use std::collections::HashMap;
use std::time::Duration;
use serde_json::Value;
use super::causal::{CellCommandIdentity, CellDispatchError, CellDispatchResult};
use super::store::{
CellStreamStore, DurableAggregateCellState, DurableCellCommand, DurableCellEvents,
DurableCellSnapshot,
};
use crate::aggregate::{Aggregate, AggregateRepository};
use crate::microsvc::error::HandlerError;
use crate::microsvc::service::{PortableCommand, Routes};
use crate::microsvc::session::Session;
use crate::microsvc::HasOutboxStore;
use crate::repository::{RepositoryError, SnapshotStore, StreamIdentity};
use crate::snapshot::{SnapshotRecord, Snapshottable};
use crate::{InMemoryOutboxStore, OutboxDispatcher};
pub struct AggregateCell<A>
where
A: Aggregate + Send + Sync + 'static,
{
shard: StreamIdentity,
routes: Routes<AggregateRepository<CellStreamStore, A>>,
#[cfg(feature = "workers-rs")]
celld_outbox: Option<super::celld_outbox::CelldOutbox>,
}
impl<A> AggregateCell<A>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn new(shard_id: impl Into<String>) -> Result<Self, RepositoryError> {
let shard = StreamIdentity::new(A::aggregate_type(), shard_id.into())?;
let store = CellStreamStore::for_identity(shard.clone());
Ok(Self {
shard,
routes: Routes::from_dependencies(AggregateRepository::new(store)),
#[cfg(feature = "workers-rs")]
celld_outbox: None,
})
}
pub fn instance_name(&self) -> String {
self.shard.to_string()
}
pub fn shard_id(&self) -> &str {
self.shard.aggregate_id()
}
pub fn mount(
mut self,
command: impl PortableCommand<AggregateRepository<CellStreamStore, A>>,
) -> Self {
self.routes = self.routes.mount(command);
self
}
pub fn command_names(&self) -> Vec<String> {
self.routes
.command_specs()
.unwrap_or_default()
.into_iter()
.map(|spec| spec.id)
.collect()
}
pub fn is_command_only(&self) -> bool {
self.routes.is_command_only()
}
pub async fn dispatch(
&self,
command: &str,
input: Value,
session: Session,
) -> Result<Value, HandlerError> {
self.routes
.dispatch_cell_command(command, input, session, &self.shard)
.await
}
pub async fn dispatch_idempotent(
&self,
command: &str,
identity: &CellCommandIdentity,
input: Value,
session: Session,
) -> Result<CellDispatchResult, CellDispatchError> {
self.routes
.dispatch_cell_causal(command, identity, input, session, &self.shard)
.await
}
pub async fn load(&self) -> Result<Option<A>, RepositoryError> {
self.routes.repo().get(self.shard.aggregate_id()).await
}
pub fn durable_events(&self) -> Result<Vec<DurableCellEvents>, RepositoryError> {
self.routes.repo().repo().durable_events()
}
pub fn restore_durable_events(
&self,
events: Vec<DurableCellEvents>,
) -> Result<(), RepositoryError> {
self.routes.repo().repo().restore_durable_events(events)
}
pub fn durable_outbox(&self) -> Result<Vec<crate::OutboxMessage>, RepositoryError> {
self.routes.repo().repo().durable_outbox()
}
pub fn outbox_dispatcher<P>(
&self,
publisher: P,
worker_id: impl Into<String>,
lease: Duration,
max_attempts: u32,
) -> OutboxDispatcher<InMemoryOutboxStore, P>
where
P: crate::bus::MessagePublisher,
{
OutboxDispatcher::new(
self.routes.repo().repo().outbox_store(),
publisher,
worker_id,
lease,
max_attempts,
)
}
#[cfg(feature = "workers-rs")]
pub fn with_celld_outbox(mut self, outbox: super::celld_outbox::CelldOutbox) -> Self {
self.celld_outbox = Some(outbox);
self
}
#[cfg(feature = "workers-rs")]
pub async fn persist_and_drain_outbox<F, E>(
&self,
env: &worker::Env,
storage: &worker::Storage,
persist: F,
) -> Result<crate::OutboxDispatchOutcome, crate::bus::TransportError>
where
F: Fn(&DurableAggregateCellState) -> Result<(), E>,
E: std::fmt::Display,
{
let outbox = self.celld_outbox.as_ref().ok_or_else(|| {
crate::bus::TransportError::permanent(
"aggregate cell has no celld outbox binding configured",
)
})?;
outbox.persist_and_drain(self, env, storage, persist).await
}
pub fn restore_durable_outbox(
&self,
messages: Vec<crate::OutboxMessage>,
) -> Result<(), RepositoryError> {
self.routes.repo().repo().restore_durable_outbox(messages)
}
pub fn durable_snapshots(&self) -> Result<Vec<DurableCellSnapshot>, RepositoryError> {
self.routes.repo().repo().durable_snapshots()
}
pub fn restore_durable_snapshots(
&self,
snapshots: Vec<DurableCellSnapshot>,
) -> Result<(), RepositoryError> {
self.routes
.repo()
.repo()
.restore_durable_snapshots(snapshots)
}
pub fn durable_commands(&self) -> Result<Vec<DurableCellCommand>, RepositoryError> {
self.routes.repo().repo().durable_commands()
}
pub fn restore_durable_commands(
&self,
commands: Vec<DurableCellCommand>,
) -> Result<(), RepositoryError> {
self.routes.repo().repo().restore_durable_commands(commands)
}
pub fn durable_state(&self) -> Result<DurableAggregateCellState, RepositoryError> {
self.routes.repo().repo().durable_state()
}
pub fn restore_durable_state(
&self,
state: DurableAggregateCellState,
) -> Result<(), RepositoryError> {
self.routes.repo().repo().restore_durable_state(state)
}
pub async fn cached_snapshot(&self) -> Result<Option<SnapshotRecord>, RepositoryError> {
SnapshotStore::get_snapshot(self.routes.repo().repo(), &self.shard).await
}
pub fn sealed_row(&self) -> Result<Option<Value>, RepositoryError> {
self.routes.repo().repo().sealed_row()
}
pub fn replace_sealed_row(&self, row: Value) -> Result<(), RepositoryError> {
self.routes.repo().repo().replace_sealed_row(row)
}
}
impl<A> AggregateCell<A>
where
A: Aggregate + Snapshottable + Send + Sync + 'static,
{
pub fn new_with_snapshots(
shard_id: impl Into<String>,
frequency: u64,
) -> Result<Self, RepositoryError> {
let shard = StreamIdentity::new(A::aggregate_type(), shard_id.into())?;
let store = CellStreamStore::for_identity(shard.clone());
Ok(Self {
shard,
routes: Routes::from_dependencies(
AggregateRepository::new(store).with_snapshots(frequency),
),
#[cfg(feature = "workers-rs")]
celld_outbox: None,
})
}
}
pub struct CellNamespace<A>
where
A: Aggregate + Send + Sync + 'static,
{
cells: HashMap<String, AggregateCell<A>>,
}
impl<A> Default for CellNamespace<A>
where
A: Aggregate + Send + Sync + 'static,
{
fn default() -> Self {
Self::new()
}
}
impl<A> CellNamespace<A>
where
A: Aggregate + Send + Sync + 'static,
{
pub fn new() -> Self {
Self {
cells: HashMap::new(),
}
}
pub fn get_by_name(&self, name: &str) -> Option<&AggregateCell<A>> {
self.cells.get(name)
}
pub fn get_by_name_mut(&mut self, name: &str) -> Option<&mut AggregateCell<A>> {
self.cells.get_mut(name)
}
pub fn insert(&mut self, cell: AggregateCell<A>) {
self.cells.insert(cell.instance_name(), cell);
}
pub fn get_or_create(
&mut self,
shard_id: &str,
mount: impl FnOnce(AggregateCell<A>) -> AggregateCell<A>,
) -> Result<&mut AggregateCell<A>, RepositoryError> {
let name = instance_name::<A>(shard_id);
if !self.cells.contains_key(&name) {
let cell = mount(AggregateCell::new(shard_id)?);
self.cells.insert(name.clone(), cell);
}
Ok(self.cells.get_mut(&name).expect("just inserted"))
}
}
pub fn instance_name<A: Aggregate>(shard_id: &str) -> String {
format!("{}:{shard_id}", A::aggregate_type())
}
pub fn parent_cell_name(parent_type: &str, parent_id: &str) -> String {
format!("{parent_type}:{parent_id}")
}