Skip to content

Commit

Permalink
Add isLoaded flag for ManyArray
Browse files Browse the repository at this point in the history
This commit refactors the internals to make it
easier to know when all records belonging to a
ManyArray have finished loading.

Now, the store keeps a map of all clientIds that
are being loaded and are members of a ManyArray.

Also, the ManyArray state manager has a new
initial `loading` state that it exits when all
of its clientIds are loaded into the store.
  • Loading branch information
Tomhuda Katzdale committed Jun 19, 2012
1 parent 37d3319 commit f707b6c
Show file tree
Hide file tree
Showing 7 changed files with 251 additions and 79 deletions.
2 changes: 1 addition & 1 deletion packages/ember-data/lib/system/associations/has_many.js
Expand Up @@ -30,7 +30,7 @@ var hasAssociation = function(type, options) {


key = options.key || get(this, 'namingConvention').keyToJSONKey(key); key = options.key || get(this, 'namingConvention').keyToJSONKey(key);
ids = findRecord(store, type, data, key); ids = findRecord(store, type, data, key);
association = store.findMany(type, ids); association = store.findMany(type, ids || []);
set(association, 'parentRecord', this); set(association, 'parentRecord', this);


return association; return association;
Expand Down
16 changes: 10 additions & 6 deletions packages/ember-data/lib/system/record_arrays/many_array.js
@@ -1,7 +1,7 @@
require("ember-data/system/record_arrays/record_array"); require("ember-data/system/record_arrays/record_array");
require("ember-data/system/record_arrays/many_array_states"); require("ember-data/system/record_arrays/many_array_states");


var get = Ember.get, set = Ember.set, getPath = Ember.getPath; var get = Ember.get, set = Ember.set, getPath = Ember.getPath, setPath = Ember.setPath;


DS.ManyArray = DS.RecordArray.extend({ DS.ManyArray = DS.RecordArray.extend({
init: function() { init: function() {
Expand All @@ -16,16 +16,20 @@ DS.ManyArray = DS.RecordArray.extend({
return getPath(this, 'stateManager.currentState.isDirty'); return getPath(this, 'stateManager.currentState.isDirty');
}).property('stateManager.currentState').cacheable(), }).property('stateManager.currentState').cacheable(),


isLoaded: Ember.computed(function() {
return getPath(this, 'stateManager.currentState.isLoaded');
}).property('stateManager.currentState').cacheable(),

send: function(event, context) {
this.get('stateManager').send(event, context);
},

fetch: function() { fetch: function() {
var clientIds = get(this, 'content'), var clientIds = get(this, 'content'),
store = get(this, 'store'), store = get(this, 'store'),
type = get(this, 'type'); type = get(this, 'type');


var ids = clientIds.map(function(clientId) { store.fetchUnloadedClientIds(type, clientIds);
return store.clientIdToId[clientId];
});

store.fetchMany(type, ids);
}, },


// Overrides Ember.Array's replace method to implement // Overrides Ember.Array's replace method to implement
Expand Down
47 changes: 41 additions & 6 deletions packages/ember-data/lib/system/record_arrays/many_array_states.js
@@ -1,4 +1,4 @@
var get = Ember.get, set = Ember.set, guidFor = Ember.guidFor; var get = Ember.get, set = Ember.set, getPath = Ember.getPath, guidFor = Ember.guidFor;


var Set = function() { var Set = function() {
this.hash = {}; this.hash = {};
Expand Down Expand Up @@ -34,7 +34,7 @@ Set.prototype = {
} }
}; };


var ManyArrayState = Ember.State.extend({ var LoadedState = Ember.State.extend({
recordWasAdded: function(manager, record) { recordWasAdded: function(manager, record) {
var dirty = manager.dirty, observer; var dirty = manager.dirty, observer;
dirty.add(record); dirty.add(record);
Expand Down Expand Up @@ -63,7 +63,21 @@ var ManyArrayState = Ember.State.extend({
}); });


var states = { var states = {
clean: ManyArrayState.create({ loading: Ember.State.create({
isLoaded: false,
isDirty: false,

loadedRecords: function(manager, count) {
manager.decrement(count);
},

becameLoaded: function(manager) {
manager.transitionTo('clean');
}
}),

clean: LoadedState.create({
isLoaded: true,
isDirty: false, isDirty: false,


recordWasAdded: function(manager, record) { recordWasAdded: function(manager, record) {
Expand All @@ -77,7 +91,8 @@ var states = {
} }
}), }),


dirty: ManyArrayState.create({ dirty: LoadedState.create({
isLoaded: true,
isDirty: true, isDirty: true,


childWasSaved: function(manager, child) { childWasSaved: function(manager, child) {
Expand All @@ -90,16 +105,36 @@ var states = {
arrayBecameSaved: function(manager) { arrayBecameSaved: function(manager) {
manager.goToState('clean'); manager.goToState('clean');
} }
}) })
}; };


DS.ManyArrayStateManager = Ember.StateManager.extend({ DS.ManyArrayStateManager = Ember.StateManager.extend({
manyArray: null, manyArray: null,
initialState: 'clean', initialState: 'loading',
states: states, states: states,


/**
This number is used to keep track of the number of outstanding
records that must be loaded before the array is considered
loaded. As results stream in, this number is decremented until
it becomes zero, at which case the `isLoaded` flag will be set
to true
*/
counter: 0,

init: function() { init: function() {
this._super(); this._super();
this.dirty = new Set(); this.dirty = new Set();
this.counter = getPath(this, 'manyArray.length');
},

decrement: function(count) {
var counter = this.counter = this.counter - count;

Ember.assert("Somehow the ManyArray loaded counter went below 0. This is probably an ember-data bug. Please report it at https://github.com/emberjs/data/issues", counter >= 0);

if (counter === 0) {
this.send('becameLoaded');
}
} }
}); });
198 changes: 139 additions & 59 deletions packages/ember-data/lib/system/store.js
Expand Up @@ -81,6 +81,13 @@ DS.Store = Ember.Object.extend({
this.clientIdToId = {}; this.clientIdToId = {};
this.recordArraysByClientId = {}; this.recordArraysByClientId = {};


// Internally, we maintain a map of all unloaded IDs requested by
// a ManyArray. As the adapter loads hashes into the store, the
// store notifies any interested ManyArrays. When the ManyArray's
// total number of loading records drops to zero, it becomes
// `isLoaded` and fires a `didLoad` event.
this.loadingRecordArrays = {};

set(this, 'defaultTransaction', this.transaction()); set(this, 'defaultTransaction', this.transaction());


return this._super(); return this._super();
Expand Down Expand Up @@ -345,78 +352,121 @@ DS.Store = Ember.Object.extend({
/** /**
@private @private
Ask the adapter to fetch IDs that are not already loaded. Given a type and array of `clientId`s, determines which of those
`clientId`s has not yet been loaded.
In preparation for loading, this method also marks any unloaded
`clientId`s as loading.
*/
neededClientIds: function(type, clientIds) {
var neededClientIds = [],
typeMap = this.typeMapFor(type),
dataCache = typeMap.cidToHash,
clientId;

for (var i=0, l=clientIds.length; i<l; i++) {
clientId = clientIds[i];
if (dataCache[clientId] === UNLOADED) {
neededClientIds.push(clientId);
dataCache[clientId] = LOADING;
}
}


This method will convert `id`s to `clientId`s, filter out return neededClientIds;
`clientId`s that already have a data hash present, and pass },
the remaining `id`s to the adapter.


@param {Class} type A model class /**
@param {Array} ids An array of ids @private
@param {Object} query
@returns {Array} An Array of all clientIds for the This method is the entry point that associations use to update
specified ids. themselves when their underlying data changes.
First, it determines which of its `clientId`s are still unloaded,
then converts the needed `clientId`s to IDs and invokes `findMany`
on the adapter.
*/ */
fetchMany: function(type, ids, query) { fetchUnloadedClientIds: function(type, clientIds) {
var typeMap = this.typeMapFor(type), var neededClientIds = this.neededClientIds(type, clientIds);
idToClientIdMap = typeMap.idToCid, this.fetchMany(type, neededClientIds);
dataCache = typeMap.cidToHash, },
data = typeMap.cidToHash,
needed;


var clientIds = Ember.A([]); /**
@private
if (ids) { This method takes a type and list of `clientId`s, converts the
needed = []; `clientId`s into IDs, and then invokes the adapter's `findMany`

method.
ids.forEach(function(id) {
// Get the clientId for the given id
var clientId = idToClientIdMap[id];

// If there is no `clientId` yet
if (clientId === undefined) {
// Create a new `clientId`, marking its data hash
// as loading. Once the adapter returns the data
// hash, it will be updated
clientId = this.pushHash(LOADING, id, type);
needed.push(id);

// If there is a clientId, but its data hash is
// marked as unloaded (this happens when a
// hasMany association creates clientIds for its
// referenced ids before they were loaded)
} else if (clientId && data[clientId] === UNLOADED) {
// change the data hash marker to loading
dataCache[clientId] = LOADING;
needed.push(id);
}
// this method is expected to return a list of It is used both by a brand new association (via the `findMany`
// all of the clientIds for the specified ids, method) or when the data underlying an existing association
// unconditionally add it. changes (via the `fetchUnloadedClientIds` method).
clientIds.push(clientId); */
}, this); fetchMany: function(type, clientIds) {
} else { var clientIdToId = this.clientIdToId;
needed = null;
}


// If there are any needed ids, ask the adapter to load them var neededIds = Ember.EnumerableUtils.map(clientIds, function(clientId) {
if ((needed && get(needed, 'length') > 0) || query) { return clientIdToId[clientId];
var adapter = get(this, '_adapter'); });
if (adapter && adapter.findMany) { adapter.findMany(this, type, needed, query); }
else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
}


return clientIds; var adapter = get(this, '_adapter');
if (adapter && adapter.findMany) { adapter.findMany(this, type, neededIds); }
else { throw fmt("Adapter is either null or does not implement `findMany` method", this); }
}, },


/** @private /**
@private
`findMany` is the entry point that associations use to generate a
new `ManyArray` for the list of IDs specified by the server for
the association.
Its responsibilities are:
* convert the IDs into clientIds
* determine which of the clientIds still need to be loaded
* create a new ManyArray whose content is *all* of the clientIds
* notify the ManyArray of the number of its elements that are
already loaded
* insert the unloaded clientIds into the `loadingRecordArrays`
bookkeeping structure, which will allow the `ManyArray` to know
when all of its loading elements are loaded from the server.
* ask the adapter to load the unloaded elements, by invoking
findMany with the still-unloaded IDs.
*/ */
findMany: function(type, ids, query) { findMany: function(type, ids) {
var clientIds = this.fetchMany(type, ids, query); // 1. Convert ids to client ids
// 2. Determine which of the client ids need to be loaded
// 3. Create a new ManyArray whose content is ALL of the clientIds
// 4. Decrement the ManyArray's counter by the number of loaded clientIds
// 5. Put the ManyArray into our bookkeeping data structure, keyed on
// the needed clientIds
// 6. Ask the adapter to load the records for the unloaded clientIds (but
// convert them back to ids)

var clientIds = this.clientIdsForIds(type, ids);

var neededClientIds = this.neededClientIds(type, clientIds),
manyArray = this.createManyArray(type, Ember.A(clientIds)),
loadedCount = clientIds.length - neededClientIds.length,
loadingRecordArrays = this.loadingRecordArrays,
clientId, i, l;

manyArray.send('loadedRecords', loadedCount);

if (neededClientIds.length) {
for (i=0, l=neededClientIds.length; i<l; i++) {
clientId = neededClientIds[i];
if (loadingRecordArrays[clientId]) {
loadingRecordArrays[clientId].push(manyArray);
} else {
this.loadingRecordArrays[clientId] = [ manyArray ];
}
}


return this.createManyArray(type, clientIds); this.fetchMany(type, neededClientIds);
}

return manyArray;
}, },


findQuery: function(type, query) { findQuery: function(type, query) {
Expand Down Expand Up @@ -653,6 +703,18 @@ DS.Store = Ember.Object.extend({
filter = get(array, 'filterFunction'); filter = get(array, 'filterFunction');
this.updateRecordArray(array, filter, type, clientId, dataProxy); this.updateRecordArray(array, filter, type, clientId, dataProxy);
}, this); }, this);

// loop through all manyArrays containing an unloaded copy of this
// clientId and notify them that the record was loaded.
var manyArrays = this.loadingRecordArrays[clientId], manyArray;

if (manyArrays) {
for (var i=0, l=manyArrays.length; i<l; i++) {
manyArrays[i].send('loadedRecords', 1);
}

this.loadingRecordArrays[clientId] = null;
}
}, },


updateRecordArray: function(array, filter, type, clientId, dataProxy) { updateRecordArray: function(array, filter, type, clientId, dataProxy) {
Expand Down Expand Up @@ -738,6 +800,24 @@ DS.Store = Ember.Object.extend({
return this.pushHash(UNLOADED, id, type); return this.pushHash(UNLOADED, id, type);
}, },


/**
@private
This method works exactly like `clientIdForId`, but does not
require looking up the `typeMap` for every `clientId` and
invoking a method per `clientId`.
*/
clientIdsForIds: function(type, ids) {
var typeMap = this.typeMapFor(type),
idToClientIdMap = typeMap.idToCid;

return Ember.EnumerableUtils.map(ids, function(id) {
var clientId = idToClientIdMap[id];
if (clientId) { return clientId; }
return this.pushHash(UNLOADED, id, type);
}, this);
},

// ................ // ................
// . LOADING DATA . // . LOADING DATA .
// ................ // ................
Expand Down

0 comments on commit f707b6c

Please sign in to comment.