impl {{ wrapper_name }} {
/// Create a new bridge wrapping a NAPI Object.
///
/// Validates that the object provides all required methods.
pub fn new(js_obj: napi::bindgen_prelude::Object<'_>) -> napi::Result<Self> {
{%- for method in required_methods %}
if !js_obj.has_named_property("{{ method.name }}").unwrap_or(false)
&& !js_obj.has_named_property("{{ method.snake_case_name }}").unwrap_or(false) {
return Err(napi::Error::new(
napi::Status::GenericFailure,
format!("Object missing required method: {}", "{{ method.name }}")
));
}
{%- endfor %}
{%- if requires_plugin_name %}
if !js_obj.has_named_property("name").unwrap_or(false) {
return Err(napi::Error::new(
napi::Status::GenericFailure,
"Object missing required method: name".to_string()
));
}
{%- endif %}
// SAFETY: The JS object is owned by the Node.js runtime and lives for
// the duration of the enclosing #[napi] call. The bridge is only used
// synchronously during that same call, so 'static is safe here.
let js_obj: napi::bindgen_prelude::Object<'static> = unsafe {
std::mem::transmute(js_obj)
};
// Cache the plugin name. `name` may be either a string property or a
// zero-arg function returning a string (the trait method form). Try the
// function first, then fall back to a string property.
let cached_name = js_obj
.get_named_property::<napi::bindgen_prelude::Function<(), String>>("name")
.and_then(|f| f.call(()))
.or_else(|_| js_obj.get_named_property::<String>("name"))
{%- if requires_plugin_name %}
.map_err(|e| napi::Error::new(
napi::Status::GenericFailure,
format!("Object missing required method: name ({e})")
))?;
{%- else %}
.unwrap_or_default();
{%- endif %}
Ok(Self {
inner: js_obj,
cached_name,
cancellation_token: Arc::new(tokio_util::sync::CancellationToken::new()),
})
}
/// Extract napi::Env from the stored Object.
fn env(&self) -> napi::Env {
// SAFETY: Object<'static> is 3 pointer-sized words; first word is napi_env.
let raw: [*mut std::ffi::c_void; 3] = unsafe { std::mem::transmute_copy(&self.inner) };
napi::Env::from_raw(raw[0] as napi::sys::napi_env)
}
/// Explicitly clean up any background tokio tasks spawned by async trait methods.
///
/// This method signals cancellation to all in-flight async operations and
/// ensures they are properly torn down before the bridge is dropped.
/// Returned Promise resolves when all cleanup is complete.
#[napi(js_name = "dispose")]
pub fn dispose(&self) -> napi::Result<napi::bindgen_prelude::Promise<()>> {
let token = self.cancellation_token.clone();
let promise = napi::bindgen_prelude::Promise::new(|env: napi::Env| {
let token = token;
Ok(env.execute_tokio_future(async move {
// Signal cancellation to all background tasks
token.cancel();
// Allow tasks to finish their cleanup
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
Ok(())
})?)
})?;
Ok(promise)
}
}
// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All access occurs on that thread. Send+Sync
// are required by the Plugin trait but the bridge is never actually moved
// across threads.
unsafe impl Send for {{ wrapper_name }} {}
unsafe impl Sync for {{ wrapper_name }} {}