Warning for infinite revalidation bugs is quite aggressive

With Octane, we got some nice and usually helpful errors warning about updating properties that could lead to infinite revalidation bugs, e.g.:

Assertion Failed: You attempted to update `isPending` on `AsyncResource`, 
but it had already been used previously in the same computation. 
 Attempting to update a value after using it in a computation can cause logical errors, 
infinite revalidation bugs, and performance issues, and is not supported.

Now generally this is great, but I’ve found places where this is - to me - quite hard to follow and reason about, and I wonder if it might be a bit overzealous in some cases.

For example, I’ve been playing around with building a minimal async resource (until `@use` and Resources by pzuraq · Pull Request #567 · emberjs/rfcs · GitHub lands) implementation like this:

import { tracked } from '@glimmer/tracking';
import { restartableTask } from 'ember-concurrency-decorators';

export class AsyncResource {
  promise;
  @tracked isPending = false;
  @tracked value;

  get state() {
    return {
      isPending: this.isPending,
      value: this.value
    };
  }

  async updatePromise(promise) {
    if (promise === this.promise) {
      return;
    }

    this._runPromise.perform(promise);
  }

  @restartableTask
  *_runPromise(promise) {
    this.promise = promise;
    this.isPending = true;

    let value = yield promise;

    this.value = value;
    this.isPending = false;
  }
}

With an accompanying component:

import Component from '@glimmer/component';
import { AsyncResource } from 'fabscale-app/utils/async-resource';

/*
 * This component takes a `promise` argument and yields an object with a `value` and a `isPending` property.
 * Note that the `value` will only change once the passed in promise is resolved!
 * This means that the last value will be used until a new value is resolved - it will not f.e. switch to `null` in between or similar.
 */
export default class AwaitPromise extends Component {
  /*
   * Arguments:
   *  - promise
   */

  _asyncResource;

  constructor() {
    super(...arguments);

    this._asyncResource = new AsyncResource();
  }

  get promiseData() {
    this._asyncResource.updatePromise(this.args.promise);

    return this._asyncResource.state;
  }
}

So usage is like this:

<AwaitPromise @promise={{this.myPromise}} |promiseData|>
  {{log promiseData}}
</AwaitPromise>

Now to me, it looks like this should work, but it triggers the invalidation error. I don’t think (?) this is a bug per se, but basically wanted to ask if that is expected/desired behavior? Or if there are any other ideas on how to better realize such behavior? I ended up with a considerably more complex code where I keep track if the promise has ever resolved and a sprinkle of yield timeout(1) before setting this.isPending, which kind of works but feels rather over-complicated.

6 Likes