Proposed public API
The public shape is organized first by delivery semantics. Capacity and retention choices are constructors inside a family, while endpoint cardinality is exposed only when it changes the caller contract.
asyncband::channel
├── oneshot
│ ├── channel<T>() -> (Sender<T>, Receiver<T>)
│ ├── Sender<T>
│ └── Receiver<T>
├── queue
│ ├── mpsc
│ │ ├── bounded<T>(capacity: usize) -> (BoundedSender<T>, BoundedReceiver<T>)
│ │ ├── unbounded<T>() -> (UnboundedSender<T>, UnboundedReceiver<T>)
│ │ ├── BoundedSender<T> / BoundedReceiver<T>
│ │ └── UnboundedSender<T> / UnboundedReceiver<T>
│ └── mpmc
│ ├── bounded<T>(capacity: usize) -> (BoundedSender<T>, BoundedReceiver<T>)
│ ├── unbounded<T>() -> (UnboundedSender<T>, UnboundedReceiver<T>)
│ ├── BoundedSender<T> / BoundedReceiver<T>
│ └── UnboundedSender<T> / UnboundedReceiver<T>
├── broadcast
│ ├── bounded<T>(capacity: usize)
│ │ -> (BoundedSender<T>, BoundedReceiver<T>)
│ ├── sliding<T>(capacity: usize)
│ │ -> (SlidingSender<T>, SlidingReceiver<T>)
│ ├── unbounded<T>()
│ │ -> (UnboundedSender<T>, UnboundedReceiver<T>)
│ ├── BoundedSender<T> / BoundedReceiver<T>
│ ├── SlidingSender<T> / SlidingReceiver<T>
│ └── UnboundedSender<T> / UnboundedReceiver<T>
└── watch
├── channel<T>(initial: T) -> (Sender<T>, Receiver<T>)
├── Sender<T>
└── Receiver<T>
SPSC is ordinary MPSC usage with one sender. SPMC is ordinary MPMC usage with one sender. Dedicated SPSC or SPMC types require a measured specialization benefit rather than completing a topology matrix for its own sake.
Notification primitives carry no value or ordered history and remain outside this channel design.
Endpoint contracts
| Path |
Endpoint capability |
Core operations |
Delivery contract |
channel::oneshot |
neither endpoint is cloneable |
synchronous send(self, T); one receive future |
transfer at most one value |
channel::queue::mpsc |
sender is cloneable; receiver is not |
waiting send, try_send, recv(&mut self), try_recv |
each accepted value goes to the single receiver |
channel::queue::mpmc |
sender and receiver are cloneable |
waiting send, try_send, recv(&self), try_recv |
cloned receivers compete; each accepted value goes to exactly one |
channel::broadcast |
sender is cloneable; subscriptions are explicit |
policy-specific send plus subscribe, recv(&mut self), and try_recv |
every active subscription has an independent cursor |
channel::watch |
sender and receiver are cloneable |
synchronous state update, subscribe, borrow, and changed |
retain one current version and coalesce intermediate updates |
Bounded queues use capacity-constrained endpoints whose send may wait and whose try_send may report Full. Unbounded queues use distinct endpoint types because send is synchronous and Full is impossible.
Common queue errors are re-exported from the leaf modules: SendError<T>, TrySendError<T>, RecvError, and TryRecvError. Sending fails and returns ownership of the value once no receiver remains. Accepted buffered values drain before receive reports Disconnected.
Example calls
use asyncband::channel::queue::mpsc;
let (tx, mut rx) = mpsc::bounded(128);
tx.send(event).await?;
let event = rx.recv().await?;
use asyncband::channel::queue::mpmc;
let (tx, rx1) = mpmc::bounded(128);
let rx2 = rx1.clone();
// rx1 and rx2 compete; one of them receives each value.
tx.send(job).await?;
Broadcast contracts
Broadcast capacity is retention for an ordered multicast log, not ordinary competing-queue capacity.
| Constructor |
Sender API |
Retention and slow-receiver behavior |
broadcast::bounded(capacity) |
async send; try_send may return Full |
lossless bounded log; the slowest active receiver gates producers |
broadcast::sliding(capacity) |
synchronous send |
retain the latest capacity events; a slow receiver gets an exact Lagged(count) error and resumes at the oldest retained event |
broadcast::unbounded() |
synchronous send |
lossless growth; reclaim a prefix after every active receiver advances or drops |
sliding names the persistent latest-N retention contract; overflow is only the condition that triggers an eviction. backpressure does not need its own public module because it follows from bounded lossless retention.
Only SlidingReceiver has lag in its error space. BoundedReceiver and UnboundedReceiver must not require callers to handle an impossible Lagged variant. New subscriptions start at the committed tail and receive future publications. Multi-producer publication establishes one committed order observed by every subscription.
use asyncband::channel::broadcast;
let (tx, mut primary) = broadcast::bounded(128);
let mut replica = tx.subscribe();
tx.send(event).await?; // waits when either subscription holds the retention window
use asyncband::channel::broadcast;
use asyncband::channel::broadcast::SlidingRecvError;
let (tx, mut rx) = broadcast::sliding(128);
tx.send(event)?;
match rx.recv().await {
Ok(event) => consume(event),
Err(SlidingRecvError::Lagged(skipped)) => recover(skipped),
Err(SlidingRecvError::Disconnected) => return,
}
Capacity contracts
- bounded backpressure requires
capacity > 0; it accepts into available capacity, while a waiting send parks and try_send reports Full; passing zero panics;
- unbounded accepts while receivers exist and remains subject to process memory limits;
- sliding retention is explicit multicast loss with exact per-receiver lag reporting;
- latest-state coalescing belongs to watch rather than queue or broadcast overflow.
Rendezvous channels are intentionally not included: independently cancellable async send and receive operations do not provide an unambiguous handoff contract consistent with the queue API.
Source ownership
The source tree mirrors the public families but deliberately does not freeze speculative backend files:
asyncband/src/channel/
├── mod.rs
├── error.rs
├── oneshot.rs
├── queue/
│ ├── mod.rs
│ ├── mpsc.rs
│ └── mpmc.rs
├── broadcast.rs
├── watch.rs
└── internal/
├── mod.rs
└── disruptor/
└── mod.rs
channel::internal::disruptor is private implementation machinery for channel backends, initially a candidate for bounded multicast sequencing and subscriber gating. It creates no public Disruptor family or claim/publish API. Callers continue to receive the nominal broadcast sender and receiver types above.
Other private files such as queue storage, cursors, waiter registration, or ring slots should be introduced only when the chosen implementation requires them. They are not part of this API design.
Direction
Public contracts are fixed before selecting or specializing storage and synchronization. Implementations should proceed in reviewable steps with topology-, capacity-, cancellation-, and fanout-specific benchmarks.
watch remains part of the design because latest-state coalescing has a clear protocol, but its implementation may be deferred until there is concrete demand.
Supersedes #57 and #95. Related prior work: #146.
Proposed public API
The public shape is organized first by delivery semantics. Capacity and retention choices are constructors inside a family, while endpoint cardinality is exposed only when it changes the caller contract.
SPSC is ordinary MPSC usage with one sender. SPMC is ordinary MPMC usage with one sender. Dedicated SPSC or SPMC types require a measured specialization benefit rather than completing a topology matrix for its own sake.
Notification primitives carry no value or ordered history and remain outside this channel design.
Endpoint contracts
channel::oneshotsend(self, T); one receive futurechannel::queue::mpscsend,try_send,recv(&mut self),try_recvchannel::queue::mpmcsend,try_send,recv(&self),try_recvchannel::broadcastsubscribe,recv(&mut self), andtry_recvchannel::watchsubscribe,borrow, andchangedBounded queues use capacity-constrained endpoints whose
sendmay wait and whosetry_sendmay reportFull. Unbounded queues use distinct endpoint types becausesendis synchronous andFullis impossible.Common queue errors are re-exported from the leaf modules:
SendError<T>,TrySendError<T>,RecvError, andTryRecvError. Sending fails and returns ownership of the value once no receiver remains. Accepted buffered values drain before receive reportsDisconnected.Example calls
Broadcast contracts
Broadcast capacity is retention for an ordered multicast log, not ordinary competing-queue capacity.
broadcast::bounded(capacity)send;try_sendmay returnFullbroadcast::sliding(capacity)sendcapacityevents; a slow receiver gets an exactLagged(count)error and resumes at the oldest retained eventbroadcast::unbounded()sendslidingnames the persistent latest-N retention contract;overflowis only the condition that triggers an eviction.backpressuredoes not need its own public module because it follows from bounded lossless retention.Only
SlidingReceiverhas lag in its error space.BoundedReceiverandUnboundedReceivermust not require callers to handle an impossibleLaggedvariant. New subscriptions start at the committed tail and receive future publications. Multi-producer publication establishes one committed order observed by every subscription.Capacity contracts
capacity > 0; it accepts into available capacity, while a waitingsendparks andtry_sendreportsFull; passing zero panics;Rendezvous channels are intentionally not included: independently cancellable async send and receive operations do not provide an unambiguous handoff contract consistent with the queue API.
Source ownership
The source tree mirrors the public families but deliberately does not freeze speculative backend files:
channel::internal::disruptoris private implementation machinery for channel backends, initially a candidate for bounded multicast sequencing and subscriber gating. It creates no public Disruptor family or claim/publish API. Callers continue to receive the nominal broadcast sender and receiver types above.Other private files such as queue storage, cursors, waiter registration, or ring slots should be introduced only when the chosen implementation requires them. They are not part of this API design.
Direction
Public contracts are fixed before selecting or specializing storage and synchronization. Implementations should proceed in reviewable steps with topology-, capacity-, cancellation-, and fanout-specific benchmarks.
watchremains part of the design because latest-state coalescing has a clear protocol, but its implementation may be deferred until there is concrete demand.Supersedes #57 and #95. Related prior work: #146.