export type ToolkitDirectoryEntry = { slug: string; name: string; logo: string | null; noAuth: boolean; }; export type ToolkitCatalogEntry = ToolkitDirectoryEntry & { connected: boolean }; export const COMPOSIO_DIRECTORY_TTL_MS = 60 * 60 * 1000; export function mergeCatalogWithConnected( directory: ToolkitDirectoryEntry[], connectedSlugs: Iterable, ): ToolkitCatalogEntry[] { const connected = new Set([...connectedSlugs].map((slug) => slug.trim().toLowerCase())); return directory.map((item) => ({ ...item, connected: connected.has(item.slug.trim().toLowerCase()), })); } /** * Coalesces directory loads and serves stale entries during refreshes. Failed * refreshes preserve stale entries for later retries; cold-load errors propagate. */ export function createToolkitDirectoryCache(opts?: { ttlMs?: number; now?: () => number }) { const ttlMs = opts?.ttlMs ?? COMPOSIO_DIRECTORY_TTL_MS; const now = opts?.now ?? Date.now; let entry: { items: ToolkitDirectoryEntry[]; fetchedAt: number } | undefined; let inflight: Promise | undefined; async function load(loader: () => Promise) { inflight ??= loader() .then((items) => { entry = { items, fetchedAt: now() }; return items; }) .finally(() => { inflight = undefined; }); return inflight; } return { peek(): ToolkitDirectoryEntry[] | undefined { return entry?.items; }, async get(loader: () => Promise): Promise { if (!entry) return load(loader); if (now() - entry.fetchedAt < ttlMs) return entry.items; if (!inflight) { // Keep stale items on failure; load clears inflight so a later read can retry. void load(loader).catch(() => undefined); } return entry.items; }, invalidate() { entry = undefined; }, }; } export const composioToolkitDirectory = createToolkitDirectoryCache();