feat(napi-derive): add #[napi(async_iterator)] macro attribute (#3072)

This commit is contained in:
LongYinan 2025-12-28 15:56:10 +08:00 committed by GitHub
parent 5e642a208c
commit 76d06b3a5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 570 additions and 12 deletions

View File

@ -28,6 +28,7 @@ pub struct NapiFn {
pub skip_typescript: bool,
pub comments: Vec<String>,
pub parent_is_generator: bool,
pub parent_is_async_generator: bool,
pub writable: bool,
pub enumerable: bool,
pub configurable: bool,
@ -90,6 +91,7 @@ pub struct NapiStruct {
pub kind: NapiStructKind,
pub has_lifetime: bool,
pub is_generator: bool,
pub is_async_generator: bool,
}
#[derive(Debug, Clone)]
@ -113,6 +115,7 @@ pub struct NapiClass {
pub fields: Vec<NapiStructField>,
pub ctor: bool,
pub implement_iterator: bool,
pub implement_async_iterator: bool,
pub is_tuple: bool,
pub use_custom_finalize: bool,
}
@ -174,6 +177,9 @@ pub struct NapiImpl {
pub iterator_yield_type: Option<Type>,
pub iterator_next_type: Option<Type>,
pub iterator_return_type: Option<Type>,
pub async_iterator_yield_type: Option<Type>,
pub async_iterator_next_type: Option<Type>,
pub async_iterator_return_type: Option<Type>,
pub js_mod: Option<String>,
pub comments: Vec<String>,
pub register_name: Ident,

View File

@ -683,6 +683,8 @@ impl NapiFn {
if self.is_ret_result {
if self.parent_is_generator {
Ok(quote! { cb.construct_generator::<false, _>(#js_name, #ret?) })
} else if self.parent_is_async_generator {
Ok(quote! { cb.construct_async_generator::<false, _>(#js_name, #ret?) })
} else {
Ok(quote! {
match #ret {
@ -698,6 +700,8 @@ impl NapiFn {
}
} else if self.parent_is_generator {
Ok(quote! { cb.construct_generator::<false, #parent>(#js_name, #ret) })
} else if self.parent_is_async_generator {
Ok(quote! { cb.construct_async_generator::<false, #parent>(#js_name, #ret) })
} else {
Ok(quote! { cb.construct::<false, #parent>(#js_name, #ret) })
}
@ -705,6 +709,8 @@ impl NapiFn {
if self.is_ret_result {
if self.parent_is_generator {
Ok(quote! { cb.generator_factory(#js_name, #ret?) })
} else if self.parent_is_async_generator {
Ok(quote! { cb.async_generator_factory(#js_name, #ret?) })
} else if self.is_async {
Ok(quote! { cb.factory(#js_name, #ret) })
} else {
@ -722,6 +728,8 @@ impl NapiFn {
}
} else if self.parent_is_generator {
Ok(quote! { cb.generator_factory(#js_name, #ret) })
} else if self.parent_is_async_generator {
Ok(quote! { cb.async_generator_factory(#js_name, #ret) })
} else {
Ok(quote! { cb.factory(#js_name, #ret) })
}

View File

@ -284,6 +284,7 @@ impl NapiStruct {
let js_name_raw = &self.js_name;
let js_name_str = format!("{js_name_raw}\0");
let iterator_implementation = self.gen_iterator_property(class, name);
let async_iterator_implementation = self.gen_async_iterator_property(class, name);
let (object_finalize_impl, to_napi_value_impl, javascript_class_ext_impl) = if self.has_lifetime
{
let name = quote! { #name<'_javascript_function_scope> };
@ -321,6 +322,7 @@ impl NapiStruct {
}
let instance_value = napi::bindgen_prelude::new_instance::<#name>(env, wrapped_value.cast(), ctor_ref)?;
#iterator_implementation
#async_iterator_implementation
Ok(instance_value)
} else {
Err(napi::bindgen_prelude::Error::new(
@ -360,6 +362,7 @@ impl NapiStruct {
{
let env = env.raw();
#iterator_implementation
#async_iterator_implementation
}
napi::bindgen_prelude::Reference::<#name>::from_value_ptr(wrapped_value.cast(), env.raw())
}
@ -402,6 +405,20 @@ impl NapiStruct {
}
}
fn gen_async_iterator_property(&self, class: &NapiClass, name: &Ident) -> TokenStream {
if !class.implement_async_iterator {
return quote! {};
}
// Note: `create_async_iterator` is NOT unsafe, unlike `create_iterator`.
// `create_iterator` is unsafe because `ScopedGenerator<'a>` has a lifetime parameter,
// requiring the caller to uphold lifetime invariants. `create_async_iterator` uses
// `AsyncGenerator` whose Future must be `Send + 'static`, so all data is owned and
// no lifetime invariants need to be upheld by the caller.
quote! {
napi::__private::create_async_iterator::<#name>(env, instance_value, wrapped_value);
}
}
fn gen_to_napi_value_ctor_impl(&self, class: &NapiClass) -> TokenStream {
let name = &self.name;
let js_name_without_null = &self.js_name;

View File

@ -28,6 +28,15 @@ impl ToTypeDef for NapiStruct {
"@see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-6.html#iterator-helper-methods", ];
js_doc.add_block(generator_doc)
}
if self.is_async_generator {
let generator_doc = [
"This type implements JavaScript's async iterable protocol.",
"It can be used with `for await...of` loops.",
"",
"@see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols",
];
js_doc.add_block(generator_doc)
}
Some(TypeDef {
kind: String::from(match self.kind {
@ -88,6 +97,37 @@ impl ToTypeDef for NapiImpl {
js_mod: self.js_mod.to_owned(),
js_doc: JSDoc::new::<Vec<String>, String>(Vec::default()),
})
} else if let Some(output_type) = &self.async_iterator_yield_type {
let yield_type = ty_to_ts_type(output_type, false, true, false).0;
let next_type = if let Some(ref ty) = self.async_iterator_next_type {
let ty_str = ty_to_ts_type(ty, false, false, false).0;
// Make TNext accept undefined so `for await...of` works (it calls next() with no args)
if ty_str == "void" || ty_str == "undefined" {
"undefined".to_owned()
} else {
format!("{} | undefined", ty_str)
}
} else {
"undefined".to_owned()
};
let return_type = if let Some(ref ty) = self.async_iterator_return_type {
ty_to_ts_type(ty, false, false, false).0
} else {
"void".to_owned()
};
// Use "impl" kind to add the [Symbol.asyncIterator]() method to the class
// instead of "extends AsyncGenerator" which is not valid TypeScript
Some(TypeDef {
kind: "impl".to_owned(),
name: self.js_name.to_owned(),
original_name: None,
def: format!(
"[Symbol.asyncIterator](): AsyncGenerator<{}, {}, {}>",
yield_type, return_type, next_type,
),
js_mod: self.js_mod.to_owned(),
js_doc: JSDoc::new::<Vec<String>, String>(Vec::default()),
})
} else {
Some(TypeDef {
kind: "impl".to_owned(),

View File

@ -71,6 +71,7 @@ macro_rules! attrgen {
(custom_finalize, CustomFinalize(Span)),
(namespace, Namespace(Span, String, Span)),
(iterator, Iterator(Span)),
(async_iterator, AsyncIterator(Span)),
(ts_args_type, TsArgsType(Span, String, Span)),
(ts_return_type, TsReturnType(Span, String, Span)),
(ts_type, TsType(Span, String, Span)),

View File

@ -26,7 +26,8 @@ use syn::{
use crate::parser::attrs::{check_recorded_struct_for_impl, record_struct};
static GENERATOR_STRUCT: OnceLock<Mutex<HashMap<String, bool>>> = OnceLock::new();
/// Stores (is_sync_generator, is_async_generator) for each struct
static GENERATOR_STRUCT: OnceLock<Mutex<HashMap<String, (bool, bool)>>> = OnceLock::new();
static REGISTER_INDEX: AtomicUsize = AtomicUsize::new(0);
@ -831,7 +832,7 @@ fn napi_fn_from_decl(
};
let namespace = opts.namespace().map(|(m, _)| m.to_owned());
let parent_is_generator = if let Some(p) = parent {
let (parent_is_generator, parent_is_async_generator) = if let Some(p) = parent {
let generator_struct = GENERATOR_STRUCT.get_or_init(|| Mutex::new(HashMap::new()));
let generator_struct = generator_struct
.lock()
@ -841,9 +842,9 @@ fn napi_fn_from_decl(
.as_ref()
.map(|n| format!("{n}::{p}"))
.unwrap_or_else(|| p.to_string());
*generator_struct.get(&key).unwrap_or(&false)
*generator_struct.get(&key).unwrap_or(&(false, false))
} else {
false
(false, false)
};
let kind = fn_kind(opts);
@ -884,6 +885,7 @@ fn napi_fn_from_decl(
ts_return_type: opts.ts_return_type().map(|(m, _)| m.to_owned()),
skip_typescript: opts.skip_typescript().is_some(),
parent_is_generator,
parent_is_async_generator,
writable: opts.writable(),
enumerable: opts.enumerable(),
configurable: opts.configurable(),
@ -1257,8 +1259,18 @@ impl ConvertToAST for syn::ItemStruct {
record_struct(&rust_struct_ident, final_js_name_for_struct.clone(), opts);
let namespace = opts.namespace().map(|(m, _)| m.to_owned());
let implement_iterator = opts.iterator().is_some();
let implement_async_iterator = opts.async_iterator().is_some();
if implement_iterator
if implement_iterator && implement_async_iterator {
bail_span!(
self,
"Cannot use both #[napi(iterator)] and #[napi(async_iterator)] on the same struct. \
Use #[napi(iterator)] for synchronous iteration (impl Generator) or \
#[napi(async_iterator)] for async iteration (impl AsyncGenerator)"
);
}
if (implement_iterator || implement_async_iterator)
&& self
.fields
.iter()
@ -1281,7 +1293,7 @@ impl ConvertToAST for syn::ItemStruct {
.as_ref()
.map(|n| format!("{n}::{rust_struct_ident}"))
.unwrap_or_else(|| rust_struct_ident.to_string());
generator_struct.insert(key, implement_iterator);
generator_struct.insert(key, (implement_iterator, implement_async_iterator));
drop(generator_struct);
let transparent = opts
@ -1350,6 +1362,7 @@ impl ConvertToAST for syn::ItemStruct {
fields,
ctor: opts.constructor().is_some(),
implement_iterator,
implement_async_iterator,
is_tuple,
use_custom_finalize: opts.custom_finalize().is_some(),
})
@ -1397,6 +1410,7 @@ impl ConvertToAST for syn::ItemStruct {
comments: extract_doc_comments(&self.attrs),
has_lifetime: lifetime.is_some(),
is_generator: implement_iterator,
is_async_generator: implement_async_iterator,
}),
})
}
@ -1427,6 +1441,9 @@ impl ConvertToAST for syn::ItemImpl {
let mut iterator_yield_type = None;
let mut iterator_next_type = None;
let mut iterator_return_type = None;
let mut async_iterator_yield_type = None;
let mut async_iterator_next_type = None;
let mut async_iterator_return_type = None;
for item in self.items.iter_mut() {
if let Some(method) = match item {
syn::ImplItem::Fn(m) => Some(m),
@ -1445,6 +1462,16 @@ impl ConvertToAST for syn::ItemImpl {
iterator_return_type = Some(m.ty.clone());
}
}
} else if ident == "AsyncGenerator" {
if let Type::Path(_) = &m.ty {
if m.ident == "Yield" {
async_iterator_yield_type = Some(m.ty.clone());
} else if m.ident == "Next" {
async_iterator_next_type = Some(m.ty.clone());
} else if m.ident == "Return" {
async_iterator_return_type = Some(m.ty.clone());
}
}
}
}
}
@ -1498,6 +1525,9 @@ impl ConvertToAST for syn::ItemImpl {
iterator_yield_type,
iterator_next_type,
iterator_return_type,
async_iterator_yield_type,
async_iterator_next_type,
async_iterator_return_type,
has_lifetime,
js_mod: namespace,
comments: extract_doc_comments(&self.attrs),
@ -1579,6 +1609,7 @@ impl ConvertToAST for syn::ItemEnum {
}),
has_lifetime: false,
is_generator: false,
is_async_generator: false,
}),
});
}

View File

@ -338,15 +338,15 @@ extern "C" fn generator_return<T: AsyncGenerator>(
}),
|env, value| {
let mut obj = Object::new(env)?;
// Per async iterator protocol, return() must ALWAYS set done: true
// The value (if any) is the final value, but iteration is complete
if let Some(v) = value {
obj.set("value", v)?;
obj.set("done", false)?;
Ok(obj)
} else {
obj.set("value", ())?;
obj.set("done", true)?;
Ok(obj)
}
obj.set("done", true)?;
Ok(obj)
},
) {
Ok(promise) => promise.inner,

View File

@ -167,6 +167,33 @@ impl<const N: usize> CallbackInfo<N> {
Ok(instance)
}
#[cfg(feature = "tokio_rt")]
pub fn construct_async_generator<
const IsEmptyStructHint: bool,
T: crate::bindgen_runtime::AsyncGenerator + ObjectFinalize + 'static,
>(
&self,
js_name: &str,
obj: T,
) -> Result<sys::napi_value> {
let (instance, generator_ptr) = self._construct::<IsEmptyStructHint, T>(js_name, obj)?;
crate::__private::create_async_iterator(self.env, instance, generator_ptr);
Ok(instance)
}
#[cfg(feature = "tokio_rt")]
pub fn async_generator_factory<
T: ObjectFinalize + crate::bindgen_runtime::AsyncGenerator + 'static,
>(
&self,
js_name: &str,
obj: T,
) -> Result<sys::napi_value> {
let (instance, generator_ptr) = self._factory(js_name, obj)?;
crate::__private::create_async_iterator(self.env, instance, generator_ptr);
Ok(instance)
}
fn _factory<T: ObjectFinalize + 'static>(
&self,
js_name: &str,

View File

@ -13,6 +13,8 @@ use crate::{JsError, Result, Status};
#[cfg(feature = "tokio_rt")]
pub mod async_iterator;
#[cfg(feature = "tokio_rt")]
pub use async_iterator::AsyncGenerator;
mod callback_info;
mod env;
mod error;

View File

@ -185,6 +185,9 @@ pub mod __private {
get_class_constructor, iterator::create_iterator, register_class, ___CALL_FROM_FACTORY,
};
#[cfg(feature = "tokio_rt")]
pub use crate::bindgen_runtime::async_iterator::create_async_iterator;
use crate::sys;
pub unsafe fn log_js_value<V: AsRef<[sys::napi_value]>>(

View File

@ -123,6 +123,29 @@ Generated by [AVA](https://avajs.dev).
}␊
export type JsAssets = Assets␊
/**␊
* This type implements JavaScript's async iterable protocol.␊
* It can be used with \`for await...of\` loops.␊
*␊
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/␊
export declare class AsyncDataSource {␊
[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined>
/** Creates an async data source that yields each item with a simulated I/O delay */␊
static fromData(data: Array<string>, delayMs: number): AsyncDataSource␊
}␊
/**␊
* This type implements JavaScript's async iterable protocol.␊
* It can be used with \`for await...of\` loops.␊
*␊
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/␊
export declare class AsyncFib {␊
[Symbol.asyncIterator](): AsyncGenerator<number, void, number | undefined>
constructor()␊
}␊
export declare class Bird {␊
name: string␊
constructor(name: string)␊
@ -218,6 +241,18 @@ Generated by [AVA](https://avajs.dev).
constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string)␊
}␊
/**␊
* This type implements JavaScript's async iterable protocol.␊
* It can be used with \`for await...of\` loops.␊
*␊
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/␊
export declare class DelayedCounter {␊
[Symbol.asyncIterator](): AsyncGenerator<number, string, undefined>
/** Creates a counter that yields values from 0 to max-1 with a delay between each */␊
constructor(max: number, delayMs: number)␊
}␊
export declare class Dog {␊
name: string␊
constructor(name: string)␊

View File

@ -1,6 +1,15 @@
import test from 'ava'
import { Fib, Fib2, Fib3, Fib4, shutdownRuntime } from '../index.cjs'
import {
Fib,
Fib2,
Fib3,
Fib4,
AsyncFib,
DelayedCounter,
AsyncDataSource,
shutdownRuntime,
} from '../index.cjs'
test.after(() => {
shutdownRuntime()
@ -85,3 +94,186 @@ test('generator should be able to return object', (t) => {
value: { number: 1 },
})
})
// AsyncGenerator tests
test('async generator should work with for-await-of', async (t) => {
if (typeof AsyncFib === 'undefined') {
t.pass(
'AsyncFib is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const fib = new AsyncFib()
const results: number[] = []
let count = 0
for await (const value of fib) {
results.push(value)
if (++count >= 5) break
}
t.deepEqual(results, [1, 1, 2, 3, 5])
})
test('async generator should support next()', async (t) => {
if (typeof AsyncFib === 'undefined') {
t.pass(
'AsyncFib is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const fib = new AsyncFib()
const iter = fib[Symbol.asyncIterator]()
t.deepEqual(await iter.next(), { value: 1, done: false })
t.deepEqual(await iter.next(), { value: 1, done: false })
t.deepEqual(await iter.next(), { value: 2, done: false })
})
test('async generator should support return()', async (t) => {
if (typeof AsyncFib === 'undefined') {
t.pass(
'AsyncFib is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const fib = new AsyncFib()
const iter = fib[Symbol.asyncIterator]()
t.deepEqual(await iter.next(), { value: 1, done: false })
t.deepEqual(await iter.return?.(), { value: undefined, done: true })
})
test('async generator should support throw()', async (t) => {
if (typeof AsyncFib === 'undefined') {
t.pass(
'AsyncFib is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const fib = new AsyncFib()
const iter = fib[Symbol.asyncIterator]()
t.deepEqual(await iter.next(), { value: 1, done: false })
// throw() should reject with the error passed to it
await t.throwsAsync(() => iter.throw!(new Error('test error')))
})
// Truly async generator tests - these use actual async delays
test('DelayedCounter should yield values with real async delays', async (t) => {
if (typeof DelayedCounter === 'undefined') {
t.pass(
'DelayedCounter is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const counter = new DelayedCounter(3, 10) // 3 values, 10ms delay each
const results: number[] = []
const startTime = Date.now()
for await (const value of counter) {
results.push(value as number)
}
const elapsed = Date.now() - startTime
t.deepEqual(results, [0, 1, 2])
// Should take at least 30ms (3 iterations * 10ms each)
// Allow some tolerance for timing
t.true(elapsed >= 25, `Expected at least 25ms, got ${elapsed}ms`)
})
test('DelayedCounter should complete and return done:true', async (t) => {
if (typeof DelayedCounter === 'undefined') {
t.pass(
'DelayedCounter is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const counter = new DelayedCounter(2, 5)
const iter = counter[Symbol.asyncIterator]()
t.deepEqual(await iter.next(), { value: 0, done: false })
t.deepEqual(await iter.next(), { value: 1, done: false })
// After max is reached, should return done: true
t.deepEqual(await iter.next(), { value: undefined, done: true })
// Verify idempotency: subsequent calls should continue returning done: true
t.deepEqual(await iter.next(), { value: undefined, done: true })
})
test('AsyncDataSource should yield string items with async delays', async (t) => {
if (typeof AsyncDataSource === 'undefined') {
t.pass(
'AsyncDataSource is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const data = ['hello', 'async', 'world']
const source = AsyncDataSource.fromData(data, 10) // 10ms delay per item
const results: string[] = []
const startTime = Date.now()
for await (const item of source) {
results.push(item as string)
}
const elapsed = Date.now() - startTime
t.deepEqual(results, ['hello', 'async', 'world'])
// Should take at least 30ms (3 items * 10ms each)
t.true(elapsed >= 25, `Expected at least 25ms, got ${elapsed}ms`)
})
test('AsyncDataSource factory pattern should work', async (t) => {
if (typeof AsyncDataSource === 'undefined') {
t.pass(
'AsyncDataSource is not available (tokio_rt feature not enabled), skipping test',
)
return
}
const source = AsyncDataSource.fromData(['a', 'b'], 5)
const iter = source[Symbol.asyncIterator]()
t.deepEqual(await iter.next(), { value: 'a', done: false })
t.deepEqual(await iter.next(), { value: 'b', done: false })
t.deepEqual(await iter.next(), { value: undefined, done: true })
})
test('async generators should run concurrently', async (t) => {
if (typeof DelayedCounter === 'undefined') {
t.pass(
'DelayedCounter is not available (tokio_rt feature not enabled), skipping test',
)
return
}
// Create two counters that each take 50ms total
const counter1 = new DelayedCounter(5, 10) // 5 * 10ms = 50ms
const counter2 = new DelayedCounter(5, 10) // 5 * 10ms = 50ms
const startTime = Date.now()
// Run both concurrently
const [results1, results2] = await Promise.all([
(async () => {
const r: number[] = []
for await (const v of counter1) r.push(v as number)
return r
})(),
(async () => {
const r: number[] = []
for await (const v of counter2) r.push(v as number)
return r
})(),
])
const elapsed = Date.now() - startTime
t.deepEqual(results1, [0, 1, 2, 3, 4])
t.deepEqual(results2, [0, 1, 2, 3, 4])
// If running concurrently, should take ~50ms, not ~100ms
// Allow very generous tolerance for CI/WASI environments
t.true(
elapsed < 300,
`Expected concurrent execution under 300ms, got ${elapsed}ms`,
)
})

View File

@ -72,6 +72,8 @@ export const Asset = __napiModule.exports.Asset
export const JsAsset = __napiModule.exports.JsAsset
export const Assets = __napiModule.exports.Assets
export const JsAssets = __napiModule.exports.JsAssets
export const AsyncDataSource = __napiModule.exports.AsyncDataSource
export const AsyncFib = __napiModule.exports.AsyncFib
export const Bird = __napiModule.exports.Bird
export const Blake2BHasher = __napiModule.exports.Blake2BHasher
export const Blake2bHasher = __napiModule.exports.Blake2bHasher
@ -92,6 +94,7 @@ export const CSSStyleSheet = __napiModule.exports.CSSStyleSheet
export const CustomFinalize = __napiModule.exports.CustomFinalize
export const CustomStruct = __napiModule.exports.CustomStruct
export const DefaultUseNullableClass = __napiModule.exports.DefaultUseNullableClass
export const DelayedCounter = __napiModule.exports.DelayedCounter
export const Dog = __napiModule.exports.Dog
export const Fib = __napiModule.exports.Fib
export const Fib2 = __napiModule.exports.Fib2

View File

@ -117,6 +117,8 @@ module.exports.Asset = __napiModule.exports.Asset
module.exports.JsAsset = __napiModule.exports.JsAsset
module.exports.Assets = __napiModule.exports.Assets
module.exports.JsAssets = __napiModule.exports.JsAssets
module.exports.AsyncDataSource = __napiModule.exports.AsyncDataSource
module.exports.AsyncFib = __napiModule.exports.AsyncFib
module.exports.Bird = __napiModule.exports.Bird
module.exports.Blake2BHasher = __napiModule.exports.Blake2BHasher
module.exports.Blake2bHasher = __napiModule.exports.Blake2bHasher
@ -137,6 +139,7 @@ module.exports.CSSStyleSheet = __napiModule.exports.CSSStyleSheet
module.exports.CustomFinalize = __napiModule.exports.CustomFinalize
module.exports.CustomStruct = __napiModule.exports.CustomStruct
module.exports.DefaultUseNullableClass = __napiModule.exports.DefaultUseNullableClass
module.exports.DelayedCounter = __napiModule.exports.DelayedCounter
module.exports.Dog = __napiModule.exports.Dog
module.exports.Fib = __napiModule.exports.Fib
module.exports.Fib2 = __napiModule.exports.Fib2

View File

@ -581,6 +581,8 @@ module.exports.Asset = nativeBinding.Asset
module.exports.JsAsset = nativeBinding.JsAsset
module.exports.Assets = nativeBinding.Assets
module.exports.JsAssets = nativeBinding.JsAssets
module.exports.AsyncDataSource = nativeBinding.AsyncDataSource
module.exports.AsyncFib = nativeBinding.AsyncFib
module.exports.Bird = nativeBinding.Bird
module.exports.Blake2BHasher = nativeBinding.Blake2BHasher
module.exports.Blake2bHasher = nativeBinding.Blake2bHasher
@ -601,6 +603,7 @@ module.exports.CSSStyleSheet = nativeBinding.CSSStyleSheet
module.exports.CustomFinalize = nativeBinding.CustomFinalize
module.exports.CustomStruct = nativeBinding.CustomStruct
module.exports.DefaultUseNullableClass = nativeBinding.DefaultUseNullableClass
module.exports.DelayedCounter = nativeBinding.DelayedCounter
module.exports.Dog = nativeBinding.Dog
module.exports.Fib = nativeBinding.Fib
module.exports.Fib2 = nativeBinding.Fib2

View File

@ -83,6 +83,29 @@ export declare class Assets {
}
export type JsAssets = Assets
/**
* This type implements JavaScript's async iterable protocol.
* It can be used with `for await...of` loops.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/
export declare class AsyncDataSource {
[Symbol.asyncIterator](): AsyncGenerator<string, void, undefined>
/** Creates an async data source that yields each item with a simulated I/O delay */
static fromData(data: Array<string>, delayMs: number): AsyncDataSource
}
/**
* This type implements JavaScript's async iterable protocol.
* It can be used with `for await...of` loops.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/
export declare class AsyncFib {
[Symbol.asyncIterator](): AsyncGenerator<number, void, number | undefined>
constructor()
}
export declare class Bird {
name: string
constructor(name: string)
@ -178,6 +201,18 @@ export declare class DefaultUseNullableClass {
constructor(requiredNumberField: number, requiredStringField: string, optionalNumberField?: number, optionalStringField?: string)
}
/**
* This type implements JavaScript's async iterable protocol.
* It can be used with `for await...of` loops.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols
*/
export declare class DelayedCounter {
[Symbol.asyncIterator](): AsyncGenerator<number, string, undefined>
/** Creates a counter that yields values from 0 to max-1 with a delay between each */
constructor(max: number, delayMs: number)
}
export declare class Dog {
name: string
constructor(name: string)

View File

@ -1,3 +1,5 @@
use std::future::Future;
use napi::{bindgen_prelude::*, iterator::ScopedGenerator};
#[napi(iterator)]
@ -142,3 +144,153 @@ impl<'a> ScopedGenerator<'a> for Fib4 {
obj.into_unknown(env).ok()
}
}
// Async iterator example - demonstrates the async generator pattern.
// This example computes Fibonacci synchronously but returns via an async block,
// showing the basic structure needed for AsyncGenerator implementations.
#[napi(async_iterator)]
pub struct AsyncFib {
current: u32,
next: u32,
}
#[napi]
impl AsyncGenerator for AsyncFib {
type Yield = u32;
type Next = i32;
type Return = ();
fn next(
&mut self,
value: Option<Self::Next>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
// The returned Future must be 'static, so we cannot borrow `self` in the async block.
// Instead, we compute the result synchronously here, update `self`, and capture
// only the computed value in the async block. This is safe because:
// 1. All mutations to `self` complete before creating the Future
// 2. The async block only captures `result` (an owned value), not `self`
let result = match value {
Some(n) => {
self.current = n as u32;
self.next = n as u32 + 1;
self.current
}
None => {
let next = self.next;
let current = self.current;
self.current = next;
self.next = current + next;
self.current
}
};
async move { Ok(Some(result)) }
}
}
#[napi]
#[allow(clippy::new_without_default)]
impl AsyncFib {
#[napi(constructor)]
pub fn new() -> Self {
AsyncFib {
current: 0,
next: 1,
}
}
}
// Truly async iterator - uses tokio::time::sleep for real async delays
#[napi(async_iterator)]
pub struct DelayedCounter {
current: u32,
max: u32,
delay_ms: u64,
}
#[napi]
impl AsyncGenerator for DelayedCounter {
type Yield = u32;
type Next = ();
type Return = String;
fn next(
&mut self,
_value: Option<Self::Next>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
let current = self.current;
let max = self.max;
let delay_ms = self.delay_ms;
self.current += 1;
async move {
// Actually sleep - this is truly async!
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
if current >= max {
Ok(None) // Signal completion
} else {
Ok(Some(current))
}
}
}
}
#[napi]
impl DelayedCounter {
/// Creates a counter that yields values from 0 to max-1 with a delay between each
#[napi(constructor)]
pub fn new(max: u32, delay_ms: u32) -> Self {
DelayedCounter {
current: 0,
max,
delay_ms: delay_ms as u64,
}
}
}
// Async iterator that simulates fetching paginated data
#[napi(async_iterator)]
pub struct AsyncDataSource {
data: Vec<String>,
index: usize,
delay_ms: u64,
}
#[napi]
impl AsyncGenerator for AsyncDataSource {
type Yield = String;
type Next = ();
type Return = ();
fn next(
&mut self,
_value: Option<Self::Next>,
) -> impl Future<Output = Result<Option<Self::Yield>>> + Send + 'static {
let item = if self.index < self.data.len() {
Some(self.data[self.index].clone())
} else {
None
};
self.index += 1;
let delay_ms = self.delay_ms;
async move {
// Simulate async I/O delay
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
Ok(item)
}
}
}
#[napi]
impl AsyncDataSource {
/// Creates an async data source that yields each item with a simulated I/O delay
#[napi(factory)]
pub fn from_data(data: Vec<String>, delay_ms: u32) -> Self {
AsyncDataSource {
data,
index: 0,
delay_ms: delay_ms as u64,
}
}
}

View File

@ -15,7 +15,7 @@
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"preserveSymlinks": true,
"target": "ES2022",
"target": "ES2024",
"sourceMap": true,
"esModuleInterop": true,
"stripInternal": true,