Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use std::cell::Cell;
use std::marker::PhantomData;
use std::mem;
use std::os::raw::c_void;
use std::panic::UnwindSafe;
use std::ptr;
use std::rc::Rc;

use crate::bindgen_runtime::JsObjectValue;
use crate::{
  bindgen_runtime::{NativeBorrowLease, PromiseRaw, ToNapiValue},
  check_status, sys, Env, Error, JsError, Result, ScopedTask, Status,
};

struct AsyncWork<'task, T: ScopedTask<'task>> {
  // `Option` so `finally` can consume the task while the rest of the box stays owned here:
  // `complete_impl` must still release `borrow_lease` after `inner_task` is gone.
  inner_task: Option<T>,
  /// Lease on the borrow scope deferred by generated callback glue for `&T`/`&mut T`/`&self`
  /// arguments captured by this task. Held (never touched) while `compute` runs on the pool
  /// thread and released on the JavaScript thread in [`complete_impl`] once the task is
  /// destroyed; the scope's guards and roots go out with the last lease, not this one.
  borrow_lease: Option<NativeBorrowLease>,
  deferred: sys::napi_deferred,
  value: mem::MaybeUninit<Result<T::Output>>,
  napi_async_work: sys::napi_async_work,
  status: Rc<Cell<u8>>,
}

pub struct AsyncWorkPromise<T> {
  pub(crate) napi_async_work: sys::napi_async_work,
  raw_promise: sys::napi_value,
  env: sys::napi_env,
  /// share with AsyncWork
  /// 0: not started
  /// 1: completed
  /// 2: canceled
  pub(crate) status: Rc<Cell<u8>>,
  _phantom: PhantomData<T>,
}

impl<T> UnwindSafe for AsyncWorkPromise<T> {}
impl<T> std::panic::RefUnwindSafe for AsyncWorkPromise<T> {}

/// The `napi_async_work` this addon has queued and whose completion callback has not run yet,
/// as `(napi_env, napi_async_work)` addresses.
///
/// On wasm a loader disposes the binding by destroying the emnapi context and terminating the
/// pool threads. Neither can run a completion callback, so a work still outstanding at that
/// point never settles its promise — and it also never balances the emnapi waiting-request
/// counter, which brackets every queued work. On Node a nonzero counter keeps a
/// `MessageChannel` port referenced, so the process cannot exit either.
///
/// Nothing observable from JavaScript can stand in for this registry. The threadless archive
/// (`emnapi-basic-napi-rs`) resolves `napi_*_async_work` through the `@emnapi/core` JavaScript
/// plugins, but the threaded one (`emnapi-napi-rs-mt`) links the C `async_work.c` backed by the
/// uv threadpool — see `emnapi_link_library` in `crates/build/src/wasi.rs` — so there the wasm
/// neither imports nor exports those symbols, and the only brackets a loader could see
/// (`_emnapi_ctx_*_waiting_request_counter`) are shared with threadsafe functions. This crate is
/// the one choke point both flavors go through.
///
/// Exported to the loader as [`napi_wasm_async_work_pending`] and
/// [`napi_wasm_cancel_pending_async_work`], the async-work half of the
/// `napi_wasm_env_cleanup_pending` handshake.
///
/// A `Mutex` rather than a thread local: queueing and completing both happen on the JavaScript
/// thread, but the registry is process-wide and this keeps it sound without depending on that.
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
static OUTSTANDING_ASYNC_WORK: std::sync::Mutex<Vec<(usize, usize)>> =
  std::sync::Mutex::new(Vec::new());

/// A lock this addon poisoned is not worth aborting a teardown over: the registry only ever
/// makes disposal wait *longer*, so a failure to read it degrades to today's behavior.
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
fn register_outstanding_async_work(env: sys::napi_env, work: sys::napi_async_work) {
  if let Ok(mut outstanding) = OUTSTANDING_ASYNC_WORK.lock() {
    outstanding.push((env as usize, work as usize));
  }
}

/// Keeps a work registered for as long as its completion callback is running, and unregisters
/// it however that callback leaves — including through one of its several `?` paths.
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
struct OutstandingAsyncWorkGuard(sys::napi_async_work);

#[cfg(all(target_family = "wasm", not(feature = "noop")))]
impl Drop for OutstandingAsyncWorkGuard {
  fn drop(&mut self) {
    unregister_outstanding_async_work(self.0);
  }
}

#[cfg(all(target_family = "wasm", not(feature = "noop")))]
fn unregister_outstanding_async_work(work: sys::napi_async_work) {
  if let Ok(mut outstanding) = OUTSTANDING_ASYNC_WORK.lock() {
    let handle = work as usize;
    if let Some(index) = outstanding.iter().position(|(_, queued)| *queued == handle) {
      outstanding.swap_remove(index);
    }
  }
}

