From 9fcb68816ba867812bdf667157d5f71560c6d738 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 24 Jul 2026 09:14:40 +0100 Subject: [PATCH] =?UTF-8?q?perf(apps):=20launch=20never=20waits=20on=20the?= =?UTF-8?q?=20credentials=20RPC=20=E2=80=94=201.2s=20budget=20+=20memo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apps-tab launches awaited package.credentials with a 5s timeout before opening anything (home-card launches skip the gate — that's why they always felt instant). The lookup now races a 1.2s budget: past it, the app launches with the static fallback config while the RPC finishes into a per-app memo, making every subsequent launch instant. Co-Authored-By: Claude Fable 5 --- neode-ui/src/views/Apps.vue | 66 ++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 26 deletions(-) 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() { -- 2.47.2