Splitting of Promises from DS.Model (or Ember.Model)


#1

Hi,

I’d like to get a public discussion going on the future of promises within the persistence libraries (i.e. ember-data and ember-model).

For the uninitiated: a Promise is an object that represents an eventual value, the retrieval of which can succeed or fail. You can specify success/error handlers on a promise object by using the .then method. Promises can have many handlers chained together in sequence, allowing for a lot of elegant patterns for managing and encapsulating your async data and performing complex async operations. For more information, please see the RSVP GitHub, which is the Promise library used within and endorsed by Ember.

Ember is more and more adopting promises within its APIs, in particular in this upcoming router API facelift and within the persistence libraries ember-data and ember-model. To a certain degree, they’ve already been around for some time, but there are some aspects that are going to change, some of which aren’t totally agreed upon yet, so let’s talk about it.

The improvement that no one seems to disagree on is that instances of Model (whether DS.Model or Ember.Model – from now on I’m not going to distinguish) should not have .then method, which is presently causing a lot of conceptual issues problems due to the fact that a promise is not a value itself but rather a discrete attempt to retrieve a value. If a promise winds up in a reject state, it should remain in that state, and any concept of retrying or reloading belongs in another promise for that specific action/attempt to reload. If you leave .then on the Model instances, and an initial attempt to load the model fails, then any future calls to that .then handler will instantly reject, among a plethora of other bizarre implications of mixing the concept of Promise with Entity.

The solution is to separate the Promise to load/materialize an entity from the Entity itself. Again, no one really disagrees on this; what’s open to debate is what the various .find methods should return from now on.

Model.find can either return a (possibly unloaded) Model object, or it can return a Promise. Whichever one is chosen, we’ll also need a way retrieve the other.

My particular take is that .find() should return a promise that resolves with the loaded model or rejects with the error that prevented the load. To me it seems way more difficult to manage an object that may or may not be loaded, particularly when it comes to the Router, and handling all the different kinds of errors that may occur when you transition to a route but your data fails to load. I’m certainly biased by the work I’ve been doing in the aforementioned router facelift, since the error handling behavior I think is quite slick when you use promises rather than possibly unloaded objects. But in general, if you don’t use promises, you start having to attach/detach/manage event handlers for state changes in the model object, etc., which I feel is often a far less elegant solution when promises are an option.

But this strong opinion is weakly held, and I’d like to get the discussion rolling for a nice API for allowing the dev to choose their poison.

What follows are some examples of what the API could look like, depending on which direction we take it.

If .find returns a promise:

Post.find(123).then(function(model) { alert(model.get('name')); });

// How to get a model object from the promise?
// Post.findRecord(123) ?
// I kind of like:
// Post.find(123).asRecord(); // returns a model instance

.find returns a (possible unloaded) record (today’s approach)

var post = Post.find(123);
post.one('didLoad', function() {
  alert(post.get('name'));
});
// How to get a loading promise?
// post.loadingPromise?
// post.loadingPromise()?
// post.load() ?

#2

I am in favor of having the various find*() methods return promises. The only caveat is that this will require support for promises in the Ember.js internals to avoid breaking things.


#3
  1. I agree that it’s important to distinguish between values (in this case instances of DS.Model and DS.RecordArray) and promises. They are not the same.

  2. If everyone agrees on 1), then off the top of my head it makes most sense that all the .find*() methods returns values, i.e. not promises. The biggest reason is that it’s handy to return a DS.Model.find() result from computed properties etc. Take this example for instance:

App.PostController = Ember.ArrayController.extend({
  category: function() {
    return App.Category.find(this.get('model.categoryId'));
  }.property('model')
});

If find() returned a promise (which is not a record array) this wouldn’t work as expected.

If you do want to use a record as a promise, you can access the record’s promise for example like this:

App.PostController = Ember.ArrayController.extend({
  category: function() {
    var category = App.Category.find(this.get('model.categoryId'));
    category.promise.then(function() {
      alert('Weehooo, I found the category');
    });
    return category;
  }.property('model')
});

It could also be a method on the record such as category.promise(), or a property category.get('promise'). A simple property (category.promise) seems simplest though. Ember Data could handle that internally by creating the promise property on record instantiation and resolve it on didLoad itself.

promise would also be a property on record arrays: App.Post.find({categoryId: 123}).promise.then(function() {...}).


#4

I personally am opposed to putting a .promise property on objects returned from Ember Data; it’s an aesthetic hazard that quickly starts looking ridiculous.

