Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Caching remote data in Local Storage with EmberData

I have a question about loading and caching remote objects with Ember. I'm developing an Ember app that uses server-side storage through a REST API. Some of the fetched data is rarely changing, so fetching it from the server each time the application loads is unnecessary. But this is also a question for apps that need to work offline and still save its data to a server.

Ember Data has a built-in storage adapter for persisting models through a REST API, and there is an adapter for Local Storage as well (as pointed out by Ken below). The problem (if it is a problem) is that a model only has one storage adapter, and there doesn't seem to be any concept of caching fetched models other than keeping them in memory.

I found similar requests in this Ember wishlist and in the comments to this talk by Tom Dale, but I haven't found any indication that this would be an existing feature in Ember.

I have two questions (the first one being the important one):

  1. What is the best way – today – to implement cached models in Local Storage and syncing them with remote data as needed?
  2. Is this a feature that is planned be included in Ember, or at least something that the maintainers feel should be added eventually?

When it comes to 1), I can think of a couple of strategies:

a) Extend an existing adapter and add a custom remote sync mechanism:

App.Store.registerAdapter('App.Post', DS.LSAdapter.extend({
  // do stuff when stuff happens
}));

b) Maintain separate model classes – one set for the remote objects, and one set for local objects – and sync between them as needed. With the standard Todo case:

RemoteTodo –*sync*– Todo
                     |
                     UI

I'm kinda hoping this is a real noob question and that there is a good established pattern for this.

Updated: Found this similar question. It has a good answer, but it's kind of theoretical. I think what I would need is some hands-on tips or pointers to example implementations.

like image 277
hannes_l Avatar asked Feb 11 '13 14:02

hannes_l


3 Answers

Just to "bump" this thread up a little, because it was one of the top results when I researched solutions for ember local cache of restful api, etc.:

Dan Gebhardt seems to do a bloody good job with Orbit.js and its integration into Ember: https://github.com/orbitjs/ember-orbit

Orbit is a standalone library for coordinating access to data sources and keeping their contents synchronized.

Orbit provides a foundation for building advanced features in client-side applications such as offline operation, maintenance and synchronization of local caches, undo/redo stacks and ad hoc editing contexts.

Orbit.js features:

  • Support any number of different data sources in an application and provide access to them through common interfaces.

  • Allow for the fulfilment of requests by different sources, including the ability to specify priority and fallback plans.

  • Allow records to simultaneously exist in different states across sources.

  • Coordinate transformations across sources. Handle merges automatically where possible but allow for complete custom control.

  • Allow for blocking and non-blocking transformations.

  • Allow for synchronous and asynchronous requests.

  • Support transactions and undo/redo by tracking inverses of operations.

  • Work with plain JavaScript objects.

And don't miss his great speech and slides about Orbit:
Introduction to Orbit.js

(UPDATE: I added some more descriptive information from the Orbit pages, as my posting got downvoted for "just" referencing external resources and not containing the actual solution in itself. But Orbit seems to me like the solution, and the only way to "include" this here is via links.)

like image 121
Michael Black Ritter Avatar answered Nov 20 '22 22:11

Michael Black Ritter


There is an implementation of a local storage adapter that you might find useful. Have a look at https://github.com/rpflorence/ember-localstorage-adapter

like image 3
ken Avatar answered Nov 20 '22 22:11

ken


Here's a way to do it. A mixin for your adapters, with a method localStoreRecord you can use to cache the record, lastly an initializer to preload the store.

Local Storage is simply a key:value store for stringified objects, so we can store all of our application data under a single key.

Note: this is using es6 modules

// app/mixins/local-storage.js

import Ember from 'ember';

export default Ember.Mixin.create({
  appName: 'myApp',
  // how many records per model to store locally, can be improved.
  // needed to prevent going over localStorage's 5mb limit
  localStorageLimit: 5,
  localStoreRecord: function(record) {
    var data = JSON.parse(localStorage.getItem(this.appName));
    data = data || {};
    data[this.modelName] = data[this.modelName] || [];
    var isNew = data[this.modelName].every(function(rec) {
      rec.id !== record.id; 
    });
    if (isNew) {
      data[this.modelName].push(record);
      if (data[this.modelName].length > this.localStorageLimit) {
        data[this.modelName].shift();
      }
      localStorage.setItem(this.appName, JSON.stringify(data));
    }
  }
});
// app/adapters/skateboard.js

import DS from 'ember-data';
import Ember from 'ember';
import LocalStorageMixin from '../mixins/local-storage';

export default DS.RESTAdapter.extend(LocalStorageMixin, {
  modelName: 'skateboard',
  find: function(store, type, id) {
    var self = this;
    var url = [type,id].join('/');
    return new Ember.RSVP.Promise(function(resolve, reject) {
      Ember.$.ajax({
        url: 'api/' + url,
        type: 'GET'
      }).done(function (response) {
        // cache the response in localStorage
        self.localStoreRecord(response);
        resolve({ type: response });
      }).fail(function(jqHXR, responseStatus) {
        reject(new Error(type +
         ' request failed with status=' + reponseStatus);  
      });
    });
  },
  updateRecord: function(store, type, record) {
    var data = this.serialize(record, { includeId: true });
    var id = record.get('id');
    var url = [type, id].join('/');
    return new Ember.RSVP.Promise(function(resolve, reject) {
      Ember.$.ajax({
        type: 'PUT',
        url: 'api/' + url,
        dataType: 'json',
        data: data
      }).then(function(data) {
        // cache the response in localStorage
        self.localStoreRecord(response);
        resolve({ type: response });
      }).fail(function(jqXHR, responseData) {
        reject(new Error(type +
         ' request failed with status=' + reponseStatus);
      });
    });
  }
});

// app/initializers/local-storage.js

export var initialize = function(container/*, application*/) {
  var appName = 'myApp';
  var store = container.lookup('store:main');
  var data = JSON.parse(localStorage.getItem(appName));
  console.log('localStorage:',data);
  if (!data) {
    return;
  }
  var keys = Object.keys(data);
  if (keys.length) {
    keys.forEach(function(key) {
      console.log(key,data[key][0]);
      store.createRecord(key, data[key][0]);
    });
  }
};

export default {
  name: 'local-storage',
  after: 'store',
  initialize: initialize
};
like image 1
Harrison Powers Avatar answered Nov 20 '22 22:11

Harrison Powers