I needed my PWA to gracefully handle network errors for users’ submissions so they would be stored in an outbox to send later. This would be kind of like the experience of using Gmail or your choice of messanging app while offline. You can still click send but the message won’t actually deliver until you regain internet access.
To do this we need to break the problem down into a few parts. First, a queue to store the failed requests. Second, a hook so failed requests can be intercepted and placed into the queue. Lastly, a couple UI elements to interact with the queue. The process will look something like the following.
Why Not Use Workbox’s Background Sync Plugin?
I think this belongs in a post of its own.
Building the Queue
I used IndexedDB for the queue. I wrote a few helper functions for adding, removing and fetching items from the queue becuase later we need to import these functions into both the service worker and the frontend.
Later be sure to call initQueue in your entrypoint of your frontend.
Intercepting Failed Requests
In our service worker we create a hook to handle failed requests and place them into the queue. For my usecase I only need to filter for POST requests to a single /items/Movements endpoint so the logic is quite straightforward.
Frontend UI
On the frontend I first wrap the queue’s helper functions in a Pinia store. At the moment it looks like it adds very little value over the helper functions itself but later I will expand upon it by adding calls to the user notification library, user authentication store and the service worker.
Then I built out a view for the user to interact with the queue.
Elewhere you may use the count value to display a badge or other UI element to indicate the queue is not empty.
Message Passing
Lastly, I need the means to pass messages from the frontend to the service worker and vice versa. This will allow the user to retry the queue manually and for the service worker to send back the results after retrying the queue for the purpose of UI notifications.
To begin I store a constant RETRY_MESSAGE_KEY for the key for my event messages.
This key is used in my auth store to tell the service worker I want to resubmit the items in the queue. I also must point out that my particular authentication pattern requires I submit the user’s authentication token in the message payload. This is because the service worker does not have access to the browser’s local storage and hence can not read it for the purpsoe of updating the request headers for the queue items so we manually pass it to the service worker. A different authentication pattern may not require this.
Another alternative would be to store a refresh token with the original payload then including logic in your service worker to fetch its own auth token before attempting to submit an item from the queue. I found that to be a bit too complicated for my needs so I opted for this pattern. But, if you wanted true background sync without requiring the user to be actively online to submit the queue items this would be a good approach.
The service worker will receive the message and replay the queue with the provided authToken. Along the way we count the success rate of the retries and return a message back to the frontend via messageClientsRetryResults. This is like the authStore before sending a message to the service worker but in reverse and I reuse the RETRY_MESSAGE_KEY constant.
Back on the frontend I have an event listener which listens for the same RETRY_MESSAGE_KEY value and then uses the UI notification library to inform the user of the results.
Finally, as mentioned way back in the beginning of this article, the queue store and the event listener on our service worker need to be initalized at the start of the application.
Conclusion
Again, my requirements are quite straightforward and therefore I could take some shortcuts in the design of this outbox. For an outbox for multiple endpoints you may need to store the endpoint with the payload for your queue or some other solution. No matter I hope this can be a good starting point for your PWA’s outbox feature.