Autosave patterns

Wondering if anyone has a working pattern for autosave or periodic saves with ember-data without blocking the user. Something like Google Docs.

The main issue I’m encountering is when a record is committed, its inflight status means changes from bound controls throw exceptions. So you can’t just trigger a commit every few minutes or on text field blur, cause if the user continues typing, it all blows up.

Anyone have a solution?

2 Likes

You could store the text the user is typing in a different property than the attribute you’re planning to save, for example on the view. This way you can copy it when you save, but there won’t be any conflict as the user is typing, because he’s just modifying the buffer, and not the actual record.

A similar issue with this is when you try to reload records via websocket, and suddenly one of them is being edited by the user, which ends up in error as well :speak_no_evil:

1 Like

I was trying to create a BridgeAdapter to better facilitate such scenarios, where the local storage adapter acts as the local “buffer” (cache), and then synchronizes with the server using REST adapter. Would be awesome when we have a working solution for this I think :wink: Also to allow the server to push model updates to clients (fx via web sockets) and having the clients update their local cache only.

Slightly unrelated and not Ember-specific, but if you are trying to handle merging concurrent updates, you might want to look at something like Operational transformation - Wikipedia for how to resolve these things.

I’m sure I missed some important point here. I feel like this means that someone needs to stop using bindings whenever using autosave then implement the “binding” work without ember. Looks like a lot of work and not really DRY.

I’ve just implemented this for our app with Ember Data.

Basically, I override setUnknownProperty on my ObjectController and schedule a save to occur using a debounce function. Also, I have to coalesce the property changes until the model is resolved, otherwise I get a complaint about the model not being able to handle the event “willSetProperty” while it’s inFlight. When I have some more time, I could write up a more detailed post.

Thanks you very much for your answer !

I will try your approach. I’m very interested in your detailed post. Especially the part about coalescing property changes to avoid inFlight. The debounce function seems easy to implement. I hope you will find the time to post it.

I’m happy to say that I can actually give you the code! :smile:: https://gist.github.com/tim-evans/5783095

The debounce function should be removed as soon as backburner supports proper debouncing (see https://github.com/ebryn/backburner.js/pull/31). As for the logic behind this, I wanted to override set on an ObjectController and prevent properties from getting set while a record was in flight. But Ember uses Ember.set a lot of places in the core code, so I had to settle with setUnknownProperty, which ends up working just fine (but has some caveats associated with it). The reasoning behind coalescing properties is that I would frequently get errors from Ember Data stating that records in flight couldn’t handle the willSetProperty event. The recursion is there to ensure that we set the properties in bulk on the model when it has been finally resolved on the client. And to prevent spammy saving, it debounces calls to save for a configurable amount of time so it saves in a predictable and frequent fashion.

As for using the mixin, all you have to do is include it in your ObjectController (if it’s a vanilla pass-through controller):

App.CandyController = Ember.ObjectController.extend(Ember.Autosave);

Now if any properties are changed by the user, it will be persisted in a timely fashion by the mixin.

If you have data transforms occuring at the ObjectController level, you’ll have to use some indirection to make the mixin work as expected:

App.MotivationLevel = DS.Model.extend({
  percent: DS.attr('string')
});

App.MotivationLevelController = Ember.ObjectController.extend(Ember.Autosave, {
  // DON'T. This property will not be automatically saved.
  percent: function (key, value) {
    if (arguments.length > 1) {
      this.set('content.percent', parseInt(value, 10) / 100);
    }
    return this.get('content.percent') * 100;
  }.property('content.percent')
});

App.PotionController = Ember.ObjectController.extend(Ember.Autosave, {
  // Do this instead.
  // The property change will be proxied correctly to
  // the Autosave mixin.
  displayPercent: function (key, value) {
    if (arguments.length > 1) {
      this.set('percent', parseInt(value, 10) / 100);
    }
    return this.get('percent') * 100;
  }.property('percent')
});

Hope this will be valuable to you guys in your apps!

Edit: just removed the inline preview. I saw it was causing some crazy rendering problems.

2 Likes

Just implemented this. It worked very well and probably saved me a few days. Thanks you very much for the code and explanations !

1 Like

Related blog article : http://blog.gaslight.co/post/53361504301/an-autosave-pattern-for-ember-and-ember-data

3 Likes

Could this be enhanced to include an undo manager? any ideas?