NullVoxPopuli / ember-resources

An implementation of Resources. Supports ember 3.28+

Home Page:https://github.com/NullVoxPopuli/ember-resources/blob/main/docs/docs/README.md

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

trackedTask value isn't updated

tcjr opened this issue · comments

I'm trying to adopt resources in a large Ember project. We use ember-concurrency for loading and refreshing our data. So, ember-resources' ember-concurrency utilities are perfect for us. We have existing tasks that look like this:

refreshData = restartableTask(async () => { /* load the data */ return data; };

We jump through a bunch of hoops to figure out when to perform refreshData, most notably with render modifiers like this:

<div
  {{did-insert (perform this.refreshData)}}
  {{did-update (perform this.refreshData) @something}}
>
  Data here: {{this.refreshData.lastSuccessful.value}}
</div>

So, using ember-resources, we can eliminate the did-insert/did-update modifier usage by creating a resource. The task and trackedTask look like this:

refreshData = restartableTask(async () => { /* load the data */ return data; }; // unchanged

latest = trackedTask(this, this.refreshData, () => [this.args.something]);

And the template updated to this:

<div>
  Data here: {{this.latest.value}}
</div>

So this mostly works great. However, I'm running into a problem because we have tasks that are performed with manual triggers as well as when args change.

I assumed that I could use {{this.latest.value}} in my templates as a drop-in replacement for {{this.refreshData.lastSuccessful.value}}.

I was surprised to learn that latest.value and refreshData.lastSuccessful.value are sometimes not the same. Specifically, when you perform the task independently, this.latest.value will not always reflect the latest value. For example, if I update the template like this:

<div>
  Data here: {{this.latest.value}}
  <button {{on "click" (perform this.refreshData)}}>refresh</button>
</div>

If that button is pressed while the task is already running, then this.latest.value will not reflect the updated response from refreshData.

Is this expected behavior? Is there a more idiomatic way I can use a resource here without rewriting the existing ember-concurrency tasks?

I have an demonstration of this here: https://stackblitz.com/edit/ember-cli-editor-output-u3yoew?file=app%2Fcomponents%2Fexample.gjs

I think this issue is caused by two things:

  1. ember-concurrency does not support derived data natively
  2. ember-concurrency is meant to handle actions / events -- things that can happen concurrently

For example (1), if you were to add

{{this.slowEcho.isRunning}}

or

{{this.slowEcho.last.isRunning}}

to your template, you get the infinite rendering error:

Uncaught Error: 
Assertion Failed: 
  You attempted to update `last` on `<Task:slowEcho>`, 
  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.

`last` was first used:

- While rendering:
  -top-level
    application
      example
        Child
          this.slowEcho.last.isRunning
            this.slowEcho.last

This error happens because last is tracked in ember-concurrency, and is immediately invoked via trying to render latestResults (which synchronously invokes the task).

And the way tasks are invoked in ember-resources/util/ember-concurrency, https://github.com/NullVoxPopuli/ember-resources/blob/main/ember-resources/src/util/ember-concurrency.ts#L238-L242
uses the return value of perform, to get the TaskInstance and use the tracked data off that object.

So, without making changes to ember-concurrency, I would recommend a slight refactor of your code (if you wanted to stay in derived data land).
The main thing is to not use ember-concurrency.
Ember-concurrency is best used for handling concurrent user-interactions (typing, clicks, submissions, etc).

// a different utility, trackedFunction
latest = trackedFunction(this, async () => {
  // content here
});

get value() {
 return this.latest.value;
}

Docs: https://ember-resources.pages.dev/funcs/util_function.trackedFunction
This utility has a retry method built in: https://ember-resources.pages.dev/classes/util_function.State#retry

Thanks!
It looks like the retry method is exactly what I need. And I guess I'll just have to use the keepLatest utility to replace the other behavior from ember-concurrency since the "old" value is not kept while the function resolves.