Decision summary
The public API is organized by delivery semantics and keeps common paths at the crate root. There is no public asyncband::channel or asyncband::queue namespace.
The source tree may still collect every channel implementation under one private channel family module and re-export selected public modules from lib.rs. This keeps implementation visibility local without adding a public path segment.
The taxonomy has three independent axes:
- the delivery family: oneshot, competing queue, broadcast, or watch;
- endpoint topology when it changes endpoint capabilities or enables a static specialization;
- capacity or retention selected by a constructor.
The taxonomy does not require every node to be implemented in 0.7. Only implemented modules are exported. The reserved sibling modules can be added later without changing existing paths or endpoint contracts.
Public taxonomy
asyncband
├── oneshot
│ └── channel<T>() -> (Sender<T>, Receiver<T>)
├── watch
│ └── channel<T>(initial: T) -> (Sender<T>, Receiver<T>)
├── mpsc # competing queue; implemented family
│ ├── bounded<T>(capacity: usize)
│ └── unbounded<T>()
├── spsc # planned competing-queue family
│ ├── bounded<T>(capacity: usize)
│ └── unbounded<T>()
├── spmc # planned competing-queue family
│ ├── bounded<T>(capacity: usize)
│ └── unbounded<T>()
├── mpmc # planned competing-queue family
│ ├── bounded<T>(capacity: usize)
│ └── unbounded<T>()
└── broadcast
├── spmc # single producer, multiple subscribers; planned
│ ├── bounded<T>(capacity: usize)
│ └── unbounded<T>()
└── mpmc # multiple producers, multiple subscribers
├── bounded<T>(capacity: usize)
└── unbounded<T>()
Every bounded constructor returns BoundedSender<T> and BoundedReceiver<T> from its leaf module. Every unbounded constructor returns UnboundedSender<T> and UnboundedReceiver<T>.
The common queue path remains short:
use asyncband::mpsc;
let (tx, mut rx) = mpsc::bounded(128);
tx.send(event).await?;
let event = rx.recv().await?;
Broadcast makes its producer topology explicit because it determines whether the sender has a static single-writer guarantee:
use asyncband::broadcast::mpmc;
let (tx, mut primary) = mpmc::bounded(128);
let mut replica = tx.subscribe();
tx.send(event).await?;
Delivery families
The distinction between a competing queue and a broadcast is ownership of receive progress, not the number of receiver handles that happen to exist at one moment.
| Family |
Receive progress |
Delivery contract |
oneshot |
one transfer state |
transfer at most one value |
spsc / mpsc |
one receiver owns one cursor |
every accepted value is delivered once |
spmc / mpmc |
receivers compete through one shared cursor |
every accepted value is delivered to exactly one receiver |
broadcast::* |
every subscription owns an independent cursor |
every active subscription observes every retained publication |
watch |
every receiver tracks an observed version |
retain one current value and coalesce intermediate updates |
At the crate root, spmc means a competing queue. Single-producer multicast is spelled broadcast::spmc. The shared topology acronym does not erase the delivery distinction.
Broadcast only exposes multiple-consumer topologies. broadcast::spsc and broadcast::mpsc are omitted because, with one receiver and no possibility of another independent subscription, multicast is not observable. SPSC remains a planned root queue family because its non-cloneable endpoints may enable a measured single-writer and single-reader specialization.
Notification primitives carry no value or ordered history and remain outside this channel taxonomy.
Queue endpoint contracts
| Path |
Sender capability |
Receiver capability |
Delivery |
spsc |
non-cloneable; sending requires &mut self |
non-cloneable; receiving requires &mut self |
one producer to one receiver |
mpsc |
cloneable; sending uses &self |
non-cloneable; receiving requires &mut self |
producers share one receiver |
spmc |
non-cloneable; sending requires &mut self |
cloneable; receiving uses &self |
receivers compete for each value |
mpmc |
cloneable; sending uses &self |
cloneable; receiving uses &self |
producers publish to competing receivers |
Only MPSC is an immediate implementation commitment. SPSC, SPMC, and MPMC reserve additive public directions. A future MPMC implementation may use Flume as one reference, while retaining Asyncband's own cancellation, runtime-independence, error, and feature contracts. SPMC is not planned for the initial implementation.
Bounded queue endpoints use capacity-constrained storage. send may wait, and try_send may return Full.
Unbounded queues use distinct endpoint types because send is synchronous and Full is impossible. They remain subject to process memory limits.
Sending fails and returns ownership of the unsent value once no receiver remains. Accepted buffered values drain before receive reports Disconnected.
Common queue errors are re-exported from each implemented leaf module: SendError<T>, TrySendError<T>, RecvError, and TryRecvError.
Broadcast topology contracts
The outer broadcast module fixes multicast delivery. Its child module fixes producer cardinality; subscribers are always multiple and advance independently.
| Path |
Sender capability |
Subscription capability |
Publication order |
broadcast::spmc |
one non-cloneable sender; publication requires &mut self |
subscribe creates independent receivers |
single-producer order observed by every subscription |
broadcast::mpmc |
cloneable concurrent senders; publication uses &self |
subscribe creates independent receivers |
one committed multi-producer order observed by every subscription |
Broadcast receivers are non-cloneable. Additional subscriptions are created explicitly so that their start at the committed tail is a visible operation. recv requires &mut self because each receiver exclusively advances its own cursor.
SPMC is a distinct public topology rather than MPMC usage with one live sender. The non-cloneable sender provides the static single-writer guarantee needed by a single-producer sequencer. MPMC remains the general broadcast topology.
Capacity and retention
Queue capacity and broadcast retention use similar constructor names but have different contracts.
Queue capacity
| Constructor |
Sender API |
Contract |
bounded(capacity) |
async send; try_send may return Full |
retain at most capacity pending values and apply backpressure when full |
unbounded() |
synchronous send |
accept while receivers exist and grow subject to process memory limits |
Broadcast retention
| Constructor |
Sender API |
Retention and slow-receiver behavior |
bounded(capacity) |
async send; try_send may return Full |
lossless bounded log; the slowest active subscription gates producers |
unbounded() |
synchronous send |
lossless growth; reclaim a prefix after every active subscription advances or drops |
capacity is a strict logical limit, not an implementation hint. The implementation may round physical ring allocation up, but a bounded sender becomes full at the requested limit rather than the physical ring size.
All bounded constructors require capacity > 0; passing zero panics.
Both current broadcast retention modes are lossless, so their receive errors do not include Lagged.
New broadcast subscriptions start at the committed tail and receive future publications. Multi-producer publication exposes only one contiguous committed order; a later reservation must not become visible before an earlier reservation is complete.
Lossy retention deferred
A sliding or drop-oldest mode is semantically possible, but it is not part of the initial API. Its contract must decide whether the requested capacity is strict or may be rounded to an effective physical ring size, when and how lag is reported, whether an exact skipped count is guaranteed, where a lagging subscription resumes, and when an evicted payload is released. These choices do not follow from multicast delivery alone.
Lossy retention also has different meanings for broadcast and competing queues. A broadcast has one cursor per subscription, while a queue has one shared unread set; evicting the oldest queue value is therefore a producer-side overflow policy rather than per-subscription lag.
The initial queue and broadcast APIs expose only bounded backpressure and unbounded growth. A future concrete workload may justify a lossy constructor or explicit send operation with nominal endpoint and error types. That API can be added without changing the bounded or unbounded contracts.
Rendezvous channels are intentionally not included: independently cancellable async send and receive operations do not provide an unambiguous handoff contract consistent with this queue API.
Source ownership and visibility
The public path and physical source ownership are deliberately different. Every channel may live under one private channel family module, following the pattern of collecting a related implementation family privately and re-exporting selected public modules at the crate root.
asyncband/src/
├── lib.rs
└── channel/ # private family module
├── mod.rs
├── error.rs
├── oneshot/
├── watch/
├── mpsc/
│ ├── bounded.rs
│ └── unbounded.rs
├── spsc/ # introduced only when implemented
├── spmc/ # introduced only when implemented
├── mpmc/ # introduced only when implemented
├── broadcast/
│ ├── mod.rs
│ ├── spmc.rs # introduced only when implemented
│ ├── mpmc.rs
│ └── internal/ # subscription and retention machinery
└── internal/
├── mod.rs
└── ring/ # shared slots and producer sequencing
At the crate root, implemented public families are flattened by re-export:
mod channel;
pub use self::channel::broadcast;
pub use self::channel::mpsc;
pub use self::channel::oneshot;
pub use self::channel::watch;
Future queue topologies are added to the same re-export list when they are implemented. The exact declarations remain feature-gated. An additive channel umbrella Cargo feature may enable all leaf channel features without creating a public module of that name.
Shared private channel machinery uses pub(super) or narrower visibility. Public endpoint types remain nominal structs with private backend fields; ring modes, sequencer modes, cursor modes, and storage generic parameters do not appear in the public API.
Disruptor and backend direction
There is no public Disruptor channel family. Disruptor-style sequencing is private implementation machinery.
Bounded broadcast is its most direct consumer: it combines a fixed ring with subscriber gating. SPMC may use a single-producer sequencer; MPMC requires multi-producer claim and contiguous publication tracking.
Bounded MPSC may reuse the lower-level ring and multi-producer sequencer with one consumer cursor. An async send must wait for logical capacity before claiming a sequence, then write and publish without another suspension point so cancellation cannot leave a permanent publication hole.
Unbounded MPSC and unbounded broadcast are not fixed-ring Disruptor structures. They may use segmented queues or growable logs while sharing only the relevant waiting, disconnection, and publication helpers.
The source layout does not freeze speculative backend files. Shared ring mechanics belong under private channel::internal; broadcast-only subscriber gating and retention belong under private channel::broadcast::internal. Implementations should be split further only when the chosen algorithm requires it.
Direction
Public delivery, topology, capacity, retention, error, and cancellation contracts are fixed independently of storage and synchronization. Implementations should proceed in reviewable steps with topology-, capacity-, cancellation-, contention-, and fanout-specific benchmarks.
The initial broadcast implementation focuses on bounded and unbounded retention. Lossy sliding or overflow retention is deferred until a concrete workload establishes its public contract; it can be added later without changing the existing endpoint types.
The initial implementation need not complete the topology matrix. Reserving the taxonomy now lets SPSC, competing SPMC or MPMC, and SPMC broadcast arrive as additive modules after 0.7 rather than forcing another public-path migration.
watch remains part of the taxonomy 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.
Decision summary
The public API is organized by delivery semantics and keeps common paths at the crate root. There is no public
asyncband::channelorasyncband::queuenamespace.The source tree may still collect every channel implementation under one private
channelfamily module and re-export selected public modules fromlib.rs. This keeps implementation visibility local without adding a public path segment.The taxonomy has three independent axes:
The taxonomy does not require every node to be implemented in 0.7. Only implemented modules are exported. The reserved sibling modules can be added later without changing existing paths or endpoint contracts.
Public taxonomy
Every bounded constructor returns
BoundedSender<T>andBoundedReceiver<T>from its leaf module. Every unbounded constructor returnsUnboundedSender<T>andUnboundedReceiver<T>.The common queue path remains short:
Broadcast makes its producer topology explicit because it determines whether the sender has a static single-writer guarantee:
Delivery families
The distinction between a competing queue and a broadcast is ownership of receive progress, not the number of receiver handles that happen to exist at one moment.
oneshotspsc/mpscspmc/mpmcbroadcast::*watchAt the crate root,
spmcmeans a competing queue. Single-producer multicast is spelledbroadcast::spmc. The shared topology acronym does not erase the delivery distinction.Broadcast only exposes multiple-consumer topologies.
broadcast::spscandbroadcast::mpscare omitted because, with one receiver and no possibility of another independent subscription, multicast is not observable. SPSC remains a planned root queue family because its non-cloneable endpoints may enable a measured single-writer and single-reader specialization.Notification primitives carry no value or ordered history and remain outside this channel taxonomy.
Queue endpoint contracts
spsc&mut self&mut selfmpsc&self&mut selfspmc&mut self&selfmpmc&self&selfOnly MPSC is an immediate implementation commitment. SPSC, SPMC, and MPMC reserve additive public directions. A future MPMC implementation may use Flume as one reference, while retaining Asyncband's own cancellation, runtime-independence, error, and feature contracts. SPMC is not planned for the initial implementation.
Bounded queue endpoints use capacity-constrained storage.
sendmay wait, andtry_sendmay returnFull.Unbounded queues use distinct endpoint types because
sendis synchronous andFullis impossible. They remain subject to process memory limits.Sending fails and returns ownership of the unsent value once no receiver remains. Accepted buffered values drain before receive reports
Disconnected.Common queue errors are re-exported from each implemented leaf module:
SendError<T>,TrySendError<T>,RecvError, andTryRecvError.Broadcast topology contracts
The outer
broadcastmodule fixes multicast delivery. Its child module fixes producer cardinality; subscribers are always multiple and advance independently.broadcast::spmc&mut selfsubscribecreates independent receiversbroadcast::mpmc&selfsubscribecreates independent receiversBroadcast receivers are non-cloneable. Additional subscriptions are created explicitly so that their start at the committed tail is a visible operation.
recvrequires&mut selfbecause each receiver exclusively advances its own cursor.SPMC is a distinct public topology rather than MPMC usage with one live sender. The non-cloneable sender provides the static single-writer guarantee needed by a single-producer sequencer. MPMC remains the general broadcast topology.
Capacity and retention
Queue capacity and broadcast retention use similar constructor names but have different contracts.
Queue capacity
bounded(capacity)send;try_sendmay returnFullcapacitypending values and apply backpressure when fullunbounded()sendBroadcast retention
bounded(capacity)send;try_sendmay returnFullunbounded()sendcapacityis a strict logical limit, not an implementation hint. The implementation may round physical ring allocation up, but a bounded sender becomes full at the requested limit rather than the physical ring size.All bounded constructors require
capacity > 0; passing zero panics.Both current broadcast retention modes are lossless, so their receive errors do not include
Lagged.New broadcast subscriptions start at the committed tail and receive future publications. Multi-producer publication exposes only one contiguous committed order; a later reservation must not become visible before an earlier reservation is complete.
Lossy retention deferred
A sliding or drop-oldest mode is semantically possible, but it is not part of the initial API. Its contract must decide whether the requested capacity is strict or may be rounded to an effective physical ring size, when and how lag is reported, whether an exact skipped count is guaranteed, where a lagging subscription resumes, and when an evicted payload is released. These choices do not follow from multicast delivery alone.
Lossy retention also has different meanings for broadcast and competing queues. A broadcast has one cursor per subscription, while a queue has one shared unread set; evicting the oldest queue value is therefore a producer-side overflow policy rather than per-subscription lag.
The initial queue and broadcast APIs expose only bounded backpressure and unbounded growth. A future concrete workload may justify a lossy constructor or explicit send operation with nominal endpoint and error types. That API can be added without changing the bounded or unbounded contracts.
Rendezvous channels are intentionally not included: independently cancellable async send and receive operations do not provide an unambiguous handoff contract consistent with this queue API.
Source ownership and visibility
The public path and physical source ownership are deliberately different. Every channel may live under one private
channelfamily module, following the pattern of collecting a related implementation family privately and re-exporting selected public modules at the crate root.At the crate root, implemented public families are flattened by re-export:
Future queue topologies are added to the same re-export list when they are implemented. The exact declarations remain feature-gated. An additive
channelumbrella Cargo feature may enable all leaf channel features without creating a public module of that name.Shared private channel machinery uses
pub(super)or narrower visibility. Public endpoint types remain nominal structs with private backend fields; ring modes, sequencer modes, cursor modes, and storage generic parameters do not appear in the public API.Disruptor and backend direction
There is no public Disruptor channel family. Disruptor-style sequencing is private implementation machinery.
Bounded broadcast is its most direct consumer: it combines a fixed ring with subscriber gating. SPMC may use a single-producer sequencer; MPMC requires multi-producer claim and contiguous publication tracking.
Bounded MPSC may reuse the lower-level ring and multi-producer sequencer with one consumer cursor. An async send must wait for logical capacity before claiming a sequence, then write and publish without another suspension point so cancellation cannot leave a permanent publication hole.
Unbounded MPSC and unbounded broadcast are not fixed-ring Disruptor structures. They may use segmented queues or growable logs while sharing only the relevant waiting, disconnection, and publication helpers.
The source layout does not freeze speculative backend files. Shared ring mechanics belong under private
channel::internal; broadcast-only subscriber gating and retention belong under privatechannel::broadcast::internal. Implementations should be split further only when the chosen algorithm requires it.Direction
Public delivery, topology, capacity, retention, error, and cancellation contracts are fixed independently of storage and synchronization. Implementations should proceed in reviewable steps with topology-, capacity-, cancellation-, contention-, and fanout-specific benchmarks.
The initial broadcast implementation focuses on bounded and unbounded retention. Lossy sliding or overflow retention is deferred until a concrete workload establishes its public contract; it can be added later without changing the existing endpoint types.
The initial implementation need not complete the topology matrix. Reserving the taxonomy now lets SPSC, competing SPMC or MPMC, and SPMC broadcast arrive as additive modules after 0.7 rather than forcing another public-path migration.
watchremains part of the taxonomy 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.