Skip to content

Simplify and consolidate Bus API methods #12

@justin808

Description

@justin808

Summary

The Bus struct has 30+ public methods with inconsistent naming and overlapping functionality. This creates a confusing API surface that's hard to learn and use correctly.

Problems

1. Method Naming Inconsistencies

Send variants:

  • send
  • send_ext
  • send_blocking
  • send_one
  • force_send
  • send_boxed
  • send_with_response

Flush variants:

  • flush
  • flush_all
  • flush2
  • flush_and_sync

Issues:

  • Not clear when to use which variant
  • Naming doesn't follow consistent pattern
  • flush2 is particularly unclear

2. Too Many Methods

30+ methods on a single struct makes the API:

  • Hard to discover the right method
  • Difficult to document effectively
  • Confusing for new users
  • Large surface area for bugs

3. Overlapping Functionality

Some methods seem to do very similar things:

  • send vs send_ext - What's the difference?
  • flush vs flush_all - When to use each?
  • send_one vs send - Not immediately obvious

Recommended Improvements

1. Use Builder Pattern for Send Options

Instead of many send variants:

// Before
bus.send(msg).await?;
bus.send_ext(msg, addr).await?;
bus.send_blocking(msg)?;
bus.send_one(msg, id).await?;

// After
bus.send(msg).await?;
bus.send(msg).to_address(addr).await?;
bus.send(msg).blocking()?;
bus.send(msg).to_receiver(id).await?;

2. Consolidate Flush Methods

Replace multiple flush methods with one configurable method:

// Before
bus.flush().await;
bus.flush_all().await;
bus.flush2(policy).await;

// After
bus.flush().await;                    // Default flush
bus.flush().all().await;              // Flush all
bus.flush().with_policy(policy).await; // Custom policy

3. Group Related Operations

Consider splitting into smaller, focused traits:

trait MessageSender {
    fn send<M: Message>(&self, msg: M) -> SendFuture<M>;
}

trait BusControl {
    fn flush(&self) -> FlushFuture;
    fn close(&self) -> CloseFuture;
}

trait RequestResponse {
    fn request<M: Message>(&self, msg: M) -> ResponseFuture<M::Response>;
}

impl MessageSender for Bus { /* ... */ }
impl BusControl for Bus { /* ... */ }
impl RequestResponse for Bus { /* ... */ }

4. Use Enums Instead of Multiple Methods

For addressing modes:

pub enum SendMode {
    Broadcast,
    ToAddress(Address),
    ToReceiver(ReceiverId),
    Random,
    Balanced,
}

// Single method instead of 5 variants
bus.send_with_mode(msg, SendMode::ToReceiver(id)).await?;

5. Improve Documentation

Whichever approach is chosen, document:

  • When to use each method
  • Performance implications
  • Ordering guarantees
  • Error conditions

Example Refactored API

Current (Simplified)

impl Bus {
    pub async fn send<M>(&self, msg: M) -> Result<(), Error>;
    pub async fn send_ext<M>(&self, msg: M, addr: Address) -> Result<(), Error>;
    pub fn send_blocking<M>(&self, msg: M) -> Result<(), Error>;
    pub async fn send_one<M>(&self, msg: M, id: ReceiverId) -> Result<(), Error>;
    pub async fn force_send<M>(&self, msg: M) -> Result<(), Error>;
    // ... 25 more methods
}

Proposed

impl Bus {
    /// Send a message (returns a builder for configuration)
    pub fn send<M: Message>(&self, msg: M) -> SendBuilder<M>;
    
    /// Flush pending messages
    pub fn flush(&self) -> FlushBuilder;
    
    /// Close the bus
    pub async fn close(self);
    
    /// Request-response pattern
    pub fn request<M: Message>(&self, msg: M) -> RequestBuilder<M>;
}

pub struct SendBuilder<M> {
    // Configure how the message is sent
}

impl<M> SendBuilder<M> {
    pub async fn await(self) -> Result<(), Error>; // Default send
    pub fn to_receiver(self, id: ReceiverId) -> Self;
    pub fn to_address(self, addr: Address) -> Self;
    pub fn balanced(self) -> Self;
    pub fn random(self) -> Self;
    pub fn blocking(self) -> Result<(), Error>;
}

Migration Strategy

This is a breaking change, so:

  1. Deprecation period

    • Mark old methods as #[deprecated]
    • Add new methods alongside
    • Provide migration guide
  2. Semver major version bump

    • Communicate changes clearly
    • Update all examples
    • Update documentation
  3. Gradual migration

    • Can keep both APIs during transition
    • Remove deprecated methods in next major version

Benefits

  1. Clearer API: Obvious what each method does
  2. Better discoverability: IDE autocomplete shows fewer options
  3. More flexible: Builder pattern allows adding options without new methods
  4. Better documentation: Less to document, clearer organization
  5. Fewer breaking changes: Adding options doesn't require new methods

Priority

Low-Medium - This is a nice-to-have improvement for API ergonomics but represents a breaking change. Best done:

  • Before 1.0 release
  • During a planned major version bump
  • When making other API changes

Related Issues

Discussion Points

  • Is backwards compatibility important?
  • When is a good time for breaking changes?
  • Which approach is preferred (builder, enums, traits)?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions