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 5".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 defined 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 created with Task and an optional AbortSignal.

#[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.