Encapsulated Tasks

Normally, you define tasks by decorating a generator function like @task *myGeneratorFn() { /* ... */ }. Occasionally, you may want to be able to expose additional state of the task, e.g. you might want to show the percentage progress of an uploadFile task, but unless you're using the techniques described below there's no good place to expose that data to the template other than to set some properties on the host object, but then you lose a lot of the benefits of encapsulation in the process.

In cases like these, you can use Encapsulated Tasks, which behave just like regular tasks, but with one crucial difference: the value of this within the task function points to the currently running TaskInstance, rather than the host object that the task lives on (e.g. a Component, Controller, etc). This allows for some nice patterns where all of the state produced/mutated by a task can be contained (encapsulated) within the Task itself, rather than having to live on the host object.

To create an encapsulated task, decorate an object (instead of a generator function) with the @task decorator that defines a perform generator function. The object can also contain initial values for task state, as well as computed properties and anything else supported by classic Ember objects.

import { task } from 'ember-concurrency';

export default class EncapsulatedTaskComponent extends Component {
  outerFoo = 123;

  @task *regularTask(value) {
    // this is a classic/regular ember-concurrency task,
    // which has direct access to the host object that it
    // lives on via `this`
    console.log(this.outerFoo); // => 123
    yield doSomeAsync();
    this.set('outerFoo', value);
  }

  @task encapsulatedTask = {
    innerFoo: 456,

    // this `*perform() {}` syntax is valid JavaScript shorthand
    // syntax for `perform: function * () {}`

    *perform(value) {
      // this is an encapulated task. It does NOT have
      // direct access to the host object it lives on, but rather
      // only the properties defined within the POJO passed
      // to the `task()` constructor.
      console.log(this.innerFoo); // => 456

      // `this` is the currently executing TaskInstance, so
      // you can also get classic TaskInstance properties
      // provided by ember-concurrency.
      console.log(this.isRunning); // => true

      yield doSomeAsync();
      this.set('innerFoo', value);
    },
  }
}

Live Example

This example demonstrates how to use encapsulated tasks to model file uploads. It keeps all of the upload state within each TaskInstance, and uses Derived State to expose the values set within the encapsulated tasks.

Queued Uploads: 0
Uploading to (): %
import { task, timeout } from 'ember-concurrency';

export default class EncapsulatedTaskController extends Controller {
  @task({ enqueue: true }) uploadFile = {
    progress: 0,
    url: null,

    stateText: computed('progress', function () {
      let progress = this.progress;
      if (progress < 49) {
        return 'Just started...';
      } else if (progress < 100) {
        return 'Halfway there...';
      } else {
        return 'Done!';
      }
    }),

    *perform(makeUrl) {
      this.set('url', makeUrl());

      while (this.progress < 100) {
        yield timeout(200);
        let newProgress = this.progress + Math.floor(Math.random() * 6) + 5;
        this.set('progress', Math.min(100, newProgress));
      }

      return '(upload result data)';
    },
  };

  makeRandomUrl() {
    return `https://www.${randomWord()}.edu`;
  }
}
<p>
  <button {{on "click" (perform this.uploadFile this.makeRandomUrl)}} type="button">
    Start Upload
  </button>
</p>

<h5>Queued Uploads: {{this.uploadFile.numQueued}}</h5>

{{#let this.uploadFile.last as |encapsTask|}}
  <h5>
    Uploading to {{encapsTask.url}} ({{encapsTask.stateText}}):
    {{encapsTask.progress}}%
  </h5>
{{/let}}

{{#if this.uploadFile.lastSuccessful}}
  <h5 style="color: green;" {{! template-lint-disable no-inline-styles }}>
    <strong>
      Upload to {{this.uploadFile.lastSuccessful.url}}:
      {{this.uploadFile.lastSuccessful.value}}
    </strong>
  </h5>
{{/if}}