Skip to content

Iteradores e iteradores assíncronos

O napi-rs pode fazer uma classe nativa implementar o protocolo de iteração síncrono ou assíncrono do JavaScript. Essas APIs estão atualmente marcadas como experimentais no código-fonte Rust: teste exatamente as versões do napi-rs e do runtime que você publica e espere refinamentos no comportamento de traits ou ciclo de vida.

Marcador e trait Rust Protocolo JavaScript Recurso do Cargo
#[napi(iterator)] + Generator ou ScopedGenerator Symbol.iterator, next, return, throw API napi base
#[napi(async_iterator)] + AsyncGenerator Symbol.asyncIterator, next, return e throw que retornam Promise tokio_rt (ou async)

Os dois atributos marcadores são mutuamente exclusivos em uma classe. Uma classe marcada também não pode ter campos públicos chamados next, return ou throw, pois o napi-rs instala esses métodos de protocolo.

Iterador síncrono

Implemente Generator quando os valores produzidos forem valores Rust próprios:

lib.rs
rust
use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(iterator)]
pub struct Counter {
  current: u32,
  end: u32,
}

#[napi]
impl Generator for Counter {
  type Yield = u32;
  type Next = u32;
  type Return = ();

  fn next(&mut self, value: Option<Self::Next>) -> Option<Self::Yield> {
    if let Some(next) = value {
      self.current = next;
    }
    if self.current >= self.end {
      return None;
    }
    let value = self.current;
    self.current += 1;
    Some(value)
  }
}

#[napi]
impl Counter {
  #[napi(constructor)]
  pub fn new(end: u32) -> Self {
    Self { current: 0, end }
  }
}
index.mjs
js
const counter = new Counter(3)

console.log(counter.next()) // { value: 0, done: false }
console.log(counter.next(2)) // { value: 2, done: false }
console.log(counter.next()) // { done: true }

console.log([...new Counter(3)]) // [0, 1, 2]

A declaração gerada estende Iterator<Yield, Return, Next>.

Tipos associados

Tipo associado Trait obrigatório Uso
Yield ToNapiValue Converte Some(value) de next ou catch para JavaScript.
Next FromNapiValue Converte o argumento opcional passado a iterator.next(value).
Return FromNapiValue Converte o argumento opcional passado a iterator.return(value).

O método recebe Option<Next> porque o JavaScript pode chamar next() sem argumento. Some(yielded) produz { value: yielded, done: false }; None produz { done: true } para essa chamada. O adaptador síncrono não persiste a conclusão natural: uma chamada posterior de next() invoca o Rust novamente. Se as chamadas posteriores precisarem continuar concluídas, registre esse estado na struct e continue retornando None.

return() e complete

Sobrescreva complete para fazer a limpeza quando o JavaScript encerrar a iteração antecipadamente, por exemplo, quando um loop for...of executar break.

lib.rs
rust
fn complete(&mut self, _value: Option<Self::Return>) -> Option<Self::Yield> {
  self.release_native_cursor();
  None
}

O adaptador síncrono atual invoca complete, marca o gerador como concluído e usa o argumento fornecido pelo JavaScript como valor do resultado de iterador retornado. O Option<Yield> retornado por complete não é exposto atualmente. Trate-o como um hook de limpeza e não dependa de seu valor de retorno até que esta API experimental seja estabilizada.

throw() e catch

O catch padrão retorna o valor JavaScript original como Err, então iterator.throw(error) lança esse valor e conclui o iterador.

Sobrescreva-o para recuperar:

lib.rs
rust
fn catch<'env>(
  &'env mut self,
  _env: Env,
  value: Unknown<'env>,
) -> std::result::Result<Option<Self::Yield>, Unknown<'env>> {
  if self.can_recover() {
    Ok(Some(self.fallback()))
  } else {
    Err(value)
  }
}
  • Err(value) lança value e conclui a iteração.
  • Ok(Some(value)) produz o valor com done: false.
  • Ok(None) conclui sem lançar.

Use std::result::Result na assinatura acima porque o lado do erro é o Unknown original, não napi::Error.

Produções síncronas com escopo

