diff --git a/.changeset/neat-apples-shout.md b/.changeset/neat-apples-shout.md new file mode 100644 index 000000000..63972609f --- /dev/null +++ b/.changeset/neat-apples-shout.md @@ -0,0 +1,7 @@ +--- +"ember-resources": patch +--- + +`trackedFunction`: Fix timing issue where updating tracked data consumed in `trackedFunction` would not re-cause the `isLoading` state to become `true` again. + +Resolves #1010 diff --git a/ember-resources/src/util/function.ts b/ember-resources/src/util/function.ts index bf1a1d1d0..897b35536 100644 --- a/ember-resources/src/util/function.ts +++ b/ember-resources/src/util/function.ts @@ -272,6 +272,13 @@ export class State { retry = async () => { if (isDestroyed(this) || isDestroying(this)) return; + // We've previously had data, but we're about to run-again. + // we need to do this again so `isLoading` goes back to `true` when re-running. + // NOTE: we want to do this _even_ if this.data is already null. + // it's all in the same tracking frame and the important thing is taht + // we can't *read* data here. + this.data = null; + // We need to invoke this before going async so that tracked properties are consumed (entangled with) synchronously this.promise = this.#fn(); diff --git a/test-app/tests/utils/function/rendering-test.gts b/test-app/tests/utils/function/rendering-test.gts index ef11d6a3a..c3e2bd4b9 100644 --- a/test-app/tests/utils/function/rendering-test.gts +++ b/test-app/tests/utils/function/rendering-test.gts @@ -7,7 +7,7 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { setOwner } from '@ember/application'; -import { use, resource, resourceFactory } from 'ember-resources'; +import { cell, use, resource, resourceFactory } from 'ember-resources'; import { trackedFunction } from 'ember-resources/util/function'; const timeout = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); diff --git a/test-app/tests/utils/function/timing-test.gts b/test-app/tests/utils/function/timing-test.gts new file mode 100644 index 000000000..fcd477927 --- /dev/null +++ b/test-app/tests/utils/function/timing-test.gts @@ -0,0 +1,111 @@ +import Component from '@glimmer/component'; +import { concat } from '@ember/helper'; +import { render, settled } from '@ember/test-helpers'; +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; + +import { cell, resource, resourceFactory } from 'ember-resources'; +import { trackedFunction } from 'ember-resources/util/function'; + +module('Utils | trackedFunction | timing', function (hooks) { + setupRenderingTest(hooks); + + test('With Argument', async function (assert) { + let step = (msg: string) => assert.step(msg); + + let state = cell(0); + + async function fn(value: number) { + step(`fn:begin:${value}`); + await Promise.resolve(); + step(`fn:end:${value}`); + return `yay:${value}`; + } + + const WithArgument = resourceFactory(num => resource(({ use }) => { + let reactive = use(trackedFunction(() => fn(num))); + + // TODO: the types should allow us to directly return the use, + // but they don't currently + return () => reactive.current; + })); + + await render( + + ); + + assert.verifySteps([ + 'fn:begin:0', 'loading', 'fn:end:0', 'loaded:yay:0' + ]); + + state.current = 1; + await settled(); + + assert.verifySteps([ + 'fn:begin:1', 'loading', 'fn:end:1', 'loaded:yay:1' + ]); + }); + + test('From a component class', async function (assert) { + let step = (msg: string) => assert.step(msg); + + let state = cell(0); + + class Example extends Component<{ Args: { value: unknown } }> { + request = trackedFunction(this, async () => { + let value = this.args.value; + step(`fn:begin:${value}`); + await Promise.resolve(); + step(`fn:end:${value}`); + return `yay:${value}`; + }); + + + } + + await render( + + ); + + assert.verifySteps([ + 'fn:begin:0', 'pending', 'loading', 'fn:end:0', 'loaded:yay:0', 'finished' + ]); + + state.current = 1; + await settled(); + + assert.verifySteps([ + 'fn:begin:1', 'pending', 'loading', 'fn:end:1', 'loaded:yay:1', 'finished' + ]); + }); +});