Skip to main content

Spawn

Suppose we are using the fetchWeekDay function from the introduction to fetch the current weekday in multiple timezones:

import { main } from 'effection';
import { fetchWeekDay } from './fetch-week-day';

main(function*() {
let dayUS = yield fetchWeekDay('est');
let daySweden = yield fetchWeekDay('cet');
console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
});

This works, but it slightly inefficient because we are running the fetches one after the other. How can we run both fetch operations at the same time?

Using async/await

If we were just using async/await and not using Effection, we might do something like this to fetch the dates at the same time:

async function() {
let dayUS = fetchWeekDay('est');
let daySweden = fetchWeekDay('cet');
console.log(`It is ${await dayUS}, in the US and ${await daySweden} in Sweden!`);
}

Or we could use a combinator such as Promise.all:

async function() {
let [dayUS, daySweden] = await Promise.all([fetchWeekDay('est'), fetchWeekDay('cet')]);
console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
}

Cancellation

This works fine as long as both fetches complete successfully, but what happens when one of them fails? Since there is no connection between the two tasks, a failure in one of them has no effect on the other. We will happily keep trying to fetch the US date, even when fetching the Swedish date has already failed!

For fetch, the consequences of this are not so severe, the worst that happens is that we have a request which is running longer than necessary, but you can imagine that the more complex the operations we're trying to combine, the more opportunity for problems there are.

We are calling these situations "dangling promises", and most significantly complex JavaScript applications suffer from this problem. async/await fundamentally does not handle cancellation very well when running multiple operations concurrently.

Effection

How does Effection deal with this situation? If we wrote the example using Effection in the exact same way as the async/await example, then we will find that it doesn't behave the same:

import { main } from 'effection';
import { fetchWeekDay } from './fetch-week-day';

main(function*() {
let dayUS = fetchWeekDay('est');
let daySweden = fetchWeekDay('cet');
console.log(`It is ${yield dayUS}, in the US and ${yield daySweden} in Sweden!`);
});

This is still running one fetch after the other, and is not fetching both at the same time!

To understand why, remember that calling a generator function does not do anything by itself, only by passing the generator to yield or run do we actually run the generator. So only when we yield to we actually start fetching the dates.

We could use run here to run our operations, and then wait for them, but this is not the correct way:

// THIS IS NOT THE CORRECT WAY!
import { main, run } from 'effection';
import { fetchWeekDay } from './fetch-week-day';

main(function*() {
let dayUS = run(fetchWeekDay('est'));
let daySweden = run(fetchWeekDay('cet'));
console.log(`It is ${yield dayUS}, in the US and ${yield daySweden} in Sweden!`);
});

This has the same problem as our async/await example: a failure in one fetch has no effect on the other!

Introducing spawn

The spawn operation is Effection's solution to this problem!

import { main, spawn } from 'effection';
import { fetchWeekDay } from './fetch-week-day';

main(function*() {
let dayUS = yield spawn(fetchWeekDay('est'));
let daySweden = yield spawn(fetchWeekDay('cet'));
console.log(`It is ${yield dayUS}, in the US and ${yield daySweden} in Sweden!`);
});

Like run and main, spawn takes an Operation and returns a Task. The difference is that this Task becomes a child of the current Task. This means it is impossible for this task to outlive its parent. And it also means that an error in the task will cause the parent to fail.

You can think of this as creating a hierarchy like this:

+-- main
|
+-- fetchWeekDay('est')
|
+-- fetchWeekDay('cet')

When fetchWeekDay('cet') fails, since it was spawned by main, it will also cause main to fail. When main fails it will make sure that none of its children outlive it, and it will halt all of its remaining children. We end up with a situation like this:

+-- main [FAILED]
|
+-- fetchWeekDay('est') [HALTED]
|
+-- fetchWeekDay('cet') [FAILED]

Effection tasks are tied to the lifetime of their parent, and it becomes impossible to create a task whose lifetime is undefined. Additionally, the behaviour of errors is very clearly defined. An error in a child will also cause the parent to error, which in turn halts any siblings.

This idea is called structured concurrency, and it has profound effects on the composability of concurrent code.

Using combinators

We previously showed how we can use the Promise.all combinator to implement the concurrent fetch. Effection also ships with some combinators, for example we can use the all combinator:

import { all, main } from 'effection';

main(function *() {
let [dayUS, daySweden] = yield all([fetchWeekDay('est'), fetchWeekDay('cet')]);
console.log(`It is ${dayUS}, in the US and ${daySweden} in Sweden!`);
});

Direct spawning

Another way of spawning tasks is to call the spawn method on a task:

let task = run();
task.spawn(fetchWeekDay('est'));
task.spawn(fetchWeekDay('cet'));

This is often useful when integrating Effection into existing promise or callback based frameworks.

When an Operation is a generator function, the first argument to the generator function is the current task. We can use this to spawn tasks:

run(function*(task) {
task.spawn(fetchWeekDay('est'));
task.spawn(fetchWeekDay('cet'));
});

This is basically the same as the previous example.