2 Comments

Retrying Network Requests in Ember.js: Part 2

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.

Retrying Requests

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 queue of 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:

    1. Get the first request from local storage.
    2. Make that request.
    3. If successful, try the next request.
    4. 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()

Review

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.