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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions