From 7843675e75c2f3bfeb8709c11c51c736073bb6c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 11 Nov 2026 00:36:34 +0000 Subject: [PATCH] refactor: share one live connection list Run, the results panel and the editor each rebuilt the list of connected servers and open SQLite files. They now read it from useLiveConnectionIds or liveConnectionIds, so the lists cannot drift. Also narrows a non-null assertion in useStartupReconnect. Closes #963 Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_014tzYxya6Umczq4EBPTYkX4 --- src/components/editor/SQLEditor.tsx | 11 ++-- src/components/layout/useStartupReconnect.ts | 2 +- .../__tests__/useLiveConnectionIds.test.ts | 51 +++++++++++++++++++ src/hooks/useLiveConnectionIds.ts | 22 ++++++++ src/hooks/useQueryExecution.ts | 10 ++-- src/hooks/useResultsFollowActiveTab.ts | 10 ++-- 6 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 src/hooks/__tests__/useLiveConnectionIds.test.ts create mode 100644 src/hooks/useLiveConnectionIds.ts diff --git a/src/components/editor/SQLEditor.tsx b/src/components/editor/SQLEditor.tsx index ad253536..86cb511c 100644 --- a/src/components/editor/SQLEditor.tsx +++ b/src/components/editor/SQLEditor.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; import { useSchemaCompletion } from "../../hooks/useSchemaCompletion"; import { statementRange } from "../../lib/error-marker"; import { monacoThemeData, monacoThemeName } from "../../lib/monaco-theme"; -import { runConnectionId } from "../../lib/run-connection"; +import { liveConnectionIds, runConnectionId } from "../../lib/run-connection"; import { createCompletionProvider } from "../../lib/schema-completion-provider"; import { formatSql } from "../../lib/sql-format"; import { sqlAtCursor } from "../../lib/statement-at-cursor"; @@ -13,7 +13,6 @@ import { setPendingContentFlush, useEditorStore } from "../../stores/editorStore import { useResultStore } from "../../stores/resultStore"; import { useSchemaStore } from "../../stores/schemaStore"; import { useSettingsStore } from "../../stores/settingsStore"; -import { useSqliteStore } from "../../stores/sqliteStore"; import { useThemeStore } from "../../stores/themeStore"; /** @@ -27,14 +26,10 @@ import { useThemeStore } from "../../stores/themeStore"; function activeTabContext(): { connectionId?: string; database?: string; session?: string } { const { tabs, activeTabId } = useEditorStore.getState(); const tab = tabs.find((t) => t.id === activeTabId); - const { selectedConnectionId, activeConnections } = useConnectionStore.getState(); - const live = [ - ...activeConnections.map((c) => c.id), - ...useSqliteStore.getState().sessions.map((s) => s.id), - ]; + const { selectedConnectionId } = useConnectionStore.getState(); return { // The server the tab is marked with, not whichever is selected (#788). - connectionId: runConnectionId(tab, selectedConnectionId, live), + connectionId: runConnectionId(tab, selectedConnectionId, liveConnectionIds()), database: tab?.database, session: tab?.type === "query" ? tab.id : undefined, }; diff --git a/src/components/layout/useStartupReconnect.ts b/src/components/layout/useStartupReconnect.ts index 02c0a4af..ed5c8ad7 100644 --- a/src/components/layout/useStartupReconnect.ts +++ b/src/components/layout/useStartupReconnect.ts @@ -17,7 +17,7 @@ export function useStartupReconnect() { const { tabs, setTabConnection: setTabConn } = useEditorStore.getState(); const profileIds = [ ...new Set( - tabs.filter((t) => t.profileId).map((t) => t.profileId!), + tabs.flatMap((t) => (t.profileId ? [t.profileId] : [])), ), ]; diff --git a/src/hooks/__tests__/useLiveConnectionIds.test.ts b/src/hooks/__tests__/useLiveConnectionIds.test.ts new file mode 100644 index 00000000..8d246334 --- /dev/null +++ b/src/hooks/__tests__/useLiveConnectionIds.test.ts @@ -0,0 +1,51 @@ +import { act, renderHook } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { liveConnectionIds } from "../../lib/run-connection"; +import { useConnectionStore } from "../../stores/connectionStore"; +import { type SqliteSession, useSqliteStore } from "../../stores/sqliteStore"; +import type { ConnectionInfo } from "../../types"; +import { useLiveConnectionIds } from "../useLiveConnectionIds"; + +vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn() })); + +const server = (id: string) => ({ id }) as unknown as ConnectionInfo; +const file = (id: string) => ({ id }) as unknown as SqliteSession; + +beforeEach(() => { + useConnectionStore.setState({ activeConnections: [] }); + useSqliteStore.setState({ sessions: [] }); +}); + +describe("useLiveConnectionIds", () => { + it("lists connected servers and open SQLite files", () => { + useConnectionStore.setState({ activeConnections: [server("dev")] }); + useSqliteStore.setState({ sessions: [file("sqlite-1")] }); + + const { result } = renderHook(() => useLiveConnectionIds()); + + expect(result.current).toEqual(["dev", "sqlite-1"]); + }); + + it("follows servers and files as they come and go", () => { + const { result } = renderHook(() => useLiveConnectionIds()); + expect(result.current).toEqual([]); + + act(() => useConnectionStore.setState({ activeConnections: [server("dev"), server("prod")] })); + act(() => useSqliteStore.setState({ sessions: [file("sqlite-1")] })); + expect(result.current).toEqual(["dev", "prod", "sqlite-1"]); + + act(() => useConnectionStore.setState({ activeConnections: [server("prod")] })); + expect(result.current).toEqual(["prod", "sqlite-1"]); + }); + + // Where a run goes and whether a tab's connection still counts (#944) read + // one list each; they must not drift apart. + it("is the same list as liveConnectionIds", () => { + useConnectionStore.setState({ activeConnections: [server("dev")] }); + useSqliteStore.setState({ sessions: [file("sqlite-1"), file("sqlite-2")] }); + + const { result } = renderHook(() => useLiveConnectionIds()); + + expect(result.current).toEqual(liveConnectionIds()); + }); +}); diff --git a/src/hooks/useLiveConnectionIds.ts b/src/hooks/useLiveConnectionIds.ts new file mode 100644 index 00000000..ff9aa530 --- /dev/null +++ b/src/hooks/useLiveConnectionIds.ts @@ -0,0 +1,22 @@ +import { useMemo } from "react"; +import { useConnectionStore } from "../stores/connectionStore"; +import { useSqliteStore } from "../stores/sqliteStore"; + +/** + * Every connection that can run a statement right now, for a component: the + * connected servers and the open SQLite files. + * + * `liveConnectionIds` (lib/run-connection) is the same list read once, for + * code outside a component. This one re-renders its caller when a server + * connects or disconnects or a file opens or closes. Both have to stay the + * same list: where a run goes and whether a tab's connection still counts + * (#944) depend on it. + */ +export function useLiveConnectionIds(): string[] { + const activeConnections = useConnectionStore((s) => s.activeConnections); + const sqliteSessions = useSqliteStore((s) => s.sessions); + return useMemo( + () => [...activeConnections.map((c) => c.id), ...sqliteSessions.map((s) => s.id)], + [activeConnections, sqliteSessions], + ); +} diff --git a/src/hooks/useQueryExecution.ts b/src/hooks/useQueryExecution.ts index 07a82f9a..e151f607 100644 --- a/src/hooks/useQueryExecution.ts +++ b/src/hooks/useQueryExecution.ts @@ -3,7 +3,7 @@ import { runConnectionId } from "../lib/run-connection"; import { useConnectionStore } from "../stores/connectionStore"; import { useEditorStore } from "../stores/editorStore"; import { useResultStore } from "../stores/resultStore"; -import { useSqliteStore } from "../stores/sqliteStore"; +import { useLiveConnectionIds } from "./useLiveConnectionIds"; /** * The editor toolbar's execution context, pre-bound. @@ -27,8 +27,7 @@ export function useQueryExecution() { const selectedConnectionId = useConnectionStore( (s) => s.selectedConnectionId, ); - const activeConnections = useConnectionStore((s) => s.activeConnections); - const sqliteSessions = useSqliteStore((s) => s.sessions); + const liveConnectionIds = useLiveConnectionIds(); const activeTabId = useEditorStore((s) => s.activeTabId); const tabs = useEditorStore((s) => s.tabs); const activeTab = tabs.find((t) => t.id === activeTabId); @@ -41,10 +40,7 @@ export function useQueryExecution() { const isExecuting = useResultStore((s) => s.isExecuting); // The server the tab is marked with, not whichever is selected (#788). - const connectionId = runConnectionId(activeTab, selectedConnectionId, [ - ...activeConnections.map((c) => c.id), - ...sqliteSessions.map((s) => s.id), - ]); + const connectionId = runConnectionId(activeTab, selectedConnectionId, liveConnectionIds); const database = activeTab?.database; // The tab's own server session, so what one run sets up is there for the // next (#731). Only a query tab has one; the others are views of an object. diff --git a/src/hooks/useResultsFollowActiveTab.ts b/src/hooks/useResultsFollowActiveTab.ts index 35cc4ec8..4a8e7342 100644 --- a/src/hooks/useResultsFollowActiveTab.ts +++ b/src/hooks/useResultsFollowActiveTab.ts @@ -3,7 +3,7 @@ import { runConnectionId } from "../lib/run-connection"; import { useConnectionStore } from "../stores/connectionStore"; import { useEditorStore } from "../stores/editorStore"; import { useResultStore } from "../stores/resultStore"; -import { useSqliteStore } from "../stores/sqliteStore"; +import { useLiveConnectionIds } from "./useLiveConnectionIds"; /** * Keep a grid read on one server from being shown under another (#842). @@ -19,16 +19,12 @@ export function useResultsFollowActiveTab() { const activeTabId = useEditorStore((s) => s.activeTabId); const tabs = useEditorStore((s) => s.tabs); const selectedConnectionId = useConnectionStore((s) => s.selectedConnectionId); - const activeConnections = useConnectionStore((s) => s.activeConnections); - const sqliteSessions = useSqliteStore((s) => s.sessions); + const liveConnectionIds = useLiveConnectionIds(); const dropForeignResults = useResultStore((s) => s.dropForeignResults); const activeTab = tabs.find((t) => t.id === activeTabId); // The tab's own server, as Run would pick it (#788). - const tabConnectionId = runConnectionId(activeTab, selectedConnectionId, [ - ...activeConnections.map((c) => c.id), - ...sqliteSessions.map((s) => s.id), - ]); + const tabConnectionId = runConnectionId(activeTab, selectedConnectionId, liveConnectionIds); useEffect(() => { dropForeignResults(tabConnectionId);