This is the second post in a series of two that cover capturing, storing, and retrying network requests using Ember and Ember Data. In the first post, I discussed how to intercept HTTP requests and store those requests in local storage. In this post, I will cover how to retrieve them from local storage and retry them.
In the previous post, we set ourselves up pretty well to be able to retry requests. In fact, we have an in-memory representation of those requests. They are stored in the
SyncQueue. As long as the browser isn’t closed or refreshed, we can use that array to begin a retry cycle (we’ll cover what happens if the browser is closed later).
Here is what it looks like to retry a request:
syncQueue: Em.inject.service('synchronization-queue') processRequests: -> entry = @get('syncQueue.firstObject') if entry? # deserialize entry into request opts = @get('syncQueueEntryRepository').jqueryOptsForEntry(entry) # Make the request Ember.$.ajax(opts).then (result) => # Success case Em.run => # Request was successful, so remove that entry @get('syncQueue').removeEntry(entry).then (val) => # Push resulting objects into the Store @handleResponse(result, entry) # Process the next request Em.run.once @, 'processRequest' , if ((not jqXHR.readyState?) or jqXHR.readyState == 0) # Connectivity problem -- keep retrying Em.run => # Update a retry timer @_backoffRetrySyncTime() # Re-run the request after the backoff has ended Ember.run.later @, 'processRequest', @get('retrySyncInSeconds') * 1000
The code above boils down to this:
- Get the first request from local storage.
- Make that request.
- If successful, try the next request.
- If not successful, wait N seconds before retrying again.
Retrying Requests After Closing the Browser
But what happens if the user closes the browser before all of the items in the queue have a chance to be processed? This is where we really see the benefit of storing the requests in local storage.
We added some logic in our
ApplicationRoute to help us recover lost data from a browser close or refresh. In our
afterModel hook, we check the contents of local storage for that user. If there are any items in local storage, we re-populate the queue with those items and can then begin the synchronization process.
Here is what our
afterModel function looked like:
afterModel: (resolvedModel) -> # Load from local storage @get('synchronizer.syncQueue').loadStateFromPersistentStorage(user.get('id')).then (entries) -> if entries.length > 0 # Re-populate queue @get('synchronizer.syncQueue').replaceWithEntries(entries) # start syncing @get('synchronizer').startSync()
Let’s take a second to review exactly what we have accomplished. When a request is made, it is captured and stored in local storage. We then process those entries from local storage. When a request is successful, we remove that entry and proceed to the next one. If a request fails, we stop synchronizing and retry later. When the user closes the browser without finishing a synchronization, we have the ability to pick up where we left off when they open the app again.
All of the code is available on GitHub.