import { useCallback, useRef } from 'react'; /** * タイムアウト付きの非同期処理を管理するカスタムフック * AbortControllerを使用して強制的に処理を中断できる */ export function useTimeoutController() { const abortControllerRef = useRef(null); const timeoutIdRef = useRef(null); /** * タイムアウト付きでPromiseを実行する * @param promise 実行するPromise * @param timeoutMs タイムアウト時間(ミリ秒) * @param onTimeout タイムアウト時のコールバック * @returns Promise結果 */ const executeWithTimeout = useCallback(async (promise: Promise, timeoutMs: number, onTimeout?: () => void): Promise => { // 前回のAbortControllerがあればクリーンアップ if (abortControllerRef.current) { abortControllerRef.current.abort(); } if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); } // 新しいAbortControllerを作成 const controller = new AbortController(); abortControllerRef.current = controller; const signal = controller.signal; // タイムアウト設定 timeoutIdRef.current = setTimeout(() => { console.error(`[TIMEOUT] Operation timed out after ${timeoutMs}ms`); controller.abort(); if (onTimeout) { onTimeout(); } }, timeoutMs); try { // Promiseとabort signalを監視 const result = await Promise.race([ promise, new Promise((_, reject) => { signal.addEventListener('abort', () => { reject(new Error('処理がタイムアウトしました')); }); }), ]); // 成功時はタイムアウトをクリア if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; } return result; } catch (error) { // エラー時もタイムアウトをクリア if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; } throw error; } }, []); /** * 複数の非同期処理を順次実行(各処理前にabortチェック) * @param tasks 実行するタスクの配列 * @param signal AbortSignal */ const executeSequentialWithAbortCheck = useCallback(async (tasks: Array<() => Promise>, signal?: AbortSignal): Promise => { const results: T[] = []; for (const task of tasks) { // 各タスク実行前にabortチェック if (signal?.aborted) { throw new Error('処理が中断されました'); } const result = await task(); results.push(result); } return results; }, []); /** * 現在の処理を強制的に中断 */ const abort = useCallback(() => { if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } if (timeoutIdRef.current) { clearTimeout(timeoutIdRef.current); timeoutIdRef.current = null; } }, []); /** * クリーンアップ(コンポーネントのアンマウント時など) */ const cleanup = useCallback(() => { abort(); }, [abort]); return { executeWithTimeout, executeSequentialWithAbortCheck, abort, cleanup, signal: abortControllerRef.current?.signal, }; } /** * API呼び出しをラップしてタイムアウトと中断をサポート */ export function wrapWithAbortSignal(apiCall: (...args: T) => Promise, signal: AbortSignal): (...args: T) => Promise { return async (...args: T): Promise => { if (signal.aborted) { throw new Error('処理が中断されました'); } const promise = apiCall(...args); return Promise.race([ promise, new Promise((_, reject) => { signal.addEventListener('abort', () => { reject(new Error('処理が中断されました')); }); }), ]); }; }