Expand description
Ergonomic macros for promise combinators.
This module provides declarative macros (all!, any!, race!, all_settled!)
that enable combining heterogeneous futures without manual type erasure.
§Problem
The method-based API (ctx.all(), etc.) requires all futures in the vector to have
the exact same concrete type. In Rust, each closure has a unique anonymous type,
which means combining different step operations doesn’t compile:
ⓘ
// This doesn't compile - each closure has a unique type!
let futures = vec![
ctx.step(|_| Ok(1), None), // Type A
ctx.step(|_| Ok(2), None), // Type B (different!)
];
let results = ctx.all(futures).await?;§Solution
These macros automatically box each future to erase their concrete types, enabling the common use case of combining different step operations:
ⓘ
use durable_execution_sdk::all;
// Clone contexts for each future
let ctx1 = ctx.clone();
let ctx2 = ctx.clone();
let ctx3 = ctx.clone();
// This works with macros!
let results = all!(ctx,
async move { ctx1.step(|_| Ok(1), None).await },
async move { ctx2.step(|_| Ok(2), None).await },
async move { ctx3.step(|_| Ok(3), None).await },
).await?;
// results: Vec<i32> = [1, 2, 3]§When to Use Macros vs Methods
§Use Macros When:
- Combining different step operations - Each closure has a unique type
- Mixing operation types - Combining step + wait + callback operations
- Writing inline futures - Ad-hoc combinations of different operations
- Small, fixed number of futures - Known at compile time
ⓘ
use durable_execution_sdk::{all, any};
// Different closures = different types - use macros!
let ctx1 = ctx.clone();
let ctx2 = ctx.clone();
let ctx3 = ctx.clone();
let results = all!(ctx,
async move { ctx1.step(|_| fetch_user_data(), None).await },
async move { ctx2.step(|_| fetch_preferences(), None).await },
async move { ctx3.step(|_| fetch_notifications(), None).await },
).await?;
// Fallback pattern with any!
let ctx1 = ctx.clone();
let ctx2 = ctx.clone();
let ctx3 = ctx.clone();
let data = any!(ctx,
async move { ctx1.step(|_| fetch_from_primary(), None).await },
async move { ctx2.step(|_| fetch_from_secondary(), None).await },
async move { ctx3.step(|_| fetch_from_cache(), None).await },
).await?;§Use Methods When:
- Processing homogeneous futures from iterators/loops - Same closure applied to different data
- Working with pre-boxed futures - Already type-erased
- Programmatically generating futures - Dynamic number of futures from a single source
- Using
ctx.map()- Preferred for iterating over collections
ⓘ
// Same closure applied to different data - use methods!
let user_ids = vec![1, 2, 3, 4, 5];
let futures: Vec<_> = user_ids
.into_iter()
.map(|id| {
let ctx = ctx.clone();
async move { ctx.step(move |_| fetch_user(id), None).await }
})
.collect();
let users = ctx.all(futures).await?;
// Or even better, use ctx.map():
let batch = ctx.map(user_ids, |child_ctx, id, _| {
Box::pin(async move { child_ctx.step(|_| fetch_user(id), None).await })
}, None).await?;§Quick Reference
| Macro | Behavior | Returns | Use Case |
|---|---|---|---|
all! | Wait for all to succeed | Vec<T> or first error | Parallel fetch, batch processing |
any! | First success wins | T or combined error | Fallback patterns, redundancy |
race! | First to settle wins | T (success or error) | Timeouts, competitive operations |
all_settled! | Wait for all to settle | BatchResult<T> | Collect all outcomes, partial success |
§Available Macros
all!- Wait for all futures to succeed, return error on first failureany!- Return first successful result, error only if all failrace!- Return first result to settle (success or failure)all_settled!- Wait for all futures to settle, return all outcomes