From 702b5d64d32e56b4479c4c483929afd1069c667c Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 23 Apr 2026 06:58:02 -0400 Subject: [PATCH] fix(ui): shorten install/uninstall/update timeouts for async RPCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- neode-ui/src/api/rpc-client.ts | 36 +++++++++++++------- neode-ui/src/stores/server.ts | 15 +++++--- neode-ui/src/views/Discover.vue | 4 +-- neode-ui/src/views/Marketplace.vue | 4 +-- neode-ui/src/views/MarketplaceAppDetails.vue | 5 +-- 5 files changed, 41 insertions(+), 23 deletions(-) diff --git a/neode-ui/src/api/rpc-client.ts b/neode-ui/src/api/rpc-client.ts index cc5f2107..0aadf022 100644 --- a/neode-ui/src/api/rpc-client.ts +++ b/neode-ui/src/api/rpc-client.ts @@ -521,23 +521,30 @@ class RPCClient { }) } - async installPackage(id: string, marketplaceUrl: string, version: string): Promise { + 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 { + 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, }) } diff --git a/neode-ui/src/stores/server.ts b/neode-ui/src/stores/server.ts index aea311b4..c5c0d6e7 100644 --- a/neode-ui/src/stores/server.ts +++ b/neode-ui/src/stores/server.ts @@ -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 { + // 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 { + 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) } diff --git a/neode-ui/src/views/Discover.vue b/neode-ui/src/views/Discover.vue index c51cadf9..e5fbabe7 100644 --- a/neode-ui/src/views/Discover.vue +++ b/neode-ui/src/views/Discover.vue @@ -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).containerConfig) { installParams.containerConfig = (app as Record).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) { diff --git a/neode-ui/src/views/Marketplace.vue b/neode-ui/src/views/Marketplace.vue index 836e6f3f..226258df 100644 --- a/neode-ui/src/views/Marketplace.vue +++ b/neode-ui/src/views/Marketplace.vue @@ -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...' }) diff --git a/neode-ui/src/views/MarketplaceAppDetails.vue b/neode-ui/src/views/MarketplaceAppDetails.vue index 6ff22d68..72b01c84 100644 --- a/neode-ui/src/views/MarketplaceAppDetails.vue +++ b/neode-ui/src/views/MarketplaceAppDetails.vue @@ -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, }) }