Now that we’ve modified our Redux store to support middleware, we have the foundation needed to integrate speech synthesis into the main flow of our application.

Leveraging the ISpeechService abstraction we previously defined, the implementation falls out relatively easily, though (as we’ll see) there are a few wrinkles that will need to be followed up.

public SpeechMiddleware(
    ISpeechService speechServices, 
    IReduxStore<WordTutorApplication> reduxStore)
{
    _speechServices = speechServices;
    _reduxStore = reduxStore;
}

The constructor for SpeechMiddleware predictably accepts an ISpeechService middleware reference, but why does it also need a reference to our store? This is so that we can asynchronously dispatch a SpeechFinishedMessage when playback completes.

Our implementation of Dispatch() reacts to a SpeakMessage, passing all messages through to the next handler in the chain.

public void Dispatch(
    IReduxMessage message,
    IReduxDispatcher next)
{
    if (next is null)
    {
        throw new ArgumentNullException(nameof(next));
    }

    if (message is SpeakMessage speak)
    {
        Speak(speak.TextToSay);
    }

    next.Dispatch(message);
}

This async void method fits the usual pattern for event dispatch, which makes sense given that we’re reacting to a message representing a particular application event.

private async void Speak(string textToSay)
{
    _reduxStore.Dispatch(new SpeechStartedMessage(textToSay));
    try
    {
        await _speechServices.SayAsync(textToSay)
            .ConfigureAwait(false);
    }
    finally
    {
        _reduxStore.Dispatch(new SpeechFinishedMessage(textToSay));
    }
}

If you run the code at this point, you’ll start getting exceptions fired within the Dispatch() method of the ReduxStore, triggered by this check:

if (_dispatching)
{
    // TOCONSIDER: If this exception becomes a problem, 
    // introduce a queue to serialize message processing instead.
    throw new InvalidOperationException(
        "Calling Dispatch() while processing Dispatch() is not permitted.");
}

What’s happening is that our middleware is trying to dispatch a second message while we’re still processing the first. Since actual reentrancy of our Redux store would result in loss of updates, we need to add support to serialize the processing of these messages (that is, to process them sequentially).

To fix this, we add a queue to buffer messages as they are received, delaying dispatch until we finish processing the current message. The trickiest piece of the implementation is the locking required to ensure multithreaded operation.

public void Dispatch(IReduxMessage message)
{
    bool currentlyDispatching;
    lock (_padlock)
    {
        _messagesToDispatch.Enqueue(message 
            ?? throw new ArgumentNullException(nameof(message)));
        currentlyDispatching = _dispatching;
        _dispatching = true;
    }

We queue the message requiring dispatch onto our internal queue. At this point, we have one of two cases.

If we’re already dispatching messages (_dispatching is true), then we don’t want to process this message ourselves. If it’s false, then we need to process the message we have, as long as any extra message it provokes.

    if (!currentlyDispatching)
    {
        while (_dispatching)
        {
            IReduxMessage messageToDispatch;
            lock (_padlock)
            {
                _dispatching = _messagesToDispatch.TryDequeue(
                    out messageToDispatch!);
            }

            if (_dispatching)
            {
                var iterator = new ReduxMiddlewareIterator(
                    _processingQueue.Value);
                iterator.Dispatch(messageToDispatch);
            }
        }
    }
}

The key to multithreaded safety is based on every manipulation of the queue and change to _dispatching happening within the control of the same lock.

Now we can work on hooking up speech generation with our maintenance screen. As well as being useful, this will be an easy way to prove that it works properly. Given the length of this post, that’s something to address next time.

Prior post in this series:
Redux Middleware Implementation
Next post in this series:
Maintenance & Speech

Comments

blog comments powered by Disqus