If you don’t know ember-concurrency…Recommended reading: This article by Alex Matchneer. Play with these examples.
Right, for those who want to crack on, let’s get into it!
Not Everything Is as It Seems
Nominally, the .drop() task modifier in ember-concurrency indicates that no more than one instance of a task may ever be running once at a time. However, you might have missed this crucial sentence from a page deep down in the Route Tasks page of the documentation:
“ember-concurrency tasks are scoped to the lifetime of the object they live on, so if that object is destroyed, all of the tasks attached to it are cancelled.”
The important takeaway here is the first bit: “ember-concurrency tasks are scoped to the lifetime of the object they live on”. i.e. ember-concurrency tasks are scoped to object instances, not classes.
As you’ll see below, you can actually have multiple instances of a task running simultaneously, despite having applied a modifier (drop, enqueue, restartable, keepLatest) to prevent that from happening. This applies to all of the task modifiers, but we’ll just stick with drop in our examples to keep things simple.
Take the following component (view the full twiddle example here):
import {task, timeout} from 'ember-concurrency'
export default Ember.Component.extend({
someTask: task(function * (id) {
console.log('Start', id)
yield timeout(1000)
console.log('Done', id)
}).drop()
})
<button onclick={{perform someTask componentName}}>Button {{componentName}}</button>
{{if someTask.isRunning 'Running'}}
{{my-component componentName="One"}}
<br>
{{my-component componentName="Two"}}
What’s Going On?
In the application.hbs template, we’ve rendered out the component twice. Since ember-concurrency tasks are scoped to instances (in this case, instances of a component), each rendered component gets its own, unique instance of myTask .
Each instance of myTask keeps its own completely independent state, and that enables us to perform myTask on both components simultaneously. To demonstrate: if we were to double-click Button One we see the text Running pop up next to Button One (but not Button Two) and we only see a single pair of logs in the console:
- Start One
- Done One
That’s ember-concurrency doing its job for us by preventing the task from being run a second time while the first is still running.
However, the same rule does not apply when we click Button Two immediately after Button One. When doing that, we see Running show up next to both buttons, and the following in the console:
- Start One
- Start Two
- Done One
- Done Two
Enter the Singleton
If necessary, we can get around this duplication effect by defining our tasks in singletons. In Ember, routes, controllers and services are all singletons, meaning only a single instance of any given route, controller, or service is created. Once created, they are never destroyed.
The obvious connection to ember-concurrency tasks is: if I had defined myTask on a route instead of in a component, it would not have been duplicated. In that case, we truly would only ever have one instance of myTask running at a time, regardless of how many times or combinations of buttons we pressed.
And there you have it! This behaviour by ember-concurrency is definitely a sane default and likely the most commonly used behaviour. On the rare occasion you do need multiple sibling components to share task state, it could cause some unexpected bugs in your application if you’re not paying attention.
Banner image: Photo by Aleksandr Popov on Unsplash