impl {{ wrapper_name }} {
/// Create a new bridge wrapping a NAPI Object.
///
/// Validates that the object provides all required methods, then builds one
/// `ThreadsafeFunction` per bridged method eagerly — this runs synchronously on the
/// JS thread, inside the `#[napi] register_*` frame, with a live `Object` in scope, which
/// is the only place a `ThreadsafeFunction` can safely be created from a JS function value.
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
};
// The thread `new()` runs on. Always the JS thread in practice (this constructor only
// ever runs inside a synchronous `#[napi]` frame), captured rather than assumed so
// every call site that branches on "am I on the JS thread" (the sync fast/slow split,
// the off-thread drop-hazard guard) asks the same fact instead of re-deriving it.
let js_thread = std::thread::current().id();
// 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,
js_thread,
obj_ref: std::sync::Mutex::new(Some(obj_ref)),
cached_name,
{%- for init in tsfn_field_inits %}
{{ init }}
{%- endfor %}
})
}
/// 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 via the fast (`self.obj()`) path — the per-method
/// `ThreadsafeFunction`s keep working independently, since they hold their own
/// reference to the underlying JS function.
fn release_ref(&self) {
let Ok(mut guard) = self.obj_ref.lock() else {
return;
};
let Some(obj_ref) = guard.take() else {
return;
};
if std::thread::current().id() != self.js_thread {
// A `napi_ref` is thread-affine: `napi_delete_reference` must run on the owning JS
// thread, or it mutates V8's GlobalHandles concurrently with that thread and
// corrupts the heap (see napi-rs's own `FunctionRef::drop` comment, which routes
// this same case through the env's internal custom-GC threadsafe function — not
// available to generated code). Dropping `obj_ref` here without calling `.unref()`
// leaks the JS object (it survives until process exit) instead of corrupting the
// heap; `ObjectRef<false>`'s own `Drop` is a silent no-op for exactly this case.
drop(obj_ref);
return;
}
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.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. `env`/`obj_ref` are only ever dereferenced from `self.env()`/`self.obj()`, both of
// which are reached only from the sync fast path — itself gated on
// `std::thread::current().id() == self.js_thread` by every caller — so a HandleScope is always
// live when they run. Every other code path (async methods; the sync slow path) goes through a
// per-method `ThreadsafeFunction`, which is `Send + Sync` by construction and safe to call from
// any thread. The bridge genuinely IS moved across threads (unlike the historic claim this
// comment replaces): async trait methods run on whatever tokio worker polls them.
unsafe impl Send for {{ wrapper_name }} {}
unsafe impl Sync for {{ wrapper_name }} {}