Skip to content

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
rust
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.

TIP

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
rust
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)?;
    let output = u32::try_from(output)
      .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))?;
    env.create_uint32(output)
  }
  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
rust
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> {
    let output = u32::try_from(output)
      .map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))?;
    env.create_uint32(output)
  } 

  fn finally(&mut self, env: Env) -> Result<()> {
    self.data.unref(env)?;
    Ok(())
  }
}

TIP

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.

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

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

index.d.ts
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
rust
use napi::bindgen_prelude::AbortSignal;

#[napi]
fn async_fib(input: u32, signal: AbortSignal) -> AsyncTask<AsyncFib> { 
  AsyncTask::with_signal(AsyncFib { input }, signal)
}

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

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

Se você chamar AbortController.abort antes que o libuv inicie a AsyncTask, o Node-API pode cancelar o trabalho enfileirado e a Promise rejeita com um erro cujo name é AbortError.

test.mjs
js
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
rust
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
ts
export function asyncFib(
  input: number,
  signal?: AbortSignal | undefined | null,
): Promise<number>

TIP

Se AsyncTask já tiver iniciado, o Node-API não pode cancelar seu callback compute; se tiver concluído, o resultado da Promise já está definido. Os callbacks AbortSignal::on_abort registrados em Rust ainda executam quando o signal JavaScript dispara, mesmo quando é tarde demais para cancelar. O adaptador atual instala seu handler ao converter o argumento e não consulta signal.aborted, portanto passe um signal que ainda não tenha sido abortado. Ele também atribui signal.onabort, substituindo qualquer handler armazenado nessa propriedade; use signal.addEventListener('abort', ...) para listeners JavaScript independentes.