Skip to content

Instantly share code, notes, and snippets.

@pzuraq
Created April 3, 2013 20:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pzuraq/5304796 to your computer and use it in GitHub Desktop.
Save pzuraq/5304796 to your computer and use it in GitHub Desktop.
// *** WHY DOES THIS FILE EXIST ***
//
// For some godawful reason, ember-data's StateManager was written in a closure
// and I can't figure out a way to extend it, which is terrible because I really
// needed to. So, I copied over most of the closure here and reinstantiated
// the states var, which meant I needed a whole bunch of other stuff from the
// closure, and then I reopened DS.StateManager and set states to the new,
// modified states var. Replace this asap, please.
(function() {
var isEmptyObject = function(object) {
for (var name in object) {
if (object.hasOwnProperty(name)) { return false; }
}
return true;
};
var hasDefinedProperties = function(object) {
for (var name in object) {
if (object.hasOwnProperty(name) && object[name]) { return true; }
}
return false;
};
var didChangeData = function(manager) {
var record = get(manager, 'record');
record.materializeData();
};
var willSetProperty = function(manager, context) {
context.oldValue = get(get(manager, 'record'), context.name);
var change = DS.AttributeChange.createChange(context);
get(manager, 'record')._changesToSync[context.attributeName] = change;
};
var didSetProperty = function(manager, context) {
var change = get(manager, 'record')._changesToSync[context.attributeName];
change.value = get(get(manager, 'record'), context.name);
change.sync();
};
// Whenever a property is set, recompute all dependent filters
var updateRecordArrays = function(manager) {
var record = manager.get('record');
record.updateRecordArraysLater();
};
var DirtyState = DS.State.extend({
initialState: 'uncommitted',
// FLAGS
isDirty: true,
// SUBSTATES
// When a record first becomes dirty, it is `uncommitted`.
// This means that there are local pending changes, but they
// have not yet begun to be saved, and are not invalid.
uncommitted: DS.State.extend({
// TRANSITIONS
enter: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameDirty(dirtyType, record);
});
},
// EVENTS
willSetProperty: willSetProperty,
didSetProperty: didSetProperty,
becomeDirty: Ember.K,
willCommit: function(manager) {
manager.transitionTo('inFlight');
},
becameClean: function(manager) {
var record = get(manager, 'record'),
dirtyType = get(this, 'dirtyType');
record.withTransaction(function(t) {
t.recordBecameClean(dirtyType, record);
});
manager.transitionTo('loaded.materializing');
},
becameInvalid: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameInFlight(dirtyType, record);
});
manager.transitionTo('invalid');
},
rollback: function(manager) {
get(manager, 'record').rollback();
},
// ****************
// THIS LINE DAMNIT
// ****************
loadedData: Ember.K
}),
// 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.
inFlight: DS.State.extend({
// FLAGS
isSaving: true,
// TRANSITIONS
enter: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.becameInFlight();
record.withTransaction(function (t) {
t.recordBecameInFlight(dirtyType, record);
});
},
// EVENTS
didCommit: function(manager) {
var dirtyType = get(this, 'dirtyType'),
record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('inflight', record);
});
manager.transitionTo('saved');
manager.send('invokeLifecycleCallbacks', dirtyType);
},
becameInvalid: function(manager, errors) {
var record = get(manager, 'record');
set(record, 'errors', errors);
manager.transitionTo('invalid');
manager.send('invokeLifecycleCallbacks');
},
becameError: function(manager) {
manager.transitionTo('error');
manager.send('invokeLifecycleCallbacks');
}
}),
// A record is in the `invalid` state when its client-side
// invalidations have failed, or if the adapter has indicated
// the the record failed server-side invalidations.
invalid: DS.State.extend({
// FLAGS
isValid: false,
exit: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameClean('inflight', record);
});
},
// EVENTS
deleteRecord: function(manager) {
manager.transitionTo('deleted');
get(manager, 'record').clearRelationships();
},
willSetProperty: willSetProperty,
didSetProperty: function(manager, context) {
var record = get(manager, 'record'),
errors = get(record, 'errors'),
key = context.name;
set(errors, key, null);
if (!hasDefinedProperties(errors)) {
manager.send('becameValid');
}
didSetProperty(manager, context);
},
becomeDirty: Ember.K,
rollback: function(manager) {
manager.send('becameValid');
manager.send('rollback');
},
becameValid: function(manager) {
manager.transitionTo('uncommitted');
},
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('becameInvalid', record);
}
})
});
// The created and updated states are created outside the state
// chart so we can reopen their substates and add mixins as
// necessary.
var createdState = DirtyState.create({
dirtyType: 'created',
// FLAGS
isNew: true
});
var updatedState = DirtyState.create({
dirtyType: 'updated'
});
createdState.states.uncommitted.reopen({
deleteRecord: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordIsMoving('created', record);
});
record.clearRelationships();
manager.transitionTo('deleted.saved');
}
});
createdState.states.uncommitted.reopen({
rollback: function(manager) {
this._super(manager);
manager.transitionTo('deleted.saved');
}
});
updatedState.states.uncommitted.reopen({
deleteRecord: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordIsMoving('updated', record);
});
manager.transitionTo('deleted');
get(manager, 'record').clearRelationships();
}
});
var states = {
rootState: Ember.State.create({
// FLAGS
isLoaded: false,
isReloading: false,
isDirty: false,
isSaving: false,
isDeleted: false,
isError: false,
isNew: false,
isValid: true,
// SUBSTATES
// A record begins its lifecycle in the `empty` state.
// If its data will come from the adapter, it will
// transition into the `loading` state. Otherwise, if
// the record is being created on the client, it will
// transition into the `created` state.
empty: DS.State.create({
// EVENTS
loadingData: function(manager) {
manager.transitionTo('loading');
},
loadedData: function(manager) {
manager.transitionTo('loaded.created');
}
}),
// A record enters this state when the store askes
// the adapter for its data. It remains in this state
// until the adapter provides the requested data.
//
// Usually, this process is asynchronous, using an
// XHR to retrieve the data.
loading: DS.State.create({
// EVENTS
loadedData: didChangeData,
materializingData: function(manager) {
manager.transitionTo('loaded.materializing.firstTime');
}
}),
// A record enters this state when its data is populated.
// Most of a record's lifecycle is spent inside substates
// of the `loaded` state.
loaded: DS.State.create({
initialState: 'saved',
// FLAGS
isLoaded: true,
// SUBSTATES
materializing: DS.State.create({
// FLAGS
isLoaded: false,
// EVENTS
willSetProperty: Ember.K,
didSetProperty: Ember.K,
didChangeData: didChangeData,
finishedMaterializing: function(manager) {
manager.transitionTo('loaded.saved');
},
// SUBSTATES
firstTime: DS.State.create({
exit: function(manager) {
var record = get(manager, 'record');
Ember.run.once(function() {
record.trigger('didLoad');
});
}
})
}),
reloading: DS.State.create({
// FLAGS
isReloading: true,
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record'),
store = get(record, 'store');
store.reloadRecord(record);
},
exit: function(manager) {
var record = get(manager, 'record');
once(record, 'trigger', 'didReload');
},
// EVENTS
loadedData: didChangeData,
materializingData: function(manager) {
manager.transitionTo('loaded.materializing');
}
}),
// If there are no local changes to a record, it remains
// in the `saved` state.
saved: DS.State.create({
// EVENTS
willSetProperty: willSetProperty,
didSetProperty: didSetProperty,
didChangeData: didChangeData,
loadedData: didChangeData,
reloadRecord: function(manager) {
manager.transitionTo('loaded.reloading');
},
materializingData: function(manager) {
manager.transitionTo('loaded.materializing');
},
becomeDirty: function(manager) {
manager.transitionTo('updated');
},
deleteRecord: function(manager) {
manager.transitionTo('deleted');
get(manager, 'record').clearRelationships();
},
unloadRecord: function(manager) {
manager.transitionTo('deleted.saved');
get(manager, 'record').clearRelationships();
},
willCommit: function(manager) {
manager.transitionTo('relationshipsInFlight');
},
invokeLifecycleCallbacks: function(manager, dirtyType) {
var record = get(manager, 'record');
if (dirtyType === 'created') {
record.trigger('didCreate', record);
} else {
record.trigger('didUpdate', record);
}
}
}),
relationshipsInFlight: Ember.State.create({
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function (t) {
t.recordBecameInFlight('clean', record);
});
},
// EVENTS
didCommit: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('inflight', record);
});
manager.transitionTo('saved');
manager.send('invokeLifecycleCallbacks');
}
}),
// A record is in this state after it has been locally
// created but before the adapter has indicated that
// it has been saved.
created: createdState,
// A record is in this state if it has already been
// saved to the server, but there are new local changes
// that have not yet been saved.
updated: updatedState
}),
// A record is in this state if it was deleted from the store.
deleted: DS.State.create({
initialState: 'uncommitted',
dirtyType: 'deleted',
// FLAGS
isDeleted: true,
isLoaded: true,
isDirty: true,
// TRANSITIONS
setup: function(manager) {
var record = get(manager, 'record'),
store = get(record, 'store');
store.removeFromRecordArrays(record);
},
// SUBSTATES
// When a record is deleted, it enters the `start`
// state. It will exit this state when the record's
// transaction starts to commit.
uncommitted: DS.State.create({
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameDirty('deleted', record);
});
},
// EVENTS
willCommit: function(manager) {
manager.transitionTo('inFlight');
},
rollback: function(manager) {
get(manager, 'record').rollback();
},
becomeDirty: Ember.K,
becameClean: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('deleted', record);
});
manager.transitionTo('loaded.materializing');
}
}),
// After a record's transaction is committing, but
// before the adapter indicates that the deletion
// has saved to the server, a record is in the
// `inFlight` substate of `deleted`.
inFlight: DS.State.create({
// FLAGS
isSaving: true,
// TRANSITIONS
enter: function(manager) {
var record = get(manager, 'record');
record.becameInFlight();
record.withTransaction(function (t) {
t.recordBecameInFlight('deleted', record);
});
},
// EVENTS
didCommit: function(manager) {
var record = get(manager, 'record');
record.withTransaction(function(t) {
t.recordBecameClean('inflight', record);
});
manager.transitionTo('saved');
manager.send('invokeLifecycleCallbacks');
}
}),
// Once the adapter indicates that the deletion has
// been saved, the record enters the `saved` substate
// of `deleted`.
saved: DS.State.create({
// FLAGS
isDirty: false,
setup: function(manager) {
var record = get(manager, 'record'),
store = get(record, 'store');
store.dematerializeRecord(record);
},
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('didDelete', record);
}
})
}),
// If the adapter indicates that there was an unknown
// error saving a record, the record enters the `error`
// state.
error: DS.State.create({
isError: true,
// EVENTS
invokeLifecycleCallbacks: function(manager) {
var record = get(manager, 'record');
record.trigger('becameError', record);
}
})
})
};
DS.StateManager.reopen({
states: states
})
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment