diff --git a/frontend/src/api/jails.ts b/frontend/src/api/jails.ts index 16d5b78..de49a92 100644 --- a/frontend/src/api/jails.ts +++ b/frontend/src/api/jails.ts @@ -237,11 +237,12 @@ export async function unbanAllBans(): Promise { * Look up ban status and geo-location for an IP address. * * @param ip - IP address to look up. + * @param signal - Optional abort signal for request cancellation. * @returns An {@link IpLookupResponse} with ban history and geo info. * @throws {ApiError} On non-2xx responses (400 for invalid IP). */ -export async function lookupIp(ip: string): Promise { - return get(ENDPOINTS.geoLookup(ip)); +export async function lookupIp(ip: string, signal?: AbortSignal): Promise { + return get(ENDPOINTS.geoLookup(ip), signal); } // --------------------------------------------------------------------------- diff --git a/frontend/src/hooks/useIpLookup.ts b/frontend/src/hooks/useIpLookup.ts index ffbc74b..a58696e 100644 --- a/frontend/src/hooks/useIpLookup.ts +++ b/frontend/src/hooks/useIpLookup.ts @@ -2,7 +2,7 @@ * React hook for looking up a single IP address. */ -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { handleFetchError } from "../utils/fetchError"; import { lookupIp } from "../api/jails"; import type { IpLookupResponse } from "../types/jail"; @@ -22,22 +22,34 @@ export function useIpLookup(): UseIpLookupResult { const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const abortRef = useRef(null); - const lookup = useCallback((ip: string) => { - setLoading(true); - setError(null); - setResult(null); + const lookup = useCallback((ip: string): void => { + (async (): Promise => { + // Abort any previous lookup + abortRef.current?.abort(); + const ctrl = new AbortController(); + abortRef.current = ctrl; - lookupIp(ip) - .then((res) => { + setLoading(true); + setError(null); + setResult(null); + + try { + const res = await lookupIp(ip, ctrl.signal); + if (ctrl.signal.aborted) return; setResult(res); - }) - .catch((err: unknown) => { + } catch (err: unknown) { + if (ctrl.signal.aborted) return; handleFetchError(err, setError, "Failed to lookup IP"); - }) - .finally(() => { - setLoading(false); - }); + } finally { + if (!ctrl.signal.aborted) { + setLoading(false); + } + } + })().catch(() => { + // Silently ignore abort errors + }); }, []); const clear = useCallback(() => { @@ -45,5 +57,12 @@ export function useIpLookup(): UseIpLookupResult { setError(null); }, []); + // Cleanup: abort any pending lookup on unmount + useEffect(() => { + return (): void => { + abortRef.current?.abort(); + }; + }, []); + return { result, loading, error, lookup, clear }; }