Based on the interfaces we defined last time, let’s integrate middleware functionality into our existing Redux store. This will lay the foundation we need for asynchronous speech generation.

There’s a whole heap of code changes in this implementation, so instead of a blow by blow description of every piece, I’ll give you an overview of the highlights by following the flow of data - as usual, all of the code is available in the linked pull request.

Middleware is added to the Redux store by calling the imaginatively named AddMiddleware() method, where it’s added to the internal list _middleware.

public void AddMiddleware(IReduxMiddleware middleware)
{
    _middleware.Add(middleware);
    _processingQueue.Clear();
}

We store middleware as a list to allow components to be easily added and removed as required (though, admittedly, we have yet to add RemoveMiddleware()).

For the purposes of iteration, we want to have them wrapped up in a queue. Instead of recreating the queue every time we have a message to dispatch, we wrap queue creation into a Cached<T> - a helper class much like Lazy<T> but for the fact that it can be cleared (reset) when needed.

private readonly Cached<ImmutableQueue<IReduxMiddleware>> _processingQueue;

We use an immutable queue so that we can pass it around without worrying about side effects. For example, if one of our middleware components decides to spontaneously unsubscribe in the middle of processing, we don’t want that to cause a crash.

In the constructor for ReduxStore, we initialise _processingQueue. Our Cached<T> requires a factory method to be passed:

_processingQueue 
    = new Cached<ImmutableQueue<IReduxMiddleware>>(
        CreateProcessingQueue);

Creating the processing queue is fairly straightforward, but contains a couple of surprises:

private ImmutableQueue<IReduxMiddleware> CreateProcessingQueue()
{
    var queue 
        = ImmutableQueue<IReduxMiddleware>.Empty;
    if (_subscriptions.Count > 0)
    {
        queue = queue.Enqueue(
            _subscriptions);
    }

    queue = _middleware.Aggregate(
        queue, (q, m) => q.Enqueue(m));
    queue = queue.Enqueue(_reducer);

    return queue;
}

The members _subscriptions and _reducer are themselves middleware components.

This is a good indication that introducing middleware as an extension point is the right way to go. Not only do we gain a way to extend our ReduxStore with new functionality, but we’re able to convert existing functionality into the new structure.

To see what’s been changed, check out the private classes SubscriptionMiddleware and ReducingMiddleware.

Iteration of the queue of middleware components is delegated to the class ReduxMiddlewareIterator.

The key method here is Dispatch(). It uses indirect recursion, calling in turn into each middleware component, passing itself as the next call in the chain. Each recursive call consumes one item from the queue of components, with the recursion unrolling once the queue is exhausted.

public void Dispatch(IReduxMessage message)
{
    if (_middleware.IsEmpty)
    {
        return;
    }

    _middleware = _middleware.Dequeue(out var stage);
    stage.Dispatch(message, this);
}

Following the usual test-driven approach, a full suite of unit tests can be found in ReduxMiddlewareTests. Leveraging the naming convention unit_scenario_expectation, we can summarize those tests as follows:

Without middleware:

  • Dispatch of a single message
    • Gives expected state

With middleware:

  • Dispatch of a single message
    • Gives expected state
    • Passes message to middleware

With middleware that blocks message processing:

  • Dispatch of a single message
    • State is not updated

That’s our whirlwind tour complete; next time we can look to implement support for asynchronous speech generation.

Prior post in this series:
Redux Middleware
Next post in this series:
Speech Middleware

Comments

blog comments powered by Disqus