use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::JoinHandle;
use std::time::Duration;
use super::{Facts, row};
use crate::app::Snapshot;
use crate::board::BoardRow;
use crate::boundary::Action;
use crate::boundary::dispatch::{self, Deps};
use crate::opslog;
use crate::start::{BallSpec, Payload};
use crate::state::SnapshotCell;
use crate::ui_state::{Clock, UiState};
pub struct PilotCtx {
pub deps: Deps,
pub cell: SnapshotCell,
pub clock: Arc<dyn Clock>,
pub ui_path: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Move {
Reap {
row: BoardRow,
claimant: String,
since: String,
},
Spawn { row: BoardRow },
}
impl PilotCtx {
pub fn period(&self) -> Duration {
crate::state::latest_snapshot(&self.cell).cadence.full_sweep
}
pub fn pass(&self) -> bool {
let snapshot = crate::state::latest_snapshot(&self.cell);
if snapshot.fleet.is_empty() {
return false;
}
let ts = self.clock.stamp();
let now: i64 = ts.parse().unwrap_or(0);
let mut ui = UiState::open(self.ui_path.clone());
let board = crate::board::build(&snapshot, &ui, now);
for fleet in &board.fleet {
if let Some(one) = plan(&snapshot, fleet, &board.rows, now) {
return self.fire(&snapshot, &mut ui, &ts, fleet, &one);
}
}
false
}
fn fire(
&self,
snapshot: &Arc<Snapshot>,
ui: &mut UiState,
ts: &str,
fleet: &Facts,
one: &Move,
) -> bool {
let deps = self.deps(snapshot);
let entry = match one {
Move::Reap {
row,
claimant,
since,
} => {
if !release(&deps, ui, ts, row, claimant) {
return false;
}
row::reaped(ts.to_owned(), &fleet.workspace, &row.id, claimant, since)
}
Move::Spawn { row } => {
let Some(conversation) = Self::birth(&deps, ui, ts, fleet, row) else {
return false;
};
row::spawned(ts.to_owned(), &fleet.workspace, &row.id, &conversation)
}
};
let _ = opslog::append(&deps.state_root, &entry);
true
}
fn birth(
deps: &Deps,
ui: &mut UiState,
ts: &str,
fleet: &Facts,
row: &BoardRow,
) -> Option<String> {
let project = deps.snapshot.project_path(&row.project).ok()?;
let ball = deps
.snapshot
.balls_by_project
.get(&project)?
.iter()
.find(|b| b.id == row.id)?;
let payload = Payload::Ball {
project: row.project.clone(),
ball: BallSpec::Existing {
id: ball.id.clone(),
title: ball.title.clone(),
body: ball.body.clone(),
join: row.state,
},
};
let prepared = dispatch::prepare(deps, ts, &fleet.workspace, &project, &payload).ok()?;
let goal = prepared.goal.clone();
let fired = dispatch::prompt(deps, ui, ts, &fleet.workspace, &prepared, &goal, None);
if fired.is_err() {
release(deps, ui, ts, row, &crate::naming::leaf(&fleet.workspace));
}
fired.ok()
}
fn deps(&self, snapshot: &Arc<Snapshot>) -> Deps {
Deps {
snapshot: Arc::clone(snapshot),
..self.deps.clone()
}
}
}
mod plan;
pub use plan::plan;
fn release(deps: &Deps, ui: &mut UiState, ts: &str, row: &BoardRow, name: &str) -> bool {
released(dispatch::dispatch(
deps,
ui,
ts,
&Action::Release {
project: row.project.clone(),
id: row.id.clone(),
name: name.to_owned(),
},
))
}
fn released(reply: Result<crate::boundary::reply::Reply, String>) -> bool {
matches!(reply, Ok(crate::boundary::reply::Reply::Outcome(o)) if o.ok())
}
pub struct Pilot {
stop: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
}
impl Pilot {
pub fn spawn(ctx: PilotCtx) -> Self {
let stop = Arc::new(AtomicBool::new(false));
let flag = Arc::clone(&stop);
let handle = std::thread::spawn(move || {
while !flag.load(Ordering::Relaxed) {
ctx.pass();
std::thread::park_timeout(ctx.period());
}
});
Self {
stop,
handle: Some(handle),
}
}
}
impl Drop for Pilot {
fn drop(&mut self) {
self.stop.store(true, Ordering::Relaxed);
if let Some(handle) = self.handle.take() {
handle.thread().unpark();
let _ = handle.join();
}
}
}
#[cfg(test)]
mod tests;