stream-transfer-limit 0.1.0

Byte-count transfer limits for fallible futures streams
Documentation
# stream-transfer-limit

`stream-transfer-limit` applies cumulative byte limits to fallible
`futures::Stream`s.

It is meant for cases where an HTTP middleware or fixed body limit is not quite
the right abstraction: for example, when the allowed transfer size is chosen
after routing, authentication, configuration, or account lookup.

```rust
use futures::{stream, StreamExt};
use stream_transfer_limit::{TransferLimit, TransferLimitError};

fn main() {
    futures::executor::block_on(async {
        let chunks = stream::iter([
            Ok::<_, std::io::Error>(vec![1, 2]),
            Ok(vec![3, 4, 5]),
        ]);

        let max_bytes = 4;
        let mut limited = TransferLimit::new(max_bytes).wrap(chunks);

        assert_eq!(limited.next().await.unwrap().unwrap(), vec![1, 2]);
        assert!(matches!(
            limited.next().await.unwrap(),
            Err(TransferLimitError::LimitExceeded { limit: 4, actual: 5 })
        ));
        assert!(limited.next().await.is_none());
    });
}
```

## Behavior

- The stream may produce exactly the configured limit.
- The first chunk that makes the cumulative byte count greater than the limit
  returns `TransferLimitError::LimitExceeded`.
- After a limit error, the wrapper terminates and does not keep polling the
  inner stream.
- The default byte counter is `usize`. If the cumulative count cannot fit in the
  selected counter type, the wrapper returns `TransferLimitError::CounterOverflow`
  and terminates.
- Inner stream errors are preserved as `TransferLimitError::Inner`.
- Progress callbacks receive cumulative bytes after every successful chunk read
  from the inner stream, including the chunk that crosses the limit.

## APIs

For new code, construct a `TransferLimit` and wrap the stream:

```rust
use futures::stream;
use stream_transfer_limit::TransferLimit;

fn main() {
    let chunks = stream::iter([Ok::<_, std::io::Error>(vec![0; 512])]);

    let _limited = TransferLimit::new(1024)
        .on_progress(|bytes_seen| {
            eprintln!("read {bytes_seen} bytes");
        })
        .wrap(chunks);
}
```

`usize` avoids unnecessary conversions for ordinary in-memory or platform-sized
limits. For very large streams, choose a wider counter explicitly:

```rust
use futures::stream;
use stream_transfer_limit::TransferLimit;

fn main() {
    let chunks = stream::iter([Ok::<_, std::io::Error>(vec![0])]);
    let huge_limit = 16_u128 * 1024 * 1024 * 1024 * 1024;

    let _limited = TransferLimit::<u128>::from_limit(huge_limit).wrap(chunks);
}
```

The crate implements counters for `usize`, `u64`, and `u128`.

## Development

The repository includes a Nix flake for a reproducible development shell:

```sh
nix develop
```

If you use direnv, allow the checked-in `.envrc` once:

```sh
direnv allow
```

The development shell provides the pinned Rust toolchain, rustfmt, clippy,
rust-analyzer, cargo helper tools, Docker client, and `act`.

Run the local CI pipeline:

```sh
scripts/ci.sh
```

Run the GitHub Actions workflow locally with `act`:

```sh
scripts/act-ci.sh
```

## Why not tower-http?

`tower_http::limit::RequestBodyLimitLayer` and `http_body_util::Limited` are
good choices when the limit is known at the HTTP-body layer. This crate is for
code that already has a `futures::Stream` and needs to choose the limit closer to
business logic, such as per-tenant transfer limits.