FE_Dev / hooks /use-timeout-controller.ts
GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
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('処理が中断されました'));
});
}),
]);
};
}