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!`);
}
Dangling Promises
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 call 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 unlike calling an async function to
create a Promise
, calling a generator function to create an
Operation
does not do anything by itself. An Operation
is
is only evaluated when passed to yield*
or to run()
. Therefore it isn't
until we yield*
do we actually start to fetch 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. Because of this, 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!`);
});
Spawning in a Scope.
The spawn()
operation always runs its operation as a child of the current
operation. Sometimes however, you might want to run an operation as a child of a
different operation. To do this we can use the Scope#run()
method.
This is often useful when integrating Effection into existing promise or
callback based frameworks. The following example creates a trivial
express
server that runs each request as an operation that is
a child of the main operation.
import express from 'express';
import { main, suspend, useScope } from "effection";
await main(function*() {
let scope = yield* useScope();
let app = express();
app.get("/", async (_, res) => {
await scope.run(function*() {
res.send("Hello World!");
})
});
let server = app.listen();
try {
yield* suspend()
} finally {
server.close();
}
});
We capture a reference to the main operation's scope in line (5) and then use that scope to run the request as an async function on line (8).
You can learn more about this in the scope guide.