Ember.js: How to load a large model without blocking UI - ember.js

I am loading a large model (2500 entries) in Ember Data from an API.
It also takes 3 HTTP Requests since the server will only return 1000 results at a time.
My whole web browser freezes for a moment while it is loading, which begs the question:
What is the best way to load large models without blocking the UI?
I tried beginPropertyChanges, endPropertyChanges:
Ember.RSVP.resolve(store.beginPropertyChanges())
.then(getAllTeams) // this loads the model
.then(function() { return Ember.RSVP.resolve(store.endPropertyChanges()); });
var getAllTeams = function(teams, skip) {
if (!teams) {
return store.find('team', {limit:1000, skip: 0}).then(function(foundTeams) {
var teams = foundTeams;
return getAllTeams(foundTeams,teams.get('length'));
});
}
else if (teams.get('length') < 1000) {
return store.find('team');
}
else {
return store.find('team', {limit: 1000, skip:skip}).then(function(foundTeams) {
return getAllTeams(foundTeams,skip+teams.get('length'));
});
}
}

Doing beginPropertyChanges on the store is not going to accomplish anything useful at all.
The default behavior of Ember is that yes, it will block on large downloads. Here is a possible approach.
// route
export default Ember.Route.extend({
model: function() {
var all = return this.store.all('team');
function get_more(n) {
return store.find('team', {limit:1000, skip: n}) .
then(function(teams) {
if (teams.length === 1000) return get_more(n+1000);
})
}(0));
return all;
});
We return a live collection of teams in the store, which initially might be zero. Asynchronously to that, we start a loop which gets items 1000 at a time. As the new items come in, the live collection will be updated and the relevant UI will as well.
Untested.

Ember provides a mechanism for handling long route render times. I believe what you are looking for is Loading / Error Substates.
Check out official EmberJS guide pages.
Happy coding :)

Related

ember mirage seed db dynamically

I know when application loads, mirage seeds the database. But I wanted to know is there a way to change the seeded database dynamically later on (for example on some user actions).
So, I have an API which gives me the status of the progress and I am polling that API call. Initially Mirage seeds the database for me but every time I make that API call, same data is returned and I want the data to change so that I can test my UI design. Is there any way to do it?
Yes, in your mirage/config.js:
let pollNum = 0;
this.get('/api/poll', () => {
pollNum++;
if (pollNum > 2) {
return { success: true }; // replace with your success fixture
} else {
return { success: false }; // replace with your in progress fixture
}
});

Continuous data update via pouchdb query

i have a pouchdb database with a number of views setup that i query whenever i need data. I am using observables to handle the querying. However i have to refresh the interface to view any data changes in the database. Is there any way i can have these data changes read directly by the observable ? My code is as:-
home.ts
this.postsService.getPosts().subscribe((posts) => {
this.posts = posts.rows.map(row => {
console.log(row.value);
return row.value;
});
});
posts.ts
getPosts(): Observable<any> {
return Observable.fromPromise(this.db.query('app/inputs'));
}
You can use use db.changes with your view, so that you'll only get events for view related changes:
db.changes({
filter: '_view',
view: 'app/inputs',
live: true,
since: 'now',
include_docs:true
}).on('change', (change) => { this.handleChange(change); });
See the filtered changes section on PouchDB docs for a more detailed explanation on this.

Ember server side pagination

