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, 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>>>mapLimit(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. 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.