Poll a flaky API until first success

FxDart wins async

Requirement

An export job's status endpoint is deterministically flaky: the first four polls answer pending, the fifth answers ready. Poll it up to ten times, keep a log of every poll, stop at the first success, and report which attempt won — plus how many polls were actually made. No randomness: the failure count is fixed in the code below, so both versions print the same thing every run.

The FxDart version writes the retry as data: range(1, 11) is the poll schedule, map is the transport, peek records the log, and dropWhile + head is the success policy. Because the chain is lazy and pulled one value at a time, head stops the polling — only five requests are ever made.

Expected output
polling export-2026-07 (up to 10 attempts):
  poll 1: pending
  poll 2: pending
  poll 3: pending
  poll 4: pending
  poll 5: ready
ready on attempt 5; polls actually made: 5

Side by side

Native Dart

FxDart

Why they differ

Honestly: the native for loop with a break is short, and nobody would call it wrong. The difference is where the pieces live. In the loop, the attempt budget, the logging, and the success test are all tangled into control flow — change one and you re-read the whole body. In the pipeline each concern is its own named step, so swapping the policy (first success → third success, add a transform, widen the budget) means editing one line. And the laziness guarantee — no polls after the winner — is structural in FxDart, while in the loop it depends on the break being in the right place.

Benchmark

Apple M1 Max, 32 GB RAM · Dart 3.12.2 (AOT-compiled) · 2026-08-24

Async case: the headline scale is N = 100,000, not 1,000,000. Every element costs an event-loop turn on both sides, so a million real awaits would measure Dart's event loop for minutes — not the pipeline. Delays are zero-length and the example's concurrency limit is kept; what the bars compare is the pipeline machinery.

N = 100

Time Tie

Native Dart 556 µs
FxDart 645 µs

Peak memory Tie

Native Dart 16.4 MB
FxDart 17.1 MB

N = 10,000

Time Native wins

Native Dart 48.2 ms
FxDart 55.5 ms

Peak memory FxDart wins

Native Dart 29.5 MB
FxDart 24.5 MB

N = 100,000

Time Native wins

Native Dart 495.5 ms
FxDart 571.7 ms

Peak memory FxDart wins

Native Dart 56.2 MB
FxDart 50.4 MB

Bars are medians of repeated timed iterations in fresh processes per side (small N is batched for timer resolution). Sides within 5% of each other — or within 0.6 ms, a difference no person can perceive — count as a tie; close relative races are re-measured up to 5 runs. In an app, anything under a few milliseconds is invisible to the user regardless of which bar is shorter. Memory is peak process RSS. The Dart VM and the dataset are identical on both sides, so the difference between the two bars is what the pipeline itself holds onto.