fix(ui): shorten install/uninstall/update timeouts for async RPCs

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.
This commit is contained in:
archipelago 2026-04-23 06:58:02 -04:00
parent 1ad889608f
commit 702b5d64d3
5 changed files with 41 additions and 23 deletions

View File

@ -521,23 +521,30 @@ class RPCClient {
})
}
async installPackage(id: string, marketplaceUrl: string, version: string): Promise<string> {
async installPackage(
id: string,
marketplaceUrl: string,
version: string,
): Promise<{ status: string; package_id: string }> {
// Backend is async — returns { status: 'installing' } in <1s after
// flipping state and spawning the pull/install pipeline. Progress is
// streamed via WebSocket (install_progress field on the package entry).
return this.call({
method: 'package.install',
params: { id, 'marketplace-url': marketplaceUrl, version },
// 45 min — IndeedHub is 6 images and gitea raw-file throughput is
// ~70 KB/s per image; 15 min was short enough to kill the install
// mid-pull and land the user on a "didn't work" screen while the
// backend kept working in the background.
timeout: 2700000,
timeout: 15000,
})
}
async uninstallPackage(id: string): Promise<void> {
async uninstallPackage(id: string): Promise<{ status: string; package_id: string }> {
// Backend is async — returns { status: 'removing' } immediately after
// flipping state. Graceful stop (up to 600s for bitcoin) and data wipe
// (up to minutes for large chainstate) run in a background task.
// Progress shown via uninstall_stage field on the package entry.
return this.call({
method: 'package.uninstall',
params: { id },
timeout: 660000, // Bitcoin Knots needs up to 600s for UTXO flush
timeout: 15000,
})
}
@ -545,7 +552,7 @@ class RPCClient {
return this.call({
method: 'package.start',
params: { id },
timeout: 60000,
timeout: 15000,
})
}
@ -553,7 +560,7 @@ class RPCClient {
return this.call({
method: 'package.stop',
params: { id },
timeout: 120000,
timeout: 15000,
})
}
@ -561,15 +568,18 @@ class RPCClient {
return this.call({
method: 'package.restart',
params: { id },
timeout: 120000,
timeout: 15000,
})
}
async updatePackage(id: string): Promise<{ status: string }> {
async updatePackage(id: string): Promise<{ status: string; package_id: string }> {
// Backend is async — returns { status: 'updating' } immediately after
// flipping state. Pull / stop / recreate / verify runs in background,
// with rollback-on-failure.
return this.call({
method: 'package.update',
params: { id },
timeout: 660000, // Bitcoin Knots needs up to 600s for graceful shutdown
timeout: 15000,
})
}

View File

@ -100,12 +100,19 @@ export const useServerStore = defineStore('server', () => {
const isShuttingDown = computed(() => sync.serverInfo?.['status-info']?.['shutting-down'] || false)
const isOffline = computed(() => !sync.isConnected || isRestarting.value || isShuttingDown.value)
// Package actions
async function installPackage(id: string, marketplaceUrl: string, version: string): Promise<string> {
// 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<void> {
async function uninstallPackage(id: string): Promise<{ status: string; package_id: string }> {
return rpcClient.uninstallPackage(id)
}
@ -121,7 +128,7 @@ export const useServerStore = defineStore('server', () => {
return rpcClient.restartPackage(id)
}
async function updatePackage(id: string): Promise<{ status: string }> {
async function updatePackage(id: string): Promise<{ status: string; package_id: string }> {
return rpcClient.updatePackage(id)
}

View File

@ -489,7 +489,7 @@ async function installApp(app: MarketplaceApp) {
try {
const installUrl = app.url || app.manifestUrl || app.s9pkUrl
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' })
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version } })
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 15000 })
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' })
startInstallPolling(app.id, 'Starting application...')
} catch (err) {
@ -511,7 +511,7 @@ async function installCommunityApp(app: MarketplaceApp) {
if ((app as Record<string, unknown>).containerConfig) {
installParams.containerConfig = (app as Record<string, unknown>).containerConfig
}
await rpcClient.call({ method: 'package.install', params: installParams, timeout: 180000 })
await rpcClient.call({ method: 'package.install', params: installParams, timeout: 15000 })
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })
startInstallPolling(app.id, 'Initializing application...')
} catch (err) {

View File

@ -418,7 +418,7 @@ async function installApp(app: MarketplaceApp) {
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'downloading', progress: 30, message: 'Downloading package...' })
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version } })
await rpcClient.call({ method: 'package.install', params: { id: app.id, url: installUrl, version: app.version }, timeout: 15000 })
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Installing package...' })
@ -447,7 +447,7 @@ async function installCommunityApp(app: MarketplaceApp) {
await rpcClient.call({
method: 'package.install',
params: { id: app.id, dockerImage: app.dockerImage, version: app.version },
timeout: 180000
timeout: 15000
})
installingApps.set(app.id, { ...installingApps.get(app.id)!, status: 'installing', progress: 60, message: 'Starting container...' })

View File

@ -546,7 +546,7 @@ async function installDependencies() {
id: dep.id,
dockerImage: dep.dockerImage,
},
timeout: 180000,
timeout: 15000,
})
// Wait for package to register before installing next
await new Promise(resolve => setTimeout(resolve, 2000))
@ -579,7 +579,7 @@ async function installApp() {
dockerImage: app.value.dockerImage,
version: app.value.version,
},
timeout: 180000,
timeout: 15000,
})
} else {
// Package-based installation
@ -591,6 +591,7 @@ async function installApp() {
url: installUrl,
version: app.value.version,
},
timeout: 15000,
})
}