Skip to content

Commit

Permalink
Merge pull request #130 from universal-ember/add-meta-to-trackedFunct…
Browse files Browse the repository at this point in the history
…ion-callback

Add meta to the trackedFunction callback
  • Loading branch information
NullVoxPopuli authored Feb 5, 2025
2 parents 839e65e + d30e9d6 commit 3032083
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 17 deletions.
54 changes: 43 additions & 11 deletions reactiveweb/src/function.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -48,7 +52,15 @@ import { resource } from 'ember-resources';
* }
* ```
*/
export function trackedFunction<Return>(fn: () => Return): State<Return>;
export function trackedFunction<Return>(
fn: (meta: {
/**
* true when state.retry() is called, false initially
* and also false when tracked data changes (new initial)
*/
isRetrying: boolean;
}) => Return
): State<Return>;

/**
* Any tracked data accessed in a tracked function _before_ an `await`
Expand Down Expand Up @@ -91,7 +103,16 @@ export function trackedFunction<Return>(fn: () => Return): State<Return>;
* @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<Return>(context: object, fn: () => Return): State<Return>;
export function trackedFunction<Return>(
context: object,
fn: (meta: {
/**
* true when state.retry() is called, false initially
* and also false when tracked data changes (new initial)
*/
isRetrying: boolean;
}) => Return
): State<Return>;

export function trackedFunction<Return>(
...args: Parameters<typeof directTrackedFunction<Return>> | Parameters<typeof classUsable<Return>>
Expand All @@ -107,11 +128,13 @@ export function trackedFunction<Return>(
assert('Unknown arity: trackedFunction must be called with 1 or 2 arguments');
}

function classUsable<Return>(fn: () => Return) {
const START = Symbol.for('__reactiveweb_trackedFunction__START__');

function classUsable<Return>(fn: (meta: CallbackMeta) => Return) {
const state = new State(fn);

let destroyable = resource<State<Return>>(() => {
state.retry();
state[START]();

return state;
});
Expand All @@ -121,11 +144,11 @@ function classUsable<Return>(fn: () => Return) {
return destroyable;
}

function directTrackedFunction<Return>(context: object, fn: () => Return) {
function directTrackedFunction<Return>(context: object, fn: (meta: CallbackMeta) => Return) {
const state = new State(fn);

let destroyable = resource<State<Return>>(context, () => {
state.retry();
state[START]();

return state;
});
Expand All @@ -151,9 +174,9 @@ export class State<Value> {
*/
@tracked caughtError: unknown;

#fn: () => Value;
#fn: (meta: CallbackMeta) => Value;

constructor(fn: () => Value) {
constructor(fn: (meta: CallbackMeta) => Value) {
this.#fn = fn;
}

Expand Down Expand Up @@ -260,6 +283,15 @@ export class State<Value> {
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.
Expand All @@ -276,14 +308,14 @@ export class State<Value> {
* - 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 }: CallbackMeta) => {
if (isDestroyed(this) || isDestroying(this)) return;

// We've previously had data, but we're about to run-again.
Expand All @@ -296,7 +328,7 @@ export class State<Value> {
// 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`
Expand Down
2 changes: 1 addition & 1 deletion tests/test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
"webpack": "^5.89.0"
},
"engines": {
"node": "16.* || >= 18"
"node": ">= 22"
},
"ember": {
"edition": "octane"
Expand Down
14 changes: 9 additions & 5 deletions tests/test-app/tests/utils/function/rendering-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -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++;
Expand All @@ -32,24 +34,26 @@ module('Utils | trackedFunction | rendering', function (hooks) {
await render(<template><TestComponent /></template>);

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) {
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;
});
Expand All @@ -61,12 +65,12 @@ module('Utils | trackedFunction | rendering', function (hooks) {
}

await render(<template><TestComponent /></template>);
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');

Expand Down

0 comments on commit 3032083

Please sign in to comment.