Skip to content

Instantly share code, notes, and snippets.

@mixonic
Created July 2, 2014 16:06
Show Gist options
  • Save mixonic/cabd38f9935ae4bb37c8 to your computer and use it in GitHub Desktop.
Save mixonic/cabd38f9935ae4bb37c8 to your computer and use it in GitHub Desktop.
Buffered Store Proxy

If you have worked much with Ember-Data, then you know the buffered object proxy pattern:

http://coryforsyth.com/2013/06/27/ember-buffered-proxy-and-method-missing/

This pattern allows us to put changes to a model into a buffer instead of on the model itself. There are several benefits of this:

  • If the model has changes come from a push source (an attribute is updated), the changes will not destroy what the user is currently entering.
  • Saves can be de-coupled from dirty tracking. If you are saving a model as a user types, the model in flight will not accept changes from the user. You will get the dreaded cannot change an object inFlight error. With a buffer, you can commit the buffer to the model and save the model, then allow the user to keep making edits against a new buffer. No conflict.
  • In general, a buffer allows you to treat Ember-Data records more like database rows. You lock them for modification (while persisting them) but only for the shortest time possible. An object is made dirty then saved immediately, it doesn't sit in a dirty state the whole time the user is editing it.
  • In general, it encourages you to be wary of the store as a global state. When a user is editing a model, why is that model in a certain state across the whole application? If you modify a user's email address, you should not see their email on the top right of the screen changing in real-time.

But the object buffer proxy is limited to a simple model with basic properties. It cannot buffer complex changes, such as buffering changes to a relationship, or tracking dirty state across multiple objects.

I propose a buffered store proxy. Something like:

App.User = DS.Model.extend({
  name: DS.attr('string'),
  addresses: DS.hasMany({async: true}),
  phones: DS.hasMany({async: true})
});
App.Address = DS.Model.extend({
  user: DS.belongsTo()
});
App.Phone = DS.Model.extend({
  user: DS.belongsTo()
});

var storeProxy = new BufferedStoreProxy(store, [App.User, App.Address]);
Ember.RSVP.Promise.all([store.find('user', 'me'), store.find('addresses', {user: 'me'})]).then(function(results){
  var user = results[0],
      addresses = results[1];
  var userProxy = storeProxy.add(user);
  storeProxy.add(addresses);

  storeProxy.get('isDirty'); // => false
  userProxy.get('addresses'); // => any addresses already in the proxy
  userProxy.get('phone'); // => Ember-Data phone relation

  storeProxy.createRecord('address', {user: userProxy});
  storeProxy.get('isDirty'); // => true
  
  userProxy.set('name', 'new name');
  storeProxy.get('isDirty'); // => true
  userProxy.get('isDirty'); // => true

  userProxy.get('name'); // => null
  user.get('isDirty'); // => false

  storeProxy.commit();

  userProxy.get('name'); // => 'new name'
  user.get('isDirty'); // => true

  return store.commit(); // => saves the new address and the user.
});

This proxy would use a second store instance, and proxy call against itself to the internal store. For each action (such as createRecord) it would track the operation to be perform and perform that action on its own store. When commit is called, it will perform those actions on the store passed in during instantiation. If rollback is called, it will clear the operations.

The provided classes at instantiation will provide the bounderies of what models to return as proxies. The buffer will only populate its internal store with objects added to it. Anything not passed in explicitly via add will be not found or be considered a new record. Maybe there are no find or fetch commands on this proxy store at all.

@ritesh83
Copy link

Hi mixonic, do you have any working example of this BufferedStoreProxy approach?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment