Bounded concurrent fetch

A bound, order kept, every failure kept. This is the job Future.wait does not have a primitive for.

fx(ids).mapConcurrent(n, fetch) // bound + order fx(ids).mapRetry(attempts, fetch, delay: backoff) // per-element retry await fx(ids).toAsync().mapOrAccumulate((r, id) async { r.ensure(ok, () => '…'); return await fetch(id); }, concurrency: n); // every failure kept

Lecture

Fetching a known list of ids is not an event stream. The data is in hand; the work is I/O; the policy is "at most n in flight, results in the original order." That is mapConcurrent (or .toAsync().map(f).concurrent(n), the same chain written in three steps). Future.wait(ids.map(fetch)) fires everything at once. Batching into groups of n waits for the slowest of each group. Doing it right by hand is a worker pool — a shared cursor, pre-sized slots, worker futures. FxDart's word for that pool is concurrent(n).

Flaky calls retry per element with mapRetry, not by wrapping the whole terminal. Validation that should report every problem — not just the first — is mapOrAccumulate with concurrency: n. Each element runs in its own raise scope, so a failure in one cannot leak into a sibling, and the failures come out in input order.

The Dart comparison of the worker-pool job verdicts fxdart on clarity; the native pool is shorter than it looks once you have written it twice. This page is that job plus the typed-error half, which the comparison examples do not show.

Demo 1 · two in flight, order kept

Six fetches, never more than two overlapping. The fake call counts in-flight requests so the bound is visible in the printout.

Demo 2 · every failure kept, still bounded

Even ids fail. mapOrAccumulate still runs three at a time, still returns in order, and the Left holds every even id — fail-slow, not fail-fast.

Related: which surface — why this is pull-async · concurrent · mapConcurrent · retry / mapRetry · mapOrAccumulate · debounced search — the time job · Dart vs FxDart: two at a time