yasshi2525/RushHour

View on GitHub
client/src/common/utils/task.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import {
  useState,
  useCallback,
  useEffect,
  useRef,
  useMemo,
  useContext
} from "react";
import {
  Errors,
  isErrors,
  CancelError,
  isCancelError,
  isOperationError
} from "interfaces/error";
import useSnack from "./snack";
import OperationContext from "./operation";

export type Task<I, O> = (signal: AbortSignal, args: I) => Promise<O>;

/**
 * [aborter, task, args]
 */
type TaskState<I, O> =
  | [undefined, undefined, undefined]
  | [AbortController, Task<I, O>, I];

export interface TaskHandler<I, O> {
  onOK?: (payload: O, args: I) => void;
  onError?: (e: Errors, args: I) => void;
  onCancel?: (e: CancelError, args: I) => void;
}

type Handlers<T> = [(args: T) => void, () => void];

/**
 * 非同期にタスクを実行する。タスクをキックするメソッドと、キャンセルするメソッドを返す
 * ```
 * const [fire, cancel] = useTask(
 *                          (sig, args) => http(sig, args), maintain,
 *                          { onOK:    (d) => console.info(`OK=${d}`),
 *                            onError: (e) => console.error(`NG=${e} on request(${args})`),
 *                            onCancel:(args, c) => console.warn(`CANCELED=${c} on request(${args})`)});
 * fire(args);
 * cancel(); // onCancel が指定されたとき cancel してもエラー扱いしない
 * ```
 */
const useTask = <I, O>(
  task: Task<I, O>,
  handlers?: TaskHandler<I, O>
): Handlers<I> => {
  const [, maintain] = useContext(OperationContext);
  const initState = useMemo<TaskState<I, O>>(
    () => [undefined, undefined, undefined],
    []
  );
  const [exec, setExecutor] = useState<TaskState<I, O>>(initState);
  const prevState = useRef<TaskState<I, O>>(exec);
  const snack = useSnack();

  /**
   * `fire()` でタスクを開始する
   */
  const fire = useCallback(
    (args: I) => {
      if (exec[0]) {
        exec[0].abort();
      }
      setExecutor([new AbortController(), task, args]);
    },
    [task, exec]
  );

  /**
   * `cancel()` でタスクを強制停止する
   */
  const cancel = useCallback(() => {
    if (exec[0]) {
      exec[0].abort();
      setExecutor(initState);
    }
  }, [exec]);

  /**
   * タスクが完了したら `onOK`、エラーなら `onError` をコールし、初期状態に戻す。
   * 実行中の場合、キャンセルする
   */
  useEffect(() => {
    console.info("effect useTask");
    if (prevState.current[0]) {
      console.warn("abort previsos task because new task is fired");
      prevState.current[0].abort();
    }
    (async () => {
      if (exec[0]) {
        try {
          const data = await exec[1](exec[0].signal, exec[2]);
          if (handlers?.onOK) {
            handlers.onOK(data, exec[2]);
          } else {
            // default message
            console.info("task ended");
          }
        } catch (e) {
          if (isErrors(e)) {
            if (isOperationError(e)) {
              maintain(e);
            } else if (isCancelError(e) && handlers?.onCancel) {
              handlers.onCancel(e, exec[2]);
            } else if (handlers?.onError) {
              handlers.onError(e, exec[2]);
            } else {
              // default message
              snack(e);
              console.warn("task ended error");
              console.warn(e);
            }
          } else {
            throw e;
          }
        }
        setExecutor(initState);
      }
    })();
  }, [prevState, exec, snack]);

  useEffect(() => {
    console.info(`update prevState running=${exec[0] !== undefined}`);
    prevState.current = exec;
  }, [exec]);

  return [fire, cancel];
};

export default useTask;