Spaces:
Sleeping
Sleeping
File size: 4,090 Bytes
68f7925 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 |
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('処理が中断されました'));
});
}),
]);
};
}
|