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 %}
        // Cache the plugin name while the borrowed object is still in scope. `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 %}

        // Capture the raw napi_env that owns the object. It is stable for the
        // lifetime of the Node process and is used to reconstruct an `Env` when
        // materialising the object or building context arguments.
        // SAFETY: `Object<'_>` is laid out as 3 pointer-sized words whose first
        // word is the owning napi_env.
        let env = unsafe {
            let raw: [*mut std::ffi::c_void; 3] = std::mem::transmute_copy(&js_obj);
            raw[0] as napi::sys::napi_env
        };
        // Hold the JS object via a persistent napi reference instead of a borrowed
        // `Object` transmuted to `'static`. The reference keeps the value alive
        // across handle scopes and, crucially, is released in `dispose()`/`Drop`
        // so it no longer pins the event loop.
        let obj_ref = js_obj.create_ref::<false>()?;

        Ok(Self {
            env,
            obj_ref: std::sync::Mutex::new(Some(obj_ref)),
            cached_name,
            cancellation_token: std::sync::Arc::new(tokio_util::sync::CancellationToken::new()),
        })
    }

    /// Materialise a live `Object` from the persistent reference.
    ///
    /// The returned object is tied to the provided `Env` and kept alive by the
    /// underlying napi reference for the duration of the current event-loop turn.
    /// Returns an error once the reference has been released (after `dispose()`).
    fn obj<'env>(&self, env: &'env napi::Env) -> napi::Result<napi::bindgen_prelude::Object<'env>> {
        let guard = self.obj_ref.lock().unwrap_or_else(|e| e.into_inner());
        match guard.as_ref() {
            Some(obj_ref) => obj_ref.get_value(env),
            None => Err(napi::Error::new(
                napi::Status::GenericFailure,
                format!("Plugin '{}' object has been disposed", self.cached_name),
            )),
        }
    }

    /// Release the persistent reference, unpinning the event loop.
    ///
    /// Idempotent: safe to call multiple times. After disposal the bridge can no
    /// longer invoke the JS object.
    fn release_ref(&self) {
        if let Ok(mut guard) = self.obj_ref.lock() {
            if let Some(obj_ref) = guard.take() {
                let env = napi::Env::from_raw(self.env);
                let _ = obj_ref.unref(&env);
            }
        }
    }

    /// Clean up TSFN callbacks and release the event loop.
    pub async fn dispose(&self) -> napi::Result<()> {
        self.cancellation_token.cancel();
        self.release_ref();
        Ok(())
    }

    /// Extract napi::Env from the captured raw env pointer.
    fn env(&self) -> napi::Env {
        napi::Env::from_raw(self.env)
    }
}

impl Drop for {{ wrapper_name }} {
    fn drop(&mut self) {
        // Defensive release in case `dispose()` was never called. Releases the
        // napi reference so the JS object can be collected and the event loop is
        // no longer pinned by this bridge.
        self.release_ref();
    }
}

// SAFETY: The bridge is created from a NAPI Object that is pinned to the
// Node.js event loop thread. All napi access occurs on that thread; the stored
// reference (`ObjectRef`) is only dereferenced while reconstructing an `Env` for
// that same 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 }} {}
