Skip to content

Commit 237930d

Browse files
authored
Simplify async fn API to just task(async () => {}) (#477)
1 parent c74f6ae commit 237930d

File tree

48 files changed

+274
-109
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+274
-109
lines changed

addon/-private/async-arrow-runtime.js

+7-11
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,25 @@
11
import { TaskFactory } from './task-factory';
22

33
/**
4-
* Instantiate and return a Task object that is bound to (i.e. its lifetime is intertwined with)
5-
* the `context` param (e.g. a Component or other class defined with modern ES6 class syntax).
4+
* This builder function is called by the transpiled code from
5+
* `task(async () => {})`. See lib/babel-plugin-transform-ember-concurrency-async-tasks.js
66
*
77
* @private
88
*/
9-
export function buildTask(
10-
context,
11-
options,
12-
taskGeneratorFn,
13-
taskName,
14-
bufferPolicyName
15-
) {
9+
export function buildTask(contextFn, options, taskName, bufferPolicyName) {
1610
let optionsWithBufferPolicy = options;
1711

1812
if (bufferPolicyName) {
1913
optionsWithBufferPolicy = Object.assign({}, optionsWithBufferPolicy);
2014
optionsWithBufferPolicy[bufferPolicyName] = true;
2115
}
2216

17+
const result = contextFn();
18+
2319
const taskFactory = new TaskFactory(
2420
taskName || '<unknown>',
25-
taskGeneratorFn,
21+
result.generator,
2622
optionsWithBufferPolicy
2723
);
28-
return taskFactory.createTask(context);
24+
return taskFactory.createTask(result.context);
2925
}

addon/-private/task-public-api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import { assert } from '@ember/debug';
6060
*/
6161
export function task(taskFnOrProtoOrDecoratorOptions, key, descriptor) {
6262
assert(
63-
`It appears you're attempting to use the new task(this, async () => { ... }) syntax, but the async arrow task function you've provided is not being properly compiled by Babel.\n\nPossible causes / remedies:\n\n1. You must pass the async function expression directly to the task() function (it is not currently supported to pass in a variable containing the async arrow fn, or any other kind of indirection)\n2. If this code is in an addon, please ensure the addon specificies ember-concurrency "2.3.0" or higher in "dependencies" (not "devDependencies")\n3. Ensure that there is only one version of ember-concurrency v2.3.0+ being used in your project (including nested dependencies) and consider using npm/yarn/pnpm resolutions to enforce a single version is used`,
63+
`It appears you're attempting to use the new task(async () => { ... }) syntax, but the async arrow task function you've provided is not being properly compiled by Babel.\n\nPossible causes / remedies:\n\n1. You must pass the async function expression directly to the task() function (it is not currently supported to pass in a variable containing the async arrow fn, or any other kind of indirection)\n2. If this code is in an addon, please ensure the addon specificies ember-concurrency "2.3.0" or higher in "dependencies" (not "devDependencies")\n3. Ensure that there is only one version of ember-concurrency v2.3.0+ being used in your project (including nested dependencies) and consider using npm/yarn/pnpm resolutions to enforce a single version is used`,
6464
!isUntranspiledAsyncFn(arguments[arguments.length - 1])
6565
);
6666

addon/index.d.ts

+11
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,11 @@ export function task<
922922
asyncArrowTaskFn: T
923923
): TaskForAsyncTaskFunction<HostObject, T>;
924924

925+
export function task<
926+
HostObject,
927+
T extends AsyncArrowTaskFunction<HostObject, any, any[]>
928+
>(asyncArrowTaskFn: T): TaskForAsyncTaskFunction<HostObject, T>;
929+
925930
export function task<
926931
HostObject,
927932
O extends TaskOptions,
@@ -932,6 +937,12 @@ export function task<
932937
asyncArrowTaskFn: T
933938
): TaskForAsyncTaskFunction<HostObject, T>;
934939

940+
export function task<
941+
HostObject,
942+
O extends TaskOptions,
943+
T extends AsyncArrowTaskFunction<HostObject, any, any[]>
944+
>(baseOptions: O, asyncArrowTaskFn: T): TaskForAsyncTaskFunction<HostObject, T>;
945+
935946
export type AsyncTaskFunction<T, Args extends any[]> = (
936947
...args: Args
937948
) => Promise<T>;

lib/babel-plugin-transform-ember-concurrency-async-tasks.js

+75-21
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@ const {
66
isArrowFunctionExpression,
77
stringLiteral,
88
nullLiteral,
9+
identifier,
910
blockStatement,
1011
returnStatement,
12+
objectExpression,
13+
objectProperty,
14+
thisExpression,
15+
arrowFunctionExpression,
16+
callExpression,
1117
} = require('@babel/types');
1218

1319
const { addNamed } = require('@babel/helper-module-imports');
@@ -105,23 +111,39 @@ function convertFunctionExpressionIntoGenerator(
105111
if (isArrowFunctionExpression(path)) {
106112
// At this point we have something that looks like
107113
//
108-
// foo = task(this, {}?, async () => {})
114+
// foo = task(this?, {}?, async () => {})
109115
//
110116
// and we need to convert it to
111117
//
112-
// foo = buildTask(this, options | null, generatorFn, taskName, bufferPolicyName?)
118+
// foo = buildTask(contextFn, options | null, taskName, bufferPolicyName?)
119+
//
120+
// where conextFn is
121+
//
122+
// () => ({ context: this, generator: function * () { ... } })
113123

114124
// Replace the async arrow fn with a generator fn
115-
let body = path.node.body;
116-
if (body.type !== 'BlockStatement') {
125+
let asyncArrowFnBody = path.node.body;
126+
if (asyncArrowFnBody.type !== 'BlockStatement') {
117127
// Need to convert `async () => expr` with `async () => { return expr }`
118-
body = blockStatement([returnStatement(body)]);
128+
asyncArrowFnBody = blockStatement([returnStatement(asyncArrowFnBody)]);
119129
}
120-
path.replaceWith(
121-
functionExpression(path.node.id, path.node.params, body, true)
130+
131+
const taskGeneratorFn = functionExpression(
132+
path.node.id,
133+
path.node.params,
134+
asyncArrowFnBody,
135+
true
122136
);
123137

124-
// Add an import to buildTask
138+
const contextFn = arrowFunctionExpression(
139+
[],
140+
objectExpression([
141+
objectProperty(identifier('context'), thisExpression()),
142+
objectProperty(identifier('generator'), taskGeneratorFn),
143+
])
144+
);
145+
146+
// Add an import to buildTask (if one hasn't already been added)
125147
if (!state._buildTaskImport) {
126148
state._buildTaskImport = addNamed(
127149
state.root,
@@ -130,31 +152,63 @@ function convertFunctionExpressionIntoGenerator(
130152
);
131153
}
132154

133-
// Rename `task()` to `buildTask()`
134-
path.parentPath.node.callee.name = state._buildTaskImport.name;
155+
const originalArgs = path.parentPath.node.arguments;
135156

136-
// If there's only 2 args (e.g. `task(this, async () => {})`), add null where `options` would be.
137-
if (path.parentPath.node.arguments.length === 2) {
138-
path.parentPath.node.arguments.splice(1, 0, nullLiteral());
157+
// task(this, async() => {}) was the original API, but we don't actually
158+
// need the `this` arg (we determine the `this` context from the contextFn async arrow fn)
159+
if (originalArgs[0] && originalArgs[0].type === 'ThisExpression') {
160+
originalArgs.shift();
139161
}
140162

141-
// Push taskName to the `task()` fn call.
142163
const taskName = extractTaskNameFromClassProperty(path);
143-
path.parentPath.node.arguments.push(stringLiteral(taskName));
164+
let optionsOrNull;
165+
166+
// remaining args should either be [options, async () => {}] or [async () => {}]
167+
switch (originalArgs.length) {
168+
case 1:
169+
optionsOrNull = nullLiteral();
170+
break;
171+
case 2:
172+
optionsOrNull = originalArgs[0];
173+
break;
174+
default:
175+
throw new Error(
176+
`The task() syntax you're using for the task named ${taskName} is incorrect.`
177+
);
178+
}
144179

145180
// Push buffer policy name to `buildTask()`
146181
const bufferPolicyName =
147182
FACTORY_FUNCTION_BUFFER_POLICY_MAPPING[factoryFunctionName];
148-
if (bufferPolicyName) {
149-
path.parentPath.node.arguments.push(stringLiteral(bufferPolicyName));
150-
}
183+
184+
// buildTask(contextFn, options | null, taskName, bufferPolicyName?)
185+
const buildTaskCall = callExpression(
186+
identifier(state._buildTaskImport.name),
187+
[
188+
contextFn,
189+
optionsOrNull,
190+
stringLiteral(taskName),
191+
bufferPolicyName ? stringLiteral(bufferPolicyName) : nullLiteral(),
192+
]
193+
);
194+
195+
let newPath = path.parentPath.replaceWith(buildTaskCall)[0];
196+
newPath.traverse({
197+
FunctionExpression(path) {
198+
if (!path.node.generator) {
199+
return;
200+
}
201+
path.traverse(TransformAwaitIntoYield);
202+
},
203+
});
151204
}
152-
path.traverse(TransformAwaitIntoYield);
153205
}
154206
}
155207

156208
const TransformAwaitIntoYield = {
157209
Function(path) {
210+
// This ensures we don't recurse into more deeply nested functions that
211+
// aren't supposed to be converted from await -> yield.
158212
path.skip();
159213
},
160214
AwaitExpression(path) {
@@ -163,10 +217,10 @@ const TransformAwaitIntoYield = {
163217
};
164218

165219
/**
166-
* Extract the name of the task, e.g. `foo = task(this, async () => {})` has a task name of "foo".
220+
* Extract the name of the task, e.g. `foo = task(async () => {})` has a task name of "foo".
167221
* Classic ember-concurrency APIs (and decorators-based ones) know the name of the task, which we
168222
* used for error messages and other diagnostic / debugging functionality, but the newer
169-
* `foo = task(this, async () => {})` API needs a bit of help from this transform to determine the name;
223+
* `foo = task(async () => {})` API needs a bit of help from this transform to determine the name;
170224
* in this method we extract the name from the ClassProperty assignment so that we can pass it in
171225
* to the options hash when constructing the Task.
172226
*

tests/dummy/app/components/ajax-throttling-example/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export default class AjaxThrottlingExampleComponent extends Component {
1717
tagName = '';
1818
logs = [];
1919

20-
ajaxTask = enqueueTask(this, { maxConcurrency: 3 }, async () => {
20+
ajaxTask = enqueueTask({ maxConcurrency: 3 }, async () => {
2121
// simulate slow AJAX
2222
await timeout(2000 + 2000 * Math.random());
2323
return {};

tests/dummy/app/components/caps-marquee/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default class CapsMarqueeComponent extends Component {
1414
scrambledText = null;
1515

1616
// BEGIN-SNIPPET caps-marquee
17-
marqueeLoop = task(this, { on: 'init' }, async () => {
17+
marqueeLoop = task({ on: 'init' }, async () => {
1818
let text = this.text;
1919
while (true) {
2020
this.set('formattedText', text);

tests/dummy/app/components/concurrency-graph/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export default class ConcurrencyGraphComponent extends Component {
5252
return Math.max(10000, timeElapsed);
5353
}
5454

55-
ticker = task(this, { drop: true }, async () => {
55+
ticker = task({ drop: true }, async () => {
5656
while (true) {
5757
let now = +new Date();
5858
this.set('timeElapsed', now - this.startTime);

tests/dummy/app/components/count-up/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class CountUpComponent extends Component {
66
count = 0;
77

88
// BEGIN-SNIPPET count-up
9-
countUp = task(this, { on: 'init' }, async () => {
9+
countUp = task({ on: 'init' }, async () => {
1010
while (true) {
1111
this.incrementProperty('count');
1212
await timeout(100);

tests/dummy/app/components/events-example/component.js

+9-9
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export default class EventsExampleComponent extends Component.extend(Evented) {
1414
// BEGIN-SNIPPET waitForEvent
1515
domEvent = null;
1616

17-
domEventLoop = task(this, async () => {
17+
domEventLoop = task(async () => {
1818
while (true) {
1919
let event = await waitForEvent(document.body, 'click');
2020
this.set('domEvent', event);
@@ -24,7 +24,7 @@ export default class EventsExampleComponent extends Component.extend(Evented) {
2424

2525
jQueryEvent = null;
2626

27-
jQueryEventLoop = task(this, async () => {
27+
jQueryEventLoop = task(async () => {
2828
let $body = $('body');
2929
while (true) {
3030
let event = await waitForEvent($body, 'click');
@@ -34,7 +34,7 @@ export default class EventsExampleComponent extends Component.extend(Evented) {
3434

3535
emberEvent = null;
3636

37-
emberEventedLoop = task(this, async () => {
37+
emberEventedLoop = task(async () => {
3838
while (true) {
3939
let event = await waitForEvent(this, 'fooEvent');
4040
this.set('emberEvent', event);
@@ -52,34 +52,34 @@ export default class EventsExampleComponent extends Component.extend(Evented) {
5252
// END-SNIPPET
5353

5454
// BEGIN-SNIPPET waitForEvent-derived-state
55-
waiterLoop = task(this, async () => {
55+
waiterLoop = task(async () => {
5656
while (true) {
5757
await this.waiter.perform();
5858
await timeout(1500);
5959
}
6060
});
6161

62-
waiter = task(this, async () => {
62+
waiter = task(async () => {
6363
let event = await waitForEvent(document.body, 'click');
6464
return event;
6565
});
6666

6767
// END-SNIPPET
6868

6969
// BEGIN-SNIPPET waitForProperty
70-
startAll = task(this, async () => {
70+
startAll = task(async () => {
7171
this.set('bazValue', 1);
7272
this.set('state', 'Start.');
7373
this.foo.perform();
7474
this.bar.perform();
7575
this.baz.perform();
7676
});
7777

78-
foo = task(this, async () => {
78+
foo = task(async () => {
7979
await timeout(500);
8080
});
8181

82-
bar = task(this, async () => {
82+
bar = task(async () => {
8383
await waitForProperty(this, 'foo.isIdle');
8484
this.set('state', `${this.state} Foo is idle.`);
8585
await timeout(500);
@@ -89,7 +89,7 @@ export default class EventsExampleComponent extends Component.extend(Evented) {
8989

9090
bazValue = 1;
9191

92-
baz = task(this, async () => {
92+
baz = task(async () => {
9393
let val = await waitForProperty(this, 'bazValue', (v) => v % 2 === 0);
9494
await timeout(500);
9595
this.set('state', `${this.state} Baz got even value ${val}.`);

tests/dummy/app/components/scrambled-text/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default class ScrambledTextComponent extends Component {
2121
scrambledText = null;
2222

2323
// BEGIN-SNIPPET scrambled-text
24-
startScrambling = task(this, { on: 'init' }, async () => {
24+
startScrambling = task({ on: 'init' }, async () => {
2525
let text = this.text;
2626
while (true) {
2727
let pauseTime = 140;

tests/dummy/app/components/start-task-example/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export default class StartTaskExampleComponent extends Component {
88

99
status = null;
1010

11-
myTask = task(this, { on: ['init', 'foo'] }, async (msg = 'init') => {
11+
myTask = task({ on: ['init', 'foo'] }, async (msg = 'init') => {
1212
let status = `myTask.perform(${msg})...`;
1313
this.set('status', status);
1414

tests/dummy/app/components/task-function-syntax-1/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class TaskFunctionSyntaxComponent1 extends Component {
66
status = null;
77

88
// BEGIN-SNIPPET task-function-syntax-1
9-
waitAFewSeconds = task(this, async () => {
9+
waitAFewSeconds = task(async () => {
1010
this.set('status', 'Gimme one second...');
1111
await timeout(1000);
1212
this.set('status', 'Gimme one more second...');

tests/dummy/app/components/task-function-syntax-2/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class TaskFunctionSyntaxComponent2 extends Component {
66
status = null;
77

88
// BEGIN-SNIPPET task-function-syntax-2
9-
pickRandomNumbers = task(this, async () => {
9+
pickRandomNumbers = task(async () => {
1010
let nums = [];
1111
for (let i = 0; i < 3; i++) {
1212
nums.push(Math.floor(Math.random() * 10));

tests/dummy/app/components/task-function-syntax-3/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class TaskFunctionSyntaxComponent3 extends Component {
66
status = null;
77

88
// BEGIN-SNIPPET task-function-syntax-3
9-
myTask = task(this, async () => {
9+
myTask = task(async () => {
1010
this.set('status', `Thinking...`);
1111
let promise = timeout(1000).then(() => 123);
1212
let resolvedValue = await promise;

tests/dummy/app/components/task-function-syntax-4/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export default class TaskFunctionSyntaxComponent4 extends Component {
66
status = null;
77

88
// BEGIN-SNIPPET task-function-syntax-4
9-
myTask = task(this, async () => {
9+
myTask = task(async () => {
1010
this.set('status', `Thinking...`);
1111
try {
1212
await timeout(1000).then(() => {

tests/dummy/app/components/tutorial-6/component.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { task } from 'ember-concurrency';
66
export default class Tutorial6 extends TutorialComponent {
77
result = null;
88

9-
findStores = task(this, async () => {
9+
findStores = task(async () => {
1010
let geolocation = this.geolocation;
1111
let store = this.store;
1212

0 commit comments

Comments
 (0)