From 0bd6939894d853ec9fdd6775f4d19b2213af6cb1 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 31 Aug 2026 17:03:35 +0100 Subject: [PATCH 1/2] fix: 7371 --- Cargo.lock | 1 - apps/app/Cargo.toml | 1 - apps/app/src/api/mod.rs | 4 ---- apps/app/src/error.rs | 18 ------------------ apps/app/src/main.rs | 1 - 5 files changed, 25 deletions(-) delete mode 100644 apps/app/src/error.rs diff --git a/Cargo.lock b/Cargo.lock index 459048aa57..4c94019842 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11172,7 +11172,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", - "tracing-error", "url", "urlencoding", "uuid 1.23.3", diff --git a/apps/app/Cargo.toml b/apps/app/Cargo.toml index b2cb4b40a3..5e7115cad3 100644 --- a/apps/app/Cargo.toml +++ b/apps/app/Cargo.toml @@ -35,7 +35,6 @@ theseus = { workspace = true, features = ["tauri"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["time"] } tracing = { workspace = true } -tracing-error = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } uuid = { workspace = true, features = ["serde", "v4"] } diff --git a/apps/app/src/api/mod.rs b/apps/app/src/api/mod.rs index 805118e991..c8570f9b02 100644 --- a/apps/app/src/api/mod.rs +++ b/apps/app/src/api/mod.rs @@ -83,11 +83,7 @@ macro_rules! impl_serialize { S: Serializer, { match self { - // For the Theseus variant, we add a special display for the error, - // to view the spans if subscribed to them (which is information that is lost when serializing) TheseusSerializableError::Theseus(theseus_error) => { - $crate::error::display_tracing_error(theseus_error); - let unavailable_reason = match theseus_error.raw.as_ref() { theseus::ErrorKind::SharedInstanceUnavailable(reason) => Some(reason), _ => None, diff --git a/apps/app/src/error.rs b/apps/app/src/error.rs deleted file mode 100644 index 22454d65e5..0000000000 --- a/apps/app/src/error.rs +++ /dev/null @@ -1,18 +0,0 @@ -use tracing_error::ExtractSpanTrace; - -pub fn display_tracing_error(err: &theseus::Error) { - match get_span_trace(err) { - Some(span_trace) => { - tracing::error!(error = %err, span_trace = %span_trace); - } - None => { - tracing::error!(error = %err); - } - } -} - -pub fn get_span_trace<'a>( - error: &'a (dyn std::error::Error + 'static), -) -> Option<&'a tracing_error::SpanTrace> { - error.source().and_then(|e| e.span_trace()) -} diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 0f89a9a6e9..b4547fe339 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -12,7 +12,6 @@ use tauri_plugin_fs::FsExt; use theseus::prelude::*; mod api; -mod error; #[cfg(target_os = "macos")] mod macos; From eeb026214b765f48dd9bb25f563844e1aaac7eb0 Mon Sep 17 00:00:00 2001 From: "Calum H. (IMB11)" Date: Mon, 31 Aug 2026 17:42:15 +0100 Subject: [PATCH 2/2] fix: continued --- apps/app-frontend/src/App.vue | 2 + .../components/ui/QuickInstanceSwitcher.vue | 60 +++++++------------ .../use-instance-metadata-refresh.ts | 60 +++++++++++++++++++ apps/app-frontend/src/pages/Index.vue | 36 +++-------- .../src/pages/instance/layout.vue | 11 +--- packages/app-lib/src/state/db.rs | 4 +- 6 files changed, 98 insertions(+), 75 deletions(-) create mode 100644 apps/app-frontend/src/composables/use-instance-metadata-refresh.ts diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index 668c557a86..35770abfc9 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -105,6 +105,7 @@ import { useCheckDisableMouseover } from '@/composables/macCssFix.js' import { useAppEvent } from '@/composables/use-app-event' import { useAppSettings } from '@/composables/use-app-settings.ts' import { useError } from '@/composables/use-error.js' +import { useInstanceMetadataRefresh } from '@/composables/use-instance-metadata-refresh' import { isDarkTheme, useTheme } from '@/composables/use-theme.ts' import { config } from '@/config' import { getAccountAppearance, rememberAccountAppearance } from '@/helpers/account-appearance.ts' @@ -190,6 +191,7 @@ const appTheme = useTheme() const router = useRouter() const route = useRoute() const { channel: appEventChannel, events: appEvents } = setupAppEventsProvider() +useInstanceMetadataRefresh(appEvents) const breadcrumbManager = createBreadcrumbManager() provideBreadcrumbManager(breadcrumbManager) const canNavigateBack = ref(false) diff --git a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue index d926b34069..99cc39a147 100644 --- a/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue +++ b/apps/app-frontend/src/components/ui/QuickInstanceSwitcher.vue @@ -8,7 +8,7 @@ import { injectNotificationManager, useVIntl, } from '@modrinth/ui' -import { useQueryClient } from '@tanstack/vue-query' +import { useQuery } from '@tanstack/vue-query' import dayjs from 'dayjs' import { computed, onMounted, onUnmounted, ref } from 'vue' import { useRouter } from 'vue-router' @@ -17,17 +17,17 @@ import NavButton from '@/components/ui/NavButton.vue' import { useAppEvent } from '@/composables/use-app-event' import { handleSevereError } from '@/composables/use-error.js' import { trackEvent } from '@/helpers/analytics' -import { getInstanceIconUrl, kill, list, run } from '@/helpers/instance' +import { getInstanceIconUrl, kill, run } from '@/helpers/instance' import { get_all } from '@/helpers/process' import { showInstanceInFolder } from '@/helpers/utils' -import { instanceKeys } from '@/pages/instance/query-options' +import { instanceListQueryOptions } from '@/pages/instance/query-options' const ITEM_SIZE = 52 const APPROX_USED_VERTICAL_SPACE = 475 // doesn't need to be exact lol just close enough so there's a little gap and no overflow const STORAGE_KEY = 'modrinth-quick-instance-count' const { handleError } = injectNotificationManager() -const queryClient = useQueryClient() +const instancesQuery = useQuery(instanceListQueryOptions()) const router = useRouter() const instanceOptions = ref() const runningInstances = ref([]) @@ -35,7 +35,24 @@ const runningInstances = ref([]) const { formatMessage } = useVIntl() const maxAuto = ref(0) -const allInstances = ref([]) +const allInstances = computed(() => + (instancesQuery.data.value ?? []).slice().sort((a, b) => { + const dateACreated = dayjs(a.created) + const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0) + + const dateBCreated = dayjs(b.created) + const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0) + + const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed + const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed + + if (dateA.isSame(dateB)) { + return a.name.localeCompare(b.name) + } + + return dateB - dateA + }), +) const dragging = ref(false) const stored = localStorage.getItem(STORAGE_KEY) @@ -134,40 +151,9 @@ const onDividerPointerUp = (event) => { endDrag(event) } -const getInstances = async () => { - const instances = await list().catch(handleError) - - for (const instance of instances) { - queryClient.setQueryData(instanceKeys.detail(instance.id), instance) - } - - allInstances.value = instances.sort((a, b) => { - const dateACreated = dayjs(a.created) - const dateAPlayed = a.last_played ? dayjs(a.last_played) : dayjs(0) - - const dateBCreated = dayjs(b.created) - const dateBPlayed = b.last_played ? dayjs(b.last_played) : dayjs(0) - - const dateA = dateACreated.isAfter(dateAPlayed) ? dateACreated : dateAPlayed - const dateB = dateBCreated.isAfter(dateBPlayed) ? dateBCreated : dateBPlayed - - if (dateA.isSame(dateB)) { - return a.name.localeCompare(b.name) - } - - return dateB - dateA - }) -} - -await getInstances() +await instancesQuery.suspense().catch(handleError) updateMaxAuto() -useAppEvent('instance', async (event) => { - if (event.event !== 'synced') { - await getInstances() - } -}) - useAppEvent('process', checkProcesses) onMounted(() => { diff --git a/apps/app-frontend/src/composables/use-instance-metadata-refresh.ts b/apps/app-frontend/src/composables/use-instance-metadata-refresh.ts new file mode 100644 index 0000000000..34bbbd836b --- /dev/null +++ b/apps/app-frontend/src/composables/use-instance-metadata-refresh.ts @@ -0,0 +1,60 @@ +import { useQueryClient } from '@tanstack/vue-query' + +import type { InstancePayload } from '@/generated/app-events/InstancePayload' +import { instanceKeys, instanceListQueryOptions } from '@/pages/instance/query-options' +import type { AppEvents } from '@/providers/app-events' + +import { useAppEvent } from './use-app-event' + +const INSTANCE_METADATA_EVENTS = new Set([ + 'created', + 'synced', + 'edited', + 'removed', +]) + +export function useInstanceMetadataRefresh(events: AppEvents) { + const queryClient = useQueryClient() + let refreshQueued = false + let refreshPromise: Promise | undefined + + function queueRefresh() { + refreshQueued = true + if (!refreshPromise) { + refreshPromise = Promise.resolve().then(async () => { + try { + do { + refreshQueued = false + const joinedExistingRequest = + queryClient.isFetching({ queryKey: instanceKeys.list(), exact: true }) > 0 + const instances = await queryClient.fetchQuery({ + ...instanceListQueryOptions(), + staleTime: 0, + }) + + for (const instance of instances) { + queryClient.setQueryData(instanceKeys.detail(instance.id), instance) + } + + if (joinedExistingRequest) { + refreshQueued = true + } + } while (refreshQueued) + } finally { + refreshPromise = undefined + } + }) + } + + return refreshPromise + } + + useAppEvent( + 'instance', + (event) => { + if (INSTANCE_METADATA_EVENTS.has(event.event)) return queueRefresh() + }, + events, + ) + useAppEvent('instance_groups_changed', queueRefresh, events) +} diff --git a/apps/app-frontend/src/pages/Index.vue b/apps/app-frontend/src/pages/Index.vue index 62003cf6cb..e1200bd3c2 100644 --- a/apps/app-frontend/src/pages/Index.vue +++ b/apps/app-frontend/src/pages/Index.vue @@ -1,17 +1,15 @@