As most experienced developers eventually figure out, modeling complex behavior is typically best done using a state machine. Internally, Ember models the behavior and state of several different objects as state machines:
- The router (via
router.js
/route_recognizer.js
) -
DS.Model
lifecycle -
Ember.View
render lifecycle
Unfortunately, all three of these use different implementations of state machines.
The router is a little bit of a unique case in the sense that it needs to track both current state (the active route) plus the current context (which model is backing that route).
However, DS.Model
and Ember.View
can likely be refactored to share a common statechart implementation.
The state manager that Ember Data uses is Ember’s Ember.StateManager
class. Work was done to refactor Ember.View
to use this same class, but it caused massive performance regressions and had to be reverted. This also explains the performance issues Ember Data users are running into when trying to load large numbers of records at once.
On the plane home from JSConf, I started spiking out a new library for building fast statecharts. However, upon landing, I discovered that @stefan.penner had already worked on a similar project with @kris.selden, with a focus on performance. I wanted to start a thread on Discourse to help us identify how we can unify our work, and make sure that we discuss all of the design constraints for a new library, so that whoever ends up doing the implementation work maximizes the value of the library to the greater Ember ecosystem.
I was tentatively calling the library I was working on governor.js
(because it manages state—get it?) but am happy to reconsider if anyone has any objections.
Important Design Decisions
Microlibrary
In keeping with our recent trend of making the core components of Ember as useful as possible to our friends outside the Ember community, this library should not have a dependency on ember-runtime
. Backbone.js users that want to use a state machine should be able to.
Performance
Given that this library is intended for use by both the Ember view system and for the creation of Ember Data models, both potentially hot paths in end user applications, performance is of the utmost importance. In particular, creating new instances of state charts should be made as cheap as possible.
The main avenues of accomplishing this are:
- All state managers should share a single instance of the tree of state objects.
- Reduce the dependence on creating
Ember.Object
instances. - Dispatching events to the state machine should be cached methods so that V8 can inline the function in cases where it becomes a hot path.
Stefan and Kris can probably share some more insight into their work on improving the performance on the view layer.
Ideally, we can add performance benchmarks to the library and be mindful of merging in PRs that cause regressions.
Improved API
The current API leads to code that is rather unwieldy and difficult for new developers to reason about. For an example, just take a look at the current state chart for DS.Model
: https://github.com/emberjs/data/blob/master/packages/ember-data/lib/system/model/states.js
I think an important feature of the Ember router API that we need to learn from is that it separates hierarchy from behavior. Currently, the two are conflated, leading to deeply nested, noisy structures that are reminiscent of the router v1 API.
I’d like to propose that the new API separate these concerns. Each state can be a separate object in a flat hierarchy, and then that hierarchy is “stitched together” in a separate function. For example, here’s my first stab at an API for this, using the current DS.Model
statechart as a strawman:
var ModelStates = Governor.Statechart.map(function() {
this.state('root', function() {
this.states('empty', 'loading');
this.state('loaded', function() {
this.states('reloading', 'saved');
this.state('materializing', function() {
this.state('firstTime');
});
this.state('created', function() {
this.state('uncommitted');
this.state('inFlight');
this.state('invalid')
});
});
});
});
While this is an improvement, it can be made even nicer for CoffeeScript users:
ModelStatechart = Governor.Statechart.map """
root
empty
loading
loaded
materializing
firstTime
reloading
saved
created
uncommitted
inFlight
invalid
updated
uncommitted
inFlight
invalid
deleted
uncommitted
inFlight
saved
error
"""
Once we have ES6, this API could be used nicely with the new string templates feature, which supports multiline strings:
var ModelStates = Governor.Statechart.map(`
root
empty
loading
loaded
materializing
firstTime
reloading
saved
created
uncommitted
inFlight
invalid
updated
uncommitted
inFlight
invalid
deleted
uncommitted
inFlight
saved
error
`);
Constrained Focus
It is of course easy to try to predict how end users will want to use a library, and add features that we anticipate that will be helpful. For the purposes of this discussion, and for getting a version 1 out the door, I would appreciate it if we could constrain API strawmen to discussing specifically the Ember.View
/DS.Model
use cases. Once we have unified the implementation for both I am happy to discuss additional features that add syntactic sugar for using the API.