Skip to content

The willSetProperty gotcha in Ember-Data: Understanding the state machine

by cory on June 10th, 2013
Ember Data, the ambitious comprehensive persistence library solution for Ember.js, has been growing in popularity, and even though it still has a prominent disclaimer that it is not production-ready, it has recently been included in the models section of the official ember.js guides. This seems like a good time to start familiarizing ourselves with it.

Summary

I'm going to describe a particular gotcha that I experienced while using ember-data, and then explore through the ember-data code and documentation to try to piece together what happened and how to avoid it. Short version: If you attempt to change a DS.Model's attribute while it is inFlight, ember will give an error. There's an interactive visual app on jsbin that you can use to explore this yourself. If people find this useful I will write a follow-up that shows an example usage of Kris Selden's Buffered Proxy as a way to mitigate this issue. Follow me on twitter to get notified when part two is up.

emberjs

DS.State Events, Flags and Transitions


The error that I kept encountering was this one, that would seemingly randomly show up in my javascript console:
Attempted to handle event `willSetProperty` on
<App.Post> while in state
rootState.loaded.updated.inFlight.
Called with {reference: [object Object],
store: <App.Store:ember418>, name: title}

There are two important parts of this error message: willSetProperty, and the current state: rootState.loaded.updated.inFlight. What the error is trying to tell us is that the model's was in the rootState.loaded.updated.inFlight state when something caused ember-data to try to set a property ('title') on the model. We get the error because the model's state machine tried to send the event 'willSetProperty' but the state machine's current state (rootState.loaded.updated.inFlight) does not define that event.

So let's back up bit more and ask: What does it mean for the record to be in the `rootState.loaded.updated.inFlight` state? How did it get that way? To answer this we need to look a bit at the state machine that ember-data uses internally with its models. That code is in the 'system/model/states.js' package of ember-data, and in the comment it gives a good explanation of how the state manager works. Quoth the docs:
A record's state manager explicitly tracks what state a record is in at any given time.
The class is `DS.State`, which is a subclass of Ember's `Ember.State`. At any rate, the important thing you need to know for our purposes about a `DS.State` is that it has three different components that make it up: Events, Flags, and Transitions.

DS.State Events, Flags and Transitions


Flags are boolean values that are meant to describe the model's current state. `isLoaded`, `isSaving`, etc are flags. The ember.js model lifecycle guide lists all the flags.

Events are, again quoting the ember-data comments: "named functions that are invoked when sent to a record. The state manager will first look for a method with the given name on the current state." States can be nested and so when attempting to respond to an event they will look up their class hierarchy at the parent, grandparent, etc. until they find an event with that name. If they don't find one, they raise an error (we'll come back to that!). It's also worth nothing that events are invoked on the record (using the method `send`), which then delegates to its internal state manager. So you do something like `myPost.send('myEvent')`, which calls `myEvent` on the post's state manager.

Ember-Data states can be `transition`-ed between using `transitionTo`. Transitions work similarly to events but they are intended to be invoked when handling an event, never directly. So a typical usage pattern might be to call `myPost.send('gotFoo')`, which would delegate to the state manager, which in turn would do some internal bookkeeping and then, perhaps, transition to a new state called 'hasFoo' using the call `transitionTo('hasFoo')`.

So with that in mind we can break down our error message. Something tried to invoke the event `willSetProperty` on our instance of `App.Post`. The App.Post was in the state `rootState.loaded.updated.inFlight`, and that `inFlight` state does not have an event named `willSetProperty`. The code for the `inFlight` `DS.State` is on github. If you look at the code you can see that the events on the `inFlight` state are: ` materializingData`, `didCommit`, `didChangeData`, `becameInvalid`, and `becameError`. There is no `willSetProperty` event, hence our error.

How does a model get into `inFlight` and why might its properties change therein?


We are getting pretty close to unraveling this error. We now know what it means. The next step is to figure out what caused it. How did our model get into `inFlight` state and why did ember-data try to change its `title` property while it was in that state?

The answer to the first question is pretty simple to answer, and in fact if we look back at the comments above the `inFlight` `DS.State` it will not only tell us how a model gets into this state but warn us that changing the model's properties should not be done during that time. The comments in the code say:
Once a record has been handed off to the adapter to be saved, it is in the 'in flight' state. Changes to the record cannot be made during this window.
So our App.Post was set to `inFlight` when we asked ember-data to save it to the backend, perhaps by calling `model.save()`. That makes sense. The post will be in the `inFlight` state from the time we call `save()` until the time that the backend api responds to ember-data's request, at which time ember-data knows that the changes have been persisted. At that point it will transition to the `saved` state. Internally, ember-data's `inFlight` state transitions by calling `manager.transitionTo('saved');`.

The last part of this puzzle is to find out why the post's `title` property was changed between the time we started saving the post and the backend api responded. There are actually a number of reasons this might happen, depending on the nature of your application. For instance, if you have an `Ember.TextField` whose value is bound to the post's title property, then if you allow the user to type in that field while the post is being saved you'll see this error. Likewise, if you have other async processes going on that could modify the post, then you need to make sure they don't do so when the post is in flight. In my specific case, the post was listening for another user's updates via websockets, and the callback that responded to the websocket message was attempting to update the post's title without ensuring that the post wasn't in the middle of saving.

Demo!


To see visually how the state and state flags change when saving an ember-data model, check out this demo app on jsbin.
Ember-Data state machine example app

From → general

2 Comments
  1. Otte permalink

    Great and informative post. I'm interested in your follow up post on using Kris Selden's Buffered Proxy.

  2. George Anderson permalink

    Nicely presented, thanks!

Leave a Reply

Note: XHTML is allowed. Your email address will never be published.

Subscribe to this comment feed via RSS