API · Async
mapAsync()
Run promise-returning work with bounded concurrency, optional completion-order output, retries, local failure recovery, timeouts, and cancellation.
Example
const profiles = exstream(userIds).mapAsync(fetchProfile, {
concurrency: 8,
ordered: false,
retry: {
retries: 3,
delay: (attempt) => 250 * 2 ** (attempt - 1),
when: (error) => error.status === 429,
},
timeout: 10_000,
}) Parameters
fn-
Called for each successful input when a concurrency slot is available. It may return immediately or return a promise-like value. The resolved value is emitted. The same input and context are reused across retry attempts.
concurrency-
Maximum inputs owned by the operator: callbacks still running plus completed results waiting for downstream demand. Positive integers and
Infinityare accepted; zero, negative values, fractions, and non-numeric values are rejected. A retry, its delay, and an ordered result waiting behind an earlier input retain their slots. Use a finite value at external I/O boundaries. ordered-
With
true, results are emitted in input order even when later tasks finish first. Withfalse, each result is emitted as soon as its task completes. Only literal booleans are accepted. retry-
A number is the count of additional attempts after the first failure. An object configures
retries,delay, andwhen. Zero andnulldisable retries. Fatal failures are never retried. retry.retries-
Number of additional attempts. For example,
retries: 2allows at most three callback calls for one input. retry.delay-
A fixed finite delay or a function returning one, synchronously or asynchronously. The function receives the one-based failed attempt, contextual error, input, and context. Invalid returned delays become record failures.
retry.when-
Return or resolve to
falseto stop retrying that failure. The attempt number is one-based. A thrown or rejected policy function becomes the final record failure. onFail-
Handles one failed attempt inside the current record's concurrency slot. Call
retry()to runfnagain with the current input,retry(nextInput)to run it with a replacement input,push(null, output)to emit a successful fallback, orpush(error, input)to propagate a record error. The failed attempt number is one-based. The handler may be asynchronous and cannot be combined withretry. timeout-
Maximum milliseconds for each individual attempt, including
0. A timeout is eligible for retry and producesMapAsyncTimeoutErrorwith codeEXSTREAM_MAP_ASYNC_TIMEOUT, plustimeoutandattemptfields. signal-
Aborts this operator. A pre-aborted signal prevents work from starting. Pass
context.signalinto cancellable I/O so cancellation and per-attempt timeouts can stop the underlying operation rather than only ignore its late result.
Passing null or undefined for options applies every default. Other non-object values and arrays are rejected when the operator is created.
The JavaScript runtime normalizes numeric policy fields with Number(), so any coercible value is accepted when it produces the required integer or finite value. The TypeScript API deliberately exposes these fields as numbers; use actual numbers for portable, explicit configuration. ordered is strictly boolean.
Order and pressure
concurrency bounds the complete operator window, not only unresolved promises. The window contains callbacks still running and completed results waiting to be accepted downstream. As soon as downstream accepts one result, that slot is released and exactly one new input may start. With a slow writer and fast callbacks, active work can therefore fall below concurrency while ready results occupy the rest of the window; the operator does not drain the whole window before refilling it.
Upstream demand pauses while the window is full. In ordered mode, completed results may wait in memory behind a slower earlier input; unordered mode avoids that head-of-line delay and emits completion order. Both modes use the same sliding-window refill rule.
The callback context exposes context.input, context.signal, and custom upstream fields. It is created lazily when fn declares its second parameter, retry.when declares its third, a dynamic retry.delay declares its fourth, or onFail declares its sixth. Declare those positional parameters rather than retrieving them through rest arguments when a materialized context is required. The context remains the same object across attempts and continues with the emitted result. During a timed attempt, only its signal is temporarily replaced with an attempt-specific signal and restored afterward.
Retries and local recovery
Retry policy applies per input and per failed attempt. The callback receives the same value and record context each time. Delay and policy evaluation happen inside the same concurrency slot, so a large retry delay reduces available throughput.
Timeouts are also per attempt. Exstream aborts the attempt context signal when the deadline expires. JavaScript promises themselves are not cancellable, so the callback must forward that signal to fetch, database clients, or other cancellable APIs.
onFail is the programmable alternative to automatic retry. It is useful when recovery must await more I/O, when the next attempt needs enriched input, or when a failure should produce a fallback output:
const processed = exstream(orders).mapAsync(processOrder, {
concurrency: 8,
onFail: async (error, input, push, attempt, retry, context) => {
if (error.code === 'MISSING_CUSTOMER' && attempt < 3) {
const customer = await recoverCustomer(input.customerId, context.signal)
retry({ ...input, customer })
return
}
push(error, input)
},
}) The handler must settle at most once. Returning without calling retry or push propagates the original failure. Throwing or rejecting replaces it with the handler failure. retry repeats the complete fn callback, so any side effects inside that callback must be safe to repeat.
Errors
A thrown callback error, rejected promise, exhausted retry policy, propagated onFail decision, invalid dynamic delay, or timeout becomes a contextual record error. If handled downstream, later tasks continue. If it reaches a terminal, that terminal rejects. Fatal graph failures and external cancellation abort the operator immediately and bypass retry and onFail policy.
Cancelling the branch stops new scheduling and ignores late completions from work that could not be cancelled.
Forms
mapAsync() is available on streams and reusable pipelines:
stream.mapAsync(fn, options)
exstream.pipeline().mapAsync(fn, options) Signature
mapAsync<U>(
fn: (value: T, context: C) => U | PromiseLike<U>,
options?: MapAsyncOptions<T, C, Awaited<U>> | null,
): Exstream<Awaited<U>, C>
interface MapAsyncOptions<T, C extends object, Output = unknown> {
concurrency?: number
ordered?: boolean
retry?: number | MapAsyncRetry<T, C> | null
onFail?: (
error: ExstreamError<T>,
input: T,
push: MapAsyncFailurePush<T, Output>,
attempt: number,
retry: MapAsyncRetryAttempt<T>,
context: C,
) => void | PromiseLike<void>
timeout?: number | null
signal?: AbortSignal
}
interface MapAsyncFailurePush<Input, Output> {
(error: null | undefined, value: Output): void
(error: unknown, input?: Input): void
}
interface MapAsyncRetryAttempt<Input> {
(): void
(input: Input): void
}
interface MapAsyncRetry<T, C extends object> {
retries?: number
delay?: number | ((attempt: number, error: ExstreamError<T>, value: T, context: C) => number | PromiseLike<number>)
when?: (error: ExstreamError<T>, value: T, context: C, attempt: number) => boolean | PromiseLike<boolean>
}