Record-oriented streaming for JavaScript
exstream
High-level record operations with end-to-end backpressure, bounded concurrency, reliable branching, and explicit error handling. Exstream connects async iterables, Node.js streams, and Web Streams to JavaScript transforms and destinations.
const orders = exstream(response.body)
.json({ path: '$.orders[*]' })
.mapAsync(enrichOrder, {
concurrency: 8
})
.filter(isActive)
await orders
.jsonlStringify()
.pipeTo(destination) 01 / Async processing
Set the amount of work and the output order.
mapAsync() runs promise-returning transforms with an explicit concurrency limit.
Results stay in input order by default, or can leave as soon as they complete.
const profiles = users.mapAsync(fetchProfile, {
concurrency: 8,
ordered: false,
retry: 2,
timeout: 5_000,
})- Concurrency
- Finite and explicit
- Order
- Input or completion
- Failure policy
- Retry and timeout per attempt
- Cancellation
- Available through AbortSignal
02 / Backpressure
Demand travels from the destination back to the source.
When a writer slows down, transforms stop asking for more input. Exstream propagates that pressure through asynchronous operators and reliable branches instead of creating an unbounded queue between them.
Backpressure in the graph →capacity propagates upstream
- Pull sources
- Pause naturally between reads
- Async operators
- Stop pulling when slots are full
- Reliable forks
- All destinations set the pace
- Hot sources
- Need an overflow policy
03 / Fork and merge
Build a graph without hiding its delivery rules.
fork() creates a reliable branch: every record must reach every active fork. merge() consumes streams with a limit on active inputs and an explicit order choice.
const database = source.fork()
const audit = source.fork()
await Promise.all([
database.pipeTo(databaseWriter),
audit.pipeTo(auditWriter),
])
const rows = pageStreams.merge(4, false)- fork()
- Reliable, participates in pressure
- observe()
- Best-effort, bounded buffer
- merge()
- Limits active inner streams
- Lifecycle
- Terminal promises own completion
04 / Streaming operations
Join and group records without first collecting the input.
Higher-level operators cover common ETL work. On pre-sorted inputs, sortedGroupBy() retains one adjacent group and sortedJoin() performs a two-stream merge join.
const groups = rowsByCustomer
.sortedGroupBy('customerId')
const joined = exstream([customers, orders])
.sortedJoin(
'id',
'customerId',
'left'
)- Transform
- map, filter, batch, reduce
- Structure
- split, flatten, keyBy
- Ordered data
- sortedGroupBy, sortedJoin
- Formats
- CSV, JSON, JSON Lines
05 / Error handling
Keep bad records separate from a broken pipeline.
Recoverable record errors can be replaced, skipped, or routed to a dead-letter stream. Source, destination, lifecycle, and cancellation failures remain fatal graph events and reject the terminal operation.
Errors and lifecycle →const { output, deadLetters } =
pipeline.routeErrors()
await Promise.all([
output.pipeTo(destination),
deadLetters.pipeTo(rejects),
])- errors()
- Replace a failed record
- skipErrors()
- Drop accepted failures
- routeErrors()
- Split data and dead letters
- failOnError()
- Promote a record error to fatal
When it fits
Use Exstream when the flow has operational constraints.
If the data already fits in memory and the work is sequential, an array or a for await loop is usually simpler. Exstream becomes useful when concurrency,
backpressure, multiple destinations, or error routing need to behave as one pipeline. See the tradeoffs →