made_core/ports/deliberation_observer.rs
1//! [`DeliberationObserverPort`] — per-call hook for observing a
2//! deliberation's lifecycle.
3//!
4//! Unlike [`MessagingPort`](super::MessagingPort), which broadcasts
5//! persistent domain events (NATS), this port is for **ephemeral
6//! per-call observation**: the server-streaming `StreamDeliberation`
7//! RPC wires an adapter that forwards events to an `mpsc::channel`
8//! becoming the response stream, and discards them when the stream
9//! closes. There is no subscription / replay semantics; the observer
10//! is call-scoped.
11//!
12//! Keeping this separate from `MessagingPort` means `DeliberateUseCase`
13//! can stay oblivious to whether a caller is listening on a stream,
14//! and messaging adapters stay oblivious to live stream semantics.
15//!
16//! The port is implementation-optional: callers that do not care pass
17//! [`NullObserver`], which discards every event.
18
19use async_trait::async_trait;
20use time::OffsetDateTime;
21
22use crate::entities::DeliberationPhase;
23use crate::value_objects::TaskId;
24
25#[async_trait]
26pub trait DeliberationObserverPort: Send + Sync {
27 /// Called when the aggregate transitions into `phase`.
28 ///
29 /// Observers must not block or the use case stalls; adapters that
30 /// need unbounded work should spawn it. Adapters whose sink has
31 /// closed (receiver dropped) should become a no-op silently —
32 /// the use case does not care whether anyone is still listening.
33 async fn on_phase_changed(
34 &self,
35 task_id: &TaskId,
36 phase: DeliberationPhase,
37 emitted_at: OffsetDateTime,
38 );
39}
40
41/// No-op observer. Use this in composition when no stream is active.
42#[derive(Debug, Default, Clone)]
43pub struct NullObserver;
44
45#[async_trait]
46impl DeliberationObserverPort for NullObserver {
47 async fn on_phase_changed(
48 &self,
49 _task_id: &TaskId,
50 _phase: DeliberationPhase,
51 _emitted_at: OffsetDateTime,
52 ) {
53 }
54}