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.
use napi::bindgen_prelude::*;
use napi_derive::napi;
fn fib(n: u32) -> u32 {
if n <= 1 {
return n;
}
fib(n - 1) + fib(n - 2)
}
pub struct AsyncFib {
input: u32,
}
#[napi]
impl Task for AsyncFib {
type Output = u32;
type JsValue = u32;
fn compute(&mut self) -> Result<Self::Output> {
Ok(fib(self.input))
}
fn resolve(&mut self, _: Env, output: u32) -> Result<Self::JsValue> {
Ok(output)
}
}
#[napi]
pub fn async_fib(input: u32) -> AsyncTask<AsyncFib> {
AsyncTask::new(AsyncFib { input })
}
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:
use napi::bindgen_prelude::*;
use napi_derive::napi;
pub struct CountBufferLength {
data: Buffer,
}
impl CountBufferLength {
pub fn new(data: Buffer) -> Self {
Self { data }
}
}
impl Task for CountBufferLength {
type Output = usize;
type JsValue = u32;
fn compute(&mut self) -> Result<Self::Output> {
if self.data.len() == 10 {
return Err(Error::new(
Status::GenericFailure,
"Random fatal error".to_string(),
));
}
Ok((&self.data).len())
}
fn resolve(&mut self, _: Env, output: Self::Output) -> Result<Self::JsValue> {
u32::try_from(output)
.map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))
}
fn reject(&mut self, _: Env, err: Error) -> Result<Self::JsValue> {
// catch the error
if err.status == Status::GenericFailure {
Ok(0)
} else {
Ok(1)
}
}
}
#[napi]
pub fn async_count_buffer_length(data: Buffer) -> AsyncTask<CountBufferLength> {
AsyncTask::new(CountBufferLength { data })
}
Você também pode fornecer um método finally para fazer algo depois que a Task for resolved ou rejected:
use napi::bindgen_prelude::*;
use napi_derive::napi;
pub struct CountBufferLength {
data: Buffer,
}
impl CountBufferLength {
pub fn new(data: Buffer) -> Self {
Self { data }
}
}
impl Task for CountBufferLength {
type Output = usize;
type JsValue = u32;
fn compute(&mut self) -> Result<Self::Output> {
if self.data.len() == 10 {
return Err(Error::new(
Status::GenericFailure,
"Random fatal error".to_string(),
));
}
Ok((&self.data).len())
}
fn resolve(&mut self, _: Env, output: Self::Output) -> Result<Self::JsValue> {
u32::try_from(output)
.map_err(|_| Error::new(Status::InvalidArg, "buffer length exceeds u32"))
}
fn reject(&mut self, _: Env, err: Error) -> Result<Self::JsValue> {
// catch the error
if err.status == Status::GenericFailure {
Ok(0)
} else {
Ok(1)
}
}
fn finally(self, _: Env) -> Result<()> {
println!("finally");
drop(self.data);
Ok(())
}
}
#[napi]
pub fn async_count_buffer_length(data: Buffer) -> AsyncTask<CountBufferLength> {
AsyncTask::new(CountBufferLength { data })
}
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.
#[napi]
fn async_fib(input: u32) -> AsyncTask<AsyncFib> {
AsyncTask::new(AsyncFib { input })
}
⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
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 AsyncTask, para que possa interromper a AsyncTask se ainda não tiver sido iniciada.
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>
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.
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:
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>
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.
ScopedTask
ScopedTask é praticamente igual a Task, mas passa &'env Env para os métodos resolve e reject, de modo que você pode criar um JsValue com lifetime a partir do &'env Env.
Por exemplo:
use napi::{JsString, ScopedTask, bindgen_prelude::*};
use napi_derive::napi;
pub struct CountBufferLength {
data: Buffer,
}
impl CountBufferLength {
pub fn new(data: Buffer) -> Self {
Self { data }
}
}
impl<'env> ScopedTask<'env> for CountBufferLength {
type Output = usize;
type JsValue = JsString<'env>;
fn compute(&mut self) -> Result<Self::Output> {
if self.data.len() == 10 {
return Err(Error::new(
Status::GenericFailure,
"Random fatal error".to_string(),
));
}
Ok((&self.data).len())
}
fn resolve(&mut self, env: &'env Env, output: Self::Output) -> Result<Self::JsValue> {
env.create_string(format!("{output}"))
}
fn reject(&mut self, env: &'env Env, err: Error) -> Result<Self::JsValue> {
// catch the error
if err.status == Status::GenericFailure {
env.create_string("Random fatal error".to_string())
} else {
env.create_string("Random error".to_string())
}
}
fn finally(self, _: Env) -> Result<()> {
drop(self.data);
Ok(())
}
}
#[napi]
pub fn async_count_buffer_length(data: Buffer) -> AsyncTask<CountBufferLength> {
AsyncTask::new(CountBufferLength { data })
}