diff --git a/frontend/src/components/DriverManagerModal.request-boundary.test.ts b/frontend/src/components/DriverManagerModal.request-boundary.test.ts index aa94ab78..fd3a717a 100644 --- a/frontend/src/components/DriverManagerModal.request-boundary.test.ts +++ b/frontend/src/components/DriverManagerModal.request-boundary.test.ts @@ -1,35 +1,91 @@ -import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { describe, expect, it } from 'vitest'; +import { describe, expect, it, vi } from 'vitest'; -const source = readFileSync( - fileURLToPath(new globalThis.URL('./DriverManagerModal.tsx', import.meta.url)), - 'utf8', -).replace(/\r\n/g, '\n'); +import { + createDriverStatusSnapshotRegistry, + restoreDriverNetworkSnapshot, + restoreDriverStatusSnapshot, + settleLatestDriverRequest, +} from '../utils/driverManagerRequestState'; describe('DriverManagerModal request coordination boundary', () => { - it('lets the latest request clear a loading state even when it is a silent refresh', () => { - expect(source).toMatch( - /finally \{\s*if \(requestGeneration === statusRequestGenerationRef\.current\) \{\s*setLoading\(false\);/s, - ); - expect(source).toMatch( - /finally \{\s*if \(requestGeneration === networkRequestGenerationRef\.current\) \{\s*setNetworkChecking\(false\);/s, - ); - expect(source).not.toContain('showLoading && requestGeneration === statusRequestGenerationRef.current'); - expect(source).not.toContain('showLoading && requestGeneration === networkRequestGenerationRef.current'); + it('lets only the latest request clear loading, including silent refreshes', () => { + const setStatusLoading = vi.fn(); + const setNetworkLoading = vi.fn(); + + expect(settleLatestDriverRequest(2, 3, setStatusLoading)).toBe(false); + expect(setStatusLoading).not.toHaveBeenCalled(); + expect(settleLatestDriverRequest(3, 3, setStatusLoading)).toBe(true); + expect(setStatusLoading).toHaveBeenCalledWith(false); + + expect(settleLatestDriverRequest(7, 7, setNetworkLoading)).toBe(true); + expect(setNetworkLoading).toHaveBeenCalledWith(false); }); - it('clears cold loading flags when another instance populated a fresh snapshot first', () => { - expect(source).toMatch(/if \(cachedStatus\) \{\s*setRows\(cachedStatus\.rows\);\s*setLoading\(false\);/s); - expect(source).toContain('downloadDirRef.current = cachedStatus.downloadDir;'); - expect(source).toMatch(/if \(cachedNetwork\) \{\s*setNetworkStatus\(cachedNetwork\.status\);\s*setNetworkChecking\(false\);/s); + it('restores fresh snapshots and clears cold loading flags', () => { + const setRows = vi.fn(); + const setStatusLoading = vi.fn(); + const setDownloadDir = vi.fn(); + const setNetworkStatus = vi.fn(); + const setNetworkLoading = vi.fn(); + const rows = [{ type: 'mysql' }]; + const networkStatus = { reachable: true }; + + expect(restoreDriverStatusSnapshot({ + rows, + downloadDir: 'D:/drivers', + cachedAt: 100, + intentSequence: 4, + }, { + setRows, + setLoading: setStatusLoading, + setDownloadDir, + })).toBe(true); + expect(setRows).toHaveBeenCalledWith(rows); + expect(setStatusLoading).toHaveBeenCalledWith(false); + expect(setDownloadDir).toHaveBeenCalledWith('D:/drivers'); + + expect(restoreDriverNetworkSnapshot({ status: networkStatus, cachedAt: 100 }, { + setStatus: setNetworkStatus, + setLoading: setNetworkLoading, + })).toBe(true); + expect(setNetworkStatus).toHaveBeenCalledWith(networkStatus); + expect(setNetworkLoading).toHaveBeenCalledWith(false); }); - it('keeps status snapshots keyed and rejects writes older than the latest intent for a directory', () => { - expect(source).toContain('const driverStatusSnapshotCache = new Map();'); - expect(source).toContain('const driverStatusSnapshotIntentByKey = new Map();'); - expect(source).toContain('preferredDriverStatusSnapshotKey = requestKey;'); - expect(source).toContain('snapshot.intentSequence < latestIntentForKey'); - expect(source).toContain('writeDriverStatusSnapshot(resolvedRequestKey, snapshot);'); + it('keeps status snapshots keyed and rejects writes older than the latest intent', () => { + const snapshots = createDriverStatusSnapshotRegistry<{ type: string }>(); + const mysqlKey = snapshots.beginRequest('D:/mysql', 2); + + expect(snapshots.write(mysqlKey, { + rows: [{ type: 'stale' }], + downloadDir: 'D:/mysql', + cachedAt: 100, + intentSequence: 1, + })).toBe(false); + expect(snapshots.getPreferred()).toBeNull(); + + expect(snapshots.write(mysqlKey, { + rows: [{ type: 'mysql' }], + downloadDir: 'D:/mysql', + cachedAt: 200, + intentSequence: 2, + })).toBe(true); + expect(snapshots.getPreferred()?.rows).toEqual([{ type: 'mysql' }]); + + const postgresKey = snapshots.beginRequest('D:/postgres', 3); + expect(snapshots.write(postgresKey, { + rows: [{ type: 'postgres' }], + downloadDir: 'D:/postgres', + cachedAt: 300, + intentSequence: 3, + })).toBe(true); + expect(snapshots.getPreferred()?.rows).toEqual([{ type: 'postgres' }]); + + expect(snapshots.write(mysqlKey, { + rows: [{ type: 'older-mysql' }], + downloadDir: 'D:/mysql', + cachedAt: 150, + intentSequence: 1, + })).toBe(false); }); }); diff --git a/frontend/src/components/DriverManagerModal.tsx b/frontend/src/components/DriverManagerModal.tsx index d7076f9e..0f55bfa8 100644 --- a/frontend/src/components/DriverManagerModal.tsx +++ b/frontend/src/components/DriverManagerModal.tsx @@ -16,6 +16,14 @@ import { getDriverLocalImportDirectoryHelp, getDriverLocalImportSingleFileHelp, } from '../utils/driverImportGuidance'; +import { + createDriverStatusSnapshotRegistry, + normalizeDriverStatusRequestKey, + restoreDriverNetworkSnapshot, + restoreDriverStatusSnapshot, + settleLatestDriverRequest, + type DriverStatusSnapshot as DriverStatusSnapshotState, +} from '../utils/driverManagerRequestState'; import { CheckDriverNetworkStatus, DownloadDriverPackage, @@ -469,16 +477,7 @@ const createDriverBatchProgress = (total: number, currentMessage: string): Drive currentMessage, }); -type DriverStatusSnapshot = { - rows: DriverStatusRow[]; - downloadDir: string; - cachedAt: number; - intentSequence: number; -}; -const DEFAULT_DRIVER_STATUS_REQUEST_KEY = ''; -const driverStatusSnapshotCache = new Map(); -const driverStatusSnapshotIntentByKey = new Map(); -let preferredDriverStatusSnapshotKey = DEFAULT_DRIVER_STATUS_REQUEST_KEY; +const driverStatusSnapshots = createDriverStatusSnapshotRegistry(); let driverNetworkSnapshotCache: { status: DriverNetworkStatus; cachedAt: number } | null = null; let driverStatusSnapshotIntentSequence = 0; type DriverStatusBackendResult = Awaited>; @@ -486,23 +485,6 @@ type DriverNetworkBackendResult = Awaited>(); let driverNetworkInFlightRequest: Promise | null = null; -const normalizeDriverStatusRequestKey = (downloadDir: string): string => ( - String(downloadDir || '').trim() || DEFAULT_DRIVER_STATUS_REQUEST_KEY -); - -const getPreferredDriverStatusSnapshot = (): DriverStatusSnapshot | null => ( - driverStatusSnapshotCache.get(preferredDriverStatusSnapshotKey) || null -); - -const writeDriverStatusSnapshot = (requestKey: string, snapshot: DriverStatusSnapshot): void => { - const latestIntentForKey = driverStatusSnapshotIntentByKey.get(requestKey) || 0; - const cachedSnapshot = driverStatusSnapshotCache.get(requestKey); - if (snapshot.intentSequence < latestIntentForKey || snapshot.intentSequence < (cachedSnapshot?.intentSequence || 0)) { - return; - } - driverStatusSnapshotCache.set(requestKey, snapshot); -}; - const requestDriverStatusShared = (downloadDir: string): Promise => { const requestKey = normalizeDriverStatusRequestKey(downloadDir); const pendingRequest = driverStatusInFlightRequests.get(requestKey); @@ -601,12 +583,12 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? () => buildDriverManagerWorkbenchTheme(darkMode, opacity), [darkMode, opacity, appearance.uiVersion], ); - const [loading, setLoading] = useState(() => open && !getPreferredDriverStatusSnapshot()); - const [downloadDir, setDownloadDir] = useState(() => getPreferredDriverStatusSnapshot()?.downloadDir || ''); + const [loading, setLoading] = useState(() => open && !driverStatusSnapshots.getPreferred()); + const [downloadDir, setDownloadDir] = useState(() => driverStatusSnapshots.getPreferred()?.downloadDir || ''); const [networkChecking, setNetworkChecking] = useState(() => open && !driverNetworkSnapshotCache); const [networkStatus, setNetworkStatus] = useState(() => driverNetworkSnapshotCache?.status || null); const [searchKeyword, setSearchKeyword] = useState(''); - const [rows, setRows] = useState(() => getPreferredDriverStatusSnapshot()?.rows || []); + const [rows, setRows] = useState(() => driverStatusSnapshots.getPreferred()?.rows || []); const [actionState, setActionState] = useState<{ driverType: string; kind: DriverActionKind }>({ driverType: '', kind: '' }); const [batchAction, setBatchAction] = useState(''); const [batchProgress, setBatchProgress] = useState(null); @@ -762,9 +744,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? const snapshotIntentSequence = driverStatusSnapshotIntentSequence + 1; driverStatusSnapshotIntentSequence = snapshotIntentSequence; const requestedDownloadDir = downloadDirRef.current; - const requestKey = normalizeDriverStatusRequestKey(requestedDownloadDir); - preferredDriverStatusSnapshotKey = requestKey; - driverStatusSnapshotIntentByKey.set(requestKey, snapshotIntentSequence); + const requestKey = driverStatusSnapshots.beginRequest(requestedDownloadDir, snapshotIntentSequence); const showLoading = options?.showLoading ?? true; if (showLoading) { setLoading(true); @@ -816,25 +796,23 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? message: String(item.message || '').trim() || undefined, })); setRows(nextRows); - const snapshot: DriverStatusSnapshot = { + const snapshot: DriverStatusSnapshotState = { rows: nextRows, downloadDir: effectiveDownloadDir, cachedAt: Date.now(), intentSequence: snapshotIntentSequence, }; - writeDriverStatusSnapshot(requestKey, snapshot); + driverStatusSnapshots.write(requestKey, snapshot); const resolvedRequestKey = normalizeDriverStatusRequestKey(effectiveDownloadDir); if (resolvedRequestKey !== requestKey) { - writeDriverStatusSnapshot(resolvedRequestKey, snapshot); + driverStatusSnapshots.write(resolvedRequestKey, snapshot); } } catch (err: any) { if (requestGeneration === statusRequestGenerationRef.current && toastOnError) { message.error(t('driver.modal.error.statusFetchWithDetail', { detail: err?.message || String(err) })); } } finally { - if (requestGeneration === statusRequestGenerationRef.current) { - setLoading(false); - } + settleLatestDriverRequest(requestGeneration, statusRequestGenerationRef.current, setLoading); } }, [resolveDriverErrorMessage]); @@ -901,9 +879,7 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? message.error(t('driver.modal.error.networkCheckWithDetail', { detail: err?.message || String(err) })); } } finally { - if (requestGeneration === networkRequestGenerationRef.current) { - setNetworkChecking(false); - } + settleLatestDriverRequest(requestGeneration, networkRequestGenerationRef.current, setNetworkChecking); } }, [resolveDriverErrorMessage]); @@ -1061,16 +1037,16 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? return; } - const cachedStatus = getPreferredDriverStatusSnapshot(); + const cachedStatus = driverStatusSnapshots.getPreferred(); const hasCachedStatus = !!cachedStatus; - if (cachedStatus) { - setRows(cachedStatus.rows); - setLoading(false); - if (cachedStatus.downloadDir) { - downloadDirRef.current = cachedStatus.downloadDir; - setDownloadDir(cachedStatus.downloadDir); - } - } + restoreDriverStatusSnapshot(cachedStatus, { + setRows, + setLoading, + setDownloadDir: (nextDownloadDir) => { + downloadDirRef.current = nextDownloadDir; + setDownloadDir(nextDownloadDir); + }, + }); const shouldRefreshStatus = !cachedStatus || !isFreshCache(cachedStatus.cachedAt, DRIVER_STATUS_CACHE_TTL_MS); if (shouldRefreshStatus) { void refreshStatus(false, { showLoading: !hasCachedStatus }); @@ -1078,10 +1054,10 @@ const DriverManagerModal: React.FC<{ open: boolean; onClose: () => void; onBack? const cachedNetwork = driverNetworkSnapshotCache; const hasCachedNetwork = !!cachedNetwork; - if (cachedNetwork) { - setNetworkStatus(cachedNetwork.status); - setNetworkChecking(false); - } + restoreDriverNetworkSnapshot(cachedNetwork, { + setStatus: setNetworkStatus, + setLoading: setNetworkChecking, + }); const shouldRefreshNetwork = !cachedNetwork || !isFreshCache(cachedNetwork.cachedAt, DRIVER_NETWORK_CACHE_TTL_MS); if (shouldRefreshNetwork) { void checkNetworkStatus(false, { showLoading: !hasCachedNetwork }); diff --git a/frontend/src/utils/driverImportGuidance.test.ts b/frontend/src/utils/driverImportGuidance.test.ts index 0426bb70..c840fa0b 100644 --- a/frontend/src/utils/driverImportGuidance.test.ts +++ b/frontend/src/utils/driverImportGuidance.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; +import { resolveDriverErrorMessageText } from '../components/DriverManagerModal'; import { t as catalogT } from '../i18n/catalog'; import { t } from '../i18n'; import { @@ -68,8 +69,6 @@ describe('driver import guidance', () => { backendWrapperKeys, expected, }) => { - const { resolveDriverErrorMessageText } = await import('../components/DriverManagerModal'); - expect(resolveDriverErrorMessageText( rawMessage, fallbackMessage, diff --git a/frontend/src/utils/driverManagerRequestState.ts b/frontend/src/utils/driverManagerRequestState.ts new file mode 100644 index 00000000..38b3f511 --- /dev/null +++ b/frontend/src/utils/driverManagerRequestState.ts @@ -0,0 +1,96 @@ +export type DriverStatusSnapshot = { + rows: TRow[]; + downloadDir: string; + cachedAt: number; + intentSequence: number; +}; + +export type DriverNetworkSnapshot = { + status: TStatus; + cachedAt: number; +}; + +const DEFAULT_DRIVER_STATUS_REQUEST_KEY = ''; + +export const normalizeDriverStatusRequestKey = (downloadDir: string): string => ( + String(downloadDir || '').trim() || DEFAULT_DRIVER_STATUS_REQUEST_KEY +); + +export const createDriverStatusSnapshotRegistry = () => { + const snapshots = new Map>(); + const latestIntentByKey = new Map(); + let preferredRequestKey = DEFAULT_DRIVER_STATUS_REQUEST_KEY; + + return { + beginRequest(downloadDir: string, intentSequence: number): string { + const requestKey = normalizeDriverStatusRequestKey(downloadDir); + preferredRequestKey = requestKey; + latestIntentByKey.set(requestKey, intentSequence); + return requestKey; + }, + + getPreferred(): DriverStatusSnapshot | null { + return snapshots.get(preferredRequestKey) || null; + }, + + write(requestKey: string, snapshot: DriverStatusSnapshot): boolean { + const normalizedRequestKey = normalizeDriverStatusRequestKey(requestKey); + const latestIntentForKey = latestIntentByKey.get(normalizedRequestKey) || 0; + const cachedSnapshot = snapshots.get(normalizedRequestKey); + if ( + snapshot.intentSequence < latestIntentForKey + || snapshot.intentSequence < (cachedSnapshot?.intentSequence || 0) + ) { + return false; + } + snapshots.set(normalizedRequestKey, snapshot); + return true; + }, + }; +}; + +export const settleLatestDriverRequest = ( + requestGeneration: number, + latestRequestGeneration: number, + setLoading: (loading: boolean) => void, +): boolean => { + if (requestGeneration !== latestRequestGeneration) { + return false; + } + setLoading(false); + return true; +}; + +export const restoreDriverStatusSnapshot = ( + snapshot: DriverStatusSnapshot | null, + callbacks: { + setRows: (rows: TRow[]) => void; + setLoading: (loading: boolean) => void; + setDownloadDir: (downloadDir: string) => void; + }, +): boolean => { + if (!snapshot) { + return false; + } + callbacks.setRows(snapshot.rows); + callbacks.setLoading(false); + if (snapshot.downloadDir) { + callbacks.setDownloadDir(snapshot.downloadDir); + } + return true; +}; + +export const restoreDriverNetworkSnapshot = ( + snapshot: DriverNetworkSnapshot | null, + callbacks: { + setStatus: (status: TStatus) => void; + setLoading: (loading: boolean) => void; + }, +): boolean => { + if (!snapshot) { + return false; + } + callbacks.setStatus(snapshot.status); + callbacks.setLoading(false); + return true; +};