TL;DR
- レートリミット対策として、要素を
maxConcurrent件ずつのチャンクに分け、チャンクごとにPromise.allで並列に処理し、チャンクの間にdelayMsだけ待つ関数を書いた。 - より厳密には、アプリケーション全体でリクエスト数を制御できるようにした方がよい。
背景
API のレートリミットに引っかからないように、Promise.all で同時に送るリクエストの数を制限したかった。
検証
TypeScript で次の関数を書いた。
interface ThrottledPromiseAllArgs<T, R> {
items: T[]
fn: (item: T) => Promise<R>
maxConcurrent: number
delayMs: number
}
export const throttledPromiseAll = async <T, R>({
items,
fn,
maxConcurrent,
delayMs,
}: ThrottledPromiseAllArgs<T, R>): Promise<R[]> => {
const chunks = items.reduce((acc, item, index) => {
const chunkIndex = Math.floor(index / maxConcurrent)
if (!acc[chunkIndex]) {
acc[chunkIndex] = []
}
acc[chunkIndex].push(item)
return acc
}, [] as T[][])
return await chunks.reduce(
async (accPromise, chunk, i) => {
const acc = await accPromise
const chunkResults = await Promise.all(chunk.map(fn))
if (chunk !== chunks[chunks.length - 1]) {
await new Promise<void>((resolve) => setTimeout(resolve, delayMs))
}
return [...acc, ...chunkResults]
},
Promise.resolve([] as R[]),
)
}まず items を maxConcurrent 件ずつのチャンクに分ける。次に reduce でチャンクを順に処理し、チャンクの中は Promise.all で並列に実行する。最後のチャンク以外では、処理の後に setTimeout で delayMs ミリ秒待つ。
考察
この関数が制限するのは、1 回の呼び出しの中のリクエスト数だけである。別の場所から同じ API を呼ぶとレートリミットを超えうるので、より厳密にはアプリケーション全体で制御できるようにした方がよい。