Spaces:
Sleeping
Sleeping
| import { useCallback, useRef } from 'react'; | |
| /** | |
| * タイムアウト付きの非同期処理を管理するカスタムフック | |
| * AbortControllerを使用して強制的に処理を中断できる | |
| */ | |
| export function useTimeoutController() { | |
| const abortControllerRef = useRef<AbortController | null>(null); | |
| const timeoutIdRef = useRef<NodeJS.Timeout | null>(null); | |
| /** | |
| * タイムアウト付きでPromiseを実行する | |
| * @param promise 実行するPromise | |
| * @param timeoutMs タイムアウト時間(ミリ秒) | |
| * @param onTimeout タイムアウト時のコールバック | |
| * @returns Promise結果 | |
| */ | |
| const executeWithTimeout = useCallback(async <T>(promise: Promise<T>, timeoutMs: number, onTimeout?: () => void): Promise<T> => { | |
| // 前回の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<never>((_, 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 <T>(tasks: Array<() => Promise<T>>, signal?: AbortSignal): Promise<T[]> => { | |
| 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<T extends any[], R>(apiCall: (...args: T) => Promise<R>, signal: AbortSignal): (...args: T) => Promise<R> { | |
| return async (...args: T): Promise<R> => { | |
| if (signal.aborted) { | |
| throw new Error('処理が中断されました'); | |
| } | |
| const promise = apiCall(...args); | |
| return Promise.race([ | |
| promise, | |
| new Promise<never>((_, reject) => { | |
| signal.addEventListener('abort', () => { | |
| reject(new Error('処理が中断されました')); | |
| }); | |
| }), | |
| ]); | |
| }; | |
| } | |