AsyncTask
We need to talk about Task
before talking about AsyncTask
.
Task
Addon modules often need to leverage async helpers from libuv as part of their implementation. This allows them to schedule work to be executed asynchronously so that their methods can return in advance of the work being completed. This allows them to avoid blocking the overall execution of the Node.js application.
The Task
trait provides a way to define such an asynchronous task that needs to run in the libuv thread. You can implement the compute
method, which will be called in the libuv thread.
use napi::{Task, Env, Result, JsNumber};
struct AsyncFib {
input: u32,
}
impl Task for AsyncFib {
type Output = u32;
type JsValue = JsNumber;
fn compute(&mut self) -> Result<Self::Output> {
Ok(fib(self.input))
}
fn resolve(&mut self, env: Env, output: u32) -> Result<Self::JsValue> {
env.create_uint32(output)
}
}
fn compute
ran on the libuv thread, you can run some heavy computation here, which will not block the main JavaScript thread.
You may notice there are two associated types on the Task
trait. The type Output
and the type JsValue
. Output
is the return type of the compute
method. JsValue
is the return type of the resolve
method.
We need separate type Output
and type JsValue
because we can not call the
JavaScript function back in fn compute
, it is not executed on the main
thread. So we need fn resolve
, which runs on the main thread, to create the
JsValue
from Output
and Env
and call it back in JavaScript.
You can use the low-level API Env::spawn
to spawn a defined Task
in the libuv thread pool. See example in Reference.
In addition to compute
and resolve
, you can also provide reject
method to do some clean up when Task
runs into error, like unref
some object:
struct CountBufferLength {
data: Ref<JsBufferValue>,
}
impl CountBufferLength {
pub fn new(data: Ref<JsBufferValue>) -> Self {
Self { data }
}
}
impl Task for CountBufferLength {
type Output = usize;
type JsValue = JsNumber;
fn compute(&mut self) -> Result<Self::Output> {
if self.data.len() == 10 {
return Err(Error::from_reason("len can't be 10".to_string()));
}
Ok((&self.data).len())
}
fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
self.data.unref(env)?;
env.create_uint32(output as _)
}
fn reject(&mut self, env: Env, err: Error) -> Result<Self::JsValue> {
self.data.unref(env)?;
Err(err)
}
}
You can also provide a finally
method to do something after Task
is resolved
or rejected
:
struct CountBufferLength {
data: Ref<JsBufferValue>,
}
impl CountBufferLength {
pub fn new(data: Ref<JsBufferValue>) -> Self {
Self { data }
}
}
#[napi]
impl Task for CountBufferLength {
type Output = usize;
type JsValue = JsNumber;
fn compute(&mut self) -> Result<Self::Output> {
if self.data.len() == 10 {
return Err(Error::from_reason("len can't be 5".to_string()));
}
Ok((&self.data).len())
}
fn resolve(&mut self, env: Env, output: Self::Output) -> Result<Self::JsValue> {
env.create_uint32(output as _)
}
fn finally(&mut self, env: Env) -> Result<()> {
self.data.unref(env)?;
Ok(())
}
}
The #[napi]
macro above the impl Task for AsyncFib
is just for .d.ts
generation. If no #[napi]
is defined here, the generated TypeScript type of returned AsyncTask
will be Promise<unknown>
.
AsyncTask
The Task
you define cannot be returned to JavaScript directly, the JavaScript engine has no idea how to run and resolve the value from your struct
. AsyncTask
is a wrapper of Task
which can return to the JavaScript engine. It can be created with Task
and an optional AbortSignal
(opens in a new tab).
#[napi]
fn async_fib(input: u32) -> AsyncTask<AsyncFib> {
AsyncTask::new(AsyncFib { input })
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
export function asyncFib(input: number) => Promise<number>
Create AsyncTask
With AbortSignal
In some scenarios, you may want to abort the queued AsyncTask
, for example, using debounce
on some compute tasks. You can provide AbortSignal
to AsyncTask
, so that you can abort the AsyncTask
if it has not been started.
use napi::bindgen_prelude::AbortSignal;
#[napi]
fn async_fib(input: u32, signal: AbortSignal) -> AsyncTask<AsyncFib> {
AsyncTask::with_signal(AsyncFib { input }, signal)
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
export function asyncFib(input: number, signal: AbortSignal) => Promise<number>
If you invoke AbortController.abort
in the JavaScript code and the AsyncTask
has not been started yet, the AsyncTask
will be aborted immediately, and reject with AbortError
.
import { asyncFib } from './index.js'
const controller = new AbortController()
asyncFib(20, controller.signal).catch((e) => {
console.error(e) // Error: AbortError
})
controller.abort()
You can also provide Option<AbortSignal>
to AsyncTask
if you don't know if the AsyncTask
needs to be aborted:
use napi::bindgen_prelude::AbortSignal;
#[napi]
fn async_fib(input: u32, signal: Option<AbortSignal>) -> AsyncTask<AsyncFib> {
AsyncTask::with_optional_signal(AsyncFib { input }, signal)
}
⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
export function asyncFib(
input: number,
signal?: AbortSignal | undefined | null,
): Promise<number>
If AsyncTask
has already been started or completed, the
AbortController.abort
will have no effect.