question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

RFC: absorb ember-concurrency-async and standardize around async functions

See original GitHub issue

This RFC proposes that:

  1. ember-concurrency-async be merged into ember-concurrency core (similar to how we merged in ember-concurrency-decorators)
  2. We introduce a new @taskFn decorator that behaves almost exactly the same as the @task decorator, but produces directly-callable “async” functions that have safe Task-like semantics
  3. We bring in the taskFor helper from ember-concurrency-ts, but we extend its (currently no-op / typing-only) behavior to extract Task objects from @taskFn functions.
  4. We introduce an async-arrow function equivalent to the above for use with task()

Background

ember-concurrency-async (ECA) is a complementary addon to ember-concurrency that allows you to use async functions instead of generator functions when defining EC tasks, e.g.

import { task } from 'ember-concurrency';

class Foo {
  @task({ restartable: true }) async foo() {
    await timeout(300);
    // ...
  }
}

The way this works is that ECA provides a babel transform to convert the async fn to a generator function (this is required because async fns are not externally cancellable, whereas generators can be used to model cancellable async functions).

This has a few benefits:

  1. TypeScript’s handling of async functions is much more robust than its much-lacking support of generators and the yield keyword.
  2. An EC tasks behaves, in spirit, much more like an async fn than how generators are used (EC is a lot of people’s first experience with generators, and lot of people have the same question as to “why don’t we just use async fns”)

There are some downsides:

  1. The use of a babel transform is a bit magical; it bends the semantics of async fns just a bit which to the astute JS programmer can understandably cause some confusion
  2. ECA still requires the well-typed async fns that perform() EC tasks to use await taskFor(this.myTask).perform() when what you really want to do is just type await this.myTask().

There isn’t much we can do about #1 until/unless TS offers better support for generator fns (unlikely) or async fns become cancellable by way of some new EcmaScript spec (also unlikely).

Proposal: @taskFn decorator

To alleviate some of the awkwardness around #2 above, I propose we merge ECA into EC, and we introduce a new @taskFn decorator that can be used in place of any @task decorator, but instead of converting the decorator generator/async function to a .perform()-able task object with derived state properties, it “converts” it to an async function that can be directly invoked/called (rather than .perform()ed) with Task-like safety guarantees. For example:

import { safe } from 'ember-concurrency';

export default class extends Component {
  @taskFn async parentDoStuff(ms: number) {
    await timeout(ms);
    await this.childDoStuff();
    return 'done!';
  }

  @taskFn async childDoStuff() {
    // ...
  }
}

The @taskFn decorator will accept the same arguments that @task decorator takes today, e.g. you could write the following:

export default class extends Component {
  @taskFn({
    maxConcurrency: 3,
    restartable: true
  })
  async doStuff() {
    // ...
  }
}

With the @taskFn keyword, we can entirely avoid the taskFor() ceremony for performing tasks in a type-safe manner; instead of await taskFor(this.doStuff).perform() you can juse to await this.doStuff()

@taskFor helper

Because @taskFn async functions themselves are just Function instances installed on the prototype (or instance), they won’t, by default, have any kind of “derived state” (like isRunning, etc) that we’ve come to know and love. In order to re-expose this derived state, I propose we bring in the @taskFor function from ember-concurrency-ts, which is currently a no-op fn used to appease TypeScript, and extend it just slightly so that you can pass it an async @taskFn and have it return to you the hidden/internal Task object (which you can then use to access derived state like .isRunning).

export default class extends Component {
  @taskFn async doStuff() {
    // ...
  }

  doStuffTask = taskFor(this.doStuff);
}

With this in place you can put the following in templates:

{{#if this.doStuffTask.isRunning}}
   wat
{{/if}}

With this scheme, there’d basically be two “entry points” to the same @taskFn async fn: 1. calling the fn directly, or 2. Calling .perform() on the Task object returned from taskFor.

async-arrow tasks

This is a more recent idea that emerged from discussions in the Discord e-concurrency channel (thank you @NullVoxPopuli!):

In addition to the above APIs which are somewhat async-function centric (i.e. the thing that gets installed on the host object is an async function with special task-y semantics), I propose we introduce a more class Task-centric API wherein the object that gets installed on the host object is a Task that you can .perform() – this will always be my favorite kind of API because Tasks are so easy and self-contained to pass around and access the derived state of.

class DemoComponent {
  foo = 5;
  doStuffTask = task(this, async () => {
    await timeout(500);
    this.foo++;
  });
}

This is a well-typed TS component, with extremely minimal ceremony (with one bonus being you don’t need to add types to this – it knows that this is a DemoComponent).

To pass options:

class DemoComponent {
  foo = 5;
  doStuffTask = task(this, { restartable: true }, async () => {
    await timeout(500);
    this.foo++;
  });
}

To make this work, we would apply the same kind of transform to async arrow fns, which would turn the above into something like:

class DemoComponent {
  foo = 5;
  doStuffTask = task(this, { restartable: true }, () => {
    return function * () {
      yield timeout(500);
      this.foo++;
    }
  });
}

We might also want to make it possible to use this API in conjunction with the @use resources decorator, but really the only thing it would do is allow you to omit that first this arg that you have to pass to task():

class DemoComponent {
  foo = 5;
  @use doStuffTask = task({ restartable: true }, async () => {
    await timeout(500);
    this.foo++;
  });
}

I prefer we land the this-based version first since it relies less on Ember-specific machinery.

Issue Analytics

  • State:closed
  • Created a year ago
  • Reactions:9
  • Comments:17 (10 by maintainers)

github_iconTop GitHub Comments

1reaction
gnclmoraiscommented, Apr 9, 2022

I think @taskFn actually looks pretty good and then fn piece might be enough to provide hints as to when/where to use it.

Same, I like this proposal — thank you for coming up with it! 🎊

0reactions
machtycommented, Aug 24, 2022

#465 implemented proposition 4, which I think is all we need at this point.

Read more comments on GitHub >

github_iconTop Results From Across the Web

chancancode/ember-concurrency-async - GitHub
3.0 introduces a new async arrow function task() API (along with codemods to automatically convert your code to the new style) which is...
Read more >
Machty Ember-Concurrency Statistics & Issues - Codesti
RFC : absorb ember-concurrency-async and standardize around async functions, closed, 17, 2022-04-07, 2022-12-14 ; Propagate resetState to canceled child tasks ...
Read more >
FluentBit 2.15.0 fails to refresh credentials after 6 hours
RFC : absorb ember-concurrency-async and standardize around async functions, 17, 2022-04-07, 2022-10-07. Player is able to put bullets when game is paused ...
Read more >
ember-concurrency - Github Plus
But I can't seem to get the return type from the perform call to be generic, ... RFC: absorb ember-concurrency-async and standardize around...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found