From b347506426d2e851b0fe4ad7fe81e7a93a0a805f Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Tue, 4 Aug 2026 14:05:30 +0100 Subject: [PATCH 1/8] refactor: app event bus --- .cargo/config.toml | 3 + .gitignore | 5 + Cargo.lock | 34 ++++ Cargo.toml | 1 + apps/app-frontend/src/App.vue | 44 +++-- .../src/components/ui/AccountsCard.vue | 10 +- .../src/components/ui/AppActionBar.vue | 8 +- .../src/components/ui/Instance.vue | 7 +- .../components/ui/QuickInstanceSwitcher.vue | 5 +- .../src/components/ui/SplashScreen.vue | 7 +- .../components/ui/modal/UpdateToPlayModal.vue | 4 +- .../SharedInstanceUpdateModal.vue | 4 +- .../src/components/ui/world/InstanceItem.vue | 10 +- .../components/ui/world/RecentWorldsList.vue | 13 +- .../browse/install-job-notifications.ts | 5 +- .../browse/use-app-server-browse.ts | 10 +- .../src/composables/use-app-event.ts | 18 ++ .../src/composables/use-friends.ts | 10 +- .../src/generated/app-events/.manifest | 31 +++ .../src/generated/app-events/AppEvent.ts | 12 ++ .../generated/app-events/CommandPayload.ts | 3 + .../src/generated/app-events/FriendPayload.ts | 4 + .../app-events/FriendStatusPayload.ts | 3 + .../app-events/ImportLauncherType.ts | 3 + .../app-events/InstallApiErrorDetails.ts | 3 + .../app-events/InstallErrorContext.ts | 3 + .../generated/app-events/InstallErrorView.ts | 7 + .../generated/app-events/InstallJavaStep.ts | 3 + .../generated/app-events/InstallJobDisplay.ts | 3 + .../generated/app-events/InstallJobKind.ts | 3 + .../app-events/InstallJobSnapshot.ts | 11 ++ .../generated/app-events/InstallJobStatus.ts | 3 + .../app-events/InstallPhaseDetails.ts | 6 + .../generated/app-events/InstallPhaseId.ts | 3 + .../generated/app-events/InstallProgress.ts | 4 + .../app-events/InstallProgressSecondary.ts | 3 + .../src/generated/app-events/InstallTarget.ts | 3 + .../InstanceBulkUpdateProgressPayload.ts | 4 + .../InstanceBulkUpdateProgressStage.ts | 3 + .../generated/app-events/InstancePayload.ts | 3 + .../generated/app-events/LoadingBarType.ts | 3 + .../generated/app-events/LoadingPayload.ts | 4 + .../src/generated/app-events/Log4jEvent.ts | 3 + .../src/generated/app-events/LogPayload.ts | 4 + .../src/generated/app-events/ModLoader.ts | 3 + .../generated/app-events/ProcessPayload.ts | 4 + .../app-events/ProcessPayloadType.ts | 3 + .../src/generated/app-events/README.md | 8 + .../SharedInstanceUnavailableReason.ts | 3 + .../generated/app-events/WarningPayload.ts | 3 + apps/app-frontend/src/helpers/ads.js | 5 - apps/app-frontend/src/helpers/events.js | 137 -------------- apps/app-frontend/src/helpers/install.ts | 149 +++------------ apps/app-frontend/src/helpers/state.ts | 22 +-- apps/app-frontend/src/helpers/worlds.ts | 17 +- apps/app-frontend/src/pages/Browse.vue | 51 ++--- apps/app-frontend/src/pages/Index.vue | 20 +- .../settings-modal/installation-settings.vue | 4 +- .../src/pages/instance/content/index.vue | 47 ++--- .../src/pages/instance/files/index.vue | 24 +-- .../src/pages/instance/layout.vue | 63 +++---- .../src/pages/instance/logs/index.vue | 13 +- .../src/pages/instance/worlds/index.vue | 36 ++-- apps/app-frontend/src/pages/library/Index.vue | 9 +- apps/app-frontend/src/pages/project/Index.vue | 13 +- apps/app-frontend/src/providers/app-events.ts | 19 ++ .../src/providers/content-install.ts | 20 +- .../src/providers/download-progress.ts | 27 +-- .../src/providers/server-install.ts | 8 +- .../src/providers/setup/app-events.ts | 64 +++++++ apps/app/src/api/ads.rs | 22 ++- apps/app/src/main.rs | 8 +- packages/app-lib/Cargo.toml | 8 + packages/app-lib/app_event_bindings.rs | 176 ++++++++++++++++++ packages/app-lib/build.rs | 45 ++++- packages/app-lib/src/api/pack/import/mod.rs | 1 + packages/app-lib/src/bin/export_app_events.rs | 22 +++ packages/app-lib/src/error.rs | 1 + packages/app-lib/src/event/emit.rs | 104 ++++------- packages/app-lib/src/event/mod.rs | 103 ++++++++-- packages/app-lib/src/install/events.rs | 6 +- packages/app-lib/src/install/model.rs | 13 ++ packages/app-lib/src/lib.rs | 2 +- packages/app-lib/src/state/friends.rs | 6 +- packages/app-lib/src/state/instance_types.rs | 1 + packages/app-lib/src/state/process.rs | 19 +- 86 files changed, 927 insertions(+), 715 deletions(-) create mode 100644 apps/app-frontend/src/composables/use-app-event.ts create mode 100644 apps/app-frontend/src/generated/app-events/.manifest create mode 100644 apps/app-frontend/src/generated/app-events/AppEvent.ts create mode 100644 apps/app-frontend/src/generated/app-events/CommandPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/FriendPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/FriendStatusPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/ImportLauncherType.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallApiErrorDetails.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallErrorContext.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallErrorView.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallJavaStep.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallJobDisplay.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallJobKind.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallJobSnapshot.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallJobStatus.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallPhaseDetails.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallPhaseId.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallProgress.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallProgressSecondary.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstallTarget.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstanceBulkUpdateProgressPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstanceBulkUpdateProgressStage.ts create mode 100644 apps/app-frontend/src/generated/app-events/InstancePayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/LoadingBarType.ts create mode 100644 apps/app-frontend/src/generated/app-events/LoadingPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/Log4jEvent.ts create mode 100644 apps/app-frontend/src/generated/app-events/LogPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/ModLoader.ts create mode 100644 apps/app-frontend/src/generated/app-events/ProcessPayload.ts create mode 100644 apps/app-frontend/src/generated/app-events/ProcessPayloadType.ts create mode 100644 apps/app-frontend/src/generated/app-events/README.md create mode 100644 apps/app-frontend/src/generated/app-events/SharedInstanceUnavailableReason.ts create mode 100644 apps/app-frontend/src/generated/app-events/WarningPayload.ts delete mode 100644 apps/app-frontend/src/helpers/events.js create mode 100644 apps/app-frontend/src/providers/app-events.ts create mode 100644 apps/app-frontend/src/providers/setup/app-events.ts create mode 100644 packages/app-lib/app_event_bindings.rs create mode 100644 packages/app-lib/src/bin/export_app_events.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d4ed40327b..61e3908b73 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,6 @@ +[alias] +export-app-events = "run -p theseus --bin export-app-events --features export-ts" + # Windows has stack overflows when calling from Tauri, so we increase the default stack size used by the compiler [target."cfg(windows)"] rustflags = ["-C", "link-args=/STACK:16777220"] diff --git a/.gitignore b/.gitignore index 064fbf0449..4de7c328a3 100644 --- a/.gitignore +++ b/.gitignore @@ -59,6 +59,11 @@ apps/frontend/src/generated .turbo target generated +!apps/app-frontend/src/generated/ +!apps/app-frontend/src/generated/app-events/ +!apps/app-frontend/src/generated/app-events/*.ts +!apps/app-frontend/src/generated/app-events/.manifest +!apps/app-frontend/src/generated/app-events/README.md .env # app testing dir diff --git a/Cargo.lock b/Cargo.lock index 6a53ac4b9b..8196661174 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10880,6 +10880,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "testcontainers" version = "0.25.2" @@ -10991,6 +11000,7 @@ dependencies = [ "tracing", "tracing-error", "tracing-subscriber", + "ts-rs", "url", "urlencoding", "uuid 1.23.3", @@ -11673,6 +11683,30 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "ts-rs" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756050066659291d47a554a9f558125db17428b073c5ffce1daf5dcb0f7231d8" +dependencies = [ + "chrono", + "thiserror 2.0.17", + "ts-rs-macros", + "uuid 1.23.3", +] + +[[package]] +name = "ts-rs-macros" +version = "12.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", + "termcolor", +] + [[package]] name = "tungstenite" version = "0.27.0" diff --git a/Cargo.toml b/Cargo.toml index 6670a240af..7f9fdfe350 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -230,6 +230,7 @@ tracing-actix-web = { version = "0.7.19", default-features = false } tracing-ecs = "0.5.0" tracing-error = "0.2.1" tracing-subscriber = "0.3.20" +ts-rs = "12.0.1" typed-path = "0.12.0" url = "2.5.7" urlencoding = "2.1.3" diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index c905b2cd07..789d9a1a15 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -89,9 +89,9 @@ import SplashScreen from '@/components/ui/SplashScreen.vue' import SurveyPopup from '@/components/ui/SurveyPopup.vue' import WindowControls from '@/components/ui/WindowControls.vue' import { useCheckDisableMouseover } from '@/composables/macCssFix.js' +import { useAppEvent } from '@/composables/use-app-event' import { config } from '@/config' import { - ads_consent_listener, hide_ads_window, init_ads_window, perform_ads_consent_action, @@ -101,7 +101,6 @@ import { import { debugAnalytics, initAnalytics, trackEvent } from '@/helpers/analytics' import { check_reachable } from '@/helpers/auth.js' import { get_user, get_version } from '@/helpers/cache.js' -import { command_listener, notification_listener, warning_listener } from '@/helpers/events.js' import { install_create_modpack_instance, install_get_modpack_preview } from '@/helpers/install' import { can_current_user_use_shared_instances, get as getInstance, run } from '@/helpers/instance' import { get as getCreds, login, logout } from '@/helpers/mr_auth.ts' @@ -140,6 +139,7 @@ import { } from '@/providers/download-progress.ts' import { createServerInstall, provideServerInstall } from '@/providers/server-install' import { setupProviders } from '@/providers/setup' +import { setupAppEventsProvider } from '@/providers/setup/app-events' import { setupAuthProvider } from '@/providers/setup/auth' import { setupLoadingStateProvider } from '@/providers/setup/loading-state' import { useError } from '@/store/error.js' @@ -155,6 +155,7 @@ import { appSettingsModalOpenProfileKey } from './providers/app-settings-modal' const themeStore = useTheming() const router = useRouter() const route = useRoute() +const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider() const breadcrumbManager = createBreadcrumbManager() provideBreadcrumbManager(breadcrumbManager) const canNavigateBack = ref(false) @@ -211,11 +212,22 @@ const notificationManager = new AppNotificationManager() provideNotificationManager(notificationManager) const { handleError, addNotification } = notificationManager +useAppEvent( + 'warning', + (event) => + addNotification({ + title: 'Warning', + text: event.message, + type: 'warning', + }), + appEvents, +) + const popupNotificationManager = new AppPopupNotificationManager() providePopupNotificationManager(popupNotificationManager) const { addPopupNotification } = popupNotificationManager let adsConsentPopupId = null -let unlistenAdsConsent +useAppEvent('ads_consent_required', handleAdsConsentRequired, appEvents) const appVersion = getVersion() const tauriApiClient = new TauriModrinthClient({ @@ -352,7 +364,6 @@ const authUnreachable = computed(() => { onMounted(async () => { await useCheckDisableMouseover() try { - unlistenAdsConsent = await ads_consent_listener(handleAdsConsentRequired) handleAdsConsentRequired(await should_show_ads_consent_popup()) } catch (error) { handleError(error) @@ -369,7 +380,6 @@ onUnmounted(async () => { document.querySelector('body').removeEventListener('auxclick', handleAuxClick) clearDelayedUpdatePopup() - await unlistenAdsConsent?.() await unlistenUpdateDownload?.() }) @@ -578,14 +588,6 @@ async function setupApp() { document.getElementsByTagName('html')[0].classList.add('windows') } - await warning_listener((e) => - addNotification({ - title: 'Warning', - text: e.message, - type: 'warning', - }), - ) - fetch(`https://api.modrinth.com/appCriticalAnnouncement.json?version=${version}`) .then((response) => response.json()) .then((res) => { @@ -634,7 +636,7 @@ async function setupApp() { } const stateFailed = ref(false) -initialize_state() +initialize_state(appEventChannel) .then(() => { setupApp().catch((err) => { stateFailed.value = true @@ -764,7 +766,7 @@ const errorModal = ref() const minecraftAuthErrorModal = ref() const minecraftRequiredModal = ref() -const contentInstall = createContentInstall({ router, handleError }) +const contentInstall = createContentInstall({ router, handleError, appEvents }) provideContentInstall(contentInstall) const { instances: contentInstallInstances, @@ -797,7 +799,12 @@ const { handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel, } = contentInstall -const serverInstall = createServerInstall({ router, handleError, popupNotificationManager }) +const serverInstall = createServerInstall({ + router, + handleError, + popupNotificationManager, + appEvents, +}) provideServerInstall(serverInstall) const { setInstallToPlayModal: setServerInstallToPlayModal, @@ -973,8 +980,8 @@ onMounted(() => { const accounts = ref(null) provide('accountsCard', accounts) -command_listener(handleCommand) -notification_listener(handleLiveNotification) +useAppEvent('command', handleCommand, appEvents) +useAppEvent('notification', handleLiveNotification, appEvents) async function markLiveNotificationRead(notification) { try { @@ -1400,6 +1407,7 @@ async function downloadUpdate(versionToDownload) { handleError(e) }) unlistenUpdateDownload = await subscribeToDownloadProgress( + appEvents, appUpdateDownload, versionToDownload.version, ) diff --git a/apps/app-frontend/src/components/ui/AccountsCard.vue b/apps/app-frontend/src/components/ui/AccountsCard.vue index 766f234026..f1cb5fa479 100644 --- a/apps/app-frontend/src/components/ui/AccountsCard.vue +++ b/apps/app-frontend/src/components/ui/AccountsCard.vue @@ -104,8 +104,9 @@ import { useVIntl, } from '@modrinth/ui' import type { Ref } from 'vue' -import { computed, onUnmounted, ref } from 'vue' +import { computed, ref } from 'vue' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' import { get_default_user, @@ -114,7 +115,6 @@ import { set_default_user, users, } from '@/helpers/auth' -import { process_listener } from '@/helpers/events' import { getPlayerHeadUrl } from '@/helpers/rendering/batch-skin-renderer.ts' import type { Skin } from '@/helpers/skins' import { get_available_skins } from '@/helpers/skins' @@ -251,16 +251,12 @@ async function logout(id: string) { trackEvent('AccountLogOut') } -const unlisten = await process_listener(async (e) => { +useAppEvent('process', async (e) => { if (e.event === 'launched') { await refreshValues() } }) -onUnmounted(() => { - unlisten() -}) - const messages = defineMessages({ notSignedIn: { id: 'minecraft-account.not-signed-in', diff --git a/apps/app-frontend/src/components/ui/AppActionBar.vue b/apps/app-frontend/src/components/ui/AppActionBar.vue index 11c172b71e..4d01a65c8f 100644 --- a/apps/app-frontend/src/components/ui/AppActionBar.vue +++ b/apps/app-frontend/src/components/ui/AppActionBar.vue @@ -147,8 +147,8 @@ import { useRouter } from 'vue-router' import AppUpdateButton from '@/components/ui/app-update-button/index.vue' import { useInstallJobNotifications } from '@/composables/browse/install-job-notifications' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' -import { loading_listener, process_listener } from '@/helpers/events' import { get_many as getInstances } from '@/helpers/instance' import { get_all as getRunningProcesses, kill as killProcess } from '@/helpers/process' import type { LoadingBar } from '@/helpers/state' @@ -266,7 +266,7 @@ onMounted(() => { window.addEventListener('online', handleOnline) }) -const unlistenProcess = await process_listener(async () => { +useAppEvent('process', async () => { await refresh() }) @@ -488,7 +488,7 @@ const installJobNotifications = await useInstallJobNotifications({ await refreshLoadingBars() -const unlistenLoading = await loading_listener(async () => { +useAppEvent('loading', async () => { await refreshLoadingBars() }) @@ -505,8 +505,6 @@ onBeforeUnmount(() => { dismissed.value = false window.removeEventListener('offline', handleOffline) window.removeEventListener('online', handleOnline) - unlistenProcess() - unlistenLoading() installJobNotifications.dispose() }) diff --git a/apps/app-frontend/src/components/ui/Instance.vue b/apps/app-frontend/src/components/ui/Instance.vue index 7c82504eb1..7540fb5865 100644 --- a/apps/app-frontend/src/components/ui/Instance.vue +++ b/apps/app-frontend/src/components/ui/Instance.vue @@ -10,11 +10,11 @@ import { import { Avatar, IconButton, injectNotificationManager, useRelativeTime } from '@modrinth/ui' import { convertFileSrc } from '@tauri-apps/api/core' import dayjs from 'dayjs' -import { computed, onMounted, onUnmounted, ref } from 'vue' +import { computed, onMounted, ref } from 'vue' import { useRouter } from 'vue-router' +import { useAppEvent } from '@/composables/use-app-event' import { trackEvent } from '@/helpers/analytics' -import { process_listener } from '@/helpers/events' import { install_existing_instance, install_pack_to_existing_instance } from '@/helpers/install' import { kill, run } from '@/helpers/instance' import { get_by_instance_id } from '@/helpers/process' @@ -136,7 +136,7 @@ defineExpose({ const currentEvent = ref(null) -const unlisten = await process_listener((e) => { +useAppEvent('process', (e) => { if (e.instance_id === props.instance.id) { currentEvent.value = e.event if (e.event === 'finished') { @@ -148,7 +148,6 @@ const unlisten = await process_listener((e) => { onMounted(() => { checkProcess() }) -onUnmounted(() => unlisten())