/// How many `napi_async_work` are queued and have not completed yet.
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
pub(crate) fn pending_async_work() -> u32 {
  OUTSTANDING_ASYNC_WORK
    .lock()
    .map(|outstanding| outstanding.len() as u32)
    .unwrap_or(0)
}

/// Cancels every outstanding `napi_async_work`, returning how many cancellations were accepted.
///
/// `napi_cancel_async_work` succeeds only for a work no thread has started. The completion
/// callback then runs with `napi_cancelled`, which [`complete_impl`] turns into an `AbortError`
/// rejection — so a cancelled work leaves this registry and balances the waiting-request counter
/// through exactly the same path an ordinary completion does. A refused cancellation means the
/// work is already executing; it is left alone to finish normally, which it can, because the
/// loader calls this *before* terminating anything.
#[cfg(all(target_family = "wasm", not(feature = "noop")))]
pub(crate) fn cancel_pending_async_work() -> u32 {
  // Snapshot and release: cancelling is the host's call, and holding the registry lock across
  // it would deadlock the moment a host delivered the cancelled completion synchronously.
  let Ok(outstanding) = OUTSTANDING_ASYNC_WORK.lock().map(|guard| guard.clone()) else {
    return 0;
  };
  let mut cancelled = 0;
  for (env, work) in outstanding {
    // SAFETY: both addresses were handed to us by `napi_create_async_work` / the `napi_env` it
    // was created with, and an entry is removed from the registry by `complete_impl` before
    // `napi_delete_async_work` frees the work — so a registered handle is always still live.
    if unsafe { sys::napi_cancel_async_work(env as sys::napi_env, work as sys::napi_async_work) }
      == sys::Status::napi_ok
    {
      cancelled += 1;
    }
  }
  cancelled
}

impl<T> AsyncWorkPromise<T> {
  pub fn promise_object<'env>(&self) -> PromiseRaw<'env, T> {
    PromiseRaw::new(self.env, self.raw_promise)
  }

  pub fn cancel(&mut self) -> Result<()> {
    // The complete callback frees the `napi_async_work` handle once it has run;
    // after that there is nothing left to cancel and `napi_cancel_async_work`
    // would touch freed memory.
    if self.status.get() == 1 {
      return Ok(());
    }
    // must be happened in the main thread, relaxed is enough
    self.status.set(2);
    check_status!(
      unsafe { sys::napi_cancel_async_work(self.env, self.napi_async_work) },
      "Cancel async work failed"
    )
  }
}

pub fn run<'task, T: ScopedTask<'task>>(
  env: sys::napi_env,
  task: T,
  abort_status: Option<Rc<Cell<u8>>>,
  borrow_lease: Option<NativeBorrowLease>,
) -> Result<AsyncWorkPromise<T::JsValue>> {
  let mut undefined = ptr::null_mut();
  check_status!(
    unsafe { sys::napi_get_undefined(env, &mut undefined) },
    "Get undefined failed in async_work::run"
  )?;
  let mut raw_promise = ptr::null_mut();
  let mut deferred = ptr::null_mut();
  check_status!(
    unsafe { sys::napi_create_promise(env, &mut deferred, &mut raw_promise) },
    "Create promise failed in async_work::run"
  )?;
  let task_status = abort_status.unwrap_or_else(|| Rc::new(Cell::new(0)));
  let result = Box::leak(Box::new(AsyncWork {
    inner_task: Some(task),
    borrow_lease,
    deferred,
    value: mem::MaybeUninit::uninit(),
    napi_async_work: ptr::null_mut(),
    status: task_status.clone(),
  }));
  // `run` still owns the `AsyncWork` allocation until `napi_queue_async_work` succeeds, so a
  // failed create/queue must reclaim it: the task and the deferred borrow lease drop here on
  // the JavaScript thread instead of leaking.
  if let Err(err) = check_status!(
    unsafe {
      sys::napi_create_async_work(
        env,
        raw_promise,
        undefined,
        Some(execute::<T>),
        Some(complete::<T>),
        (result as *mut AsyncWork<T>).cast(),
        &mut result.napi_async_work,
      )
    },
    "Create async work failed in async_work::run"
  ) {
    // `env` is live on the owner thread here, so the lease releases its roots directly
    // instead of leaking them on builds without the custom-GC delegate.
    let borrow_lease = result.borrow_lease.take();
    drop(unsafe { Box::from_raw(result) });
    if let Some(borrow_lease) = borrow_lease {
      borrow_lease.release(env);
    }
    return Err(err);
  }
  if let Err(err) = check_status!(
    unsafe { sys::napi_queue_async_work(env, result.napi_async_work) },
    "Queue async work failed in async_work::run"
  ) {
    unsafe { sys::napi_delete_async_work(env, result.napi_async_work) };
    let borrow_lease = result.borrow_lease.take();
    drop(unsafe { Box::from_raw(result) });
    if let Some(borrow_lease) = borrow_lease {
      borrow_lease.release(env);
    }
    return Err(err);
  }
  // Only a queue that succeeded holds a waiting-request reference for a teardown to balance.
  #[cfg(all(target_family = "wasm", not(feature = "noop")))]
  register_outstanding_async_work(env, result.napi_async_work);
  Ok(AsyncWorkPromise {
    napi_async_work: result.napi_async_work,
    raw_promise,
    env,
    status: task_status,
    _phantom: PhantomData,
  })
}

