From b97f205a65809545b2ddf53720583c3fa60e57c7 Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Sun, 5 Nov 2023 23:19:26 +0100 Subject: [PATCH 1/5] Initial proposal for application state handling. --- src-tauri/src/app/_aeon_app.rs | 8 + src-tauri/src/app/_aeon_error.rs | 109 ++++++++++ .../{gui/undo_stack.rs => app/_undo_stack.rs} | 59 ++--- src-tauri/src/app/event.rs | 70 ++++++ src-tauri/src/app/mod.rs | 21 ++ src-tauri/src/app/state/_consumed.rs | 38 ++++ src-tauri/src/app/state/_state_app.rs | 205 ++++++++++++++++++ src-tauri/src/app/state/_state_atomic.rs | 97 +++++++++ src-tauri/src/app/state/_state_map.rs | 68 ++++++ src-tauri/src/app/state/mod.rs | 18 ++ src-tauri/src/gui/events.rs | 47 ---- src-tauri/src/gui/mod.rs | 3 - src-tauri/src/gui/window_context.rs | 7 - src-tauri/src/lib.rs | 2 + src-tauri/src/main.rs | 66 +++++- src-tauri/tauri.conf.json | 9 - src/html/sketch-editor.html | 14 +- src/main.ts | 55 ++++- 18 files changed, 790 insertions(+), 106 deletions(-) create mode 100644 src-tauri/src/app/_aeon_app.rs create mode 100644 src-tauri/src/app/_aeon_error.rs rename src-tauri/src/{gui/undo_stack.rs => app/_undo_stack.rs} (84%) create mode 100644 src-tauri/src/app/event.rs create mode 100644 src-tauri/src/app/mod.rs create mode 100644 src-tauri/src/app/state/_consumed.rs create mode 100644 src-tauri/src/app/state/_state_app.rs create mode 100644 src-tauri/src/app/state/_state_atomic.rs create mode 100644 src-tauri/src/app/state/_state_map.rs create mode 100644 src-tauri/src/app/state/mod.rs delete mode 100644 src-tauri/src/gui/events.rs delete mode 100644 src-tauri/src/gui/mod.rs delete mode 100644 src-tauri/src/gui/window_context.rs create mode 100644 src-tauri/src/lib.rs diff --git a/src-tauri/src/app/_aeon_app.rs b/src-tauri/src/app/_aeon_app.rs new file mode 100644 index 0000000..a23689b --- /dev/null +++ b/src-tauri/src/app/_aeon_app.rs @@ -0,0 +1,8 @@ +use tauri::AppHandle; + +/// Serves as a global "application context" through which we can emit events or modify the +/// application state. +#[derive(Clone)] +pub struct AeonApp { + pub tauri: AppHandle, +} diff --git a/src-tauri/src/app/_aeon_error.rs b/src-tauri/src/app/_aeon_error.rs new file mode 100644 index 0000000..16aaa88 --- /dev/null +++ b/src-tauri/src/app/_aeon_error.rs @@ -0,0 +1,109 @@ +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, +} + +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, source: Option) -> AeonError { + AeonError { + description: description.into(), + source, + } + } + + /// 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`, 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 { + /// 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(description: impl Into) -> Result { + 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` 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 { + /// match num.parse::() { + /// 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( + description: impl Into, + source: impl Into, + ) -> Result { + 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()) + } +} diff --git a/src-tauri/src/gui/undo_stack.rs b/src-tauri/src/app/_undo_stack.rs similarity index 84% rename from src-tauri/src/gui/undo_stack.rs rename to src-tauri/src/app/_undo_stack.rs index 0f19877..40952a1 100644 --- a/src-tauri/src/gui/undo_stack.rs +++ b/src-tauri/src/app/_undo_stack.rs @@ -1,5 +1,5 @@ +use crate::app::event::UserAction; use crate::debug; -use crate::gui::events::GuiEvent; use std::collections::VecDeque; pub const DEFAULT_EVENT_LIMIT: usize = 1 << 16; // ~64k @@ -12,14 +12,14 @@ pub const DEFAULT_PAYLOAD_LIMIT: usize = 1 << 28; // 256MB /// a part of the undo/redo stack, but there are other ways for triggering those. #[derive(Clone, Eq, PartialEq)] pub struct UndoStackEntry { - perform_action: GuiEvent, - reverse_action: GuiEvent, + perform_action: UserAction, + reverse_action: UserAction, } impl UndoStackEntry { /// The sum of payload sizes for the underlying UI actions. pub fn payload_size(&self) -> usize { - self.perform_action.payload_size() + self.reverse_action.payload_size() + self.perform_action.event.payload_size() + self.reverse_action.event.payload_size() } } @@ -58,6 +58,13 @@ impl UndoStack { } } + /// Remove all elements from the [UndoStack]. + pub fn clear(&mut self) { + self.current_payload_size = 0; + self.undo_stack.clear(); + self.redo_stack.clear(); + } + /// The number of events that can be un-done. pub fn undo_len(&self) -> usize { self.undo_stack.len() @@ -74,28 +81,28 @@ impl UndoStack { /// Returns `true` if the events were successfully saved, or `false` if an error occurred, /// e.g. due to excessive payload size. #[must_use] - pub fn do_action(&mut self, perform: GuiEvent, reverse: GuiEvent) -> bool { + pub fn do_action(&mut self, perform: UserAction, reverse: UserAction) -> bool { // Items from `redo_stack` are no longer relevant since the self.redo_stack.clear(); // Drop events that are over limit (first event limit, then payload limit). while self.undo_stack.len() >= self.event_limit { let Some(event) = self.drop_undo_event() else { - break; // The stack is empty. + break; // The stack is empty. }; debug!( - "Event count exceeded. Dropping event: `{}`.", - event.perform_action.action() + "Event count exceeded. Dropping event: `{:?}`.", + event.perform_action.event.full_path ); } - let additional_payload = perform.payload_size() + reverse.payload_size(); + let additional_payload = perform.event.payload_size() + reverse.event.payload_size(); while self.current_payload_size + additional_payload >= self.payload_limit { let Some(event) = self.drop_undo_event() else { - break; // The stack is empty. + break; // The stack is empty. }; debug!( - "Payload size exceeded. Dropping event: `{}`.", - event.perform_action.action() + "Payload size exceeded. Dropping event: `{:?}`.", + event.perform_action.event.full_path ); } @@ -132,7 +139,7 @@ impl UndoStack { /// `Self::redo_action`. Returns `None` if there is no action to undo, or the "reverse" /// `GuiEvent` originally supplied to `Self::do_action`. #[must_use] - pub fn undo_action(&mut self) -> Option { + pub fn undo_action(&mut self) -> Option { let Some(entry) = self.undo_stack.pop_back() else { return None; }; @@ -147,7 +154,7 @@ impl UndoStack { /// `Self::undo_action`. Returns `None` if there is no action to redo, or the "perform" /// `GuiEvent` originally supplied to `Self::do_action`. #[must_use] - pub fn redo_action(&mut self) -> Option { + pub fn redo_action(&mut self) -> Option { let Some(entry) = self.redo_stack.pop_back() else { return None; }; @@ -175,14 +182,14 @@ impl Default for UndoStack { #[cfg(test)] mod tests { - use crate::gui::events::GuiEvent; - use crate::gui::undo_stack::UndoStack; + use crate::app::_undo_stack::UndoStack; + use crate::app::event::{Event, UserAction}; #[test] pub fn test_normal_behaviour() { let mut stack = UndoStack::default(); - let e1 = GuiEvent::with_action(&[], "action 1"); - let e2 = GuiEvent::with_action(&[], "action 2"); + let e1: UserAction = Event::build(&[], Some("payload 1")).into(); + let e2: UserAction = Event::build(&[], Some("payload 2")).into(); // We can do a bunch of events and undo/redo them. assert!(stack.do_action(e1.clone(), e2.clone())); @@ -209,12 +216,11 @@ mod tests { #[test] pub fn test_basic_limits() { - let e1 = GuiEvent::with_action(&[], "action 1"); - let e2 = GuiEvent::with_action(&[], "action 2"); - let e3 = - GuiEvent::with_action_and_payload(&["path".to_string()], "action 3", "some payload"); + let e1: UserAction = Event::build(&[], None).into(); + let e2: UserAction = Event::build(&[], None).into(); + let e3: UserAction = Event::build(&["path"], Some("payload 3")).into(); - let mut stack = UndoStack::new(4, 2 * e3.payload_size() + 1); + let mut stack = UndoStack::new(4, 2 * e3.event.payload_size() + 1); // Test that the even limit is respected. We should be able to fit 4 events. assert!(stack.do_action(e2.clone(), e1.clone())); @@ -250,10 +256,9 @@ mod tests { #[test] pub fn test_extreme_limits() { - let e1 = GuiEvent::with_action(&[], "action 1"); - let e2 = GuiEvent::with_action(&[], "action 2"); - let e3 = - GuiEvent::with_action_and_payload(&["path".to_string()], "action 3", "some payload"); + let e1: UserAction = Event::build(&[], None).into(); + let e2: UserAction = Event::build(&[], None).into(); + let e3: UserAction = Event::build(&["path"], Some("payload 3")).into(); let mut stack = UndoStack::new(0, 1024); // Cannot perform action because the stack size is zero. diff --git a/src-tauri/src/app/event.rs b/src-tauri/src/app/event.rs new file mode 100644 index 0000000..7b4dd0f --- /dev/null +++ b/src-tauri/src/app/event.rs @@ -0,0 +1,70 @@ +/// An [Event] object holds information about one particular state update. +/// +/// It consists of a segmented `path` and an optional `payload`. Under normal circumstances, +/// the payload is expected to be either a single value (e.g. a string encoding of a single +/// integer), or a JSON object (encoding multiple values). +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub struct Event { + pub full_path: Vec, + pub payload: Option, +} + +/// A [UserAction] is a type of event that (in some sense) originates in the GUI. +/// +/// It does not necessarily need to be triggered by the user directly, but it is expected that +/// it somehow corresponds to some action by the user to which the app should respond. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UserAction { + pub event: Event, +} + +/// A [StateChange] is a type of event that (in some sense) originates in the backend. +/// +/// Typically, a state change is triggered once a [UserAction] is handled by the application +/// such that the state of the application changed. However, it can be also triggered +/// automatically, e.g. by a long-running computation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StateChange { + pub event: Event, +} + +impl Event { + pub fn build(path: &[&str], payload: Option<&str>) -> Event { + Event { + full_path: path.iter().map(|it| it.to_string()).collect::>(), + payload: payload.map(|it| it.to_string()), + } + } + + /// An estimated size of the `payload` in this event, or zero if there is no `payload`. + /// + /// Note that this is not guaranteed to be the exact size. Rather, it is only an estimate + /// to determine how costly it is to store a particular collection of events in memory. + pub fn payload_size(&self) -> usize { + self.payload.as_ref().map(|it| it.len()).unwrap_or(0) + } +} + +impl From for StateChange { + fn from(value: UserAction) -> Self { + StateChange { event: value.event } + } +} + +impl From for UserAction { + fn from(value: StateChange) -> Self { + UserAction { event: value.event } + } +} + +impl From for StateChange { + fn from(value: Event) -> Self { + StateChange { event: value } + } +} + +impl From for UserAction { + fn from(value: Event) -> Self { + UserAction { event: value } + } +} diff --git a/src-tauri/src/app/mod.rs b/src-tauri/src/app/mod.rs new file mode 100644 index 0000000..bb82aa2 --- /dev/null +++ b/src-tauri/src/app/mod.rs @@ -0,0 +1,21 @@ +use std::error::Error; + +mod _aeon_app; +mod _aeon_error; +mod _undo_stack; +pub mod event; +pub mod state; + +pub use _aeon_app::AeonApp; +pub use _aeon_error::AeonError; +pub use _undo_stack::UndoStack; + +pub const EVENT_USER_ACTION: &str = "aeon-user-action"; +pub const EVENT_STATE_CHANGE: &str = "aeon-state-change"; + +/// 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)>; diff --git a/src-tauri/src/app/state/_consumed.rs b/src-tauri/src/app/state/_consumed.rs new file mode 100644 index 0000000..210bd9d --- /dev/null +++ b/src-tauri/src/app/state/_consumed.rs @@ -0,0 +1,38 @@ +use crate::app::event::{StateChange, UserAction}; +use crate::app::DynError; + +/// A [Consumed] object describes possible outcomes of trying to consume a [UserAction] +/// by the [AppState]. +#[derive(Debug)] +pub enum Consumed { + /// Action was successfully consumed, resulting in the given [StateChange]. + /// + /// Furthermore, the action is reversible, meaning it can be saved to the back-stack + /// using the provided pair of `(perform, reverse)` actions. Note that the `perform` + /// action can be a copy of the original user action, but it can be also a different event, + /// for example if the state object performed some automatic value normalization. + /// + /// Note that this does not *guarantee* that the action will be saved to the back stack. + /// If the payloads for the `(perform, reverse)` actions are too large, [AppState] can + /// still refuse to save it, in which case the stack will be simply reset to empty. + Reversible(StateChange, (UserAction, UserAction)), + + /// Action was successfully consumed, resulting in the given [StateChange]. + /// + /// However, the action is irreversible, meaning the back-stack needs to be reset. + Irreversible(StateChange), + + /// Action cannot be consumed at its intended path and should be re-emitted as + /// the freshly constructed [UserAction]. + Restart(UserAction), + + /// 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, +} diff --git a/src-tauri/src/app/state/_state_app.rs b/src-tauri/src/app/state/_state_app.rs new file mode 100644 index 0000000..5d2c115 --- /dev/null +++ b/src-tauri/src/app/state/_state_app.rs @@ -0,0 +1,205 @@ +use crate::app::event::UserAction; +use crate::app::state::{Consumed, DynSessionState, MapState, SessionState}; +use crate::app::{AeonApp, AeonError, DynError, UndoStack, EVENT_STATE_CHANGE}; +use crate::debug; +use std::ops::DerefMut; +use std::sync::Mutex; +use tauri::Manager; + +/// [AppState] is a special wrapper around [MapState] which implements the "top level" +/// container for the whole application state. +/// +/// Specifically, it ensures: +/// - Synchronization by locking the app state. +/// - Ability to add/remove windows as they are opened/closed. +/// - Error handling and retransmission of events. +/// +/// As such, [AppState] does not actually implement the [SessionState] trait. +pub struct AppState { + state: Mutex<(MapState, UndoStack)>, +} + +impl Default for AppState { + fn default() -> Self { + AppState { + state: Mutex::new((MapState::default(), UndoStack::default())), + } + } +} + +impl AppState { + pub fn window_created(&self, label: &str, state: impl Into) { + let mut guard = self.state.lock().unwrap_or_else(|_e| { + panic!("Main application state is poisoned. Cannot recover."); + }); + let (windows, _stack) = guard.deref_mut(); + if windows.state.contains_key(label) { + // TODO: This should be a normal error. + panic!("Window already exists"); + } + windows.state.insert(label.to_string(), state.into()); + } + + pub fn undo(&self, app: &AeonApp) -> Result<(), DynError> { + let mut guard = self + .state + .lock() + .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); + let (windows, stack) = guard.deref_mut(); + let mut event = match stack.undo_action() { + Some(reverse) => reverse, + None => { + debug!("Cannot undo."); + return Ok(()); + } + }; + let state_change = loop { + let path = event + .event + .full_path + .iter() + .map(|it| it.as_str()) + .collect::>(); + let result = windows.consume_event(&path, &event); + match result { + Err(error) => { + debug!("Event error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::NoChange) => { + debug!("No change."); + return Ok(()); + } + // TODO: We should treat this error differently. + Ok(Consumed::InputError(error)) => { + debug!("User input error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::Irreversible(_)) => { + // TODO: This probably shouldn't happen here. + panic!("Irreversible action as a result of undo.") + } + Ok(Consumed::Reversible(state, _)) => { + debug!("Reversible state change: `{:?}`.", state); + break state; + } + Ok(Consumed::Restart(action)) => { + event = action; + } + } + }; + if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { + return AeonError::throw_with_source("Error sending state event.", e); + } + Ok(()) + } + + pub fn redo(&self, app: &AeonApp) -> Result<(), DynError> { + let mut guard = self + .state + .lock() + .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); + let (windows, stack) = guard.deref_mut(); + let mut event = match stack.redo_action() { + Some(perform) => perform, + None => { + debug!("Cannot redo."); + return Ok(()); + } + }; + let state_change = loop { + let path = event + .event + .full_path + .iter() + .map(|it| it.as_str()) + .collect::>(); + let result = windows.consume_event(&path, &event); + match result { + Err(error) => { + debug!("Event error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::NoChange) => { + debug!("No change."); + return Ok(()); + } + // TODO: We should treat this error differently. + Ok(Consumed::InputError(error)) => { + debug!("User input error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::Irreversible(_)) => { + // TODO: This probably shouldn't happen here. + panic!("Irreversible action as a result of redo.") + } + Ok(Consumed::Reversible(state, _)) => { + debug!("Reversible state change: `{:?}`.", state); + break state; + } + Ok(Consumed::Restart(action)) => { + event = action; + } + } + }; + if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { + return AeonError::throw_with_source("Error sending state event.", e); + } + Ok(()) + } + + pub fn consume_event(&self, app: &AeonApp, mut event: UserAction) -> Result<(), DynError> { + let mut guard = self + .state + .lock() + .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); + let (windows, stack) = guard.deref_mut(); + let state_change = loop { + let path = event + .event + .full_path + .iter() + .map(|it| it.as_str()) + .collect::>(); + let result = windows.consume_event(&path, &event); + match result { + Err(error) => { + debug!("Event error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::NoChange) => { + debug!("No change."); + return Ok(()); + } + // TODO: We should treat this error differently. + Ok(Consumed::InputError(error)) => { + debug!("User input error: `{:?}`.", error); + return Err(error); + } + Ok(Consumed::Irreversible(state)) => { + debug!("Irreversible state change: `{:?}`.", state); + stack.clear(); + break state; + } + Ok(Consumed::Reversible(state, (perform, reverse))) => { + debug!("Reversible state change: `{:?}`.", state); + if !stack.do_action(perform, reverse) { + // TODO: + // This is a warning, because the state has been applied at + // this point, but we should think a bit more about how this + // should be ideally handled. + stack.clear(); + } + break state; + } + Ok(Consumed::Restart(action)) => { + event = action; + } + } + }; + if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { + return AeonError::throw_with_source("Error sending state event.", e); + } + Ok(()) + } +} diff --git a/src-tauri/src/app/state/_state_atomic.rs b/src-tauri/src/app/state/_state_atomic.rs new file mode 100644 index 0000000..b724d02 --- /dev/null +++ b/src-tauri/src/app/state/_state_atomic.rs @@ -0,0 +1,97 @@ +use crate::app::event::{StateChange, UserAction}; +use crate::app::state::{Consumed, SessionState}; +use crate::app::{AeonError, DynError}; +use std::str::FromStr; + +/// Atomic state is a [SessionState] which holds exactly one value of a generic type `T`. +/// +/// The generic type `T` needs to implement [FromStr] and [ToString] to make it serializable +/// into the [crate::app::event::Event] payload. Furthermore, we need [PartialEq] to check +/// if the value changed, and [Clone] to enable state replication. +/// +/// Each [AtomicState] only consumes one type of event: the remaining `path` must be empty +/// and the event `payload` must deserialize into a valid `T` value. +#[derive(Clone, Debug)] +pub struct AtomicState(T) +where + T: FromStr + ToString + PartialEq + Clone; + +impl From for AtomicState { + fn from(value: T) -> Self { + AtomicState(value) + } +} + +impl Default for AtomicState { + fn default() -> Self { + AtomicState(T::default()) + } +} + +impl SessionState for AtomicState { + fn consume_event(&mut self, path: &[&str], action: &UserAction) -> Result { + if !path.is_empty() { + let msg = format!("Atomic state cannot consume a path `{:?}`.", path); + return AeonError::throw(msg); + } + let Some(payload) = &action.event.payload else { + return AeonError::throw("Missing payload for atomic state event."); + }; + let Ok(payload) = T::from_str(payload) else { + let msg = format!( + "Cannot convert input `{}` to type `{}`.", + payload, + std::any::type_name::() + ); + return Ok(Consumed::InputError(Box::new(AeonError::new(msg, None)))); + }; + if self.0 == payload { + return Ok(Consumed::NoChange); + } + let perform_event = action.clone(); + let mut reverse_event = action.clone(); + reverse_event.event.payload = Some(self.0.to_string()); + self.0 = payload; + + Ok(Consumed::Reversible( + StateChange::from(action.clone()), + (perform_event, reverse_event), + )) + } +} + +#[cfg(test)] +mod tests { + use crate::app::event::{Event, UserAction}; + use crate::app::state::_state_atomic::AtomicState; + use crate::app::state::{Consumed, SessionState}; + + #[test] + fn test_atomic_state() { + let mut state = AtomicState::from(3); + + let event_3: UserAction = Event::build(&["segment"], Some("3")).into(); + let event_4: UserAction = Event::build(&["segment"], Some("4")).into(); + let event_empty: UserAction = Event::build(&["segment"], None).into(); + let event_invalid: UserAction = Event::build(&["segment"], Some("abc")).into(); + + let result = state.consume_event(&[], &event_3).unwrap(); + assert!(matches!(result, Consumed::NoChange)); + + let result = state.consume_event(&[], &event_4).unwrap(); + assert!(matches!(result, Consumed::Reversible(..))); + + let result = state.consume_event(&["foo"], &event_3).unwrap_err(); + assert_eq!( + "Atomic state cannot consume a path `[\"foo\"]`.", + format!("{}", result) + ); + let result = state.consume_event(&[], &event_empty).unwrap_err(); + assert_eq!( + "Missing payload for atomic state event.", + format!("{}", result) + ); + let result = state.consume_event(&[], &event_invalid).unwrap(); + assert!(matches!(result, Consumed::InputError(..))); + } +} diff --git a/src-tauri/src/app/state/_state_map.rs b/src-tauri/src/app/state/_state_map.rs new file mode 100644 index 0000000..210a625 --- /dev/null +++ b/src-tauri/src/app/state/_state_map.rs @@ -0,0 +1,68 @@ +use crate::app::event::UserAction; +use crate::app::state::{Consumed, DynSessionState, SessionState}; +use crate::app::{AeonError, DynError}; +use std::collections::HashMap; + +/// A group of several dynamic state objects accessible using path segments. +/// +/// It is generally assumed that the managed state objects are created statically at the +/// same time as [MapState], because we cannot create/remove states dynamically from this +/// map using just events (although you could in theory do it internally from Rust). +#[derive(Default)] +pub struct MapState { + pub state: HashMap, +} + +impl<'a> FromIterator<(&'a str, DynSessionState)> for MapState { + fn from_iter>(iter: T) -> Self { + let map = HashMap::from_iter(iter.into_iter().map(|(k, v)| (k.to_string(), v))); + MapState { state: map } + } +} + +impl SessionState for MapState { + fn consume_event(&mut self, path: &[&str], action: &UserAction) -> Result { + let Some(prefix) = path.first() else { + return AeonError::throw("State map cannot consume an empty path."); + }; + let Some(sub_state) = self.state.get_mut(*prefix) else { + let msg = format!("Unknown path segment `{}`.", prefix); + return AeonError::throw(msg); + }; + sub_state.consume_event(&path[1..], action) + } +} + +#[cfg(test)] +mod tests { + use crate::app::event::{Event, UserAction}; + use crate::app::state::_state_map::MapState; + use crate::app::state::{AtomicState, Consumed, DynSessionState, SessionState}; + + #[test] + fn test_map_state() { + let inner: Vec<(&str, DynSessionState)> = vec![ + ("state_1", Box::new(AtomicState::from(5i32))), + ("state_2", Box::new(AtomicState::from("test".to_string()))), + ("state_3", Box::new(AtomicState::from(123u64))), + ]; + let mut state_map = MapState::from_iter(inner.into_iter()); + + let event_3: UserAction = Event::build(&["state_1"], Some("3")).into(); + let event_str: UserAction = Event::build(&["state_2"], Some("test")).into(); + + let result = state_map.consume_event(&["state_1"], &event_3).unwrap(); + assert!(matches!(result, Consumed::Reversible(..))); + let result = state_map.consume_event(&["state_2"], &event_str).unwrap(); + assert!(matches!(result, Consumed::NoChange)); + + let result = state_map.consume_event(&[], &event_3).unwrap_err(); + assert_eq!( + "State map cannot consume an empty path.", + format!("{}", result) + ); + + let result = state_map.consume_event(&["state_4"], &event_3).unwrap_err(); + assert_eq!("Unknown path segment `state_4`.", format!("{}", result)); + } +} diff --git a/src-tauri/src/app/state/mod.rs b/src-tauri/src/app/state/mod.rs new file mode 100644 index 0000000..30837d2 --- /dev/null +++ b/src-tauri/src/app/state/mod.rs @@ -0,0 +1,18 @@ +use crate::app::event::UserAction; +use crate::app::DynError; + +mod _consumed; +mod _state_app; +mod _state_atomic; +mod _state_map; + +pub use _consumed::Consumed; +pub use _state_app::AppState; +pub use _state_atomic::AtomicState; +pub use _state_map::MapState; + +pub type DynSessionState = Box<(dyn SessionState + Send + 'static)>; + +pub trait SessionState { + fn consume_event(&mut self, path: &[&str], action: &UserAction) -> Result; +} diff --git a/src-tauri/src/gui/events.rs b/src-tauri/src/gui/events.rs deleted file mode 100644 index 5dc2a87..0000000 --- a/src-tauri/src/gui/events.rs +++ /dev/null @@ -1,47 +0,0 @@ -/// At this point, we don't really know what types of GUI events we'll be sending, -/// so it's just a generic collection of values which tells us: -/// - What's going on (`action`); -/// - What component is affected (`path`); -/// - And whether there is any extra data (`payload`); -/// -/// In the future, we might want to instead have an enum of a few common even types, plus -/// a very generic "any" event like this. -/// -/// TODO: -/// - Add a nicer debug format. -#[derive(Clone, Eq, PartialEq, Debug)] -pub struct GuiEvent { - path: Vec, - action: String, - payload: Option, -} - -impl GuiEvent { - pub fn with_action(path: &[String], action: &str) -> GuiEvent { - GuiEvent { - path: Vec::from_iter(path.iter().cloned()), - action: action.to_string(), - payload: None, - } - } - - pub fn with_action_and_payload(path: &[String], action: &str, payload: &str) -> GuiEvent { - GuiEvent { - path: Vec::from_iter(path.iter().cloned()), - action: action.to_string(), - payload: Some(payload.to_string()), - } - } - - /// An estimated size of the payload in this event, or zero if there is no payload. - /// - /// Note that this is not guaranteed to be the exact size. Rather, it is only an estimate - /// to determine how costly it is to store a particular collection of events in memory. - pub fn payload_size(&self) -> usize { - self.payload.as_ref().map(|it| it.len()).unwrap_or(0) - } - - pub fn action(&self) -> &str { - self.action.as_str() - } -} diff --git a/src-tauri/src/gui/mod.rs b/src-tauri/src/gui/mod.rs deleted file mode 100644 index 80ffd5e..0000000 --- a/src-tauri/src/gui/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod events; -pub mod undo_stack; -pub mod window_context; diff --git a/src-tauri/src/gui/window_context.rs b/src-tauri/src/gui/window_context.rs deleted file mode 100644 index e161ec4..0000000 --- a/src-tauri/src/gui/window_context.rs +++ /dev/null @@ -1,7 +0,0 @@ -/// Window context is an "god object" that owns all essential data structures of a single AEON -/// window. However, note that a window can actually have some of its components in "detached" -/// windows. In such cases, the window context also tracks the detached windows and delivers -/// necessary information to them (i.e. detached windows technically have their own context, -/// but their context is only a proxy for the "main" window context which holds the actual -/// state of all values). -pub struct WindowContext {} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 0000000..334b858 --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,2 @@ +pub mod app; +pub mod logging; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a07e8f1..f063871 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,18 +1,66 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -pub mod gui; -pub mod logging; - -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -#[tauri::command] -fn greet(name: &str) -> String { - format!("Hello, {}! You've been greeted from Rust!", name) -} +use aeon_sketchbook::app::event::{Event, UserAction}; +use aeon_sketchbook::app::state::{AppState, AtomicState, DynSessionState, MapState}; +use aeon_sketchbook::app::{AeonApp, EVENT_USER_ACTION}; +use aeon_sketchbook::debug; +use tauri::Manager; fn main() { + let editor_state: Vec<(&str, DynSessionState)> = vec![ + ("counter", Box::new(AtomicState::from(0))), + ("text", Box::new(AtomicState::from("".to_string()))), + ]; + let editor_state: DynSessionState = Box::new(MapState::from_iter(editor_state)); + let state = AppState::default(); + state.window_created("editor", editor_state); tauri::Builder::default() - .invoke_handler(tauri::generate_handler![greet]) + .manage(state) + .setup(|app| { + let handle = app.handle(); + let aeon_original = AeonApp { + tauri: handle.clone(), + }; + let aeon = aeon_original.clone(); + app.listen_global(EVENT_USER_ACTION, move |e| { + let Some(payload) = e.payload() else { + // TODO: This should be an error. + panic!("No payload in user action."); + }; + debug!("Received user action: `{}`.", payload); + let event: UserAction = match serde_json::from_str::(payload) { + Ok(event) => event.into(), + Err(e) => { + // TODO: This should be a normal error. + panic!("Payload deserialize error {:?}.", e); + } + }; + let state = aeon.tauri.state::(); + let result = state.consume_event(&aeon, event); + if let Err(e) = result { + // TODO: This should be a normal error. + panic!("Event error: {:?}", e); + } + }); + let aeon = aeon_original.clone(); + app.listen_global("undo", move |_e| { + let state = aeon.tauri.state::(); + if let Err(e) = state.undo(&aeon) { + // TODO: This should be a normal error. + panic!("Undo error: {:?}", e); + } + }); + let aeon = aeon_original.clone(); + app.listen_global("redo", move |_e| { + let state = aeon.tauri.state::(); + if let Err(e) = state.redo(&aeon) { + // TODO: This should be a normal error. + panic!("Redo error: {:?}", e); + } + }); + Ok(()) + }) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index c167e7d..7fac7c1 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -34,15 +34,6 @@ "csp": null }, "windows": [ - { - "label": "signpost", - "fullscreen": false, - "resizable": true, - "title": "aeon-signpost", - "width": 800, - "height": 600, - "url": "src/html/signpost.html" - }, { "label": "editor", "fullscreen": false, diff --git a/src/html/sketch-editor.html b/src/html/sketch-editor.html index 63d350e..cdad16e 100644 --- a/src/html/sketch-editor.html +++ b/src/html/sketch-editor.html @@ -2,6 +2,18 @@ - +
+ + +
+
+ + 0 + +
+
+ + +
\ No newline at end of file diff --git a/src/main.ts b/src/main.ts index dc10e12..cf4c30b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,13 +1,60 @@ -import { invoke } from "@tauri-apps/api/tauri"; +import { emit, listen } from '@tauri-apps/api/event' import UIkit from 'uikit'; import Icons from 'uikit/dist/js/uikit-icons'; + + // loads the Icon plugin UIkit.use(Icons); +window.addEventListener("DOMContentLoaded", async () => { + const counter = document.querySelector("#counter") as HTMLElement; + const counter_up = document.querySelector("#counter-up") as HTMLElement; + const counter_down = document.querySelector("#counter-down") as HTMLElement; + const text = document.querySelector("#text") as HTMLInputElement; + await listen("aeon-state-change", (event) => { + const aeon_path = event.payload.full_path; + const aeon_payload = event.payload.payload; + if(aeon_path.length == 2 && aeon_path[0] == "editor" && aeon_path[1] == "counter") { + counter.innerText = aeon_payload + } + if(aeon_path.length == 2 && aeon_path[0] == "editor" && aeon_path[1] == "text") { + text.value = aeon_payload + } + }); + counter_up.addEventListener("click", () => { + emit("aeon-user-action", { + full_path: ["editor", "counter"], + payload: (parseInt(counter.innerText) + 1).toString() + }); + }); + counter_down.addEventListener("click", () => { + emit("aeon-user-action", { + full_path: ["editor", "counter"], + payload: (parseInt(counter.innerText) - 1).toString() + }); + }); + text.addEventListener("input", () => { + emit("aeon-user-action", { + full_path: ["editor", "text"], + payload: text.value + }); + }); + + const undo = document.querySelector("#undo") as HTMLElement; + undo.addEventListener("click", () => { + emit("undo"); + }); + const redo = document.querySelector("#redo") as HTMLElement; + redo.addEventListener("click", () => { + emit("redo"); + }); + +}); + // components can be called from the imported UIkit reference // UIkit.notification('Hello world.'); - +/* let greetInputEl: HTMLInputElement | null; let greetMsgEl: HTMLElement | null; @@ -27,4 +74,6 @@ window.addEventListener("DOMContentLoaded", () => { e.preventDefault(); greet(); }); -}); +});*/ + + From 81669f858993c55da694a832a187c51c1cdd192e Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 6 Dec 2023 19:46:09 +0100 Subject: [PATCH 2/5] Working `undo/redo` including complex state such as tab pinning. --- package-lock.json | 1876 ++++++++++------- src-tauri/Cargo.lock | 101 + src-tauri/Cargo.toml | 2 +- src-tauri/src/app/_aeon_app.rs | 2 + src-tauri/src/app/_aeon_error.rs | 5 + src-tauri/src/app/event.rs | 64 +- src-tauri/src/app/mod.rs | 12 +- src-tauri/src/app/state/_consumed.rs | 55 +- src-tauri/src/app/state/_state_app.rs | 223 +- src-tauri/src/app/state/_state_atomic.rs | 83 +- src-tauri/src/app/state/_state_map.rs | 5 + src-tauri/src/app/{ => state}/_undo_stack.rs | 72 +- .../app/state/editor/_state_editor_session.rs | 206 ++ .../src/app/state/editor/_state_tab_bar.rs | 35 + src-tauri/src/app/state/editor/mod.rs | 7 + src-tauri/src/app/state/mod.rs | 27 +- src-tauri/src/main.rs | 78 +- src-tauri/tauri.conf.json | 3 + src/aeon_events.ts | 355 ++++ .../component/content-pane/content-pane.ts | 13 +- .../root-component/root-component.ts | 47 +- src/html/component/tab-bar/tab-bar.ts | 13 +- src/html/component/undo-redo/undo-redo.ts | 24 +- src/html/sketch-editor.html | 15 +- src/html/util/config.ts | 3 +- src/main.ts | 72 - 26 files changed, 2236 insertions(+), 1162 deletions(-) rename src-tauri/src/app/{ => state}/_undo_stack.rs (79%) create mode 100644 src-tauri/src/app/state/editor/_state_editor_session.rs create mode 100644 src-tauri/src/app/state/editor/_state_tab_bar.rs create mode 100644 src-tauri/src/app/state/editor/mod.rs create mode 100644 src/aeon_events.ts diff --git a/package-lock.json b/package-lock.json index ecd7016..3588cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -434,9 +434,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -456,88 +456,43 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz", - "integrity": "sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.5.1.tgz", + "integrity": "sha512-GkWzv+L6d2bI5f/Vk6ikJ9xtl7dfXtoRu3YGE6nq0p/FFqA1ebMOAWg3XgRyb0I6LYyYkiAo+3/KrwuBp8xG7A==", "hasInstallScript": true, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz", - "integrity": "sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.5.1.tgz", + "integrity": "sha512-MfRCYlQPXoLlpem+egxjfkEuP9UQswTrlCOsknus/NcMoblTH2g0jPrapbcIb04KGA7E2GZxbAccGZfWoYgsrQ==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.2" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz", - "integrity": "sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.5.1.tgz", + "integrity": "sha512-S1PPfU3mIJa59biTtXJz1oI0+KAXW6bkAb31XKhxdxtuXDiUIFsih4JR1v5BbxY7hVHsD1RKq+jRkVRaf773NQ==", "hasInstallScript": true, "dependencies": { - "@fortawesome/fontawesome-common-types": "6.4.2" + "@fortawesome/fontawesome-common-types": "6.5.1" }, "engines": { "node": ">=6" @@ -557,51 +512,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -691,9 +601,9 @@ "integrity": "sha512-jnOD+/+dSrfTWYfSXBXlo5l5f0q1UuJo3tkbMDCYA2lKUYq79jaxqtGEvnRoh049nt1vdo1+45RinipU6FGY2g==" }, "node_modules/@lit/reactive-element": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.1.tgz", - "integrity": "sha512-eu50SQXHRthFwWJMp0oAFg95Rvm6MTPjxSXWuvAu7It90WVFLFpNBoIno7XOXSDvVgTrtKnUV4OLJqys2Svn4g==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.0.2.tgz", + "integrity": "sha512-SVOwLAWUQg3Ji1egtOt1UiFe4zdDpnWHyc5qctSceJ5XIu0Uc76YmGpIjZgx9YJ0XtdW0Jm507sDvjOu+HnB8w==", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.1.2" } @@ -757,10 +667,166 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.6.1.tgz", + "integrity": "sha512-0WQ0ouLejaUCRsL93GD4uft3rOmB8qoQMU05Kb8CmMtMBe7XUDLAltxVZI1q6byNqEtU7N1ZX1Vw5lIpgulLQA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.6.1.tgz", + "integrity": "sha512-1TKm25Rn20vr5aTGGZqo6E4mzPicCUD79k17EgTLAsXc1zysyi4xXKACfUbwyANEPAEIxkzwue6JZ+stYzWUTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.6.1.tgz", + "integrity": "sha512-cEXJQY/ZqMACb+nxzDeX9IPLAg7S94xouJJCNVE5BJM8JUEP4HeTF+ti3cmxWeSJo+5D+o8Tc0UAWUkfENdeyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.6.1.tgz", + "integrity": "sha512-LoSU9Xu56isrkV2jLldcKspJ7sSXmZWkAxg7sW/RfF7GS4F5/v4EiqKSMCFbZtDu2Nc1gxxFdQdKwkKS4rwxNg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.6.1.tgz", + "integrity": "sha512-EfI3hzYAy5vFNDqpXsNxXcgRDcFHUWSx5nnRSCKwXuQlI5J9dD84g2Usw81n3FLBNsGCegKGwwTVsSKK9cooSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.6.1.tgz", + "integrity": "sha512-9lhc4UZstsegbNLhH0Zu6TqvDfmhGzuCWtcTFXY10VjLLUe4Mr0Ye2L3rrtHaDd/J5+tFMEuo5LTCSCMXWfUKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.6.1.tgz", + "integrity": "sha512-FfoOK1yP5ksX3wwZ4Zk1NgyGHZyuRhf99j64I5oEmirV8EFT7+OhUZEnP+x17lcP/QHJNWGsoJwrz4PJ9fBEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.6.1.tgz", + "integrity": "sha512-DNGZvZDO5YF7jN5fX8ZqmGLjZEXIJRdJEdTFMhiyXqyXubBa0WVLDWSNlQ5JR2PNgDbEV1VQowhVRUh+74D+RA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.6.1.tgz", + "integrity": "sha512-RkJVNVRM+piYy87HrKmhbexCHg3A6Z6MU0W9GHnJwBQNBeyhCJG9KDce4SAMdicQnpURggSvtbGo9xAWOfSvIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.6.1.tgz", + "integrity": "sha512-v2FVT6xfnnmTe3W9bJXl6r5KwJglMK/iRlkKiIFfO6ysKs0rDgz7Cwwf3tjldxQUrHL9INT/1r4VA0n9L/F1vQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.6.1.tgz", + "integrity": "sha512-YEeOjxRyEjqcWphH9dyLbzgkF8wZSKAKUkldRY6dgNR5oKs2LZazqGB41cWJ4Iqqcy9/zqYgmzBkRoVz3Q9MLw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.6.1.tgz", + "integrity": "sha512-0zfTlFAIhgz8V2G8STq8toAjsYYA6eci1hnXuyOTUFnymrtJwnS6uGKiv3v5UrPZkBlamLvrLV2iiaeqCKzb0A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@tauri-apps/api": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.4.0.tgz", - "integrity": "sha512-Jd6HPoTM1PZSFIzq7FB8VmMu3qSSyo/3lSwLpoapW+lQ41CL5Dow2KryLg+gyazA/58DRWI9vu/XpEeHK4uMdw==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-1.5.1.tgz", + "integrity": "sha512-6unsZDOdlXTmauU3NhWhn+Cx0rODV+rvNvTdvolE5Kls5ybA6cqndQENDt1+FS0tF7ozCP66jwWoH6a5h90BrA==", "engines": { "node": ">= 14.6.0", "npm": ">= 6.6.0", @@ -772,9 +838,9 @@ } }, "node_modules/@tauri-apps/cli": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.6.tgz", - "integrity": "sha512-k4Y19oVCnt7WZb2TnDzLqfs7o98Jq0tUoVMv+JQSzuRDJqaVu2xMBZ8dYplEn+EccdR5SOMyzaLBJWu38TVK1A==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-1.5.7.tgz", + "integrity": "sha512-z7nXLpDAYfQqR5pYhQlWOr88DgPq1AfQyxHhGiakiVgWlaG0ikEfQxop2txrd52H0TRADG0JHR9vFrVFPv4hVQ==", "dev": true, "bin": { "tauri": "tauri.js" @@ -787,22 +853,22 @@ "url": "https://opencollective.com/tauri" }, "optionalDependencies": { - "@tauri-apps/cli-darwin-arm64": "1.5.6", - "@tauri-apps/cli-darwin-x64": "1.5.6", - "@tauri-apps/cli-linux-arm-gnueabihf": "1.5.6", - "@tauri-apps/cli-linux-arm64-gnu": "1.5.6", - "@tauri-apps/cli-linux-arm64-musl": "1.5.6", - "@tauri-apps/cli-linux-x64-gnu": "1.5.6", - "@tauri-apps/cli-linux-x64-musl": "1.5.6", - "@tauri-apps/cli-win32-arm64-msvc": "1.5.6", - "@tauri-apps/cli-win32-ia32-msvc": "1.5.6", - "@tauri-apps/cli-win32-x64-msvc": "1.5.6" + "@tauri-apps/cli-darwin-arm64": "1.5.7", + "@tauri-apps/cli-darwin-x64": "1.5.7", + "@tauri-apps/cli-linux-arm-gnueabihf": "1.5.7", + "@tauri-apps/cli-linux-arm64-gnu": "1.5.7", + "@tauri-apps/cli-linux-arm64-musl": "1.5.7", + "@tauri-apps/cli-linux-x64-gnu": "1.5.7", + "@tauri-apps/cli-linux-x64-musl": "1.5.7", + "@tauri-apps/cli-win32-arm64-msvc": "1.5.7", + "@tauri-apps/cli-win32-ia32-msvc": "1.5.7", + "@tauri-apps/cli-win32-x64-msvc": "1.5.7" } }, "node_modules/@tauri-apps/cli-darwin-arm64": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.6.tgz", - "integrity": "sha512-NNvG3XLtciCMsBahbDNUEvq184VZmOveTGOuy0So2R33b/6FDkuWaSgWZsR1mISpOuP034htQYW0VITCLelfqg==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.7.tgz", + "integrity": "sha512-eUpOUhs2IOpKaLa6RyGupP2owDLfd0q2FR/AILzryjtBtKJJRDQQvuotf+LcbEce2Nc2AHeYJIqYAsB4sw9K+g==", "cpu": [ "arm64" ], @@ -816,9 +882,9 @@ } }, "node_modules/@tauri-apps/cli-darwin-x64": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.6.tgz", - "integrity": "sha512-nkiqmtUQw3N1j4WoVjv81q6zWuZFhBLya/RNGUL94oafORloOZoSY0uTZJAoeieb3Y1YK0rCHSDl02MyV2Fi4A==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-1.5.7.tgz", + "integrity": "sha512-zfumTv1xUuR+RB1pzhRy+51tB6cm8I76g0xUBaXOfEdOJ9FqW5GW2jdnEUbpNuU65qJ1lB8LVWHKGrSWWKazew==", "cpu": [ "x64" ], @@ -832,9 +898,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.6.tgz", - "integrity": "sha512-z6SPx+axZexmWXTIVPNs4Tg7FtvdJl9EKxYN6JPjOmDZcqA13iyqWBQal2DA/GMZ1Xqo3vyJf6EoEaKaliymPQ==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-1.5.7.tgz", + "integrity": "sha512-JngWNqS06bMND9PhiPWp0e+yknJJuSozsSbo+iMzHoJNRauBZCUx+HnUcygUR66Cy6qM4eJvLXtsRG7ApxvWmg==", "cpu": [ "arm" ], @@ -848,9 +914,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-gnu": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.6.tgz", - "integrity": "sha512-QuQjMQmpsCbzBrmtQiG4uhnfAbdFx3nzm+9LtqjuZlurc12+Mj5MTgqQ3AOwQedH3f7C+KlvbqD2AdXpwTg7VA==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-1.5.7.tgz", + "integrity": "sha512-WyIYP9BskgBGq+kf4cLAyru8ArrxGH2eMYGBJvuNEuSaqBhbV0i1uUxvyWdazllZLAEz1WvSocUmSwLknr1+sQ==", "cpu": [ "arm64" ], @@ -864,9 +930,9 @@ } }, "node_modules/@tauri-apps/cli-linux-arm64-musl": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.6.tgz", - "integrity": "sha512-8j5dH3odweFeom7bRGlfzDApWVOT4jIq8/214Wl+JeiNVehouIBo9lZGeghZBH3XKFRwEvU23i7sRVjuh2s8mg==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.5.7.tgz", + "integrity": "sha512-OrDpihQP2MB0JY1a/wP9wsl9dDjFDpVEZOQxt4hU+UVGRCZQok7ghPBg4+Xpd1CkNkcCCuIeY8VxRvwLXpnIzg==", "cpu": [ "arm64" ], @@ -880,9 +946,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-gnu": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.6.tgz", - "integrity": "sha512-gbFHYHfdEGW0ffk8SigDsoXks6USpilF6wR0nqB/JbWzbzFR/sBuLVNQlJl1RKNakyJHu+lsFxGy0fcTdoX8xA==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-1.5.7.tgz", + "integrity": "sha512-4T7FAYVk76rZi8VkuLpiKUAqaSxlva86C1fHm/RtmoTKwZEV+MI3vIMoVg+AwhyWIy9PS55C75nF7+OwbnFnvQ==", "cpu": [ "x64" ], @@ -896,9 +962,9 @@ } }, "node_modules/@tauri-apps/cli-linux-x64-musl": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.6.tgz", - "integrity": "sha512-9v688ogoLkeFYQNgqiSErfhTreLUd8B3prIBSYUt+x4+5Kcw91zWvIh+VSxL1n3KCGGsM7cuXhkGPaxwlEh1ug==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-1.5.7.tgz", + "integrity": "sha512-LL9aMK601BmQjAUDcKWtt5KvAM0xXi0iJpOjoUD3LPfr5dLvBMTflVHQDAEtuZexLQyqpU09+60781PrI/FCTw==", "cpu": [ "x64" ], @@ -912,9 +978,9 @@ } }, "node_modules/@tauri-apps/cli-win32-arm64-msvc": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.6.tgz", - "integrity": "sha512-DRNDXFNZb6y5IZrw+lhTTA9l4wbzO4TNRBAlHAiXUrH+pRFZ/ZJtv5WEuAj9ocVSahVw2NaK5Yaold4NPAxHog==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-1.5.7.tgz", + "integrity": "sha512-TmAdM6GVkfir3AUFsDV2gyc25kIbJeAnwT72OnmJGAECHs/t/GLP9IkFLLVcFKsiosRf8BXhVyQ84NYkSWo14w==", "cpu": [ "arm64" ], @@ -928,9 +994,9 @@ } }, "node_modules/@tauri-apps/cli-win32-ia32-msvc": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.6.tgz", - "integrity": "sha512-oUYKNR/IZjF4fsOzRpw0xesl2lOjhsQEyWlgbpT25T83EU113Xgck9UjtI7xemNI/OPCv1tPiaM1e7/ABdg5iA==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-1.5.7.tgz", + "integrity": "sha512-bqWfxwCfLmrfZy69sEU19KHm5TFEaMb8KIekd4aRq/kyOlrjKLdZxN1PyNRP8zpJA1lTiRHzfUDfhpmnZH/skg==", "cpu": [ "ia32" ], @@ -944,9 +1010,9 @@ } }, "node_modules/@tauri-apps/cli-win32-x64-msvc": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.6.tgz", - "integrity": "sha512-RmEf1os9C8//uq2hbjXi7Vgz9ne7798ZxqemAZdUwo1pv3oLVZSz1/IvZmUHPdy2e6zSeySqWu1D0Y3QRNN+dg==", + "version": "1.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-1.5.7.tgz", + "integrity": "sha512-OxLHVBNdzyQ//xT3kwjQFnJTn/N5zta/9fofAkXfnL7vqmVn6s/RY1LDa3sxCHlRaKw0n3ShpygRbM9M8+sO9w==", "cpu": [ "x64" ], @@ -960,9 +1026,9 @@ } }, "node_modules/@types/cytoscape": { - "version": "3.19.15", - "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.15.tgz", - "integrity": "sha512-v1PNoMBzoIrOGZfuU/PFwDEPxfP4GnfVCTrZPx4M2G4EFS7BV/FLCCoVMOzdBG98MJbNBXpx1LCrs8wh0vybEw==" + "version": "3.19.16", + "resolved": "https://registry.npmjs.org/@types/cytoscape/-/cytoscape-3.19.16.tgz", + "integrity": "sha512-A3zkjaZ6cOGyqEvrVuC1YUgiRSJhDZOj8Qhd1ALH2/+YxH2za1BOmR4RWQsKYHsc+aMP/IWoqg1COuUbZ39t/g==" }, "node_modules/@types/cytoscape-dagre": { "version": "2.3.3", @@ -992,9 +1058,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.44.6", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.6.tgz", - "integrity": "sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==", + "version": "8.44.8", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.8.tgz", + "integrity": "sha512-4K8GavROwhrYl2QXDXm0Rv9epkA8GBFu0EI+XrrnnuCl7u8CWBRusX7fXJfanhZTDWSAL24gDI/UqXyUM0Injw==", "dev": true, "peer": true, "dependencies": { @@ -1003,9 +1069,9 @@ } }, "node_modules/@types/eslint-scope": { - "version": "3.7.6", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.6.tgz", - "integrity": "sha512-zfM4ipmxVKWdxtDaJ3MP3pBurDXOCoyjvlpE3u6Qzrmw4BPbfm4/ambIeTk/r/J0iq/+2/xp0Fmt+gFvXJY2PQ==", + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", "dev": true, "peer": true, "dependencies": { @@ -1014,16 +1080,15 @@ } }, "node_modules/@types/estree": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.4.tgz", - "integrity": "sha512-2JwWnHK9H+wUZNorf2Zr6ves96WHoWDJIftkcxPKsS7Djta6Zu519LarhRNljPXkpsZR2ZMwNCPeW7omW07BJw==", - "dev": true, - "peer": true + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true }, "node_modules/@types/json-schema": { - "version": "7.0.14", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.14.tgz", - "integrity": "sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/json5": { @@ -1033,32 +1098,35 @@ "dev": true }, "node_modules/@types/less": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.5.tgz", - "integrity": "sha512-OdhItUN0/Cx9+sWumdb3dxASoA0yStnZahvKcaSQmSR5qd7hZ6zhSriSQGUU3F8GkzFpIILKzut4xn9/GvhusA==", + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.6.tgz", + "integrity": "sha512-PecSzorDGdabF57OBeQO/xFbAkYWo88g4Xvnsx7LRwqLC17I7OoKtA3bQB9uXkY6UkMWCOsA8HSVpaoitscdXw==", "dev": true }, "node_modules/@types/lodash": { - "version": "4.14.201", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.201.tgz", - "integrity": "sha512-y9euML0cim1JrykNxADLfaG0FgD1g/yTHwUs/Jg9ZIU7WKj2/4IW9Lbb1WZbvck78W/lfGXFfe+u2EGfIJXdLQ==", + "version": "4.14.202", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.202.tgz", + "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==", "dev": true }, "node_modules/@types/node": { - "version": "20.3.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", - "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==", - "dev": true + "version": "20.10.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", + "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/prop-types": { - "version": "15.7.10", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.10.tgz", - "integrity": "sha512-mxSnDQxPqsZxmeShFH+uwQ4kO4gcJcGahjjMFeLbKE95IAZiiZyiEepGZjtXJ7hN/yfu0bu9xN2ajcU0JcxX6A==" + "version": "15.7.11", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", + "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, "node_modules/@types/react": { - "version": "16.14.51", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.51.tgz", - "integrity": "sha512-4T/wsDXStA5OUGTj6w2INze3ZCz22IwQiWcApgqqNRU2A6vNUIPXpNkjAMUFxx6diYPVkvz+d7gEtU7AZ+0Xqg==", + "version": "16.14.52", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.52.tgz", + "integrity": "sha512-4+ZN73hgRW3Gang3QMqWjrqPPkf+lWZYiyG4uXtUbpd+7eiBDw6Gemila6rXDd8DorADupTiIERL6Mb5BQTF2w==", "dependencies": { "@types/prop-types": "*", "@types/scheduler": "*", @@ -1066,9 +1134,9 @@ } }, "node_modules/@types/rollup-plugin-less": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@types/rollup-plugin-less/-/rollup-plugin-less-1.1.4.tgz", - "integrity": "sha512-UHH1PAI9Lyv7vUtrkM1M+ClDR4m8QIgAMBfbyHwJi3v5Xnz55mnkB3IvctE0l/7ZFIoMNTlmmd71DsnGSkVAfA==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/rollup-plugin-less/-/rollup-plugin-less-1.1.5.tgz", + "integrity": "sha512-m4BDbs04cztW16Hc1+AoUz8Iox58Vz2PUuPKccc3vJ2H1oIPRk3IPT2llBPPcSfsOyObATYs6PHduWaaw/24Lw==", "dev": true, "dependencies": { "@types/less": "*", @@ -1076,58 +1144,39 @@ "rollup": "^0.63.4" } }, - "node_modules/@types/rollup-plugin-less/node_modules/@types/estree": { - "version": "0.0.39", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", - "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", - "dev": true - }, - "node_modules/@types/rollup-plugin-less/node_modules/rollup": { - "version": "0.63.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.63.5.tgz", - "integrity": "sha512-dFf8LpUNzIj3oE0vCvobX6rqOzHzLBoblyFp+3znPbjiSmSvOoK2kMKx+Fv9jYduG1rvcCfCveSgEaQHjWRF6g==", - "dev": true, - "dependencies": { - "@types/estree": "0.0.39", - "@types/node": "*" - }, - "bin": { - "rollup": "bin/rollup" - } - }, "node_modules/@types/scheduler": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.6.tgz", - "integrity": "sha512-Vlktnchmkylvc9SnwwwozTv04L/e1NykF5vgoQ0XTmI8DD+wxfjQuHuvHS3p0r2jz2x2ghPs2h1FVeDirIteWA==" + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", + "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==" }, "node_modules/@types/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/trusted-types": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.5.tgz", - "integrity": "sha512-I3pkr8j/6tmQtKV/ZzHtuaqYSQvyjGRKH4go60Rr0IDLlFxuRT5V32uvB1mecM5G1EVAUyF/4r4QZ1GHgz+mxA==" + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" }, "node_modules/@types/uikit": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/@types/uikit/-/uikit-3.14.0.tgz", - "integrity": "sha512-1d3UY6h6PVfkvOxINerFUVvlseP9hk4lfdtmWoIEbtCgQSYyrX8gd7VbibLa7K+LkD8DpyYm0+yA/7OO7FmXDQ==", + "version": "3.14.4", + "resolved": "https://registry.npmjs.org/@types/uikit/-/uikit-3.14.4.tgz", + "integrity": "sha512-h87ursZGhr7sb6nudFTKZhKOyi/zsbK4NaUHLT3tfNf7DRzRiiF4/Vup+oZ7L4NWiGqT92fUNKW3qdJ2TJLYEg==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.9.1.tgz", - "integrity": "sha512-w0tiiRc9I4S5XSXXrMHOWgHgxbrBn1Ro+PmiYhSg2ZVdxrAJtQgzU5o2m1BfP6UOn7Vxcc6152vFjQfmZR4xEg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.2.tgz", + "integrity": "sha512-3+9OGAWHhk4O1LlcwLBONbdXsAhLjyCFogJY/cWy2lxdVJ2JrcTF2pTGMaLl2AE7U1l31n8Py4a8bx5DLf/0dQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/type-utils": "6.9.1", - "@typescript-eslint/utils": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/type-utils": "6.13.2", + "@typescript-eslint/utils": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1152,103 +1201,42 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "node_modules/@typescript-eslint/parser": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.2.tgz", + "integrity": "sha512-MUkcC+7Wt/QOGeVlM8aGGJZy1XV5YKjTpq9jK6r6/iLsGXhBVaGP5N0UYvFsu9BFlSpwY9kMretzdBH01rkRXg==", "dev": true, "dependencies": { - "ms": "2.1.2" + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", + "debug": "^4.3.4" }, "engines": { - "node": ">=6.0" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.2.tgz", + "integrity": "sha512-CXQA0xo7z6x13FeDYCgBkjWzNqzBn8RXaE3QVQVIUm74fWJLkJkaHmHdKStrxQllGh6Q4eUGyNpMe0b1hMkXFA==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.9.1.tgz", - "integrity": "sha512-C7AK2wn43GSaCUZ9do6Ksgi2g3mwFkMO3Cis96kzmgudoVaKyt62yNzJOktP0HDLb/iO2O0n2lBOzJgr6Q/cyg==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.9.1.tgz", - "integrity": "sha512-38IxvKB6NAne3g/+MyXMs2Cda/Sz+CEpmm+KLGEM8hx/CvnSRuw51i8ukfwB/B/sESdeTGet1NH1Wj7I0YXswg==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1" + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1259,13 +1247,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.9.1.tgz", - "integrity": "sha512-eh2oHaUKCK58qIeYp19F5V5TbpM52680sB4zNSz29VBQPTWIlE/hCj5P5B1AChxECe/fmZlspAWFuRniep1Skg==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.2.tgz", + "integrity": "sha512-Qr6ssS1GFongzH2qfnWKkAQmMUyZSyOr0W54nZNU1MDfo+U4Mv3XveeLZzadc/yq8iYhQZHYT+eoXJqnACM1tw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.9.1", - "@typescript-eslint/utils": "6.9.1", + "@typescript-eslint/typescript-estree": "6.13.2", + "@typescript-eslint/utils": "6.13.2", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1285,33 +1273,10 @@ } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/@typescript-eslint/types": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.9.1.tgz", - "integrity": "sha512-BUGslGOb14zUHOUmDB2FfT6SI1CcZEJYfF3qFwBeUrU6srJfzANonwRYHDpLBuzbq3HaoF2XL2hcr01c8f8OaQ==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.2.tgz", + "integrity": "sha512-7sxbQ+EMRubQc3wTfTsycgYpSujyVbI1xw+3UMRUcrhSy+pN09y/lWzeKDbvhoqcRbHdc+APLs/PWYi/cisLPg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1322,13 +1287,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.9.1.tgz", - "integrity": "sha512-U+mUylTHfcqeO7mLWVQ5W/tMLXqVpRv61wm9ZtfE5egz7gtnmqVIw9ryh0mgIlkKk9rZLY3UHygsBSdB9/ftyw==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.2.tgz", + "integrity": "sha512-SuD8YLQv6WHnOEtKv8D6HZUzOub855cfPnPMKvdM/Bh1plv1f7Q/0iFUDLKKlxHcEstQnaUU4QZskgQq74t+3w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/visitor-keys": "6.9.1", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/visitor-keys": "6.13.2", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1348,56 +1313,18 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.9.1.tgz", - "integrity": "sha512-L1T0A5nFdQrMVunpZgzqPL6y2wVreSyHhKGZryS6jrEN7bD9NplVAyMryUhXsQ4TWLnZmxc2ekar/lSGIlprCA==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.2.tgz", + "integrity": "sha512-b9Ptq4eAZUym4idijCRzl61oPCwwREcfDI8xGk751Vhzig5fFZR9CyzDz4Sp/nxSLBYxUPyh4QdIDqWykFhNmQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.9.1", - "@typescript-eslint/types": "6.9.1", - "@typescript-eslint/typescript-estree": "6.9.1", + "@typescript-eslint/scope-manager": "6.13.2", + "@typescript-eslint/types": "6.13.2", + "@typescript-eslint/typescript-estree": "6.13.2", "semver": "^7.5.4" }, "engines": { @@ -1411,28 +1338,13 @@ "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.9.1.tgz", - "integrity": "sha512-MUaPUe/QRLEffARsmNfmpghuQkW436DvESW+h+M52w0coICHRfD6Np9/K6PdACwnrq1HmuLl+cSPZaJmeVPkSw==", + "version": "6.13.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.2.tgz", + "integrity": "sha512-OGznFs0eAQXJsp+xSd6k/O1UbFi/K/L7WjqeRoFE7vadjAF9y0uppXhYNQNEqygjou782maGClOoZwPqF0Drlw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.9.1", + "@typescript-eslint/types": "6.13.2", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1450,19 +1362,18 @@ "dev": true }, "node_modules/@vituum/vite-plugin-posthtml": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@vituum/vite-plugin-posthtml/-/vite-plugin-posthtml-1.0.0.tgz", - "integrity": "sha512-6OMnQKfrwzft03Q+1blNacZPVJJi2qTm4aRh6qZjeuytMxiH+yPXK4FO656OZn7gv8IA0KrkOWAQvs/Le7p4ww==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vituum/vite-plugin-posthtml/-/vite-plugin-posthtml-1.1.0.tgz", + "integrity": "sha512-k5lzWzuKBN8z2iYSg9tC/boZRdeySR/GBxfbg2bsDtJW4TsgYml7XUPo9v1yf8oRRPXMse10ka0uX1qhHDVUFQ==", "dev": true, "dependencies": { - "posthtml": "^0.16.6", - "posthtml-extend": "^0.6.5", - "posthtml-include": "^1.7.4", - "vituum": "^1.0.0" + "posthtml": "^0.16", + "posthtml-extend": "^0.6", + "posthtml-include": "^1.7", + "vituum": "^1.1" }, "engines": { - "node": ">=16.0.0", - "npm": ">=9.0.0" + "node": "^18.0.0 || >=20.0.0" } }, "node_modules/@webassemblyjs/ast": { @@ -1885,12 +1796,13 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { @@ -1906,9 +1818,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz", - "integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==", + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", + "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", "dev": true, "funding": [ { @@ -1926,9 +1838,9 @@ ], "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001541", - "electron-to-chromium": "^1.4.535", - "node-releases": "^2.0.13", + "caniuse-lite": "^1.0.30001565", + "electron-to-chromium": "^1.4.601", + "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, "bin": { @@ -1945,6 +1857,18 @@ "dev": true, "peer": true }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/builtins": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", @@ -1954,21 +1878,6 @@ "semver": "^7.0.0" } }, - "node_modules/builtins/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/call-bind": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", @@ -1993,9 +1902,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001561", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001561.tgz", - "integrity": "sha512-NTt0DNoKe958Q0BE0j0c1V9jbUzhBxHIEJy7asmGrpE0yG63KTV7PLHPnK2E1O9RsQrQ081I3NLuXGS6zht3cw==", + "version": "1.0.30001566", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001566.tgz", + "integrity": "sha512-ggIhCsTxmITBAMmK8yZjEhCO5/47jKXPu6Dha/wuCS4JePVL+3uiDEBuhu2aIoT+bqTOR8L76Ip1ARL9xYsEJA==", "dev": true, "funding": [ { @@ -2056,6 +1965,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -2197,12 +2118,20 @@ "integrity": "sha512-UNG2qao2GaHrnVQtarekOjUt1BvU6vt1OUnYSp+3BuFNs+iKjiBmIz2cabXtP2ZjthM8n3ox1lRKrUxp4SKG9A==" }, "node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/deep-is": { @@ -2340,9 +2269,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.576", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.576.tgz", - "integrity": "sha512-yXsZyXJfAqzWk1WKryr0Wl0MN2D47xodPvEEwlVePBnhU5E7raevLQR+E6b9JAD3GfL/7MbAL9ZtWQQPcLx7wA==", + "version": "1.4.605", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.605.tgz", + "integrity": "sha512-V52j+P5z6cdRqTjPR/bYNxx7ETCHIkm5VIGuyCy3CMrfSnbEpIlLnk5oHmZo7gYvDfh2TfHeanB6rawyQ23ktg==", "dev": true, "peer": true }, @@ -2439,9 +2368,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.4.1.tgz", + "integrity": "sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==", "dev": true, "peer": true }, @@ -2545,15 +2474,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -2599,6 +2528,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-compat-utils": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz", + "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, "node_modules/eslint-config-standard": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz", @@ -2657,6 +2598,15 @@ "resolve": "^1.22.4" } }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-module-utils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", @@ -2674,14 +2624,24 @@ } } }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-es-x": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.3.0.tgz", - "integrity": "sha512-W9zIs+k00I/I13+Bdkl/zG1MEO07G97XjUSQuH117w620SJ6bHtLUmoMvkGA2oYnI/gNdr+G7BONLyYnFaLLEQ==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", + "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.1.2", - "@eslint-community/regexpp": "^4.6.0" + "@eslint-community/regexpp": "^4.6.0", + "eslint-compat-utils": "^0.1.2" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -2724,14 +2684,13 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" } }, - "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "ms": "^2.1.1" } }, "node_modules/eslint-plugin-import/node_modules/doctrine": { @@ -2746,18 +2705,6 @@ "node": ">=0.10.0" } }, - "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-plugin-import/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -2785,9 +2732,9 @@ } }, "node_modules/eslint-plugin-n": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.2.0.tgz", - "integrity": "sha512-AQER2jEyQOt1LG6JkGJCCIFotzmlcCZFur2wdKrp1JX2cNotC7Ae0BcD/4lLv3lUAArM9uNS8z/fsvXTd0L71g==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.3.1.tgz", + "integrity": "sha512-w46eDIkxQ2FaTHcey7G40eD+FhTXOdKudDXPUO2n9WNcslze/i/HT2qJ3GXjHngYSGDISIgPNhwGtgoix4zeOw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", @@ -2795,6 +2742,7 @@ "eslint-plugin-es-x": "^7.1.0", "get-tsconfig": "^4.7.0", "ignore": "^5.2.4", + "is-builtin-module": "^3.2.1", "is-core-module": "^2.12.1", "minimatch": "^3.1.2", "resolve": "^1.22.2", @@ -2810,69 +2758,32 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-plugin-n/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/eslint-plugin-promise": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", + "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, - "node_modules/eslint-plugin-n/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "dependencies": { - "brace-expansion": "^1.1.7" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-plugin-promise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz", - "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -2887,63 +2798,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3020,9 +2874,9 @@ "dev": true }, "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -3035,6 +2889,18 @@ "node": ">=8.6.0" } }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3103,9 +2969,9 @@ } }, "node_modules/flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { "flatted": "^3.2.9", @@ -3113,7 +2979,7 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -3138,9 +3004,9 @@ "dev": true }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, "optional": true, @@ -3251,15 +3117,15 @@ } }, "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "is-glob": "^4.0.3" }, "engines": { - "node": ">= 6" + "node": ">=10.13.0" } }, "node_modules/glob-to-regexp": { @@ -3269,28 +3135,6 @@ "dev": true, "peer": true }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { "version": "13.23.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", @@ -3492,9 +3336,9 @@ } }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz", + "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==", "dev": true, "engines": { "node": ">= 4" @@ -3622,6 +3466,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -3996,29 +3855,29 @@ } }, "node_modules/lit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.0.2.tgz", - "integrity": "sha512-ZoVUPGgXOQocP4OvxehEOBmC4rWB4cRYDPaz7aFmH8DFytsCi/NeACbr4C6vNPGDEC07BrhUos7uVNayDKLQ2Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.1.0.tgz", + "integrity": "sha512-rzo/hmUqX8zmOdamDAeydfjsGXbbdtAFqMhmocnh2j9aDYqbu0fjXygjCa0T99Od9VQ/2itwaGrjZz/ZELVl7w==", "dependencies": { "@lit/reactive-element": "^2.0.0", "lit-element": "^4.0.0", - "lit-html": "^3.0.0" + "lit-html": "^3.1.0" } }, "node_modules/lit-element": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.1.tgz", - "integrity": "sha512-OxRMJem4HKZt0320HplLkBPoi4KHiEHoPHKd8Lzf07ZQVAOKIjZ32yPLRKRDEolFU1RgrQBfSHQMoxKZ72V3Kw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.0.2.tgz", + "integrity": "sha512-/W6WQZUa5VEXwC7H9tbtDMdSs9aWil3Ou8hU6z2cOKWbsm/tXPAcsoaHVEtrDo0zcOIE5GF6QgU55tlGL2Nihg==", "dependencies": { "@lit-labs/ssr-dom-shim": "^1.1.2", "@lit/reactive-element": "^2.0.0", - "lit-html": "^3.0.0" + "lit-html": "^3.1.0" } }, "node_modules/lit-html": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.0.2.tgz", - "integrity": "sha512-Q1A5lHza3bnmxoWJn6yS6vQZQdExl4fghk8W1G+jnAEdoFNYo5oeBBb/Ol7zSEdKd3TR7+r0zsJQyuWEVguiyQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.1.0.tgz", + "integrity": "sha512-FwAjq3iNsaO6SOZXEIpeROlJLUlrbyMkn4iuv4f4u1H40Jw8wkeR/OUXZUHUoiYabGk8Y4Y0F/rgq+R4MrOLmA==", "dependencies": { "@types/trusted-types": "^2.0.2" } @@ -4106,6 +3965,16 @@ "node": ">=6" } }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -4172,18 +4041,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", - "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -4196,15 +4062,15 @@ } }, "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, "node_modules/nanoid": { - "version": "3.3.6", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", - "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, "funding": [ { @@ -4243,6 +4109,16 @@ "node": ">= 4.4.x" } }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -4251,9 +4127,9 @@ "peer": true }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true, "peer": true }, @@ -4293,13 +4169,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -4519,9 +4395,9 @@ } }, "node_modules/postcss": { - "version": "8.4.31", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", - "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "funding": [ { @@ -4538,7 +4414,7 @@ } ], "dependencies": { - "nanoid": "^3.3.6", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -4560,9 +4436,9 @@ } }, "node_modules/posthtml-expressions": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/posthtml-expressions/-/posthtml-expressions-1.11.1.tgz", - "integrity": "sha512-2zA5SRM7quupTGa422xH72T0n3tF5quZeZ66czmMa/4QAj8HFzCTlN5l42wWVjLCxj7XOmMmQJEeK0+p3AgH+w==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/posthtml-expressions/-/posthtml-expressions-1.11.3.tgz", + "integrity": "sha512-S75Sp3UUoOQJV2vtubJOuv6u/R9hqJE62rgihOTxKCTog68A5OIRNBoIWLKMVD8VJ8l0vQaIEKzWZ4Vm3ht5hw==", "dev": true, "dependencies": { "fclone": "^1.0.11", @@ -4831,19 +4707,16 @@ } }, "node_modules/rollup": { - "version": "3.29.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", - "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", + "version": "0.63.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.63.5.tgz", + "integrity": "sha512-dFf8LpUNzIj3oE0vCvobX6rqOzHzLBoblyFp+3znPbjiSmSvOoK2kMKx+Fv9jYduG1rvcCfCveSgEaQHjWRF6g==", "dev": true, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=14.18.0", - "npm": ">=8.0.0" + "dependencies": { + "@types/estree": "0.0.39", + "@types/node": "*" }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "bin": { + "rollup": "bin/rollup" } }, "node_modules/run-parallel": { @@ -4930,9 +4803,9 @@ "optional": true }, "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", "dev": true, "optional": true }, @@ -4956,13 +4829,18 @@ } }, "node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, - "optional": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { - "semver": "bin/semver" + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/serialize-javascript": { @@ -5190,9 +5068,9 @@ } }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.25.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.25.0.tgz", + "integrity": "sha512-we0I9SIsfvNUMP77zC9HG+MylwYYsGFSBG8qm+13oud2Yh+O104y614FRbyjpxys16jZwot72Fpi827YvGzuqg==", "dev": true, "peer": true, "dependencies": { @@ -5286,9 +5164,9 @@ } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, "node_modules/type-check": { @@ -5394,9 +5272,9 @@ } }, "node_modules/uikit": { - "version": "3.17.8", - "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.17.8.tgz", - "integrity": "sha512-zs7oFYlW2JFVZ1Rz70qnrHRfWqRvKIzSobVhQDVeIPOOZXKiyad/r1oveiWvHQSlb7wKaW6T8Z6/FefBi3Sm5w==" + "version": "3.17.11", + "resolved": "https://registry.npmjs.org/uikit/-/uikit-3.17.11.tgz", + "integrity": "sha512-B5DL8pQnjNWKsiuoIyLQoV7ODr/UH8Qcyt0hhYXdvNIaGnwezIBLV7asnid2EW/rt8e5d44J4xgJqPBHWa1VWA==" }, "node_modules/uikit-icons": { "version": "0.5.0", @@ -5422,6 +5300,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -5469,9 +5353,9 @@ "dev": true }, "node_modules/vite": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.0.tgz", - "integrity": "sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.1.tgz", + "integrity": "sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==", "dev": true, "dependencies": { "esbuild": "^0.18.10", @@ -5523,39 +5407,550 @@ } } }, - "node_modules/vituum": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vituum/-/vituum-1.0.0.tgz", - "integrity": "sha512-Xs2LrGx1RUSOWSgueo6d1OL3rjmi/eEYkzXa8Wqnke9+zSbDxgKQs/Q/58nTNJ7q1eNOmaw8c/DIXwEn87Ctow==", + "node_modules/vite/node_modules/rollup": { + "version": "3.29.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.4.tgz", + "integrity": "sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==", "dev": true, - "dependencies": { - "chokidar": "^3.5", - "fast-glob": "^3.2", - "lodash": "^4.17", - "minimatch": "^9.0", - "vite": "^4.3" + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=16.0.0", - "npm": ">=9.0.0" + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "node_modules/watchpack": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", - "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "node_modules/vituum": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vituum/-/vituum-1.1.0.tgz", + "integrity": "sha512-MinuWgpNvkkXz7RAyj6SqDKL4yIok1NM8WnodBQOP1wnDWHCbE6RSSmg+5dYW2V9uskDJJyVV3YS0z/0eDu2iA==", "dev": true, - "peer": true, "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" + "chokidar": "^3.5", + "fast-glob": "^3.3", + "lodash": "^4.17", + "minimatch": "^9.0", + "vite": "^5.0" }, "engines": { - "node": ">=10.13.0" + "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/webpack": { - "version": "5.89.0", + "node_modules/vituum/node_modules/@esbuild/android-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", + "integrity": "sha512-31E2lxlGM1KEfivQl8Yf5aYU/mflz9g06H6S15ITUFQueMFtFjESRMoDSkvMo8thYvLBax+VKTPlpnx+sPicOA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/android-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.8.tgz", + "integrity": "sha512-B8JbS61bEunhfx8kasogFENgQfr/dIp+ggYXwTqdbMAgGDhRa3AaPpQMuQU0rNxDLECj6FhDzk1cF9WHMVwrtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/android-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.8.tgz", + "integrity": "sha512-rdqqYfRIn4jWOp+lzQttYMa2Xar3OK9Yt2fhOhzFXqg0rVWEfSclJvZq5fZslnz6ypHvVf3CT7qyf0A5pM682A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.8.tgz", + "integrity": "sha512-RQw9DemMbIq35Bprbboyf8SmOr4UXsRVxJ97LgB55VKKeJOOdvsIPy0nFyF2l8U+h4PtBx/1kRf0BelOYCiQcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/darwin-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.8.tgz", + "integrity": "sha512-3sur80OT9YdeZwIVgERAysAbwncom7b4bCI2XKLjMfPymTud7e/oY4y+ci1XVp5TfQp/bppn7xLw1n/oSQY3/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.8.tgz", + "integrity": "sha512-WAnPJSDattvS/XtPCTj1tPoTxERjcTpH6HsMr6ujTT+X6rylVe8ggxk8pVxzf5U1wh5sPODpawNicF5ta/9Tmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.8.tgz", + "integrity": "sha512-ICvZyOplIjmmhjd6mxi+zxSdpPTKFfyPPQMQTK/w+8eNK6WV01AjIztJALDtwNNfFhfZLux0tZLC+U9nSyA5Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-arm": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.8.tgz", + "integrity": "sha512-H4vmI5PYqSvosPaTJuEppU9oz1dq2A7Mr2vyg5TF9Ga+3+MGgBdGzcyBP7qK9MrwFQZlvNyJrvz6GuCaj3OukQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.8.tgz", + "integrity": "sha512-z1zMZivxDLHWnyGOctT9JP70h0beY54xDDDJt4VpTX+iwA77IFsE1vCXWmprajJGa+ZYSqkSbRQ4eyLCpCmiCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.8.tgz", + "integrity": "sha512-1a8suQiFJmZz1khm/rDglOc8lavtzEMRo0v6WhPgxkrjcU0LkHj+TwBrALwoz/OtMExvsqbbMI0ChyelKabSvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-loong64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.8.tgz", + "integrity": "sha512-fHZWS2JJxnXt1uYJsDv9+b60WCc2RlvVAy1F76qOLtXRO+H4mjt3Tr6MJ5l7Q78X8KgCFudnTuiQRBhULUyBKQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.8.tgz", + "integrity": "sha512-Wy/z0EL5qZYLX66dVnEg9riiwls5IYnziwuju2oUiuxVc+/edvqXa04qNtbrs0Ukatg5HEzqT94Zs7J207dN5Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.8.tgz", + "integrity": "sha512-ETaW6245wK23YIEufhMQ3HSeHO7NgsLx8gygBVldRHKhOlD1oNeNy/P67mIh1zPn2Hr2HLieQrt6tWrVwuqrxg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.8.tgz", + "integrity": "sha512-T2DRQk55SgoleTP+DtPlMrxi/5r9AeFgkhkZ/B0ap99zmxtxdOixOMI570VjdRCs9pE4Wdkz7JYrsPvsl7eESg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-s390x": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.8.tgz", + "integrity": "sha512-NPxbdmmo3Bk7mbNeHmcCd7R7fptJaczPYBaELk6NcXxy7HLNyWwCyDJ/Xx+/YcNH7Im5dHdx9gZ5xIwyliQCbg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/linux-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.8.tgz", + "integrity": "sha512-lytMAVOM3b1gPypL2TRmZ5rnXl7+6IIk8uB3eLsV1JwcizuolblXRrc5ShPrO9ls/b+RTp+E6gbsuLWHWi2zGg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.8.tgz", + "integrity": "sha512-hvWVo2VsXz/8NVt1UhLzxwAfo5sioj92uo0bCfLibB0xlOmimU/DeAEsQILlBQvkhrGjamP0/el5HU76HAitGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.8.tgz", + "integrity": "sha512-/7Y7u77rdvmGTxR83PgaSvSBJCC2L3Kb1M/+dmSIvRvQPXXCuC97QAwMugBNG0yGcbEGfFBH7ojPzAOxfGNkwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/sunos-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.8.tgz", + "integrity": "sha512-9Lc4s7Oi98GqFA4HzA/W2JHIYfnXbUYgekUP/Sm4BG9sfLjyv6GKKHKKVs83SMicBF2JwAX6A1PuOLMqpD001w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/win32-arm64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.8.tgz", + "integrity": "sha512-rq6WzBGjSzihI9deW3fC2Gqiak68+b7qo5/3kmB6Gvbh/NYPA0sJhrnp7wgV4bNwjqM+R2AApXGxMO7ZoGhIJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/win32-ia32": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.8.tgz", + "integrity": "sha512-AIAbverbg5jMvJznYiGhrd3sumfwWs8572mIJL5NQjJa06P8KfCPWZQ0NwZbPQnbQi9OWSZhFVSUWjjIrn4hSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/@esbuild/win32-x64": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.8.tgz", + "integrity": "sha512-bfZ0cQ1uZs2PqpulNL5j/3w+GDhP36k1K5c38QdQg+Swy51jFZWWeIkteNsufkQxp986wnqRRsb/bHbY1WQ7TA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vituum/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/vituum/node_modules/esbuild": { + "version": "0.19.8", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.8.tgz", + "integrity": "sha512-l7iffQpT2OrZfH2rXIp7/FkmaeZM0vxbxN9KfiCwGYuZqzMg/JdvX26R31Zxn/Pxvsrg3Y9N6XTcnknqDyyv4w==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.19.8", + "@esbuild/android-arm64": "0.19.8", + "@esbuild/android-x64": "0.19.8", + "@esbuild/darwin-arm64": "0.19.8", + "@esbuild/darwin-x64": "0.19.8", + "@esbuild/freebsd-arm64": "0.19.8", + "@esbuild/freebsd-x64": "0.19.8", + "@esbuild/linux-arm": "0.19.8", + "@esbuild/linux-arm64": "0.19.8", + "@esbuild/linux-ia32": "0.19.8", + "@esbuild/linux-loong64": "0.19.8", + "@esbuild/linux-mips64el": "0.19.8", + "@esbuild/linux-ppc64": "0.19.8", + "@esbuild/linux-riscv64": "0.19.8", + "@esbuild/linux-s390x": "0.19.8", + "@esbuild/linux-x64": "0.19.8", + "@esbuild/netbsd-x64": "0.19.8", + "@esbuild/openbsd-x64": "0.19.8", + "@esbuild/sunos-x64": "0.19.8", + "@esbuild/win32-arm64": "0.19.8", + "@esbuild/win32-ia32": "0.19.8", + "@esbuild/win32-x64": "0.19.8" + } + }, + "node_modules/vituum/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vituum/node_modules/rollup": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.6.1.tgz", + "integrity": "sha512-jZHaZotEHQaHLgKr8JnQiDT1rmatjgKlMekyksz+yk9jt/8z9quNjnKNRoaM0wd9DC2QKXjmWWuDYtM3jfF8pQ==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.6.1", + "@rollup/rollup-android-arm64": "4.6.1", + "@rollup/rollup-darwin-arm64": "4.6.1", + "@rollup/rollup-darwin-x64": "4.6.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.6.1", + "@rollup/rollup-linux-arm64-gnu": "4.6.1", + "@rollup/rollup-linux-arm64-musl": "4.6.1", + "@rollup/rollup-linux-x64-gnu": "4.6.1", + "@rollup/rollup-linux-x64-musl": "4.6.1", + "@rollup/rollup-win32-arm64-msvc": "4.6.1", + "@rollup/rollup-win32-ia32-msvc": "4.6.1", + "@rollup/rollup-win32-x64-msvc": "4.6.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/vituum/node_modules/vite": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.6.tgz", + "integrity": "sha512-MD3joyAEBtV7QZPl2JVVUai6zHms3YOmLR+BpMzLlX2Yzjfcc4gTgNi09d/Rua3F4EtC8zdwPU8eQYyib4vVMQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.19.3", + "postcss": "^8.4.32", + "rollup": "^4.2.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "peer": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.89.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.89.0.tgz", "integrity": "sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==", "dev": true, @@ -5612,6 +6007,13 @@ "node": ">=10.13.0" } }, + "node_modules/webpack/node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "peer": true + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fe9fcea..b842df8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1659,6 +1659,17 @@ dependencies = [ "objc_exception", ] +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + [[package]] name = "objc_exception" version = "0.1.2" @@ -2138,6 +2149,30 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2617,6 +2652,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "rfd", "semver", "serde", "serde_json", @@ -3156,6 +3192,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -3185,6 +3233,16 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "0.18.2" @@ -3301,6 +3359,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + [[package]] name = "windows" version = "0.39.0" @@ -3407,6 +3478,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + [[package]] name = "windows_aarch64_msvc" version = "0.39.0" @@ -3425,6 +3502,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + [[package]] name = "windows_i686_gnu" version = "0.39.0" @@ -3443,6 +3526,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + [[package]] name = "windows_i686_msvc" version = "0.39.0" @@ -3461,6 +3550,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + [[package]] name = "windows_x86_64_gnu" version = "0.39.0" @@ -3491,6 +3586,12 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + [[package]] name = "windows_x86_64_msvc" version = "0.39.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 105efa3..47e79e0 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,7 +13,7 @@ edition = "2021" tauri-build = { version = "1.4", features = [] } [dependencies] -tauri = { version = "1.4", features = ["shell-open"] } +tauri = { version = "1.4", features = [ "dialog-all", "shell-open"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src-tauri/src/app/_aeon_app.rs b/src-tauri/src/app/_aeon_app.rs index a23689b..7a696be 100644 --- a/src-tauri/src/app/_aeon_app.rs +++ b/src-tauri/src/app/_aeon_app.rs @@ -2,6 +2,8 @@ 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, diff --git a/src-tauri/src/app/_aeon_error.rs b/src-tauri/src/app/_aeon_error.rs index 16aaa88..178054b 100644 --- a/src-tauri/src/app/_aeon_error.rs +++ b/src-tauri/src/app/_aeon_error.rs @@ -30,6 +30,11 @@ impl AeonError { } } + /// The same as [Self::new], but returns [DynError] instead. + pub fn dyn_new(description: impl Into) -> 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. /// diff --git a/src-tauri/src/app/event.rs b/src-tauri/src/app/event.rs index 7b4dd0f..51a45a3 100644 --- a/src-tauri/src/app/event.rs +++ b/src-tauri/src/app/event.rs @@ -1,70 +1,88 @@ /// An [Event] object holds information about one particular state update. /// -/// It consists of a segmented `path` and an optional `payload`. Under normal circumstances, -/// the payload is expected to be either a single value (e.g. a string encoding of a single -/// integer), or a JSON object (encoding multiple values). +/// 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 full_path: Vec, + pub path: Vec, pub payload: Option, } -/// A [UserAction] is a type of event that (in some sense) originates in the GUI. +/// 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 some action by the user to which the app should respond. +/// 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 event: Event, + pub events: Vec, } -/// A [StateChange] is a type of event that (in some sense) originates in the backend. +/// A [StateChange] is internally the same as [UserAction], but it represents a collection +/// of value updates that happened on the backend. /// -/// Typically, a state change is triggered once a [UserAction] is handled by the application -/// such that the state of the application changed. However, it can be also triggered -/// automatically, e.g. by a long-running computation. +/// 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 event: Event, + pub events: Vec, } impl Event { pub fn build(path: &[&str], payload: Option<&str>) -> Event { Event { - full_path: path.iter().map(|it| it.to_string()).collect::>(), + path: path.iter().map(|it| it.to_string()).collect::>(), payload: payload.map(|it| it.to_string()), } } - /// An estimated size of the `payload` in this event, or zero if there is no `payload`. - /// - /// Note that this is not guaranteed to be the exact size. Rather, it is only an estimate - /// to determine how costly it is to store a particular collection of events in memory. - pub fn payload_size(&self) -> usize { - self.payload.as_ref().map(|it| it.len()).unwrap_or(0) + /// 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::(); + 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::() } } impl From for StateChange { fn from(value: UserAction) -> Self { - StateChange { event: value.event } + StateChange { + events: value.events, + } } } impl From for UserAction { fn from(value: StateChange) -> Self { - UserAction { event: value.event } + UserAction { + events: value.events, + } } } impl From for StateChange { fn from(value: Event) -> Self { - StateChange { event: value } + StateChange { + events: vec![value], + } } } impl From for UserAction { fn from(value: Event) -> Self { - UserAction { event: value } + UserAction { + events: vec![value], + } } } diff --git a/src-tauri/src/app/mod.rs b/src-tauri/src/app/mod.rs index bb82aa2..90a8e37 100644 --- a/src-tauri/src/app/mod.rs +++ b/src-tauri/src/app/mod.rs @@ -2,16 +2,20 @@ use std::error::Error; mod _aeon_app; mod _aeon_error; -mod _undo_stack; pub mod event; pub mod state; pub use _aeon_app::AeonApp; pub use _aeon_error::AeonError; -pub use _undo_stack::UndoStack; -pub const EVENT_USER_ACTION: &str = "aeon-user-action"; -pub const EVENT_STATE_CHANGE: &str = "aeon-state-change"; +/// 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]. /// diff --git a/src-tauri/src/app/state/_consumed.rs b/src-tauri/src/app/state/_consumed.rs index 210bd9d..d332fc9 100644 --- a/src-tauri/src/app/state/_consumed.rs +++ b/src-tauri/src/app/state/_consumed.rs @@ -1,30 +1,49 @@ -use crate::app::event::{StateChange, UserAction}; +use crate::app::event::Event; use crate::app::DynError; -/// A [Consumed] object describes possible outcomes of trying to consume a [UserAction] -/// by the [AppState]. +/// A [Consumed] object describes possible outcomes of trying to consume an [Event] +/// by some session state object. #[derive(Debug)] pub enum Consumed { - /// Action was successfully consumed, resulting in the given [StateChange]. + /// Event was successfully consumed, resulting the provided `state_change` [Event]. /// - /// Furthermore, the action is reversible, meaning it can be saved to the back-stack - /// using the provided pair of `(perform, reverse)` actions. Note that the `perform` - /// action can be a copy of the original user action, but it can be also a different event, - /// for example if the state object performed some automatic value normalization. + /// 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. /// - /// Note that this does not *guarantee* that the action will be saved to the back stack. - /// If the payloads for the `(perform, reverse)` actions are too large, [AppState] can - /// still refuse to save it, in which case the stack will be simply reset to empty. - Reversible(StateChange, (UserAction, UserAction)), + /// 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 [StateChange]. + /// Action was successfully consumed, resulting in the given `state_change` [Event]. /// - /// However, the action is irreversible, meaning the back-stack needs to be reset. - Irreversible(StateChange), + /// 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 at its intended path and should be re-emitted as - /// the freshly constructed [UserAction]. - Restart(UserAction), + /// 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), /// The action was consumed, but the provided user input is invalid and cannot be applied. /// diff --git a/src-tauri/src/app/state/_state_app.rs b/src-tauri/src/app/state/_state_app.rs index 5d2c115..19f8a0e 100644 --- a/src-tauri/src/app/state/_state_app.rs +++ b/src-tauri/src/app/state/_state_app.rs @@ -1,13 +1,12 @@ use crate::app::event::UserAction; -use crate::app::state::{Consumed, DynSessionState, MapState, SessionState}; -use crate::app::{AeonApp, AeonError, DynError, UndoStack, EVENT_STATE_CHANGE}; -use crate::debug; -use std::ops::DerefMut; +use crate::app::state::DynSession; +use crate::app::{AeonApp, AeonError, DynError, AEON_VALUE}; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; use std::sync::Mutex; -use tauri::Manager; +use tauri::{Manager, Window}; -/// [AppState] is a special wrapper around [MapState] which implements the "top level" -/// container for the whole application state. +/// [AppState] implements mapping between session IDs and session objects. /// /// Specifically, it ensures: /// - Synchronization by locking the app state. @@ -16,190 +15,96 @@ use tauri::Manager; /// /// As such, [AppState] does not actually implement the [SessionState] trait. pub struct AppState { - state: Mutex<(MapState, UndoStack)>, + // Assigns a state object to every session. + session_state: Mutex>, + // Assigns a session ID to every window. + window_to_session: Mutex>, } impl Default for AppState { fn default() -> Self { AppState { - state: Mutex::new((MapState::default(), UndoStack::default())), + session_state: Mutex::new(HashMap::new()), + window_to_session: Mutex::new(HashMap::new()), } } } impl AppState { - pub fn window_created(&self, label: &str, state: impl Into) { - let mut guard = self.state.lock().unwrap_or_else(|_e| { + pub fn session_created(&self, id: &str, session: impl Into) { + let mut guard = self.session_state.lock().unwrap_or_else(|_e| { panic!("Main application state is poisoned. Cannot recover."); }); - let (windows, _stack) = guard.deref_mut(); - if windows.state.contains_key(label) { + let session_map = guard.deref_mut(); + if session_map.contains_key(id) { // TODO: This should be a normal error. - panic!("Window already exists"); + panic!("Session already exists."); } - windows.state.insert(label.to_string(), state.into()); + session_map.insert(id.to_string(), session.into()); } - pub fn undo(&self, app: &AeonApp) -> Result<(), DynError> { - let mut guard = self - .state - .lock() - .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); - let (windows, stack) = guard.deref_mut(); - let mut event = match stack.undo_action() { - Some(reverse) => reverse, - None => { - debug!("Cannot undo."); - return Ok(()); - } - }; - let state_change = loop { - let path = event - .event - .full_path - .iter() - .map(|it| it.as_str()) - .collect::>(); - let result = windows.consume_event(&path, &event); - match result { - Err(error) => { - debug!("Event error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::NoChange) => { - debug!("No change."); - return Ok(()); - } - // TODO: We should treat this error differently. - Ok(Consumed::InputError(error)) => { - debug!("User input error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::Irreversible(_)) => { - // TODO: This probably shouldn't happen here. - panic!("Irreversible action as a result of undo.") - } - Ok(Consumed::Reversible(state, _)) => { - debug!("Reversible state change: `{:?}`.", state); - break state; - } - Ok(Consumed::Restart(action)) => { - event = action; - } - } - }; - if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { - return AeonError::throw_with_source("Error sending state event.", e); + pub fn window_created(&self, id: &str, session_id: &str) { + let mut guard = self.window_to_session.lock().unwrap_or_else(|_e| { + panic!("Main application state is poisoned. Cannot recover."); + }); + let map = guard.deref_mut(); + if map.contains_key(id) { + // TODO: This should be a normal error. + panic!("Window already exists."); } - Ok(()) + map.insert(id.to_string(), session_id.to_string()); } - pub fn redo(&self, app: &AeonApp) -> Result<(), DynError> { + pub fn get_session_id(&self, window: &Window) -> String { + let guard = self.window_to_session.lock().unwrap_or_else(|_e| { + panic!("Main application state is poisoned. Cannot recover."); + }); + let map = guard.deref(); + map.get(window.label()).cloned().unwrap_or_else(|| { + panic!("Unknown window label {}.", window.label()); + }) + } + + pub fn consume_event( + &self, + app: &AeonApp, + session_id: &str, + action: &UserAction, + ) -> Result<(), DynError> { let mut guard = self - .state + .session_state .lock() .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); - let (windows, stack) = guard.deref_mut(); - let mut event = match stack.redo_action() { - Some(perform) => perform, - None => { - debug!("Cannot redo."); - return Ok(()); - } - }; - let state_change = loop { - let path = event - .event - .full_path - .iter() - .map(|it| it.as_str()) - .collect::>(); - let result = windows.consume_event(&path, &event); - match result { - Err(error) => { - debug!("Event error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::NoChange) => { - debug!("No change."); - return Ok(()); - } - // TODO: We should treat this error differently. - Ok(Consumed::InputError(error)) => { - debug!("User input error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::Irreversible(_)) => { - // TODO: This probably shouldn't happen here. - panic!("Irreversible action as a result of redo.") - } - Ok(Consumed::Reversible(state, _)) => { - debug!("Reversible state change: `{:?}`.", state); - break state; - } - Ok(Consumed::Restart(action)) => { - event = action; - } - } - }; - if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { + let session = guard.deref_mut().get_mut(session_id).unwrap_or_else(|| { + panic!("Unknown session id {}.", session_id); + }); + let state_change = session.perform_action(action)?; + if let Err(e) = app.tauri.emit_all(AEON_VALUE, state_change.events) { return AeonError::throw_with_source("Error sending state event.", e); } + Ok(()) } - pub fn consume_event(&self, app: &AeonApp, mut event: UserAction) -> Result<(), DynError> { + pub fn refresh( + &self, + app: &AeonApp, + session_id: &str, + full_path: &[String], + ) -> Result<(), DynError> { let mut guard = self - .state + .session_state .lock() .unwrap_or_else(|_e| panic!("Main application state is poisoned. Cannot recover.")); - let (windows, stack) = guard.deref_mut(); - let state_change = loop { - let path = event - .event - .full_path - .iter() - .map(|it| it.as_str()) - .collect::>(); - let result = windows.consume_event(&path, &event); - match result { - Err(error) => { - debug!("Event error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::NoChange) => { - debug!("No change."); - return Ok(()); - } - // TODO: We should treat this error differently. - Ok(Consumed::InputError(error)) => { - debug!("User input error: `{:?}`.", error); - return Err(error); - } - Ok(Consumed::Irreversible(state)) => { - debug!("Irreversible state change: `{:?}`.", state); - stack.clear(); - break state; - } - Ok(Consumed::Reversible(state, (perform, reverse))) => { - debug!("Reversible state change: `{:?}`.", state); - if !stack.do_action(perform, reverse) { - // TODO: - // This is a warning, because the state has been applied at - // this point, but we should think a bit more about how this - // should be ideally handled. - stack.clear(); - } - break state; - } - Ok(Consumed::Restart(action)) => { - event = action; - } - } - }; - if let Err(e) = app.tauri.emit_all(EVENT_STATE_CHANGE, state_change.event) { + let session = guard.deref_mut().get_mut(session_id).unwrap_or_else(|| { + panic!("Unknown session id {}.", session_id); + }); + let at_path = full_path.iter().map(|it| it.as_str()).collect::>(); + let state_change = session.refresh(full_path, &at_path)?; + if let Err(e) = app.tauri.emit_all(AEON_VALUE, vec![state_change]) { return AeonError::throw_with_source("Error sending state event.", e); } + Ok(()) } } diff --git a/src-tauri/src/app/state/_state_atomic.rs b/src-tauri/src/app/state/_state_atomic.rs index b724d02..db34ec6 100644 --- a/src-tauri/src/app/state/_state_atomic.rs +++ b/src-tauri/src/app/state/_state_atomic.rs @@ -1,12 +1,13 @@ -use crate::app::event::{StateChange, UserAction}; +use crate::app::event::Event; use crate::app::state::{Consumed, SessionState}; use crate::app::{AeonError, DynError}; -use std::str::FromStr; +use serde::de::DeserializeOwned; +use serde::Serialize; /// Atomic state is a [SessionState] which holds exactly one value of a generic type `T`. /// -/// The generic type `T` needs to implement [FromStr] and [ToString] to make it serializable -/// into the [crate::app::event::Event] payload. Furthermore, we need [PartialEq] to check +/// The generic type `T` needs to implement [Serialize] and [DeserializeOwned] to make it +/// serializable into the [Event] payload. Furthermore, we need [PartialEq] to check /// if the value changed, and [Clone] to enable state replication. /// /// Each [AtomicState] only consumes one type of event: the remaining `path` must be empty @@ -14,30 +15,38 @@ use std::str::FromStr; #[derive(Clone, Debug)] pub struct AtomicState(T) where - T: FromStr + ToString + PartialEq + Clone; + T: Serialize + DeserializeOwned + PartialEq + Clone; -impl From for AtomicState { +impl From for AtomicState { fn from(value: T) -> Self { AtomicState(value) } } -impl Default for AtomicState { +impl Default for AtomicState { fn default() -> Self { AtomicState(T::default()) } } -impl SessionState for AtomicState { - fn consume_event(&mut self, path: &[&str], action: &UserAction) -> Result { - if !path.is_empty() { - let msg = format!("Atomic state cannot consume a path `{:?}`.", path); +impl AtomicState { + pub fn value_string(&self) -> String { + serde_json::to_string(&self.0).unwrap_or_else(|_e| { + unreachable!("Value was received as payload but cannot be converted back?"); + }) + } +} + +impl SessionState for AtomicState { + fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result { + if !at_path.is_empty() { + let msg = format!("Atomic state cannot consume a path `{:?}`.", at_path); return AeonError::throw(msg); } - let Some(payload) = &action.event.payload else { + let Some(payload) = &event.payload else { return AeonError::throw("Missing payload for atomic state event."); }; - let Ok(payload) = T::from_str(payload) else { + let Ok(payload) = serde_json::from_str(payload.as_str()) else { let msg = format!( "Cannot convert input `{}` to type `{}`.", payload, @@ -48,21 +57,33 @@ impl SessionState for AtomicState if self.0 == payload { return Ok(Consumed::NoChange); } - let perform_event = action.clone(); - let mut reverse_event = action.clone(); - reverse_event.event.payload = Some(self.0.to_string()); + let perform_event = event.clone(); + let mut reverse_event = event.clone(); + let old_value_str = self.value_string(); + reverse_event.payload = Some(old_value_str); self.0 = payload; - Ok(Consumed::Reversible( - StateChange::from(action.clone()), - (perform_event, reverse_event), - )) + Ok(Consumed::Reversible { + state_change: perform_event.clone(), + perform_reverse: (perform_event, reverse_event), + }) + } + + fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { + if !at_path.is_empty() { + let msg = format!("Atomic state cannot consume a path `{:?}`.", at_path); + return AeonError::throw(msg); + } + Ok(Event { + path: full_path.to_vec(), + payload: Some(self.value_string()), + }) } } #[cfg(test)] mod tests { - use crate::app::event::{Event, UserAction}; + use crate::app::event::Event; use crate::app::state::_state_atomic::AtomicState; use crate::app::state::{Consumed, SessionState}; @@ -70,28 +91,28 @@ mod tests { fn test_atomic_state() { let mut state = AtomicState::from(3); - let event_3: UserAction = Event::build(&["segment"], Some("3")).into(); - let event_4: UserAction = Event::build(&["segment"], Some("4")).into(); - let event_empty: UserAction = Event::build(&["segment"], None).into(); - let event_invalid: UserAction = Event::build(&["segment"], Some("abc")).into(); + let event_3 = Event::build(&["segment"], Some("3")); + let event_4 = Event::build(&["segment"], Some("4")); + let event_empty = Event::build(&["segment"], None); + let event_invalid = Event::build(&["segment"], Some("abc")); - let result = state.consume_event(&[], &event_3).unwrap(); + let result = state.perform_event(&event_3, &[]).unwrap(); assert!(matches!(result, Consumed::NoChange)); - let result = state.consume_event(&[], &event_4).unwrap(); - assert!(matches!(result, Consumed::Reversible(..))); + let result = state.perform_event(&event_4, &[]).unwrap(); + assert!(matches!(result, Consumed::Reversible { .. })); - let result = state.consume_event(&["foo"], &event_3).unwrap_err(); + let result = state.perform_event(&event_3, &["foo"]).unwrap_err(); assert_eq!( "Atomic state cannot consume a path `[\"foo\"]`.", format!("{}", result) ); - let result = state.consume_event(&[], &event_empty).unwrap_err(); + let result = state.perform_event(&event_empty, &[]).unwrap_err(); assert_eq!( "Missing payload for atomic state event.", format!("{}", result) ); - let result = state.consume_event(&[], &event_invalid).unwrap(); + let result = state.perform_event(&event_invalid, &[]).unwrap(); assert!(matches!(result, Consumed::InputError(..))); } } diff --git a/src-tauri/src/app/state/_state_map.rs b/src-tauri/src/app/state/_state_map.rs index 210a625..e994838 100644 --- a/src-tauri/src/app/state/_state_map.rs +++ b/src-tauri/src/app/state/_state_map.rs @@ -1,3 +1,7 @@ +/* + +THIS IS DISABLED FOR NOW, BECAUSE WE DON'T REALLY NEED IT SO FAR. + use crate::app::event::UserAction; use crate::app::state::{Consumed, DynSessionState, SessionState}; use crate::app::{AeonError, DynError}; @@ -66,3 +70,4 @@ mod tests { assert_eq!("Unknown path segment `state_4`.", format!("{}", result)); } } +*/ diff --git a/src-tauri/src/app/_undo_stack.rs b/src-tauri/src/app/state/_undo_stack.rs similarity index 79% rename from src-tauri/src/app/_undo_stack.rs rename to src-tauri/src/app/state/_undo_stack.rs index 40952a1..460343b 100644 --- a/src-tauri/src/app/_undo_stack.rs +++ b/src-tauri/src/app/state/_undo_stack.rs @@ -1,4 +1,6 @@ -use crate::app::event::UserAction; +use crate::app::event::{Event, UserAction}; +use crate::app::state::{Consumed, SessionState}; +use crate::app::{AeonError, DynError}; use crate::debug; use std::collections::VecDeque; @@ -19,11 +21,14 @@ pub struct UndoStackEntry { impl UndoStackEntry { /// The sum of payload sizes for the underlying UI actions. pub fn payload_size(&self) -> usize { - self.perform_action.event.payload_size() + self.reverse_action.event.payload_size() + self.perform_action.byte_size() + self.reverse_action.byte_size() } } -/// The stack that keeps track of all the events that +/// The stack that keeps track of all the events that can be reversed. +/// +/// It has a "normal" Rust API, but it also implements [SessionState] so that parts of it can be +/// accessed as an app state object through events. #[derive(Clone, Eq, PartialEq)] pub struct UndoStack { /// The number of events this `UndoStack` is allowed to track. @@ -58,6 +63,14 @@ impl UndoStack { } } + pub fn can_undo(&self) -> bool { + !self.undo_stack.is_empty() + } + + pub fn can_redo(&self) -> bool { + !self.redo_stack.is_empty() + } + /// Remove all elements from the [UndoStack]. pub fn clear(&mut self) { self.current_payload_size = 0; @@ -75,34 +88,35 @@ impl UndoStack { self.redo_stack.len() } - /// Notify the undo stack that a new action will be performed. This creates a new stack + /// Notify the undo stack that a new action has been performed. This creates a new stack /// entry for this action. Furthermore, it erases any available "redo" actions. /// /// Returns `true` if the events were successfully saved, or `false` if an error occurred, /// e.g. due to excessive payload size. #[must_use] pub fn do_action(&mut self, perform: UserAction, reverse: UserAction) -> bool { - // Items from `redo_stack` are no longer relevant since the + // Items from the `redo_stack` are no longer relevant. self.redo_stack.clear(); - // Drop events that are over limit (first event limit, then payload limit). + // Drop events even the stack is too deep. while self.undo_stack.len() >= self.event_limit { let Some(event) = self.drop_undo_event() else { break; // The stack is empty. }; debug!( - "Event count exceeded. Dropping event: `{:?}`.", - event.perform_action.event.full_path + "Event count exceeded. Dropping action with {} events.", + event.perform_action.events.len(), ); } - let additional_payload = perform.event.payload_size() + reverse.event.payload_size(); + // Drop events if the payloads are too big. + let additional_payload = perform.byte_size() + reverse.byte_size(); while self.current_payload_size + additional_payload >= self.payload_limit { let Some(event) = self.drop_undo_event() else { break; // The stack is empty. }; debug!( - "Payload size exceeded. Dropping event: `{:?}`.", - event.perform_action.event.full_path + "Payload size exceeded. Dropping action with {} events.", + event.perform_action.events.len() ); } @@ -136,8 +150,8 @@ impl UndoStack { } /// Try to undo the current top of the undo stack. This action can be later re-done using - /// `Self::redo_action`. Returns `None` if there is no action to undo, or the "reverse" - /// `GuiEvent` originally supplied to `Self::do_action`. + /// [Self::redo_action]. Returns [None] if there is no action to undo, or the "reverse" + /// [UserAction] originally supplied to [Self::do_action]. #[must_use] pub fn undo_action(&mut self) -> Option { let Some(entry) = self.undo_stack.pop_back() else { @@ -151,8 +165,8 @@ impl UndoStack { } /// Try to redo the current top of the redo stack. This action can be later un-done using - /// `Self::undo_action`. Returns `None` if there is no action to redo, or the "perform" - /// `GuiEvent` originally supplied to `Self::do_action`. + /// [Self::undo_action]. Returns [None] if there is no action to redo, or the "perform" + /// [UserAction] originally supplied to [Self::do_action]. #[must_use] pub fn redo_action(&mut self) -> Option { let Some(entry) = self.redo_stack.pop_back() else { @@ -165,7 +179,7 @@ impl UndoStack { result } - /// Internal function to drop an entry from the `undo_stack`. + /// Internal function to drop an [UndoStackEntry] from the `undo_stack`. fn drop_undo_event(&mut self) -> Option { let entry = self.undo_stack.pop_front()?; self.current_payload_size -= entry.payload_size(); @@ -174,6 +188,28 @@ impl UndoStack { } } +impl SessionState for UndoStack { + fn perform_event(&mut self, _event: &Event, _at_path: &[&str]) -> Result { + AeonError::throw("`UndoStack` cannot consume events.") + } + + fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { + // We could probably simplify this slightly, but if we ever add new entries, we will + // have to rewrite the whole thing. Now we can just add a new branch. + match at_path { + ["can_undo"] => Ok(Event { + path: full_path.to_vec(), + payload: serde_json::to_string(&self.can_undo()).ok(), + }), + ["can_redo"] => Ok(Event { + path: full_path.to_vec(), + payload: serde_json::to_string(&self.can_redo()).ok(), + }), + _ => AeonError::throw(format!("`UndoStack` has no path `{:?}`.", at_path)), + } + } +} + impl Default for UndoStack { fn default() -> Self { UndoStack::new(DEFAULT_EVENT_LIMIT, DEFAULT_PAYLOAD_LIMIT) @@ -182,8 +218,8 @@ impl Default for UndoStack { #[cfg(test)] mod tests { - use crate::app::_undo_stack::UndoStack; use crate::app::event::{Event, UserAction}; + use crate::app::state::_undo_stack::UndoStack; #[test] pub fn test_normal_behaviour() { @@ -220,7 +256,7 @@ mod tests { let e2: UserAction = Event::build(&[], None).into(); let e3: UserAction = Event::build(&["path"], Some("payload 3")).into(); - let mut stack = UndoStack::new(4, 2 * e3.event.payload_size() + 1); + let mut stack = UndoStack::new(4, 2 * e3.byte_size() + 1); // Test that the even limit is respected. We should be able to fit 4 events. assert!(stack.do_action(e2.clone(), e1.clone())); diff --git a/src-tauri/src/app/state/editor/_state_editor_session.rs b/src-tauri/src/app/state/editor/_state_editor_session.rs new file mode 100644 index 0000000..e30a646 --- /dev/null +++ b/src-tauri/src/app/state/editor/_state_editor_session.rs @@ -0,0 +1,206 @@ +use crate::app::event::{Event, StateChange, UserAction}; +use crate::app::state::_undo_stack::UndoStack; +use crate::app::state::editor::TabBarState; +use crate::app::state::{Consumed, Session, SessionState}; +use crate::app::{AeonError, DynError}; +use crate::debug; + +/// The state of one editor session. +/// +/// An editor session is the "main" app session where a model is created/edited and from which +/// different analysis sessions can be started. +/// +pub struct EditorSession { + id: String, + undo_stack: UndoStack, + tab_bar: TabBarState, +} + +impl EditorSession { + pub fn new(id: &str) -> EditorSession { + EditorSession { + id: id.to_string(), + undo_stack: UndoStack::default(), + tab_bar: TabBarState::default(), + } + } + + fn perform_action( + &mut self, + action: &UserAction, + ignore_stack: bool, + ) -> Result { + // Events that need to be consume (last to first) in order to complete this action. + let mut to_perform = action.events.clone(); + to_perform.reverse(); + + // The events representing successful state changes. + let mut state_changes: Vec = Vec::new(); + // The events that can be used to create a redo stack entry if the action is reversible. + let mut reverse: Option> = + if ignore_stack { None } else { Some(Vec::new()) }; + let mut reset_stack = false; + + while let Some(event) = to_perform.pop() { + let event_path = event.path.iter().map(|it| it.as_str()).collect::>(); + let result = match self.perform_event(&event, &event_path) { + Ok(result) => result, + Err(error) => { + // TODO: + // We should probably first emit the state change and then the + // error, because now we are losing state of compound actions that fail. + return Err(error); + } + }; + match result { + Consumed::Reversible { + state_change, + perform_reverse, + } => { + state_changes.push(state_change); + if let Some(reverse) = reverse.as_mut() { + // If we can reverse this action, save the events. + reverse.push(perform_reverse); + } + } + Consumed::Irreversible { + state_change, + reset, + } => { + state_changes.push(state_change); + if reset { + // We cannot reverse this event, but the rest can be reversed. + reverse = None; + reset_stack = true; + } + } + Consumed::Restart(mut events) => { + // Just push the new events to the execution stack and continue + // to the next event. + events.reverse(); + while let Some(e) = events.pop() { + to_perform.push(e); + } + } + Consumed::InputError(error) => { + // TODO: + // The same as above. We should report this as a separate event from the + // state change that was performed. + return Err(error); + } + Consumed::NoChange => { + // Do nothing. + } + } + } + + // If the action is not irreversible, we should add an entry to the undo stack. + if let Some(events) = reverse { + if !events.is_empty() { + // Only add undo action if the stack is not empty. + let mut perform = Vec::new(); + let mut reverse = Vec::new(); + for (p, r) in events { + perform.push(p); + reverse.push(r); + } + let perform = UserAction { events: perform }; + let reverse = UserAction { events: reverse }; + if !self.undo_stack.do_action(perform, reverse) { + // TODO: Not match we can do here, maybe except issuing a warning. + self.undo_stack.clear(); + } + + // Notify about the changes in the stack state. + // TODO: Maybe we don't need to emit this always. + self.append_stack_updates(&mut state_changes); + } + } else if !ignore_stack && reset_stack { + debug!("Back stack cleared due to irreversible action."); + self.undo_stack.clear(); + } + + Ok(StateChange { + events: state_changes, + }) + } + + fn append_stack_updates(&self, state_changes: &mut Vec) { + let can_undo = serde_json::to_string(&self.undo_stack.can_undo()); + let can_redo = serde_json::to_string(&self.undo_stack.can_redo()); + state_changes.push(Event::build( + &["undo_stack", "can_undo"], + Some(can_undo.unwrap().as_str()), + )); + state_changes.push(Event::build( + &["undo_stack", "can_redo"], + Some(can_redo.unwrap().as_str()), + )); + } +} + +impl Session for EditorSession { + fn perform_action(&mut self, action: &UserAction) -> Result { + // Explicit test for undo-stack actions. + // TODO: + // Figure out a nicer way to do this. Probably modify the `Consumed` enum? + // We basically need a way to say "restart with these events, but as an + // Irreversible action that won't reset the stack." + 'undo: { + if action.events.len() == 1 { + let event = &action.events[0]; + if event.path.len() == 2 && event.path[0] == "undo_stack" { + let action = match event.path[1].as_str() { + "undo" => { + let Some(undo) = self.undo_stack.undo_action() else { + return AeonError::throw("Nothing to undo."); + }; + undo + } + "redo" => { + let Some(redo) = self.undo_stack.redo_action() else { + return AeonError::throw("Nothing to redo."); + }; + redo + } + _ => break 'undo, + }; + let mut state_change = self.perform_action(&action, true)?; + self.append_stack_updates(&mut state_change.events); + return Ok(state_change); + } + } + } + self.perform_action(action, false) + } + + fn id(&self) -> &str { + self.id.as_str() + } +} + +impl SessionState for EditorSession { + fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result { + if at_path.is_empty() { + return AeonError::throw("`EditorSession` cannot process an empty path."); + } + + match at_path[0] { + "undo_stack" => self.undo_stack.perform_event(event, &at_path[1..]), + "tab_bar" => self.tab_bar.perform_event(event, &at_path[1..]), + it => AeonError::throw(format!("Unknown path in `EditorSession`: `{}`", it)), + } + } + + fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { + if at_path.is_empty() { + return AeonError::throw("`EditorSession` cannot process an empty path."); + } + + match at_path[0] { + "undo_stack" => self.undo_stack.refresh(full_path, &at_path[1..]), + "tab_bar" => self.tab_bar.refresh(full_path, &at_path[1..]), + it => AeonError::throw(format!("Unknown path in `EditorSession`: `{}`", it)), + } + } +} diff --git a/src-tauri/src/app/state/editor/_state_tab_bar.rs b/src-tauri/src/app/state/editor/_state_tab_bar.rs new file mode 100644 index 0000000..5e1f4ae --- /dev/null +++ b/src-tauri/src/app/state/editor/_state_tab_bar.rs @@ -0,0 +1,35 @@ +use crate::app::event::Event; +use crate::app::state::{AtomicState, Consumed, SessionState}; +use crate::app::{AeonError, DynError}; + +#[derive(Default)] +pub struct TabBarState { + active: AtomicState, + pinned: AtomicState>, +} + +impl SessionState for TabBarState { + fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result { + if at_path.is_empty() { + return AeonError::throw("`TabBar` cannot process an empty path."); + } + + match at_path[0] { + "active" => self.active.perform_event(event, &at_path[1..]), + "pinned" => self.pinned.perform_event(event, &at_path[1..]), + it => AeonError::throw(format!("Unknown path in `TabBar`: `{}`", it)), + } + } + + fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { + if at_path.is_empty() { + return AeonError::throw("`TabBar` cannot process an empty path."); + } + + match at_path[0] { + "active" => self.active.refresh(full_path, &at_path[1..]), + "pinned" => self.pinned.refresh(full_path, &at_path[1..]), + it => AeonError::throw(format!("Unknown path in `TabBar`: `{}`", it)), + } + } +} diff --git a/src-tauri/src/app/state/editor/mod.rs b/src-tauri/src/app/state/editor/mod.rs new file mode 100644 index 0000000..704414d --- /dev/null +++ b/src-tauri/src/app/state/editor/mod.rs @@ -0,0 +1,7 @@ +/// Declares [EditorSession]: the root state object of the sketchbook editor. +mod _state_editor_session; +/// Declares [TabBarState]: the state object of the main tab navigation element. +mod _state_tab_bar; + +pub use _state_editor_session::EditorSession; +pub use _state_tab_bar::TabBarState; diff --git a/src-tauri/src/app/state/mod.rs b/src-tauri/src/app/state/mod.rs index 30837d2..58b9866 100644 --- a/src-tauri/src/app/state/mod.rs +++ b/src-tauri/src/app/state/mod.rs @@ -1,18 +1,39 @@ -use crate::app::event::UserAction; +use crate::app::event::{Event, StateChange, UserAction}; use crate::app::DynError; mod _consumed; mod _state_app; mod _state_atomic; mod _state_map; +pub mod _undo_stack; + +/// Declares state objects that are unique to the sketchbook editor window. +pub mod editor; pub use _consumed::Consumed; pub use _state_app::AppState; pub use _state_atomic::AtomicState; -pub use _state_map::MapState; +//pub use _state_map::MapState; pub type DynSessionState = Box<(dyn SessionState + Send + 'static)>; +pub type DynSession = Box<(dyn Session + Send + 'static)>; pub trait SessionState { - fn consume_event(&mut self, path: &[&str], action: &UserAction) -> Result; + /// Modify the session state using the provided `event`. The possible outcomes are + /// described by [Consumed]. + fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result; + + /// "Read" session state into an event without modifying it. + fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result; +} + +pub trait Session: SessionState { + /// Perform a user action on this session state object. This usually involves propagating + /// the events to the internal [SessionState] objects and collecting the results into a + /// single [StateChange] entry. + fn perform_action(&mut self, action: &UserAction) -> Result; + + /// Returns the string identifier of this particular session. Each session identifier must + /// be unique within the application. + fn id(&self) -> &str; } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f063871..3aa52dd 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,19 +2,37 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] use aeon_sketchbook::app::event::{Event, UserAction}; -use aeon_sketchbook::app::state::{AppState, AtomicState, DynSessionState, MapState}; -use aeon_sketchbook::app::{AeonApp, EVENT_USER_ACTION}; +use aeon_sketchbook::app::state::editor::EditorSession; +use aeon_sketchbook::app::state::{AppState, DynSession}; +use aeon_sketchbook::app::{AeonApp, AEON_ACTION, AEON_REFRESH}; use aeon_sketchbook::debug; -use tauri::Manager; +use serde::{Deserialize, Serialize}; +use tauri::{command, Manager, State, Window}; + +#[command] +fn get_session_id(window: Window, state: State) -> String { + state.get_session_id(&window) +} + +#[derive(Serialize, Deserialize)] +struct AeonAction { + session: String, + events: Vec, +} + +#[derive(Serialize, Deserialize)] +struct AeonRefresh { + session: String, + path: Vec, +} fn main() { - let editor_state: Vec<(&str, DynSessionState)> = vec![ - ("counter", Box::new(AtomicState::from(0))), - ("text", Box::new(AtomicState::from("".to_string()))), - ]; - let editor_state: DynSessionState = Box::new(MapState::from_iter(editor_state)); + // Initialize empty app state. let state = AppState::default(); - state.window_created("editor", editor_state); + let session: DynSession = Box::new(EditorSession::new("editor-1")); + state.session_created("editor-1", session); + state.window_created("editor", "editor-1"); + tauri::Builder::default() .manage(state) .setup(|app| { @@ -23,44 +41,56 @@ fn main() { tauri: handle.clone(), }; let aeon = aeon_original.clone(); - app.listen_global(EVENT_USER_ACTION, move |e| { + app.listen_global(AEON_ACTION, move |e| { let Some(payload) = e.payload() else { // TODO: This should be an error. panic!("No payload in user action."); }; debug!("Received user action: `{}`.", payload); - let event: UserAction = match serde_json::from_str::(payload) { - Ok(event) => event.into(), + let event: AeonAction = match serde_json::from_str::(payload) { + Ok(action) => action, Err(e) => { // TODO: This should be a normal error. panic!("Payload deserialize error {:?}.", e); } }; let state = aeon.tauri.state::(); - let result = state.consume_event(&aeon, event); + let session_id = event.session.clone(); + let action = UserAction { + events: event.events, + }; + let result = state.consume_event(&aeon, session_id.as_str(), &action); if let Err(e) = result { // TODO: This should be a normal error. panic!("Event error: {:?}", e); } }); let aeon = aeon_original.clone(); - app.listen_global("undo", move |_e| { - let state = aeon.tauri.state::(); - if let Err(e) = state.undo(&aeon) { - // TODO: This should be a normal error. - panic!("Undo error: {:?}", e); - } - }); - let aeon = aeon_original.clone(); - app.listen_global("redo", move |_e| { + app.listen_global(AEON_REFRESH, move |e| { + let Some(payload) = e.payload() else { + // TODO: This should be an error. + panic!("No payload in user action."); + }; + debug!("Received user action: `{}`.", payload); + let event: AeonRefresh = match serde_json::from_str::(payload) { + Ok(action) => action, + Err(e) => { + // TODO: This should be a normal error. + panic!("Payload deserialize error {:?}.", e); + } + }; let state = aeon.tauri.state::(); - if let Err(e) = state.redo(&aeon) { + let session_id = event.session.clone(); + let path = event.path; + let result = state.refresh(&aeon, session_id.as_str(), &path); + if let Err(e) = result { // TODO: This should be a normal error. - panic!("Redo error: {:?}", e); + panic!("Event error: {:?}", e); } }); Ok(()) }) + .invoke_handler(tauri::generate_handler![get_session_id]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 7fac7c1..d617172 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -16,6 +16,9 @@ "shell": { "all": false, "open": true + }, + "dialog": { + "all": true } }, "bundle": { diff --git a/src/aeon_events.ts b/src/aeon_events.ts new file mode 100644 index 0000000..ffcde7a --- /dev/null +++ b/src/aeon_events.ts @@ -0,0 +1,355 @@ +import { Event, emit, listen } from '@tauri-apps/api/event' +import { dialog, invoke } from '@tauri-apps/api'; + +/* Names of relevant events that communicate with the Tauri backend. */ + +const AEON_ACTION = 'aeon-action' +const AEON_VALUE = 'aeon-value' +const AEON_REFRESH = 'aeon-refresh' + + +/** + * A type-safe representation of the state managed by an Aeon session. + * + * Under the hood, this uses `AeonEvents` to implement actions and listeners. + */ +type AeonState = { + + /** Access to the internal state of the undo-redo stack. */ + undo_stack: { + /** True if the stack has actions that can be undone. */ + can_undo: ObservableState, + /** True if the stack has actions that can be redone. */ + can_redo: ObservableState, + /** Try to undo an action. Emits an error if no actions can be undone. */ + undo(): void, + /** Try to redo an action. Emits an error if no actions can be redone. */ + redo(): void, + }, + + /** The state of the main navigation tab-bar. */ + tab_bar: { + /** Integer ID of the currently active tab. */ + active: MutableObservableState, + /** A *sorted* list of IDs of all pinned tabs. */ + pinned: MutableObservableState, + /** Pins a single tab if not currently pinned. */ + pin(id: number): void, + /** Unpins a single tab if currently pinned. */ + unpin(id: number): void, + } + +} + +/** A function that is notified when a state value changes. */ +type OnStateValue = (value: T) => void; + +/** + * An object that can be emitted through `AeonEvents` as a user action. + * + * The `path` property specifies which state item is affected by the event, while + * the `payload` represents the contents of the event. Multiple events can be + * joined together to create a "compound" event that is treated as + * a single user action in the context of the undo-redo stack. + */ +type AeonEvent = { + path: string[], + payload: string | null, +} + +/** + * One item of the observable application state of type `T`. The UI can listen to + * the value changes. + * + * This structure assumes the state is not directly user editable. That is, the user cannot + * directly write value of type `T` into this item. However, it could be still editable + * indirectly through other items or triggers. + */ +class ObservableState { + path: string[] + last_value: T + listeners: OnStateValue[] = [] + + constructor(path: string[], initial: T) { + this.path = path; + this.last_value = initial; + aeon_events.setEventListener(path, this.#acceptPayload.bind(this)); + } + + /** + * Register a listener to this specific observable state item. + * + * The listener is notified right away with the current value of the state item. + * + * @param listener The listener function which should be invoked when the value + * of this state item changes. + * @param notifyNow If set to `false`, the listener is not notified with the current + * value of the state item upon registering. + * @returns `true` if the listener was added, `false` if it was already registered. + */ + addEventListener(listener: OnStateValue, notifyNow = true) { + if (this.listeners.includes(listener)) { + return false; + } + this.listeners.push(listener); + if (notifyNow) { + this.#notifyListener(listener, this.last_value); + } + return true; + } + + /** + * Unregister a listener previously added through `addEventListener`. + * + * @param listener The listener to be unregistered. + * @returns `true` if the listener was removed, `false` if it was not registered. + */ + removeEventListener(listener: OnStateValue) { + const index = this.listeners.indexOf(listener); + if (index > -1) { + this.listeners.splice(index, 1); + return true; + } + return false; + } + + /** + * Re-emit the latest state of this item to all event listeners. + */ + refresh() { + aeon_events.refresh(this.path); + } + + /** + * The last value that was emitted for this observable state. + */ + value(): T { + return this.last_value; + } + + /** + * Accept a payload value coming from the backend. + * @param payload The actual JSON-encoded payload value. + */ + #acceptPayload(payload: string | null) { + try { + const value = JSON.parse(payload === null ? "null" : payload); + this.last_value = value; + for (let listener of this.listeners) { + this.#notifyListener(listener, value); + } + } catch (error) { + dialog.message( + `Cannot dispatch event [${this.path}] with payload ${payload}: ${error}`, + { title: "Internal app error", type: "error" } + ); + } + } + + #notifyListener(listener: OnStateValue, value: T) { + try { + listener(value); + } catch (error) { + dialog.message( + `Cannot handle event [${this.path}] with value ${value}: ${error}`, + { title: "Internal app error", type: "error" } + ); + } + } +} + +/** + * A version of `ObservableState` which supports direct mutation. + */ +class MutableObservableState extends ObservableState { + constructor(path: string[], initial: T) { + super(path, initial) + } + + /** + * Emit a user action which triggers value mutation on the backend. This should then + * result in a new value being observable through the registered event listeners. + * + * @param value New value. + */ + emitValue(value: T) { + aeon_events.emitAction(this.set(value)) + } + + /** + * Create a `set` event for the provided `value` and this observable item. Note that the event + * is not emitted automatically, but needs to be sent through the `aeon_events`. You can use + * `this.emitValue` to create and emit the event as one action. + * + * The reason why you might want to create the event but not emit it is that events can be + * bundled to create complex reversible user actions. + * + * @param value New value. + * @returns An event that can be emitted through `AeonEvents`. + */ + set(value: T): AeonEvent { + return { + path: this.path, + payload: JSON.stringify(value) + } + } +} + +/** + * A (reasonably) type-safe wrapper around AEON related tauri events that communicate the state + * of the application between the frontend and the backend. + * + * Each event path only supports one event listener. If you need more listenerers, you likely + * want to wrap the path into an `ObservableState` object. + */ +class AeonEvents { + /** Uniquely identifies one "session". Multiple windows can share a session. */ + session_id: string + listeners: any + + constructor(session_id: string) { + this.session_id = session_id; + this.listeners = {} + listen(AEON_VALUE, this.#onStateChange.bind(this)) + } + + /** Request a retransmission of state data managed at the provided path. */ + refresh(path: string[]) { + emit(AEON_REFRESH, { + session: this.session_id, + path: path, + }).catch((error) => { + dialog.message( + `Cannot refresh [${path}]: ${error}`, + { title: "Internal app error", type: "error" } + ); + }); + } + + /** + * Emit one or more aeon events as a single user action. + * + * Assuming all events are reversible, the item will be saved as a single undo-redo entry. + */ + emitAction(events: AeonEvent | AeonEvent[]) { + if (!(events instanceof Array)) { + events = [events] + } + emit(AEON_ACTION, { + session: this.session_id, + events: events + }).catch((error) => { + dialog.message( + `Cannot process events [${events}]: ${error}`, + { title: "Internal app error", type: "error" } + ); + }); + } + + /** + * Set an event listener that will be notified when the specified path changes. + * + * Note that only one listener is supported for each path. If you want more listeners, + * consider the `ObservableState` class. + */ + setEventListener( + path: string[], + listener: (payload: string | null) => void + ) { + let listeners = this.listeners + while (path.length > 1) { + let key = path[0] + path = path.slice(1) + if (!(key in listeners)) { + listeners[key] = {} + } + listeners = listeners[key] + } + listeners[path[0]] = listener; + } + + /** + * React to (possibly multiple) value changes. + * + * Note that if multiple values changed, the listeners are notified + * in sequence. + */ + #onStateChange(event: Event) { + for (let e of event.payload) { + this.#notifyValueChange(e) + } + } + + #notifyValueChange(event: AeonEvent) { + // Find listener residing at the specified path, or return if no listener exists. + let listener = this.listeners; + let path = event.path; + while (path.length > 0) { + let key = path[0] + path = path.slice(1) + if (!(key in listener)) { + console.log("Event ignored.", event.path); + return; + } + listener = listener[key]; + } + + // Emit event. + try { + listener(event.payload); + } catch (error) { + dialog.message( + `Cannot handle event [${event.path}] with payload ${event.payload}: ${error}`, + { title: "Internal app error", type: "error" } + ); + } + } + +} + +/** + * A singleton object which implements access to Tauri events. + */ +export let aeon_events = new AeonEvents(await invoke('get_session_id')); + +/** + * A singleton state management object for the current Aeon session. + */ +export let aeon_state: AeonState = { + undo_stack: { + can_undo: new ObservableState(["undo_stack", "can_undo"], false), + can_redo: new ObservableState(["undo_stack", "can_redo"], false), + undo() { + aeon_events.emitAction({ + path: ["undo_stack", "undo"], + payload: null, + }) + }, + redo() { + aeon_events.emitAction({ + path: ["undo_stack", "redo"], + payload: null, + }) + }, + }, + tab_bar: { + active: new MutableObservableState(["tab_bar", "active"], 0), + pinned: new MutableObservableState(["tab_bar", "pinned"], []), + pin (id: number): void { + const value = this.pinned.value(); + if (!value.includes(id)) { + value.push(id); + value.sort(); + this.pinned.emitValue(value); + } + }, + unpin (id: number): void { + const value = this.pinned.value(); + const index = value.indexOf(id); + if (index > -1) { + value.splice(index, 1); + this.pinned.emitValue(value); + } + } + } +}; \ No newline at end of file diff --git a/src/html/component/content-pane/content-pane.ts b/src/html/component/content-pane/content-pane.ts index 0fa367f..ea538dc 100644 --- a/src/html/component/content-pane/content-pane.ts +++ b/src/html/component/content-pane/content-pane.ts @@ -5,6 +5,7 @@ import { type TabData } from '../../util/tab-data' import { library, icon, findIconDefinition } from '@fortawesome/fontawesome-svg-core' import '../regulations-editor/regulations-editor' import { faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons' +import { aeon_state } from '../../../aeon_events' library.add(faLock, faLockOpen) @customElement('content-pane') @@ -14,13 +15,11 @@ export class ContentPane extends LitElement { @property() declare private readonly tab: TabData private pin (): void { - this.dispatchEvent(new CustomEvent(this.tab.pinned ? 'unpin-tab' : 'pin-tab', { - detail: { - tabId: this.tab.id - }, - bubbles: true, - composed: true - })) + if (this.tab.pinned) { + aeon_state.tab_bar.unpin(this.tab.id) + } else { + aeon_state.tab_bar.pin(this.tab.id) + } } protected render (): TemplateResult { diff --git a/src/html/component/root-component/root-component.ts b/src/html/component/root-component/root-component.ts index 0b7e88e..cfa0007 100644 --- a/src/html/component/root-component/root-component.ts +++ b/src/html/component/root-component/root-component.ts @@ -6,6 +6,7 @@ import '../content-pane/content-pane' import '../nav-bar/nav-bar' import { type TabData } from '../../util/tab-data' import { tabList } from '../../util/config' +import { aeon_state } from '../../../aeon_events' const SAVE_KEY = 'tab-data' @@ -14,53 +15,25 @@ class RootComponent extends LitElement { static styles = css`${unsafeCSS(style_less)}` @state() tabs: TabData[] = tabList constructor () { - super() - this.loadTabs() - this.addEventListener('switch-tab', this.switchTab) - this.addEventListener('pin-tab', this.pinTab) - this.addEventListener('unpin-tab', this.pinTab) + super() + aeon_state.tab_bar.active.addEventListener(this.#onSwitched.bind(this)) + aeon_state.tab_bar.pinned.addEventListener(this.#onPinned.bind(this)) } - private saveTabs (): void { - const tabData = this.tabs.map((tab) => ({ - id: tab.id, - active: tab.active, - pinned: tab.pinned - })) - localStorage.setItem(SAVE_KEY, JSON.stringify(tabData)) - } - - private loadTabs (): void { - const tabData = JSON.parse(localStorage.getItem(SAVE_KEY) ?? '[]') - tabData.forEach((data: { id: number, active: boolean, pinned: boolean }) => { - this.tabs[data.id] = this.tabs[data.id].copy({ - active: data.active, - pinned: data.pinned + #onPinned (pinned: number[]): void { + this.tabs = this.tabs.map((tab) => + tab.copy({ + pinned: pinned.includes(tab.id) }) - }) - } - - private pinTab (e: Event): void { - const tabId = (e as CustomEvent).detail.tabId - if (tabId === undefined) return - const tabIndex = this.tabs.findIndex((tab) => tab.id === tabId) - if (tabIndex === -1) return - const updatedTabs = this.tabs.slice() - updatedTabs[tabIndex] = updatedTabs[tabIndex].copy({ pinned: e.type === 'pin-tab' }) - this.tabs = updatedTabs - this.saveTabs() + ) } - private switchTab (e: Event): void { - const tabId = (e as CustomEvent).detail.tabId - if (tabId === undefined) return + #onSwitched (tabId: number) { this.tabs = this.tabs.map((tab) => tab.copy({ active: tab.id === tabId }) ) - this.requestUpdate() - this.saveTabs() } render (): TemplateResult { diff --git a/src/html/component/tab-bar/tab-bar.ts b/src/html/component/tab-bar/tab-bar.ts index 464538d..16b859b 100644 --- a/src/html/component/tab-bar/tab-bar.ts +++ b/src/html/component/tab-bar/tab-bar.ts @@ -5,6 +5,7 @@ import style_less from './tab-bar.less?inline' import { type TabData } from '../../util/tab-data' import { fas, type IconName } from '@fortawesome/free-solid-svg-icons' import { findIconDefinition, icon, library } from '@fortawesome/fontawesome-svg-core' +import { aeon_state } from '../../../aeon_events' library.add(fas) @customElement('tab-bar') @@ -15,14 +16,7 @@ class TabBar extends LitElement { switchTab (tabId: number) { return () => { - this.dispatchEvent(new CustomEvent('switch-tab', { - detail: { - tabId - }, - bubbles: true, - composed: true - })) - this.requestUpdate() + aeon_state.tab_bar.active.emitValue(tabId) } } @@ -31,8 +25,7 @@ class TabBar extends LitElement {
${map(this.tabs, (tab) => html` - + +
` } diff --git a/src/html/sketch-editor.html b/src/html/sketch-editor.html index 286c12a..f9b8e37 100644 --- a/src/html/sketch-editor.html +++ b/src/html/sketch-editor.html @@ -1,19 +1,6 @@ - -
- - -
-
- - 0 - -
-
- - -
+
\ No newline at end of file diff --git a/src/html/util/config.ts b/src/html/util/config.ts index 204538d..1142f5d 100644 --- a/src/html/util/config.ts +++ b/src/html/util/config.ts @@ -8,7 +8,8 @@ export const tabList: TabData[] = [ id: index++, name: 'Regulations', data: html``, - icon: 'r' + icon: 'r', + active: true }), TabData.create({ id: index++, diff --git a/src/main.ts b/src/main.ts index 022bb92..4ab549c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,77 +1,5 @@ -import { emit, listen } from '@tauri-apps/api/event' import UIkit from 'uikit'; import Icons from 'uikit/dist/js/uikit-icons'; - - // loads the Icon plugin UIkit.use(Icons) - -window.addEventListener("DOMContentLoaded", async () => { - const counter = document.querySelector("#counter") as HTMLElement; - const counter_up = document.querySelector("#counter-up") as HTMLElement; - const counter_down = document.querySelector("#counter-down") as HTMLElement; - const text = document.querySelector("#text") as HTMLInputElement; - await listen("aeon-state-change", (event) => { - const aeon_path = event.payload.full_path; - const aeon_payload = event.payload.payload; - if(aeon_path.length == 2 && aeon_path[0] == "editor" && aeon_path[1] == "counter") { - counter.innerText = aeon_payload - } - if(aeon_path.length == 2 && aeon_path[0] == "editor" && aeon_path[1] == "text") { - text.value = aeon_payload - } - }); - counter_up.addEventListener("click", () => { - emit("aeon-user-action", { - full_path: ["editor", "counter"], - payload: (parseInt(counter.innerText) + 1).toString() - }); - }); - counter_down.addEventListener("click", () => { - emit("aeon-user-action", { - full_path: ["editor", "counter"], - payload: (parseInt(counter.innerText) - 1).toString() - }); - }); - text.addEventListener("input", () => { - emit("aeon-user-action", { - full_path: ["editor", "text"], - payload: text.value - }); - }); - - const undo = document.querySelector("#undo") as HTMLElement; - undo.addEventListener("click", () => { - emit("undo"); - }); - const redo = document.querySelector("#redo") as HTMLElement; - redo.addEventListener("click", () => { - emit("redo"); - }); - -}); - -// components can be called from the imported UIkit reference -// UIkit.notification('Hello world.'); - -// let greetInputEl: HTMLInputElement | null -// let greetMsgEl: HTMLElement | null - -// async function greet () { -// if (greetMsgEl && greetInputEl) { -// // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -// greetMsgEl.textContent = await invoke('greet', { -// name: greetInputEl.value -// }) -// } -// } -// -// window.addEventListener('DOMContentLoaded', () => { -// greetInputEl = document.querySelector('#greet-input') -// greetMsgEl = document.querySelector('#greet-msg') -// document.querySelector('#greet-form')?.addEventListener('submit', (e) => { -// e.preventDefault() -// await greet() -// }) -// }) From fb55b99a717992ea09f07c5e4b3726dc738942ba Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 6 Dec 2023 19:55:15 +0100 Subject: [PATCH 3/5] Cleanup. --- src/html/component/undo-redo/undo-redo.ts | 2 +- src/html/sketch-editor.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/html/component/undo-redo/undo-redo.ts b/src/html/component/undo-redo/undo-redo.ts index 24ddfef..dd67bb6 100644 --- a/src/html/component/undo-redo/undo-redo.ts +++ b/src/html/component/undo-redo/undo-redo.ts @@ -1,5 +1,5 @@ import { html, css, unsafeCSS, LitElement, type TemplateResult } from 'lit' -import { customElement, property, state } from 'lit/decorators.js' +import { customElement, state } from 'lit/decorators.js' import style_less from './undo-redo.less?inline' import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons' import { findIconDefinition, icon, library } from '@fortawesome/fontawesome-svg-core' diff --git a/src/html/sketch-editor.html b/src/html/sketch-editor.html index f9b8e37..1330cca 100644 --- a/src/html/sketch-editor.html +++ b/src/html/sketch-editor.html @@ -1,6 +1,6 @@ - + \ No newline at end of file From be23fc922078a0be91330a648f79b7075fbd7b3e Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Wed, 6 Dec 2023 23:19:46 +0100 Subject: [PATCH 4/5] Fix lint issues. --- src/aeon_events.ts | 491 +++++++++--------- .../component/content-pane/content-pane.ts | 8 +- .../root-component/root-component.ts | 16 +- src/html/component/tab-bar/tab-bar.ts | 4 +- src/html/component/undo-redo/undo-redo.ts | 19 +- src/main.ts | 4 +- 6 files changed, 275 insertions(+), 267 deletions(-) diff --git a/src/aeon_events.ts b/src/aeon_events.ts index ffcde7a..361f82a 100644 --- a/src/aeon_events.ts +++ b/src/aeon_events.ts @@ -1,5 +1,5 @@ -import { Event, emit, listen } from '@tauri-apps/api/event' -import { dialog, invoke } from '@tauri-apps/api'; +import { type Event, emit, listen } from '@tauri-apps/api/event' +import { dialog, invoke } from '@tauri-apps/api' /* Names of relevant events that communicate with the Tauri backend. */ @@ -7,349 +7,358 @@ const AEON_ACTION = 'aeon-action' const AEON_VALUE = 'aeon-value' const AEON_REFRESH = 'aeon-refresh' - /** * A type-safe representation of the state managed by an Aeon session. - * + * * Under the hood, this uses `AeonEvents` to implement actions and listeners. */ -type AeonState = { +interface AeonState { - /** Access to the internal state of the undo-redo stack. */ - undo_stack: { - /** True if the stack has actions that can be undone. */ - can_undo: ObservableState, - /** True if the stack has actions that can be redone. */ - can_redo: ObservableState, - /** Try to undo an action. Emits an error if no actions can be undone. */ - undo(): void, - /** Try to redo an action. Emits an error if no actions can be redone. */ - redo(): void, - }, + /** Access to the internal state of the undo-redo stack. */ + undo_stack: { + /** True if the stack has actions that can be undone. */ + can_undo: ObservableState + /** True if the stack has actions that can be redone. */ + can_redo: ObservableState + /** Try to undo an action. Emits an error if no actions can be undone. */ + undo: () => void + /** Try to redo an action. Emits an error if no actions can be redone. */ + redo: () => void + } - /** The state of the main navigation tab-bar. */ - tab_bar: { - /** Integer ID of the currently active tab. */ - active: MutableObservableState, - /** A *sorted* list of IDs of all pinned tabs. */ - pinned: MutableObservableState, - /** Pins a single tab if not currently pinned. */ - pin(id: number): void, - /** Unpins a single tab if currently pinned. */ - unpin(id: number): void, - } + /** The state of the main navigation tab-bar. */ + tab_bar: { + /** Integer ID of the currently active tab. */ + active: MutableObservableState + /** A *sorted* list of IDs of all pinned tabs. */ + pinned: MutableObservableState + /** Pins a single tab if not currently pinned. */ + pin: (id: number) => void + /** Unpins a single tab if currently pinned. */ + unpin: (id: number) => void + } } /** A function that is notified when a state value changes. */ -type OnStateValue = (value: T) => void; +type OnStateValue = (value: T) => void /** * An object that can be emitted through `AeonEvents` as a user action. - * - * The `path` property specifies which state item is affected by the event, while + * + * The `path` property specifies which state item is affected by the event, while * the `payload` represents the contents of the event. Multiple events can be - * joined together to create a "compound" event that is treated as + * joined together to create a "compound" event that is treated as * a single user action in the context of the undo-redo stack. */ -type AeonEvent = { - path: string[], - payload: string | null, +interface AeonEvent { + path: string[] + payload: string | null } /** * One item of the observable application state of type `T`. The UI can listen to * the value changes. - * + * * This structure assumes the state is not directly user editable. That is, the user cannot - * directly write value of type `T` into this item. However, it could be still editable + * directly write value of type `T` into this item. However, it could be still editable * indirectly through other items or triggers. */ -class ObservableState { - path: string[] - last_value: T - listeners: OnStateValue[] = [] +class ObservableState { + path: string[] + last_value: T + listeners: Array> = [] - constructor(path: string[], initial: T) { - this.path = path; - this.last_value = initial; - aeon_events.setEventListener(path, this.#acceptPayload.bind(this)); - } + constructor (path: string[], initial: T) { + this.path = path + this.last_value = initial + aeonEvents.setEventListener(path, this.#acceptPayload.bind(this)) + } - /** - * Register a listener to this specific observable state item. - * + /** + * Register a listener to this specific observable state item. + * * The listener is notified right away with the current value of the state item. - * + * * @param listener The listener function which should be invoked when the value * of this state item changes. * @param notifyNow If set to `false`, the listener is not notified with the current * value of the state item upon registering. * @returns `true` if the listener was added, `false` if it was already registered. */ - addEventListener(listener: OnStateValue, notifyNow = true) { - if (this.listeners.includes(listener)) { - return false; - } - this.listeners.push(listener); - if (notifyNow) { - this.#notifyListener(listener, this.last_value); - } - return true; + addEventListener (listener: OnStateValue, notifyNow = true): boolean { + if (this.listeners.includes(listener)) { + return false } + this.listeners.push(listener) + if (notifyNow) { + this.#notifyListener(listener, this.last_value) + } + return true + } - /** + /** * Unregister a listener previously added through `addEventListener`. - * + * * @param listener The listener to be unregistered. * @returns `true` if the listener was removed, `false` if it was not registered. */ - removeEventListener(listener: OnStateValue) { - const index = this.listeners.indexOf(listener); - if (index > -1) { - this.listeners.splice(index, 1); - return true; - } - return false; + removeEventListener (listener: OnStateValue): boolean { + const index = this.listeners.indexOf(listener) + if (index > -1) { + this.listeners.splice(index, 1) + return true } + return false + } - /** + /** * Re-emit the latest state of this item to all event listeners. */ - refresh() { - aeon_events.refresh(this.path); - } + refresh (): void { + aeonEvents.refresh(this.path) + } - /** + /** * The last value that was emitted for this observable state. */ - value(): T { - return this.last_value; - } + value (): T { + return this.last_value + } - /** + /** * Accept a payload value coming from the backend. * @param payload The actual JSON-encoded payload value. */ - #acceptPayload(payload: string | null) { - try { - const value = JSON.parse(payload === null ? "null" : payload); - this.last_value = value; - for (let listener of this.listeners) { - this.#notifyListener(listener, value); - } - } catch (error) { - dialog.message( - `Cannot dispatch event [${this.path}] with payload ${payload}: ${error}`, - { title: "Internal app error", type: "error" } - ); - } + #acceptPayload (payload: string | null): void { + payload = payload ?? 'null' + try { + const value = JSON.parse(payload) + this.last_value = value + for (const listener of this.listeners) { + this.#notifyListener(listener, value) + } + } catch (error) { + const path = JSON.stringify(this.path) + dialog.message( + `Cannot dispatch event ${path} with payload ${payload}: ${String(error)}`, + { title: 'Internal app error', type: 'error' } + ).catch((e) => { + console.error(e) + }) } + } - #notifyListener(listener: OnStateValue, value: T) { - try { - listener(value); - } catch (error) { - dialog.message( - `Cannot handle event [${this.path}] with value ${value}: ${error}`, - { title: "Internal app error", type: "error" } - ); - } + #notifyListener (listener: OnStateValue, value: T): void { + try { + listener(value) + } catch (error) { + const path = JSON.stringify(this.path) + dialog.message( + `Cannot handle event ${path} with value ${String(value)}: ${String(value)}`, + { title: 'Internal app error', type: 'error' } + ).catch((e) => { + console.error(e) + }) } + } } /** * A version of `ObservableState` which supports direct mutation. */ class MutableObservableState extends ObservableState { - constructor(path: string[], initial: T) { - super(path, initial) - } - - /** - * Emit a user action which triggers value mutation on the backend. This should then + /** + * Emit a user action which triggers value mutation on the backend. This should then * result in a new value being observable through the registered event listeners. - * + * * @param value New value. */ - emitValue(value: T) { - aeon_events.emitAction(this.set(value)) - } + emitValue (value: T): void { + aeonEvents.emitAction(this.set(value)) + } - /** + /** * Create a `set` event for the provided `value` and this observable item. Note that the event - * is not emitted automatically, but needs to be sent through the `aeon_events`. You can use - * `this.emitValue` to create and emit the event as one action. - * - * The reason why you might want to create the event but not emit it is that events can be + * is not emitted automatically, but needs to be sent through the `aeon_events`. You can use + * `this.emitValue` to create and emit the event as one action. + * + * The reason why you might want to create the event but not emit it is that events can be * bundled to create complex reversible user actions. - * + * * @param value New value. * @returns An event that can be emitted through `AeonEvents`. */ - set(value: T): AeonEvent { - return { - path: this.path, - payload: JSON.stringify(value) - } + set (value: T): AeonEvent { + return { + path: this.path, + payload: JSON.stringify(value) } + } } /** * A (reasonably) type-safe wrapper around AEON related tauri events that communicate the state * of the application between the frontend and the backend. - * + * * Each event path only supports one event listener. If you need more listenerers, you likely * want to wrap the path into an `ObservableState` object. */ class AeonEvents { - /** Uniquely identifies one "session". Multiple windows can share a session. */ - session_id: string - listeners: any + /** Uniquely identifies one "session". Multiple windows can share a session. */ + sessionId: string + listeners: any - constructor(session_id: string) { - this.session_id = session_id; - this.listeners = {} - listen(AEON_VALUE, this.#onStateChange.bind(this)) - } + constructor (sessionId: string) { + this.sessionId = sessionId + this.listeners = {} + listen(AEON_VALUE, this.#onStateChange.bind(this)).catch((e) => { + console.error(e) + }) + } - /** Request a retransmission of state data managed at the provided path. */ - refresh(path: string[]) { - emit(AEON_REFRESH, { - session: this.session_id, - path: path, - }).catch((error) => { - dialog.message( - `Cannot refresh [${path}]: ${error}`, - { title: "Internal app error", type: "error" } - ); - }); - } + /** Request a retransmission of state data managed at the provided path. */ + refresh (path: string[]): void { + emit(AEON_REFRESH, { + session: this.sessionId, + path + }).catch((error) => { + dialog.message( + `Cannot refresh [${JSON.stringify(path)}]: ${String(error)}`, + { title: 'Internal app error', type: 'error' } + ).catch((e) => { + console.error(e) + }) + }) + } - /** - * Emit one or more aeon events as a single user action. - * - * Assuming all events are reversible, the item will be saved as a single undo-redo entry. + /** + * Emit one or more aeon events as a single user action. + * + * Assuming all events are reversible, the item will be saved as a single undo-redo entry. */ - emitAction(events: AeonEvent | AeonEvent[]) { - if (!(events instanceof Array)) { - events = [events] - } - emit(AEON_ACTION, { - session: this.session_id, - events: events - }).catch((error) => { - dialog.message( - `Cannot process events [${events}]: ${error}`, - { title: "Internal app error", type: "error" } - ); - }); + emitAction (events: AeonEvent | AeonEvent[]): void { + if (!(events instanceof Array)) { + events = [events] } + emit(AEON_ACTION, { + session: this.sessionId, + events + }).catch((error) => { + dialog.message( + `Cannot process events [${JSON.stringify(events)}]: ${error}`, + { title: 'Internal app error', type: 'error' } + ).catch((e) => { + console.error(e) + }) + }) + } - /** + /** * Set an event listener that will be notified when the specified path changes. - * + * * Note that only one listener is supported for each path. If you want more listeners, * consider the `ObservableState` class. */ - setEventListener( - path: string[], - listener: (payload: string | null) => void - ) { - let listeners = this.listeners - while (path.length > 1) { - let key = path[0] - path = path.slice(1) - if (!(key in listeners)) { - listeners[key] = {} - } - listeners = listeners[key] - } - listeners[path[0]] = listener; + setEventListener ( + path: string[], + listener: (payload: string | null) => void + ): void { + let listeners = this.listeners + while (path.length > 1) { + const key = path[0] + path = path.slice(1) + if (!(key in listeners)) { + listeners[key] = {} + } + listeners = listeners[key] } + listeners[path[0]] = listener + } - /** - * React to (possibly multiple) value changes. - * + /** + * React to (possibly multiple) value changes. + * * Note that if multiple values changed, the listeners are notified * in sequence. */ - #onStateChange(event: Event) { - for (let e of event.payload) { - this.#notifyValueChange(e) - } + #onStateChange (event: Event): void { + for (const e of event.payload) { + this.#notifyValueChange(e) } + } - #notifyValueChange(event: AeonEvent) { - // Find listener residing at the specified path, or return if no listener exists. - let listener = this.listeners; - let path = event.path; - while (path.length > 0) { - let key = path[0] - path = path.slice(1) - if (!(key in listener)) { - console.log("Event ignored.", event.path); - return; - } - listener = listener[key]; - } - - // Emit event. - try { - listener(event.payload); - } catch (error) { - dialog.message( - `Cannot handle event [${event.path}] with payload ${event.payload}: ${error}`, - { title: "Internal app error", type: "error" } - ); - } + #notifyValueChange (event: AeonEvent): void { + // Find listener residing at the specified path, or return if no listener exists. + let listener = this.listeners + let path = event.path + while (path.length > 0) { + const key = path[0] + path = path.slice(1) + if (!(key in listener)) { + console.log('Event ignored.', event.path) + return + } + listener = listener[key] } + // Emit event. + try { + listener(event.payload) + } catch (error) { + dialog.message( + `Cannot handle event [${JSON.stringify(event.path)}] with payload ${String(event.payload)}: ${String(error)}`, + { title: 'Internal app error', type: 'error' } + ).catch((e) => { + console.error(e) + }) + } + } } /** * A singleton object which implements access to Tauri events. */ -export let aeon_events = new AeonEvents(await invoke('get_session_id')); +export const aeonEvents = new AeonEvents(await invoke('get_session_id')) /** * A singleton state management object for the current Aeon session. */ -export let aeon_state: AeonState = { - undo_stack: { - can_undo: new ObservableState(["undo_stack", "can_undo"], false), - can_redo: new ObservableState(["undo_stack", "can_redo"], false), - undo() { - aeon_events.emitAction({ - path: ["undo_stack", "undo"], - payload: null, - }) - }, - redo() { - aeon_events.emitAction({ - path: ["undo_stack", "redo"], - payload: null, - }) - }, - }, - tab_bar: { - active: new MutableObservableState(["tab_bar", "active"], 0), - pinned: new MutableObservableState(["tab_bar", "pinned"], []), - pin (id: number): void { - const value = this.pinned.value(); - if (!value.includes(id)) { - value.push(id); - value.sort(); - this.pinned.emitValue(value); - } - }, - unpin (id: number): void { - const value = this.pinned.value(); - const index = value.indexOf(id); - if (index > -1) { - value.splice(index, 1); - this.pinned.emitValue(value); - } - } +export const aeonState: AeonState = { + undo_stack: { + can_undo: new ObservableState(['undo_stack', 'can_undo'], false), + can_redo: new ObservableState(['undo_stack', 'can_redo'], false), + undo () { + aeonEvents.emitAction({ + path: ['undo_stack', 'undo'], + payload: null + }) + }, + redo () { + aeonEvents.emitAction({ + path: ['undo_stack', 'redo'], + payload: null + }) } -}; \ No newline at end of file + }, + tab_bar: { + active: new MutableObservableState(['tab_bar', 'active'], 0), + pinned: new MutableObservableState(['tab_bar', 'pinned'], []), + pin (id: number): void { + const value = this.pinned.value() + if (!value.includes(id)) { + value.push(id) + value.sort((a, b) => a - b) + this.pinned.emitValue(value) + } + }, + unpin (id: number): void { + const value = this.pinned.value() + const index = value.indexOf(id) + if (index > -1) { + value.splice(index, 1) + this.pinned.emitValue(value) + } + } + } +} diff --git a/src/html/component/content-pane/content-pane.ts b/src/html/component/content-pane/content-pane.ts index ea538dc..4571e58 100644 --- a/src/html/component/content-pane/content-pane.ts +++ b/src/html/component/content-pane/content-pane.ts @@ -5,7 +5,7 @@ import { type TabData } from '../../util/tab-data' import { library, icon, findIconDefinition } from '@fortawesome/fontawesome-svg-core' import '../regulations-editor/regulations-editor' import { faLock, faLockOpen } from '@fortawesome/free-solid-svg-icons' -import { aeon_state } from '../../../aeon_events' +import { aeonState } from '../../../aeon_events' library.add(faLock, faLockOpen) @customElement('content-pane') @@ -16,10 +16,10 @@ export class ContentPane extends LitElement { private pin (): void { if (this.tab.pinned) { - aeon_state.tab_bar.unpin(this.tab.id) + aeonState.tab_bar.unpin(this.tab.id) } else { - aeon_state.tab_bar.pin(this.tab.id) - } + aeonState.tab_bar.pin(this.tab.id) + } } protected render (): TemplateResult { diff --git a/src/html/component/root-component/root-component.ts b/src/html/component/root-component/root-component.ts index 565e611..33e272a 100644 --- a/src/html/component/root-component/root-component.ts +++ b/src/html/component/root-component/root-component.ts @@ -6,29 +6,27 @@ import '../content-pane/content-pane' import '../nav-bar/nav-bar' import { type TabData } from '../../util/tab-data' import { tabList } from '../../util/config' -import { aeon_state } from '../../../aeon_events' - -const SAVE_KEY = 'tab-data' +import { aeonState } from '../../../aeon_events' @customElement('root-component') class RootComponent extends LitElement { static styles = css`${unsafeCSS(style_less)}` @state() tabs: TabData[] = tabList constructor () { - super() - aeon_state.tab_bar.active.addEventListener(this.#onSwitched.bind(this)) - aeon_state.tab_bar.pinned.addEventListener(this.#onPinned.bind(this)) + super() + aeonState.tab_bar.active.addEventListener(this.#onSwitched.bind(this)) + aeonState.tab_bar.pinned.addEventListener(this.#onPinned.bind(this)) } - #onPinned (pinned: number[]): void { - this.tabs = this.tabs.map((tab) => + #onPinned (pinned: number[]): void { + this.tabs = this.tabs.map((tab) => tab.copy({ pinned: pinned.includes(tab.id) }) ) } - #onSwitched (tabId: number) { + #onSwitched (tabId: number): void { this.tabs = this.tabs.map((tab) => tab.copy({ active: tab.id === tabId diff --git a/src/html/component/tab-bar/tab-bar.ts b/src/html/component/tab-bar/tab-bar.ts index 16b859b..8154877 100644 --- a/src/html/component/tab-bar/tab-bar.ts +++ b/src/html/component/tab-bar/tab-bar.ts @@ -5,7 +5,7 @@ import style_less from './tab-bar.less?inline' import { type TabData } from '../../util/tab-data' import { fas, type IconName } from '@fortawesome/free-solid-svg-icons' import { findIconDefinition, icon, library } from '@fortawesome/fontawesome-svg-core' -import { aeon_state } from '../../../aeon_events' +import { aeonState } from '../../../aeon_events' library.add(fas) @customElement('tab-bar') @@ -16,7 +16,7 @@ class TabBar extends LitElement { switchTab (tabId: number) { return () => { - aeon_state.tab_bar.active.emitValue(tabId) + aeonState.tab_bar.active.emitValue(tabId) } } diff --git a/src/html/component/undo-redo/undo-redo.ts b/src/html/component/undo-redo/undo-redo.ts index dd67bb6..bb3529b 100644 --- a/src/html/component/undo-redo/undo-redo.ts +++ b/src/html/component/undo-redo/undo-redo.ts @@ -3,7 +3,7 @@ import { customElement, state } from 'lit/decorators.js' import style_less from './undo-redo.less?inline' import { faArrowLeft, faArrowRight } from '@fortawesome/free-solid-svg-icons' import { findIconDefinition, icon, library } from '@fortawesome/fontawesome-svg-core' -import { aeon_state } from '../../../aeon_events' +import { aeonState } from '../../../aeon_events' library.add(faArrowLeft, faArrowRight) @customElement('undo-redo') @@ -12,16 +12,17 @@ class UndoRedo extends LitElement { @state() private can_undo: boolean = false + @state() private can_redo: boolean = false - - constructor() { + + constructor () { super() - aeon_state.undo_stack.can_undo.addEventListener((it) => { - this.can_undo = it; + aeonState.undo_stack.can_undo.addEventListener((it) => { + this.can_undo = it }) - aeon_state.undo_stack.can_redo.addEventListener((it) => { - this.can_redo = it; + aeonState.undo_stack.can_redo.addEventListener((it) => { + this.can_redo = it }) } @@ -29,9 +30,9 @@ class UndoRedo extends LitElement { return html`
+ @click=${aeonState.undo_stack.undo} ?disabled=${!this.can_undo}>${icon(findIconDefinition({ prefix: 'fas', iconName: 'arrow-left' })).node} + @click=${aeonState.undo_stack.redo} ?disabled=${!this.can_redo}>${icon(findIconDefinition({ prefix: 'fas', iconName: 'arrow-right' })).node}
` } diff --git a/src/main.ts b/src/main.ts index 4ab549c..81a9233 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ -import UIkit from 'uikit'; -import Icons from 'uikit/dist/js/uikit-icons'; +import UIkit from 'uikit' +import Icons from 'uikit/dist/js/uikit-icons' // loads the Icon plugin UIkit.use(Icons) From d071b53d36e78865db15bf1803ececeda007d2ae Mon Sep 17 00:00:00 2001 From: Samuel Pastva Date: Fri, 8 Dec 2023 13:53:47 +0100 Subject: [PATCH 5/5] Add helper methods for path checking in events. --- .../app/state/editor/_state_editor_session.rs | 32 +++++++------- .../src/app/state/editor/_state_tab_bar.rs | 34 +++++++-------- src-tauri/src/app/state/mod.rs | 42 ++++++++++++++++++- 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/src-tauri/src/app/state/editor/_state_editor_session.rs b/src-tauri/src/app/state/editor/_state_editor_session.rs index e30a646..ac18294 100644 --- a/src-tauri/src/app/state/editor/_state_editor_session.rs +++ b/src-tauri/src/app/state/editor/_state_editor_session.rs @@ -1,7 +1,7 @@ use crate::app::event::{Event, StateChange, UserAction}; use crate::app::state::_undo_stack::UndoStack; use crate::app::state::editor::TabBarState; -use crate::app::state::{Consumed, Session, SessionState}; +use crate::app::state::{Consumed, Session, SessionHelper, SessionState}; use crate::app::{AeonError, DynError}; use crate::debug; @@ -179,28 +179,26 @@ impl Session for EditorSession { } } +impl SessionHelper for EditorSession {} + impl SessionState for EditorSession { fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result { - if at_path.is_empty() { - return AeonError::throw("`EditorSession` cannot process an empty path."); - } - - match at_path[0] { - "undo_stack" => self.undo_stack.perform_event(event, &at_path[1..]), - "tab_bar" => self.tab_bar.perform_event(event, &at_path[1..]), - it => AeonError::throw(format!("Unknown path in `EditorSession`: `{}`", it)), + if let Some(at_path) = Self::starts_with("undo_stack", at_path) { + self.undo_stack.perform_event(event, at_path) + } else if let Some(at_path) = Self::starts_with("tab_bar", at_path) { + self.tab_bar.perform_event(event, at_path) + } else { + Self::invalid_path_error(at_path) } } fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { - if at_path.is_empty() { - return AeonError::throw("`EditorSession` cannot process an empty path."); - } - - match at_path[0] { - "undo_stack" => self.undo_stack.refresh(full_path, &at_path[1..]), - "tab_bar" => self.tab_bar.refresh(full_path, &at_path[1..]), - it => AeonError::throw(format!("Unknown path in `EditorSession`: `{}`", it)), + if let Some(at_path) = Self::starts_with("undo_stack", at_path) { + self.undo_stack.refresh(full_path, at_path) + } else if let Some(at_path) = Self::starts_with("tab_bar", at_path) { + self.tab_bar.refresh(full_path, at_path) + } else { + Self::invalid_path_error(at_path) } } } diff --git a/src-tauri/src/app/state/editor/_state_tab_bar.rs b/src-tauri/src/app/state/editor/_state_tab_bar.rs index 5e1f4ae..8076781 100644 --- a/src-tauri/src/app/state/editor/_state_tab_bar.rs +++ b/src-tauri/src/app/state/editor/_state_tab_bar.rs @@ -1,6 +1,6 @@ use crate::app::event::Event; -use crate::app::state::{AtomicState, Consumed, SessionState}; -use crate::app::{AeonError, DynError}; +use crate::app::state::{AtomicState, Consumed, SessionHelper, SessionState}; +use crate::app::DynError; #[derive(Default)] pub struct TabBarState { @@ -8,28 +8,26 @@ pub struct TabBarState { pinned: AtomicState>, } +impl SessionHelper for TabBarState {} + impl SessionState for TabBarState { fn perform_event(&mut self, event: &Event, at_path: &[&str]) -> Result { - if at_path.is_empty() { - return AeonError::throw("`TabBar` cannot process an empty path."); - } - - match at_path[0] { - "active" => self.active.perform_event(event, &at_path[1..]), - "pinned" => self.pinned.perform_event(event, &at_path[1..]), - it => AeonError::throw(format!("Unknown path in `TabBar`: `{}`", it)), + if let Some(at_path) = Self::starts_with("active", at_path) { + self.active.perform_event(event, at_path) + } else if let Some(at_path) = Self::starts_with("pinned", at_path) { + self.pinned.perform_event(event, at_path) + } else { + Self::invalid_path_error(at_path) } } fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result { - if at_path.is_empty() { - return AeonError::throw("`TabBar` cannot process an empty path."); - } - - match at_path[0] { - "active" => self.active.refresh(full_path, &at_path[1..]), - "pinned" => self.pinned.refresh(full_path, &at_path[1..]), - it => AeonError::throw(format!("Unknown path in `TabBar`: `{}`", it)), + if let Some(at_path) = Self::starts_with("active", at_path) { + self.active.refresh(full_path, at_path) + } else if let Some(at_path) = Self::starts_with("pinned", at_path) { + self.pinned.refresh(full_path, at_path) + } else { + Self::invalid_path_error(at_path) } } } diff --git a/src-tauri/src/app/state/mod.rs b/src-tauri/src/app/state/mod.rs index 58b9866..7fcb6f5 100644 --- a/src-tauri/src/app/state/mod.rs +++ b/src-tauri/src/app/state/mod.rs @@ -1,5 +1,5 @@ use crate::app::event::{Event, StateChange, UserAction}; -use crate::app::DynError; +use crate::app::{AeonError, DynError}; mod _consumed; mod _state_app; @@ -27,6 +27,46 @@ pub trait SessionState { fn refresh(&self, full_path: &[String], at_path: &[&str]) -> Result; } +trait SessionHelper { + /// A utility function which checks if `at_path` starts with a specific first segment. + /// If yes, returns the remaining part of the path. + fn starts_with<'a, 'b>(prefix: &str, at_path: &'a [&'b str]) -> Option<&'a [&'b str]> { + if let Some(x) = at_path.get(0) { + if x == &prefix { + Some(&at_path[1..]) + } else { + None + } + } else { + None + } + } + + /// A utility function which checks if `at_path` is exactly + fn matches(expected: &[&str], at_path: &[&str]) -> bool { + if expected.len() != at_path.len() { + return false; + } + + for (a, b) in expected.iter().zip(at_path) { + if a != b { + return false; + } + } + + true + } + + /// A utility function which emits a generic "invalid path" error. + fn invalid_path_error(at_path: &[&str]) -> Result { + AeonError::throw(format!( + "`{}` cannot process path `{:?}`.", + std::any::type_name::(), + at_path + )) + } +} + pub trait Session: SessionState { /// Perform a user action on this session state object. This usually involves propagating /// the events to the internal [SessionState] objects and collecting the results into a