実装
import type { AbortSignal } from 'abort-controller'
// Node.js など DOMException がない場合に必要に応じて定義
export const DOMException =
('undefined' !== typeof window && window.DOMException) ||
('undefined' !== typeof self && self.DOMException) ||
class DOMException extends Error {
constructor(message: string, name: string) {
super(message)
this.name = name
}
}
// キャンセル機能が不要な場合、同時実行数のみを渡せるように
type Options = number | { concurrency: number; signal?: AbortSignal }
export const promiseAllWithConcurrencyLimit =
async <T>(promises: (() => Promise<T>)[], options: Options) => {
const opts = 'number' === typeof options ? { concurrency: options } : options
const { concurrency, signal } = opts
const results: T[] = []
let idx = 0
await Promise.all(Array.from({ length: concurrency }).map(async () => {
while (true) {
if (signal?.aborted)
return Promise.reject(new DOMException('aborted', 'AbortError'))
const cur = idx++
const task = promises[cur]
if (!task) return
results[cur] = await task()
}
}))
return results
}
記事を書くまでの経緯
以前、並列処理数を指定できる Promise.all の実装という記事を書きました
長時間かかるタスクの実行中になんらかの要因でやっぱり中止したいと思うかもしれません
fetch の仕様を参考にキャンセルできるようにしたのが上記になります
使用例
const controller = new AbortController()
const onCancelButtonClicked = () => {
controller.abort()
}
try {
await promiseAllWithConcurrencyLimit(
userIds.map(id => async () => fetchUserById(id)),
{ concurrency: 3, signal: controller.signal }
)
} catch (err) {
if ('AbortError' !== err?.name) throw err
}