How would you cache ember-data records from a REST API with localStorage?

I am trying to figure out a way to cache records I retrieve using ember-data with the ActiveModelAdapter to localStorage. In the past, I have used the localForageAdapter for ember-data models. I am looking for suggestions for a way of caching ember-data records locally via localStorage. I was thinking about duplicating my ActiveModel adapter models with localForage adapter models and duplicating all records retrieved by ActiveModel models to the equivalent LocalForage models. This approach doesn’t feel very clean. Another approach I was considering was overwriting the ActiveModelSerializer to attempt to store data to localStorage. The last approach I was considering was to scrap out ember-data and just use ember-orbit although that would require a big rewrite and also I ember-orbit doesn’t seem that well documented and battle tested.

Can anyone give advice on how to locally cache data in localStorage?

I would override the adapter and provide your own custom saving logic:

App.ApplicationAdapter = DS.ActiveModelAdapter.extend({
    find: function(store, type, id, record) {
        return this._super(store, type, id, record).then(function(payload) {
            // store payload data in localStorage
        });
    }
});

@mrinterweb If I understand you correctly, you would want to substitute the browser RAM with localStorage as much as possible, right?

This would imply push records to localStorage after retrieving them from the server and only keep in memory those that actually much some find research…

@gordon_kristan I think I’ll give this strategy a go. I like this strategy since it gets out of your main application code and defaults to caching to localStorage. Of course, the tricky part will be trying to figure out how to expire the cache.

@nandosan yes. I am trying to minimize REST HTTP requests. I am targeting mobile devices and modern browsers. I can safely rely on having localStorage available. Frequently, web pages or phonegap apps need to restart on mobile devices when resuming the app. Using the in-memory store:main is not sufficient for my application since store:main is not necessarily going to be around for long. Also, slow networks cause my application to really drag if I don’t use localStorage. If I could replace store:main with localStorage/indexedDB/webSQL (using localForage), that would be amazing! Even if I could not replace store:main, it would be cool to have store:main synchronized with localForage and then deserialize/load localForage to store:main when the application resumes/reloads.

@gordon_kristan something that I have realized is that when I take your approach is that most of the time find, findMany, and the rest of the find* methods on ActiveModelAdapter are not called when I am retrieving records from the store with @store.find. I am using ember-data 1.0-beta-9. I don’t understand why my extended find functions are not always being called. I am not switching adapters on the models that should be calling my find method.

Couple suggestions for cache invalidation:

  • Add a hook in the find method that compares the date of the record to the current date, if the difference is too great replace the record.

  • Create a single API endpoint that will return a boolean value depending on if content has been updated recently (or any other criteria). You can put this in the application initializer and query for new records based on the result.

These would of course depend on what kind of data your application uses. If it’s particularly data intensive you should probably look into server-side events or full on web sockets.

Might be able to help you out a bit more if you can describe your use case.

“There are only two problems in computer science: naming things, cache invalidation, and off by one errors”

@bjones6 I am using websockets to send data updates. I’ll be able to take care of my cash expiration. I know it’s a pain, but I can take care of that part. The problem I’m having is extending ActiveModelAdapter.

Here’s what I’m doing to try to extend ActiveModelSerializer.

https://gist.github.com/mrinterweb/f77469c590413193b326

Well, I’ve been meaning to look into using Ember with web sockets. I’ll get back to you when I know more.

In the meantime, you could implement a hook in the afterModel function of your route that saves a model that uses the local storage adapter. Then in your application initializer you would dig through local storage and instantiate the records with active model adapter (only if you need to use an already retrieved model for something like a PUT request, otherwise you’re golden because you can just grab the data from local storage). Obviously its less elegant and you’re doing it the right way by overloading the adapter itself.

Sorry I can’t be more help then that.

@bjones6, I appreciate the advice, and I think I’ll likely take your suggestion. I would have liked to do it in the adapter, but for some reason, extending find is not working out for me. Thank you.

The store caches records, so it might not always go to the adapter. I don’t believe there’s a way in the public API to ignore the cache, but it can certainly be done. The cache is stored in the store’s typeMaps property. I would override the typeMaps property with a computed property, that way you have complete control over the cache.

Or you might be able to ignore that problem completely. If you’re trying to cache data in the localStorage, it shouldn’t matter that it’s also being cached in memory.

1 Like

@gordon_kristan, I will take a look at overriding the typeMaps property.

I was able to get a caching strategy working. Granted this is far from pretty and not generic enough for much reuse, but I think I should be able to take the concepts I did here and refactor this into something more reusable.

https://gist.github.com/mrinterweb/2642c26d4b0ea0ff819b

Wouldn’t be easier to take LocalStorage adapter as base and add some functionality that allow you to control the object retrieve/refreshing from the server?

@nandosan, I tried the inverse of that technique which was to try to extend ActiveModelAdapter with caching to localStorage. I thought it would be much easier to extend the functionality of ActiveModelAdapter and cache than extending a localStorage adapter. Replicating the functionality of ActiveModelAdapter would not be trivial. I’d envision that being a lot of work. Perhaps you have an easier idea as to how that would work.

I refined my caching solution a bit to make it more reusable with other models. This refactored strategy extends a base App.Cache object. This makes it very easy to define serializers for each model that intends to use this. Clearly this code has not been put through its paces yet. I’m sure I’ll refine it more. Might even put it up on github if I think it might help someone else out. I hope that this can be helpful for someone or if anyone has any feedback on this code, let me know.

https://gist.github.com/mrinterweb/7e23a153a47c25adba1d

typically, which folder does this file go to?