diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index 9e562a0e..1993cfc6 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -646,37 +646,51 @@ function launchAppNow(id: string) { useAppLauncherStore().openSession(id) } -async function maybeShowCredentialsBeforeLaunch(id: string): Promise { - try { - const result = await rpcClient.call({ +// Per-app credentials memo: the pre-launch RPC could hold an Apps-tab launch +// hostage for its full 5s timeout over the mesh (home-card launches skip this +// gate entirely, which is why they always felt instant). First launch waits at +// most LAUNCH_CRED_BUDGET_MS; the RPC keeps running in the background and its +// answer is memoized, so every later launch of that app resolves instantly. +const LAUNCH_CRED_BUDGET_MS = 1200 +const credentialsCache = new Map() + +function fetchCredentials(id: string): Promise { + return rpcClient + .call({ method: 'package.credentials', params: { app_id: id }, timeout: 5000, }) - const credentials = resolveAppCredentials(id, result) - if (!credentials) return false - credentialModal.value = { - show: true, - appId: id, - title: credentials.title || `${packages.value[id]?.manifest.title || id} credentials`, - description: credentials.description || 'Use these credentials when the app asks you to sign in.', - credentials: credentials.credentials, - copied: '', - } - return true - } catch { - const credentials = resolveAppCredentials(id, null) - if (!credentials) return false - credentialModal.value = { - show: true, - appId: id, - title: credentials.title || `${packages.value[id]?.manifest.title || id} credentials`, - description: credentials.description || 'Use these credentials when the app asks you to sign in.', - credentials: credentials.credentials, - copied: '', - } - return true + .then((r) => { + credentialsCache.set(id, r) + return r + }) + .catch(() => { + credentialsCache.set(id, null) + return null + }) +} + +async function maybeShowCredentialsBeforeLaunch(id: string): Promise { + const result = credentialsCache.has(id) + ? credentialsCache.get(id) ?? null + : await Promise.race([ + fetchCredentials(id), + // Budget exceeded → launch with the static fallback config; the + // in-flight RPC still lands in the cache for next time. + new Promise((resolve) => setTimeout(() => resolve(null), LAUNCH_CRED_BUDGET_MS)), + ]) + const credentials = resolveAppCredentials(id, result) + if (!credentials) return false + credentialModal.value = { + show: true, + appId: id, + title: credentials.title || `${packages.value[id]?.manifest.title || id} credentials`, + description: credentials.description || 'Use these credentials when the app asks you to sign in.', + credentials: credentials.credentials, + copied: '', } + return true } function closeCredentialModal() {