refactor: app event bus (#6985)

* refactor: app event bus

* fix: use messagepack + cleanup

* fix: use postcard

* fix: lint

* fix: import gate

* fix: prettierignore + lint

* fix: lint

* fix: rev
This commit is contained in:
Calum H.
2026-08-13 16:33:06 +00:00
committed by GitHub
parent 5f7493491f
commit 579109a478
93 changed files with 2669 additions and 834 deletions
+42 -79
View File
@@ -1,16 +1,16 @@
use super::{FriendPayload, LoadingBarId};
#[cfg(feature = "tauri")]
use crate::event::{
AppEvent, InstancePayload, LoadingPayload, ProcessPayload, WarningPayload,
};
use crate::event::{
CommandPayload, EventError, InstanceBulkUpdateProgressPayload,
InstancePayloadType, LoadingBar, LoadingBarType, ProcessPayloadType,
};
#[cfg(feature = "tauri")]
use crate::event::{
InstancePayload, LoadingPayload, ProcessPayload, WarningPayload,
};
use futures::prelude::*;
use serde_json::Value;
#[cfg(feature = "tauri")]
use tauri::{Emitter, Manager};
use tauri::Manager;
use uuid::Uuid;
#[cfg(feature = "cli")]
@@ -61,7 +61,7 @@ pub async fn init_loading_unsafe(
total: f64,
title: &str,
) -> crate::Result<LoadingBarId> {
let event_state = crate::EventState::get()?;
let event_state = crate::EventState::get();
let key = LoadingBarId(Uuid::new_v4());
event_state.loading_bars.insert(
@@ -105,7 +105,7 @@ pub fn emit_loading(
increment_frac: f64,
message: Option<&str>,
) -> crate::Result<()> {
let event_state = crate::EventState::get()?;
let event_state = crate::EventState::get();
let Some(mut loading_bar) = event_state.loading_bars.get_mut(&key.0) else {
return Err(EventError::NoLoadingBar(key.0).into());
@@ -131,25 +131,17 @@ pub fn emit_loading(
//Emit event to tauri
#[cfg(feature = "tauri")]
event_state
.app
.emit(
"loading",
LoadingPayload {
fraction: if display_frac >= 1.0 {
None // by convention, when its done, we submit None
// any further updates will be ignored (also sending None)
} else {
Some(display_frac)
},
message: message
.unwrap_or(&loading_bar.message)
.to_string(),
event: loading_bar.bar_type.clone(),
loader_uuid: loading_bar.loading_bar_uuid,
},
)
.map_err(EventError::from)?;
event_state.send(AppEvent::Loading(LoadingPayload {
fraction: if display_frac >= 1.0 {
None // by convention, when its done, we submit None
// any further updates will be ignored (also sending None)
} else {
Some(display_frac)
},
message: message.unwrap_or(&loading_bar.message).to_string(),
event: loading_bar.bar_type.clone(),
loader_uuid: loading_bar.loading_bar_uuid.to_string(),
}))?;
#[cfg(not(any(feature = "cli", feature = "tauri")))]
let _ = message;
@@ -164,16 +156,10 @@ pub fn emit_loading(
pub async fn emit_warning(message: &str) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"warning",
WarningPayload {
message: message.to_string(),
},
)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::Warning(WarningPayload {
message: message.to_string(),
}))?;
}
tracing::warn!("{}", message);
Ok(())
@@ -185,11 +171,8 @@ pub async fn emit_instance_bulk_update_progress(
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("instance_bulk_update_progress", payload)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::InstanceBulkUpdateProgress(payload))?;
}
Ok(())
}
@@ -201,11 +184,8 @@ pub async fn emit_command(command: CommandPayload) -> crate::Result<()> {
tracing::debug!("Command: {}", serde_json::to_string(&command)?);
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("command", command)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::Command(command))?;
if let Some(window) = event_state.app.get_window("main") {
let _ = window.set_focus();
@@ -224,19 +204,13 @@ pub async fn emit_process(
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"process",
ProcessPayload {
instance_id: instance_id.to_string(),
uuid,
event,
message: message.to_string(),
},
)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::Process(ProcessPayload {
instance_id: instance_id.to_string(),
uuid: uuid.to_string(),
event,
message: message.to_string(),
}))?;
}
Ok(())
}
@@ -249,17 +223,11 @@ pub async fn emit_instance(
) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit(
"instance",
InstancePayload {
instance_id: instance_id.to_string(),
event,
},
)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::Instance(InstancePayload {
instance_id: instance_id.to_string(),
event,
}))?;
}
Ok(())
}
@@ -268,11 +236,8 @@ pub async fn emit_instance(
pub async fn emit_friend(payload: FriendPayload) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
event_state
.app
.emit("friend", payload)
.map_err(EventError::from)?;
let event_state = crate::EventState::get();
event_state.send(AppEvent::Friend(payload))?;
}
Ok(())
@@ -282,11 +247,9 @@ pub async fn emit_friend(payload: FriendPayload) -> crate::Result<()> {
pub async fn emit_notification(payload: Value) -> crate::Result<()> {
#[cfg(feature = "tauri")]
{
let event_state = crate::EventState::get()?;
let event_state = crate::EventState::get();
event_state
.app
.emit("notification", payload)
.map_err(EventError::from)?;
.send(AppEvent::Notification(serde_json::to_string(&payload)?))?;
}
Ok(())
+299 -81
View File
@@ -1,15 +1,18 @@
//! Theseus state management system
use ariadne::ids::UserId;
use ariadne::users::UserStatus;
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::{path::PathBuf, sync::Arc};
#[cfg(feature = "tauri")]
use tauri::Emitter;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
#[cfg(feature = "export-ts")]
use std::path::PathBuf;
use std::sync::Arc;
#[cfg(feature = "tauri")]
use tauri::ipc::{Channel, InvokeResponseBody};
use tokio::sync::OnceCell;
use uuid::Uuid;
use crate::install::InstallJobSnapshot;
pub mod emit;
// Global event state
@@ -19,21 +22,30 @@ pub struct EventState {
/// Tauri app
#[cfg(feature = "tauri")]
pub app: tauri::AppHandle,
#[cfg(feature = "tauri")]
event_channel: RwLock<Channel<InvokeResponseBody>>,
pub loading_bars: DashMap<Uuid, LoadingBar>,
}
impl EventState {
#[cfg(feature = "tauri")]
pub async fn init(app: tauri::AppHandle) -> crate::Result<Arc<Self>> {
EVENT_STATE
pub async fn init(
app: tauri::AppHandle,
event_channel: Channel<InvokeResponseBody>,
) -> crate::Result<Arc<Self>> {
let state = EVENT_STATE
.get_or_try_init(|| async {
Ok(Arc::new(Self {
Ok::<_, crate::Error>(Arc::new(Self {
app,
event_channel: RwLock::new(event_channel.clone()),
loading_bars: DashMap::new(),
}))
})
.await
.cloned()
.cloned()?;
*state.event_channel.write() = event_channel;
Ok(state)
}
#[cfg(not(feature = "tauri"))]
@@ -48,14 +60,28 @@ impl EventState {
.cloned()
}
pub fn get() -> crate::Result<Arc<Self>> {
Ok(EVENT_STATE.get().ok_or(EventError::NotInitialized)?.clone())
pub fn get() -> Arc<Self> {
EVENT_STATE
.get()
.expect("should be initialized when used")
.clone()
}
#[cfg(feature = "tauri")]
pub fn send(&self, event: AppEvent) -> crate::Result<()> {
let payload =
postcard::to_allocvec(&event).map_err(EventError::from)?;
self.event_channel
.read()
.send(InvokeResponseBody::Raw(payload))
.map_err(EventError::from)?;
Ok(())
}
// Values provided should not be used directly, as they are clones and are not guaranteed to be up-to-date
pub async fn list_progress_bars() -> crate::Result<DashMap<Uuid, LoadingBar>>
{
let value = Self::get()?;
let value = Self::get();
Ok(value.loading_bars.clone())
}
@@ -63,11 +89,122 @@ impl EventState {
pub async fn get_main_window() -> crate::Result<Option<tauri::WebviewWindow>>
{
use tauri::Manager;
let value = Self::get()?;
let value = Self::get();
Ok(value.app.get_webview_window("main"))
}
}
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
#[cfg_attr(
feature = "export-ts",
ts(
tag = "type",
content = "payload",
rename_all = "snake_case",
export_to = "AppEvent.ts"
)
)]
pub enum AppEvent {
Loading(LoadingPayload),
Process(ProcessPayload),
Instance(InstancePayload),
InstanceBulkUpdateProgress(InstanceBulkUpdateProgressPayload),
InstallJob(std::sync::Arc<InstallJobSnapshot>),
Command(CommandPayload),
Warning(WarningPayload),
Friend(FriendPayload),
Notification(
#[cfg_attr(feature = "export-ts", ts(type = "unknown"))] String,
),
Log(LogPayload),
AdsConsentRequired(bool),
}
#[cfg(feature = "export-ts")]
pub fn export_app_event_bindings(
output: impl Into<PathBuf>,
) -> Result<(), Box<dyn std::error::Error>> {
use postcard_bindgen::{PackageInfo, generate_bindings, javascript};
use ts_rs::{Config, TS};
let output = output.into();
let config = Config::default()
.with_out_dir(output.clone())
.with_large_int("number");
AppEvent::export_all(&config)?;
javascript::build_package(
&output,
PackageInfo {
name: "postcard".into(),
version: "0.0.0".try_into()?,
},
javascript::GenerationSettings::default()
.type_script_types(true)
.module_structure(false)
.esm_module(true),
generate_bindings!(
AppEvent,
LoadingBarType,
LoadingPayload,
WarningPayload,
InstanceBulkUpdateProgressPayload,
InstanceBulkUpdateProgressStage,
CommandPayload,
ProcessPayload,
ProcessPayloadType,
InstancePayload,
InstancePayloadType,
FriendPayload,
FriendStatusPayload,
LogEvent,
LogPayload,
crate::state::Log4jEvent,
crate::install::InstallJobSnapshot,
crate::install::InstallJobKind,
crate::install::InstallJobStatus,
crate::install::model::InstallTarget,
crate::install::InstallPhaseId,
crate::install::InstallProgress,
crate::install::InstallProgressSecondary,
crate::install::InstallPhaseDetails,
crate::install::InstallJavaStep,
crate::install::model::InstallJobDisplay,
crate::install::InstallErrorView,
crate::install::model::InstallApiErrorDetails,
crate::install::InstallErrorContext,
crate::api::pack::import::ImportLauncherType,
crate::state::ModLoader,
crate::SharedInstanceUnavailableReason
),
)?;
fix_postcard_javascript_utf8(&output.join("postcard/index.js"))?;
Ok(())
}
#[cfg(feature = "export-ts")]
fn fix_postcard_javascript_utf8(
output: &std::path::Path,
) -> Result<(), Box<dyn std::error::Error>> {
const GENERATED: &str = "deserialize_string = () => { const str = this.pop_n(Number(this.try_take(U32_BYTES))); return String.fromCharCode(...str) }";
const UTF8: &str = "deserialize_string = () => new TextDecoder().decode(new Uint8Array(this.pop_n(Number(this.try_take(U32_BYTES)))))";
let javascript = std::fs::read_to_string(output)?;
if !javascript.contains(GENERATED) {
return Err(
"postcard-bindgen's generated string decoder changed".into()
);
}
std::fs::write(output, javascript.replace(GENERATED, UTF8))?;
Ok(())
}
#[derive(Serialize, Debug, Clone)]
pub struct LoadingBar {
// loading_bar_uuid not be used directly by external functions as it may not reflect the current state of the loading bar/hashmap
@@ -91,55 +228,58 @@ impl Drop for LoadingBarId {
fn drop(&mut self) {
let loader_uuid = self.0;
tokio::spawn(async move {
if let Ok(event_state) = EventState::get() {
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some((_, bar)) =
event_state.loading_bars.remove(&loader_uuid)
let event_state = EventState::get();
#[cfg(any(feature = "tauri", feature = "cli"))]
if let Some((_, bar)) =
event_state.loading_bars.remove(&loader_uuid)
{
#[cfg(feature = "tauri")]
{
#[cfg(feature = "tauri")]
{
let loader_uuid = bar.loading_bar_uuid;
let event = bar.bar_type.clone();
let fraction = bar.current / bar.total;
let loader_uuid = bar.loading_bar_uuid;
let event = bar.bar_type.clone();
let fraction = bar.current / bar.total;
let _ = event_state.app.emit(
"loading",
LoadingPayload {
fraction: None,
message: "Completed".to_string(),
event,
loader_uuid,
},
);
tracing::trace!(
"Exited at {fraction} for loading bar: {:?}",
loader_uuid
);
}
// Emit event to indicatif progress bar arc
#[cfg(feature = "cli")]
{
let cli_progress_bar = bar.cli_progress_bar;
cli_progress_bar.finish();
}
let _ =
event_state.send(AppEvent::Loading(LoadingPayload {
fraction: None,
message: "Completed".to_string(),
event,
loader_uuid: loader_uuid.to_string(),
}));
tracing::trace!(
"Exited at {fraction} for loading bar: {:?}",
loader_uuid
);
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
event_state.loading_bars.remove(&loader_uuid);
// Emit event to indicatif progress bar arc
#[cfg(feature = "cli")]
{
let cli_progress_bar = bar.cli_progress_bar;
cli_progress_bar.finish();
}
}
#[cfg(not(any(feature = "tauri", feature = "cli")))]
event_state.loading_bars.remove(&loader_uuid);
});
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)]
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(tag = "type")]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "export-ts", ts(tag = "type", rename_all = "snake_case"))]
pub enum LoadingBarType {
LegacyDataMigration,
DirectoryMove {
old: PathBuf,
new: PathBuf,
old: String,
new: String,
},
JavaDownload {
version: u32,
@@ -153,7 +293,7 @@ pub enum LoadingBarType {
PackDownload {
instance_id: String,
pack_name: String,
icon: Option<PathBuf>,
icon: Option<String>,
pack_id: Option<String>,
pack_version: Option<String>,
},
@@ -173,10 +313,10 @@ pub enum LoadingBarType {
instance_name: String,
},
ConfigChange {
new_path: PathBuf,
new_path: String,
},
CopyInstance {
import_location: PathBuf,
import_location: String,
instance_name: String,
},
LauncherUpdate {
@@ -185,22 +325,32 @@ pub enum LoadingBarType {
},
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
pub struct LoadingPayload {
pub event: LoadingBarType,
pub loader_uuid: Uuid,
pub loader_uuid: String,
pub fraction: Option<f64>, // by convention, if optional, it means the loading is done
pub message: String,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
pub struct WarningPayload {
pub message: String,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde(rename_all = "camelCase")]
pub struct InstanceBulkUpdateProgressPayload {
pub instance_id: String,
@@ -209,7 +359,11 @@ pub struct InstanceBulkUpdateProgressPayload {
pub total: usize,
}
#[derive(Serialize, Clone)]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde(rename_all = "snake_case")]
pub enum InstanceBulkUpdateProgressStage {
ResolvingVersions,
@@ -217,8 +371,14 @@ pub enum InstanceBulkUpdateProgressStage {
Finishing,
}
#[derive(Serialize, Clone)]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(tag = "event")]
#[cfg_attr(feature = "export-ts", ts(tag = "event"))]
pub enum CommandPayload {
InstallMod {
id: String,
@@ -242,36 +402,54 @@ pub enum CommandPayload {
},
RunMRPack {
// run or install .mrpack
path: PathBuf,
path: String,
},
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
pub struct ProcessPayload {
pub instance_id: String,
pub uuid: Uuid,
pub uuid: String,
pub event: ProcessPayloadType,
pub message: String,
}
#[derive(Serialize, Clone, Debug)]
#[derive(Serialize, Deserialize, Clone, Debug)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde(rename_all = "snake_case")]
pub enum ProcessPayloadType {
Launched,
Finished,
}
#[derive(Serialize, Clone)]
#[cfg(feature = "tauri")]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
pub struct InstancePayload {
pub instance_id: String,
#[serde(flatten)]
#[cfg_attr(feature = "export-ts", ts(flatten))]
pub event: InstancePayloadType,
}
#[derive(Serialize, Clone)]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(tag = "event", rename_all = "snake_case")]
#[cfg_attr(feature = "export-ts", ts(tag = "event", rename_all = "snake_case"))]
pub enum InstancePayloadType {
Created,
Synced,
@@ -282,7 +460,7 @@ pub enum InstancePayloadType {
ServerJoined {
host: String,
port: u16,
timestamp: DateTime<Utc>,
timestamp: String,
},
Edited,
ContentInstallFinished {
@@ -295,47 +473,87 @@ pub enum InstancePayloadType {
Removed,
}
#[derive(Serialize, Clone)]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(rename_all = "snake_case")]
#[serde(tag = "event")]
#[cfg_attr(feature = "export-ts", ts(tag = "event", rename_all = "snake_case"))]
pub enum FriendPayload {
FriendRequest { from: UserId },
UserOffline { id: UserId },
StatusUpdate { user_status: UserStatus },
FriendRequest { from: String },
UserOffline { id: String },
StatusUpdate { user_status: FriendStatusPayload },
StatusSync,
}
#[cfg(feature = "tauri")]
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
pub struct FriendStatusPayload {
pub user_id: String,
pub profile_name: Option<String>,
pub last_update: String,
}
impl From<ariadne::users::UserStatus> for FriendStatusPayload {
fn from(status: ariadne::users::UserStatus) -> Self {
Self {
user_id: status.user_id.to_string(),
profile_name: status.profile_name,
last_update: status.last_update.to_rfc3339(),
}
}
}
pub use self::log_types::*;
#[cfg(feature = "tauri")]
mod log_types {
use crate::state::Log4jEvent;
use serde::Serialize;
#[derive(Serialize, Clone)]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(
feature = "export-ts",
ts(tag = "type", rename_all = "snake_case")
)]
pub enum LogEvent {
Log4j(Log4jEvent),
Legacy { message: String },
}
#[derive(Serialize, Clone)]
#[derive(Clone)]
#[cfg_attr(
feature = "export-ts",
derive(ts_rs::TS, postcard_bindgen::PostcardBindings)
)]
#[serde_binhum::serde_binhum]
pub struct LogPayload {
pub instance_id: String,
#[serde(flatten)]
#[cfg_attr(feature = "export-ts", ts(flatten))]
pub event: LogEvent,
}
}
#[derive(Debug, thiserror::Error)]
pub enum EventError {
#[error("Event state was not properly initialized")]
NotInitialized,
#[error("Non-existent loading bar of key: {0}")]
NoLoadingBar(Uuid),
#[cfg(feature = "tauri")]
#[error("Postcard encoding error: {0}")]
PostcardEncode(#[from] postcard::Error),
#[cfg(feature = "tauri")]
#[error("Tauri error: {0}")]
TauriError(#[from] tauri::Error),