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('処理が中断されました'));
        });
      }),
    ]);
  };
}