Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Groundwork for application state #23

Merged
merged 8 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,876 changes: 1,139 additions & 737 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ edition = "2021"
tauri-build = { version = "1.4", features = [] }

[dependencies]
tauri = { version = "1.4", features = [ "window-set-focus", "window-set-size", "window-close", "window-create", "dialog-ask", "shell-open"] }
tauri = { version = "1.4", features = [ "window-set-focus", "window-set-size", "window-close", "window-create", "dialog-all", "shell-open"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/app/_aeon_app.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use tauri::AppHandle;

/// Serves as a global "application context" through which we can emit events or modify the
/// application state.
///
/// TODO: This isn't really implemented yet, it might never be? We'll see if we actually need it.
#[derive(Clone)]
pub struct AeonApp {
pub tauri: AppHandle,
}
114 changes: 114 additions & 0 deletions src-tauri/src/app/_aeon_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use crate::app::DynError;
use std::error::Error;
use std::fmt::{Debug, Display, Formatter};

/// [AeonError] is an implementation of [Error] which is
/// intended as a general "runtime error" in the AEON application.
pub struct AeonError {
description: String,
source: Option<DynError>,
}

impl AeonError {
/// Create a new instance of [AeonError] with the provided `description` and
/// an optional `source` [DynError].
///
/// Refer to [Error] regarding recommended error description format.
///
/// ```rust
/// # use aeon_sketchbook::app::AeonError;
/// # use std::error::Error;
/// let error = AeonError::new("something failed", None);
/// let other_error = AeonError::new("something else failed", Some(Box::new(error)));
/// assert_eq!("something else failed", format!("{}", other_error));
/// assert_eq!("something failed", format!("{}", other_error.source().unwrap()));
/// ```
pub fn new(description: impl Into<String>, source: Option<DynError>) -> AeonError {
AeonError {
description: description.into(),
source,
}
}

/// The same as [Self::new], but returns [DynError] instead.
pub fn dyn_new(description: impl Into<String>) -> DynError {
Box::new(Self::new(description, None))
}

/// Create a new instance of [AeonError], convert it to [DynError] and return it as
/// the specified [Result] type.
///
/// This function is useful when you want to return an error from a function which
/// returns some `Result<R, DynError>`, because you don't need to convert the error
/// into the expected result type.
///
/// See also [AeonError::throw_with_source].
///
/// ```rust
/// # use aeon_sketchbook::app::{AeonError, DynError};
/// fn division(numerator: i32, denominator: i32) -> Result<i32, DynError> {
/// if denominator == 0 {
/// AeonError::throw("division by zero")
/// } else {
/// Ok(numerator / denominator)
/// }
/// }
///
/// assert_eq!(5, division(10, 2).unwrap());
/// assert_eq!("division by zero", format!("{}", division(10, 0).unwrap_err()));
/// ```
pub fn throw<R>(description: impl Into<String>) -> Result<R, DynError> {
Err(Box::new(AeonError::new(description, None)))
}

/// The same as [AeonError::throw], but also includes a generic error `source`.
///
/// Note that compared to [AeonError::new], `source` can be any `Into<DynError>` type,
/// which means you can avoid conversions when they can be performed automatically
/// (see the example below).
///
/// ```rust
/// # use aeon_sketchbook::app::{AeonError, DynError};
/// fn read_number(num: &str) -> Result<i32, DynError> {
/// match num.parse::<i32>() {
/// Ok(num) => Ok(num),
/// Err(e) => AeonError::throw_with_source("invalid number", e),
/// }
/// }
///
/// assert_eq!(5, read_number("5").unwrap());
/// assert_eq!("invalid number", format!("{}", read_number("abc").unwrap_err()));
/// ```
pub fn throw_with_source<R>(
description: impl Into<String>,
source: impl Into<DynError>,
) -> Result<R, DynError> {
Err(Box::new(AeonError::new(description, Some(source.into()))))
}
}

impl Debug for AeonError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if let Some(source) = self.source.as_ref() {
write!(
f,
"AeonError[description=\"{}\",source=\"{}\"]",
self.description, source
)
} else {
write!(f, "AeonError[description=\"{}\"]", self.description)
}
}
}

impl Display for AeonError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.description)
}
}

impl Error for AeonError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source.as_ref().map(|it| it.as_ref())
}
}
88 changes: 88 additions & 0 deletions src-tauri/src/app/event.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/// An [Event] object holds information about one particular state update.
///
/// It consists of a segmented `path` and an optional `payload`. The `payload` is expected
/// to be a JSON-encoded value.
///
/// Multiple events can be combined together to create one [UserAction] or [StateChange].
/// The purpose of this mechanism is to indicate that these events should be handled together,
/// as if they represented a single UI operations (e.g. they should only
/// hold one undo stack entry).
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Event {
pub path: Vec<String>,
pub payload: Option<String>,
}

