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
+4
View File
@@ -2,3 +2,7 @@
*.gltf
src/locales/
src/assets/**/*.svg
# Generated app-event bindings
src/generated/app-events/*.ts
src/generated/app-events/postcard/**
+6 -1
View File
@@ -1,2 +1,7 @@
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
export default config
export default config.append([
{
ignores: ['src/generated/app-events/*.ts', 'src/generated/app-events/postcard/**'],
},
])
+26 -18
View File
@@ -90,9 +90,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,
@@ -103,7 +103,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'
@@ -142,6 +141,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'
@@ -157,6 +157,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)
@@ -243,11 +244,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({
@@ -384,7 +396,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)
@@ -407,7 +418,6 @@ onUnmounted(async () => {
fullscreenAdsWindowHold = false
await release_ads_window_hold().catch(handleError)
}
await unlistenAdsConsent?.()
await unlistenUpdateDownload?.()
})
@@ -616,14 +626,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) => {
@@ -672,7 +674,7 @@ async function setupApp() {
}
const stateFailed = ref(false)
initialize_state()
initialize_state(appEventChannel)
.then(() => {
setupApp().catch((err) => {
stateFailed.value = true
@@ -802,7 +804,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,
@@ -835,7 +837,12 @@ const {
handleIncompatibilityWarningCancel: handleContentInstallIncompatibilityWarningCancel,
} = contentInstall
const serverInstall = createServerInstall({ router, handleError, popupNotificationManager })
const serverInstall = createServerInstall({
router,
handleError,
popupNotificationManager,
appEvents,
})
provideServerInstall(serverInstall)
const {
setInstallToPlayModal: setServerInstallToPlayModal,
@@ -1011,8 +1018,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 {
@@ -1438,6 +1445,7 @@ async function downloadUpdate(versionToDownload) {
handleError(e)
})
unlistenUpdateDownload = await subscribeToDownloadProgress(
appEvents,
appUpdateDownload,
versionToDownload.version,
)
@@ -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',
@@ -148,8 +148,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'
@@ -294,7 +294,7 @@ onMounted(() => {
window.addEventListener('online', handleOnline)
})
const unlistenProcess = await process_listener(async () => {
useAppEvent('process', async () => {
await refresh()
})
@@ -554,7 +554,7 @@ const installJobNotifications = await useInstallJobNotifications({
await refreshLoadingBars()
const unlistenLoading = await loading_listener(async () => {
useAppEvent('loading', async () => {
await refreshLoadingBars()
})
@@ -573,8 +573,6 @@ onBeforeUnmount(() => {
dismissed.value = false
window.removeEventListener('offline', handleOffline)
window.removeEventListener('online', handleOnline)
unlistenProcess()
unlistenLoading()
installJobNotifications.dispose()
})
</script>
@@ -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())
</script>
<template>
@@ -7,7 +7,7 @@ import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import NavButton from '@/components/ui/NavButton.vue'
import { instance_listener } from '@/helpers/events.js'
import { useAppEvent } from '@/composables/use-app-event'
import { list } from '@/helpers/instance'
import { instanceKeys } from '@/pages/instance/query-options'
@@ -148,7 +148,7 @@ const getInstances = async () => {
await getInstances()
updateMaxAuto()
const unlistenInstance = await instance_listener(async (event) => {
useAppEvent('instance', async (event) => {
if (event.event !== 'synced') {
await getInstances()
}
@@ -162,7 +162,6 @@ onUnmounted(() => {
window.removeEventListener('resize', updateMaxAuto)
document.body.classList.remove('quick-instance-dragging')
clearOverdragFlash()
unlistenInstance()
})
const messages = defineMessages({
@@ -82,7 +82,7 @@ import { injectLoadingState } from '@modrinth/ui'
import { ref, watch } from 'vue'
import ProgressBar from '@/components/ui/ProgressBar.vue'
import { loading_listener } from '@/helpers/events.js'
import { useAppEvent } from '@/composables/use-app-event'
const doneLoading = ref(false)
const loadingProgress = ref(0)
@@ -132,13 +132,10 @@ function fakeLoadingIncrease() {
}
}
loading_listener(async (e) => {
useAppEvent('loading', (e) => {
if (e.event.type === 'directory_move') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Updating app directory...'
} else if (e.event.type === 'checking_for_updates') {
loadingProgress.value = 100 * (e.fraction ?? 1)
message.value = 'Checking for updates...'
}
})
</script>
@@ -34,6 +34,7 @@ import { get_project_many, get_version, get_version_many } from '@/helpers/cache
import { wait_for_install_job } from '@/helpers/install'
import { update_managed_modrinth_version } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { injectAppEvents } from '@/providers/app-events'
import { injectServerInstall } from '@/providers/server-install'
type Dependency = Labrinth.Versions.v3.Dependency
@@ -74,6 +75,7 @@ type ProjectInfo = {
}
const { formatMessage } = useVIntl()
const appEvents = injectAppEvents()
const { startInstallingServer, stopInstallingServer } = injectServerInstall()
type UpdateCompleteCallback = () => void | Promise<void>
@@ -253,7 +255,7 @@ async function handleUpdate() {
try {
if (modpackVersionId.value && instance.value) {
const job = await update_managed_modrinth_version(instance.value.id, modpackVersionId.value)
await wait_for_install_job(job.job_id)
await wait_for_install_job(appEvents, job.job_id)
await onUpdateComplete.value()
}
} catch (error) {
@@ -39,6 +39,7 @@ import {
} from '@/helpers/install'
import { useSharedInstanceErrors } from '@/helpers/shared-instance-errors'
import type { GameInstance } from '@/helpers/types'
import { injectAppEvents } from '@/providers/app-events'
type UpdateCompleteCallback = () => void | Promise<void>
@@ -54,6 +55,7 @@ const instance = ref<GameInstance | null>(null)
const preview = ref<SharedInstanceUpdatePreview | null>(null)
const onComplete = ref<UpdateCompleteCallback>(() => {})
const { formatMessage } = useVIntl()
const appEvents = injectAppEvents()
const { notifySharedInstanceError } = useSharedInstanceErrors()
const diffs = computed<ContentDiffItem[]>(
() =>
@@ -78,7 +80,7 @@ async function update() {
try {
if (instance.value) {
const job = await install_update_shared_instance(instance.value.id)
await wait_for_install_job(job.job_id)
await wait_for_install_job(appEvents, job.job_id)
await onComplete.value()
successful = true
}
@@ -21,12 +21,12 @@ import {
import { capitalizeString } from '@modrinth/utils'
import { convertFileSrc } from '@tauri-apps/api/core'
import type { Dayjs } from 'dayjs'
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
import { computed, nextTick, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { get_project } from '@/helpers/cache'
import { process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
@@ -108,7 +108,7 @@ const stop = async (event: MouseEvent) => {
loading.value = false
}
const unlistenProcesses = await process_listener(async () => {
useAppEvent('process', async () => {
await checkProcess()
})
@@ -121,10 +121,6 @@ const checkProcess = async () => {
onMounted(() => {
checkProcess()
})
onUnmounted(() => {
unlistenProcesses()
})
</script>
<template>
<SmartClickable>
@@ -5,12 +5,12 @@ import { GAME_MODES, injectNotificationManager } from '@modrinth/ui'
import { platform } from '@tauri-apps/plugin-os'
import type { Dayjs } from 'dayjs'
import dayjs from 'dayjs'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import InstanceItem from '@/components/ui/world/InstanceItem.vue'
import WorldItem from '@/components/ui/world/WorldItem.vue'
import { useAppEvent } from '@/composables/use-app-event'
import { trackEvent } from '@/helpers/analytics'
import { instance_listener, process_listener } from '@/helpers/events'
import { kill, run } from '@/helpers/instance'
import { get_all } from '@/helpers/process'
import { get_game_versions } from '@/helpers/tags'
@@ -215,11 +215,11 @@ async function stopInstance(path: string) {
const currentInstance = ref<string>()
const currentWorld = ref<string>()
const unlistenProcesses = await process_listener(async () => {
useAppEvent('process', async () => {
await checkProcesses()
})
const unlistenInstances = await instance_listener(async () => {
useAppEvent('instance', async () => {
await populateJumpBackIn().catch(() => {
console.error('Failed to populate jump back in')
})
@@ -251,11 +251,6 @@ onMounted(() => {
checkProcesses()
linuxPopulateCount.value = 0
})
onUnmounted(() => {
unlistenProcesses()
unlistenInstances()
})
</script>
<template>
@@ -10,7 +10,6 @@ import { convertFileSrc } from '@tauri-apps/api/core'
import { computed, ref } from 'vue'
import type { Router } from 'vue-router'
import { install_job_listener } from '@/helpers/events'
import {
install_job_dismiss,
install_job_list,
@@ -23,6 +22,7 @@ import {
type InstallProgress,
} from '@/helpers/install'
import { get_many as getInstances } from '@/helpers/instance'
import { injectAppEvents } from '@/providers/app-events'
import { useTheming } from '@/store/state'
const messages = defineMessages({
@@ -234,6 +234,7 @@ export async function useInstallJobNotifications(opts: {
handleError: (err: unknown) => void
onChange: () => void
}) {
const appEvents = injectAppEvents()
const { formatMessage } = useVIntl()
const themeStore = useTheming()
const jobs = ref<InstallJobSnapshot[]>([])
@@ -635,7 +636,7 @@ export async function useInstallJobNotifications(opts: {
void refreshMetadata()
}
const unlisten = await install_job_listener((job: InstallJobSnapshot) => applyJobUpdate(job))
const unlisten = appEvents.on('install_job', applyJobUpdate)
await refresh(false)
return {
@@ -12,7 +12,7 @@ import {
fetchCachedServerStatus,
getFreshCachedServerStatus,
} from '@/composables/instances/use-server-status-query'
import { process_listener } from '@/helpers/events'
import { useAppEvent } from '@/composables/use-app-event'
import { kill, list as listInstances } from '@/helpers/instance'
import { get_by_instance_id } from '@/helpers/process'
import type { GameInstance } from '@/helpers/types'
@@ -78,7 +78,6 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
const lastServerHits = shallowRef<Labrinth.Search.v3.ResultSearchProject[]>([])
const contextMenuRef = ref<ContextMenuHandle | null>(null)
let serverPingsActive = true
let unlistenProcesses: (() => void) | null = null
async function checkServerRunningStates(hits: Labrinth.Search.v3.ResultSearchProject[]) {
debugLog('checkServerRunningStates', { hitCount: hits.length })
@@ -280,7 +279,7 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
process_listener((event: { event: string; instance_id: string }) => {
useAppEvent('process', (event) => {
debugLog('process event', event)
if (event.event === 'finished') {
const projectId = Object.entries(runningServerProjects.value).find(
@@ -292,14 +291,9 @@ export function useAppServerBrowse(options: UseAppServerBrowseOptions) {
}
}
})
.then((unlisten) => {
unlistenProcesses = unlisten
})
.catch(options.handleError)
onUnmounted(() => {
serverPingsActive = false
unlistenProcesses?.()
})
return {
@@ -0,0 +1,18 @@
import { onScopeDispose } from 'vue'
import {
type AppEventHandler,
type AppEvents,
type AppEventType,
injectAppEvents,
} from '@/providers/app-events'
export function useAppEvent<Type extends AppEventType>(
type: Type,
handler: AppEventHandler<Type>,
events: AppEvents = injectAppEvents(),
) {
const unsubscribe = events.on(type, handler)
onScopeDispose(unsubscribe)
return unsubscribe
}
@@ -1,8 +1,8 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, type MaybeRefOrGetter, onUnmounted, toValue } from 'vue'
import { computed, type MaybeRefOrGetter, toValue } from 'vue'
import { useAppEvent } from '@/composables/use-app-event'
import { toError } from '@/helpers/errors'
import { friend_listener } from '@/helpers/events.js'
import {
acceptCachedFriend,
add_friend,
@@ -127,13 +127,9 @@ export function useFriends(options: {
)
}
let unlisten: (() => void) | undefined
void friend_listener(() => {
useAppEvent('friend', () => {
void queryClient.invalidateQueries({ queryKey: queryKey.value })
}).then((listener) => {
unlisten = listener
})
onUnmounted(() => unlisten?.())
return {
query,
@@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { CommandPayload } from "./CommandPayload";
import type { FriendPayload } from "./FriendPayload";
import type { InstallJobSnapshot } from "./InstallJobSnapshot";
import type { InstanceBulkUpdateProgressPayload } from "./InstanceBulkUpdateProgressPayload";
import type { InstancePayload } from "./InstancePayload";
import type { LoadingPayload } from "./LoadingPayload";
import type { LogPayload } from "./LogPayload";
import type { ProcessPayload } from "./ProcessPayload";
import type { WarningPayload } from "./WarningPayload";
export type AppEvent = { "type": "loading", "payload": LoadingPayload } | { "type": "process", "payload": ProcessPayload } | { "type": "instance", "payload": InstancePayload } | { "type": "instance_bulk_update_progress", "payload": InstanceBulkUpdateProgressPayload } | { "type": "install_job", "payload": InstallJobSnapshot } | { "type": "command", "payload": CommandPayload } | { "type": "warning", "payload": WarningPayload } | { "type": "friend", "payload": FriendPayload } | { "type": "notification", "payload": unknown } | { "type": "log", "payload": LogPayload } | { "type": "ads_consent_required", "payload": boolean };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CommandPayload = { "event": "InstallMod", id: string, } | { "event": "InstallVersion", id: string, } | { "event": "InstallModpack", id: string, } | { "event": "InstallServer", id: string, } | { "event": "LaunchInstance", id: string, server: string | null, singleplayer_world: string | null, } | { "event": "InstallSharedInstanceInvite", invite_id: string, } | { "event": "RunMRPack", path: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { FriendStatusPayload } from "./FriendStatusPayload";
export type FriendPayload = { "event": "friend_request", from: string, } | { "event": "user_offline", id: string, } | { "event": "status_update", user_status: FriendStatusPayload, } | { "event": "status_sync" };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FriendStatusPayload = { user_id: string, profile_name: string | null, last_update: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ImportLauncherType = "MultiMC" | "PrismLauncher" | "ATLauncher" | "GDLauncher" | "Curseforge" | "Unknown";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallApiErrorDetails = { error: string, status?: number, method?: string, url?: string, route?: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallErrorContext = { operation: string, source_path?: string, target_path?: string, file_path?: string, entry_path?: string, urls: Array<string>, expected_hash?: string, expected_size?: number, project_id?: string, version_id?: string, minecraft_version?: string, loader?: string, java_version?: number, os?: string, arch?: string, };
@@ -0,0 +1,7 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallApiErrorDetails } from "./InstallApiErrorDetails";
import type { InstallErrorContext } from "./InstallErrorContext";
import type { InstallPhaseId } from "./InstallPhaseId";
import type { SharedInstanceUnavailableReason } from "./SharedInstanceUnavailableReason";
export type InstallErrorView = { code: string, phase?: InstallPhaseId, message: string, reason?: SharedInstanceUnavailableReason, api?: InstallApiErrorDetails, context?: InstallErrorContext, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJavaStep = "resolving" | "fetching_metadata" | "downloading" | "extracting" | "validating";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobDisplay = { title: string, icon: string | null, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobKind = "create_instance" | "create_modpack_instance" | "create_shared_instance" | "import_instance" | "duplicate_instance" | "install_existing_instance" | "install_pack_to_existing_instance" | "update_shared_instance";
@@ -0,0 +1,11 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallErrorView } from "./InstallErrorView";
import type { InstallJobDisplay } from "./InstallJobDisplay";
import type { InstallJobKind } from "./InstallJobKind";
import type { InstallJobStatus } from "./InstallJobStatus";
import type { InstallPhaseDetails } from "./InstallPhaseDetails";
import type { InstallPhaseId } from "./InstallPhaseId";
import type { InstallProgress } from "./InstallProgress";
import type { InstallTarget } from "./InstallTarget";
export type InstallJobSnapshot = { job_id: string, instance_id: string | null, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | null, details: InstallPhaseDetails, display: InstallJobDisplay | null, error: InstallErrorView | null, rollback_error: InstallErrorView | null, created: string, modified: string, finished: string | null, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallJobStatus = "queued" | "running" | "succeeded" | "failed" | "interrupted" | "canceled";
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImportLauncherType } from "./ImportLauncherType";
import type { InstallJavaStep } from "./InstallJavaStep";
import type { ModLoader } from "./ModLoader";
export type InstallPhaseDetails = { "type": "empty" } | { "type": "instance", name: string, } | { "type": "minecraft", game_version: string, loader: ModLoader, } | { "type": "java", major_version: number, step: InstallJavaStep, } | { "type": "modpack", project_id: string | null, version_id: string | null, title: string | null, } | { "type": "import", launcher_type: ImportLauncherType, instance_folder: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallPhaseId = "preparing_instance" | "resolving_pack" | "downloading_pack_file" | "reading_pack_manifest" | "downloading_content" | "extracting_overrides" | "resolving_minecraft" | "resolving_loader" | "preparing_java" | "downloading_minecraft" | "running_loader_processors" | "finalizing" | "rolling_back";
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstallProgressSecondary } from "./InstallProgressSecondary";
export type InstallProgress = { current: number, total: number, secondary?: InstallProgressSecondary, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallProgressSecondary = { current: number, total: number, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstallTarget = { "type": "new_instance", instance_id: string | null, } | { "type": "existing_instance", instance_id: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { InstanceBulkUpdateProgressStage } from "./InstanceBulkUpdateProgressStage";
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: number, total: number, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstanceBulkUpdateProgressStage = "resolving_versions" | "downloading" | "finishing";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type InstancePayload = { instance_id: string, } & ({ "event": "created" } | { "event": "synced" } | { "event": "servers_updated" } | { "event": "world_updated", world: string, } | { "event": "server_joined", host: string, port: number, timestamp: string, } | { "event": "edited" } | { "event": "content_install_finished", project_ids: Array<string>, } | { "event": "content_install_failed", project_ids: Array<string>, message: string, } | { "event": "removed" });
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type LoadingBarType = { "type": "legacy_data_migration" } | { "type": "directory_move", old: string, new: string, } | { "type": "java_download", version: number, } | { "type": "pack_file_download", instance_id: string, pack_name: string, icon: string | null, pack_version: string, } | { "type": "pack_download", instance_id: string, pack_name: string, icon: string | null, pack_id: string | null, pack_version: string | null, } | { "type": "minecraft_download", instance_id: string, instance_name: string, } | { "type": "instance_update", instance_id: string, instance_name: string, } | { "type": "zip_extract", instance_id: string, instance_name: string, } | { "type": "config_change", new_path: string, } | { "type": "copy_instance", import_location: string, instance_name: string, } | { "type": "launcher_update", version: string, current_version: string, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { LoadingBarType } from "./LoadingBarType";
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: number | null, message: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Log4jEvent = { timestamp_millis: number | null, logger_name: string | null, level: string | null, thread_name: string | null, message: string | null, throwable: string | null, };
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Log4jEvent } from "./Log4jEvent";
export type LogPayload = { instance_id: string, } & ({ "type": "log4j" } & Log4jEvent | { "type": "legacy", message: string, });
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ModLoader = "vanilla" | "forge" | "fabric" | "quilt" | "neoforge";
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ProcessPayloadType } from "./ProcessPayloadType";
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string, };
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type ProcessPayloadType = "launched" | "finished";
@@ -0,0 +1,8 @@
# App event bindings
> [!WARNING]
> Do not edit the generated TypeScript or Postcard binding files in this directory manually.
The event bus types are determined by the Rust [`AppEvent`](../../../../../packages/app-lib/src/event/mod.rs) contract.
They are regenerated automatically when you run `pnpm app:dev` from the workspace root. Commit any generated changes with the Rust contract change.
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SharedInstanceUnavailableReason = "deleted" | "access_revoked" | "quarantined";
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type WarningPayload = { message: string, };
@@ -0,0 +1,78 @@
declare type u8 = number
declare type u16 = number
declare type u32 = number
declare type u64 = bigint
declare type u128 = bigint
declare type usize = bigint
declare type i8 = number
declare type i16 = number
declare type i32 = number
declare type i64 = bigint
declare type i128 = bigint
declare type isize = bigint
declare type NonZeroU8 = number
declare type NonZeroU16 = number
declare type NonZeroU32 = number
declare type NonZeroU64 = bigint
declare type NonZeroU128 = bigint
declare type NonZeroUsize = bigint
declare type NonZeroI8 = number
declare type NonZeroI16 = number
declare type NonZeroI32 = number
declare type NonZeroI64 = bigint
declare type NonZeroI128 = bigint
declare type NonZeroIsize = bigint
declare type f32 = number
declare type f64 = number
declare type ArrayLengthMutationKeys = "splice" | "push" | "pop" | "shift" | "unshift"
declare type FixedLengthArray<T, L extends number, TObj = [T, ...Array<T>]> =
Pick<TObj, Exclude<keyof TObj, ArrayLengthMutationKeys>>
& {
readonly length: L
[ I : number ] : T
[Symbol.iterator]: () => IterableIterator<T>
}
export type AppEvent = { tag: "loading", value: LoadingPayload } | { tag: "process", value: ProcessPayload } | { tag: "instance", value: InstancePayload } | { tag: "instance_bulk_update_progress", value: InstanceBulkUpdateProgressPayload } | { tag: "install_job", value: InstallJobSnapshot } | { tag: "command", value: CommandPayload } | { tag: "warning", value: WarningPayload } | { tag: "friend", value: FriendPayload } | { tag: "notification", value: string } | { tag: "log", value: LogPayload } | { tag: "ads_consent_required", value: boolean }
export type LoadingBarType = { tag: "legacy_data_migration" } | { tag: "directory_move", value: { old: string, new: string } } | { tag: "java_download", value: { version: u32 } } | { tag: "pack_file_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_version: string } } | { tag: "pack_download", value: { instance_id: string, pack_name: string, icon: string | undefined, pack_id: string | undefined, pack_version: string | undefined } } | { tag: "minecraft_download", value: { instance_id: string, instance_name: string } } | { tag: "instance_update", value: { instance_id: string, instance_name: string } } | { tag: "zip_extract", value: { instance_id: string, instance_name: string } } | { tag: "config_change", value: { new_path: string } } | { tag: "copy_instance", value: { import_location: string, instance_name: string } } | { tag: "launcher_update", value: { version: string, current_version: string } }
export type LoadingPayload = { event: LoadingBarType, loader_uuid: string, fraction: f64 | undefined, message: string }
export type WarningPayload = { message: string }
export type InstanceBulkUpdateProgressPayload = { instanceId: string, stage: InstanceBulkUpdateProgressStage, current: u64, total: u64 }
export type InstanceBulkUpdateProgressStage = { tag: "resolving_versions" } | { tag: "downloading" } | { tag: "finishing" }
export type CommandPayload = { tag: "InstallMod", value: { id: string } } | { tag: "InstallVersion", value: { id: string } } | { tag: "InstallModpack", value: { id: string } } | { tag: "InstallServer", value: { id: string } } | { tag: "LaunchInstance", value: { id: string, server: string | undefined, singleplayer_world: string | undefined } } | { tag: "InstallSharedInstanceInvite", value: { invite_id: string } } | { tag: "RunMRPack", value: { path: string } }
export type ProcessPayload = { instance_id: string, uuid: string, event: ProcessPayloadType, message: string }
export type ProcessPayloadType = { tag: "launched" } | { tag: "finished" }
export type InstancePayload = { instance_id: string, event: InstancePayloadType }
export type InstancePayloadType = { tag: "created" } | { tag: "synced" } | { tag: "servers_updated" } | { tag: "world_updated", value: { world: string } } | { tag: "server_joined", value: { host: string, port: u16, timestamp: string } } | { tag: "edited" } | { tag: "content_install_finished", value: { project_ids: string[] } } | { tag: "content_install_failed", value: { project_ids: string[], message: string } } | { tag: "removed" }
export type FriendPayload = { tag: "friend_request", value: { from: string } } | { tag: "user_offline", value: { id: string } } | { tag: "status_update", value: { user_status: FriendStatusPayload } } | { tag: "status_sync" }
export type FriendStatusPayload = { user_id: string, profile_name: string | undefined, last_update: string }
export type SharedInstanceUnavailableReason = { tag: "deleted" } | { tag: "access_revoked" } | { tag: "quarantined" }
export type LogEvent = { tag: "log4j", value: Log4jEvent } | { tag: "legacy", value: { message: string } }
export type LogPayload = { instance_id: string, event: LogEvent }
export type Log4jEvent = { timestamp_millis: i64 | undefined, logger_name: string | undefined, level: string | undefined, thread_name: string | undefined, message: string | undefined, throwable: string | undefined }
export type ModLoader = { tag: "vanilla" } | { tag: "forge" } | { tag: "fabric" } | { tag: "quilt" } | { tag: "neoforge" }
export type InstallJobSnapshot = { job_id: string, instance_id: string | undefined, kind: InstallJobKind, status: InstallJobStatus, target: InstallTarget, phase: InstallPhaseId, progress: InstallProgress | undefined, details: InstallPhaseDetails, display: InstallJobDisplay | undefined, error: InstallErrorView | undefined, rollback_error: InstallErrorView | undefined, created: string, modified: string, finished: string | undefined }
export type InstallJobKind = { tag: "create_instance" } | { tag: "create_modpack_instance" } | { tag: "create_shared_instance" } | { tag: "import_instance" } | { tag: "duplicate_instance" } | { tag: "install_existing_instance" } | { tag: "install_pack_to_existing_instance" } | { tag: "update_shared_instance" }
export type InstallJobStatus = { tag: "queued" } | { tag: "running" } | { tag: "succeeded" } | { tag: "failed" } | { tag: "interrupted" } | { tag: "canceled" }
export type InstallTarget = { tag: "new_instance", value: { instance_id: string | undefined } } | { tag: "existing_instance", value: { instance_id: string } }
export type InstallPhaseId = { tag: "preparing_instance" } | { tag: "resolving_pack" } | { tag: "downloading_pack_file" } | { tag: "reading_pack_manifest" } | { tag: "downloading_content" } | { tag: "extracting_overrides" } | { tag: "resolving_minecraft" } | { tag: "resolving_loader" } | { tag: "preparing_java" } | { tag: "downloading_minecraft" } | { tag: "running_loader_processors" } | { tag: "finalizing" } | { tag: "rolling_back" }
export type InstallProgress = { current: u64, total: u64, secondary: InstallProgressSecondary | undefined }
export type InstallProgressSecondary = { current: u64, total: u64 }
export type InstallPhaseDetails = { tag: "empty" } | { tag: "instance", value: { name: string } } | { tag: "minecraft", value: { game_version: string, loader: ModLoader } } | { tag: "java", value: { major_version: u32, step: InstallJavaStep } } | { tag: "modpack", value: { project_id: string | undefined, version_id: string | undefined, title: string | undefined } } | { tag: "import", value: { launcher_type: ImportLauncherType, instance_folder: string } }
export type InstallJavaStep = { tag: "resolving" } | { tag: "fetching_metadata" } | { tag: "downloading" } | { tag: "extracting" } | { tag: "validating" }
export type InstallJobDisplay = { title: string, icon: string | undefined }
export type InstallErrorView = { code: string, phase: InstallPhaseId | undefined, message: string, reason: SharedInstanceUnavailableReason | undefined, api: InstallApiErrorDetails | undefined, context: InstallErrorContext | undefined }
export type InstallApiErrorDetails = { error: string, status: u16 | undefined, method: string | undefined, url: string | undefined, route: string | undefined }
export type InstallErrorContext = { operation: string, source_path: string | undefined, target_path: string | undefined, file_path: string | undefined, entry_path: string | undefined, urls: string[], expected_hash: string | undefined, expected_size: u64 | undefined, project_id: string | undefined, version_id: string | undefined, minecraft_version: string | undefined, loader: string | undefined, java_version: u32 | undefined, os: string | undefined, arch: string | undefined }
export type ImportLauncherType = { tag: "MultiMC" } | { tag: "PrismLauncher" } | { tag: "ATLauncher" } | { tag: "GDLauncher" } | { tag: "Curseforge" } | { tag: "Unknown" }
export type Type = "AppEvent" | "LoadingBarType" | "LoadingPayload" | "WarningPayload" | "InstanceBulkUpdateProgressPayload" | "InstanceBulkUpdateProgressStage" | "CommandPayload" | "ProcessPayload" | "ProcessPayloadType" | "InstancePayload" | "InstancePayloadType" | "FriendPayload" | "FriendStatusPayload" | "LogEvent" | "LogPayload" | "Log4jEvent" | "InstallJobSnapshot" | "InstallJobKind" | "InstallJobStatus" | "InstallTarget" | "InstallPhaseId" | "InstallProgress" | "InstallProgressSecondary" | "InstallPhaseDetails" | "InstallJavaStep" | "InstallJobDisplay" | "InstallErrorView" | "InstallApiErrorDetails" | "InstallErrorContext" | "ImportLauncherType" | "ModLoader" | "SharedInstanceUnavailableReason"
declare type ValueType<T extends Type> = T extends "AppEvent" ? AppEvent : T extends "LoadingBarType" ? LoadingBarType : T extends "LoadingPayload" ? LoadingPayload : T extends "WarningPayload" ? WarningPayload : T extends "InstanceBulkUpdateProgressPayload" ? InstanceBulkUpdateProgressPayload : T extends "InstanceBulkUpdateProgressStage" ? InstanceBulkUpdateProgressStage : T extends "CommandPayload" ? CommandPayload : T extends "ProcessPayload" ? ProcessPayload : T extends "ProcessPayloadType" ? ProcessPayloadType : T extends "InstancePayload" ? InstancePayload : T extends "InstancePayloadType" ? InstancePayloadType : T extends "FriendPayload" ? FriendPayload : T extends "FriendStatusPayload" ? FriendStatusPayload : T extends "LogEvent" ? LogEvent : T extends "LogPayload" ? LogPayload : T extends "Log4jEvent" ? Log4jEvent : T extends "InstallJobSnapshot" ? InstallJobSnapshot : T extends "InstallJobKind" ? InstallJobKind : T extends "InstallJobStatus" ? InstallJobStatus : T extends "InstallTarget" ? InstallTarget : T extends "InstallPhaseId" ? InstallPhaseId : T extends "InstallProgress" ? InstallProgress : T extends "InstallProgressSecondary" ? InstallProgressSecondary : T extends "InstallPhaseDetails" ? InstallPhaseDetails : T extends "InstallJavaStep" ? InstallJavaStep : T extends "InstallJobDisplay" ? InstallJobDisplay : T extends "InstallErrorView" ? InstallErrorView : T extends "InstallApiErrorDetails" ? InstallApiErrorDetails : T extends "InstallErrorContext" ? InstallErrorContext : T extends "ImportLauncherType" ? ImportLauncherType : T extends "ModLoader" ? ModLoader : T extends "SharedInstanceUnavailableReason" ? SharedInstanceUnavailableReason : void
export interface Result<T extends Type> {
value: ValueType<T>;
bytes: Uint8Array;
}
export function deserialize<T extends Type>(type: T, bytes: Uint8Array): Result<T>
@@ -0,0 +1,953 @@
const BITS_PER_BYTE = 8, BITS_PER_VARINT_BYTE = 7, U8_BYTES = 1, U16_BYTES = 2, U32_BYTES = 4, U64_BYTES = 8, U128_BYTES = 16
const de_zig_zag_signed = (n) => (n >> 1n) ^ (-(n & 0b1n))
const zig_zag = (n_bytes, n) => (n << 1n) ^ (n >> BigInt(n_bytes * BITS_PER_BYTE - 1))
const varint_max = (n_bytes) => Math.floor((n_bytes * BITS_PER_BYTE + (BITS_PER_BYTE - 1)) / BITS_PER_VARINT_BYTE)
const max_of_last_byte = (n_bytes) => (1 << (n_bytes * BITS_PER_BYTE) % 7) - 1
const to_number_if_safe = (n) => Number.MAX_SAFE_INTEGER < ((n < 0n) ? -n : n) ? n : Number(n)
const varint = (n_bytes, n) => { let value = BigInt(n), out = []; for (let i = 0; i < varint_max(n_bytes); i++) { out.push(Number(value & 0xFFn)); if (value < 128n) { return out } out[i] |= 0x80; value >>= 7n } }
class Deserializer {
constructor(bytes_in) { this.bytes = Array.from(bytes_in); }
pop_next = () => { const next = this.bytes.shift(); if (next === undefined) { throw "input buffer too small" } return next }
pop_n = (n) => { const bytes = Array(); for (let i = 0; i < n; i++) { bytes.push(this.bytes.shift()) } return bytes }
get_int8 = (signed) => signed ? new Int8Array([this.pop_next()])[0] : this.pop_next();
try_take = (n_bytes) => { let out = 0n, v_max = varint_max(n_bytes); for (let i = 0; i < v_max; i++) { const val = this.pop_next(), carry = BigInt(val & 0x7F); out |= carry << BigInt(7 * i); if ((val & 0x80) === 0) { if (i === v_max - 1 && val > max_of_last_byte(n_bytes)) { throw "Bad Variant" } else return out } } throw "Bad Variant"; }
deserialize_bool = () => { const byte = this.pop_next(); return byte === undefined ? undefined : byte > 0 ? true : false }
deserialize_number = (n_bytes, signed) => { if (n_bytes === U8_BYTES) { return this.get_int8(signed) } else if (n_bytes === U16_BYTES || n_bytes === U32_BYTES || n_bytes === U64_BYTES || n_bytes === U128_BYTES) { const val = this.try_take(n_bytes); return to_number_if_safe(signed ? de_zig_zag_signed(val) : val) } else { throw "byte count not supported" } }
deserialize_number_float = (n_bytes) => { const b_buffer = new ArrayBuffer(n_bytes), b_view = new DataView(b_buffer); this.pop_n(n_bytes).forEach((b, i) => b_view.setUint8(i, b)); if (n_bytes === U32_BYTES) { return b_view.getFloat32(0, true) } else if (n_bytes === U64_BYTES) { return b_view.getFloat64(0, true) } else { throw "byte count not supported" } }
deserialize_string = () => new TextDecoder().decode(new Uint8Array(this.pop_n(Number(this.try_take(U32_BYTES)))))
deserialize_array = (des, len) => Array.from({length: len === undefined ? Number(this.try_take(U32_BYTES)) : len}, (v, i) => des(this))
deserialize_string_key_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { prev[this.deserialize_string()] = des(this); return prev }, {}) }
deserialize_map = (des) => { return [...Array(Number(this.try_take(U32_BYTES)))].reduce((prev) => { const d = des(this); prev.set(d[0], d[1]); return prev }, new Map()) }
release_bytes = () => { return new Uint8Array(this.bytes); }
}
function deserialize_APP_EVENT(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "loading",
value: deserialize_LOADING_PAYLOAD(d)
};
case 1:
return {
tag: "process",
value: deserialize_PROCESS_PAYLOAD(d)
};
case 2:
return {
tag: "instance",
value: deserialize_INSTANCE_PAYLOAD(d)
};
case 3:
return {
tag: "instance_bulk_update_progress",
value: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d)
};
case 4:
return {
tag: "install_job",
value: deserialize_INSTALL_JOB_SNAPSHOT(d)
};
case 5:
return {
tag: "command",
value: deserialize_COMMAND_PAYLOAD(d)
};
case 6:
return {
tag: "warning",
value: deserialize_WARNING_PAYLOAD(d)
};
case 7:
return {
tag: "friend",
value: deserialize_FRIEND_PAYLOAD(d)
};
case 8:
return {
tag: "notification",
value: d.deserialize_string()
};
case 9:
return {
tag: "log",
value: deserialize_LOG_PAYLOAD(d)
};
case 10:
return {
tag: "ads_consent_required",
value: d.deserialize_bool()
};
default:
throw "variant not implemented"
}
}
function deserialize_LOADING_BAR_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "legacy_data_migration"
};
case 1:
return {
tag: "directory_move",
value: {
old: d.deserialize_string(),
new: d.deserialize_string()
}
};
case 2:
return {
tag: "java_download",
value: {
version: d.deserialize_number(U32_BYTES, false)
}
};
case 3:
return {
tag: "pack_file_download",
value: {
instance_id: d.deserialize_string(),
pack_name: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_version: d.deserialize_string()
}
};
case 4:
return {
tag: "pack_download",
value: {
instance_id: d.deserialize_string(),
pack_name: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
pack_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "minecraft_download",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 6:
return {
tag: "instance_update",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 7:
return {
tag: "zip_extract",
value: {
instance_id: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 8:
return {
tag: "config_change",
value: {
new_path: d.deserialize_string()
}
};
case 9:
return {
tag: "copy_instance",
value: {
import_location: d.deserialize_string(),
instance_name: d.deserialize_string()
}
};
case 10:
return {
tag: "launcher_update",
value: {
version: d.deserialize_string(),
current_version: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_LOADING_PAYLOAD(d) {
return {
event: deserialize_LOADING_BAR_TYPE(d),
loader_uuid: d.deserialize_string(),
fraction: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number_float(U64_BYTES),
message: d.deserialize_string()
};
}
function deserialize_WARNING_PAYLOAD(d) {
return {
message: d.deserialize_string()
};
}
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d) {
return {
instanceId: d.deserialize_string(),
stage: deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d),
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false)
};
}
function deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "resolving_versions"
};
case 1:
return {
tag: "downloading"
};
case 2:
return {
tag: "finishing"
};
default:
throw "variant not implemented"
}
}
function deserialize_COMMAND_PAYLOAD(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "InstallMod",
value: {
id: d.deserialize_string()
}
};
case 1:
return {
tag: "InstallVersion",
value: {
id: d.deserialize_string()
}
};
case 2:
return {
tag: "InstallModpack",
value: {
id: d.deserialize_string()
}
};
case 3:
return {
tag: "InstallServer",
value: {
id: d.deserialize_string()
}
};
case 4:
return {
tag: "LaunchInstance",
value: {
id: d.deserialize_string(),
server: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
singleplayer_world: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "InstallSharedInstanceInvite",
value: {
invite_id: d.deserialize_string()
}
};
case 6:
return {
tag: "RunMRPack",
value: {
path: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_PROCESS_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
uuid: d.deserialize_string(),
event: deserialize_PROCESS_PAYLOAD_TYPE(d),
message: d.deserialize_string()
};
}
function deserialize_PROCESS_PAYLOAD_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "launched"
};
case 1:
return {
tag: "finished"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTANCE_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
event: deserialize_INSTANCE_PAYLOAD_TYPE(d)
};
}
function deserialize_INSTANCE_PAYLOAD_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "created"
};
case 1:
return {
tag: "synced"
};
case 2:
return {
tag: "servers_updated"
};
case 3:
return {
tag: "world_updated",
value: {
world: d.deserialize_string()
}
};
case 4:
return {
tag: "server_joined",
value: {
host: d.deserialize_string(),
port: d.deserialize_number(U16_BYTES, false),
timestamp: d.deserialize_string()
}
};
case 5:
return {
tag: "edited"
};
case 6:
return {
tag: "content_install_finished",
value: {
project_ids: d.deserialize_array(() => d.deserialize_string())
}
};
case 7:
return {
tag: "content_install_failed",
value: {
project_ids: d.deserialize_array(() => d.deserialize_string()),
message: d.deserialize_string()
}
};
case 8:
return {
tag: "removed"
};
default:
throw "variant not implemented"
}
}
function deserialize_FRIEND_PAYLOAD(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "friend_request",
value: {
from: d.deserialize_string()
}
};
case 1:
return {
tag: "user_offline",
value: {
id: d.deserialize_string()
}
};
case 2:
return {
tag: "status_update",
value: {
user_status: deserialize_FRIEND_STATUS_PAYLOAD(d)
}
};
case 3:
return {
tag: "status_sync"
};
default:
throw "variant not implemented"
}
}
function deserialize_FRIEND_STATUS_PAYLOAD(d) {
return {
user_id: d.deserialize_string(),
profile_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
last_update: d.deserialize_string()
};
}
function deserialize_LOG_EVENT(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "log4j",
value: deserialize_LOG4J_EVENT(d)
};
case 1:
return {
tag: "legacy",
value: {
message: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_LOG_PAYLOAD(d) {
return {
instance_id: d.deserialize_string(),
event: deserialize_LOG_EVENT(d)
};
}
function deserialize_LOG4J_EVENT(d) {
return {
timestamp_millis: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, true),
logger_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
level: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
thread_name: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
message: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
throwable: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_JOB_SNAPSHOT(d) {
return {
job_id: d.deserialize_string(),
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
kind: deserialize_INSTALL_JOB_KIND(d),
status: deserialize_INSTALL_JOB_STATUS(d),
target: deserialize_INSTALL_TARGET(d),
phase: deserialize_INSTALL_PHASE_ID(d),
progress: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS(d),
details: deserialize_INSTALL_PHASE_DETAILS(d),
display: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_JOB_DISPLAY(d),
error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
rollback_error: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_VIEW(d),
created: d.deserialize_string(),
modified: d.deserialize_string(),
finished: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_JOB_KIND(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "create_instance"
};
case 1:
return {
tag: "create_modpack_instance"
};
case 2:
return {
tag: "create_shared_instance"
};
case 3:
return {
tag: "import_instance"
};
case 4:
return {
tag: "duplicate_instance"
};
case 5:
return {
tag: "install_existing_instance"
};
case 6:
return {
tag: "install_pack_to_existing_instance"
};
case 7:
return {
tag: "update_shared_instance"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JOB_STATUS(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "queued"
};
case 1:
return {
tag: "running"
};
case 2:
return {
tag: "succeeded"
};
case 3:
return {
tag: "failed"
};
case 4:
return {
tag: "interrupted"
};
case 5:
return {
tag: "canceled"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_TARGET(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "new_instance",
value: {
instance_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 1:
return {
tag: "existing_instance",
value: {
instance_id: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_PHASE_ID(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "preparing_instance"
};
case 1:
return {
tag: "resolving_pack"
};
case 2:
return {
tag: "downloading_pack_file"
};
case 3:
return {
tag: "reading_pack_manifest"
};
case 4:
return {
tag: "downloading_content"
};
case 5:
return {
tag: "extracting_overrides"
};
case 6:
return {
tag: "resolving_minecraft"
};
case 7:
return {
tag: "resolving_loader"
};
case 8:
return {
tag: "preparing_java"
};
case 9:
return {
tag: "downloading_minecraft"
};
case 10:
return {
tag: "running_loader_processors"
};
case 11:
return {
tag: "finalizing"
};
case 12:
return {
tag: "rolling_back"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_PROGRESS(d) {
return {
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false),
secondary: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PROGRESS_SECONDARY(d)
};
}
function deserialize_INSTALL_PROGRESS_SECONDARY(d) {
return {
current: d.deserialize_number(U64_BYTES, false),
total: d.deserialize_number(U64_BYTES, false)
};
}
function deserialize_INSTALL_PHASE_DETAILS(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "empty"
};
case 1:
return {
tag: "instance",
value: {
name: d.deserialize_string()
}
};
case 2:
return {
tag: "minecraft",
value: {
game_version: d.deserialize_string(),
loader: deserialize_MOD_LOADER(d)
}
};
case 3:
return {
tag: "java",
value: {
major_version: d.deserialize_number(U32_BYTES, false),
step: deserialize_INSTALL_JAVA_STEP(d)
}
};
case 4:
return {
tag: "modpack",
value: {
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
title: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
}
};
case 5:
return {
tag: "import",
value: {
launcher_type: deserialize_IMPORT_LAUNCHER_TYPE(d),
instance_folder: d.deserialize_string()
}
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JAVA_STEP(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "resolving"
};
case 1:
return {
tag: "fetching_metadata"
};
case 2:
return {
tag: "downloading"
};
case 3:
return {
tag: "extracting"
};
case 4:
return {
tag: "validating"
};
default:
throw "variant not implemented"
}
}
function deserialize_INSTALL_JOB_DISPLAY(d) {
return {
title: d.deserialize_string(),
icon: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_ERROR_VIEW(d) {
return {
code: d.deserialize_string(),
phase: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_PHASE_ID(d),
message: d.deserialize_string(),
reason: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d),
api: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_API_ERROR_DETAILS(d),
context: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : deserialize_INSTALL_ERROR_CONTEXT(d)
};
}
function deserialize_INSTALL_API_ERROR_DETAILS(d) {
return {
error: d.deserialize_string(),
status: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U16_BYTES, false),
method: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
url: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
route: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_INSTALL_ERROR_CONTEXT(d) {
return {
operation: d.deserialize_string(),
source_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
target_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
file_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
entry_path: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
urls: d.deserialize_array(() => d.deserialize_string()),
expected_hash: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
expected_size: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U64_BYTES, false),
project_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
version_id: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
minecraft_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
loader: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
java_version: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_number(U32_BYTES, false),
os: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string(),
arch: (d.deserialize_number(U32_BYTES, false) === 0) ? undefined : d.deserialize_string()
};
}
function deserialize_IMPORT_LAUNCHER_TYPE(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "MultiMC"
};
case 1:
return {
tag: "PrismLauncher"
};
case 2:
return {
tag: "ATLauncher"
};
case 3:
return {
tag: "GDLauncher"
};
case 4:
return {
tag: "Curseforge"
};
case 5:
return {
tag: "Unknown"
};
default:
throw "variant not implemented"
}
}
function deserialize_MOD_LOADER(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "vanilla"
};
case 1:
return {
tag: "forge"
};
case 2:
return {
tag: "fabric"
};
case 3:
return {
tag: "quilt"
};
case 4:
return {
tag: "neoforge"
};
default:
throw "variant not implemented"
}
}
function deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d) {
switch (d.deserialize_number(U32_BYTES, false)) {
case 0:
return {
tag: "deleted"
};
case 1:
return {
tag: "access_revoked"
};
case 2:
return {
tag: "quarantined"
};
default:
throw "variant not implemented"
}
}
/**
* Deserialize a value from an array of bytes.
* @param {string} type - The type of the value to deserialize.
* @param {Uint8Array} bytes - The byte array to deserialize from.
* @return {Object} The deserialized value and remaining bytes.
*/
function deserialize(type, bytes) {
if (!(typeof type === "string")) {
throw "type must be a string";
}
const d = new Deserializer(bytes);
var return_value = undefined;
switch (type) {
case "AppEvent":
return_value = deserialize_APP_EVENT(d);
break;
case "LoadingBarType":
return_value = deserialize_LOADING_BAR_TYPE(d);
break;
case "LoadingPayload":
return_value = deserialize_LOADING_PAYLOAD(d);
break;
case "WarningPayload":
return_value = deserialize_WARNING_PAYLOAD(d);
break;
case "InstanceBulkUpdateProgressPayload":
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_PAYLOAD(d);
break;
case "InstanceBulkUpdateProgressStage":
return_value = deserialize_INSTANCE_BULK_UPDATE_PROGRESS_STAGE(d);
break;
case "CommandPayload":
return_value = deserialize_COMMAND_PAYLOAD(d);
break;
case "ProcessPayload":
return_value = deserialize_PROCESS_PAYLOAD(d);
break;
case "ProcessPayloadType":
return_value = deserialize_PROCESS_PAYLOAD_TYPE(d);
break;
case "InstancePayload":
return_value = deserialize_INSTANCE_PAYLOAD(d);
break;
case "InstancePayloadType":
return_value = deserialize_INSTANCE_PAYLOAD_TYPE(d);
break;
case "FriendPayload":
return_value = deserialize_FRIEND_PAYLOAD(d);
break;
case "FriendStatusPayload":
return_value = deserialize_FRIEND_STATUS_PAYLOAD(d);
break;
case "LogEvent":
return_value = deserialize_LOG_EVENT(d);
break;
case "LogPayload":
return_value = deserialize_LOG_PAYLOAD(d);
break;
case "Log4jEvent":
return_value = deserialize_LOG4J_EVENT(d);
break;
case "InstallJobSnapshot":
return_value = deserialize_INSTALL_JOB_SNAPSHOT(d);
break;
case "InstallJobKind":
return_value = deserialize_INSTALL_JOB_KIND(d);
break;
case "InstallJobStatus":
return_value = deserialize_INSTALL_JOB_STATUS(d);
break;
case "InstallTarget":
return_value = deserialize_INSTALL_TARGET(d);
break;
case "InstallPhaseId":
return_value = deserialize_INSTALL_PHASE_ID(d);
break;
case "InstallProgress":
return_value = deserialize_INSTALL_PROGRESS(d);
break;
case "InstallProgressSecondary":
return_value = deserialize_INSTALL_PROGRESS_SECONDARY(d);
break;
case "InstallPhaseDetails":
return_value = deserialize_INSTALL_PHASE_DETAILS(d);
break;
case "InstallJavaStep":
return_value = deserialize_INSTALL_JAVA_STEP(d);
break;
case "InstallJobDisplay":
return_value = deserialize_INSTALL_JOB_DISPLAY(d);
break;
case "InstallErrorView":
return_value = deserialize_INSTALL_ERROR_VIEW(d);
break;
case "InstallApiErrorDetails":
return_value = deserialize_INSTALL_API_ERROR_DETAILS(d);
break;
case "InstallErrorContext":
return_value = deserialize_INSTALL_ERROR_CONTEXT(d);
break;
case "ImportLauncherType":
return_value = deserialize_IMPORT_LAUNCHER_TYPE(d);
break;
case "ModLoader":
return_value = deserialize_MOD_LOADER(d);
break;
case "SharedInstanceUnavailableReason":
return_value = deserialize_SHARED_INSTANCE_UNAVAILABLE_REASON(d);
break;
default:
throw "type not implemented";
}
return { value: return_value, bytes: d.release_bytes() };
}
export {
deserialize
};
@@ -0,0 +1,8 @@
{
"name": "postcard",
"description": "Auto generated bindings for postcard format serializing and deserializing javascript to and from bytes.",
"version": "0.0.0",
"main": "index.js",
"types": "index.d.ts",
"type": "module"
}
-5
View File
@@ -1,5 +1,4 @@
import { invoke } from '@tauri-apps/api/core'
import { listen } from '@tauri-apps/api/event'
export async function init_ads_window(overrideShown = false) {
return await invoke('plugin:ads|init_ads_window', {
@@ -47,10 +46,6 @@ export async function open_ads_consent_preferences() {
return await invoke('plugin:ads|open_ads_consent_preferences')
}
export async function ads_consent_listener(callback) {
return await listen('ads-consent-required', (event) => callback(event.payload))
}
export async function record_ads_click() {
return await invoke('plugin:ads|record_ads_click')
}
-137
View File
@@ -1,137 +0,0 @@
/*
Event listeners for interacting with the Rust api
These are all async functions that return a promise that resolves to the payload object (whatever Rust is trying to deliver)
*/
/*
callback is a function that takes a single argument, which is the payload object (whatever Rust is trying to deliver)
You can call these to await any kind of emitted signal from Rust, and then do something with the payload object
An example place to put this is at the start of main.js before the state is initialized- that way
you can listen for any emitted signal from Rust and do something with it as the state is being initialized
Example:
import { loading_listener } from '@/helpers/events'
await loading_listener((event) => {
// event.event is the event name (useful if you want to use a single callback fn for multiple event types)
// event.payload is the payload object
console.log(event)
})
Putting that in a script will print any emitted signal from rust
*/
import { listen } from '@tauri-apps/api/event'
/// Payload for the 'loading' event
/*
LoadingPayload {
event: {
type: string, one of "StateInit", "PackDownload", etc
(Optional fields depending on event type)
pack_name: name of the pack
pack_id, optional, the id of the modpack
pack_version, optional, the version of the modpack
instance_name: name of the instance
instance_id: unique identification of the instance
}
loader_uuid: unique identification of the loading bar
fraction: number, (as a fraction of 1, how much we've loaded so far). If null, by convention, loading is finished
message: message to display to the user
}
*/
export async function loading_listener(callback) {
return await listen('loading', (event) => callback(event.payload))
}
/// Payload for the 'process' event
/*
ProcessPayload {
uuid: unique identification of the process in the state (currently identified by PID, but that will change)
pid: process ID
event: event type ("Launched", "Finished")
message: message to display to the user
}
*/
export async function process_listener(callback) {
return await listen('process', (event) => callback(event.payload))
}
/// Payload for the 'instance' event
/*
InstancePayload {
instance_id: unique identification of the instance
event: event type ("Created", "Added", "Edited", "Removed")
}
*/
export async function instance_listener(callback) {
return await listen('instance', (event) => callback(event.payload))
}
/// Payload for the 'instance_bulk_update_progress' event
/*
InstanceBulkUpdateProgress {
instanceId: string
stage: "resolving_versions" | "downloading" | "finishing"
current: number
total: number
}
*/
export async function instance_bulk_update_progress_listener(callback) {
return await listen('instance_bulk_update_progress', (event) => callback(event.payload))
}
export async function install_job_listener(callback) {
return await listen('install_job', (event) => callback(event.payload))
}
/// Payload for the 'command' event
/*
CommandPayload {
event: event type ("InstallMod", "InstallModpack", "InstallVersion"),
id: string id of the mod/modpack/version to install
}
*/
export async function command_listener(callback) {
return await listen('command', (event) => {
callback(event.payload)
})
}
/// Payload for the 'warning' event
/*
WarningPayload {
message: message to display to the user
}
*/
export async function warning_listener(callback) {
return await listen('warning', (event) => callback(event.payload))
}
export async function friend_listener(callback) {
return await listen('friend', (event) => callback(event.payload))
}
export async function notification_listener(callback) {
return await listen('notification', (event) => callback(event.payload))
}
/// Payload for the 'log' event
/*
LogPayload {
instance_id: string,
type: "log4j" | "legacy",
// log4j fields (when type === "log4j"):
timestamp_millis?: number,
logger_name?: string,
level?: string,
thread_name?: string,
message?: string,
throwable?: string,
// legacy fields (when type === "legacy"):
message?: string,
}
*/
export async function log_listener(callback) {
return await listen('log', (event) => callback(event.payload))
}
+24 -125
View File
@@ -1,8 +1,28 @@
import { invoke } from '@tauri-apps/api/core'
import { install_job_listener } from './events'
import type { InstallErrorView } from '@/generated/app-events/InstallErrorView'
import type { InstallJavaStep } from '@/generated/app-events/InstallJavaStep'
import type { InstallJobSnapshot } from '@/generated/app-events/InstallJobSnapshot'
import type { InstallJobStatus } from '@/generated/app-events/InstallJobStatus'
import type { InstallPhaseId } from '@/generated/app-events/InstallPhaseId'
import type { InstallProgress } from '@/generated/app-events/InstallProgress'
import type { InstallProgressSecondary } from '@/generated/app-events/InstallProgressSecondary'
import type { SharedInstanceUnavailableReason } from '@/generated/app-events/SharedInstanceUnavailableReason'
import type { AppEvents } from '@/providers/app-events'
import type { InstanceLink, InstanceLoader } from './types'
export type {
InstallErrorView,
InstallJavaStep,
InstallJobSnapshot,
InstallJobStatus,
InstallPhaseId,
InstallProgress,
InstallProgressSecondary,
SharedInstanceUnavailableReason,
}
export interface PackLocationVersionId {
type: 'fromVersionId'
project_id: string
@@ -104,8 +124,6 @@ export interface SharedInstanceUpdateDiff {
export const SHARED_INSTANCE_UNAVAILABLE_ERROR_CODE = 'shared_instance_unavailable'
export const SHARED_INSTANCES_API_ERROR_CODE = 'shared_instances_api_error'
export type SharedInstanceUnavailableReason = 'deleted' | 'access_revoked' | 'quarantined'
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
}
@@ -136,116 +154,6 @@ export function getErrorMessage(error: unknown): string {
return 'Unknown error'
}
export type InstallJobStatus =
| 'queued'
| 'running'
| 'succeeded'
| 'failed'
| 'interrupted'
| 'canceled'
export type InstallPhaseId =
| 'preparing_instance'
| 'resolving_pack'
| 'downloading_pack_file'
| 'reading_pack_manifest'
| 'downloading_content'
| 'extracting_overrides'
| 'resolving_minecraft'
| 'resolving_loader'
| 'preparing_java'
| 'downloading_minecraft'
| 'running_loader_processors'
| 'finalizing'
| 'rolling_back'
export interface InstallProgress {
current: number
total: number
secondary?: InstallProgressSecondary | null
}
export interface InstallProgressSecondary {
current: number
total: number
}
export type InstallJavaStep =
| 'resolving'
| 'fetching_metadata'
| 'downloading'
| 'extracting'
| 'validating'
export interface InstallErrorView {
code: string
phase?: InstallPhaseId | null
message: string
reason?: SharedInstanceUnavailableReason | null
api?: {
error: string
status?: number | null
method?: string | null
url?: string | null
route?: string | null
} | null
context?: {
operation: string
source_path?: string | null
target_path?: string | null
file_path?: string | null
entry_path?: string | null
urls?: string[]
expected_hash?: string | null
expected_size?: number | null
project_id?: string | null
version_id?: string | null
minecraft_version?: string | null
loader?: string | null
java_version?: number | null
os?: string | null
arch?: string | null
} | null
}
export interface InstallJobSnapshot {
job_id: string
instance_id?: string | null
kind:
| 'create_instance'
| 'create_modpack_instance'
| 'create_shared_instance'
| 'update_shared_instance'
| 'import_instance'
| 'duplicate_instance'
| 'install_existing_instance'
| 'install_pack_to_existing_instance'
status: InstallJobStatus
target:
| { type: 'new_instance'; instance_id?: string | null }
| { type: 'existing_instance'; instance_id: string }
phase: InstallPhaseId
progress?: InstallProgress | null
details:
| { type: 'empty' }
| { type: 'instance'; name: string }
| { type: 'minecraft'; game_version: string; loader: InstanceLoader }
| { type: 'java'; major_version: number; step: InstallJavaStep }
| {
type: 'modpack'
project_id?: string | null
version_id?: string | null
title?: string | null
}
| { type: 'import'; launcher_type: string; instance_folder: string }
display?: { title: string; icon?: string | null } | null
error?: InstallErrorView | null
rollback_error?: InstallErrorView | null
created: string
modified: string
finished?: string | null
}
export async function install_get_modpack_preview(location: CreatePackLocation) {
return await invoke<InstallModpackPreview>('plugin:install|install_get_modpack_preview', {
location,
@@ -399,7 +307,7 @@ function settleInstallJob(job: InstallJobSnapshot) {
throw new Error(`Install job ${job.job_id} ${job.status}`)
}
export async function wait_for_install_job(jobId: string) {
export async function wait_for_install_job(events: AppEvents, jobId: string) {
const current = await install_job_get(jobId)
if (isInstallJobFinished(current.status)) return settleInstallJob(current)
@@ -434,16 +342,7 @@ export async function wait_for_install_job(jobId: string) {
reject(err)
}
install_job_listener(resolveJob)
.then((listener) => {
if (finished) {
listener()
return
}
unlisten = listener
install_job_get(jobId).then(resolveJob).catch(rejectWait)
})
.catch(rejectWait)
unlisten = events.on('install_job', resolveJob)
install_job_get(jobId).then(resolveJob).catch(rejectWait)
})
}
+6 -16
View File
@@ -3,7 +3,9 @@
* So, for example, addDefaultInstance creates a blank instance object, where the Rust struct is serialized,
* and deserialized into a usable JS object.
*/
import { invoke } from '@tauri-apps/api/core'
import { type Channel, invoke } from '@tauri-apps/api/core'
import type { AppEvent } from '@/generated/app-events/AppEvent'
export interface LoadingBarType {
type?: string
@@ -24,24 +26,12 @@ export interface LoadingBar {
bar_type?: LoadingBarType
}
export type OpeningCommandEvent =
| 'RunMRPack'
| 'InstallServer'
| 'InstallVersion'
| 'InstallMod'
| 'InstallModpack'
| string
export interface OpeningCommand {
event: OpeningCommandEvent
id?: string
path?: string
}
export type OpeningCommand = Extract<AppEvent, { type: 'command' }>['payload']
// Initialize the theseus API state
// This should be called during the initializion/opening of the launcher
export async function initialize_state() {
return await invoke<void>('initialize_state')
export async function initialize_state(events: Channel<ArrayBuffer>) {
return await invoke<void>('initialize_state', { events })
}
// Gets active progress bars
+2 -15
View File
@@ -3,6 +3,7 @@ import { autoToHTML } from '@sfirew/minecraft-motd-parser'
import { invoke } from '@tauri-apps/api/core'
import dayjs from 'dayjs'
import type { InstancePayload } from '@/generated/app-events/InstancePayload'
import { get_full_path } from '@/helpers/instance'
import { openPath } from '@/helpers/utils'
@@ -532,18 +533,4 @@ export function hasWorldQuickPlaySupport(gameVersions: GameVersion[], currentVer
return versionIndex !== -1 && targetIndex !== -1 && versionIndex <= targetIndex
}
export type InstanceEvent = { instance_id: string } & (
| {
event: 'servers_updated'
}
| {
event: 'world_updated'
world: string
}
| {
event: 'server_joined'
host: string
port: number
timestamp: string
}
)
export type InstanceEvent = InstancePayload
+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(
@@ -0,0 +1,19 @@
import { createContext } from '@modrinth/ui'
import type { AppEvent } from '@/generated/app-events/AppEvent'
export type AppEventType = AppEvent['type']
export type AppEventPayload<Type extends AppEventType> = Extract<
AppEvent,
{ type: Type }
>['payload']
export type AppEventHandler<Type extends AppEventType> = (
payload: AppEventPayload<Type>,
) => void | Promise<void>
export interface AppEvents {
on<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>): () => void
once<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>): () => void
}
export const [injectAppEvents, provideAppEvents] = createContext<AppEvents>('root', 'appEvents')
@@ -20,7 +20,6 @@ import {
get_team,
get_version_many,
} from '@/helpers/cache.js'
import { instance_listener } from '@/helpers/events.js'
import {
install_create_instance,
install_create_modpack_instance,
@@ -38,6 +37,7 @@ import {
} from '@/helpers/instance'
import { get_game_versions } from '@/helpers/tags'
import type { GameInstance, InstanceLoader } from '@/helpers/types'
import type { AppEvents } from '@/providers/app-events'
import { useTheming } from '@/store/state'
interface ModalRef {
show: (initialVersionId?: string) => void
@@ -60,13 +60,6 @@ type InstallingProjectDisplay = {
organization?: string | null
team?: string
}
type ContentInstallInstanceEvent = {
event: string
instance_id: string
project_ids?: string[]
message?: string
}
const LOADER_ORDER = ['vanilla', 'fabric', 'quilt', 'neoforge', 'forge']
const SUPPORTED_LOADERS: Set<string> = new Set(['vanilla', 'forge', 'fabric', 'quilt', 'neoforge'])
const VANILLA_COMPATIBLE_LOADERS: Set<string> = new Set(['minecraft', 'datapack'])
@@ -191,6 +184,7 @@ export const [injectContentInstall, provideContentInstall] = createContext<Conte
export function createContentInstall(opts: {
router: Router
handleError: (err: unknown) => void
appEvents: AppEvents
}): ContentInstallContext {
const { formatMessage } = useVIntl()
const themeStore = useTheming()
@@ -394,17 +388,17 @@ export function createContentInstall(opts: {
installFailureRevisionByInstance.value = next
}
void instance_listener((event: ContentInstallInstanceEvent) => {
opts.appEvents.on('instance', (event) => {
if (event.event === 'content_install_finished') {
markInstanceContentChanged(event.instance_id)
removeInstallingItems(event.instance_id, event.project_ids ?? [])
removeInstallingItems(event.instance_id, event.project_ids)
} else if (event.event === 'content_install_failed') {
removeInstallingItems(event.instance_id, event.project_ids ?? [])
removeInstallingItems(event.instance_id, event.project_ids)
markInstanceContentInstallFailed(event.instance_id)
markInstanceContentChanged(event.instance_id)
opts.handleError(event.message ?? 'Failed to install content')
opts.handleError(event.message)
}
}).catch(opts.handleError)
})
let modalRef: ModalRef | null = null
let modpackAlreadyInstalledModalRef: ModpackAlreadyInstalledModalRef | null = null
@@ -1,7 +1,7 @@
import { createContext } from '@modrinth/ui'
import type { Ref } from 'vue'
import { loading_listener } from '@/helpers/events'
import type { AppEvents } from '@/providers/app-events'
export interface AppDownloadProgressContext {
progress: Ref<number>
@@ -10,26 +10,19 @@ export interface AppDownloadProgressContext {
/* returns unlisten function */
export async function subscribeToDownloadProgress(
events: AppEvents,
context: AppDownloadProgressContext,
version: string,
) {
return await loading_listener(
(event: {
event: {
type: 'launcher_update'
version: string
return events.on('loading', (event) => {
if (event.event.type === 'launcher_update') {
if (!version || event.event.version === version) {
context.progress.value = event.fraction ?? 1.0
context.version.value = event.event.version
console.log(`Progress: ${context.progress.value} ${context.version.value}`)
}
fraction?: number
}) => {
if (event.event.type === 'launcher_update') {
if (!version || event.event.version === version) {
context.progress.value = event.fraction ?? 1.0
context.version.value = event.event.version
console.log(`Progress: ${context.progress.value} ${context.version.value}`)
}
}
},
)
}
})
}
export const [injectAppUpdateDownloadProgress, provideAppUpdateDownloadProgress] =
@@ -17,6 +17,7 @@ import { edit, get, list } from '@/helpers/instance'
import type { GameInstance } from '@/helpers/types'
import { ensureManagedServerWorldExists, getServerAddress } from '@/helpers/worlds'
import { start_join_server } from '@/helpers/worlds.ts'
import type { AppEvents } from '@/providers/app-events'
import { handleSevereError } from '@/store/error.js'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -74,6 +75,7 @@ export function createServerInstall(opts: {
router: Router
handleError: (err: unknown) => void
popupNotificationManager: AbstractPopupNotificationManager
appEvents: AppEvents
}): ServerInstallContext {
const installingServerProjects = ref<string[]>([])
@@ -134,7 +136,7 @@ export function createServerInstall(opts: {
const instanceId = installJobInstanceId(job)
if (!instanceId) return null
await wait_for_install_job(job.job_id)
await wait_for_install_job(opts.appEvents, job.job_id)
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
return instanceId
@@ -145,7 +147,7 @@ export function createServerInstall(opts: {
await edit(instance.id, { game_version: targetGameVersion })
const job = await install_existing_instance(instance.id, false)
await wait_for_install_job(job.job_id)
await wait_for_install_job(opts.appEvents, job.job_id)
}
function showModpackInstallSuccess(project: GameInstance, serverAddress: string | null) {
@@ -261,7 +263,7 @@ export function createServerInstall(opts: {
const instanceId = installJobInstanceId(createJob)
if (!instanceId) return
await wait_for_install_job(createJob.job_id)
await wait_for_install_job(opts.appEvents, createJob.job_id)
await ensureManagedServerWorldExists(instanceId, project.title, serverAddress)
}
@@ -0,0 +1,245 @@
import type { AppEvent } from '@/generated/app-events/AppEvent'
import { deserialize } from '@/generated/app-events/postcard'
type WireObject = Record<string, unknown>
interface WireEnum {
tag: string
value?: unknown
}
function wireObject(value: unknown): WireObject {
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Invalid Postcard app event object')
}
return value as WireObject
}
function wireEnum(value: unknown): WireEnum {
const event = wireObject(value)
if (typeof event.tag !== 'string') {
throw new TypeError('Invalid Postcard app event enum')
}
return event as unknown as WireEnum
}
function taggedObject(value: unknown, tag: string): WireObject {
const event = wireEnum(value)
return {
[tag]: event.tag,
...(event.value === undefined ? {} : wireObject(event.value)),
}
}
function unitVariant(value: unknown): string {
return wireEnum(value).tag
}
function nullable(value: unknown): unknown {
return value === undefined ? null : value
}
function number(value: unknown): unknown {
return typeof value === 'bigint' ? Number(value) : value
}
function normalizeLoadingPayload(value: unknown): WireObject {
const payload = wireObject(value)
const event = taggedObject(payload.event, 'type')
for (const field of ['icon', 'pack_id', 'pack_version']) {
if (field in event) event[field] = nullable(event[field])
}
return {
...payload,
event,
fraction: nullable(payload.fraction),
}
}
function normalizeProcessPayload(value: unknown): WireObject {
const payload = wireObject(value)
return { ...payload, event: unitVariant(payload.event) }
}
function normalizeInstancePayload(value: unknown): WireObject {
const payload = wireObject(value)
const event = wireEnum(payload.event)
return {
instance_id: payload.instance_id,
event: event.tag,
...(event.value === undefined ? {} : wireObject(event.value)),
}
}
function normalizeBulkUpdatePayload(value: unknown): WireObject {
const payload = wireObject(value)
return {
...payload,
stage: unitVariant(payload.stage),
current: number(payload.current),
total: number(payload.total),
}
}
function normalizeCommandPayload(value: unknown): WireObject {
const command = taggedObject(value, 'event')
if (command.event === 'LaunchInstance') {
command.server = nullable(command.server)
command.singleplayer_world = nullable(command.singleplayer_world)
}
return command
}
function normalizeFriendPayload(value: unknown): WireObject {
const event = taggedObject(value, 'event')
if (event.event === 'status_update') {
const status = wireObject(event.user_status)
event.user_status = {
...status,
profile_name: nullable(status.profile_name),
}
}
return event
}
function normalizeLogPayload(value: unknown): WireObject {
const payload = wireObject(value)
const event = taggedObject(payload.event, 'type')
if (event.type === 'log4j') {
for (const field of [
'timestamp_millis',
'logger_name',
'level',
'thread_name',
'message',
'throwable',
]) {
event[field] = nullable(event[field])
}
event.timestamp_millis = number(event.timestamp_millis)
}
return { instance_id: payload.instance_id, ...event }
}
function normalizeInstallProgress(value: unknown): WireObject {
const progress = wireObject(value)
const secondary = progress.secondary
return {
...progress,
current: number(progress.current),
total: number(progress.total),
secondary:
secondary === undefined
? undefined
: {
...wireObject(secondary),
current: number(wireObject(secondary).current),
total: number(wireObject(secondary).total),
},
}
}
function normalizeInstallDetails(value: unknown): WireObject {
const details = taggedObject(value, 'type')
if (details.type === 'minecraft') details.loader = unitVariant(details.loader)
if (details.type === 'java') details.step = unitVariant(details.step)
if (details.type === 'import') details.launcher_type = unitVariant(details.launcher_type)
if (details.type === 'modpack') {
details.project_id = nullable(details.project_id)
details.version_id = nullable(details.version_id)
details.title = nullable(details.title)
}
return details
}
function normalizeInstallContext(value: unknown): WireObject {
const context = wireObject(value)
return {
...context,
expected_size: context.expected_size === undefined ? undefined : number(context.expected_size),
}
}
function normalizeInstallError(value: unknown): WireObject {
const error = wireObject(value)
return {
...error,
phase: error.phase === undefined ? undefined : unitVariant(error.phase),
reason: error.reason === undefined ? undefined : unitVariant(error.reason),
api: error.api === undefined ? undefined : wireObject(error.api),
context: error.context === undefined ? undefined : normalizeInstallContext(error.context),
}
}
function normalizeInstallJob(value: unknown): WireObject {
const job = wireObject(value)
const display = job.display === undefined ? null : wireObject(job.display)
if (display !== null) display.icon = nullable(display.icon)
return {
...job,
instance_id: nullable(job.instance_id),
kind: unitVariant(job.kind),
status: unitVariant(job.status),
target: (() => {
const target = taggedObject(job.target, 'type')
if ('instance_id' in target) target.instance_id = nullable(target.instance_id)
return target
})(),
phase: unitVariant(job.phase),
progress: job.progress === undefined ? null : normalizeInstallProgress(job.progress),
details: normalizeInstallDetails(job.details),
display,
error: job.error === undefined ? null : normalizeInstallError(job.error),
rollback_error:
job.rollback_error === undefined ? null : normalizeInstallError(job.rollback_error),
finished: nullable(job.finished),
}
}
export function decodeAppEvent(payload: ArrayBuffer): AppEvent {
const result = deserialize('AppEvent', new Uint8Array(payload))
if (result.bytes.length !== 0) {
throw new TypeError('Postcard app event contained trailing bytes')
}
const event = result.value as WireEnum
const decoded = (() => {
switch (event.tag) {
case 'loading':
return { type: event.tag, payload: normalizeLoadingPayload(event.value) }
case 'process':
return { type: event.tag, payload: normalizeProcessPayload(event.value) }
case 'instance':
return { type: event.tag, payload: normalizeInstancePayload(event.value) }
case 'instance_bulk_update_progress':
return { type: event.tag, payload: normalizeBulkUpdatePayload(event.value) }
case 'install_job':
return { type: event.tag, payload: normalizeInstallJob(event.value) }
case 'command':
return { type: event.tag, payload: normalizeCommandPayload(event.value) }
case 'warning':
return { type: event.tag, payload: wireObject(event.value) }
case 'friend':
return { type: event.tag, payload: normalizeFriendPayload(event.value) }
case 'notification':
return { type: event.tag, payload: JSON.parse(String(event.value)) as unknown }
case 'log':
return { type: event.tag, payload: normalizeLogPayload(event.value) }
case 'ads_consent_required':
return { type: event.tag, payload: event.value }
default:
throw new TypeError(`Unknown Postcard app event: ${event.tag}`)
}
})()
return decoded as AppEvent
}
@@ -0,0 +1,65 @@
import { Channel } from '@tauri-apps/api/core'
import { onScopeDispose } from 'vue'
import {
type AppEventHandler,
type AppEvents,
type AppEventType,
provideAppEvents,
} from '@/providers/app-events'
import { decodeAppEvent } from '@/providers/setup/app-event-codec'
type UntypedAppEventHandler = (payload: unknown) => void | Promise<void>
export function setupAppEventsProvider() {
const handlers = new Map<AppEventType, Set<UntypedAppEventHandler>>()
function on<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>) {
let eventHandlers = handlers.get(type)
if (!eventHandlers) {
eventHandlers = new Set()
handlers.set(type, eventHandlers)
}
const untypedHandler = handler as unknown as UntypedAppEventHandler
eventHandlers.add(untypedHandler)
return () => {
eventHandlers.delete(untypedHandler)
if (eventHandlers.size === 0) handlers.delete(type)
}
}
function once<Type extends AppEventType>(type: Type, handler: AppEventHandler<Type>) {
let unsubscribe = () => {}
unsubscribe = on(type, async (payload) => {
unsubscribe()
await handler(payload)
})
return unsubscribe
}
const events: AppEvents = { on, once }
const channel = new Channel<ArrayBuffer>((payload) => {
const event = decodeAppEvent(payload)
const eventHandlers = handlers.get(event.type)
if (!eventHandlers) return
for (const handler of [...eventHandlers]) {
void Promise.resolve()
.then(() => handler(event.payload))
.catch((error) => {
console.error(`Unhandled ${event.type} app event`, error)
})
}
})
provideAppEvents(events)
function dispose() {
handlers.clear()
}
onScopeDispose(dispose)
return { channel, events, dispose }
}
+1
View File
@@ -77,6 +77,7 @@ default = ["custom-protocol"]
# this feature is used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
export-app-events = ["theseus/export-ts"]
updater = []
[lints]
+1 -1
View File
@@ -3,7 +3,7 @@
"scripts": {
"tauri": "tauri",
"build": "tauri build",
"dev": "tauri dev",
"dev": "tauri dev --features export-app-events",
"test": "cargo nextest run --all-targets --no-fail-fast",
"lint": "cargo fmt --check && cargo clippy --all-targets && cargo clippy --all-targets --features updater",
"lint:ancillary": "prettier --check .",
+13 -9
View File
@@ -3,9 +3,9 @@ use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tauri::plugin::TauriPlugin;
use tauri::{Emitter, Manager, PhysicalPosition, PhysicalSize, Runtime};
use tauri::{Manager, PhysicalPosition, PhysicalSize, Runtime};
use tauri_plugin_opener::OpenerExt;
use theseus::settings;
use theseus::{AppEvent, EventState, settings};
use tokio::sync::RwLock;
pub struct AdsState {
@@ -20,7 +20,6 @@ pub struct AdsState {
}
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
const ADS_CONSENT_REQUIRED_EVENT: &str = "ads-consent-required";
const APP_TITLE_BAR_HEIGHT: f32 = 48.0;
#[cfg(any(windows, target_os = "macos"))]
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 0.5;
@@ -38,6 +37,12 @@ const ADS_USER_AGENT: &str = concat!(
" (Modrinth App)",
);
fn emit_ads_consent_required(required: bool) {
EventState::get()
.send(AppEvent::AdsConsentRequired(required))
.ok();
}
#[cfg(windows)]
fn ads_user_agent_override_params() -> String {
serde_json::json!({
@@ -666,7 +671,7 @@ pub async fn init_ads_window<R: Runtime>(
&& state.consent_required
&& state.consent_notification_enabled
{
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
emit_ads_consent_required(true);
}
Ok(())
@@ -704,7 +709,7 @@ pub async fn update_ads_window_hold<R: Runtime>(
&& state.consent_required
&& state.consent_notification_enabled
{
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, true).ok();
emit_ads_consent_required(true);
}
Ok(())
@@ -730,7 +735,7 @@ pub async fn hide_ads_window<R: Runtime>(
}
if reset {
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
emit_ads_consent_required(false);
}
Ok(())
@@ -761,8 +766,7 @@ pub async fn show_ads_consent_ui<R: Runtime>(
)?;
}
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, show_notification)
.ok();
emit_ads_consent_required(show_notification);
Ok(())
}
@@ -841,7 +845,7 @@ pub async fn finish_ads_consent_flow<R: Runtime>(
}
}
app.emit_to("main", ADS_CONSENT_REQUIRED_EVENT, false).ok();
emit_ads_consent_required(false);
Ok(())
}
+12 -2
View File
@@ -25,9 +25,12 @@ mod updater_impl_noop;
// Should be called in launcher initialization
#[tracing::instrument(skip_all)]
#[tauri::command]
async fn initialize_state(app: tauri::AppHandle) -> api::Result<()> {
async fn initialize_state(
app: tauri::AppHandle,
events: tauri::ipc::Channel<tauri::ipc::InvokeResponseBody>,
) -> api::Result<()> {
tracing::info!("Initializing app event state...");
theseus::EventState::init(app.clone()).await?;
theseus::EventState::init(app.clone(), events).await?;
tracing::info!("Initializing app state...");
State::init(app.config().identifier.clone()).await?;
@@ -111,6 +114,13 @@ async fn set_restart_after_pending_update(
// if Tauri app is called with arguments, then those arguments will be treated as commands
// ie: deep links or filepaths for .mrpacks
fn main() {
#[cfg(feature = "export-app-events")]
theseus::export_app_event_bindings(
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../app-frontend/src/generated/app-events"),
)
.expect("failed to export app event TypeScript bindings");
/*
tracing is set basd on the environment variable RUST_LOG=xxx, depending on the amount of logs to show
ERROR > WARN > INFO > DEBUG > TRACE