With the backend flipped to async-spawn, install/uninstall/update return
immediately with a { status, package_id } envelope. Client timeouts of
45m/11m were a leftover from synchronous handlers and masked real RPC
failures.
Drop all install/uninstall/update RPC timeouts to 15s. Progress and
terminal state still arrive through the live state stream — the RPC
only needs to confirm the spawn was accepted.
Return-type annotations updated in rpc-client.ts and stores/server.ts.
Five direct rpcClient.call sites across Marketplace.vue, Discover.vue,
and MarketplaceAppDetails.vue updated with the shorter timeout.
192 lines
6.5 KiB
TypeScript
192 lines
6.5 KiB
TypeScript
// Server store — computed server state and RPC action proxies
|
|
|
|
import { defineStore } from 'pinia'
|
|
import { computed, ref, watch } from 'vue'
|
|
import { rpcClient } from '../api/rpc-client'
|
|
import { useSyncStore } from './sync'
|
|
import type { InstallProgress } from '../views/marketplace/marketplaceData'
|
|
|
|
export const useServerStore = defineStore('server', () => {
|
|
const sync = useSyncStore()
|
|
|
|
// Global install/uninstall tracking — persists across navigation
|
|
const installingApps = ref<Map<string, InstallProgress>>(new Map())
|
|
const uninstallingApps = ref<Set<string>>(new Set())
|
|
|
|
// Watch WebSocket data for real install progress — runs globally, not just on Marketplace page
|
|
watch(() => sync.packages, (packages) => {
|
|
if (!packages) return
|
|
for (const [appId, pkg] of Object.entries(packages)) {
|
|
if ((pkg.state as string) === 'installing' || (pkg.state as string) === 'updating') {
|
|
// Backend confirms it's installing — update or create tracking entry
|
|
if (!installingApps.value.has(appId)) {
|
|
installingApps.value.set(appId, {
|
|
id: appId,
|
|
title: pkg.manifest?.title || appId,
|
|
status: 'downloading',
|
|
progress: 0,
|
|
message: 'Installing...',
|
|
attempt: 0,
|
|
})
|
|
}
|
|
const progress = pkg['install-progress']
|
|
if (progress) {
|
|
const current = installingApps.value.get(appId)!
|
|
const pct = progress.size > 0 ? Math.round((progress.downloaded / progress.size) * 100) : 0
|
|
const downloadedMB = (progress.downloaded / (1024 * 1024)).toFixed(1)
|
|
const totalMB = (progress.size / (1024 * 1024)).toFixed(1)
|
|
let message = 'Downloading...'
|
|
if (progress.size > 1024 && pct < 100) {
|
|
message = `Downloading: ${downloadedMB} / ${totalMB} MB (${pct}%)`
|
|
} else if (pct >= 100 || (progress.size > 0 && progress.downloaded >= progress.size)) {
|
|
message = 'Installing package...'
|
|
}
|
|
installingApps.value.set(appId, {
|
|
...current,
|
|
status: pct >= 100 ? 'installing' : 'downloading',
|
|
progress: Math.min(pct, 95),
|
|
message,
|
|
})
|
|
}
|
|
} else if (installingApps.value.has(appId)) {
|
|
const state = pkg.state as string
|
|
// Only clear when app is fully running or definitively stopped — not during 'starting' transition
|
|
if (state === 'running' || state === 'stopped' || state === 'exited') {
|
|
installingApps.value.delete(appId)
|
|
}
|
|
}
|
|
}
|
|
// Clear installingApps entries for apps that vanished from backend data
|
|
// Only clean up entries that have errored — active installs may take minutes to pull images
|
|
for (const [appId] of installingApps.value) {
|
|
if (packages && !(appId in packages)) {
|
|
const entry = installingApps.value.get(appId)
|
|
if (entry && entry.status === 'error') {
|
|
installingApps.value.delete(appId)
|
|
}
|
|
}
|
|
}
|
|
// Clear uninstallingApps when the container disappears from backend data
|
|
for (const appId of uninstallingApps.value) {
|
|
if (packages && !(appId in packages)) {
|
|
uninstallingApps.value.delete(appId)
|
|
}
|
|
}
|
|
}, { deep: true })
|
|
|
|
function setInstallProgress(appId: string, progress: Partial<InstallProgress> & { id: string; title: string }) {
|
|
const existing = installingApps.value.get(appId)
|
|
installingApps.value.set(appId, {
|
|
status: 'downloading',
|
|
progress: 0,
|
|
message: 'Preparing...',
|
|
attempt: 0,
|
|
...existing,
|
|
...progress,
|
|
})
|
|
}
|
|
|
|
function clearInstallProgress(appId: string) {
|
|
installingApps.value.delete(appId)
|
|
}
|
|
|
|
function isInstalling(appId: string): boolean {
|
|
return installingApps.value.has(appId)
|
|
}
|
|
|
|
// Computed — derived from sync store's data
|
|
const serverName = computed(() => sync.serverInfo?.name || 'Archipelago')
|
|
const isRestarting = computed(() => sync.serverInfo?.['status-info']?.restarting || false)
|
|
const isShuttingDown = computed(() => sync.serverInfo?.['status-info']?.['shutting-down'] || false)
|
|
const isOffline = computed(() => !sync.isConnected || isRestarting.value || isShuttingDown.value)
|
|
|
|
// Package actions. install/uninstall/update are async on the backend:
|
|
// the RPC returns immediately with { status: 'installing'|'removing'|'updating',
|
|
// package_id } after flipping state, and the real work runs in a spawn.
|
|
// Progress is streamed via the WebSocket state push, not the RPC response.
|
|
async function installPackage(
|
|
id: string,
|
|
marketplaceUrl: string,
|
|
version: string,
|
|
): Promise<{ status: string; package_id: string }> {
|
|
return rpcClient.installPackage(id, marketplaceUrl, version)
|
|
}
|
|
|
|
async function uninstallPackage(id: string): Promise<{ status: string; package_id: string }> {
|
|
return rpcClient.uninstallPackage(id)
|
|
}
|
|
|
|
async function startPackage(id: string): Promise<void> {
|
|
return rpcClient.startPackage(id)
|
|
}
|
|
|
|
async function stopPackage(id: string): Promise<void> {
|
|
return rpcClient.stopPackage(id)
|
|
}
|
|
|
|
async function restartPackage(id: string): Promise<void> {
|
|
return rpcClient.restartPackage(id)
|
|
}
|
|
|
|
async function updatePackage(id: string): Promise<{ status: string; package_id: string }> {
|
|
return rpcClient.updatePackage(id)
|
|
}
|
|
|
|
// Server actions
|
|
async function updateServer(marketplaceUrl: string): Promise<'updating' | 'no-updates'> {
|
|
return rpcClient.updateServer(marketplaceUrl)
|
|
}
|
|
|
|
async function restartServer(): Promise<void> {
|
|
return rpcClient.restartServer()
|
|
}
|
|
|
|
async function shutdownServer(): Promise<void> {
|
|
return rpcClient.shutdownServer()
|
|
}
|
|
|
|
async function getMetrics(): Promise<Record<string, unknown>> {
|
|
return rpcClient.getMetrics()
|
|
}
|
|
|
|
// Marketplace actions
|
|
async function getMarketplace(url: string): Promise<Record<string, unknown>> {
|
|
return rpcClient.getMarketplace(url)
|
|
}
|
|
|
|
function updateServerName(name: string) {
|
|
if (sync.data?.['server-info']) {
|
|
sync.data['server-info'].name = name
|
|
}
|
|
}
|
|
|
|
return {
|
|
// Computed
|
|
serverName,
|
|
isRestarting,
|
|
isShuttingDown,
|
|
isOffline,
|
|
|
|
// Install/uninstall tracking (global, persists across navigation)
|
|
installingApps,
|
|
setInstallProgress,
|
|
clearInstallProgress,
|
|
isInstalling,
|
|
uninstallingApps,
|
|
|
|
// Actions
|
|
installPackage,
|
|
uninstallPackage,
|
|
startPackage,
|
|
stopPackage,
|
|
restartPackage,
|
|
updatePackage,
|
|
updateServer,
|
|
restartServer,
|
|
shutdownServer,
|
|
getMetrics,
|
|
getMarketplace,
|
|
updateServerName,
|
|
}
|
|
})
|