I'm not trying to provide pagination within the view itself.
My API returns 500 records at a time and if there are more I'd like to automatically load them.
Although my solution right now does make the requests, I don't think it is the best way, but it does work.
App.StructureAdapter = App.ApplicationAdapter.extend({
findHasMany: function(store, record, url) {
// based on the normal `findHasMany` code
var host = Em.get(this, 'host'),
id = Em.get(record, 'id'),
type = record.constructor.typeKey;
if (host && url.charAt(0) === '/' && url.charAt(1) !== '/') {
url = host + url;
}
return this.findWithURL(this.urlPrefix(url, this.buildURL(type, id)), 1);
},
findWithURL: function(url, page) {
var that = this;
var completeUrl = url + "?page=" + page;
var nextPage = page + 1;
return this.ajax(completeUrl, 'GET').then(function(data) {
Em.Logger.log("calling then");
if (data.structures.length > 0){
that.findWithURL(url, nextPage);
}
return data;
});
}
});
My questions are:
Is there a better way to automatically get all of the pages for a given request?
How do I properly make sure the relationships are built. My Structure object has parent/children relationships on it, but only the first page of results is actually being associated correctly.
Update
Here is what my json response looks like:
{
"structures": [
{
"id": 6536,
"name": "Building",
"updated_at": "2013-05-21T07:14:54-06:00",
"person_id": 6535,
"notes": ""
},
... 499 more objects ...
]
}
It works properly, it loads the first group just fine. And I can adjust it in the extract/normalize methods if I need to.
Here is my normalize method as it is right now:
App.StructureSerializer = App.ApplicationSerializer.extend({
normalize: function(type, hash, prop) {
// adds the properly link to get children
hash.links = { "children": "structures" };
// change structure_id to parent_id
hash.parent_id = hash.structure_id;
delete hash.structure_id;
return this._super(type, hash, prop);
},
});
Again, the links makes it automatically know where to look for the has many relationship.
Looking at it closer, though the paginated pages actually do get called, they are not loaded into Ember data at all. So maybe if they did get loaded then the relationships would build properly.
Here's the best idea I have, I dunno how well it'd work and you might need to play around with it a bit.
In your StructureRoute, go ahead and return the model as normal, so:
App.StructureRoute = Ember.Route.extend({
model:function() {
return this.store.find('structure');
}
});
That'll fetch your first 500 objects and begin the route transition.
Then in your StructureController, fetch the other models using query parameters like this:
App.StructureController = Ember.ArrayController.extend({
init:function() {
this.loadNextPage(2);
this._super(); // this may not be necessary still, but the docs call for it
},
loadNextPage: function(page) {
var self = this;
var promise = this.store.find('structure',{page:page});
promise.then(function(structures) {
if(structures.get('length') < 500) {
self.loadNextPage(page + 1);
}
});
}
});
So when the StructureController initiates, it'll call the recursive function loadNextPage. This will keep running until it hits a page contains less then 500 models. Hopefully, that'll be the last page. By providing the second parameter to find, Ember should trigger a request to /structure?page=2. Inversely, you could do all of this in the route, if you don't mind the slow load time.
If at all possible, I would suggest modifying your API to add some pagination meta data to your request. Then you can use that metadata to control when to stop the recursive function. You can see how to handle metadata here.
Finally, I'm not sure if that's a typo in your json, but you may need to override your pluralization.
Anywho, hope that helps and I didn't overly simply the problem!
I really don't like this solution, but this does work. Please post if you have a much cleaner way of doing this.
Step 1: Load the Data into Ember Data
Since the data wasn't being loaded into Ember Data for the other pages I had to manually load it. I did that by adjusting the findWithURL function I created above.
findWithURL: function(url, page) {
var that = this;
var completeUrl = url + "?page=" + page;
var nextPage = page + 1;
var store = EditUserApp.__container__.lookup('store:main');
return this.ajax(completeUrl, 'GET').then(function(data) {
if (data.structures.length > 0){
that.findWithURL(url, nextPage);
}
store.pushPayload('structure', data);
return data;
});
},
I feel like there should be a cleaner way to do this, but it works.
Step 2: Rebuild the relationships
For some reason it didn't seem to be rebuilding the child/parent relationships. To take care of that I had to use the didLoad callback inside of the Structure model.
didLoad: function() {
var parent = this.get('parent');
if (parent) {
var that = this;
parent.get('children').then(function(children) {
children.addObject(that);
});
}
},
Any suggestions for how to improve this solution are welcome. Ideally I feel like there should be a better Ember way to handle this whole scenario.

How can I ignore dirty records when refreshing a list from the server?