/// A [UserAction] is a collection of events that originate in one GUI action.
///
/// It does not necessarily need to be triggered by the user directly, but it is expected that
/// it somehow corresponds to a single action by the user to which the app should respond.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserAction {
pub events: Vec<Event>,
}

/// A [StateChange] is internally the same as [UserAction], but it represents a collection
/// of value updates that happened on the backend.
///
/// Typically, a [StateChange] is emitted as a result of a [UserAction]. But it can be also
/// triggered automatically, for example as part of a long-running computation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StateChange {
pub events: Vec<Event>,
}

impl Event {
pub fn build(path: &[&str], payload: Option<&str>) -> Event {
Event {
path: path.iter().map(|it| it.to_string()).collect::<Vec<_>>(),
payload: payload.map(|it| it.to_string()),
}
}

/// An estimated amount of bytes consumed by the data stored in this [Event].
pub fn byte_size(&self) -> usize {
let path_len = self.path.iter().map(|it| it.len()).sum::<usize>();
let payload_len = self.payload.as_ref().map(|it| it.len()).unwrap_or(0);
path_len + payload_len
}
}

impl UserAction {
/// An estimated size of this [UserAction] in bytes.
pub fn byte_size(&self) -> usize {
self.events.iter().map(|it| it.byte_size()).sum::<usize>()
}
}

impl From<UserAction> for StateChange {
fn from(value: UserAction) -> Self {
StateChange {
events: value.events,
}
}
}

impl From<StateChange> for UserAction {
fn from(value: StateChange) -> Self {
UserAction {
events: value.events,
}
}
}

impl From<Event> for StateChange {
fn from(value: Event) -> Self {
StateChange {
events: vec![value],
}
}
}

impl From<Event> for UserAction {
fn from(value: Event) -> Self {
UserAction {
events: vec![value],
}
}
}
25 changes: 25 additions & 0 deletions src-tauri/src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use std::error::Error;

mod _aeon_app;
mod _aeon_error;
pub mod event;
pub mod state;

pub use _aeon_app::AeonApp;
pub use _aeon_error::AeonError;

/// Label for frontend events that are changing the app state.
pub const AEON_ACTION: &str = "aeon-action";

/// Label for backend events that are notifying about a state change.
pub const AEON_VALUE: &str = "aeon-value";

/// Label for frontend events that are requesting a value retransmission.
pub const AEON_REFRESH: &str = "aeon-refresh";

/// A [DynError] is a "generic" heap-allocated trait object which implements [std::error::Error].
///
/// You can convert most standard "typed" errors into [DynError]. If you want to
/// throw a general "runtime error" with no particular type, you can also use
/// [AeonError] (see [AeonError::throw] and [AeonError::throw_with_cause]).
pub type DynError = Box<(dyn Error + 'static)>;
57 changes: 57 additions & 0 deletions src-tauri/src/app/state/_consumed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use crate::app::event::Event;
use crate::app::DynError;

/// A [Consumed] object describes possible outcomes of trying to consume an [Event]
/// by some session state object.
#[derive(Debug)]
pub enum Consumed {
/// Event was successfully consumed, resulting the provided `state_change` [Event].
///
/// Furthermore, the operation is reversible. This means it can be save to the back-stack
/// as a pair of `(perform, reverse)` events. Note that the `perform` event does not
/// *need* to be the original event. For example, there could be automated normalization
/// which won't need to be repeated.
///
/// At the moment, each event must be reversible by a single event. If this is not the
/// case, you can "restart" the evaluation process with a new, more granular event chain
/// by returning [Consumed::Restart]. It is the responsibility of the session state to
/// record this whole chain as a single reversible action.
///
/// However, note that this does not *guarantee* that the action will be saved to the
/// undo stack. If the payloads for the `(perform, reverse)` actions are too large,
/// session state can still refuse to save it, in which case the stack will be simply
/// reset to empty. Finally, some sessions may not have an undo stack at all, in which case
/// the `(perform, reverse)` pair is completely ignored.
Reversible {
state_change: Event,
perform_reverse: (Event, Event),
},

/// Action was successfully consumed, resulting in the given `state_change` [Event].
///
/// However, the action is irreversible. This means the undo stack should be either
/// cleared if `reset=true`, or the action should bypass the stack completely
/// if `reset=false`.
Irreversible { state_change: Event, reset: bool },

/// Action cannot be consumed as is and should be instead replaced by the provided
/// list of events.
///
/// Note that the original event may or may not be a member of this list. In particular,
/// if you want to retry the event, you have to manually copy it into the list.
///
/// You can use this result type to perform additional events that are necessary to execute
/// before the original event can be completed safely. For example, if we want to delete
/// a variable, we want to first delete all associated regulations.
Restart(Vec<Event>),

/// The action was consumed, but the provided user input is invalid and cannot be applied.
///
/// Note that this should only be used when the *user input* is wrong. If some other part
/// of the action is wrong (e.g. unknown path, missing fields in payload), the action cannot
/// be consumed!
InputError(DynError),

/// The action was consumed, but the application state did not change.
NoChange,
}
Loading