Task Modifiers

Starting with ember-concurrency v2.2.0, public Task Modifier APIs have been added to allow for custom task modifiers to be registered from third-party addons and application code to provide behavior for specific situations not suited to inclusion within ember-concurrency's core itself.

Task modifiers have been a concept built-in to `ember-concurrency` since the beginning. However, until `0.7.19` they were only specifyable within `ember-concurrency` internals, and not extendable by users. `0.7.19` added the ability to specify new modifiers as prototype extensions on `TaskProperty`. Unfortunately, `TaskProperty` is inherently tied to Ember internals and is not used when using decorators, and using prototype extensions does not make clear what modifiers exist, so the `getModifier`, `hasModifier`, and `registerModifier` APIs were introduced to provide a way to manage modifiers and make them discoverable.

Defining a custom task modifier

Let's say we want to build a benchmarking modifier to help us get a sense for how long our tasks are running.

// app/task-modifiers/benchmark.js
import { registerModifier } from 'ember-concurrency';

function benchmarkModifier(taskFactory, option) {
  if (!window && !window.performance) {
    return;
  }

  if (option) {
    let taskDefinition = taskFactory.taskDefinition;
    let benchmarkedDefinition = function* (...args) {
      let taskName = taskFactory.name;
      let namespace = `ember-concurrency.${taskName}`;
      window.performance.mark(`${namespace}.start`);

      try {
        yield* taskDefinition(...args);
        window.performance.measure(
          `${namespace}.success`,
          `${namespace}.start`
        );
      } catch (e) {
        window.performance.measure(`${namespace}.error`, `${namespace}.start`);
        throw e;
      } finally {
        window.performance.measure(
          `${namespace}.runtime`,
          `${namespace}.start`
        );
      }
    };

    taskFactory.setTaskDefinition(benchmarkedDefinition);
  }
}

registerModifier('benchmark', benchmarkModifier);

export default benchmarkModifier;

Now that we have a new modifier defined, we can apply it to any tasks that we wish and have it apply the behavior we built. Let's see it in action!

<button {{on "click" (perform this.doWork)}} type="button">
  Benchmark task
</button>

{{#if this.doWork.isRunning}}
  Running benchmark...
{{/if}}

<ol>
  {{#each this.perfEntries as |entry|}}
    <li>Start time: {{entry.startTime}}ms after page-load; duration = {{entry.duration}}ms</li>
  {{/each}}
</ol>
import { task, timeout } from 'ember-concurrency';

// registerModifer is called in the module defining the modifier,
// so we're really just importing it here for the side-effect. This is mostly for
// terseness in this illustration. You may want to seperate defining the modifier
// and registering it with registerModifier, and be explicit about where you
// register (e.g. addon, library, or app initialization)
import 'dummy/task-modifiers/benchmark';

let performance =
  typeof window !== 'undefined' && window.performance
    ? window.performance
    : { getEntriesByName() {} };

export default class TaskModifiersController extends Controller {
  @task({ drop: true, benchmark: true })
  *doWork() {
    yield timeout(20000 * Math.random());
  }

  @computed('doWork.isRunning')
  get perfEntries() {
    if (this.doWork.isRunning) {
      return [];
    } else {
      return performance.getEntriesByName(
        'ember-concurrency.doWork.runtime',
        'measure'
      );
    }
  }
}

More about Task Modifiers

The behavior and implementation of task modifier APIs are defined further in the Task Modifiers RFC. Additionally, the API for TaskFactory is documented here.