Article summary
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.