A better solution is to have Ember.js support promises at a deeper level, so that returning a promise for an eventually-realized RecordArray would have the expected behavior.


#5

Do you mean that:

 category: function() {
  return App.Category.find(123);
}.property()

…returns a promise, that Ember internally calls .then on and waits for a resolved value? But how will that work with code that consumes computed properties right away:

var isRecordArray = this.get('category') instanceof DS.RecordArray; //false, since the value has not been resolved yet
var categoryId = this.get('category.id`); //undefined I guess

#6

@machty, thanks for the write-up, it’s an important discussion for sure.

I’ve run into having to perform a ton of checks for whether a record is loaded already (.get('isLoaded')) or is still in the process of being so (.on('didLoad, ..)). This is due to today’s approach which I find rather… Verbose, at least.

Personally, I’m in favor of returning a promise that either resolves or rejects with an error. The ability to type App.Post.find(60).then(function() { .. }); is just too intuitive to pass up.


#7

As an application developer (as opposed to a library/toolkit/framework developer), I find the use of promises much, much preferable to the current system. I would expect the following two accessors to always work the same:

var cat = obtain_category_from_somewhere();

var categoryId = cat.get('id');
var categoryName = cat.get('name');

As it is now, categoryId will usually be correct, while categoryName will sometimes be correct. I have to know how cat was loaded (via find(n) or find({})?). To reliably use both values, you have to wrap the code in some sort of isLoaded check like @KasperTidemann mentioned.

I come from a Dojo background where deferreds have been around for a while. One thing they have is the when() function. It is a method for transparently handling values and/or promises. Using Dojo’s when, the above could be written like this:

when(obtain_category_from_somewhere(), function(cat){
  var categoryId = cat.get('id');
  var categoryName = cat.get('name');
});

dojo/when src: https://github.com/dojo/dojo/blob/master/when.js


#8

Is there some conceptual difference between using .when vs

obtain_whatever().then(function(cat) {
   var category...;
});

?


#9

No.

The difference is that when() will work with values or promises. So,

var cat = { id: 123, name: 'stuff' };

or var cat = Category.find_by_name(‘stuff’);

would both work with:

when(cat, function(cat){ ... });

It is particularly handy for the case where you develop with a synchronous data provider, but eventually move to an async implementation. It’s essentially just pushing the promise-aware code into when() instead of having it everywhere you use it.


#10

Ah ok, in RSVP land you would do

RSVP.resolve(maybeAPromise).then(...);

#11

The Promises/A spec describes three methods that the promise provides:

  1. then(onSuccess, onFailure) – this has been thoroughly discussed above
  2. get(propertyName) – “Requests the given property from the target of this promise. Returns a promise to provide the value of the stated property from this promise’s target.”
  3. call(functionName, arg1, arg2, …)-- Request to call the given method/function on the target of this promise. Returns a promise to provide the return value of the requested function call.

The contract requires get and call to return Promises, but it doesn’t require that the resulting Promise should resolve/reject exactly when the underlying Promise does. (It might imply that, but I don’t see it.) Thus, one possibility would be:

  • Ember Models (generically) have a fetch method that returns a Promise that we’ll assign to modelFetch
  • modelFetch.get returns an already-resolved promise that resolves with the current value (this is likely undefined for most fetch-able properties until the fetch itself resolves)

#12

As much as I like the idea of returning only promises, it seems like there is compelling use cases when someone want a value right away. In this PR (https://github.com/emberjs/data/pull/1076) after some discussion with @stefan.penner and @machty I introduced a fetch method. It have the same semantics as find but return a promise. I agree with @tomdale about the ugliness of keeping promises on the records/arrays. I would be probably in favor of keeping them in a map on the store. Another solution would be to do the inverse - map values by promises. But it seems more tricky from memory leak point of view.


#13

As it stands now (0.13), the find() function responds to then, and it appears to fire only when loaded - as expected. Is ember-data holding a reference to that promise, and due to the nature of promises, that promises’ data? Will that data ever be garbage collected?


Ember RC6 and phonegap slow
#14

I think the PR with the fetch method is the right approach. Then, as far as ember-data, if you remove the thennable from find, I think you get the best of both worlds.

  • Using find(), you can get the entity strait away, and just use the lifecycle events to perform actions when it is completed. This will also allow your routes to transition immediately.

  • Using fetch(), you can take advantage of the promise and conform to the conventions of the new router facelift.

Plus it should also help with backwards compatibility.


Internal view lifecycle hooks for animation purposes