Skip to content

Commit ed657c4

Browse files
Merge pull request #81 from NullVoxPopuli/fix-async-functions-in-production
feat(functions): BREAKING CHANGE: all functions async
2 parents b9fb33e + de354ee commit ed657c4

File tree

4 files changed

+49
-31
lines changed

4 files changed

+49
-31
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
- ember-3.26
7272
- ember-release
7373
- ember-beta
74-
- ember-canary
74+
# - ember-canary
7575
- embroider-safe
7676
- embroider-optimized
7777
steps:

README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ class StarWarsInfo {
180180
}, () => [this.ids /* defined somewhere */])
181181
}
182182
```
183+
> `characters` would be accessed via `this.info.value.characters` in the `StarWarsInfo` class
183184
184185
While this example is a bit contrived, hopefully it demonstrates how the `state` arg
185186
works. During the first invocation, `state` is falsey, allowing the rest of the
@@ -193,7 +194,8 @@ as the function doesn't interact with `this`).
193194
In this example, where the function is `async`, the "value" of `info.value` is `undefined` until the
194195
function completes.
195196

196-
If a function is synchronous, you can avoid the thunk altogether,
197+
To help prevent accidental async footguns, even if a function is synchronous, it is still ran
198+
asynchronously, therefor, the thunk cannot be avoided.
197199

198200
```ts
199201
class MyClass {
@@ -205,14 +207,14 @@ class MyClass {
205207
}
206208
```
207209

208-
`this.info.value` will be `6`
210+
`this.info.value` will be `undefined`, then `6` and will not change when `num` changes.
209211

210212

211213
### Thunks
212214

213215
With the exception of the `useResource` + `class` combination, all Thunks are optional.
214-
The main caveat is that if you want your resource to update, you _must_ consume the tracked
215-
properties during setup / initial execution.
216+
The main caveat is that if your resources will not update without a thunk -- or consuming
217+
tracked data within setup / initialization (which is done for you with `useFunction`).
216218

217219

218220
- The thunk is "just a function" that allows tracked data to be lazily consumed by the resource.

addon/-private/resources/function-runner.ts

+24-24
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export class FunctionRunner<
5454
this.update();
5555
}
5656

57+
/**
58+
* NOTE: there is no reliable way to determine if a function is async before the function is ran.
59+
* - we can't use fun[Symbol.toStringtag] === 'AsyncFunction' because minifiers may remove the
60+
* async keyword
61+
*/
5762
update() {
5863
/**
5964
* NOTE: All positional args are consumed
@@ -63,40 +68,35 @@ export class FunctionRunner<
6368
}
6469

6570
const fun = this[FUNCTION_TO_RUN];
66-
// TS doesn't add the default function symbols to function types...
67-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
68-
const isAsync = (fun as any)[Symbol.toStringTag] === 'AsyncFunction';
71+
6972
/**
7073
* Do not access "value" directly in this function. You'll have infinite re-rendering errors
7174
*/
7275
const previous = this[SECRET_VALUE];
7376

74-
if (isAsync) {
75-
const asyncWaiter = async () => {
76-
// in case the async function tries to consume things on the parent `this`,
77-
// be sure we start with a fresh frame
78-
await new Promise((resolve) => schedule('afterRender', resolve));
77+
const asyncWaiter = async () => {
78+
// in case the async function tries to consume things on the parent `this`,
79+
// be sure we start with a fresh frame
80+
await new Promise((resolve) => schedule('afterRender', resolve));
7981

80-
if (isDestroying(this) || isDestroyed(this)) {
81-
return;
82-
}
82+
if (isDestroying(this) || isDestroyed(this)) {
83+
return;
84+
}
8385

84-
const value = await fun(previous, ...this.funArgs);
86+
const value = await fun(previous, ...this.funArgs);
8587

86-
if (isDestroying(this) || isDestroyed(this)) {
87-
return;
88-
}
88+
if (isDestroying(this) || isDestroyed(this)) {
89+
return;
90+
}
8991

90-
this[SECRET_VALUE] = value;
91-
this[HAS_RUN] = true;
92-
dirty(this, SECRET_VALUE);
93-
};
92+
this[SECRET_VALUE] = value;
93+
this[HAS_RUN] = true;
94+
dirty(this, SECRET_VALUE);
95+
};
9496

95-
waitForPromise(asyncWaiter());
96-
97-
return;
98-
}
97+
waitForPromise(asyncWaiter());
9998

100-
this[SECRET_VALUE] = fun(previous, ...this.funArgs) as Return;
99+
// If we ever want to bring sync-support back:
100+
// this[SECRET_VALUE] = fun(previous, ...this.funArgs) as Return;
101101
}
102102
}

tests/unit/use-function-test.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -56,24 +56,37 @@ module('useFunction', function () {
5656

5757
data = useFunction(
5858
this,
59-
(previous: number, count: number) => count * (previous || 1),
59+
(previous: number, count: number) => {
60+
return count * (previous || 1);
61+
},
6062
() => [this.count]
6163
);
6264
}
6365

6466
let foo = new Test();
6567

68+
assert.equal(foo.data.value, undefined);
69+
await settled();
70+
6671
assert.equal(foo.data.value, 1);
6772

6873
foo.count = 2;
74+
foo.data.value;
6975
await settled();
7076

7177
assert.equal(foo.data.value, 2);
7278

7379
foo.count = 6;
80+
foo.data.value;
7481
await settled();
7582

7683
assert.equal(foo.data.value, 12);
84+
85+
foo.count = 7;
86+
foo.data.value;
87+
await settled();
88+
89+
assert.equal(foo.data.value, 84);
7790
});
7891

7992
test('it works with async functions', async function (assert) {
@@ -252,7 +265,10 @@ module('useFunction', function () {
252265

253266
await click('button');
254267

255-
assert.dom('out').hasText('4');
268+
// NOTE: this may be unintuitive because of the direct access to this.count.
269+
// It's important to know that the function is invoked async, so there is no way to
270+
// know that this.count should cause re-invocations without the thunk
271+
assert.dom('out').hasText('2');
256272
});
257273
});
258274
});

0 commit comments

Comments
 (0)