Skip to main content

Module macros

Module macros 

Source
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

MacroBehaviorReturnsUse Case
all!Wait for all to succeedVec<T> or first errorParallel fetch, batch processing
any!First success winsT or combined errorFallback patterns, redundancy
race!First to settle winsT (success or error)Timeouts, competitive operations
all_settled!Wait for all to settleBatchResult<T>Collect all outcomes, partial success

§Available Macros

  • all! - Wait for all futures to succeed, return error on first failure
  • any! - Return first successful result, error only if all fail
  • race! - Return first result to settle (success or failure)
  • all_settled! - Wait for all futures to settle, return all outcomes