alef 0.23.25

Opinionated polyglot binding generator for Rust libraries
Documentation
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 }} {}