mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-14 02:19:58 +08:00
🐛 fix(sql-editor): 防止事务重复提交误报失败
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
import React from 'react';
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useSqlEditorTransactionController } from './useSqlEditorTransactionController';
|
||||
import type { PendingSqlEditorTransaction } from './QueryEditorTransactionToolbar';
|
||||
|
||||
const storeState = vi.hoisted(() => ({
|
||||
setSqlEditorPendingTransaction: vi.fn(),
|
||||
}));
|
||||
|
||||
const backendApp = vi.hoisted(() => ({
|
||||
DBCommitTransaction: vi.fn(),
|
||||
DBRollbackTransaction: vi.fn(),
|
||||
}));
|
||||
|
||||
const messageApi = vi.hoisted(() => ({
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../store', () => ({
|
||||
useStore: (selector: (state: typeof storeState) => unknown) => selector(storeState),
|
||||
}));
|
||||
|
||||
vi.mock('../../wailsjs/go/app/App', () => backendApp);
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
message: messageApi,
|
||||
}));
|
||||
|
||||
const createPendingTransaction = (overrides: Partial<PendingSqlEditorTransaction> = {}): PendingSqlEditorTransaction => ({
|
||||
id: 'tx-1',
|
||||
commitMode: 'manual',
|
||||
autoCommitDelayMs: 0,
|
||||
createdAt: Date.now(),
|
||||
statementCount: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('useSqlEditorTransactionController', () => {
|
||||
let controller: ReturnType<typeof useSqlEditorTransactionController> | null = null;
|
||||
let renderer: ReactTestRenderer | null = null;
|
||||
|
||||
const renderController = () => {
|
||||
const Harness = () => {
|
||||
controller = useSqlEditorTransactionController({ tabId: 'tab-1' });
|
||||
return null;
|
||||
};
|
||||
|
||||
act(() => {
|
||||
renderer = create(<Harness />);
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
controller = null;
|
||||
renderer = null;
|
||||
storeState.setSqlEditorPendingTransaction.mockReset();
|
||||
backendApp.DBCommitTransaction.mockReset();
|
||||
backendApp.DBRollbackTransaction.mockReset();
|
||||
messageApi.error.mockReset();
|
||||
messageApi.success.mockReset();
|
||||
backendApp.DBCommitTransaction.mockResolvedValue({ success: true, message: '事务已提交' });
|
||||
backendApp.DBRollbackTransaction.mockResolvedValue({ success: true, message: '事务已回滚' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => {
|
||||
renderer?.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores duplicate finish requests for the same pending transaction', async () => {
|
||||
renderController();
|
||||
|
||||
await act(async () => {
|
||||
controller?.activatePendingSqlTransaction(createPendingTransaction());
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
const first = controller?.finishPendingSqlTransaction('commit', 'manual');
|
||||
const second = controller?.finishPendingSqlTransaction('commit', 'manual');
|
||||
await Promise.all([first, second]);
|
||||
});
|
||||
|
||||
expect(backendApp.DBCommitTransaction).toHaveBeenCalledTimes(1);
|
||||
expect(backendApp.DBCommitTransaction).toHaveBeenCalledWith('tx-1');
|
||||
expect(backendApp.DBRollbackTransaction).not.toHaveBeenCalled();
|
||||
expect(messageApi.success).toHaveBeenCalledWith('SQL 事务已提交');
|
||||
});
|
||||
|
||||
it('does not rollback a transaction while its auto commit is in flight', async () => {
|
||||
let resolveCommit!: (value: { success: boolean; message: string }) => void;
|
||||
backendApp.DBCommitTransaction.mockReturnValue(new Promise((resolve) => {
|
||||
resolveCommit = resolve;
|
||||
}));
|
||||
renderController();
|
||||
|
||||
await act(async () => {
|
||||
controller?.activatePendingSqlTransaction(createPendingTransaction({
|
||||
commitMode: 'auto',
|
||||
autoCommitDelayMs: 0,
|
||||
}));
|
||||
});
|
||||
|
||||
const finishPromise = controller?.finishPendingSqlTransaction('commit', 'auto');
|
||||
act(() => {
|
||||
renderer?.unmount();
|
||||
renderer = null;
|
||||
});
|
||||
|
||||
expect(backendApp.DBRollbackTransaction).not.toHaveBeenCalled();
|
||||
expect(backendApp.DBCommitTransaction).toHaveBeenCalledTimes(1);
|
||||
|
||||
await act(async () => {
|
||||
resolveCommit({ success: true, message: '事务已提交' });
|
||||
await finishPromise;
|
||||
});
|
||||
|
||||
expect(messageApi.success).toHaveBeenCalledWith('SQL 事务已自动提交');
|
||||
});
|
||||
});
|
||||
@@ -19,6 +19,7 @@ export const useSqlEditorTransactionController = ({
|
||||
const setSqlEditorPendingTransaction = useStore(state => state.setSqlEditorPendingTransaction);
|
||||
const [pendingSqlTransaction, setPendingSqlTransaction] = useState<PendingSqlEditorTransaction | null>(null);
|
||||
const pendingSqlTransactionRef = useRef<PendingSqlEditorTransaction | null>(null);
|
||||
const finishingTransactionIdsRef = useRef<Set<string>>(new Set());
|
||||
const autoCommitTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const autoCommitCountdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||
const [autoCommitRemainingSeconds, setAutoCommitRemainingSeconds] = useState<number | null>(null);
|
||||
@@ -50,13 +51,17 @@ export const useSqlEditorTransactionController = ({
|
||||
if (!transaction || (transactionId && transaction.id !== transactionId)) {
|
||||
return;
|
||||
}
|
||||
if (finishingTransactionIdsRef.current.has(transaction.id)) {
|
||||
return;
|
||||
}
|
||||
clearAutoCommitTimer();
|
||||
finishingTransactionIdsRef.current.add(transaction.id);
|
||||
updatePendingSqlTransaction(null);
|
||||
try {
|
||||
const res = action === 'commit'
|
||||
? await DBCommitTransaction(transaction.id)
|
||||
: await DBRollbackTransaction(transaction.id);
|
||||
if (res?.success) {
|
||||
updatePendingSqlTransaction(null);
|
||||
if (action === 'commit') {
|
||||
message.success(source === 'auto' ? 'SQL 事务已自动提交' : 'SQL 事务已提交');
|
||||
} else {
|
||||
@@ -64,13 +69,13 @@ export const useSqlEditorTransactionController = ({
|
||||
}
|
||||
return;
|
||||
}
|
||||
updatePendingSqlTransaction(null);
|
||||
const fallback = action === 'commit' ? '提交失败' : '回滚失败';
|
||||
message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(res?.message || '未知错误')}`);
|
||||
} catch (err: any) {
|
||||
updatePendingSqlTransaction(null);
|
||||
const fallback = action === 'commit' ? '提交失败' : '回滚失败';
|
||||
message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(err?.message || err || '未知错误')}`);
|
||||
} finally {
|
||||
finishingTransactionIdsRef.current.delete(transaction.id);
|
||||
}
|
||||
}, [clearAutoCommitTimer, updatePendingSqlTransaction]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user