Weird behavior of arrayComputed depending on non-array properties


#1

I’ve looked all over the internet of anyone doing the same without success. It is kind of documented in the API docs but it not very clear what happens when an arrayComputed depends, in addition to a property of type Array, in other properties that are not enumerable.

Quick example:

I’ve created an object that is a “season balance” that contains all the transaction the user has performed in a season. Since I want to be able to only show the transaction within a specified time window, my balance also has 2 date attributes, timeWindowStart and timeWindowEnd, which by default the are initializes to the beginning of the season and the current moment respectively. The attribute transactions contains Transaction objects (id, concept, amount and performedAt fields).

I’ve created an arrayComputed property called visibleTransactions that only contains the transactions within the time window.

/**
 * Computes an array from the transactions in the content that only contains
 * the transactions within the time window of the balance.
 * @return {Array}
 */
visibleTransactions: Ember.arrayComputed('content', 'timeWindowStart', 'timeWindowEnd', {
  addedItem: function(array, item, changeMeta, instanceMeta){
    var performedAt = item.get('performedAt'),
      timeWindowStart = this.get('timeWindowStart'),
      timeWindowEnd = this.get('timeWindowEnd');
    if (performedAt >= timeWindowStart && performedAt <= timeWindowEnd){
      array.insertAt(Math.min(changeMeta.index, array.length), item);
    }
    return array;
  }
}),

It works great when adding new elements to the transactions. However, when I change any of the bounds of my time window, the whole property needs to be recomputed (array becomes empty addedItem is called once per each element in content). I am sure that this can be improved, but its ok. I can live with that.

The problem is that I also have another arrayComputed property that dependes on visibleTransactions. This is it:

/**
 * Computes a new array from the visible transactions. Each element of the
 * array is a BalanceEntry, witch groups all the transactions within the time window
 * window by its concept.
 * @return {Array}
 */
arrangedContent: Ember.arrayComputed('visibleTransactions', {
  initialize: function(array, changeMeta, instanceMeta){
    instanceMeta.concepts = [];
  },
  addedItem: function(array, item, changeMeta, instanceMeta){
    var concept = item.get('concept');
    var summaryIndex = instanceMeta.concepts.indexOf(concept);
    var balanceEntry;
    if (summaryIndex == -1){
      balanceEntry = BalanceEntry.create({concept: concept, transactions: [item]});
      array.push(balanceEntry);
      instanceMeta.concepts.push(concept);
    } else {
      balanceEntry = array[summaryIndex];
      balanceEntry.get('transactions').push(item);
    }
    return array;
  },
  removedItem: function(array, item, changeMeta, instanceMeta){
    var index = instanceMeta.concepts.indexOf(item.get('concept'));
    var balanceEntry = array[index];
    if (balanceEntry.get('transactions.length') == 1){
      array.removeAt(index);
      instanceMeta.concepts.removeAt(index);
    } else {
      balanceEntry.get('transactions').removeObject(item);
    }
    return array;
  }
})

I am doing some stuff with the instance meta for optimization but the idea is simple. Each time a new transaction appears I check if there is already a BalanceEntry object with that concept. If it exist, I add this transaction to that entry. If it is a new concept, I create a new balanceEntry for group transactions with that new concept.

Again, it works great when adding transactions.

The problem appears when I change the bounds of the time windows:

Since this property depends only in the visible transactions, I expected addedItem and removedItem to be called once visibleTransactions gets modified, BUT this property is updated BEFORE visibleTransactions is updated.

So, the facts happends in that extrange order:

  1. Initial status. I have default bounds in my time window and all the transactions are therefore visible.
  2. I set the new bounds of my time window: Ember.run(balance, 'setProperties', {timeWindowStart: twoDaysAgo, timeWindowEnd: yesterday});
  3. Surprise: arrangedContent#addedItem(array, item, changeMeta, instanceMeta) is called. The array property is empty but visibleTransactions is still unchanged, so all the items in visibleTransactions are added 1 by one util all is like it was before. Basically the property is recomputed again from scratch (with identical result since the array on which it depends has not changed yet)
  4. visibleTransactions starts to be recomputed. It is empty and is called once for each transaction. In each iteration the transaction might be within the time window or not. When it is, the transaction is added and after that arrangedContent#addedItem is called inmediatly. The arrangedContent is empty again and we start adding transactions. This time since it is called each time a transaction “passes” the visibility filter, addedItem is called only a few times and the result is correct.

I don’t know if i’ve explained the problem clearly, but is quite difficult to provide a gist for that since it is something that is only visible while debugging, because even if it is sub-obtimal, the result is correct.

Maybe I am doing something wrong, but it seems that when an arrayComputed depends has dependencies that are not arrays, other CP that depend on that arrayComputed have an unexpected behavior.

I’ll be glad to pair-program with anybody with a deeper knowledge of arrayComputed than me.

Ideally I would expect visible arrays to get recalculated first, and after that arrangedContent#removedItem or arrangedContent#addedItem being called with transactions have left/entered the new time window.