Files
MyGoNavi/frontend/src/components/useSqlEditorTransactionController.ts

136 lines
5.6 KiB
TypeScript

import { useCallback, useEffect, useRef, useState } from 'react';
import { message } from 'antd';
import { DBCommitTransaction, DBRollbackTransaction } from '../../wailsjs/go/app/App';
import { useStore } from '../store';
import { formatSqlExecutionError } from '../utils/sqlErrorSemantics';
import type { PendingSqlEditorTransaction } from './QueryEditorTransactionToolbar';
type FinishSqlEditorTransactionAction = 'commit' | 'rollback';
type FinishSqlEditorTransactionSource = 'manual' | 'auto';
type UseSqlEditorTransactionControllerOptions = {
tabId: string;
};
export const useSqlEditorTransactionController = ({
tabId,
}: UseSqlEditorTransactionControllerOptions) => {
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);
const clearAutoCommitTimer = useCallback(() => {
if (autoCommitTimerRef.current) {
clearTimeout(autoCommitTimerRef.current);
autoCommitTimerRef.current = null;
}
if (autoCommitCountdownRef.current) {
clearInterval(autoCommitCountdownRef.current);
autoCommitCountdownRef.current = null;
}
setAutoCommitRemainingSeconds(null);
}, []);
const updatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction | null) => {
pendingSqlTransactionRef.current = transaction;
setPendingSqlTransaction(transaction);
setSqlEditorPendingTransaction(tabId, transaction);
}, [setSqlEditorPendingTransaction, tabId]);
const finishPendingSqlTransaction = useCallback(async (
action: FinishSqlEditorTransactionAction,
source: FinishSqlEditorTransactionSource = 'manual',
transactionId?: string,
) => {
const transaction = pendingSqlTransactionRef.current;
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) {
if (action === 'commit') {
message.success(source === 'auto' ? 'SQL 事务已自动提交' : 'SQL 事务已提交');
} else {
message.success('SQL 事务已回滚');
}
return;
}
const fallback = action === 'commit' ? '提交失败' : '回滚失败';
message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(res?.message || '未知错误')}`);
} catch (err: any) {
const fallback = action === 'commit' ? '提交失败' : '回滚失败';
message.error(`${source === 'auto' ? '自动提交失败' : fallback}: ${formatSqlExecutionError(err?.message || err || '未知错误')}`);
} finally {
finishingTransactionIdsRef.current.delete(transaction.id);
}
}, [clearAutoCommitTimer, updatePendingSqlTransaction]);
const activatePendingSqlTransaction = useCallback((transaction: PendingSqlEditorTransaction) => {
clearAutoCommitTimer();
const autoCommitDelayMs = Math.max(0, Number(transaction.autoCommitDelayMs) || 0);
const dueAt = transaction.commitMode === 'auto' ? Date.now() + autoCommitDelayMs : null;
const nextTransaction = { ...transaction, autoCommitDelayMs, autoCommitDueAt: dueAt };
updatePendingSqlTransaction(nextTransaction);
if (nextTransaction.commitMode !== 'auto' || !dueAt) {
return;
}
if (autoCommitDelayMs === 0) {
setAutoCommitRemainingSeconds(0);
autoCommitTimerRef.current = setTimeout(() => {
autoCommitTimerRef.current = null;
setAutoCommitRemainingSeconds(null);
void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id);
}, 0);
return;
}
const updateRemaining = () => {
setAutoCommitRemainingSeconds(Math.max(1, Math.ceil((dueAt - Date.now()) / 1000)));
};
updateRemaining();
autoCommitCountdownRef.current = setInterval(updateRemaining, 250);
autoCommitTimerRef.current = setTimeout(() => {
autoCommitTimerRef.current = null;
if (autoCommitCountdownRef.current) {
clearInterval(autoCommitCountdownRef.current);
autoCommitCountdownRef.current = null;
}
setAutoCommitRemainingSeconds(null);
void finishPendingSqlTransaction('commit', 'auto', nextTransaction.id);
}, autoCommitDelayMs);
}, [clearAutoCommitTimer, finishPendingSqlTransaction, updatePendingSqlTransaction]);
useEffect(() => {
return () => {
clearAutoCommitTimer();
const transaction = pendingSqlTransactionRef.current;
if (transaction?.id) {
pendingSqlTransactionRef.current = null;
setSqlEditorPendingTransaction(tabId, null);
void DBRollbackTransaction(transaction.id);
}
};
}, [clearAutoCommitTimer, setSqlEditorPendingTransaction, tabId]);
return {
activatePendingSqlTransaction,
autoCommitRemainingSeconds,
finishPendingSqlTransaction,
pendingSqlTransaction,
pendingSqlTransactionRef,
};
};