Skip to content

Async Ranges#449

Draft
PyXiion wants to merge 1 commit intojbaldwin:mainfrom
PyXiion:ranges
Draft

Async Ranges#449
PyXiion wants to merge 1 commit intojbaldwin:mainfrom
PyXiion:ranges

Conversation

@PyXiion
Copy link
Contributor

@PyXiion PyXiion commented Mar 1, 2026

While working with std::ranges, I've got this idea. We already have nice coroutine primitives and networking API in libcoro, but when it comes to stream-style processing, everything still ends up as manual while (co_await ...) loops. It works, but it’s not composable, and it doesn’t scale nicely once you add filtering, logging, etc.

So I experimented with a small async range layer built on top of existing coroutine primitives.

Idea

An async stream is just a type that provides:

auto next() -> coro::task<std::optional<T>>;

Although I think using optional is not the best option, maybe smth like has_next() -> bool will do better. Or not, because it would require more co_awaits

How would it work:

auto result = co_await (
    coro::ranges::to_stream(client)
    | coro::ranges::take_until([](std::byte b) { return b == std::byte{'\n'}; })
    | coro::ranges::to_vector
);

Everything remains fully lazy and very nice to use with coroutines.

My PR is a very early draft, because I just wanted to show this idea.

Idk if this idea makes sense, so I need feedback on the overall direction before taking this further, so I didn't focus on buffering, error handling and cancellation semantics yet

How could it look (when finished)

// Proxying from one client to another
auto redirect_task = coro::ranges::to_stream(client1)
    | coro::ranges::take(1024 * 1024) // Limit to 1 MiB
    | coro::ranges::inspect([&](auto& b) { 
        bytes_transferred++; // Logging transferred bytes
    })
    | coro::ranges::to(coro::ranges::write_to(client2));


auto task = coro::ranges::to_stream(client)
    | coro::ranges::inspect([](auto byte) { std::cout << (int)byte; }) // sideeffect
    | coro::ranges::to(coro::ranges::write_to(file_output));
    
// Ratelimiting
auto throttled_task = co_await (
    coro::ranges::to_stream(client)
    | coro::ranges::chunk(1024)
    | coro::ranges::throttle(100ms)   // one chunk per 100 ms
    | coro::ranges::to(coro::ranges::write_to(file))
);

// Calculating simple XOR checksum on fly
uint8_t checksum = 0;
auto processing_task = coro::ranges::to_stream(client)
    | coro::ranges::inspect([&](std::byte b) {
          checksum ^= static_cast<uint8_t>(b);
      })
    | coro::ranges::to<std::vector>();
    
// Imagine reading a stream of URLs from a socket and fetching them concurrently
auto fetch_results = co_await (
    coro::ranges::to_stream(url_client)
    | coro::ranges::split_by("\n")
    // Launch up to 4 concurrent coroutines to fetch data
    | coro::ranges::flat_map_concurrent(4, [](std::string_view url) -> coro::task<Result> {
        auto response = co_await http::get(url);
        co_return parse(response);
    })
    | coro::ranges::to<std::vector>()
);

// Download a large payload via HTTP and stream it directly to a file
auto download_task = co_await (
    coro::ranges::to_stream(http_client)
    // Skip data until we find the end of the HTTP headers (\r\n\r\n)
    | coro::ranges::drop_until(is_start_of_body) 
    // Chunking by 4 KiB
    | coro::ranges::chunk(4096) 
    | coro::ranges::to(coro::ranges::write_to_file("cat.jpeg"))
);

// Simple teeing
auto stream = coro::ranges::to_stream(client);

auto [logger, processor] = stream | coro::ranges::tee();

auto log_task = logger
    | coro::ranges::inspect([](auto b) { log_byte(b); })
    | coro::ranges::drain();

auto process_task = processor
    | coro::ranges::chunk(1024)
    | coro::ranges::to(coro::ranges::write_to(file));

co_await when_all(log_task, process_task);

// How to handle errors?
auto result = co_await (
    coro::ranges::to_stream(client)
    | coro::ranges::chunk(1024)
    | coro::ranges::on_error([](auto const& err) {
          std::cerr << "stream error: " << err.message() << "\n";
      })
    | coro::ranges::to<std::vector>()
);

// How could cancellation work?
cancellation_source cancel;

auto task = co_await (
    coro::ranges::to_stream(client)
    | coro::ranges::with_cancellation(cancel.token())
    | coro::ranges::chunk(4096)
    | coro::ranges::to(coro::ranges::write_to(file))
);

// somewhere else
cancel.request_cancellation();

@PyXiion PyXiion changed the title Async Ranges API for networking Async Ranges Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant