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
+16 -37
View File
@@ -35,14 +35,14 @@ import {
import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { convertFileSrc } from '@tauri-apps/api/core'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import type { LocationQuery } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
import { useAppServerBrowse } from '@/composables/browse/use-app-server-browse'
import { useAppEvent } from '@/composables/use-app-event'
import { get_project, get_search_results_v3, get_version_many } from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events.js'
import {
get_installed_project_ids as getInstalledProjectIds,
list as listInstances,
@@ -1158,44 +1158,23 @@ if (instance.value?.game_version) {
void searchState.refreshSearch()
type UnlistenFn = () => void
let isUnmounted = false
let unlistenInstances: UnlistenFn | null = null
onMounted(() => {
instance_listener(async (event: { event: string; instance_id: string }) => {
if (event.event === 'added' || event.event === 'created' || event.event === 'removed') {
if (!route.query.i) {
await refreshInstalledProjectIds()
if (projectType.value === 'modpack') {
if (event.event === 'removed') {
syncHiddenInstanceProjectIds()
}
await searchState.refreshSearch()
}
}
}
if (instance.value && event.instance_id === instance.value.id && event.event === 'synced') {
useAppEvent('instance', async (event) => {
if (event.event === 'created' || event.event === 'removed') {
if (!route.query.i) {
await refreshInstalledProjectIds()
await searchState.refreshSearch()
}
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
if (projectType.value === 'modpack') {
if (event.event === 'removed') {
syncHiddenInstanceProjectIds()
}
await searchState.refreshSearch()
}
}
}
unlistenInstances = unlisten
})
.catch(handleError)
})
onUnmounted(() => {
isUnmounted = true
unlistenInstances?.()
if (instance.value && event.instance_id === instance.value.id && event.event === 'synced') {
await refreshInstalledProjectIds()
await searchState.refreshSearch()
}
})
function getProjectBrowseQuery() {
+7 -13
View File
@@ -3,12 +3,12 @@ import { HomeIcon } from '@modrinth/assets'
import { injectNotificationManager } from '@modrinth/ui'
import type { SearchResult } from '@modrinth/utils'
import dayjs from 'dayjs'
import { computed, onUnmounted, ref } from 'vue'
import { computed, ref } from 'vue'
import RowDisplay from '@/components/RowDisplay.vue'
import RecentWorldsList from '@/components/ui/world/RecentWorldsList.vue'
import { useAppEvent } from '@/composables/use-app-event'
import { get_search_results } from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events'
import { list } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
@@ -89,18 +89,12 @@ async function refreshFeaturedProjects() {
await fetchInstances()
await refreshFeaturedProjects()
const unlistenInstance = await instance_listener(
async (e: { event: string; instance_id: string }) => {
await fetchInstances()
useAppEvent('instance', async (event) => {
await fetchInstances()
if (e.event === 'added' || e.event === 'created' || e.event === 'removed') {
await refreshFeaturedProjects()
}
},
)
onUnmounted(() => {
unlistenInstance()
if (event.event === 'created' || event.event === 'removed') {
await refreshFeaturedProjects()
}
})
</script>
@@ -32,6 +32,7 @@ import {
} from '@/helpers/instance'
import { get_loader_versions } from '@/helpers/metadata'
import { get_game_versions, get_loaders } from '@/helpers/tags'
import { injectAppEvents } from '@/providers/app-events'
import { provideInstanceBackup } from '@/providers/instance-backup'
import { useTheming } from '@/store/state'
@@ -41,6 +42,7 @@ import { injectInstanceSettings } from './instance-settings-context.ts'
import SharedInstanceInstallationSettingsControls from './shared-instance-installation-settings-controls.vue'
const { handleError } = injectNotificationManager()
const appEvents = injectAppEvents()
const filePicker = injectFilePicker()
const { formatMessage } = useVIntl()
const queryClient = useQueryClient()
@@ -198,7 +200,7 @@ async function installLocalModpackFromPicker() {
}).catch(handleError)
if (!job) return false
const completed = await wait_for_install_job(job.job_id).catch(handleError)
const completed = await wait_for_install_job(appEvents, job.job_id).catch(handleError)
return !!completed
}
@@ -127,13 +127,9 @@ import { useRouter } from 'vue-router'
import ExportModal from '@/components/ui/ExportModal.vue'
import ShareModalWrapper from '@/components/ui/modal/ShareModalWrapper.vue'
import { useManagedContentPolicy } from '@/composables/instances/use-managed-content-policy'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { get_project_versions, get_version, get_version_many } from '@/helpers/cache.js'
import {
instance_bulk_update_progress_listener,
instance_listener,
type InstanceBulkUpdateProgress,
} from '@/helpers/events.js'
import {
add_project_from_path,
edit,
@@ -151,6 +147,7 @@ import { type InstanceContentData, loadInstanceContentData } from '@/helpers/ins
import { get as getSettings, set as setSettings } from '@/helpers/settings'
import type { CacheBehaviour } from '@/helpers/types'
import { highlightModInInstance } from '@/helpers/utils.js'
import { type AppEventPayload, injectAppEvents } from '@/providers/app-events'
import { injectContentInstall } from '@/providers/content-install'
import { useTheming } from '@/store/state'
import type { FeatureFlag } from '@/store/theme'
@@ -159,6 +156,8 @@ import { injectInstancePage } from '../instance-context'
import { instanceContentQueryOptions, instanceKeys } from '../query-options'
import { injectSharedInstance } from '../shared-instance-context'
type InstanceBulkUpdateProgress = AppEventPayload<'instance_bulk_update_progress'>
const messages = defineMessages({
modpackContentHeader: {
id: 'app.instance.content.managed-content.modpack-header',
@@ -229,6 +228,7 @@ function contentOwnerLink(owner: ContentOwner): NonNullable<ContentOwner['link']
const { formatMessage } = useVIntl()
const { handleError, addNotification } = injectNotificationManager()
const appEvents = injectAppEvents()
const { installingItems, installRevisionByInstance, installFailureRevisionByInstance } =
injectContentInstall()
const router = useRouter()
@@ -955,7 +955,7 @@ async function bulkUpdateAllProjects(onProgress?: (status: BulkOperationStatus)
message: formatMessage(messages.bulkUpdateResolvingVersions),
waiting: true,
})
unlisten = await instance_bulk_update_progress_listener((progress) => {
unlisten = appEvents.on('instance_bulk_update_progress', (progress) => {
if (progress.instanceId !== instance.value.id) return
onProgress(formatBulkUpdateProgress(progress))
})
@@ -1725,7 +1725,18 @@ const removeBeforeEach = router.beforeEach(() => {
let isUnmounted = false
let unlistenDragDrop: UnlistenFn | null = null
let unlistenInstances: UnlistenFn | null = null
useAppEvent('instance', async (event) => {
if (
instance.value &&
event.instance_id === instance.value.id &&
event.event === 'synced' &&
instance.value.install_stage === 'installed' &&
!isBulkOperating.value
) {
await initProjects()
}
})
onMounted(() => {
void getCurrentWebview()
@@ -1747,27 +1758,6 @@ onMounted(() => {
unlistenDragDrop = unlisten
})
.catch(handleError)
void instance_listener(async (event: { event: string; instance_id: string }) => {
if (
instance.value &&
event.instance_id === instance.value.id &&
event.event === 'synced' &&
instance.value.install_stage === 'installed' &&
!isBulkOperating.value
) {
await initProjects()
}
})
.then((unlisten) => {
if (isUnmounted) {
unlisten()
return
}
unlistenInstances = unlisten
})
.catch(handleError)
})
watch(
@@ -1803,6 +1793,5 @@ onUnmounted(() => {
isUnmounted = true
removeBeforeEach()
unlistenDragDrop?.()
unlistenInstances?.()
})
</script>
@@ -23,9 +23,9 @@ import {
writeFile as writeFileBytes,
writeTextFile,
} from '@tauri-apps/plugin-fs'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, ref, watch } from 'vue'
import { instance_listener } from '@/helpers/events'
import { useAppEvent } from '@/composables/use-app-event'
import { get_full_path } from '@/helpers/instance'
import { highlightInFolder } from '@/helpers/utils'
@@ -307,20 +307,12 @@ async function handleExtractFile(path: string, override: boolean, dry: boolean)
}
}
debug('setup: registering instance_listener')
const unlistenInstances = await instance_listener(
async (event: { event: string; instance_id: string }) => {
debug('instance_listener: event =', event.event, 'path =', event.instance_id)
if (event.instance_id === instanceId.value && event.event === 'synced') {
debug('instance_listener: synced event matched, calling refresh')
await refresh()
}
},
)
debug('setup: instance_listener registered')
onUnmounted(() => {
unlistenInstances()
useAppEvent('instance', async (event) => {
debug('app event: instance =', event.event, 'path =', event.instance_id)
if (event.instance_id === instanceId.value && event.event === 'synced') {
debug('app event: synced instance matched, calling refresh')
await refresh()
}
})
watch(instanceId, async () => {
+21 -40
View File
@@ -113,7 +113,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { useOnline } from '@vueuse/core'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, type ComputedRef, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, type ComputedRef, onUnmounted, ref, watch } from 'vue'
import { onBeforeRouteUpdate, useRoute, useRouter } from 'vue-router'
import ContextMenu from '@/components/ui/ContextMenu.vue'
@@ -126,9 +126,9 @@ import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { trackEvent } from '@/helpers/analytics'
import { instance_listener, process_listener } from '@/helpers/events'
import {
getSharedInstanceUnavailableReason,
install_existing_instance,
@@ -776,10 +776,6 @@ const handleOptionsClick = async (args: { option: string; item: unknown }) => {
}
}
let unlistenInstances: (() => void) | null = null
let unlistenProcesses: (() => void) | null = null
let instancePageAlive = true
provideInstancePage({
instanceId,
instance: instance as ComputedRef<GameInstance>,
@@ -812,39 +808,27 @@ watch(instanceId, (currentInstanceId, previousInstanceId) => {
destroyInstanceConsole(previousInstanceId)
})
onMounted(() => {
void instance_listener(async (event: { instance_id: string; event: string }) => {
if (event.instance_id !== instanceId.value) return
if (event.event === 'removed' || route.path === '/') {
if (route.path !== '/') await router.push({ path: '/' })
return
}
await queryClient.invalidateQueries({
queryKey: instanceKeys.detail(event.instance_id),
exact: true,
})
useAppEvent('instance', async (event) => {
if (event.instance_id !== instanceId.value) return
if (event.event === 'removed' || route.path === '/') {
if (route.path !== '/') await router.push({ path: '/' })
return
}
await queryClient.invalidateQueries({
queryKey: instanceKeys.detail(event.instance_id),
exact: true,
})
.then((unlisten) => {
if (instancePageAlive) unlistenInstances = unlisten
else unlisten()
})
.catch(handleError)
})
void process_listener((event: { event: string; instance_id: string }) => {
if (event.instance_id !== instanceId.value) return
if (event.event === 'finished') {
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [])
useInstanceConsole(event.instance_id).invalidate()
void queryClient.invalidateQueries({ queryKey: instanceKeys.logs(event.instance_id) })
} else if (event.event === 'launched') {
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [true])
}
})
.then((unlisten) => {
if (instancePageAlive) unlistenProcesses = unlisten
else unlisten()
})
.catch(handleError)
useAppEvent('process', (event) => {
if (event.instance_id !== instanceId.value) return
if (event.event === 'finished') {
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [])
useInstanceConsole(event.instance_id).invalidate()
void queryClient.invalidateQueries({ queryKey: instanceKeys.logs(event.instance_id) })
} else if (event.event === 'launched') {
queryClient.setQueryData(instanceKeys.processes(event.instance_id), [true])
}
})
const icon = computed(() =>
@@ -858,9 +842,6 @@ const timePlayed = computed(() => {
})
onUnmounted(() => {
instancePageAlive = false
unlistenProcesses?.()
unlistenInstances?.()
if (instanceId.value) {
destroyInstanceConsole(instanceId.value)
}
@@ -12,10 +12,10 @@ import {
provideConsoleManager,
} from '@modrinth/ui'
import { useQuery } from '@tanstack/vue-query'
import { computed, onUnmounted, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
import { computed, ref, shallowRef, triggerRef, watch, watchEffect } from 'vue'
import { useAppEvent } from '@/composables/use-app-event'
import { useInstanceConsole } from '@/composables/useInstanceConsole'
import { log_listener, process_listener } from '@/helpers/events.js'
import { delete_logs_by_filename, get_output_by_filename } from '@/helpers/logs.js'
import { injectInstancePage } from '../instance-context'
@@ -191,7 +191,7 @@ if (!instancePage.playing.value) {
void analyseForCrash()
}
const unlistenLog = await log_listener((payload) => {
useAppEvent('log', (payload) => {
if (payload.instance_id !== instanceId.value) return
if (payload.type === 'log4j') {
@@ -201,7 +201,7 @@ const unlistenLog = await log_listener((payload) => {
}
})
const unlistenProcesses = await process_listener(async (e) => {
useAppEvent('process', async (e) => {
if (e.instance_id !== instanceId.value) return
if (e.event === 'launched') {
liveConsole.clear()
@@ -216,9 +216,4 @@ const unlistenProcesses = await process_listener(async (e) => {
void analyseForCrash()
}
})
onUnmounted(() => {
unlistenLog()
unlistenProcesses()
})
</script>
@@ -156,9 +156,9 @@ import ConfirmRemoveWorldModal from '@/components/ui/world/modal/ConfirmRemoveWo
import EditServerModal from '@/components/ui/world/modal/EditServerModal.vue'
import EditWorldModal from '@/components/ui/world/modal/EditSingleplayerWorldModal.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { get_project, get_project_v3 } from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events'
import { get_game_versions } from '@/helpers/tags'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import {
@@ -169,7 +169,6 @@ import {
handleDefaultInstanceUpdateEvent,
hasServerQuickPlaySupport,
hasWorldQuickPlaySupport,
type InstanceEvent,
normalizeServerAddress,
type ProtocolVersion,
refreshServerData,
@@ -399,35 +398,31 @@ watch(
{ immediate: true },
)
let unlistenInstance: (() => void) | null = null
let worldsTabAlive = true
useAppEvent('instance', async (event) => {
if (event.instance_id !== instance.value.id) return
console.info(`Handling instance event '${event.event}' for instance: ${event.instance_id}`)
if (event.event === 'servers_updated') {
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
if (isLinux) linuxRefreshCount.value++
await refreshAllWorlds()
}
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, event)
})
async function initWorldsTab() {
const [_unlistenInstance, resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
instance_listener(async (e: InstanceEvent) => {
if (e.instance_id !== instance.value.id) return
console.info(`Handling instance event '${e.event}' for instance: ${e.instance_id}`)
if (e.event === 'servers_updated') {
if (isLinux && linuxRefreshCount.value >= MAX_LINUX_REFRESHES) return
if (isLinux) linuxRefreshCount.value++
await refreshAllWorlds()
}
await handleDefaultInstanceUpdateEvent(worlds.value, instance.value.id, e)
}),
const [resolvedProtocolVersion, resolvedGameVersions] = await Promise.all([
get_instance_protocol_version(instance.value.id).catch(() => null),
get_game_versions().catch(() => [] as GameVersion[]),
])
if (!worldsTabAlive) {
_unlistenInstance()
return
}
if (!worldsTabAlive) return
unlistenInstance = _unlistenInstance
protocolVersion.value = resolvedProtocolVersion
gameVersions.value = resolvedGameVersions
protocolVersionReady.value = true
@@ -724,6 +719,5 @@ async function proceedRemoveWorld(world: World) {
onBeforeUnmount(() => {
worldsTabAlive = false
unlistenInstance?.()
})
</script>
@@ -1,11 +1,11 @@
<script setup lang="ts">
import { LibraryIcon, PlusIcon } from '@modrinth/assets'
import { Button, injectNotificationManager, NavTabs } from '@modrinth/ui'
import { inject, onUnmounted, ref, shallowRef } from 'vue'
import { inject, ref, shallowRef } from 'vue'
import { useRoute } from 'vue-router'
import { NewInstanceImage } from '@/assets/icons'
import { instance_listener } from '@/helpers/events.js'
import { useAppEvent } from '@/composables/use-app-event'
import { list } from '@/helpers/instance'
import { useRootBreadcrumb } from '@/providers/breadcrumbs'
@@ -31,12 +31,9 @@ window.addEventListener('online', () => {
offline.value = false
})
const unlistenInstance = await instance_listener(async () => {
useAppEvent('instance', async () => {
instances.value = await list().catch(handleError)
})
onUnmounted(() => {
unlistenInstance()
})
</script>
<template>
+3 -10
View File
@@ -284,7 +284,7 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { openUrl } from '@tauri-apps/plugin-opener'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { computed, onUnmounted, ref, shallowRef, watch } from 'vue'
import { computed, ref, shallowRef, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { SwapIcon } from '@/assets/icons/index.js'
@@ -294,6 +294,7 @@ import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { useAppEvent } from '@/composables/use-app-event'
import {
get_organization,
get_project,
@@ -302,7 +303,6 @@ import {
get_version,
get_version_many,
} from '@/helpers/cache.js'
import { process_listener } from '@/helpers/events'
import {
get as getInstance,
get_projects as getInstanceProjects,
@@ -814,8 +814,7 @@ function fetchDeferredServerData(project) {
await fetchProjectData()
let unlistenProcesses
process_listener((e) => {
useAppEvent('process', (e) => {
if (
e.event === 'finished' &&
serverInstancePath.value &&
@@ -823,12 +822,6 @@ process_listener((e) => {
) {
serverPlaying.value = false
}
}).then((unlisten) => {
unlistenProcesses = unlisten
})
onUnmounted(() => {
unlistenProcesses?.()
})
watch(