updated error handling

This commit is contained in:
JasterV 2026-01-29 12:51:48 +01:00
parent 7ad9f943e7
commit 9771abfed4
5 changed files with 85 additions and 44 deletions

View file

@ -1,11 +1,17 @@
//! Actor responsible of processing side effects sent by the update actor.
use std::sync::mpsc::{Receiver, SendError, Sender};
#[derive(thiserror::Error, Debug)]
pub enum EffectsError<M> {
#[error("Failed to send message to update process")]
MessageSend(#[from] SendError<M>),
}
pub(crate) fn run<M, Msg, Eff, F>(
effects_fn: F,
rx: Receiver<(M, Eff)>,
tx: Sender<Msg>,
) -> Result<(), SendError<Msg>>
) -> Result<(), EffectsError<Msg>>
where
Msg: Send + Sync + 'static,
F: Fn(&M, Eff) -> Option<Msg>,

View file

@ -4,16 +4,16 @@ use std::fmt::Debug;
use std::sync::mpsc::{SendError, Sender};
#[derive(thiserror::Error, Debug)]
pub(crate) enum EventError<M> {
pub enum EventLoopError<M> {
#[error("Failed to send message to update process")]
MessageSend(#[from] SendError<M>),
#[error("Failed to read crossterm event")]
EventRead(#[from] std::io::Error),
}
pub(crate) fn run<M>(tx: Sender<M>) -> Result<(), EventError<M>>
pub(crate) fn run<M>(tx: Sender<M>) -> Result<(), EventLoopError<M>>
where
M: From<crossterm::event::Event> + Debug + Sync + Send + 'static,
M: From<crossterm::event::Event> + Sync + Send + 'static,
{
loop {
let message = M::from(event::read()?);

View file

@ -33,6 +33,7 @@
//! You can find a folder with example projects in the [examples](https://github.com/JasterV/teatui/tree/main/examples) folder.
use ratatui::widgets::Widget;
use std::fmt::Debug;
use std::sync::mpsc::SendError;
use std::{
sync::mpsc::{Sender, channel},
thread,
@ -40,11 +41,33 @@ use std::{
pub use update::Update;
use crate::effects::EffectsError;
use crate::events::EventLoopError;
use crate::update::UpdateError;
use crate::view::ViewError;
mod effects;
mod events;
mod update;
mod view;
#[derive(thiserror::Error, Debug)]
pub enum ProgramError<M, Msg, Eff>
where
Eff: Send + Sync + 'static,
{
#[error("The update process crashed: '{0}'")]
UpdateError(UpdateError<M, Eff>),
#[error("The effects process crashed: '{0}'")]
EffectsError(EffectsError<Msg>),
#[error("The view process crashed: '{0}'")]
ViewError(ViewError),
#[error("The event loop error crashed: '{0}'")]
EventLoopError(EventLoopError<Msg>),
#[error("Couldn't gracefully shutdown the program")]
GracefulShutdownError,
}
/// Starts the runtime which manages all the internal
/// processes and message passing.
///
@ -62,11 +85,11 @@ pub fn start<M, Msg, Eff, W, IF, UF, VF, EF>(
update_fn: UF,
view_fn: VF,
effects_fn: EF,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
) -> Result<(), ProgramError<M, Msg, Eff>>
where
M: Clone + Debug + Send + Sync + 'static,
M: Clone + Send + Sync + 'static,
Eff: Debug + Send + Sync + 'static,
Msg: From<crossterm::event::Event> + Debug + Sync + Send + 'static,
Msg: From<crossterm::event::Event> + Sync + Send + 'static,
W: Widget,
IF: Fn() -> (M, Option<Eff>) + Send + Sync + 'static,
UF: Fn(M, Msg) -> Update<M, Eff> + Send + Sync + 'static,
@ -78,7 +101,7 @@ where
let (model, effect) = init_fn();
// Channel for signaling when a task completes
let (shutdown_tx, shutdown_rx) = channel::<Result<(), _>>();
let (shutdown_tx, shutdown_rx) = channel::<Result<(), ProgramError<M, Msg, Eff>>>();
// Channels for inter-thread communication
let (update_tx, update_rx) = channel::<Msg>();
@ -88,28 +111,48 @@ where
// Spawn order is important.
// If the view actor is started after the update actor, it could happen
// that both actors have an out of sync version of the model for a bit.
//
let model_1 = model.clone();
spawn_thread(
|| view::run(model_1, terminal, view_fn, view_rx).map_err(Box::from),
shutdown_tx.clone(),
);
thread::spawn({
let model = model.clone();
let shutdown_tx = shutdown_tx.clone();
spawn_thread(
|| update::run(model, effect, update_fn, update_rx, view_tx, effects_tx).map_err(Box::from),
shutdown_tx.clone(),
);
move || {
let result = view::run(model, terminal, view_fn, view_rx)
.map_err(|err| ProgramError::ViewError(err));
let effects_update_tx = update_tx.clone();
spawn_thread(
|| effects::run(effects_fn, effects_rx, effects_update_tx).map_err(Box::from),
shutdown_tx.clone(),
);
let _ = shutdown_tx.send(result);
}
});
spawn_thread(
|| events::run(update_tx).map_err(Box::from),
shutdown_tx.clone(),
);
thread::spawn({
let shutdown_tx = shutdown_tx.clone();
move || {
let result = update::run(model, effect, update_fn, update_rx, view_tx, effects_tx)
.map_err(|err| ProgramError::UpdateError(err));
let _ = shutdown_tx.send(result);
}
});
thread::spawn({
let update_tx = update_tx.clone();
let shutdown_tx = shutdown_tx.clone();
move || {
let result = effects::run(effects_fn, effects_rx, update_tx)
.map_err(|err| ProgramError::EffectsError(err));
let _ = shutdown_tx.send(result);
}
});
thread::spawn({
let shutdown_tx = shutdown_tx.clone();
move || {
let result = events::run(update_tx).map_err(|err| ProgramError::EventLoopError(err));
let _ = shutdown_tx.send(result);
}
});
let result = shutdown_rx.recv().ok();
@ -117,20 +160,6 @@ where
match result {
Some(result) => result,
None => Ok(()),
None => Err(ProgramError::GracefulShutdownError),
}
}
fn spawn_thread<F>(
callback: F,
shutdown: Sender<Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>>,
) -> thread::JoinHandle<()>
where
F: FnOnce() -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>,
F: Send + 'static,
{
thread::spawn(move || {
let result = callback();
let _ = shutdown.send(result);
})
}

View file

@ -12,7 +12,7 @@ pub enum Update<M, E> {
}
#[derive(thiserror::Error, Debug)]
pub(crate) enum UpdateError<M, Eff>
pub enum UpdateError<M, Eff>
where
Eff: Send + Sync + 'static,
{

View file

@ -3,12 +3,18 @@ use ratatui::DefaultTerminal;
use ratatui::widgets::Widget;
use std::sync::mpsc::Receiver;
#[derive(thiserror::Error, Debug)]
pub enum ViewError {
#[error("Failed to render a widget into the terminal")]
RenderError(#[from] std::io::Error),
}
pub(crate) fn run<M, F, W>(
mut model: M,
mut terminal: DefaultTerminal,
view_fn: F,
rx: Receiver<M>,
) -> Result<(), std::io::Error>
) -> Result<(), ViewError>
where
W: Widget,
F: Fn(&M) -> W,