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::{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:
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
:
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).
#[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 a 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
no código JavaScript e a AsyncTask
ainda não tiver sido iniciada, a AsyncTask
será abortada imediatamente e rejeitada com 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>
Caso a AsyncTask
já tenha sido iniciada ou concluída, o
AbortController.abort
não terá efeito.