import { useEffect, useMemo, useRef, useState } from 'react'; import { ImageRequest } from '/@/shared/types/domain-types'; type FetchPriority = 'auto' | 'high' | 'low'; interface NativeImageState { displaySrc?: string; status: 'error' | 'idle' | 'loaded' | 'loading'; } interface UseNativeImageArgs { enabled: boolean; fetchPriority?: FetchPriority; onFetchError?: () => void; request?: ImageRequest | null; } export function useNativeImage({ enabled, fetchPriority, onFetchError, request, }: UseNativeImageArgs) { const abortControllerRef = useRef(null); const loadedRequestSignatureRef = useRef(null); const objectUrlRef = useRef(null); const onFetchErrorRef = useRef(onFetchError); const [state, setState] = useState({ status: 'idle' }); const requestSignature = useMemo(() => { if (!request) { return null; } return JSON.stringify({ cacheKey: request.cacheKey, credentials: request.credentials, headers: request.headers, url: request.url, }); }, [request]); onFetchErrorRef.current = onFetchError; useEffect(() => { const abortCurrentRequest = () => { abortControllerRef.current?.abort(); abortControllerRef.current = null; }; const revokeObjectUrl = () => { if (!objectUrlRef.current) { return; } URL.revokeObjectURL(objectUrlRef.current); objectUrlRef.current = null; loadedRequestSignatureRef.current = null; }; if (!request || !requestSignature) { abortCurrentRequest(); revokeObjectUrl(); setState({ status: 'idle' }); return; } if (!enabled) { abortCurrentRequest(); setState((currentState) => currentState.displaySrc ? { ...currentState, status: 'loaded' } : { status: 'idle' }, ); return; } if (loadedRequestSignatureRef.current === requestSignature && objectUrlRef.current) { setState({ displaySrc: objectUrlRef.current, status: 'loaded' }); return; } abortCurrentRequest(); revokeObjectUrl(); setState({ status: 'loading' }); const abortController = new AbortController(); abortControllerRef.current = abortController; void (async () => { try { const init = { credentials: request.credentials, headers: request.headers, signal: abortController.signal, } as RequestInit & { priority?: FetchPriority }; if (fetchPriority) { init.priority = fetchPriority; } const response = await fetch(request.url, init); if (!response.ok) { throw new Error(`Failed to load image: ${response.status}`); } const blob = await response.blob(); if (abortController.signal.aborted) { return; } const objectUrl = URL.createObjectURL(blob); objectUrlRef.current = objectUrl; loadedRequestSignatureRef.current = requestSignature; setState({ displaySrc: objectUrl, status: 'loaded' }); } catch { if (abortController.signal.aborted) { return; } revokeObjectUrl(); setState({ status: 'error' }); onFetchErrorRef.current?.(); } finally { if (abortControllerRef.current === abortController) { abortControllerRef.current = null; } } })(); return () => { abortController.abort(); if (abortControllerRef.current === abortController) { abortControllerRef.current = null; } }; }, [enabled, fetchPriority, request, requestSignature]); useEffect(() => { return () => { abortControllerRef.current?.abort(); if (objectUrlRef.current) { URL.revokeObjectURL(objectUrlRef.current); } }; }, []); return { displaySrc: state.displaySrc, isError: state.status === 'error', isLoaded: state.status === 'loaded', isLoading: state.status === 'loading', }; }