diff --git a/addon/-private/system/model/internal-model.js b/addon/-private/system/model/internal-model.js index 236dd1617af..aa15574a875 100644 --- a/addon/-private/system/model/internal-model.js +++ b/addon/-private/system/model/internal-model.js @@ -16,11 +16,33 @@ import { HasManyReference } from "ember-data/-private/system/references"; -var Promise = Ember.RSVP.Promise; -var get = Ember.get; -var set = Ember.set; -var copy = Ember.copy; -var assign = Ember.assign || Ember.merge; +const { + get, + set, + copy, + Error: EmberError, + inspect, + isEmpty, + isEqual, + run: emberRun, + setOwner, + RSVP, + RSVP: { Promise } +} = Ember; + +const assign = Ember.assign || Ember.merge; + +/* + The TransitionChainMap caches the `state.enters`, `state.setups`, and final state reached + when transitioning from one state to another, so that future transitions can replay the + transition without needing to walk the state tree, collect these hook calls and determine + the state to transition into. + + A future optimization would be to build a single chained method out of the collected enters + and setups. It may also be faster to do a two level cache (from: { to }) instead of caching based + on a key that adds the two together. + */ +const TransitionChainMap = new EmptyObject(); var _extractPivotNameCache = new EmptyObject(); var _splitOnDotCache = new EmptyObject(); @@ -37,12 +59,6 @@ function extractPivotName(name) { ); } -function retrieveFromCurrentState(key) { - return function() { - return get(this.currentState, key); - }; -} - // this (and all heimdall instrumentation) will be stripped by a babel transform // https://github.com/heimdalljs/babel5-plugin-strip-heimdall const { @@ -87,72 +103,154 @@ const { @private @class InternalModel */ +export default class InternalModel { + constructor(type, id, store, _, data) { + heimdall.increment(new_InternalModel); + this.type = type; + this.id = id; + this.store = store; + this._data = data || new EmptyObject(); + this.modelName = type.modelName; + this.dataHasInitialized = false; + this._loadingPromise = null; + this._recordArrays = undefined; + this.currentState = RootState.empty; + this.isReloading = false; + this._isDestroyed = false; + this.isError = false; + this.error = null; + + // caches for lazy getters + this.__deferredTriggers = null; + this._references = null; + this._recordReference = null; + this.__inFlightAttributes = null; + this.__relationships = null; + this.__attributes = null; + this.__implicitRelationships = null; + } + + get recordReference() { + if (this._recordReference === null) { + this._recordReference = new RecordReference(this.store, this) + } + return this._recordReference; + } + + get references() { + if (this._references === null) { + this._references = new EmptyObject(); + } + return this._references; + } + + get _deferredTriggers() { + if (this.__deferredTriggers === null) { + this.__deferredTriggers = []; + } + return this.__deferredTriggers; + } + + get _attributes() { + if (this.__attributes === null) { + this.__attributes = new EmptyObject(); + } + return this.__attributes; + } + + set _attributes(v) { + this.__attributes = v; + } + + get _relationships() { + if (this.__relationships === null) { + this.__relationships = new Relationships(this); + } + + return this.__relationships; + } + + get _inFlightAttributes() { + if (this.__inFlightAttributes === null) { + this.__inFlightAttributes = new EmptyObject(); + } + return this.__inFlightAttributes; + } + + set _inFlightAttributes(v) { + this.__inFlightAttributes = v; + } -export default function InternalModel(type, id, store, _, data) { - heimdall.increment(new_InternalModel); - this.type = type; - this.id = id; - this.store = store; - this._data = data || new EmptyObject(); - this.modelName = type.modelName; - this.dataHasInitialized = false; - //Look into making this lazy - this._deferredTriggers = []; - this._attributes = new EmptyObject(); - this._inFlightAttributes = new EmptyObject(); - this._relationships = new Relationships(this); - this._recordArrays = undefined; - this.currentState = RootState.empty; - this.recordReference = new RecordReference(store, this); - this.references = {}; - this.isReloading = false; - this._isDestroyed = false; - this.isError = false; - this.error = null; - this.__ember_meta__ = null; - this[Ember.GUID_KEY] = Ember.guidFor(this); /* - implicit relationships are relationship which have not been declared but the inverse side exists on - another record somewhere - For example if there was + implicit relationships are relationship which have not been declared but the inverse side exists on + another record somewhere + For example if there was - ```app/models/comment.js - import DS from 'ember-data'; + ```app/models/comment.js + import DS from 'ember-data'; - export default DS.Model.extend({ - name: DS.attr() - }) - ``` + export default DS.Model.extend({ + name: DS.attr() + }) + ``` - but there is also + but there is also - ```app/models/post.js - import DS from 'ember-data'; + ```app/models/post.js + import DS from 'ember-data'; - export default DS.Model.extend({ - name: DS.attr(), - comments: DS.hasMany('comment') - }) - ``` + export default DS.Model.extend({ + name: DS.attr(), + comments: DS.hasMany('comment') + }) + ``` - would have a implicit post relationship in order to be do things like remove ourselves from the post - when we are deleted + would have a implicit post relationship in order to be do things like remove ourselves from the post + when we are deleted */ - this._implicitRelationships = new EmptyObject(); -} + get _implicitRelationships() { + if (this.__implicitRelationships === null) { + this.__implicitRelationships = new EmptyObject(); + } + return this.__implicitRelationships; + } + + isEmpty() { + return this.currentState.isEmpty; + } + + isLoading() { + return this.currentState.isLoading; + } + + isLoaded() { + return this.currentState.isLoaded; + } + + hasDirtyAttributes() { + return this.currentState.hasDirtyAttributes; + } + + isSaving() { + return this.currentState.isSaving; + } + + isDeleted() { + return this.currentState.isDeleted; + } + + isNew() { + return this.currentState.isNew; + } + + isValid() { + return this.currentState.isValid; + } + + dirtyType() { + return this.currentState.dirtyType; + } -InternalModel.prototype = { - isEmpty: retrieveFromCurrentState('isEmpty'), - isLoading: retrieveFromCurrentState('isLoading'), - isLoaded: retrieveFromCurrentState('isLoaded'), - hasDirtyAttributes: retrieveFromCurrentState('hasDirtyAttributes'), - isSaving: retrieveFromCurrentState('isSaving'), - isDeleted: retrieveFromCurrentState('isDeleted'), - isNew: retrieveFromCurrentState('isNew'), - isValid: retrieveFromCurrentState('isValid'), - dirtyType: retrieveFromCurrentState('dirtyType'), - - constructor: InternalModel, materializeRecord() { heimdall.increment(materializeRecord); assert("Materialized " + this.modelName + " record with id:" + this.id + "more than once", this.record === null || this.record === undefined); @@ -163,14 +261,14 @@ InternalModel.prototype = { store: this.store, _internalModel: this, id: this.id, - currentState: get(this, 'currentState'), + currentState: this.currentState, isError: this.isError, adapterError: this.error }; - if (Ember.setOwner) { - // ensure that `Ember.getOwner(this)` works inside a model instance - Ember.setOwner(createOptions, getOwner(this.store)); + if (setOwner) { + // ensure that `getOwner(this)` works inside a model instance + setOwner(createOptions, getOwner(this.store)); } else { createOptions.container = this.store.container; } @@ -178,37 +276,37 @@ InternalModel.prototype = { this.record = this.type._create(createOptions); this._triggerDeferredTriggers(); - }, + } recordObjectWillDestroy() { this.record = null; - }, + } deleteRecord() { this.send('deleteRecord'); - }, + } save(options) { var promiseLabel = "DS: Model#save " + this; - var resolver = Ember.RSVP.defer(promiseLabel); + var resolver = RSVP.defer(promiseLabel); this.store.scheduleSave(this, resolver, options); return resolver.promise; - }, + } startedReloading() { this.isReloading = true; if (this.record) { set(this.record, 'isReloading', true); } - }, + } finishedReloading() { this.isReloading = false; if (this.record) { set(this.record, 'isReloading', false); } - }, + } reload() { this.startedReloading(); @@ -226,30 +324,30 @@ InternalModel.prototype = { record.finishedReloading(); record.updateRecordArrays(); }); - }, + } getRecord() { if (!this.record) { this.materializeRecord(); } return this.record; - }, + } unloadRecord() { this.send('unloadRecord'); - }, + } eachRelationship(callback, binding) { return this.type.eachRelationship(callback, binding); - }, + } eachAttribute(callback, binding) { return this.type.eachAttribute(callback, binding); - }, + } inverseFor(key) { return this.type.inverseFor(key); - }, + } setupData(data) { heimdall.increment(setupData); @@ -260,29 +358,29 @@ InternalModel.prototype = { this.record._notifyProperties(changedKeys); } this.didInitializeData(); - }, + } becameReady() { - Ember.run.schedule('actions', this.store.recordArrayManager, this.store.recordArrayManager.recordWasLoaded, this); - }, + emberRun.schedule('actions', this.store.recordArrayManager, this.store.recordArrayManager.recordWasLoaded, this); + } didInitializeData() { if (!this.dataHasInitialized) { this.becameReady(); this.dataHasInitialized = true; } - }, + } get isDestroyed() { return this._isDestroyed; - }, + } destroy() { this._isDestroyed = true; if (this.record) { return this.record.destroy(); } - }, + } /* @method createSnapshot @@ -291,7 +389,7 @@ InternalModel.prototype = { createSnapshot(options) { heimdall.increment(createSnapshot); return new Snapshot(this, options); - }, + } /* @method loadingData @@ -300,7 +398,7 @@ InternalModel.prototype = { */ loadingData(promise) { this.send('loadingData', promise); - }, + } /* @method loadedData @@ -309,7 +407,7 @@ InternalModel.prototype = { loadedData() { this.send('loadedData'); this.didInitializeData(); - }, + } /* @method notFound @@ -317,7 +415,7 @@ InternalModel.prototype = { */ notFound() { this.send('notFound'); - }, + } /* @method pushedData @@ -325,18 +423,18 @@ InternalModel.prototype = { */ pushedData() { this.send('pushedData'); - }, + } flushChangedAttributes() { heimdall.increment(flushChangedAttributes); this._inFlightAttributes = this._attributes; this._attributes = new EmptyObject(); - }, + } hasChangedAttributes() { heimdall.increment(hasChangedAttributes); return Object.keys(this._attributes).length > 0; - }, + } /* Checks if the attributes which are considered as changed are still @@ -352,16 +450,17 @@ InternalModel.prototype = { heimdall.increment(updateChangedAttributes); var changedAttributes = this.changedAttributes(); var changedAttributeNames = Object.keys(changedAttributes); + let attrs = this._attributes; for (let i = 0, length = changedAttributeNames.length; i < length; i++) { let attribute = changedAttributeNames[i]; let [oldData, newData] = changedAttributes[attribute]; if (oldData === newData) { - delete this._attributes[attribute]; + delete attrs[attribute]; } } - }, + } /* Returns an object, whose keys are changed properties, and value is an @@ -386,7 +485,7 @@ InternalModel.prototype = { } return diffData; - }, + } /* @method adapterWillCommit @@ -394,7 +493,7 @@ InternalModel.prototype = { */ adapterWillCommit() { this.send('willCommit'); - }, + } /* @method adapterDidDirty @@ -403,7 +502,7 @@ InternalModel.prototype = { adapterDidDirty() { this.send('becomeDirty'); this.updateRecordArraysLater(); - }, + } /* @method send @@ -413,38 +512,38 @@ InternalModel.prototype = { */ send(name, context) { heimdall.increment(send); - var currentState = get(this, 'currentState'); + var currentState = this.currentState; if (!currentState[name]) { this._unhandledEvent(currentState, name, context); } return currentState[name](this, context); - }, + } notifyHasManyAdded(key, record, idx) { if (this.record) { this.record.notifyHasManyAdded(key, record, idx); } - }, + } notifyHasManyRemoved(key, record, idx) { if (this.record) { this.record.notifyHasManyRemoved(key, record, idx); } - }, + } notifyBelongsToChanged(key, record) { if (this.record) { this.record.notifyBelongsToChanged(key, record); } - }, + } notifyPropertyChange(key) { if (this.record) { this.record.notifyPropertyChange(key); } - }, + } rollbackAttributes() { var dirtyKeys = Object.keys(this._attributes); @@ -475,8 +574,7 @@ InternalModel.prototype = { this.send('rolledBack'); this.record._notifyProperties(dirtyKeys); - - }, + } /* @method transitionTo @@ -489,42 +587,55 @@ InternalModel.prototype = { // always having direct reference to state objects var pivotName = extractPivotName(name); - var currentState = get(this, 'currentState'); - var state = currentState; + var state = this.currentState; + let transitionMapId = `${state.stateName}->${name}`; do { if (state.exit) { state.exit(this); } state = state.parentState; - } while (!state.hasOwnProperty(pivotName)); + } while (!state[pivotName]); + + let setups; + let enters; + let i; + let l; + let map = TransitionChainMap[transitionMapId]; + + if (map) { + setups = map.setups; + enters = map.enters; + state = map.state; + } else { + setups = []; + enters = []; + + let path = splitOnDot(name); - var path = splitOnDot(name); - var setups = []; - var enters = []; - var i, l; + for (i = 0, l = path.length; i < l; i++) { + state = state[path[i]]; - for (i=0, l=path.length; i`; - } - }, + return `<${this.modelName}:${this.id}>`; + } referenceFor(type, name) { var reference = this.references[name]; diff --git a/addon/-private/system/model/model.js b/addon/-private/system/model/model.js index cddcd8693f6..4abc94f5bbd 100644 --- a/addon/-private/system/model/model.js +++ b/addon/-private/system/model/model.js @@ -9,12 +9,15 @@ import { DidDefinePropertyMixin, RelationshipsClassMethodsMixin, RelationshipsIn import { AttrClassMethodsMixin, AttrInstanceMethodsMixin } from 'ember-data/-private/system/model/attr'; import isEnabled from 'ember-data/-private/features'; +const { + get, + computed +} = Ember; + /** @module ember-data */ -var get = Ember.get; - function intersection (array1, array2) { var result = []; array1.forEach((element) => { @@ -30,7 +33,7 @@ var RESERVED_MODEL_PROPS = [ 'currentState', 'data', 'store' ]; -var retrieveFromCurrentState = Ember.computed('currentState', function(key) { +var retrieveFromCurrentState = computed('currentState', function(key) { return get(this._internalModel.currentState, key); }).readOnly(); @@ -122,7 +125,7 @@ var Model = Ember.Object.extend(Ember.Evented, { @type {Boolean} @readOnly */ - hasDirtyAttributes: Ember.computed('currentState.isDirty', function() { + hasDirtyAttributes: computed('currentState.isDirty', function() { return this.get('currentState.isDirty'); }), /** @@ -305,6 +308,7 @@ var Model = Ember.Object.extend(Ember.Evented, { @private @type {Object} */ + currentState: null, /** When the record is in the `invalid` state this object will contain @@ -357,7 +361,7 @@ var Model = Ember.Object.extend(Ember.Evented, { @property errors @type {DS.Errors} */ - errors: Ember.computed(function() { + errors: computed(function() { let errors = Errors.create(); errors._registerHandlers(this._internalModel, diff --git a/addon/-private/system/model/states.js b/addon/-private/system/model/states.js index 23fddd3ae71..ed5536e8dd1 100644 --- a/addon/-private/system/model/states.js +++ b/addon/-private/system/model/states.js @@ -4,7 +4,10 @@ import Ember from 'ember'; import { assert } from "ember-data/-private/debug"; -const { get } = Ember; +const { + K +} = Ember; + /* This file encapsulates the various states that a record can transition through during its lifecycle. @@ -257,7 +260,7 @@ const DirtyState = { heimdall.stop(token); }, - becomeDirty() { }, + becomeDirty() {}, willCommit(internalModel) { internalModel.transitionTo('inFlight'); @@ -430,7 +433,7 @@ createdState.uncommitted.pushedData = function(internalModel) { internalModel.triggerLater('didLoad'); }; -createdState.uncommitted.propertyWasReset = Ember.K; +createdState.uncommitted.propertyWasReset = K; function assertAgainstUnloadRecord(internalModel) { assert("You can only unload a record which is not inFlight. `" + internalModel + "`", false); @@ -581,9 +584,7 @@ const RootState = { internalModel.transitionTo('deleted.saved'); }, - didCommit(internalModel) { - internalModel.send('invokeLifecycleCallbacks', get(internalModel, 'lastDirtyType')); - }, + didCommit() {}, // loaded.saved.notFound would be triggered by a failed // `reload()` on an unchanged record @@ -715,7 +716,6 @@ const RootState = { deleteRecord() { }, willCommit() { }, - rolledBack(internalModel) { internalModel.clearErrorMessages(); internalModel.transitionTo('loaded.saved'); diff --git a/addon/-private/system/record-array-manager.js b/addon/-private/system/record-array-manager.js index 1b44f1279d3..669b0c54d35 100644 --- a/addon/-private/system/record-array-manager.js +++ b/addon/-private/system/record-array-manager.js @@ -8,9 +8,14 @@ import { FilteredRecordArray, AdapterPopulatedRecordArray } from "ember-data/-private/system/record-arrays"; -const { get, MapWithDefault } = Ember; import OrderedSet from "ember-data/-private/system/ordered-set"; +const { + get, + MapWithDefault, + run: emberRun +} = Ember; + const { _addRecordToRecordArray, _recordWasChanged, @@ -78,7 +83,7 @@ export default Ember.Object.extend({ heimdall.increment(recordDidChange); if (this.changedRecords.push(record) !== 1) { return; } - Ember.run.schedule('actions', this, this.updateRecordArrays); + emberRun.schedule('actions', this, this.updateRecordArrays); }, recordArraysForRecord(record) { diff --git a/addon/-private/system/store.js b/addon/-private/system/store.js index 8953f59f7dc..c153a72ad75 100644 --- a/addon/-private/system/store.js +++ b/addon/-private/system/store.js @@ -7,9 +7,7 @@ import Model from 'ember-data/model'; import { instrument, assert, warn, runInDebug } from "ember-data/-private/debug"; import _normalizeLink from "ember-data/-private/system/normalize-link"; import normalizeModelName from "ember-data/-private/system/normalize-model-name"; -import { - InvalidError -} from 'ember-data/adapters/errors'; +import { InvalidError } from 'ember-data/adapters/errors'; import { promiseArray, @@ -22,13 +20,8 @@ import { _objectIsAlive } from "ember-data/-private/system/store/common"; -import { - normalizeResponseHelper -} from "ember-data/-private/system/store/serializer-response"; - -import { - serializerForAdapter -} from "ember-data/-private/system/store/serializers"; +import { normalizeResponseHelper } from "ember-data/-private/system/store/serializer-response"; +import { serializerForAdapter } from "ember-data/-private/system/store/serializers"; import { _find, @@ -40,25 +33,27 @@ import { _queryRecord } from "ember-data/-private/system/store/finders"; -import { - getOwner -} from 'ember-data/-private/utils'; - +import { getOwner } from 'ember-data/-private/utils'; import coerceId from "ember-data/-private/system/coerce-id"; - import RecordArrayManager from "ember-data/-private/system/record-array-manager"; import ContainerInstanceCache from 'ember-data/-private/system/store/container-instance-cache'; - import InternalModel from "ember-data/-private/system/model/internal-model"; - import EmptyObject from "ember-data/-private/system/empty-object"; - import isEnabled from 'ember-data/-private/features'; export let badIdFormatAssertion = '`id` passed to `findRecord()` has to be non-empty string or number'; -const Backburner = Ember._Backburner; -var Map = Ember.Map; +const { + _Backburner: Backburner, + copy, + get, + isNone, + isPresent, + Map, + run: emberRun, + set, + Service +} = Ember; //Get the materialized model from the internalModel/promise that returns //an internal model and return it in a promiseObject. Useful for returning @@ -68,19 +63,9 @@ function promiseRecord(internalModel, label) { return promiseObject(toReturn, label); } -var once = Ember.run.once; -var Promise = Ember.RSVP.Promise; -var Store; +const Promise = Ember.RSVP.Promise; -const { - copy, - get, - GUID_KEY, - isNone, - isPresent, - set, - Service -} = Ember; +let Store; // Implementors Note: // @@ -807,7 +792,7 @@ Store = Service.extend({ } else { this._pendingFetch.get(typeClass).push(pendingFetchItem); } - Ember.run.scheduleOnce('afterRender', this, this.flushAllPendingFetches); + emberRun.scheduleOnce('afterRender', this, this.flushAllPendingFetches); return promise; }, @@ -1784,7 +1769,7 @@ Store = Service.extend({ snapshot: snapshot, resolver: resolver }); - once(this, 'flushPendingSave'); + emberRun.once(this, this.flushPendingSave); }, /** @@ -1839,7 +1824,7 @@ Store = Service.extend({ } if (data) { // normalize relationship IDs into records - this._backburner.schedule('normalizeRelationships', this, '_setupRelationships', internalModel, data); + this._backburner.schedule('normalizeRelationships', this, this._setupRelationships, internalModel, data); this.updateId(internalModel, data); } else { assert(`Your ${internalModel.type.modelName} record was saved to the server, but the response does not have an id and no id has been set client side. Records must have ids. Please update the server response to provide an id in the response or generate the id on the client side either before saving the record or while normalizing the response.`, internalModel.id); @@ -1893,7 +1878,7 @@ Store = Service.extend({ var id = coerceId(data.id); // ID absolutely can't be missing if the oldID is empty (missing Id in response for a new record) - assert(`'${internalModel.type.modelName}:${internalModel[GUID_KEY]}' was saved to the server, but the response does not have an id and your record does not either.`, !(id === null && oldId === null)); + assert(`'${internalModel.type.modelName}' was saved to the server, but the response does not have an id and your record does not either.`, !(id === null && oldId === null)); // ID absolutely can't be different than oldID if oldID is not null assert(`'${internalModel.type.modelName}:${oldId}' was saved to the server, but the response returned the new id '${id}'. The store cannot assign a new id to a record that already has an id.`, !(oldId !== null && id !== oldId)); @@ -2262,7 +2247,7 @@ Store = Service.extend({ var internalModel = this._load(data); this._backburner.join(() => { - this._backburner.schedule('normalizeRelationships', this, '_setupRelationships', internalModel, data); + this._backburner.schedule('normalizeRelationships', this, this._setupRelationships, internalModel, data); }); return internalModel; diff --git a/ember-cli-build.js b/ember-cli-build.js index 4b6ad84bd75..0e697964f0b 100644 --- a/ember-cli-build.js +++ b/ember-cli-build.js @@ -23,7 +23,7 @@ module.exports = function(defaults) { babel: { plugins: [ // while ember-data strips itself, ember does not currently - {transformer: stripClassCallCheck, position: 'after'} + { transformer: stripClassCallCheck, position: 'after' } ] } }); diff --git a/lib/babel-build.js b/lib/babel-build.js index da3a2bbf968..1135048aee4 100644 --- a/lib/babel-build.js +++ b/lib/babel-build.js @@ -16,7 +16,8 @@ function babelOptions(libraryName, _options) { 'es6.properties.shorthand', 'es6.blockScoping', 'es6.constants', - 'es6.modules' + 'es6.modules', + 'es6.classes' ], sourceMaps: false, modules: 'amdStrict', diff --git a/lib/stripped-build-plugins.js b/lib/stripped-build-plugins.js index 24e0141bd2b..478622b780a 100644 --- a/lib/stripped-build-plugins.js +++ b/lib/stripped-build-plugins.js @@ -3,6 +3,7 @@ var path = require('path'); var filterImports = require('babel-plugin-filter-imports'); var featureFlags = require('babel-plugin-feature-flags'); var stripHeimdall = require('babel5-plugin-strip-heimdall'); +var stripClassCallCheck = require('babel5-plugin-strip-class-callcheck'); function uniqueAdd(obj, key, values) { var a = obj[key] = obj[key] || []; @@ -32,7 +33,8 @@ module.exports = function(environment) { featureFlags({ import: { module: 'ember-data/-private/features' }, features: features - }) + }), + { transformer: stripClassCallCheck, position: 'after' } ]; if (process.env.INSTRUMENT_HEIMDALL === 'false') { diff --git a/tests/unit/store/unload-test.js b/tests/unit/store/unload-test.js index f2a131ff676..9bcd77696c5 100644 --- a/tests/unit/store/unload-test.js +++ b/tests/unit/store/unload-test.js @@ -55,7 +55,7 @@ testInDebug("unload a dirty record asserts", function(assert) { assert.expectAssertion(function() { record.unloadRecord(); - }, "You can only unload a record which is not inFlight. `" + Ember.inspect(record) + "`", "can not unload dirty record"); + }, "You can only unload a record which is not inFlight. `" + record._internalModel.toString() + "`", "can not unload dirty record"); // force back into safe to unload mode. run(function() {