Promiseとリトライ

HTML

<div>
  タスク成功率: <input type="number" id="successRate" max="100" min="0" value="5">%
</div>
<div>
  リトライ回数: <input type="number" id="retryCount" max="100000" min="0" value="10">回
</div>
<div>
  リトライ実装: <select id="retryImpl">
    <option value="retryN">retryN</option>
    <option value="retryExponentialBackoff">retryExponentialBackoff</option>
  </select>
</div>
<div>
 <button id="start">実行</button>
 <button id="cancel" disabled="disabled">リトライキャンセル</button>
</div>
<div>
  <textarea id="console" rows="20" cols="80" placeholder="ここに処理結果が出力されます" readonly></textarea>
</div>

JavaScript

const stopWatch = (() => {
  let startTime = new Date().getTime();
  return {
    getTimeStamp() {
      const current = new Date().getTime();
      const time = current - startTime;
      startTime = current;
      return "+" + Math.floor(time / 1000) + "." + (("" + (time % 1000)).padStart(3, '0'));
    },
    clear() {
      startTime = new Date().getTime()
    }
  }
})();

const clearLog = function() {
  document.getElementById('console').innerHTML = "";
  stopWatch.clear();
};

const printLog = function(...args) {
  document.getElementById('console').innerHTML += `${stopWatch.getTimeStamp()}: ${args.join(' ')}\n`;
};


/**
 * 1秒ぐらいかかってランダムで成功・失敗するタスク。
 */
function doTaskAsync(successRate) {
  return new Promise((resolve, reject) => {
    printLog("[Task]", "processing...")
    setTimeout(() => {
      if (Math.random() < successRate / 100.0) {
        printLog("[Task]", "success")
        resolve("SUCCESS");
      } else {
        printLog("[Task]", "failed")
        reject("FAILED");
      }
    }, 1000);
  })
}


/* ここから */


// キャンセルを通知するためのオブジェクトを作る
// AbortControllerを使ったほうがよさそう
window.AbortController = function(){
  const signal = new EventTarget();
  signal.aborted = false;
  return {
    signal,
    abort(){
      signal.aborted = true;
      signal.dispatchEvent(new Event('abort'));
    }
  }
};

/**
 * 指定されたミリ秒だけPromiseの完了を待つ。
 * AbortSignalが渡されていたら、Signalの状態によってはキャンセル(reject)する。
 */
function sleepAsync(milliseconds, signal) {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, milliseconds);
    if (signal) {
      const onabort = () => {
        clearTimeout(timer);
        reject();
      };
      if(signal.aborted){
        // イベント後に呼ばれると下のコールバックでは拾えないので、そのケースを考慮している
        onabort();
      }else{
        signal.addEventListener('abort',  onabort);
      }
    }
  });
}

/**
 * リトライ実装のベース
 */
function retry(func, retryDelay, determinationRetry, signal) {
  const isFunction = (obj) =>...