Ember Concurrency and `store.unloadAll` fall out of sync


#1

Hello,

Here’s the issue as best as I can describe, and I’d love some input on what might be causing it as well as any general “red flags” folks might notice (aside from this fetchData task being Really Long and a good candidate for refactoring).

(Note: I use e-concurrency decorators here, @restartableTask is what it sounds like in the EC API).

Context: We need “append-based” pagination - you click a button at the bottom of the list, it appends more to the result set. The snippet below is how I’ve accomplished that:

My problem is that when I QUICKLY change a filter by clicking it over and over (which triggers the restartable task to fetch new data), the view falls out sync, specifically the response body’s “meta” object and the store:

I’m not quite sure what’s going on here. What we should be seeing in the view above is no results, but somehow an older response in the task is saved somewhere.

Perhaps it’s that I’m returning a POJO and there’s memory leaking somewhere? Or I’m mixing up synchronous and asynchronous functions (I guess peekAll and unloadAll are synchronous, sorry if that’s completely wrong)?

Thank you for any thoughts!


#2

Adding a debounce helps, but this seems like a bandaid :frowning:

yield timeout(DEBOUNCE_MS);


#3

If I were to guess I would put my finger on the unloadAll/peekAll dance your doing. I know that in my apps the moment I attempt to unload a record it causes much dismay through out the app.

My thought is that your array of models is driven by the return value of the task (fetchData.lastSuccessful.value) but once the unloadAll happens the models referenced are in a wacky state and Ember probably freaks out.

Instead I would suggest maintaining your own set and let the store accumulate models. This way ember-data has a chance to reuse resources instead of loosing them all each time.

(some example code in normal style since I’ve yet to understand decorators)

init() {
  this._super(...arguments);
  this.set('myList', []);
},

fetchData: task(function* ({ unloadAll = false } = {}) {
  let myList = this.get('myList');

  let params = this.get('allQueryParams');
  let {
    'applied-filters': appliedFilters,
  } = params;
  let page = unloadAll ? 1 : this.get('page');
  let queryOptions = {
    page,
  }

  for (const key of appliedFilters) {
    queryOptions[key] = params[key];
  }

  // fetch any new projects
  let projects = yield this.store.query('project', queryOptions);
  let meta = projects.get('meta');

  // unload all if query changes
  if (unloadAll) {
    this.set('page', 1);
    myList.clear();
  }

  myList.pushObjects(projects);

  return {
    meta,
    projects: myList
  };
}).keepLatest()

I also think keepLatest is more performent and likely lowers the async complexity in this use case.


#4

Thank you @sukima! This fixed my problem. I liked what you said about the store - I’m still a little uncomfortable with how to use tasks when dealing with the store.

The only change I need to make was this:

myList.pushObjects(projects.toArray());

There is also a keepLatest task: https://github.com/machty/ember-concurrency-decorators/blob/master/tests/unit/decorators-js-test.js#L30.

Here’s what I ended up with, courtesy of your snippet there!

  @keepLatestTask
  fetchData = function*({ unloadAll = false } = {}) {
    const myList = this.get('myList');
    const queryOptions = this.get('appliedQueryParams');

    // fetch any new projects
    const projects = yield this.store.query('project', queryOptions);
    const meta = projects.get('meta');

    // include the entire, un-paginated response
    if (unloadAll) {
      this.set('page', 1);
      myList.clear();
    }

    myList.pushObjects(projects.toArray());

    return {
      meta,
      projects: myList,
    };
  }

I guess my only nagging concern is with mutating state from within the task while also returning things from it. It seems like I should either do one or the other. In this case, I have to have myList because I need to cache the results across completed tasks. Maybe this is okay, it just seems like it could get confusing.


#5

yeah in this case I think the side effect in a task is ok. Glad it worked out.