A use case for the RxJS expand operator
RxJS has a lot of operators, and most of the time we grab for the same operators. On rare occasions, these frequently used operators aren't enough, for me that was the case while I was writing rx-query.
You can think of rx-query as a wrapper around HTTP requests, it automatically retries and caches queries, and it keeps track of the request state (
error). When a query fails, rx-query has a retry built-in and it emits the retry count to the consumer.
To give you an idea, this is how it looks like when a query failed and is retrying:
The implementation of the above screen looks like this with rx-query:
To implement the retry feature, I intended to use Alex Okrushko's library, backoff-rxjs. But because this library uses the
retryWhen operator, it doesn't emit a value on a retry. The
retryWhen operator only emits a value when it succeeds or when the implementation rethrows the error.
This isn't what I had in mind for rx-query because the retry count might be important.
I started to look at the RxJS operators that I'm unfamiliar with, and the
expand operator looked suitable. After reading Nicholas Jamieson's post "RxJS: Understanding Expand", in which he compares the operator to a delay pedal, I was confident that this was the operator I was looking for.
It's similar to
mergeMap, but applies the projection function to every source value as well as every output value. It's recursive.
To give you an understanding on how you can use it, we're going to implement the retry functionality in small iterations.
The first step is to show the result of the request.
Within an Angular application this is common, and we use the
async pipe to unwrap the value from an Observable (in this case the HTTP request).
But what happens if the service throws an error? In most of the application code, only the happy path is implemented and the unhappy paths are often forgotten (or poorly implemented).
In this case, when the service fails, we end up the value
To handle errors, we use the
To keep things simple, we simply return the error in this example.
While it's important to correctly handle errors for our users, catching this error is also important for the next step. This will make sure that the consumer will receive a notification, instead of an error.
Now that we handle the successful path and the error path, we can start to implement the retry mechanism by using the
We're almost at the point to use the
The only step left is to add the state to the successful result and the error result. This state is used to determine if the request should be retried. Because we don't want the request to be retried until it's successful, we also add a retry count to the result.
We can finally use the
expand operator now.
By rewriting the
characters$ Observable to a method we can invoke it with the retry count. This is needed so the number of retries can be added to the result. Inside the callback of the
expand operator, we recursively invoke the method until the request is successful or until the maximum number of retries has been reached.
While this solution works, it fires the requests quickly after each other when there's a failure.
Probably resulting that all of the retried requests will also have failed.
To give the backend more time to recover, we add a delay between the requests by using the
expand emits every value, we end up with the desired result of having an incremented retry count for the consumers.
Please consider supporting me if have you enjoyed this post and found it useful: