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

Trigger error from failing Collection.create with wait:true #4265

Merged
merged 8 commits into from
Jul 23, 2023

Conversation

jgonggrijp
Copy link
Collaborator

This fixes #4262.

The bug arose because the transaction with the backend is mediated through model.save and the 'error' event is initially triggered by the model. Normally, the collection will forward all events from the model, but with wait: true, the model is not yet added to the collection when the event triggers, so the collection is not listening, either. I addressed this by special-casing the 'error' event in this particular scenario.

I determined that a similar fix is not required for the 'sync' event on successful create. In that case, the model is still added to the collection before the event triggers.

I foresee two possible criticisms on the solution:

  1. One could argue that a similar fix is still needed for the 'request' event. However, since the timing of this event is fully predictable (i.e., just before the end of the call to create), users do not need to listen for it in order to match the timing of a callback. Adding special case code for the 'request' event as well would further inflate the code size.
  2. It is theoretically possible that a user would call collection.create with a model that is already in the collection. In that case, with wait: true, I expect that the collection will forward the 'error' event twice. If this scenario is considered problematic enough, I am willing to address it.

On the other hand, someone might also argue that this issue does not need fixing at all. The old behavior, while surprising, could be argued to be consistent, since the collection cannot be expected to forward events from a model it does not yet contain.

Reviews and discussion welcome!

CC @paulfalgout @ogonkov @alanhamlett @recursivk

@jgonggrijp jgonggrijp self-assigned this Jul 19, 2023
@alanhamlett
Copy link

alanhamlett commented Jul 19, 2023

  1. It is theoretically possible that a user would call collection.create with a model that is already in the collection. In that case, with wait: true, I expect that the collection will forward the 'error' event twice. If this scenario is considered problematic enough, I am willing to address it.

I think it's better to get an error message rendered twice rather than not at all. Calling collection.add then collection.create is already using the api in an unexpected/unintended way so I'm guessing it's unlikely to be an issue.

Copy link
Collaborator

@paulfalgout paulfalgout left a comment

Choose a reason for hiding this comment

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

Makes sense to me

@jgonggrijp
Copy link
Collaborator Author

Thanks, both!

I realized something after submitting the PR. Even in the old situation, create would return the new model so you could listen on that for the error event. It can even be a oneliner if you don't need a lasting handle to the model:

someCollection.create({...props}, {wait: true}).on('error', ...);

In other words, listening for the event is already possible, just not on the collection.

With that in mind, do you still consider the change worthwhile?

@alanhamlett
Copy link

Also what about view.listenTo(collection, 'error', callback)? Does that already work or also not firing?

@alanhamlett
Copy link

With that in mind, do you still consider the change worthwhile?

I think it's worthwhile to make sure the error event triggers as expected, because error cases are often not as tested as success cases so someone might not notice the error even never fires in their code.

@jgonggrijp
Copy link
Collaborator Author

Also what about view.listenTo(collection, 'error', callback)? Does that already work or also not firing?

Not firing. That depends on the collection forwarding the event again.

@Rayraz
Copy link

Rayraz commented Jul 19, 2023

2. It is theoretically possible that a user would call collection.create with a model that is already in the collection. In that case, with wait: true, I expect that the collection will forward the 'error' event twice. If this scenario is considered problematic enough, I am willing to address it.

Imho, sending the error event twice should be considered a bug. If it's not that much of a hassle, I think I would opt to go the extra mile to make sure it only fires once.

If it turns out it will actually be a significat hassle to make sure it only fires once, perhaps that's a sign the status quo is actually more correct than the fix.

Rephrasing the question slightly, if I would have to choose which is less wrong:

  1. A collection not forwarding an event for a model that technically isn't part of the collection yet anyway
  2. A collection that broadcasts the same error twice

I'd say the first option 1 seems significantly less wrong to me.

Copy link
Collaborator Author

@jgonggrijp jgonggrijp left a comment

Choose a reason for hiding this comment

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

@Rayraz thanks for chiming in. It's not much of a hassle to prevent double event forwarding (less than one line of code), but it is somewhat costly performancewise. I will highlight the considerations with code comments below:

backbone.js Show resolved Hide resolved
backbone.js Outdated Show resolved Hide resolved
backbone.js Outdated Show resolved Hide resolved
test/collection.js Show resolved Hide resolved
@Rayraz
Copy link

Rayraz commented Jul 19, 2023

I can see why you would be hesitant to introduce the this.has(model) check, especially if it becomes significantly slower the more models are in the collection... 🤔 Does the _.noop version negate the this.has(model) check?

If de-duplicating the event is going to be expensive no matter what, perhaps that's a sign that the status quo is a sensible default behavior. I think the argument could be made that having a collection forward events for a model it doesn't actually own is a nice-to-have rather than a must.

If it's an expensive nice-to-have, would it be so terrible to hide this type of event-forwarding behind a feature flag? The 'unexpectedness' of the default behavior could simply be documented.

For people who are already being mindful about performance due to working with large datasets, this is probably just fine:

someCollection.create({...props}, {wait: true}).on('error', callback);

The view.listenTo(collection, 'error', callback) equivalent equally doesn't strike me as completely horrible for someone who's already being very particular for the sake of performance:

view.listenToOnce(someCollection.create({...props}, {wait: true}), 'error', callback);

@jgonggrijp
Copy link
Collaborator Author

I can see why you would be hesitant to introduce the this.has(model) check, especially if it becomes significantly slower the more models are in the collection... 🤔

This is not the case. has is implemented using get, which in turn uses the internal _byId hash table. There is no loop, although it might make a constant number of nested function calls:

backbone/backbone.js

Lines 996 to 1003 in 3b37e6a

// Get a model from the set by id, cid, model object with id or cid
// properties, or an attributes object that is transformed through modelId.
get: function(obj) {
if (obj == null) return void 0;
return this._byId[obj] ||
this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj, obj.idAttribute)] ||
obj.cid && this._byId[obj.cid];
},

Does the _.noop version negate the this.has(model) check?

No. It only prevents worse bugs. It is cheap, though.

If de-duplicating the event is going to be expensive no matter what, perhaps that's a sign that the status quo is a sensible default behavior. I think the argument could be made that having a collection forward events for a model it doesn't actually own is a nice-to-have rather than a must.

Agreed.

If it's an expensive nice-to-have, would it be so terrible to hide this type of event-forwarding behind a feature flag? The 'unexpectedness' of the default behavior could simply be documented.

I'm open to either documenting the unexpected behavior or silently fixing it (like this PR attempts to do) (although it should be mentioned in the change log). In my opinion, adding features would be too much honor for a corner case like this.

@Rayraz
Copy link

Rayraz commented Jul 19, 2023

The get code doesn't look that bad to me and people probably shouldn't be adding models in bulk this way anyways if they really care about speed? If you'd really like to silently fix this, I would vote to include the this.has(model) fix to avoid double events as well.

If you really don't want to add the this.has(model) check, I'd say double events mean you'd just be replacing one unexpected behavior with another arguably more obscure one. In that case documenting the current behavior would be the better alternative in my opinion.

@jgonggrijp
Copy link
Collaborator Author

I don't have a strong opinion on the this.has(model) check. In order to have one, I would need solid performance measurements, but getting those is a hassle. That would probably not be worth the time.

@alanhamlett have your thoughts on the matter evolved?

@paulfalgout
Copy link
Collaborator

TBH the wait kind of makes it less likely that we can know at the point of adding the error if we should attach the listener.. ie: we might not know what the id is to check against until the server returns it.

backbone.js Outdated Show resolved Hide resolved
@jgonggrijp jgonggrijp merged commit 9ef1cba into jashkenas:master Jul 23, 2023
1 check passed
@jgonggrijp jgonggrijp deleted the wait-error-fix branch July 23, 2023 10:55
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.

error event not firing for Collection.create with wait true
4 participants