A shared TaskSignal
represents a relationship between two tasks, and two tasks
that share a signal can be reprioritized together. A frequent request we see
from web developers pertaining to scheduling is the desire to inherit or
propagate the currently running task's priority. Consider the following example:
function asyncSubtask() {}
function task1() {
// ... do task1 work ...
scheduler.postTask(asyncSubtask, { priority: ?? });
// ... do more work ...
}
scheduler.postTask(task1, { priority: 'background' });
When task1
runs, it might be the case that either or both of the following hold:
- asyncSubtask should be have the same priority as task1.
- asyncSubtask should be canceled if task1 is aborted.
TaskSignals
can solve both (1) and (2), but in the MVP API they need to be
explicitly passed to any function that might need them, which can be onerous.
Any function that needs the current signal would need to be modified to take an argument. The example can be rewritten as follows:
function asyncSubtask() {}
function task1(signal) {
// ... do task1 work ...
scheduler.postTask(asyncSubtask, { signal });
// ... do more work ...
}
const controller = new TaskController({priority: 'background'});
scheduler.postTask(task1, { signal }, signal);
Rather than forcing developers to explicitly pass signals everywhere, we are
proposing to expose the current TaskSignal
so it can be inherited across async
boundaries. The current TaskSignal
is propagated through postTask
and
through any Promise chains that begin inside of a postTask
task, but will not
propagate through other callbacks (e.g., asynchronous event handlers,
setTimeout
, etc.). When there is no current TaskSignal
,
scheduler.currentTaskSignal
will return a default TaskSignal
, representing
a default priority with no way to cancel the task.
For example:
function asyncSubtask() {}
function task1() {
// ... do task1 work ...
// Inherit the current signal.
let res = scheduler.postTask(asyncSubtask, {signal: scheduler.currentTaskSignal});
res.then(() => {
// The currentTaskSignal is retained across this async boundary as well,
// i.e. when the microtask runs.
scheduler.postTask(asyncSubtask, {signal: scheduler.currentTaskSignal});
});
// ... do more work ...
}
const controller = new TaskController({priority: 'background'});
scheduler.postTask(task1, { signal });
If the priority doesn't need to be changed, i.e. a TaskController
wasn't
created, a special scheduler.currentTaskSignal
can be created so that
propagation can still occur:
function asyncSubtask() {}
function task1() {
// logs 'background'.
console.log(scheduler.currentTaskSignal.priority);
}
scheduler.postTask(task1, { priority: 'background' });
It is also possible to mix and match priorities and TaskSignals
.
In this example, if controller.abort()
is called before asyncSubtask()
runs, it will still be cancelled, even though it is running at a different
priority than controller.signal.priority
. This is accomplished by creating an
implicit TaskSignal
with no associated TaskController
instead of inheriting
controller.signal
in its entirety. This implicit TaskSignal
follows the
controller.signal
for abort, but has its own priority that does not change
when controller.signal.priority
changes.
function asyncSubtask() {
// logs 'background'.
console.log(scheduler.currentTaskSignal.priority);
}
function task1() {
// ... do task1 work ...
scheduler.postTask(asyncSubtask,
{signal: scheduler.currentTaskSignal, priority: 'background'});
// ... do more work ...
}
const controller = new TaskController({priority: 'user-blocking'});
scheduler.postTask(task1, { controller.signal });