Skip to content

Add Sync bound to StreamResolver trait object in execute() #302

@jwilger

Description

@jwilger

Problem

The future returned by eventcore::execute() is !Send because stream_resolver() returns Option<&dyn StreamResolver<State>> where the StreamResolver trait object lacks a Sync bound. This means the dyn StreamResolver<State> reference held across .await points makes the overall future !Send.

This is a problem in async frameworks that require Send futures. For example, Leptos's #[server] macro (and Axum handlers in general) require the future to be Send. The compiler error looks like:

error[E0277]: `dyn StreamResolver<CreateProjectState>` cannot be shared between threads safely

Current Workaround

Callers must wrap eventcore::execute() in tokio::task::spawn_blocking + Handle::block_on to run the !Send future on a blocking thread:

let handle = tokio::runtime::Handle::current();
let result = tokio::task::spawn_blocking(move || {
    handle.block_on(eventcore::execute(store, command, RetryPolicy::new()))
})
.await
.expect("spawn_blocking task should not panic")?;

This works but adds unnecessary complexity and a thread-pool hop for every command execution.

Proposed Fix

Add a Sync bound to the StreamResolver trait object in the execute() function (or wherever the trait object is constructed). Since StreamResolver implementations are typically stateless or use Arc-wrapped state, requiring Sync should not be a breaking change in practice.

This would make the future from execute() Send, allowing direct use in async contexts without the spawn_blocking workaround.

Context

Discovered while integrating eventcore 0.5.0 into a Leptos + Axum application (stochastic_macro PR #186).

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions