Documentação
Conceitos
AsyncTask

AsyncTask

Precisamos falar sobre Task antes de falar sobre AsyncTask.

Task

Os módulos de complemento geralmente precisam aproveitar os ajudantes assíncronos do libuv como parte de sua implementação. Isso lhes permite agendar o trabalho para ser executado de forma assíncrona, para que seus métodos possam retornar antecipadamente antes que o trabalho seja concluído. Isso permite evitar o bloqueio da execução geral da aplicação Node.js.

O trait Task fornece uma maneira de definir uma tarefa assíncrona que precisa ser executada na thread do libuv. Você pode implementar o método compute , que será chamado na thread do libuv.

lib.rs
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 é executado na thread do libuv, você pode executar algum cálculo pesado aqui, o que não bloqueará a thread JavaScript principal.

Você pode notar que existem dois tipos associados no trait Task. O type Output e o type JsValue. Output é o tipo de retorno do método compute. JsValue é o tipo de retorno do método resolve.

💡

Precisamos de type Output e type JsValue separados porque não podemos chamar a função JavaScript de volta em fn compute, pois ela não é executada na thread principal. Portanto, precisamos de fn resolve, que é executado na thread principal, para criar o JsValue a partir de Output e Env e chamá-lo de volta em JavaScript.

Você pode usar a API de baixo nível Env::spawn para iniciar uma Task definida no pool de threads libuv. Veja um exemplo na Referência.

Além de compute e resolve, você também pode fornecer o método reject para fazer alguma limpeza quando a Task apresenta erro, como unref algum objeto:

lib.rs
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)
  }
}

Você também pode fornecer um método finally para fazer algo depois que a Task for resolved ou rejected:

lib.rs
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(())
  }
}
💡

O #[napi] macro acima do impl Task for AsyncFib é apenas para a geração do .d.ts. Se nenhum #[napi] for definido aqui, o tipo TypeScript gerado para a AsyncTask retornada será Promise<unknown>.

AsyncTask

A Task que você define não pode ser retornada diretamente para JavaScript, o mecanismo do JavaScript não sabe como executar e resolver o valor da sua struct. AsyncTask é um wrapper de Task que pode ser retornado ao mecanismo do JavaScript. Ele pode ser criado com Task e um opcional AbortSignal (opens in a new tab).

lib.rs
#[napi]
fn async_fib(input: u32) -> AsyncTask<AsyncFib> {
  AsyncTask::new(AsyncFib { input })
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export function asyncFib(input: number) => Promise<number>

Criar AsyncTask com AbortSignal

Em alguns cenários, pode ser desejável interromper a AsyncTask, na fila, por exemplo, usando debounce em algumas tarefas de cálculo. Você pode fornecer um AbortSignal para a AsyncTask, para que possa interromper a AsyncTask se ainda não tiver sido iniciada.

lib.rs
use napi::bindgen_prelude::AbortSignal;
 
#[napi]
fn async_fib(input: u32, signal: AbortSignal) -> AsyncTask<AsyncFib> {
  AsyncTask::with_signal(AsyncFib { input }, signal)
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export function asyncFib(input: number, signal: AbortSignal) => Promise<number>

Se você chamar AbortController.abort no código JavaScript e a AsyncTask ainda não tiver sido iniciada, a AsyncTask será abortada imediatamente e rejeitada com AbortError.

test.mjs
import { asyncFib } from './index.js'
 
const controller = new AbortController()
 
asyncFib(20, controller.signal).catch((e) => {
  console.error(e) // Error: AbortError
})
 
controller.abort()

Você também pode fornecer Option<AbortSignal> para AsyncTask se não souber se a AsyncTask precisa ser abortada:

lib.rs
use napi::bindgen_prelude::AbortSignal;
 
#[napi]
fn async_fib(input: u32, signal: Option<AbortSignal>) -> AsyncTask<AsyncFib> {
  AsyncTask::with_optional_signal(AsyncFib { input }, signal)
}

⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️

index.d.ts
export function asyncFib(
  input: number,
  signal?: AbortSignal | undefined | null,
): Promise<number>
💡

Caso a AsyncTask já tenha sido iniciada ou concluída, o AbortController.abort não terá efeito.