Generator::Yield deve ser um valor próprio ou diretamente conversível. Implemente ScopedGenerator<'env> quando um valor produzido tomar emprestado o ambiente JavaScript atual:

lib.rs
rust
use napi::iterator::ScopedGenerator;

#[napi(iterator)]
pub struct ObjectCounter {
  current: u32,
  end: u32,
}

#[napi]
impl<'env> ScopedGenerator<'env> for ObjectCounter {
  type Yield = Object<'env>;
  type Next = ();
  type Return = ();

  fn next(
    &mut self,
    env: &'env Env,
    _value: Option<Self::Next>,
  ) -> Option<Self::Yield> {
    if self.current >= self.end {
      return None;
    }
    let mut object = Object::new(env).ok()?;
    object.set("value", self.current).ok()?;
    self.current += 1;
    Some(object)
  }
}

O trait com escopo recebe &Env em next e catch. O valor produzido é convertido imediatamente na thread JavaScript; ele não pode ser armazenado na classe nem movido para outra thread.

Helpers de iterador e protótipos

Symbol.iterator retorna a própria instância da classe. Em runtimes que expõem o construtor global Iterator, o napi-rs ajusta o protótipo da classe gerada para herdar de Iterator.prototype, disponibilizando métodos auxiliares de iterador como map, filter, take e drop. Em runtimes sem esse global, o protocolo básico de iteração continua funcionando, mas esses helpers não estão disponíveis.

Como a integração de protótipo faz parte desta API experimental, teste subclasses e qualquer código que congele ou substitua protótipos de classe.

Iterador assíncrono

Habilite o runtime assíncrono:

Cargo.toml
toml
[dependencies]
napi = { version = "3", features = ["async", "tokio_time"] }
napi-derive = "3"

Em seguida, marque a classe e implemente AsyncGenerator:

lib.rs
rust
use std::future::Future;

use napi::bindgen_prelude::*;
use napi_derive::napi;

#[napi(async_iterator)]
pub struct DelayedCounter {
  current: u32,
  end: u32,
  delay_ms: u64,
}

#[napi]
impl AsyncGenerator for DelayedCounter {
  type Yield = u32;
  type Next = ();
  type Return = ();

  fn next(
    &mut self,
    _value: Option<Self::Next>,
  ) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
    let value = self.current;
    let end = self.end;
    let delay_ms = self.delay_ms;
    self.current += 1;

    async move {
      napi::tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
      Ok((value < end).then_some(value))
    }
  }
}

#[napi]
impl DelayedCounter {
  #[napi(constructor)]
  pub fn new(end: u32, delay_ms: u32) -> Self {
    Self { current: 0, end, delay_ms: delay_ms as u64 }
  }
}
index.mjs
js
for await (const value of new DelayedCounter(3, 10)) {
  console.log(value) // 0, 1, 2
}

A classe gerada implementa:

ts
[Symbol.asyncIterator](): AsyncGenerator<Yield, Return, Next | undefined>

Limites assíncronos

AsyncGenerator impede deliberadamente que valores com escopo JavaScript atravessem um ponto de await:

rust
type Yield: ToNapiValue + Send + 'static;