unsafe impl<'task, T: ScopedTask<'task> + Send> Send for AsyncWork<'task, T> {}
unsafe impl<'task, T: ScopedTask<'task> + Sync> Sync for AsyncWork<'task, T> {}

/// env here is the same with the one in `CallContext`.
/// So it actually could do nothing here, because `execute` function is called in the other thread mostly.
unsafe extern "C" fn execute<'task, T: ScopedTask<'task>>(_env: sys::napi_env, data: *mut c_void) {
  // An emnapi pool thread gets this work from the JS thread and may hold a stale memory size.
  crate::on_thread_handoff();
  let work = Box::leak(unsafe { Box::from_raw(data as *mut AsyncWork<T>) });
  let value = work
    .inner_task
    .as_mut()
    .expect("inner_task is taken only by the complete callback")
    .compute();
  work.value.write(value);
}

unsafe extern "C" fn complete<'task, T: ScopedTask<'task>>(
  env: sys::napi_env,
  status: sys::napi_status,
  data: *mut c_void,
) {
  if let Err(e) = complete_impl::<T>(env, status, data) {
    let js_err = JsError::from(e);
    unsafe { js_err.throw_into(env) };
  }
}

/// Checks the status of `napi_resolve_deferred`/`napi_reject_deferred`,
/// reporting whether the deferred was actually settled.
///
/// Both refuse to run JavaScript once the environment starts tearing down —
/// Worker exit can outlive a queued completion callback (#3535) — reporting
/// `napi_pending_exception` for module API < 10 or `napi_cannot_run_js` for
/// API >= 10. The deferred is already dead in that state, so those statuses
/// settle as a no-op instead of propagating: handing them to `throw_into` would
/// fail the same way, and its debug assertion would abort the process.
fn check_settle_status(status: sys::napi_status, message: &str) -> Result<bool> {
  match status {
    sys::Status::napi_ok => Ok(true),
    sys::Status::napi_pending_exception | sys::Status::napi_cannot_run_js => Ok(false),
    _ => Err(Error::new(Status::from(status), message.to_owned())),
  }
}

