mod handle;
pub use handle::{EngineGone, EngineHandle};
use std::future::Future;
use std::time::Duration;
use chrono::{DateTime, Local};
use tokio::sync::mpsc;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use crate::app::lifecycle::RuntimeLifecycle;
use crate::effect::EffectRunner;
use mermaid_domain::{Cmd, Msg, State, TurnState, update};
pub trait EffectSink {
fn dispatch(&mut self, cmd: Cmd);
}
impl EffectSink for EffectRunner {
fn dispatch(&mut self, cmd: Cmd) {
Self::dispatch(self, cmd);
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct DropEffects;
impl EffectSink for DropEffects {
fn dispatch(&mut self, _cmd: Cmd) {}
}
pub struct Observation<'a> {
pub now: DateTime<Local>,
pub msg: &'a Msg,
pub state: &'a State,
}
pub trait StepObserver {
fn observe(&mut self, obs: Observation<'_>) -> impl Future<Output = ()> + Send;
}
impl StepObserver for () {
async fn observe(&mut self, _obs: Observation<'_>) {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StepOutcome {
pub should_exit: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DriveExit {
Settled,
Exited,
Cancelled,
TimedOut,
Closed,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StopWhen {
Exit,
Settled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OnCancel {
Abort,
Unwind { grace: Duration },
}
#[derive(Debug, Clone)]
pub struct DrivePolicy {
pub stop: StopWhen,
pub cancel: Option<CancellationToken>,
pub on_cancel: OnCancel,
pub deadline: Option<Duration>,
}
impl DrivePolicy {
#[must_use]
pub const fn until_exit() -> Self {
Self {
stop: StopWhen::Exit,
cancel: None,
on_cancel: OnCancel::Abort,
deadline: None,
}
}
#[must_use]
pub const fn until_settled() -> Self {
Self {
stop: StopWhen::Settled,
cancel: None,
on_cancel: OnCancel::Abort,
deadline: None,
}
}
#[must_use]
pub fn cancel_with(mut self, token: Option<CancellationToken>, on_cancel: OnCancel) -> Self {
self.cancel = token;
self.on_cancel = on_cancel;
self
}
#[must_use]
pub const fn deadline(mut self, deadline: Option<Duration>) -> Self {
self.deadline = deadline;
self
}
}
pub struct Inbox<'a> {
msgs: &'a mut mpsc::Receiver<Msg>,
lifecycle: Option<&'a mut RuntimeLifecycle>,
}
impl<'a> Inbox<'a> {
#[must_use]
pub const fn new(msgs: &'a mut mpsc::Receiver<Msg>) -> Self {
Self {
msgs,
lifecycle: None,
}
}
#[must_use]
pub const fn with_lifecycle(mut self, lifecycle: &'a mut RuntimeLifecycle) -> Self {
self.lifecycle = Some(lifecycle);
self
}
async fn next(&mut self) -> Option<Msg> {
loop {
let Some(lifecycle) = self.lifecycle.as_mut() else {
return self.msgs.recv().await;
};
tokio::select! {
m = self.msgs.recv() => return m,
s = lifecycle.next_msg() => match s {
Some(s) => return Some(s),
None => self.lifecycle = None,
},
}
}
}
}
pub struct Engine<S: EffectSink, O: StepObserver = ()> {
state: Option<State>,
sink: S,
observer: O,
}
impl<S: EffectSink> Engine<S, ()> {
pub const fn new(state: State, sink: S) -> Self {
Self {
state: Some(state),
sink,
observer: (),
}
}
}
impl<S: EffectSink, O: StepObserver> Engine<S, O> {
pub fn with_observer<O2: StepObserver>(self, observer: O2) -> Engine<S, O2> {
Engine {
state: self.state,
sink: self.sink,
observer,
}
}
#[must_use]
pub const fn state(&self) -> &State {
Self::present(self.state.as_ref())
}
pub const fn state_mut(&mut self) -> &mut State {
self.state
.as_mut()
.expect("engine state is present between reductions")
}
pub const fn sink_mut(&mut self) -> &mut S {
&mut self.sink
}
pub fn into_parts(self) -> (State, S, O) {
(
self.state
.expect("engine state is present between reductions"),
self.sink,
self.observer,
)
}
#[must_use]
pub const fn is_idle(&self) -> bool {
matches!(self.state().turn, TurnState::Idle)
}
#[must_use]
pub fn is_settled(&self) -> bool {
self.is_idle() && self.state().ui.queued_messages.is_empty()
}
const fn present(state: Option<&State>) -> &State {
state.expect("engine state is present between reductions")
}
const fn take_state(&mut self) -> State {
self.state
.take()
.expect("engine state is present between reductions")
}
pub fn reduce(&mut self, now: DateTime<Local>, msg: Msg) -> StepOutcome {
let mut state = self.take_state();
state.now = now;
let (state, cmds) = update(state, msg);
let should_exit = state.should_exit;
self.state = Some(state);
for cmd in cmds {
self.sink.dispatch(cmd);
}
StepOutcome { should_exit }
}
pub async fn step(&mut self, msg: Msg) -> StepOutcome {
self.step_at(Local::now(), msg).await
}
pub async fn step_at(&mut self, now: DateTime<Local>, msg: Msg) -> StepOutcome {
self.notify(now, &msg).await;
self.reduce(now, msg)
}
async fn notify(&mut self, now: DateTime<Local>, msg: &Msg) {
let obs = Observation {
now,
msg,
state: Self::present(self.state.as_ref()),
};
self.observer.observe(obs).await;
}
pub async fn drive(&mut self, inbox: &mut Inbox<'_>, policy: &DrivePolicy) -> DriveExit {
let deadline = policy.deadline.map(|d| Instant::now() + d);
let mut unwind_by: Option<Instant> = None;
loop {
if matches!(policy.on_cancel, OnCancel::Abort)
&& policy
.cancel
.as_ref()
.is_some_and(CancellationToken::is_cancelled)
{
return DriveExit::Cancelled;
}
if matches!(policy.stop, StopWhen::Settled)
&& self.is_idle()
&& (self.state().ui.queued_messages.is_empty() || unwind_by.is_some())
{
return if unwind_by.is_some() {
DriveExit::Cancelled
} else {
DriveExit::Settled
};
}
let far_future = || Instant::now() + Duration::from_secs(86_400);
let msg = tokio::select! {
biased;
() = async {
match &policy.cancel {
Some(token) => token.cancelled().await,
None => std::future::pending().await,
}
}, if policy.cancel.is_some() && unwind_by.is_none() => {
match policy.on_cancel {
OnCancel::Abort => return DriveExit::Cancelled,
OnCancel::Unwind { grace } => {
unwind_by = Some(Instant::now() + grace);
Msg::CancelTurn
},
}
},
() = tokio::time::sleep_until(unwind_by.unwrap_or_else(far_future)),
if unwind_by.is_some() => {
tracing::warn!("cancelled run did not unwind within grace; hard-stopping");
return DriveExit::Cancelled;
},
() = tokio::time::sleep_until(deadline.unwrap_or_else(far_future)),
if deadline.is_some() => return DriveExit::TimedOut,
m = inbox.next() => match m {
Some(m) => m,
None => return DriveExit::Closed,
},
};
let now = Local::now();
self.notify(now, &msg).await;
if self.reduce(now, msg).should_exit {
return DriveExit::Exited;
}
}
}
}
#[cfg(test)]
mod tests;