Using latest ember and ember-data.
I have a single-page application with a list of items, and the ability to open the items in tabs.
I can edit the items in the open tabs, and without commiting the dirty record, go back to the list.
If I refresh the list, I get the error:
Error: Attempted to handle event loadedData on <> while in state rootState.loaded.updated.uncommitted
This is of course because I have done a App.TestObject.find() in the list, and still have the dirty uncommitted records (opened and edited records in tabs).
My goal is to show the list with updated records, but do nothing with the uncommited records.
I do not want to do a rollback on the uncommited records.
Is there a best practice for this?
This is a similar question, but I do not want the records reverted to original state.
This is a similar case with a fiddle, but here the rollback is the right solution.
How can I solve the fiddle if I wanted to ignore the uncommitted records when I go back to the list?
I only have a workaround for this issue by monkey-patching DS.Model.
DS.Model.reopen({
loadedData: function() {
if (this.get('isDirty') === false) {
this._super.apply(this, arguments);
}
}
});
Resulting the model to not to update itself when in dirty state, no matter what's in the new JSON regarding this record. The other records will update themselves just fine.
If you don't want to monkey-patch DS.Model.loadedData, here's another solution:
App.Contact.reopenClass({
// Results of our find() call.
cache: null,
// Either find our data for the first time, or refresh what we have.
findAllWithRefresh: function () {
if (this.cache === null) {
this.cache = this.find();
} else {
this.cache.forEach(function (c) {
// This appears to be a correct list of conditions for calling
// refresh(), but you may need to tweak it.
if (c.get("isLoaded") && !c.get("isSaving") && !c.get("isError") && !c.get("isDeleted") && !c.get("isDirty") && !c.get("isReloading")) {
console.log("Refreshing", c);
c.reload();
} else {
console.log("Can't refresh", c);
}
});
}
return this.cache;
}
});
App.ContactsRoute = Ember.Route.extend({
model: function (params) {
// Note that we won't see any new records using this approach.
return App.Contact.findAllWithRefresh();
}
});
Here's a working jsFiddle.
The underlying problem is that you can't safely call App.Contact.find() when there are records with uncommitted changes. This seems like a design problem in Ember Data.

Ember-Data: How to use `DS.Adapter.findHasMany`

