Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2: Can of Promises #3729

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open

v2: Can of Promises #3729

wants to merge 22 commits into from

Conversation

au-phiware
Copy link

I'm a big fan of Backbone and of Promises, this is my take on combining the two.

I understand that this may be a can of worms and I have changed many, many functions and it may not be your cup of tea. For this reason I have also made sure all the tests pass (with changes, of course) and I have tried to be backwards compatible (the Todos app still works). I haven't added any new tests yet but I would like to, as use cases arise and as I start to use this code in my own projects.

I have many use cases in mind and have already started with my own major changes: for instances Router#execute will catch errors and trigger an error event (on the router). My intention is that route actions should return the promise(s) from other methods (like render), this will mean that no error gets swallowed or forgotten about.

I'm making this PR, ignorant of the opinions of others (I've only just realised that there are already discussions about this) but keen to learn those opinions and to get some feedback.

I have made some assumptions:

  • Promise, as per ES6, is a global object. In my view it's very easy to include the standard polyfill.
  • jQuery.Deferred is not Promise and needs to be avoided.
  • Promises are useful outside of database interaction.

That last point is important to me. I am currently working on a big project that uses requirejs and pouchdb/couchdb. We use promises to orchestrate module dependencies as well as data from the server, whereby a Promise is created inside initialize and then is called inside render.

var success = options.success || _.identity;
var error = options.error;
delete options.success;
delete options.error;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't modify the options object

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

err, but the master branch does modify the options object already... I guess you object to the delete lines?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, but I'm not modifying the object that is provided in the arguments.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you care to delete these properties? It shouldn't matter if they stick around, no?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want them called by ajax (i.e. jQuery or whatever it might be), because I want them to be apart of the promise chain.

@megawac
Copy link
Collaborator

megawac commented Jul 26, 2015

I'm sympathetic to this changeset and think a movement like this may be a good way to move backbone forward (along with @jmeas' proposed router changes) in v2. So a hesitant 👍 after this has been carefully reviewed

I know ECMA wouldn't break the internet, but one day `catch` might be a
reserved word.

On the suggestion of @megawac
I see no need to refer to `Backbone.Promise` everywhere since the
reference from the arguments list isn't going to disappear.
The Promise contract with Backbone is only two function, `then` and
`catch`, that both return another `Promise`.

On the suggestion of @megawac, see
jashkenas#3729 (comment)
};
if (isPromise(defaults))
return defaults.then(consume);
return consume(defaults);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about returning a promise from initialize

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went there

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👎 on this. Initialize is always synchronous, therefore this will always be resolved after you've created the instance -- in other words, exactly where you are with the current Backbone. Revert this please.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have real world use cases where initialize is not synchronous. Of course, the constructor will not return a Promise if the user never returns a Promise... but I can tell that you feel strongly about this and I'm happy to agree to disagree.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love to hear those use cases if you feel like sharing.

On Mon, Jul 27, 2015, 9:06 AM Corin Lawson [email protected] wrote:

In backbone.js
#3729 (comment):

  • this.changed = {};
  • this.initialize.apply(this, arguments);
  • var model = this, args = slice.call(arguments);
  • var defaults = _.result(this, 'defaults');
  • var consume = function(defaults) {
  •  attrs = _.defaults({}, attrs, defaults);
    
  •  model.set(attrs, options);
    
  •  model.changed = {};
    
  •  var init = model.initialize.apply(model, args);
    
  •  if (isPromise(init))
    
  •    return init.then(_.constant(model));
    
  •  return model;
    
  • };
  • if (isPromise(defaults))
  •  return defaults.then(consume);
    
  • return consume(defaults);

I have real world use cases where initialize is not synchronous. Of
course, the constructor will not return a Promise if the user never
returns a Promise... but I can tell that you feel strongly about this and
I'm happy to agree to disagree.


Reply to this email directly or view it on GitHub
https://github.com/jashkenas/backbone/pull/3729/files#r35532739.

Adam K (mobile)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'd rather let the user handle this themselves -- either with a custom wrapper or waiting for the promise to resolve

defaults.then(attrs => new Backbone.Model(attrs)
.then(model => {

});

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have reverted the commit that affects the constructors, and I hinted at a use case in the last sentence of my OP. I would like to create an example to illustrate why I started on this course in the first place, but that will take time to prune back some real world code into something bite-sited :)

