Retrying Network Requests in Ember.js: Part 1

My team is currently working on an application to help students learn how to conduct experiments. The app is an Ember.js frontend supported by a Rails API backend. Since the application is used in schools where Internet connectivity is spotty at best, we needed a way to support dropping off the network periodically. I think we came up with a pretty cool solution that I’ll explain at a high level in a series of posts. This post will cover how we stored requests in local storage.

1. Intercept Requests

The first step in storing an HTTP request involved tracking that request. Whenever the application needed to make a request, we intercepted the request in our application adapter to delegate to the service we built. Our application adapter looked something like this:

ApplicationAdpater = DS.ActiveModelAdapter.extend
  synchronizer: Em.inject.service()
  ajax: (url, type, options) ->
    hash = @ajaxOptions(url, type, options)
    # set up hash.success & hash.error callbacks normally
    @get('synchronizer').handleRequest(hash)

2. Store Requests in Local Storage

In order to retry any failed requests, we needed to store those requests somewhere they could be retrieved and retried. The logical place was local storage. Whenever a request came into our synchronizer service, we pushed that entry into local storage.

Synchronizer = Em.Object.extend
  handleRequest: (opts) ->
    # Create an object that describes the request
    @get('syncQueueEntryRepository').entryForJqueryOpts(opts).then (entry) =>
      # Push that object into local storage
      @get('syncQueue').pushEntry(entry).finally =>
        # Tell the application that the request was successful
        opts.success({}, "success", entry)

You’ll notice that we are lying to the application and telling it that the request succeeded when it could actually fail. We will cover handling that in a later post.

The syncQueue interfaced with our wrapper of localForage to store data in local storage.

SyncQueue = Em.Object.extend
  init: ->
    @set 'queue', []
  pushEntry: (entry) ->
    @get('queue').pushObject(entry)
    @get('storageService').pushEntry(entry)

The queue property of the SyncQueue will become more interesting in later posts. For now, we will focus on the storageService. It was responsible for tracking the order of requests as well as storing those requests in local storage. Before we look at the code, here is an example of the data structure stored in local storage.

{
  id: "69b4df96-18f4-4612-9986-060056481cea"
  order: 10
  queueEntry: {
    id: "9c0d80ff-7027-468d-a5d7-287cb6ec6376"
    type: "POST"
    url: "http://my-api.com/api/v1/comment"
    data: {
      authorId: "1"
      text: "Here is the comment"
    }
    headers: {}
    contentType: {}
  }
  syncModel: {
    entryDescription: "Create comment"
  }

The queueEntry contained the data necessary to retry the request. The syncModel stored information that could be used to communicate  a summary of the change to the user (for display purposes later).

StorageService = Em.Object.extend
  init: ->
    @set 'maxOrder', 0
    @set 'localForage', localforage
    @populateMaxOrder()

  # The request order is determined by what already exists in local storage
  populateMaxOrder: ->
    new Em.RSVP.Promise (resolve, reject) =>
      @get('localForage').iterate (value, key, index) =>
        if @get('maxOrder') > value.order 
          @set 'maxOrder', value.order 
          # Return is only necessary on iterate in CoffeeScript 
          return
    modelJson = @get('syncModelRepository').jsonForModel entry.get('model')
    serialized = {
      id: @idForEntry(entry) # Generate UUID to identify entry
      order: @get('maxOrder') + 1
      queueEntry: @get('syncQueueEntryRepository').jsonForEntry(entry) # Serialize entry for storage
      model: modelJson
    }
    new Em.RSVP.Promise(resolve, reject) =>
      @get('localforage').setItem(@idForEntry(entry), serialized).then (value) =>
        resolve(value)
      , (err) =>
        reject(err)

As we stand, the application can now intercept each request and store that request in local storage. In my next post, I’ll cover retrieving those requests and retrying them.