fn next(
  &mut self,
  value: Option<Self::Next>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static;

A future não pode tomar self emprestado. Atualize o estado síncrono e copie ou clone tudo de que a future precisa antes de criar o bloco async move, como no exemplo. Um Object<'env>, Function<'env, ...> ou BufferSlice<'env> com escopo não pode ser o tipo produzido. Retorne valores próprios ou crie um valor JavaScript posteriormente em outra API que forneça explicitamente Env na thread JavaScript.

next() assíncrono

Cada chamada retorna uma Promise:

  • Ok(Some(value)) resolve com { value, done: false }.
  • Ok(None) resolve com { value: undefined, done: true }.
  • Err(error) rejeita a Promise.

O adaptador assíncrono experimental atual não mantém um sinalizador separado de estado terminal após Ok(None). Se uma chamada posterior precisar continuar concluída, mantenha esse estado em sua struct Rust e continue retornando Ok(None).

Não presuma que chamadas sobrepostas de next() sejam serializadas para você. A mutação do estado antes de a future ser retornada acontece imediatamente na thread JavaScript, enquanto as futures resultantes podem continuar em andamento ao mesmo tempo. Projete a máquina de estados para operações simultâneas em andamento ou documente que os chamadores devem aguardar um resultado antes de solicitar o próximo.

return() assíncrono

Sobrescreva complete para fazer limpeza assíncrona:

lib.rs
rust
fn complete(
  &mut self,
  _value: Option<Self::Return>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
  let handle = self.take_handle();
  async move {
    handle.close().await.map_err(Error::from)?;
    Ok(None)
  }
}

A Promise retornada sempre resolve com done: true; Some(value) se torna seu valor final, e None se torna undefined. Um erro causa rejeição. Assim como em next, o próprio adaptador não persiste um sinalizador terminal para operações posteriores, portanto registre a conclusão em sua classe se os chamadores puderem reter e reutilizar o objeto iterador.

Há uma incompatibilidade experimental de tipos aqui: em tempo de execução, complete retorna Option<Self::Yield>, enquanto a declaração gerada AsyncGenerator<Yield, Return, Next> tipa o valor final como Return. Se complete puder retornar Some(value), use o mesmo tipo para Yield e Return; caso contrário, retorne None. Com tipos Yield e Return diferentes, a declaração gerada pode divergir do valor em tempo de execução.

O JavaScript normalmente chama return() quando um loop for await...of termina antecipadamente, mas a limpeza ainda deve tolerar que o iterador seja coletado sem um return ordenado.

throw() assíncrono

O catch padrão transforma o valor JavaScript lançado em napi::Error, então a Promise retornada é rejeitada.

lib.rs
rust
fn catch(
  &mut self,
  _env: Env,
  value: Unknown,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
  let error: Error = value.into();
  async move { Err(error) }
}

Um catch personalizado pode recuperar com Ok(Some(value)). No adaptador experimental atual, um Ok(None) recuperado é representado como um resultado não terminal com valor null; use Err para relançar ou Ok(Some(...)) para recuperar e use return()/estado explícito da classe para modelar a conclusão.

Consulte Tratamento de erros para preservar erros JavaScript em trabalho assíncrono.

Ciclo de vida e coleta de lixo

Na iteração assíncrona, [Symbol.asyncIterator]() cria um objeto iterador que mantém uma referência oculta, não enumerável e não gravável à instância da classe nativa. Isso impede que a classe seja coletada enquanto o iterador estiver retido. A referência é liberada quando o objeto iterador é finalizado.

Essa referência não cancela uma future Rust em andamento. Futures e os recursos externos que possuem precisam de seu próprio projeto de cancelamento e encerramento. Mantenha a limpeza idempotente para que possa ser chamada com segurança a partir de complete, métodos explícitos da classe e caminhos de Drop/finalização.

O iterador síncrono é a própria instância da classe, portanto a alcançabilidade normal da instância mantém o valor Rust vivo.

Escolhendo outra abstração

Use um iterador quando cada solicitação produzir naturalmente um item e o consumidor controlar o ritmo.

  • Use um array normal ou Vec<T> para resultados pequenos e já materializados.
  • Use ReadableStream para streaming com backpressure e semântica de cancelamento de Web Streams.
  • Use AsyncTask para um único resultado que exige muita CPU no pool de workers do libuv.
  • Use uma função assíncrona para uma future Tokio e uma Promise.
  • Use ThreadsafeFunction para callbacks repetidos que se originam em uma thread nativa.

Como o suporte a iteradores é experimental, prefira essas abstrações estabelecidas quando a interoperabilidade ou a estabilidade da API no longo prazo importar mais do que a sintaxe de iterador.

Checklist de testes

  • next() com e sem seu argumento.
  • Conclusão natural e chamadas após a conclusão.
  • break antecipado, return(value) explícito e falha na limpeza.
  • Comportamento padrão e recuperado de throw(error).
  • Duas chamadas assíncronas sobrepostas de next() se a API permitir.
  • Descartar a classe assíncrona original mantendo somente seu iterador.
  • Coleta de lixo forçada e encerramento do ambiente de worker.
  • Runtimes com e sem a API helper global Iterator.