/// The JavaScript-facing half of [`complete_impl`]: settle the deferred and
/// hand the task's output or error back to it.
///
/// Returns whether settlement ran its course. `Ok(false)` means the
/// environment refused every attempt — teardown — so the deferred was left
/// alone; callers then skip `finally` just as they do after a settlement
/// error, because there is no JavaScript left for it to run against.
fn settle<'task, T: ScopedTask<'task>>(
  env: sys::napi_env,
  status: sys::napi_status,
  work: &mut AsyncWork<'task, T>,
  deferred: sys::napi_deferred,
) -> Result<bool> {
  if status == sys::Status::napi_cancelled {
    const ABORT_ERROR_NAME: &str = "AbortError";
    let wrapped_env = Env::from_raw(env);
    let mut error =
      wrapped_env.create_error(Error::new(Status::Cancelled, ABORT_ERROR_NAME.to_owned()))?;
    error.set_named_property("name", ABORT_ERROR_NAME)?;
    return check_settle_status(
      unsafe { sys::napi_reject_deferred(env, deferred, error.0.value) },
      "Reject AbortError failed",
    );
  }
  let inner_task = work
    .inner_task
    .as_mut()
    .expect("inner_task is taken only by finally");
  let value_ptr = unsafe { work.value.assume_init_read() };
  let value = match value_ptr {
    Ok(output) => inner_task.resolve(
      // SAFETY: `Env` is long lived
      unsafe { std::mem::transmute::<&Env, &'task Env>(&Env::from_raw(env)) },
      output,
    ),
    Err(e) => inner_task.reject(
      // SAFETY: `Env` is long lived
      unsafe { std::mem::transmute::<&Env, &'task Env>(&Env::from_raw(env)) },
      e,
    ),
  };
  if work.status.get() != 2 {
    return match check_status!(status)
      .and_then(move |_| value)
      .and_then(|v| unsafe { ToNapiValue::to_napi_value(env, v) })
    {
      Ok(v) => check_settle_status(
        unsafe { sys::napi_resolve_deferred(env, deferred, v) },
        "Resolve promise failed",
      ),
      Err(e) => {
        // `ToNapiValue for Error` hands the retained value back verbatim —
        // the completion callback runs on the owning env/thread, exactly
        // where the retained reference is restorable — and synthesizes a
        // fresh `Error` from `status`/`reason`/`cause` when there is nothing
        // to hand back (an error built on the libuv thread holds no
        // reference; one captured on a foreign env or thread fails the owner
        // gates in `referenced_value` and falls back to synthesis instead of
        // dereferencing a foreign reference). `JsError::into_value` cannot
        // be used here: it gates reuse on `napi_is_error`, so a task
        // rejecting with a retained primitive or plain object — same
        // contract as the deferred settlement path — would settle with a
        // synthesized `Error` instead of the captured value.
        let rejection = unsafe { ToNapiValue::to_napi_value(env, e) }?;
        check_settle_status(
          unsafe { sys::napi_reject_deferred(env, deferred, rejection) },
          "Reject promise failed",
        )
      }
    };
  }
  // The promise was cancelled through `AsyncWorkPromise`/`AbortSignal`: the
  // deferred is deliberately left alone, but settlement ran its course, so
  // `finally` still applies.
  Ok(true)
}

fn complete_impl<'task, T: ScopedTask<'task>>(
  env: sys::napi_env,
  status: sys::napi_status,
  data: *mut c_void,
) -> Result<()> {
  let mut work = unsafe { Box::from_raw(data as *mut AsyncWork<T>) };
  let napi_async_work = mem::replace(&mut work.napi_async_work, ptr::null_mut());
  // A work stops being outstanding when this callback *finishes*, not when it starts. Settling
  // the deferred and running the task's own `resolve`/`finally` can re-enter JavaScript — a
  // setter on the value being handed back, a threadsafe-function callback — and that JavaScript
  // can call `dispose()`. Unregistering up front would let such a disposal read zero and tear
  // the environment down from inside this frame, before the promise is settled and before
  // `finally` runs: the promise then hangs forever, which is the very thing the registry
  // exists to prevent.
  //
  // A guard rather than a call at the end, so the `?` paths below still unregister: an entry
  // left behind would make a drain wait for a work that already completed, and the drain has
  // no deadline. Dropped explicitly before `napi_delete_async_work` frees the handle, so a
  // cancel sweep can never be handed a dangling one.
  #[cfg(all(target_family = "wasm", not(feature = "noop")))]
  let outstanding_entry = OutstandingAsyncWorkGuard(napi_async_work);
  let deferred = mem::replace(&mut work.deferred, ptr::null_mut());
  let settle_result = settle::<T>(env, status, &mut work, deferred);
  // The handle is freed below no matter how settlement went, so the shared
  // status must read terminal on every path: `on_abort` and
  // `AsyncWorkPromise::cancel` only refrain from `napi_cancel_async_work` once
  // it reads 1, and a stale status would send them to freed memory.
  work.status.set(1);
  // `finally` runs only when settlement ran — an error or a dead env skips it,
  // same as before.
  let result = match settle_result {
    Ok(true) => work
      .inner_task
      .take()
      .expect("inner_task is taken only by finally")
      .finally(Env::from_raw(env)),
    Ok(false) => Ok(()),
    Err(e) => Err(e),
  };
  // Settlement and `finally` have run (or been skipped). Destroy the box first so a task that
  // never reached `finally` — and every forged `&'static` reference it captured — is dropped
  // before this lease releases its hold on the borrow scope's alias guards and JavaScript
  // roots. Releasing the last lease deletes roots, which may run wrapper finalizers
  // (re-entrant JavaScript), so this stays inside the window where the work is still
  // registered as outstanding.
  let borrow_lease = work.borrow_lease.take();
  drop(work);
  if let Some(borrow_lease) = borrow_lease {
    borrow_lease.release(env);
  }
  // Everything that can re-enter JavaScript has run. Leave the registry before the handle is
  // freed below.
  #[cfg(all(target_family = "wasm", not(feature = "noop")))]
  drop(outstanding_entry);
  // `napi_delete_async_work` runs no JavaScript and stays legal on a torn-down
  // env, so the handle is freed on every path — an error above must not leak it.
  result.and(check_status!(
    unsafe { sys::napi_delete_async_work(env, napi_async_work) },
    "Delete async work failed"
  ))
}