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);