@au-phiware
Copy link
Author

I get 7 failed tests when I use either this branch or the jashkenas/master branch.

}

}(function(root, Backbone, _, $) {
}(function(root, Backbone, _, $, Promise) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan of requiring a particular Promise implementation. Many people will end up requiring both the library you have chosen here and whatever Promise library they actually use.

In my libs I prefer doing:

if (typeof Promise !== 'undefined') {
  Backbone.Promise = Promise;
}

That way, if someone's using native Promises, and they wish to continue using native promises, they can. Or if they'd rather use RSVP, they can do that, too. And so on.

An extra install instruction – telling people to configure Backbone.Promise – is worth the added benefit of flexibility, I think.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree... I felt dirty trying to pick an implementation from npm, bower, etc.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Let's stick with the global Promise, or throw an error if one isn't defined (see @jridgewell's branch).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the dependency on any one promise implementation but still attempt to load it via require (or fallback to global)... Do you see any problems with that?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... if your project didn't include the promise library this bit of code would fail. It's much better to require the dev to provide a Promise polyfill or set one on Backbone.Promise directly instead.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but still attempt to load it via require (or fallback to global)... Do you see any problems with that?

Yeah, that is still going to be a problem for people. You should remove everything related to promises from both package.json, bower.json, and from the UMD wrapper.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I thought that the UMD wrapper would simply receive undefined... no probs

@megawac
Copy link
Collaborator

megawac commented Jul 26, 2015

Also, there is significant overlap with #3651

@akre54
Copy link
Collaborator

akre54 commented Jul 27, 2015

I think this pull really showcases some of the downsides of Promises (and asynchronicity in your code in general) in that once you use them in a few places, you practically have to make your entire API asynchronous.

I see very little reason for Model#defaults or Router#execute, to give two examples, to be asynchronous. That's not the right place for network or user-facing code to exist. You're contorting Backbone's API — and by extension your code — to fit a small assortment of problems that are better handled elsewhere.

I'm fully in support of wrapping asynchronous calls (sync is the biggest one) in Promises, however.

@au-phiware
Copy link
Author

Ha! Perhaps I should have added: "WARNING: May cause Scoliosis"

But on a serious note, I don't like the isPromise checks but I think there's power in giving the option to have a value, a function or a Promise at those points. IMO, writing with Promises demands a different way of thinking. Remember that a Promise does not represent an async call, instead it is simply a dependency on an async call, which could have been fulfilled at any poiny in the past. So it is not correct to assume that whereever there is a Promise there will be a network call or something else that hasn't happen.

So I agree that defaults is not the right place to be accessing the network, etc. But I can't dictate that it can't be time dependent on something else.

@au-phiware au-phiware closed this Jul 28, 2015
@au-phiware
Copy link
Author

Sorry, that close was a slip of the thumb...

@au-phiware au-phiware reopened this Jul 28, 2015
@akre54
Copy link
Collaborator

akre54 commented Jul 28, 2015

Scoliosis

Ha. @paulmillr made a fork of Backbone a while back called Scoliosis (since renamed Exoskeleton). Maybe we could implement it there first.

Remember that a Promise does not represent an async call, instead it is simply a dependency on an async call, which could have been fulfilled at any poiny in the past.

Right of course. But it starts to get ugly fast when you mix sync and async code using promises. In my experience they tend to take over your entire application when really you should've been writing it reactively in the first place.

I don't like the isPromise checks but I think there's power in giving the option to have a value, a function or a Promise at those points.

There's power but there's also a great deal of sloppiness. You know what Ben Parker (and Teddy Roosevelt) said about power and responsibility...

So I agree that defaults is not the right place to be accessing the network, etc. But I can't dictate that it can't be time dependent on something else.

Can you give an example?

@akre54 akre54 mentioned this pull request Aug 27, 2015
@godlark
Copy link

godlark commented Aug 28, 2015

I think if we want to return a promise it would be good to pass parsed response to then's callback. Read more at #3779

@prantlf
Copy link

prantlf commented Jan 7, 2018

The sync method in Backbone 1.x returns promise using the interface of jQuery.Deferred. Be careful with switching to Promise/A interface in the result of this method. It would be a breaking change. You should schedule it for Backbone 2.x.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants