Task Lifecycle
We have talked about how Effection operations are able to clean up after themselves, but how are they able to do this, and how can you implement your own operations which clean up after themselves?
Halt
In order to understand the lifecycle of a Task, we must first understand the concept of halting a Task.
In Effection, any task can be halted:
import { main } from 'effection';
let task = main(function*() {
yield
});
task.halt();
Or:
main(function*() {
let task = yield spawn();
yield task.halt();
});
Halting a Task means that the task itself is cancelled, it also causes any Task that has been spawned from the Task to be halted.
We have previously mentioned that when an error occurs in a Task, the task becomes errored, and also causes its parent to become errored. However, if a Task is halted, the parent task is unaffected.
Return
If a Task is driving a generator, we call return()
on the generator. This
behaves somewhat similarly to if you would replace the yield
statement with a
return
statement.
Let's look at an example where a task is suspended using yield
with no
arguments and what happens when we call halt
on it:
import { main } from 'effection';
let task = main(function*() {
yield; // we will "return" from here
console.log('we will never get here');
});
task.halt();
This would behave somewhat similarly to the following:
import { main } from 'effection';
main(function*() {
return;
console.log('we will never get here');
});
Crucially, when this happens, just like with a regular return
, we can use try/finally
:
import { main, sleep } from 'effection';
let task = main(function*() {
try {
yield // we will "return" from here
} finally {
console.log('yes, this will be printed!');
}
});
task.halt();
Cleaning up
We can use this mechanism to run code as a Task is shutting down, whether it happens because the Task completes successfully, it becomes halted, or it is rejected due to an error.
Imagine that we're doing something with an HTTP server, and we're using node's
createServer
function. In order to properly clean up after ourselves, we
should call close()
on the server when we're done.
Using Effection and try/finally
, we could do something like this:
import { main } from 'effection';
import { createServer } from 'http';
let task = main(function*() {
let server = createServer();
try {
// in real code we would do something more interesting here
yield;
} finally {
server.close();
}
});
task.halt();
Asynchronous halt
You might be wondering what happens when we yield
in a finally block. In
fact, Effection handles this case for you:
import { main, sleep } from 'effection';
let task = main(function*() {
try {
yield;
} finally {
console.log('this task is slow to halt');
yield sleep(2000);
console.log('now it has been halted');
}
});
task.halt();
While performing asynchronous operations while halting is sometimes necessary, it is good practice to keep halting speedy and simple. We recommend avoiding expensive operations during halt where possible, and avoiding throwing any errors during halting.
Ensure
Sometimes you want to avoid the rightward drift of using lots of try/finally
blocks.
The ensure
operation that ships with Effection can help you clean up this type of code.
The following behaves identically to our try/finally
implementation above:
import { main, ensure } from 'effection';
import { createServer } from 'http';
let task = main(function*() {
let server = createServer();
yield ensure(() => server.close());
// in real code we would do something more interesting here
yield;
});
task.halt();
Abort Signal
While cancellation and teardown is handled automatically for us as long as we
are using Effection operations, what do we do when we want to integrate with a
3rd party API? One very common answer is to use the JavaScript standard
AbortSignal
which can broadcast an event whenever it is time for an operation to be
cancelled. Effection makes it easy to create abort signals, and pass them around
so that they can notify dependencies whenever an operation terminates.
To create an abort signal, we use the createAbortSignal
that comes with
Effection.
AbortSignal
s instantiated with the createAbortSignal()
operation are
implicitly bound to the task in which they were created, and whenever that task
ceases running, they will emit an abort
event.
import { main, sleep, createAbortSignal } from 'effection';
main(function*() {
let signal = yield createAbortSignal();
signal.addEventListener('abort', () => console.log('done!'));
yield sleep(5000);
// prints 'done!'
});
It is very common (though not universal) that APIs which perform
asynchronous operations will accept an AbortSignal
in order to make
sure those operations go away if needed. For example, the standard
fetch
function
accepts an abort signal to cancel itself when needed.
function* request(url) {
let signal = yield createAbortSignal();
let response = yield fetch('/some/url', { signal });
if (response.ok) {
return yield response.text();
} else {
throw new Error(`failed: ${ response.status }: ${response.statusText}`);
}
}
Now, no matter what happens, when the request
operation is completed (or
cancelled), the HTTP request is guaranteed to be shut down.
Lifecycle
The current state of a Task can be accesses through the state
property:
import { main } from 'effection';
let task = main(function*() {
yield;
});
console.log(`task is ${task.state}`) // prints "task is running"
The state
can be any of:
- pending: the Task has not yet been started
- running: the Task is currently running
- completing: the Task has completed and is in the process of halting its children
- halting:
halt
has been called on the Task and it is in the process of halting itself and its children - erroring: an error has occurred in the Task or any of its children. The task is being halted, as well as its children.
- completed: the Task is fully complete and all of its children have been halted
- halted: the Task is fully halted and all of its children have been halted as well
- errored: an error has occurred in the Task or any of its children. The task is fully halted and all of its children have been halted as well.