Which surface?

Four surfaces, one import. Pick the surface the job is, then stay on it.

// Pick the surface the job is. fx(iterable) // data in hand fx(ids).mapConcurrent(n, fetch) // bounded I/O, order kept fxEvents(stream) // values over time either((r) { … }) / .mapEither / .attempt // failures as values

Lecture

FxDart is not four libraries in a trenchcoat. It is one package because a real program crosses these jobs, and the names stay the same when you do. The mistake is starting from a function list. Start from the job:

The job is…Start withNot
Data already in hand; only part of it is needed fx(iterable) wrapping a one-line map/where/take
I/O over a known collection, at most n in flight, order kept .toAsync().map(f).concurrent(n) or .mapConcurrent(n, f) Future.wait(xs.map(f))
Values arrive when they arrive (keystrokes, ticks, sockets) fxEvents(stream) a pull pipeline with sleep
The caller handles the failure either / mapEither / attempt on the surface you are already on throw for domain errors; null with the reason lost

Pull names win collisions. takeUntil is the FxTS predicate; the events-layer analogue is stopOn. One word, one meaning. Cross at a named seam: .toAsync() lifts data into demand, .pull() lifts events into demand, .toStream() goes the other way.

The 0.8.10 channel rule. attempt after retryOn / retryOnError, never before. Those operators watch the error channel; once a failure is a Left there is nothing left there to retry.

Two jobs that cross surfaces are worked as tutorials of their own: debounced search (time → latest query wins → typed parse) and bounded concurrent fetch (a bound, order kept, every failure kept). The pull on-ramp still starts at fx.

Demo · four jobs, four surfaces

The same import. Each block is the smallest program that belongs on that row of the table:

Related: fx — the pull chain · concurrent — the I/O bound · fxEvents — the push chain · typed errors — failures as values · debounced search · bounded concurrent fetch