Skip to content

Conversation

@mxgrey
Copy link
Contributor

@mxgrey mxgrey commented Jan 16, 2026

This PR resolves #17

The API of Promise was rather convoluted. There were numerous states that a promise could be in:

  • Available
  • Pending
  • Cancelled
  • Disposed
  • Taken

Users were often put in the awkward position of handling every one of those states or willfully ignoring all but one or two of them. There were also use cases where some of these states are known to be impossible for the promise to be in, so we were wasting the mental bandwidth of the user by making them sort through these variants.

This PR replaces all uses of promise with either tokio::sync::oneshot::Receiver or a newly introduced Outcome struct. The Outcome struct is really just a wrapper around tokio::sync::oneshot::Receiver<Result<T, Cancellation>> that allows the user to receive a simple Result<T, Cancellation> instead of Result<Result<T, Cancellation>, RecvError> which is what the user would get if we simply handed them a tokio oneshot receiver.

An old code snippet using Promise might look like this:

match promise.await {
    PromiseState::Available(available) => {
        println!("The final response is {available}");
    }
    PromiseState::Cancelled(cancellation) => {
        println!("The request was cancelled: {cancellation}");
    }
    PromiseState::Disposed => {
        // This generally should not happen. It means something wiped out the
        // entities of your request or service.
        println!("Somehow the request was disposed");
    }
    PromiseState::Taken => {
        println!("The final response was taken before you began awaiting the promise");
    }
    PromiseState::Pending => {
        // The promise cannot have this state after being awaited
        unreachable!();
    }
}

Using Outcome the above code looks like this instead:

match outcome.await {
    Ok(response) => {
        println!("The final response is {response}");
    }
    Err(cancellation) => {
        println!("The request was cancelled: {cancellation}");
    }
}

Here's a list of the newly deprecated functions and their replacements:

  • Series::take() -> Recipient is replaced by Series::capture() -> Capture
  • Series::take_response() -> Promise is replaced by Series::outcome() -> Outcome
  • Channel::query(_, _) -> Promise is replaced by Channel::request_outcome(_, _) -> Outcome
  • Channel::command(_) -> Promise is replaced by Channel::commands(_) -> Reply

We'll keep Promise and its related APIs for now for backwards compatibility, but they're marked as deprecated. In version 0.1.0 we'll remove all these deprecated functions and structs.

The issue of Promise being too complex was first brought to my attention by @luca-della-vedova so I'd be interested in whether you feel this new API is a move in the right direction.

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
… condition

Signed-off-by: Michael X. Grey <greyxmike@gmail.com>
@mxgrey
Copy link
Contributor Author

mxgrey commented Jan 17, 2026

I've updated the PR to also introduce a new struct Reply.

Similar to Outcome, Reply is a wrapper around tokio::sync::oneshot::Receiver<T>. What makes Reply different is that its future yields a plain T instead of the Result<T, RecvError> that a oneshot::Receiver would normally yield. This is possible only because we are trusting the implementation of crossflow's async channel and execution system to ensure that the oneshot::Sender never gets dropped and that the command gets executed.

Hypothetically if there is a flaw in crossflow's async channel + execution pipeline then there would be some small risk to using Reply—specifically their .await would never return, permanently blocking their async task. For users who are not comfortable with that risk, there is a Reply::safely() method that provides the raw oneshot::Receiver, allowing those users to explicitly handle the RecvError.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Inbox

Development

Successfully merging this pull request may close these issues.

Consider ways to simplify the variants for Promise's outputs

2 participants