UPDATE
Note that this question applies to Ember Data pre-1.0 beta, the mechanism for loading relationships via URL has changed significantly post-1.0 beta!
I asked a much longer question a while back, but since the library has changed since then, I'll ask a much simpler version:
How do you use DS.Adapter.findHasMany? I am building an adapter and I want to be able to load the contents of a relationship on get of the relationship property, and this looks like the way to do it. However, looking at the Ember Data code, I don't see how this function can ever be called (I can explain in comments if needed).
There's not an easy way with my backend to include an array of ids in the property key in the JSON I send--the serializer I'm using doesn't allow me to hook in anywhere good to change that, and it would also be computationally expensive.
Once upon a time, the Ember Data front page showed an example of doing this "lazy loading"...Is this possible, or is this "Handle partially-loaded records" as listed on the Roadmap, and can't yet be done.?
I'm on API revision 11, master branch as of Jan 15.
Update
Okay, the following mostly works. First, I made the following findHasMany method in my adapter, based on the test case's implementation:
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
this.ajax(url, "GET", {
success: function(json) {
var serializer = this.get('serializer');
var pluralRoot = serializer.pluralize(root);
var hashes = json[pluralRoot]; //FIXME: Should call some serializer method to get this?
store.loadMany(type, hashes);
// add ids to record...
var ids = [];
var len = hashes.length;
for(var i = 0; i < len; i++){
ids.push(serializer.extractId(type, hashes[i]));
}
store.loadHasMany(record, relationship.key, ids);
}
});
}
Prerequisite for above is you have to have a well-working extractId method in your serializer, but the built-in one from RESTAdapter will probably do in most cases.
This works, but has one significant problem that I haven't yet really gotten around in any attempt at this lazy-loading approach: if the original record is reloaded from the server, everything goes to pot. The simplest use case that shows this is if you load a single record, then retrieve the hasMany, then later load all the parent records. For example:
var p = App.Post.find(1);
var comments = p.get('comments');
// ...later...
App.Post.find();
In the case of only the code above, what happens is that when Ember Data re-materializes the record it recognizes that there was already a value on the record (posts/1), tries to re-populate it, and follows a different code path which treats the URL string in the JSON hash as an array of single-character IDs. Specifically, it passes the value from the JSON to Ember.EnumerableUtils.map, which understandably enumerates the string's characters as array members.
Therefore, I tried to work around this by "patching" DS.Model.hasManyDidChange, where this occurs, like so:
// Need this function for transplanted hasManyDidChange function...
var map = Ember.EnumerableUtils.map;
DS.Model.reopen({
});
(^ Never mind, this was a really bad idea.)
Update 2
I found I had to do (at least) one more thing to solve the problem mentioned above, when a parent model is re-loaded from the server. The code path where the URL was getting split into single-characters was in DS.Model.reloadHasManys. So, I overrode this method with the following code:
DS.Model.reopen({
reloadHasManys: function() {
var relationships = get(this.constructor, 'relationshipsByName');
this.updateRecordArraysLater();
relationships.forEach(function(name, relationship) {
if (relationship.kind === 'hasMany') {
// BEGIN FIX FOR OPAQUE HASMANY DATA
var cachedValue = this.cacheFor(relationship.key);
var idsOrReferencesOrOpaque = this._data.hasMany[relationship.key] || [];
if(cachedValue && !Ember.isArray(idsOrReferencesOrOpaque)){
var adapter = this.store.adapterForType(relationship.type);
var reloadBehavior = relationship.options.reloadBehavior;
relationship.name = relationship.name || relationship.key; // workaround bug in DS.Model.clearHasMany()?
if (adapter && adapter.findHasMany) {
switch (reloadBehavior) {
case 'ignore':
//FIXME: Should probably replace this._data with references/ids, currently has a string!
break;
case 'force':
case 'reset':
default:
this.clearHasMany(relationship);
cachedValue.set('isLoaded', false);
if (reloadBehavior == 'force' || Ember.meta(this).watching[relationship.key]) {
// reload the data now...
adapter.findHasMany(this.store, this, relationship, idsOrReferencesOrOpaque);
} else {
// force getter code to rerun next time the property is accessed...
delete Ember.meta(this).cache[relationship.key];
}
break;
}
} else if (idsOrReferencesOrOpaque !== undefined) {
Ember.assert("You tried to load many records but you have no adapter (for " + type + ")", adapter);
Ember.assert("You tried to load many records but your adapter does not implement `findHasMany`", adapter.findHasMany);
}
} else {
this.hasManyDidChange(relationship.key);
}
//- this.hasManyDidChange(relationship.key);
// END FIX FOR OPAQUE HASMANY DATA
}
}, this);
}
});
With that addition, using URL-based hasManys is almost usable, with two main remaining problems:
First, inverse belongsTo relationships don't work correctly--you'll have to remove them all. This appears to be a problem with the way RecordArrays are done using ArrayProxies, but it's complicated. When the parent record gets reloaded, both relationships get processed for "removal", so while a loop is iterating over the array, the belongsTo disassociation code removes items from the array at the same time and then the loop freaks out because it tries to access an index that is no longer there. I haven't figured this one out yet, and it's tough.
Second, it's often inefficient--I end up reloading the hasMany from the server too often...but at least maybe I can work around this by sending a few cache headers on the server side.
Anyone trying to use the solutions in this question, I suggest you add the code above to your app, it may get you somewhere finally. But this really needs to get fixed in Ember Data for it to work right, I think.
I'm hoping this gets better supported eventually. On the one hand, the JSONAPI direction they're going explicitly says that this kind of thing is part of the spec. But on the other hand, Ember Data 0.13 (or rev 12?) changed the default serialized format so that if you want to do this, your URL has to be in a JSON property called *_ids... e.g. child_object_ids ... when it's not even IDs you're sending in this case! This seems to suggest that not using an array of IDs is not high on their list of use-cases. Any Ember Data devs reading this: PLEASE SUPPORT THIS FEATURE!
Welcome further thoughts on this!
Instead of an array of ids, the payload needs to contain "something else" than an array.
In the case of the RESTAdapter, the returned JSON is like that:
{blog: {id: 1, comments: [1, 2, 3]}
If you want to handle manually/differently the association, you can return a JSON like that instead:
{blog: {id: 1, comments: "/posts/1/comments"}
It's up to your adapter then to fetch the data from the specified URL.
See the associated test: https://github.com/emberjs/data/blob/master/packages/ember-data/tests/integration/has_many_test.js#L112
I was glad to find this post, helped me. Here is my version, based off the current ember-data and your code.
findHasMany: function(store, record, relationship, details) {
var adapter = this;
var serializer = this.get('serializer');
var type = relationship.type;
var root = this.rootForType(type);
var url = (typeof(details) == 'string' || details instanceof String) ? details : this.buildURL(root);
return this.ajax(url, "GET", {}).then(function(json) {
adapter.didFindMany(store, type, json);
var list = $.map(json[relationship.key], function(o){ return serializer.extractId(type, o);});
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
},
for the reload issue, I did this, based on code I found in another spot, inside the serializer I overrode:
materializeHasMany: function(name, record, hash, relationship) {
var type = record.constructor,
key = this._keyForHasMany(type, relationship.key),
cache = record.cacheFor('data');
if(cache) {
var hasMany = cache.hasMany[relationship.key];
if (typeof(hasMany) == 'object' || hasMany instanceof Object) {
record.materializeHasMany(name, hasMany);
return;
}
}
var value = this.extractHasMany(type, hash, key);
record.materializeHasMany(name, value);
}
I'm still working on figuring out paging, since some of the collections I'm working with need it.
I got a small step closer to getting it working with revision 13 and based myself on sfossen's findHasMany implementation. For an Ember model 'Author' with a hasMany relationship 'blogPosts', my rest api looks like '/api/authors/:author_id/blog_posts'. When querying the rest api for an author with id 11 the blog_posts field reads '/authors/11/blog_posts'.
I now see the related blog posts being returned by the server, but Ember still throws an obscure error that it can not read 'id' from an undefined model object when rendering the page. So I'm not quite there yet, but at least the related data is correctly requested from the rest service.
My complete adapter:
App.Adapter = DS.RESTAdapter.extend({
url: 'http://localhost:3000',
namespace: 'api',
serializer: DS.RESTSerializer.extend({
keyForHasMany: function(type, name) {
return Ember.String.underscore(name);
},
extractHasMany: function(record, json, relationship) {
var relationShip = relationship + '_path';
return { url : json[relationShip] }
}
}),
findHasMany: function(store, record, relationship, details) {
var type = relationship.type;
var root = this.rootForType(type);
var url = this.url + '/' + this.namespace + details.url;
var serializer = this.get('serializer');
return this.ajax(url, "GET", {}).then(
function(json) {
var relationship_key = Ember.String.underscore(relationship.key);
store.loadMany(type, json[relationship_key]);
var list = $.map(json[relationship_key], function(o){
return serializer.extractId(type, o);}
);
store.loadHasMany(record, relationship.key, list);
}).then(null, $.rejectionHandler);
}
});
Here is my solution but it is on Ember-data 0.14, so the world has moved on, even if we are still on this code base:
findHasMany: function(store, record, relationship, details) {
if(relationship.key !== 'activities') {
return;
}
var type = relationship.type,
root = this.rootForType(type),
url = this.url + details.url,
self = this;
this.ajax(url, "GET", {
data: {page: 1}
}).then(function(json) {
var data = record.get('data'),
ids = [],
references = json[relationship.key];
ids = references.map(function(ref){
return ref.id;
});
data[relationship.key] = ids;
record.set('data', data);
self.didFindMany(store, type, json);
record.suspendRelationshipObservers(function() {
record.hasManyDidChange(relationship.key);
});
}).then(null, DS.rejectionHandler);
},
I found replacing the data with the ids worked for me.