For the current release (2.0.0) there are two dispatchers, a synchronous one and an asynchronous one. There are two implementations because there is no way to make an adapter to asynchronous middleware from a synchronous one or vice-versa.
This can get confusing and is only done because the middleware needs two execution modes, sync and async. Writing middleware should be rare and reusable, most likely anyone implementing middleware will be implementing both a synchronous one and an asynchronous counterpart.
To simplify this, there should be only one dispatcher implementation that exposes both a synchronous dispatch method and an asynchronous one. A middleware must have both synchronous and asynchronous implementations, this will increase the complexity of a middleware element but as stated, writing middleware should not happen often and is rather a reusable portion of code that will most likely implement both flows.
The dispatcher will handle both sync and async flows where when calling a sync dispatch will go only through the sync middleware implementation and an async dispatch will go only through the async middleware implementation. This will make the usage of a dispatcher a lot easier and have both sync and async methods available in all cases.
Reasoning for the Initial Split
The problem boils down to "blocking" the asynchronous call until it completes when we are using the synchronous method. This is impossible because all dispatches must be carried out on the UI thread (when a dispatch is initiated from the UI thread, this is the expected behaviour) in order execute action handlers (stores) on the UI thread that eventually notify the UI components and update themselves (through binding expressions).
This restriction implies that a synchronous dispatch initiated on the UI thread must complete after the action was handled by all registered stores (or action handlers). If along the way we have an asynchronous operation (e.g.: an async middleware) then control is returned from the async method before it completes and that async middleware may dispatch actions that need to be handled on the UI thread.
Waiting for the async middleware to execute on the UI thread creates a deadlock because all methods (and fragments resulting from async transformation by the compiler) execute on the UI thread and the method blocking the execution by waiting the async middleware will actually wait until a method fragment completes that is placed after itself in the execution queue. The fragment does not execute until the method waiting for it completes.
A synchronous method can be adapted to be asynchronous to some extent. For instance, we can create a TaskCompletionSource to create the task result of the asynchronous adapter and simply call the synchronous method.
public Task MethodAsync()
{
var taskCompletionSource = new TaskCompletionSource<object>();
try
{
Method(); // The synchronous method we are adapting
taskCompletionSource.SetResult(null); // Can be the result from the sync method
}
catch (Exception exception)
{
taskCompletionSource.SetException(exception);
}
return taskCompletionSource.Task;
}
Unfortunately we cannot adapt our synchronous middleware to async one because if we want to continue to the next middleware element in the pipeline we need to call context.Next()
which is synchronous in this case. This is the same issue, context.Next()
should needs to adapt asynchronous middleware from the pipeline in the same meaner a synchronous dispatch needs to adapt asynchronous middleware. It cannot be done since all code needs to execute on the UI thread by default.
To keep the implementation of a middleware element simple, they were split in two, one for sync flows and the other for async flows. This led to two dispatchers, one that is synchronous and one that is asynchronous because neither middleware implementation can be adapted to the other which only made it more complicated from the usage perspective.