From a4c5de5a2b5cf5e9d09715251b8f1bf1ae1eb84c Mon Sep 17 00:00:00 2001 From: hugocasa Date: Mon, 14 Sep 2026 17:22:33 +0200 Subject: [PATCH] fix: share the hub integration list cache, backfill admins only Co-Authored-By: Claude Opus 5 (1M context) --- ...14131148_resource_type_display_name.up.sql | 8 +++-- .../src/lib/components/displayNameLoaders.ts | 31 ++++++++++++++----- .../flows/pickers/PickHubScript.svelte | 7 ++--- .../components/mcp/McpScopeSelector.svelte | 11 +++---- 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/backend/migrations/20260914131148_resource_type_display_name.up.sql b/backend/migrations/20260914131148_resource_type_display_name.up.sql index 68af132255..213622ef94 100644 --- a/backend/migrations/20260914131148_resource_type_display_name.up.sql +++ b/backend/migrations/20260914131148_resource_type_display_name.up.sql @@ -2,8 +2,8 @@ -- "Google Sheets". Null where nobody named the type; readers derive a label from the name. ALTER TABLE resource_type ADD COLUMN display_name VARCHAR(100); --- The names the hub carries today, so existing instances show them before any sync. Not --- limited to admins: a workspace that copied a hub type keeps the type's name. +-- The names the hub carries today, so existing instances show them before any sync. Only in +-- admins, where hub resource types live and every workspace reads them from. UPDATE resource_type SET display_name = v.display_name FROM (VALUES ('bamboo_hr', 'BambooHR'), @@ -19,4 +19,6 @@ FROM (VALUES ('snowflake_oauth', 'Snowflake (OAuth)'), ('their_stack', 'TheirStack') ) AS v(name, display_name) -WHERE resource_type.name = v.name AND resource_type.display_name IS NULL; +WHERE resource_type.workspace_id = 'admins' + AND resource_type.name = v.name + AND resource_type.display_name IS NULL; diff --git a/frontend/src/lib/components/displayNameLoaders.ts b/frontend/src/lib/components/displayNameLoaders.ts index aaa5e2db15..1725977fd8 100644 --- a/frontend/src/lib/components/displayNameLoaders.ts +++ b/frontend/src/lib/components/displayNameLoaders.ts @@ -5,10 +5,10 @@ import { createCache } from '$lib/utils' import { setHubIntegrationDisplayNames, setResourceTypeDisplayNames } from './resourceTypeDisplay' /** - * Loads the names `resourceTypeDisplayName` and `integrationDisplayName` read, for a surface that - * shows a label without already fetching the rows it comes from. Apart from `resourceTypeDisplay`, - * which makes no API calls so it can be unit-tested alone. Cached briefly: drawers and pickers - * reopen often, and a name rarely changes. + * Loads what `resourceTypeDisplayName` and `integrationDisplayName` read: a type's stored name for a + * surface that holds no row for it, and the hub's integration list, which every picker that needs + * it shares. Apart from `resourceTypeDisplay`, which makes no API calls so it can be unit-tested + * alone. Cached briefly: drawers and pickers reopen often, and a name rarely changes. */ const CACHE_MS = 60_000 @@ -29,12 +29,24 @@ export function loadResourceTypeDisplayName(workspace: string, name: string): Pr return resourceTypeRowCached({ workspace, name }) } -const hubIntegrationNamesCached = createCache( - (_: Record) => - IntegrationService.listHubIntegrations().then(setHubIntegrationDisplayNames, () => {}), +const hubIntegrationsCached = createCache( + ({ kind }: { kind?: string }) => + IntegrationService.listHubIntegrations({ kind }).then((integrations) => { + setHubIntegrationDisplayNames(integrations) + return integrations + }), { invalidateMs: CACHE_MS } ) +/** + * The hub's integration list, read once a minute per `kind` however many pickers ask, recording + * each integration's name on the way. A failed read is kept for that minute too, and rejects, so + * a picker can say the hub is unavailable. + */ +export function listHubIntegrationsShared(kind?: string) { + return hubIntegrationsCached({ kind }) +} + /** * Fill `integrationDisplayName` for a picker whose integrations come from its own items rather * than the hub's integration list, as the hub app and flow pickers do. Unfiltered: `kind` @@ -42,5 +54,8 @@ const hubIntegrationNamesCached = createCache( */ export function loadHubIntegrationDisplayNames(): Promise { if (get(disableHubStore)) return Promise.resolve() - return hubIntegrationNamesCached({}) + return listHubIntegrationsShared().then( + () => {}, + () => {} + ) } diff --git a/frontend/src/lib/components/flows/pickers/PickHubScript.svelte b/frontend/src/lib/components/flows/pickers/PickHubScript.svelte index d1ed785ba3..05205d8335 100644 --- a/frontend/src/lib/components/flows/pickers/PickHubScript.svelte +++ b/frontend/src/lib/components/flows/pickers/PickHubScript.svelte @@ -4,9 +4,9 @@ import { capitalize } from '$lib/utils' import NoItemFound from '$lib/components/home/NoItemFound.svelte' import { APP_TO_ICON_COMPONENT } from '$lib/components/icons' - import { setHubIntegrationDisplayNames } from '$lib/components/resourceTypeDisplay' + import { listHubIntegrationsShared } from '$lib/components/displayNameLoaders' import ListFilters from '$lib/components/home/ListFilters.svelte' - import { IntegrationService, ScriptService, type HubScriptKind } from '$lib/gen' + import { ScriptService, type HubScriptKind } from '$lib/gen' import { Loader2 } from 'lucide-svelte' import TextInput from '$lib/components/text_input/TextInput.svelte' import { disableHubStore, workspaceStore } from '$lib/stores' @@ -65,10 +65,9 @@ hubNotAvailable = false // Independent reads, so they share one round trip before first paint. const [integrations, local] = await Promise.all([ - IntegrationService.listHubIntegrations({ kind: filterKind }), + listHubIntegrationsShared(filterKind), $workspaceStore ? localCountsByIntegration($workspaceStore) : {} ]) - setHubIntegrationDisplayNames(integrations) const hubPicks = Object.fromEntries(integrations.map((x) => [x.name, x.picks ?? 0])) popularity = byPopularity(hubPicks, local) allApps = integrations.map((x) => x.name).sort(popularity) diff --git a/frontend/src/lib/components/mcp/McpScopeSelector.svelte b/frontend/src/lib/components/mcp/McpScopeSelector.svelte index 78cc9dbe5f..46274d10d0 100644 --- a/frontend/src/lib/components/mcp/McpScopeSelector.svelte +++ b/frontend/src/lib/components/mcp/McpScopeSelector.svelte @@ -5,12 +5,10 @@ import Popover from '$lib/components/Popover.svelte' import MultiSelect from '$lib/components/select/MultiSelect.svelte' import { safeSelectItems } from '$lib/components/select/utils.svelte' - import { - integrationDisplayName, - setHubIntegrationDisplayNames - } from '$lib/components/resourceTypeDisplay' + import { integrationDisplayName } from '$lib/components/resourceTypeDisplay' + import { listHubIntegrationsShared } from '$lib/components/displayNameLoaders' import TextInput from '$lib/components/text_input/TextInput.svelte' - import { FlowService, FolderService, IntegrationService, ScriptService } from '$lib/gen' + import { FlowService, FolderService, ScriptService } from '$lib/gen' import { mcpEndpointTools } from '$lib/mcpEndpointTools' import { endpointPathPolicy, @@ -272,8 +270,7 @@ if (allApps.length > 0) return try { loadingApps = true - const integrations = await IntegrationService.listHubIntegrations({ kind: 'script' }) - setHubIntegrationDisplayNames(integrations) + const integrations = await listHubIntegrationsShared('script') allApps = integrations.map((x) => x.name) } catch (err) { console.error('Hub is not available')