mirror of
https://github.com/napi-rs/napi-rs.git
synced 2026-02-01 16:41:24 +00:00
feat(napi-derive): add #[napi(async_iterator)] macro attribute (#3072)
This commit is contained in:
parent
5e642a208c
commit
76d06b3a5f
@ -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,
|
||||
|
||||
@ -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) })
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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)),
|
||||
|
||||
@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]>>(
|
||||
|
||||
@ -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)␊
|
||||
|
||||
Binary file not shown.
@ -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`,
|
||||
)
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"preserveSymlinks": true,
|
||||
"target": "ES2022",
|
||||
"target": "ES2024",
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"stripInternal": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user