yieldless/all
Tuple-aware parallel combinators that share cancellation.
yieldless/all gives you helpers for tuple-returning parallel work: all(tasks) waits for every task or aborts siblings on the first error, allSettled(tasks) runs every task to completion and reports each tuple, race(tasks) resolves with the first settled result and aborts the rest, and mapLimit(items, mapper, options) processes a collection with bounded concurrency.
Exports
type SafeTask<T, E = Error> = (signal: AbortSignal) => PromiseLike<SafeResult<T, E>> | SafeResult<T, E>type MapLimitMapper<Item, Value, E = Error> = (item: Item, index: number, signal: AbortSignal) => PromiseLike<SafeResult<Value, E>> | SafeResult<Value, E>all(tasks, options): Promise<SafeResult<AllValues<Tasks>, ParallelError<Tasks>>>allSettled(tasks, options): Promise<AllSettledResults<Tasks>>— one tuple per task, no fail-fastmapLimit(items, mapper, { concurrency, signal }): Promise<SafeResult<Value[], E>>race(tasks, options): Promise<SafeResult<Value, Error>>
Example
import { all } from "yieldless/all";
import { safeTry } from "yieldless/error";
const result = await all([
(signal) => safeTry(readPrimary(signal)),
(signal) => safeTry(readReplica(signal)),
]);For large batches, use mapLimit() to avoid starting every item at once:
import { mapLimit } from "yieldless/all";
import { safeTry } from "yieldless/error";
const [error, avatars] = await mapLimit(
users,
(user, _index, signal) =>
safeTry(fetchAvatar(user.avatarUrl, { signal })),
{ concurrency: 4 },
);Behavior notes
all([])succeeds with an empty array.mapLimit([], mapper, options)succeeds with an empty array.mapLimit()preserves input order and throws aRangeErrorwhenconcurrencyis less than1or not an integer.race([])throws aRangeError.- If any task or mapped item returns
[error, null], siblings are aborted before the final tuple is returned. allSettled()never fails fast: every task runs to completion and reports its own tuple. The parentsignaloption still cancels all tasks.race()aborts losing tasks immediately, then waits for them to settle before it returns.- Thrown task and mapper failures are normalized into tuple failures internally.
When to prefer runTaskGroup() instead
Use all(), race(), and mapLimit() when the work is already tuple-native. Use runTaskGroup() when you want imperative fan-out and regular promise values.
Good
Use all() for a small, fixed set of independent tuple tasks.
const [error, [profile, permissions]] = await all([
(signal) => loadProfile(userId, signal),
(signal) => loadPermissions(userId, signal),
]);Use mapLimit() when a list could be large or expensive.
const [error, summaries] = await mapLimit(
repositories,
(repo, _index, signal) => readSummary(repo.path, signal),
{ concurrency: 4, signal },
);Use race() when the first success or first failure should settle the operation.
const result = await race([
(signal) => readPrimary(signal),
(signal) => readReplica(signal),
]);Avoid
Do not pass work that ignores the signal and expect cancellation to be immediate.
await all([
async () => safeTry(expensiveCpuLoop()),
async () => safeTry(readRemoteData()),
]);Do not use all() for thousands of items. Use mapLimit() or yieldless/iterable so you can control pressure.