fix race condition

This commit is contained in:
2026-03-19 12:13:37 +08:00
parent 5d96c565c1
commit ace38a84e2
2 changed files with 38 additions and 14 deletions

View File

@@ -116,26 +116,33 @@ const AppDetail: React.FC = () => {
async (onSuccessCallback?: () => Promise<void> | void) => { async (onSuccessCallback?: () => Promise<void> | void) => {
if (!currentDomain || !selectedAppId) return undefined; if (!currentDomain || !selectedAppId) return undefined;
// Capture at request time to detect staleness after awaits
const capturedDomainId = currentDomain.id;
const capturedAppId = selectedAppId;
setLoading(true); setLoading(true);
try { try {
const result = await window.api.getAppDetail({ const result = await window.api.getAppDetail({
domainId: currentDomain.id, domainId: capturedDomainId,
appId: selectedAppId, appId: capturedAppId,
}); });
// Check if we're still on the same app and component is mounted before updating
if (result.success) { if (result.success) {
setCurrentApp(result.data); // Guard: discard stale responses from previous domain/app switches
// Store revision after callback to avoid being cleared by clearChanges const nowDomainId = useDomainStore.getState().currentDomain?.id;
const revision = result.data.customization?.revision; const nowAppId = useAppStore.getState().selectedAppId;
// Call the callback if provided if (nowDomainId !== capturedDomainId || nowAppId !== capturedAppId) return undefined;
if (onSuccessCallback) { if (onSuccessCallback) {
await onSuccessCallback(); await onSuccessCallback();
} }
// Store the revision from Kintone API for remote change detection // Store revision after callback to avoid being cleared by clearChanges
const revision = result.data.customization?.revision;
setCurrentApp(result.data);
// Must be AFTER callback since clearChanges() resets serverRevision to null // Must be AFTER callback since clearChanges() resets serverRevision to null
if (revision && currentDomain) { // Only update knownRevision when user explicitly refreshes or after deployment
fileChangeStore.setServerRevision(currentDomain.id, selectedAppId, revision); if (revision && shouldUpdateKnownRevision) {
fileChangeStore.setServerRevision(capturedDomainId, capturedAppId, revision);
} }
return result.data; return result.data;
} }
@@ -144,8 +151,14 @@ const AppDetail: React.FC = () => {
console.error("Failed to load app detail:", error); console.error("Failed to load app detail:", error);
return undefined; return undefined;
} finally { } finally {
// Only reset loading if still on the same context; otherwise the new
// request's loading state should not be cleared by this stale response.
const nowDomainId = useDomainStore.getState().currentDomain?.id;
const nowAppId = useAppStore.getState().selectedAppId;
if (nowDomainId === capturedDomainId && nowAppId === capturedAppId) {
setLoading(false); setLoading(false);
} }
}
}, },
[currentDomain, selectedAppId, setCurrentApp, setLoading] [currentDomain, selectedAppId, setCurrentApp, setLoading]
); );
@@ -160,6 +173,9 @@ const AppDetail: React.FC = () => {
// Initialize file change store from Kintone data // Initialize file change store from Kintone data
useEffect(() => { useEffect(() => {
if (!currentApp || !currentDomain || !selectedAppId) return; if (!currentApp || !currentDomain || !selectedAppId) return;
// Guard against race condition: currentApp may be a stale response from a
// previous app's request that resolved after selectedAppId already changed.
if (String(currentApp.appId) !== String(selectedAppId)) return;
const customize = currentApp.customization; const customize = currentApp.customization;
if (!customize) return; if (!customize) return;

View File

@@ -88,24 +88,32 @@ const AppList: React.FC = () => {
const handleLoadApps = async () => { const handleLoadApps = async () => {
if (!currentDomain) return; if (!currentDomain) return;
const domainIdAtStart = currentDomain.id;
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const result = await window.api.getApps({ const result = await window.api.getApps({
domainId: currentDomain.id, domainId: domainIdAtStart,
}); });
// Discard result if domain switched during the request
if (useDomainStore.getState().currentDomain?.id !== domainIdAtStart) return;
if (result.success) { if (result.success) {
setApps(result.data); setApps(result.data);
} else { } else {
setError(result.error || t("loadAppsFailed")); setError(result.error || t("loadAppsFailed"));
} }
} catch (err) { } catch (err) {
if (useDomainStore.getState().currentDomain?.id === domainIdAtStart) {
setError(err instanceof Error ? err.message : t("loadAppsFailed")); setError(err instanceof Error ? err.message : t("loadAppsFailed"));
}
} finally { } finally {
if (useDomainStore.getState().currentDomain?.id === domainIdAtStart) {
setLoading(false); setLoading(false);
} }
}
}; };
// Sort apps // Sort apps