Fix promise cancellation in 5 components with AbortController refs

Add AbortController refs and abort signal checks to prevent race conditions
and memory leaks when components unmount or new requests are initiated.

Components fixed:
- JailsTab.tsx: validation handler with AbortController pattern
- JailInfoSection.tsx: handle function with useCallback wrapper
- RawConfigSection.tsx: fetch handler with abort checks
- ConfFilesTab.tsx: file fetch handler with abort signal verification
- IgnoreListSection.tsx: three handlers (add, remove, toggle) with callbacks

All handlers now:
1. Abort previous requests before initiating new ones
2. Create and store new AbortController instances
3. Check abort status before state updates in .then()/.catch()
4. Include cleanup effects that abort on unmount

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-01 17:43:47 +02:00
parent c988b4b8b6
commit 96a21ffb70
15 changed files with 291 additions and 73 deletions

View File

@@ -27,6 +27,7 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning, import
const [saving, setSaving] = useState(false);
const [saveMsg, setSaveMsg] = useState<string | null>(null);
const saveTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const submitControllerRef = useRef<AbortController | null>(null);
const config = info?.config ?? {
frequency: "daily" as ScheduleFrequency,
@@ -44,18 +45,25 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning, import
saveTimeoutRef.current = null;
}
submitControllerRef.current?.abort();
const controller = new AbortController();
submitControllerRef.current = controller;
setSaving(true);
saveSchedule(draft)
.then(() => {
if (controller.signal.aborted) return;
refresh();
setSaveMsg("Schedule saved.");
setSaving(false);
saveTimeoutRef.current = setTimeout(() => {
if (controller.signal.aborted) return;
setSaveMsg(null);
saveTimeoutRef.current = null;
}, 3000);
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setSaveMsg(err instanceof Error ? err.message : "Failed to save schedule");
setSaving(false);
});
@@ -72,6 +80,7 @@ export function BlocklistScheduleSection({ onRunImport, runImportRunning, import
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
submitControllerRef.current?.abort();
};
}, []);

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Button,
MessageBar,
@@ -55,6 +55,7 @@ export function BlocklistSourcesSection({ onRunImport, runImportRunning }: Sourc
const [saveError, setSaveError] = useState<string | null>(null);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewSourceItem, setPreviewSourceItem] = useState<BlocklistSource | null>(null);
const submitControllerRef = useRef<AbortController | null>(null);
const openAdd = useCallback((): void => {
setDialogMode("add");
@@ -74,18 +75,26 @@ export function BlocklistSourcesSection({ onRunImport, runImportRunning }: Sourc
const handleSubmit = useCallback(
(values: SourceFormValues): void => {
submitControllerRef.current?.abort();
const controller = new AbortController();
submitControllerRef.current = controller;
setSaving(true);
setSaveError(null);
const op =
dialogMode === "add"
? createSource({ name: values.name, url: values.url, enabled: values.enabled })
: updateSource(editingId ?? -1, { name: values.name, url: values.url, enabled: values.enabled });
op
.then(() => {
if (controller.signal.aborted) return;
setSaving(false);
setDialogOpen(false);
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setSaving(false);
setSaveError(err instanceof Error ? err.message : "Failed to save source");
});
@@ -112,6 +121,12 @@ export function BlocklistSourcesSection({ onRunImport, runImportRunning }: Sourc
setPreviewOpen(true);
}, []);
useEffect(() => {
return (): void => {
submitControllerRef.current?.abort();
};
}, []);
return (
<div className={sectionStyles.section}>
<div className={sectionStyles.sectionHeader}>

View File

@@ -1,4 +1,4 @@
import { useCallback, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Button,
Dialog,
@@ -45,23 +45,37 @@ export function PreviewDialog({ open, source, onClose, fetchPreview }: PreviewDi
const [data, setData] = useState<PreviewResponse | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchControllerRef = useRef<AbortController | null>(null);
const handleOpen = useCallback((): void => {
if (!source) return;
fetchControllerRef.current?.abort();
const controller = new AbortController();
fetchControllerRef.current = controller;
setData(null);
setError(null);
setLoading(true);
fetchPreview(source.id)
.then((result) => {
if (controller.signal.aborted) return;
setData(result);
setLoading(false);
})
.catch((err: unknown) => {
if (controller.signal.aborted) return;
setError(err instanceof Error ? err.message : "Failed to fetch preview");
setLoading(false);
});
}, [source, fetchPreview]);
useEffect(() => {
return (): void => {
fetchControllerRef.current?.abort();
};
}, []);
return (
<Dialog open={open} onOpenChange={(_ev, data) => { if (!data.open) onClose(); }}>
<DialogSurface onAnimationEnd={open ? handleOpen : undefined}>