From 2d7f88a1a4fa7b039960c9c9aded9513f5634cee Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:35:37 -0500 Subject: [PATCH 1/3] Add meta to the trackedFunction callback --- reactiveweb/src/function.ts | 21 ++++++++++++++----- .../tests/utils/function/rendering-test.gts | 4 ++-- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/reactiveweb/src/function.ts b/reactiveweb/src/function.ts index c1919a8..2805713 100644 --- a/reactiveweb/src/function.ts +++ b/reactiveweb/src/function.ts @@ -107,11 +107,13 @@ export function trackedFunction( assert('Unknown arity: trackedFunction must be called with 1 or 2 arguments'); } +const START = Symbol.for('__reactiveweb_trackedFunction__START__'); + function classUsable(fn: () => Return) { const state = new State(fn); let destroyable = resource>(() => { - state.retry(); + state[START](); return state; }); @@ -151,7 +153,7 @@ export class State { */ @tracked caughtError: unknown; - #fn: () => Value; + #fn: (meta: { isRetrying: boolean }) => Value; constructor(fn: () => Value) { this.#fn = fn; @@ -260,6 +262,15 @@ export class State { return this.data?.error ?? null; } + async [START]() { + try { + await this._dangerousRetry({ isRetrying: false }); + } catch (e) { + if (isDestroyed(this) || isDestroying(this)) return; + this.caughtError = e; + } + } + /** * Will re-invoke the function passed to `trackedFunction` * this will also re-set some properties on the `State` instance. @@ -276,14 +287,14 @@ export class State { * - immediately when inovking `fn` (where auto-tracking occurs) * - after an await, "eventually" */ - await this._dangerousRetry(); + await this._dangerousRetry({ isRetrying: true }); } catch (e) { if (isDestroyed(this) || isDestroying(this)) return; this.caughtError = e; } }; - _dangerousRetry = async () => { + _dangerousRetry = async ({ isRetrying }: { isRetrying: boolean }) => { if (isDestroyed(this) || isDestroying(this)) return; // We've previously had data, but we're about to run-again. @@ -296,7 +307,7 @@ export class State { // this._internalError = null; // We need to invoke this before going async so that tracked properties are consumed (entangled with) synchronously - this.promise = this.#fn(); + this.promise = this.#fn({ isRetrying }); // TrackedAsyncData interacts with tracked data during instantiation. // We don't want this internal state to entangle with `trackedFunction` diff --git a/tests/test-app/tests/utils/function/rendering-test.gts b/tests/test-app/tests/utils/function/rendering-test.gts index 960f0d3..9ac39b3 100644 --- a/tests/test-app/tests/utils/function/rendering-test.gts +++ b/tests/test-app/tests/utils/function/rendering-test.gts @@ -42,14 +42,14 @@ module('Utils | trackedFunction | rendering', function (hooks) { let count = 0; class TestComponent extends Component { - data = trackedFunction(this, () => { + data = trackedFunction(this, ({ isRetrying }) => { // Copy the count so asynchrony of trackedFunction evaluation // doesn't return a newer value than existed at the time // of the function invocation. let localCount = count; count++; - assert.step(`ran trackedFunction ${localCount}`); + assert.step(`ran trackedFunction ${localCount} & ${isRetrying}`); return localCount; }); From a9df5108e28046776da8d577d4ef436bb351e5b8 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:48:40 -0500 Subject: [PATCH 2/3] Fix types --- reactiveweb/src/function.ts | 33 ++++++++++++++++++++++++++------- tests/test-app/package.json | 2 +- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/reactiveweb/src/function.ts b/reactiveweb/src/function.ts index 2805713..c5d9f4e 100644 --- a/reactiveweb/src/function.ts +++ b/reactiveweb/src/function.ts @@ -5,6 +5,10 @@ import { associateDestroyableChild, destroy, isDestroyed, isDestroying } from '@ import { TrackedAsyncData } from 'ember-async-data'; import { resource } from 'ember-resources'; +interface CallbackMeta { + isRetrying: boolean; +} + /** * Any tracked data accessed in a tracked function _before_ an `await` * will "entangle" with the function -- we can call these accessed tracked @@ -48,7 +52,14 @@ import { resource } from 'ember-resources'; * } * ``` */ -export function trackedFunction(fn: () => Return): State; +export function trackedFunction( + fn: (meta: { + /** + * true when state.retry() is called, false initially + */ + isRetrying: boolean; + }) => Return +): State; /** * Any tracked data accessed in a tracked function _before_ an `await` @@ -91,7 +102,15 @@ export function trackedFunction(fn: () => Return): State; * @param {Object} context destroyable parent, e.g.: component instance aka "this" * @param {Function} fn the function to run with the return value available on .value */ -export function trackedFunction(context: object, fn: () => Return): State; +export function trackedFunction( + context: object, + fn: (meta: { + /** + * true when state.retry() is called, false initially + */ + isRetrying: boolean; + }) => Return +): State; export function trackedFunction( ...args: Parameters> | Parameters> @@ -109,7 +128,7 @@ export function trackedFunction( const START = Symbol.for('__reactiveweb_trackedFunction__START__'); -function classUsable(fn: () => Return) { +function classUsable(fn: (meta: CallbackMeta) => Return) { const state = new State(fn); let destroyable = resource>(() => { @@ -123,7 +142,7 @@ function classUsable(fn: () => Return) { return destroyable; } -function directTrackedFunction(context: object, fn: () => Return) { +function directTrackedFunction(context: object, fn: (meta: CallbackMeta) => Return) { const state = new State(fn); let destroyable = resource>(context, () => { @@ -153,9 +172,9 @@ export class State { */ @tracked caughtError: unknown; - #fn: (meta: { isRetrying: boolean }) => Value; + #fn: (meta: CallbackMeta) => Value; - constructor(fn: () => Value) { + constructor(fn: (meta: CallbackMeta) => Value) { this.#fn = fn; } @@ -294,7 +313,7 @@ export class State { } }; - _dangerousRetry = async ({ isRetrying }: { isRetrying: boolean }) => { + _dangerousRetry = async ({ isRetrying }: CallbackMeta) => { if (isDestroyed(this) || isDestroying(this)) return; // We've previously had data, but we're about to run-again. diff --git a/tests/test-app/package.json b/tests/test-app/package.json index 3e6200c..d356883 100644 --- a/tests/test-app/package.json +++ b/tests/test-app/package.json @@ -92,7 +92,7 @@ "webpack": "^5.89.0" }, "engines": { - "node": "16.* || >= 18" + "node": ">= 22" }, "ember": { "edition": "octane" From d30e9d69df8ecdd4eac7ffef031814b320d1d682 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Wed, 5 Feb 2025 16:58:33 -0500 Subject: [PATCH 3/3] Oops --- reactiveweb/src/function.ts | 4 +++- tests/test-app/tests/utils/function/rendering-test.gts | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/reactiveweb/src/function.ts b/reactiveweb/src/function.ts index c5d9f4e..2905599 100644 --- a/reactiveweb/src/function.ts +++ b/reactiveweb/src/function.ts @@ -56,6 +56,7 @@ export function trackedFunction( fn: (meta: { /** * true when state.retry() is called, false initially + * and also false when tracked data changes (new initial) */ isRetrying: boolean; }) => Return @@ -107,6 +108,7 @@ export function trackedFunction( fn: (meta: { /** * true when state.retry() is called, false initially + * and also false when tracked data changes (new initial) */ isRetrying: boolean; }) => Return @@ -146,7 +148,7 @@ function directTrackedFunction(context: object, fn: (meta: CallbackMeta) const state = new State(fn); let destroyable = resource>(context, () => { - state.retry(); + state[START](); return state; }); diff --git a/tests/test-app/tests/utils/function/rendering-test.gts b/tests/test-app/tests/utils/function/rendering-test.gts index 9ac39b3..718cd22 100644 --- a/tests/test-app/tests/utils/function/rendering-test.gts +++ b/tests/test-app/tests/utils/function/rendering-test.gts @@ -18,7 +18,9 @@ module('Utils | trackedFunction | rendering', function (hooks) { class TestComponent extends Component { @tracked count = 1; - data = trackedFunction(this, () => { + data = trackedFunction(this, ({ isRetrying }) => { + assert.step(`${isRetrying}`); + return this.count; }); increment = () => this.count++; @@ -32,10 +34,12 @@ module('Utils | trackedFunction | rendering', function (hooks) { await render(); assert.dom('out').hasText('1'); + assert.verifySteps(['false']); await click('button'); assert.dom('out').hasText('2'); + assert.verifySteps(['false']); }); test('it is retryable', async function (assert) { @@ -61,12 +65,12 @@ module('Utils | trackedFunction | rendering', function (hooks) { } await render(); - assert.verifySteps(['ran trackedFunction 0']); + assert.verifySteps(['ran trackedFunction 0 & false']); assert.dom('out').hasText('0'); await click('button'); - assert.verifySteps(['ran trackedFunction 1']); + assert.verifySteps(['ran trackedFunction 1 & true']); assert.dom('out').hasText('1');