fix: share the hub integration list cache, backfill admins only

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-09-14 17:22:33 +02:00
co-authored by Claude Opus 5
parent 414e8bfd54
commit a4c5de5a2b
4 changed files with 35 additions and 22 deletions
@@ -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;
@@ -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<string, never>) =>
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<void> {
if (get(disableHubStore)) return Promise.resolve()
return hubIntegrationNamesCached({})
return listHubIntegrationsShared().then(
() => {},
() => {}
)
}
@@ -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)
@@ -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')