Skip to main content

napi_async_runtime/
adapter.rs

1//! napi adapter: registers the shared scheduler as the addon's
2//! [`AsyncRuntime`] backend and exports the JavaScript-facing host protocol
3//! (CurrentThread task host, JS timer host + relay, config/metrics).
4//!
5//! Hosts call [`install`] from their own `#[napi_derive::module_init]` hook.
6//! This module deliberately ships no `module_init` of its own: the host must
7//! resolve its runtime configuration (environment variables, defaults) first
8//! and stay in charge of that resolution order.
9
10use std::{future::Future, pin::Pin, ptr};
11
12use napi::bindgen_prelude::{
13  AsyncRuntime, AsyncRuntimeRejection, AsyncRuntimeTask, FnArgs, Promise, Unknown,
14  register_async_runtime,
15};
16use napi::threadsafe_function::ThreadsafeFunctionCallMode;
17use napi_derive::napi;
18
19use crate::MAX_ASYNC_RUNTIME_WORKER_THREADS;
20use crate::async_runtime::{
21  CurrentThreadTaskDelivery, CurrentThreadTaskDriver, CurrentThreadTaskDriverId, RuntimeFlavor,
22  RuntimeMetricsSnapshot, RuntimeOptions, RuntimeOptionsPatch, TimerDriver, TimerDriverId, TimerId,
23  acknowledge_current_thread_task_delivery, configure, configure_partial, configured_options,
24  drive_current_thread_tasks, fail_current_thread_task_delivery, metrics,
25  register_current_thread_task_driver, register_timer_driver, request_current_thread_task_drain,
26  reset_metrics, shutdown, start, try_block_on_dyn, try_spawn, try_spawn_blocking,
27  try_spawn_detached, unregister_current_thread_task_driver, unregister_timer_driver,
28};
29use crate::js_callback::{InvalidReturnValue, JsCallback};
30
31struct SharedAsyncRuntime;
32
33// SAFETY: See the implementation notes in this crate's README. Shutdown closes
34// admission, waits for the scheduler generation to quiesce, joins native
35// workers, and releases active resources. Independently, napi-rs permanently
36// retains the native image after a module that registered this backend exports
37// successfully, so externally cloned wakers cannot call into unmapped code.
38unsafe impl AsyncRuntime for SharedAsyncRuntime {
39  fn spawn(
40    &self,
41    task: AsyncRuntimeTask,
42  ) -> std::result::Result<(), AsyncRuntimeRejection<AsyncRuntimeTask>> {
43    match try_spawn(task) {
44      Ok(handle) => {
45        handle.detach();
46        Ok(())
47      }
48      Err((error, task)) => Err(AsyncRuntimeRejection::new(
49        task,
50        napi::Error::from_reason(error.to_string()),
51      )),
52    }
53  }
54
55  fn block_on(&self, future: Pin<&mut dyn Future<Output = ()>>) -> napi::Result<()> {
56    try_block_on_dyn(future).map_err(|error| napi::Error::from_reason(error.to_string()))
57  }
58
59  fn spawn_blocking(
60    &self,
61    work: Box<dyn FnOnce() + Send + 'static>,
62  ) -> std::result::Result<(), AsyncRuntimeRejection<Box<dyn FnOnce() + Send + 'static>>> {
63    // Route blocking work submitted through this SPI to the same bounded lane
64    // as the scheduler's spawn_blocking facade. See the implementation notes in this crate's README.
65    match try_spawn_blocking(work) {
66      Ok(handle) => {
67        handle.detach();
68        Ok(())
69      }
70      Err((error, work)) => Err(AsyncRuntimeRejection::new(
71        work,
72        napi::Error::from_reason(error.to_string()),
73      )),
74    }
75  }
76
77  fn start(&self) -> napi::Result<()> {
78    start().map_err(|error| napi::Error::from_reason(error.to_string()))
79  }
80
81  fn shutdown(&self) -> napi::Result<()> {
82    shutdown().map_err(|error| napi::Error::from_reason(error.to_string()))
83  }
84}
85
86#[napi(string_enum)]
87#[derive(Clone, Copy)]
88pub enum BindingRuntimeFlavor {
89  CurrentThread,
90  MultiThread,
91}
92
93impl From<BindingRuntimeFlavor> for RuntimeFlavor {
94  fn from(value: BindingRuntimeFlavor) -> Self {
95    match value {
96      BindingRuntimeFlavor::CurrentThread => Self::CurrentThread,
97      BindingRuntimeFlavor::MultiThread => Self::MultiThread,
98    }
99  }
100}
101
102impl From<RuntimeFlavor> for BindingRuntimeFlavor {
103  fn from(value: RuntimeFlavor) -> Self {
104    match value {
105      RuntimeFlavor::CurrentThread => Self::CurrentThread,
106      RuntimeFlavor::MultiThread => Self::MultiThread,
107    }
108  }
109}
110
111#[napi(object)]
112pub struct BindingRuntimeOptions {
113  pub flavor: Option<BindingRuntimeFlavor>,
114  /// Positive integer worker count. Values above 256 are rejected.
115  pub worker_threads: Option<f64>,
116  /// Positive integer blocking-task limit. Values above 256 are rejected.
117  pub max_blocking_tasks: Option<f64>,
118}
119
120impl TryFrom<BindingRuntimeOptions> for RuntimeOptionsPatch {
121  type Error = napi::Error;
122
123  fn try_from(value: BindingRuntimeOptions) -> Result<Self, Self::Error> {
124    Ok(Self {
125      flavor: value.flavor.map(Into::into),
126      worker_threads: validate_binding_thread_count(
127        "workerThreads",
128        value.worker_threads,
129        MAX_ASYNC_RUNTIME_WORKER_THREADS,
130      )?,
131      max_blocking_tasks: validate_binding_thread_count(
132        "maxBlockingTasks",
133        value.max_blocking_tasks,
134        MAX_ASYNC_RUNTIME_WORKER_THREADS,
135      )?,
136    })
137  }
138}
139
140fn validate_binding_thread_count(
141  field: &str,
142  value: Option<f64>,
143  maximum: usize,
144) -> napi::Result<Option<usize>> {
145  value
146    .map(|count| {
147      #[expect(
148        clippy::cast_precision_loss,
149        reason = "the small production thread limit is exactly representable as f64"
150      )]
151      let maximum_as_f64 = maximum as f64;
152      if !count.is_finite() || count < 1.0 || count.fract() != 0.0 || count > maximum_as_f64 {
153        return Err(napi::Error::from_reason(format!(
154          "`{field}` must be a positive integer no greater than {maximum}"
155        )));
156      }
157
158      #[expect(
159        clippy::cast_possible_truncation,
160        clippy::cast_sign_loss,
161        reason = "the finite positive integer was range-checked against the usize-backed limit"
162      )]
163      let count = count as usize;
164      Ok(count)
165    })
166    .transpose()
167}
168
169#[napi(object)]
170pub struct BindingRuntimeConfig {
171  pub flavor: BindingRuntimeFlavor,
172  pub worker_threads: u32,
173  pub max_blocking_tasks: u32,
174}
175
176impl From<RuntimeOptions> for BindingRuntimeConfig {
177  fn from(value: RuntimeOptions) -> Self {
178    Self {
179      flavor: value.flavor.into(),
180      worker_threads: saturating_u32(value.worker_threads as u64),
181      max_blocking_tasks: saturating_u32(value.max_blocking_tasks as u64),
182    }
183  }
184}
185
186#[napi(object)]
187pub struct BindingRuntimeMetrics {
188  pub flavor: BindingRuntimeFlavor,
189  pub worker_threads: u32,
190  pub max_blocking_tasks: u32,
191  pub tasks_spawned: f64,
192  pub tasks_completed: f64,
193  pub tasks_panicked: f64,
194  pub runnable_schedules: f64,
195  pub runnable_polls: f64,
196  pub queued_runnables: f64,
197  pub max_queued_runnables: f64,
198  pub active_runnables: f64,
199  pub max_active_runnables: f64,
200  pub blocking_tasks_started: f64,
201  pub blocking_tasks_completed: f64,
202  pub active_blocking_tasks: f64,
203  pub max_active_blocking_tasks: f64,
204}
205
206impl From<RuntimeMetricsSnapshot> for BindingRuntimeMetrics {
207  fn from(value: RuntimeMetricsSnapshot) -> Self {
208    Self {
209      flavor: value.flavor.into(),
210      worker_threads: saturating_u32(value.worker_threads as u64),
211      max_blocking_tasks: saturating_u32(value.max_blocking_tasks as u64),
212      tasks_spawned: safe_js_number(value.tasks_spawned),
213      tasks_completed: safe_js_number(value.tasks_completed),
214      tasks_panicked: safe_js_number(value.tasks_panicked),
215      runnable_schedules: safe_js_number(value.runnable_schedules),
216      runnable_polls: safe_js_number(value.runnable_polls),
217      queued_runnables: safe_js_number(value.queued_runnables),
218      max_queued_runnables: safe_js_number(value.max_queued_runnables),
219      active_runnables: safe_js_number(value.active_runnables),
220      max_active_runnables: safe_js_number(value.max_active_runnables),
221      blocking_tasks_started: safe_js_number(value.blocking_tasks_started),
222      blocking_tasks_completed: safe_js_number(value.blocking_tasks_completed),
223      active_blocking_tasks: safe_js_number(value.active_blocking_tasks),
224      max_active_blocking_tasks: safe_js_number(value.max_active_blocking_tasks),
225    }
226  }
227}
228
229fn saturating_u32(value: u64) -> u32 {
230  u32::try_from(value).unwrap_or(u32::MAX)
231}
232
233const MAX_SAFE_JS_INTEGER: u64 = (1_u64 << 53) - 1;
234
235#[expect(
236  clippy::cast_precision_loss,
237  reason = "the value is clamped to JavaScript's exactly representable integer range"
238)]
239fn safe_js_number(value: u64) -> f64 {
240  value.min(MAX_SAFE_JS_INTEGER) as f64
241}
242
243#[napi]
244/// Override the shared async runtime's flavor and thread counts.
245///
246/// Must be called before the first async binding call.
247pub fn configure_async_runtime(options: BindingRuntimeOptions) -> napi::Result<()> {
248  configure_partial(options.try_into()?)
249    .map_err(|error| napi::Error::from_reason(error.to_string()))
250}
251
252#[napi]
253/// Return the effective async runtime configuration.
254///
255/// Reports the runtime controller's validated options, including a
256/// pre-first-use `configureAsyncRuntime` override. The environment is never
257/// re-read.
258pub fn get_async_runtime_config() -> BindingRuntimeConfig {
259  configured_options().into()
260}
261
262#[napi]
263/// Return a snapshot of the shared async runtime's task and scheduler counters.
264pub fn get_async_runtime_metrics() -> BindingRuntimeMetrics {
265  metrics().into()
266}
267
268#[napi]
269/// Reset cumulative async runtime event counters to zero.
270///
271/// Live gauges and their lifetime high-water marks are preserved so active
272/// task guards can complete without corrupting concurrent observations.
273pub fn reset_async_runtime_metrics() {
274  reset_metrics();
275}
276
277#[napi(object)]
278pub struct BindingHostRegistration {
279  pub high: u32,
280  pub low: u32,
281}
282
283impl BindingHostRegistration {
284  fn from_id(id: u64) -> Self {
285    Self {
286      high: (id >> 32) as u32,
287      low: u32::try_from(id & u64::from(u32::MAX))
288        .expect("the masked host registration low word must fit in u32"),
289    }
290  }
291
292  fn id(high: u32, low: u32) -> u64 {
293    (u64::from(high) << 32) | u64::from(low)
294  }
295}
296
297static NEXT_HOST_REGISTRATION_ID: std::sync::atomic::AtomicU64 =
298  std::sync::atomic::AtomicU64::new(1);
299
300static RESERVED_HOST_REGISTRATIONS: std::sync::LazyLock<
301  std::sync::Mutex<rustc_hash::FxHashSet<u64>>,
302> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashSet::default()));
303
304fn reserve_host_registration_id() -> napi::Result<u64> {
305  let id = NEXT_HOST_REGISTRATION_ID
306    .fetch_update(
307      std::sync::atomic::Ordering::SeqCst,
308      std::sync::atomic::Ordering::SeqCst,
309      |current| current.checked_add(1),
310    )
311    .map_err(|_| {
312      napi::Error::new(
313        napi::Status::GenericFailure,
314        "JavaScript host registration id space exhausted",
315      )
316    })?;
317  RESERVED_HOST_REGISTRATIONS
318    .lock()
319    .unwrap_or_else(std::sync::PoisonError::into_inner)
320    .insert(id);
321  Ok(id)
322}
323
324fn claim_host_registration_id(registration_high: u32, registration_low: u32) -> napi::Result<u64> {
325  let id = BindingHostRegistration::id(registration_high, registration_low);
326  if RESERVED_HOST_REGISTRATIONS
327    .lock()
328    .unwrap_or_else(std::sync::PoisonError::into_inner)
329    .remove(&id)
330  {
331    Ok(id)
332  } else {
333    Err(napi::Error::new(
334      napi::Status::InvalidArg,
335      "CurrentThread host registration was not reserved or was already consumed",
336    ))
337  }
338}
339
340fn release_host_registration_id(id: u64) {
341  RESERVED_HOST_REGISTRATIONS
342    .lock()
343    .unwrap_or_else(std::sync::PoisonError::into_inner)
344    .remove(&id);
345}
346
347#[napi]
348/// Reserve an exact CurrentThread host registration capability before either
349/// task or timer installation performs side effects. The JavaScript package
350/// validates the returned words and passes them back to one registration call.
351pub fn reserve_current_thread_host_registration() -> napi::Result<BindingHostRegistration> {
352  reserve_host_registration_id().map(BindingHostRegistration::from_id)
353}
354
355fn current_thread_task_host_napi_result(
356  status: napi::sys::napi_status,
357  context: &'static str,
358) -> napi::Result<()> {
359  if status == napi::sys::Status::napi_ok {
360    Ok(())
361  } else {
362    Err(napi::Error::new(napi::Status::from(status), context))
363  }
364}
365
366fn contain_current_thread_task_host_unwind<T>(operation: impl FnOnce() -> T) -> Option<T> {
367  match std::panic::catch_unwind(std::panic::AssertUnwindSafe(operation)) {
368    Ok(value) => Some(value),
369    Err(payload) => {
370      if let Err(nested) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload)))
371      {
372        std::mem::forget(nested);
373      }
374      None
375    }
376  }
377}
378
379#[cfg(test)]
380thread_local! {
381  static NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK:
382    std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
383      const { std::cell::RefCell::new(None) };
384  static NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK:
385    std::cell::RefCell<Option<Box<dyn FnOnce()>>> =
386      const { std::cell::RefCell::new(None) };
387}
388
389#[cfg(test)]
390fn run_native_task_host_after_drive_test_hook() {
391  if let Some(hook) = NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK.with(|slot| slot.borrow_mut().take()) {
392    hook();
393  }
394}
395
396#[cfg(test)]
397fn run_native_task_host_after_payload_drop_test_hook() {
398  if let Some(hook) =
399    NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK.with(|slot| slot.borrow_mut().take())
400  {
401    hook();
402  }
403}
404
405unsafe extern "C" fn finalize_native_current_thread_task_host(
406  _env: napi::sys::napi_env,
407  finalize_data: *mut std::ffi::c_void,
408  _finalize_hint: *mut std::ffi::c_void,
409) {
410  if finalize_data.is_null() {
411    return;
412  }
413  let _ = contain_current_thread_task_host_unwind(|| {
414    let weak = unsafe {
415      std::sync::Weak::<NativeCurrentThreadTaskHostInner>::from_raw(finalize_data.cast())
416    };
417    if let Some(inner) = weak.upgrade() {
418      inner.finalized();
419    }
420  });
421}
422
423unsafe extern "C" fn call_native_current_thread_task_host(
424  env: napi::sys::napi_env,
425  _js_callback: napi::sys::napi_value,
426  _context: *mut std::ffi::c_void,
427  data: *mut std::ffi::c_void,
428) {
429  if data.is_null() {
430    return;
431  }
432
433  let payload = unsafe { Box::<NativeCurrentThreadTaskHostPayload>::from_raw(data.cast()) };
434  let delivery = payload.delivery;
435  let callback_lease = if env.is_null() {
436    None
437  } else {
438    contain_current_thread_task_host_unwind(|| {
439      let lease = drive_current_thread_tasks(delivery.capability());
440      #[cfg(test)]
441      if lease.is_some() {
442        run_native_task_host_after_drive_test_hook();
443      }
444      lease
445    })
446    .flatten()
447  };
448  let claimed = callback_lease.is_some();
449  let completed = contain_current_thread_task_host_unwind(|| {
450    if claimed {
451      acknowledge_current_thread_task_delivery(delivery);
452    } else {
453      fail_current_thread_task_delivery(delivery);
454    }
455  });
456  if completed.is_none() {
457    let _ = contain_current_thread_task_host_unwind(|| {
458      fail_current_thread_task_delivery(delivery);
459    });
460  }
461  let _ = contain_current_thread_task_host_unwind(|| drop(payload));
462  #[cfg(test)]
463  run_native_task_host_after_payload_drop_test_hook();
464  let _ = contain_current_thread_task_host_unwind(|| drop(callback_lease));
465}
466
467struct NativeCurrentThreadTaskHostPayload {
468  delivery: CurrentThreadTaskDelivery,
469  #[cfg(test)]
470  drop_observer: Option<std::sync::Arc<std::sync::atomic::AtomicUsize>>,
471}
472
473impl NativeCurrentThreadTaskHostPayload {
474  fn new(delivery: CurrentThreadTaskDelivery) -> Self {
475    Self {
476      delivery,
477      #[cfg(test)]
478      drop_observer: None,
479    }
480  }
481
482  #[cfg(test)]
483  fn with_drop_observer(
484    delivery: CurrentThreadTaskDelivery,
485    drop_observer: std::sync::Arc<std::sync::atomic::AtomicUsize>,
486  ) -> Self {
487    Self {
488      delivery,
489      drop_observer: Some(drop_observer),
490    }
491  }
492}
493
494#[cfg(test)]
495impl Drop for NativeCurrentThreadTaskHostPayload {
496  fn drop(&mut self) {
497    if let Some(observer) = &self.drop_observer {
498      observer.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
499    }
500  }
501}
502
503// See the implementation notes in this crate's README.
504struct NativeCurrentThreadTaskHost {
505  inner: std::sync::Arc<NativeCurrentThreadTaskHostInner>,
506}
507
508struct NativeCurrentThreadTaskHostInner {
509  threadsafe_function: std::sync::Mutex<Option<usize>>,
510  dead: std::sync::atomic::AtomicBool,
511  environment_closing: std::sync::atomic::AtomicBool,
512  host_registration: u64,
513  registration: std::sync::Mutex<Option<CurrentThreadTaskDriverId>>,
514}
515
516static NATIVE_CURRENT_THREAD_TASK_HOSTS: std::sync::LazyLock<
517  std::sync::Mutex<rustc_hash::FxHashMap<u64, std::sync::Weak<NativeCurrentThreadTaskHostInner>>>,
518> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
519
520fn registered_current_thread_task_host(
521  id: u64,
522) -> Option<std::sync::Arc<NativeCurrentThreadTaskHostInner>> {
523  let mut registrations = NATIVE_CURRENT_THREAD_TASK_HOSTS
524    .lock()
525    .unwrap_or_else(std::sync::PoisonError::into_inner);
526  let inner = registrations.get(&id).and_then(std::sync::Weak::upgrade);
527  if inner.is_none() {
528    registrations.remove(&id);
529  }
530  inner
531}
532
533impl NativeCurrentThreadTaskHostInner {
534  fn new(env: &napi::Env, host_registration: u64) -> napi::Result<std::sync::Arc<Self>> {
535    const ASYNC_RESOURCE_NAME: &[u8] = b"napi_async_runtime_current_thread_task_host";
536    let name_len = isize::try_from(ASYNC_RESOURCE_NAME.len())
537      .expect("the CurrentThread task-host resource name length must fit");
538    let mut async_resource_name = ptr::null_mut();
539    current_thread_task_host_napi_result(
540      unsafe {
541        napi::sys::napi_create_string_utf8(
542          env.raw(),
543          ASYNC_RESOURCE_NAME.as_ptr().cast(),
544          name_len,
545          &raw mut async_resource_name,
546        )
547      },
548      "Failed to create the CurrentThread task-host async resource name",
549    )?;
550
551    let inner = std::sync::Arc::new(Self {
552      threadsafe_function: std::sync::Mutex::default(),
553      dead: std::sync::atomic::AtomicBool::new(false),
554      environment_closing: std::sync::atomic::AtomicBool::new(false),
555      host_registration,
556      registration: std::sync::Mutex::default(),
557    });
558    let finalize_data = std::sync::Weak::into_raw(std::sync::Arc::downgrade(&inner))
559      .cast_mut()
560      .cast();
561    let mut threadsafe_function = ptr::null_mut();
562    let create_status = unsafe {
563      napi::sys::napi_create_threadsafe_function(
564        env.raw(),
565        ptr::null_mut(),
566        ptr::null_mut(),
567        async_resource_name,
568        1,
569        1,
570        finalize_data,
571        Some(finalize_native_current_thread_task_host),
572        ptr::null_mut(),
573        Some(call_native_current_thread_task_host),
574        &raw mut threadsafe_function,
575      )
576    };
577    if create_status != napi::sys::Status::napi_ok {
578      unsafe {
579        drop(std::sync::Weak::<Self>::from_raw(finalize_data.cast()));
580      }
581      return Err(napi::Error::new(
582        napi::Status::from(create_status),
583        "Failed to create the native CurrentThread task host",
584      ));
585    }
586    *inner
587      .threadsafe_function
588      .lock()
589      .unwrap_or_else(std::sync::PoisonError::into_inner) = Some(threadsafe_function as usize);
590
591    if let Err(error) = current_thread_task_host_napi_result(
592      unsafe { napi::sys::napi_unref_threadsafe_function(env.raw(), threadsafe_function) },
593      "Failed to unref the native CurrentThread task host",
594    ) {
595      inner.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::abort);
596      return Err(error);
597    }
598    Ok(inner)
599  }
600
601  fn is_live(&self) -> bool {
602    !self.dead.load(std::sync::atomic::Ordering::SeqCst)
603      && !self
604        .environment_closing
605        .load(std::sync::atomic::Ordering::SeqCst)
606      && self
607        .threadsafe_function
608        .lock()
609        .unwrap_or_else(std::sync::PoisonError::into_inner)
610        .is_some()
611  }
612
613  fn call_threadsafe_function_with(
614    &self,
615    data: *mut std::ffi::c_void,
616    call: impl FnOnce(
617      napi::sys::napi_threadsafe_function,
618      *mut std::ffi::c_void,
619    ) -> napi::sys::napi_status,
620  ) -> Option<napi::sys::napi_status> {
621    let mut slot = self
622      .threadsafe_function
623      .lock()
624      .unwrap_or_else(std::sync::PoisonError::into_inner);
625    if self.dead.load(std::sync::atomic::Ordering::SeqCst)
626      || self
627        .environment_closing
628        .load(std::sync::atomic::Ordering::SeqCst)
629    {
630      return None;
631    }
632    let threadsafe_function = (*slot)? as napi::sys::napi_threadsafe_function;
633    let status = call(threadsafe_function, data);
634    if status == napi::sys::Status::napi_closing {
635      // Node-API decrements this caller's acquisition before returning
636      // `napi_closing`; the pointer is no longer safe for any later API call.
637      self
638        .environment_closing
639        .store(true, std::sync::atomic::Ordering::SeqCst);
640      slot.take();
641    }
642    Some(status)
643  }
644
645  fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
646    let data: *mut std::ffi::c_void =
647      Box::into_raw(Box::new(NativeCurrentThreadTaskHostPayload::new(delivery))).cast();
648    let Some(status) =
649      self.call_threadsafe_function_with(data, |threadsafe_function, data| unsafe {
650        napi::sys::napi_call_threadsafe_function(
651          threadsafe_function,
652          data,
653          napi::sys::ThreadsafeFunctionCallMode::nonblocking,
654        )
655      })
656    else {
657      unsafe {
658        drop(Box::<NativeCurrentThreadTaskHostPayload>::from_raw(
659          data.cast(),
660        ));
661      }
662      return false;
663    };
664    if status != napi::sys::Status::napi_ok {
665      unsafe {
666        drop(Box::<NativeCurrentThreadTaskHostPayload>::from_raw(
667          data.cast(),
668        ));
669      }
670      return false;
671    }
672    true
673  }
674
675  fn finalized(&self) {
676    self
677      .environment_closing
678      .store(true, std::sync::atomic::Ordering::SeqCst);
679    // Finalization means Node is already destroying the TSFN. Invalidate the
680    // pointer without calling back into Node from its own finalizer.
681    self
682      .threadsafe_function
683      .lock()
684      .unwrap_or_else(std::sync::PoisonError::into_inner)
685      .take();
686    self.evict_inner(true, false);
687  }
688
689  fn environment_cleanup(&self) {
690    self
691      .environment_closing
692      .store(true, std::sync::atomic::Ordering::SeqCst);
693    // Cleanup is registered after the TSFN's own cleanup hook, so it normally
694    // runs first. Keep owner retirement independent from registry eviction:
695    // even a contained eviction panic must not retain the initial acquisition.
696    let _ = contain_current_thread_task_host_unwind(|| self.evict_inner(true, false));
697    self.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::release);
698  }
699
700  fn evict_after_sweep(&self) {
701    let abort = !self
702      .environment_closing
703      .load(std::sync::atomic::Ordering::SeqCst);
704    self.evict_inner(false, abort);
705  }
706
707  fn rollback(&self) {
708    self.evict_inner(true, true);
709  }
710
711  fn evict_inner(&self, request_redispatch: bool, abort: bool) {
712    self.dead.store(true, std::sync::atomic::Ordering::SeqCst);
713    NATIVE_CURRENT_THREAD_TASK_HOSTS
714      .lock()
715      .unwrap_or_else(std::sync::PoisonError::into_inner)
716      .remove(&self.host_registration);
717    let registration = self
718      .registration
719      .lock()
720      .unwrap_or_else(std::sync::PoisonError::into_inner)
721      .take();
722    if let Some(id) = registration {
723      unregister_current_thread_task_driver(id);
724      if request_redispatch {
725        // The dead host may have accepted a weak-TSFN callback that its env
726        // discarded before delivery. Republish the same internal capability to
727        // remaining live hosts through their exact delivery identities.
728        request_current_thread_task_drain();
729      }
730    }
731    if abort {
732      self.release_threadsafe_function(napi::sys::ThreadsafeFunctionReleaseMode::abort);
733    }
734  }
735
736  fn release_threadsafe_function(&self, mode: napi::sys::napi_threadsafe_function_release_mode) {
737    self.release_threadsafe_function_with(mode, |threadsafe_function, mode| unsafe {
738      napi::sys::napi_release_threadsafe_function(threadsafe_function, mode)
739    });
740  }
741
742  fn release_threadsafe_function_with(
743    &self,
744    mode: napi::sys::napi_threadsafe_function_release_mode,
745    release: impl FnOnce(
746      napi::sys::napi_threadsafe_function,
747      napi::sys::napi_threadsafe_function_release_mode,
748    ) -> napi::sys::napi_status,
749  ) {
750    let threadsafe_function = self
751      .threadsafe_function
752      .lock()
753      .unwrap_or_else(std::sync::PoisonError::into_inner)
754      .take();
755    let Some(threadsafe_function) = threadsafe_function else {
756      return;
757    };
758    let status = release(
759      threadsafe_function as napi::sys::napi_threadsafe_function,
760      mode,
761    );
762    if status == napi::sys::Status::napi_closing {
763      self
764        .environment_closing
765        .store(true, std::sync::atomic::Ordering::SeqCst);
766    }
767  }
768}
769
770impl CurrentThreadTaskDriver for NativeCurrentThreadTaskHost {
771  fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
772    self.inner.dispatch(delivery)
773  }
774
775  fn is_live(&self) -> bool {
776    self.inner.is_live()
777  }
778
779  fn on_swept(&self) {
780    // The registry is already selecting/dispatching a fallback. Avoid
781    // recursively starting another selection pass from its sweep callback.
782    self.inner.evict_after_sweep();
783  }
784}
785
786fn reject_current_thread_task_host_callback(dispatch: Option<Unknown<'_>>) -> napi::Result<()> {
787  if dispatch.is_none() {
788    Ok(())
789  } else {
790    Err(napi::Error::new(
791      napi::Status::InvalidArg,
792      "registerCurrentThreadTaskHost does not accept a JavaScript callback",
793    ))
794  }
795}
796
797const CURRENT_THREAD_TASK_HOST_CONTRACT_VERSION: u32 = 4;
798
799#[napi]
800/// Return the native CurrentThread task-host ABI expected by the JavaScript
801/// package before it invokes either async-runtime host registration. Version 4
802/// reserves and validates an exact registration capability before host
803/// installation performs side effects.
804pub fn get_current_thread_task_host_contract_version() -> u32 {
805  CURRENT_THREAD_TASK_HOST_CONTRACT_VERSION
806}
807
808#[napi]
809/// Return whether one exact CurrentThread task- or timer-host registration is
810/// still live. The JavaScript package revalidates its process-global marker on
811/// every module evaluation so native eviction cannot leave a stale installed
812/// bit that permanently suppresses replacement registration.
813pub fn is_current_thread_host_registration_active(
814  registration_high: u32,
815  registration_low: u32,
816) -> bool {
817  let id = BindingHostRegistration::id(registration_high, registration_low);
818  registered_current_thread_task_host(id).is_some_and(|inner| inner.is_live())
819    || registered_timer_host(id).is_some_and(|inner| inner.is_live())
820}
821
822fn install_cleanup_hook_or_rollback<T>(
823  install: impl FnOnce() -> napi::Result<T>,
824  rollback: impl FnOnce(),
825) -> napi::Result<()> {
826  match install() {
827    Ok(_) => Ok(()),
828    Err(error) => {
829      rollback();
830      Err(error)
831    }
832  }
833}
834
835fn install_host_driver_registration<T>(
836  dead: &std::sync::atomic::AtomicBool,
837  registration: &std::sync::Mutex<Option<T>>,
838  install: impl FnOnce() -> T,
839  publish: impl FnOnce(),
840  rollback: impl FnOnce(T),
841) -> bool {
842  // `install` may synchronously wake arbitrary task wakers. Keep it outside
843  // the exact-registration mutex so a reentrant liveness sweep can evict this
844  // host instead of deadlocking on the half-published registration.
845  let mut installed = Some(install());
846  {
847    let mut slot = registration
848      .lock()
849      .unwrap_or_else(std::sync::PoisonError::into_inner);
850    if !dead.load(std::sync::atomic::Ordering::SeqCst) {
851      *slot = installed.take();
852      // Publication is serialized with eviction by `registration`. This
853      // callback must only update binding-owned indexes.
854      publish();
855    }
856  }
857  if let Some(installed) = installed {
858    rollback(installed);
859    false
860  } else {
861    true
862  }
863}
864
865#[napi(ts_args_type = "registrationHigh: number, registrationLow: number, dispatch?: never")]
866/// Install a native-owned host turn used to poll CurrentThread runnables
867/// without re-entering arbitrary future waker locks. Called once per importing
868/// environment. JavaScript callbacks are rejected synchronously.
869pub fn register_current_thread_task_host(
870  env: &napi::Env,
871  registration_high: u32,
872  registration_low: u32,
873  dispatch: Option<Unknown<'_>>,
874) -> napi::Result<()> {
875  reject_current_thread_task_host_callback(dispatch)?;
876  let host_registration = claim_host_registration_id(registration_high, registration_low)?;
877  let inner = NativeCurrentThreadTaskHostInner::new(env, host_registration)?;
878  {
879    let mut slot = inner
880      .registration
881      .lock()
882      .unwrap_or_else(std::sync::PoisonError::into_inner);
883    *slot = Some(register_current_thread_task_driver(std::sync::Arc::new(
884      NativeCurrentThreadTaskHost {
885        inner: std::sync::Arc::clone(&inner),
886      },
887    )));
888  }
889  NATIVE_CURRENT_THREAD_TASK_HOSTS
890    .lock()
891    .unwrap_or_else(std::sync::PoisonError::into_inner)
892    .insert(host_registration, std::sync::Arc::downgrade(&inner));
893  request_current_thread_task_drain();
894  let hook_inner = std::sync::Arc::clone(&inner);
895  install_cleanup_hook_or_rollback(
896    || {
897      env.add_env_cleanup_hook(hook_inner, |inner| {
898        let _ = contain_current_thread_task_host_unwind(|| inner.environment_cleanup());
899      })
900    },
901    || inner.rollback(),
902  )?;
903  Ok(())
904}
905
906#[napi]
907/// Evict exactly one native host installed by `registerCurrentThreadTaskHost`.
908/// Managed workerd disposal uses this before environment cleanup so a later
909/// throwing cleanup hook cannot leave the process-global driver selected.
910pub fn unregister_current_thread_task_host(registration_high: u32, registration_low: u32) {
911  let id = BindingHostRegistration::id(registration_high, registration_low);
912  release_host_registration_id(id);
913  if let Some(inner) = registered_current_thread_task_host(id) {
914    inner.rollback();
915  }
916}
917
918/// Host timer driver for the shared runtime's CurrentThread flavor:
919/// `sleep_until` on the single-thread executor cannot park a
920/// helper thread (none exists on threadless wasm), so it delegates each timer
921/// to the host event loop through the JS callback registered at import --
922/// `(id, ms) => new Promise((resolve) => setTimeout(resolve, ms))`, paired
923/// with a cancellation callback that clears the timeout and resolves the
924/// relay promise.
925///
926/// Per timer id: the FIRST `register` arms one host timeout via a detached
927/// relay task; re-registers (`Sleep` re-polls) only refresh the stored waker.
928/// `cancel` removes the pending waker and either invokes the host cancellation
929/// callback or leaves a pre-arm tombstone for the accepted TSFN delivery. The
930/// JS side clears the timeout and resolves its promise, so a dropped sleep
931/// leaves neither a live timeout nor a detached relay task.
932///
933/// LIFETIME: each importing napi env registers its own
934/// host, and a host dies WITH its env -- the weak threadsafe function does
935/// not keep a worker's event loop alive, so a worker that imported the
936/// binding can exit at any time and orphan its host. A dead host must never
937/// keep timer duty (the registry would busy-fail every debounce against it),
938/// so it is EVICTED -- proactively by the env-cleanup hook installed at
939/// registration, and reactively by the `is_live` probe (the threadsafe
940/// function's `aborted` flag) and by relay-call failure. Eviction wakes every
941/// sleep armed here so each re-polls onto the registry's next live registrant
942/// (see `TimerDriverRegistry`).
943struct JsTimerHost {
944  inner: std::sync::Arc<JsTimerHostInner>,
945}
946
947#[derive(Default)]
948struct RelayIdAllocator {
949  next: std::sync::atomic::AtomicU64,
950}
951
952impl RelayIdAllocator {
953  fn reserve(&self) -> Result<u32, RelayIdExhausted> {
954    self
955      .next
956      .fetch_update(
957        std::sync::atomic::Ordering::Relaxed,
958        std::sync::atomic::Ordering::Relaxed,
959        |next| u32::try_from(next).is_ok().then_some(next + 1),
960      )
961      .map(|id| u32::try_from(id).expect("the checked relay id must fit in u32"))
962      .map_err(|_| RelayIdExhausted)
963  }
964}
965
966#[derive(Clone, Copy, Debug, PartialEq, Eq)]
967struct RelayIdExhausted;
968
969#[derive(Clone, Copy, Debug, PartialEq, Eq)]
970#[repr(u8)]
971enum RelayCancellationAccounting {
972  HostHealth = 0,
973  CleanupOnly = 1,
974}
975
976struct HostTimerRelayHealth {
977  cancellation_accounting: std::sync::atomic::AtomicU8,
978  failure_recorded: std::sync::atomic::AtomicBool,
979}
980
981impl Default for HostTimerRelayHealth {
982  fn default() -> Self {
983    Self {
984      cancellation_accounting: std::sync::atomic::AtomicU8::new(
985        RelayCancellationAccounting::HostHealth as u8,
986      ),
987      failure_recorded: std::sync::atomic::AtomicBool::new(false),
988    }
989  }
990}
991
992impl HostTimerRelayHealth {
993  fn set_cancellation_accounting(&self, accounting: RelayCancellationAccounting) {
994    self
995      .cancellation_accounting
996      .store(accounting as u8, std::sync::atomic::Ordering::Release);
997  }
998
999  fn cancellation_accounting(&self) -> RelayCancellationAccounting {
1000    match self
1001      .cancellation_accounting
1002      .load(std::sync::atomic::Ordering::Acquire)
1003    {
1004      value if value == RelayCancellationAccounting::CleanupOnly as u8 => {
1005        RelayCancellationAccounting::CleanupOnly
1006      }
1007      _ => RelayCancellationAccounting::HostHealth,
1008    }
1009  }
1010
1011  fn claim_failure(&self) -> bool {
1012    self
1013      .failure_recorded
1014      .compare_exchange(
1015        false,
1016        true,
1017        std::sync::atomic::Ordering::AcqRel,
1018        std::sync::atomic::Ordering::Acquire,
1019      )
1020      .is_ok()
1021  }
1022
1023  fn failure_recorded(&self) -> bool {
1024    self
1025      .failure_recorded
1026      .load(std::sync::atomic::Ordering::Acquire)
1027  }
1028}
1029
1030/// Consecutive NON-LIFETIME relay failures tolerated on one live host before
1031/// eviction. A transient failure (a one-off JS rejection, a queueing hiccup)
1032/// must not poison a live driver -- on a
1033/// main-only process that would leave NO driver and every later CT sleep
1034/// would hit the loud no-driver panic. But a PERSISTENTLY failing live
1035/// callback can never fire a timer either, so after this many consecutive
1036/// failures the host is evicted anyway (announced in the log). Reset on any
1037/// successful relay. Small on purpose: each strike costs one wasted arm/wake
1038/// round-trip for the affected sleep.
1039const HOST_TIMER_MAX_TRANSIENT_FAILURES: u32 = 3;
1040
1041/// Does this relay error mean the HOST IS GONE (evict immediately), as
1042/// opposed to a callback failure on a live host (strike-counted)?
1043///
1044/// ERROR CLASSIFICATION: no message strings. A rejected JS promise is
1045/// coerced into `GenericFailure` CARRYING THE JS REJECTION STRING (pinned
1046/// napi 3.10, error.rs `From<Unknown> for Error`: native coerces the value
1047/// to a string, wasm reads `.message` -- both always `GenericFailure`), so
1048/// any message match is forgeable by a live callback rejecting with a
1049/// colliding string (e.g. `Error('oneshot canceled')`) and would evict a
1050/// live host, bypassing the strike budget. Two string-free authorities
1051/// instead:
1052/// - `Status::Closing` is LIFETIME: it originates only from the TSFN layer
1053///   (aborted pre-check, raw `napi_closing`), and the JS coercion above can
1054///   never produce it -- unforgeable.
1055/// - Everything else defers to the LIVENESS PROBE (`is_live` = the dead
1056///   latch + the threadsafe function's own `aborted` flag): the genuine
1057///   teardown shapes (queue drained at env teardown, env died before the JS
1058///   promise settled) all coincide with the env being torn down, which the
1059///   probe observes directly.
1060///
1061/// Race walk: env dies between the error and the probe read -> the probe
1062/// reads dead -> evict: correct. Env alive at the probe but dying a
1063/// microsecond later -> strike now (the affected sleep is re-woken); the
1064/// death is then caught by the env-cleanup hook, by the aborted-probe sweep
1065/// at the next selection, or by the next relay failure (which will probe
1066/// dead) -> bounded, correct. A LIVE host's failure -- whatever its message
1067/// says -- takes the strike path.
1068fn should_evict_for_relay_error(status: napi::Status, host_is_live: bool) -> bool {
1069  status == napi::Status::Closing || !host_is_live
1070}
1071
1072#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1073enum HostTimerFailureAction {
1074  Duplicate,
1075  EvictHost,
1076  EvictHostAfterStrikes(u32),
1077  Retry(u32),
1078}
1079
1080fn record_host_timer_failure(
1081  transient_failures: &std::sync::atomic::AtomicU32,
1082  relay_health: &HostTimerRelayHealth,
1083  status: napi::Status,
1084  host_is_live: bool,
1085) -> HostTimerFailureAction {
1086  if !relay_health.claim_failure() {
1087    return HostTimerFailureAction::Duplicate;
1088  }
1089  if should_evict_for_relay_error(status, host_is_live) {
1090    return HostTimerFailureAction::EvictHost;
1091  }
1092  let strikes = transient_failures.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
1093  if strikes >= HOST_TIMER_MAX_TRANSIENT_FAILURES {
1094    HostTimerFailureAction::EvictHostAfterStrikes(strikes)
1095  } else {
1096    HostTimerFailureAction::Retry(strikes)
1097  }
1098}
1099
1100fn reset_host_timer_failures_after_success(
1101  transient_failures: &std::sync::atomic::AtomicU32,
1102  relay_health: &HostTimerRelayHealth,
1103) {
1104  if !relay_health.failure_recorded() {
1105    transient_failures.store(0, std::sync::atomic::Ordering::SeqCst);
1106  }
1107}
1108
1109struct JsTimerHostInner {
1110  callback: JsCallback<FnArgs<(u32, f64)>, Promise<()>>,
1111  cancel_callback: JsCallback<FnArgs<(u32,)>, ()>,
1112  pending: std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1113  relay_ids: RelayIdAllocator,
1114  /// Latched by [`JsTimerHostInner::evict`]: this host's env is gone (or its
1115  /// callback failed) and it must never serve another timer.
1116  dead: std::sync::atomic::AtomicBool,
1117  /// Exact JavaScript-facing capability used to unregister this host before
1118  /// emnapi starts running fallible environment cleanup hooks.
1119  host_registration: u64,
1120  /// This host's registration in the global driver registry. Installation and
1121  /// eviction use this mutex as their publication boundary, but the core
1122  /// registration call itself runs outside it because that call may wake
1123  /// arbitrary task wakers.
1124  registration: std::sync::Mutex<Option<TimerDriverId>>,
1125  /// Consecutive non-lifetime relay failures (see
1126  /// [`HOST_TIMER_MAX_TRANSIENT_FAILURES`]); reset on success.
1127  transient_failures: std::sync::atomic::AtomicU32,
1128}
1129
1130static JS_TIMER_HOSTS: std::sync::LazyLock<
1131  std::sync::Mutex<rustc_hash::FxHashMap<u64, std::sync::Weak<JsTimerHostInner>>>,
1132> = std::sync::LazyLock::new(|| std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
1133
1134fn registered_timer_host(id: u64) -> Option<std::sync::Arc<JsTimerHostInner>> {
1135  let mut registrations = JS_TIMER_HOSTS
1136    .lock()
1137    .unwrap_or_else(std::sync::PoisonError::into_inner);
1138  let inner = registrations.get(&id).and_then(std::sync::Weak::upgrade);
1139  if inner.is_none() {
1140    registrations.remove(&id);
1141  }
1142  inner
1143}
1144
1145struct PendingHostTimer {
1146  cancellation: Option<futures::channel::oneshot::Sender<()>>,
1147  relay_health: std::sync::Arc<HostTimerRelayHealth>,
1148  relay_id: u32,
1149  waker: std::task::Waker,
1150  schedule_state: RelayScheduleState,
1151}
1152
1153impl PendingHostTimer {
1154  fn signal_native_cancellation(&mut self) {
1155    if let Some(sender) = self.cancellation.take() {
1156      let _ = sender.send(());
1157    }
1158  }
1159}
1160
1161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1162enum RelayScheduleState {
1163  AwaitingCallback,
1164  CallbackComplete,
1165}
1166
1167trait PendingRelayState: Send + Sync + 'static {
1168  fn take_pending_relay(
1169    &self,
1170    id: TimerId,
1171    relay_id: u32,
1172    accounting: RelayCancellationAccounting,
1173  ) -> Option<PendingHostTimer>;
1174  /// Record that the JavaScript schedule callback has returned. `true` means
1175  /// Rust cleanup already removed this exact relay, so cancellation must be
1176  /// submitted now that it cannot overtake timer creation.
1177  fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool;
1178  fn cancel_relay(
1179    self: &std::sync::Arc<Self>,
1180    relay_id: u32,
1181    relay_health: std::sync::Arc<HostTimerRelayHealth>,
1182  );
1183}
1184
1185struct PendingRelayDropGuard<T: PendingRelayState> {
1186  state: std::sync::Arc<T>,
1187  id: TimerId,
1188  relay_id: u32,
1189  cleanup_on_drop: bool,
1190}
1191
1192impl<T: PendingRelayState> PendingRelayDropGuard<T> {
1193  fn new(state: std::sync::Arc<T>, id: TimerId, relay_id: u32) -> Self {
1194    Self {
1195      state,
1196      id,
1197      relay_id,
1198      cleanup_on_drop: true,
1199    }
1200  }
1201
1202  fn disarm(&mut self) {
1203    self.cleanup_on_drop = false;
1204  }
1205}
1206
1207impl<T: PendingRelayState> Drop for PendingRelayDropGuard<T> {
1208  fn drop(&mut self) {
1209    if !self.cleanup_on_drop {
1210      return;
1211    }
1212    let Some(pending) = self.state.take_pending_relay(
1213      self.id,
1214      self.relay_id,
1215      RelayCancellationAccounting::HostHealth,
1216    ) else {
1217      return;
1218    };
1219    // An awaiting callback owns its exact, never-reused relay id and will
1220    // submit cancellation after JavaScript returns. Waking may re-enter timer
1221    // registration, but a later timer can never receive this id.
1222    retire_pending_relay(&self.state, pending);
1223  }
1224}
1225
1226enum PendingHostTimerRegistration {
1227  Refreshed(std::task::Waker),
1228  Armed {
1229    relay_id: u32,
1230    cancellation: futures::channel::oneshot::Receiver<()>,
1231    relay_health: std::sync::Arc<HostTimerRelayHealth>,
1232  },
1233  Exhausted(std::task::Waker),
1234}
1235
1236fn register_pending_host_timer_locked(
1237  pending: &mut rustc_hash::FxHashMap<TimerId, PendingHostTimer>,
1238  relay_ids: &RelayIdAllocator,
1239  id: TimerId,
1240  waker: std::task::Waker,
1241) -> PendingHostTimerRegistration {
1242  if let Some(slot) = pending.get_mut(&id) {
1243    return PendingHostTimerRegistration::Refreshed(std::mem::replace(&mut slot.waker, waker));
1244  }
1245  let Ok(relay_id) = relay_ids.reserve() else {
1246    return PendingHostTimerRegistration::Exhausted(waker);
1247  };
1248  let (cancellation_sender, cancellation) = futures::channel::oneshot::channel();
1249  let relay_health = std::sync::Arc::new(HostTimerRelayHealth::default());
1250  pending.insert(
1251    id,
1252    PendingHostTimer {
1253      cancellation: Some(cancellation_sender),
1254      relay_health: std::sync::Arc::clone(&relay_health),
1255      relay_id,
1256      waker,
1257      schedule_state: RelayScheduleState::AwaitingCallback,
1258    },
1259  );
1260  PendingHostTimerRegistration::Armed {
1261    relay_id,
1262    cancellation,
1263    relay_health,
1264  }
1265}
1266
1267#[cfg(test)]
1268fn register_pending_host_timer(
1269  pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1270  relay_ids: &RelayIdAllocator,
1271  id: TimerId,
1272  waker: std::task::Waker,
1273) -> PendingHostTimerRegistration {
1274  let mut pending = pending
1275    .lock()
1276    .unwrap_or_else(std::sync::PoisonError::into_inner);
1277  register_pending_host_timer_locked(&mut pending, relay_ids, id, waker)
1278}
1279
1280fn register_pending_host_timer_if_live(
1281  pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1282  relay_ids: &RelayIdAllocator,
1283  id: TimerId,
1284  waker: std::task::Waker,
1285  is_live: impl FnOnce() -> bool,
1286) -> Result<PendingHostTimerRegistration, std::task::Waker> {
1287  let mut pending = pending
1288    .lock()
1289    .unwrap_or_else(std::sync::PoisonError::into_inner);
1290  if !is_live() {
1291    return Err(waker);
1292  }
1293  Ok(register_pending_host_timer_locked(
1294    &mut pending,
1295    relay_ids,
1296    id,
1297    waker,
1298  ))
1299}
1300
1301fn take_pending_host_timers(
1302  pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1303  accounting: RelayCancellationAccounting,
1304) -> rustc_hash::FxHashMap<TimerId, PendingHostTimer> {
1305  let mut pending = pending
1306    .lock()
1307    .unwrap_or_else(std::sync::PoisonError::into_inner);
1308  for timer in pending.values() {
1309    timer.relay_health.set_cancellation_accounting(accounting);
1310  }
1311  std::mem::take(&mut *pending)
1312}
1313
1314fn take_pending_host_timer(
1315  pending: &std::sync::Mutex<rustc_hash::FxHashMap<TimerId, PendingHostTimer>>,
1316  id: TimerId,
1317  relay_id: u32,
1318  accounting: RelayCancellationAccounting,
1319) -> Option<PendingHostTimer> {
1320  let mut pending = pending
1321    .lock()
1322    .unwrap_or_else(std::sync::PoisonError::into_inner);
1323  if pending
1324    .get(&id)
1325    .is_some_and(|slot| slot.relay_id == relay_id)
1326  {
1327    pending
1328      .get(&id)
1329      .expect("the matching pending relay must remain present")
1330      .relay_health
1331      .set_cancellation_accounting(accounting);
1332    pending.remove(&id)
1333  } else {
1334    None
1335  }
1336}
1337
1338fn run_host_timer_cleanup_safely(cleanup: impl FnOnce()) {
1339  if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(cleanup))
1340    && let Err(nested_payload) =
1341      std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| drop(payload)))
1342  {
1343    // The original unwind is contained. A hostile payload destructor may
1344    // panic again; quarantine only that nested payload so its destructor
1345    // cannot unwind through the napi env cleanup hook either.
1346    std::mem::forget(nested_payload);
1347  }
1348}
1349
1350fn retire_pending_relay<T: PendingRelayState>(
1351  state: &std::sync::Arc<T>,
1352  mut pending: PendingHostTimer,
1353) {
1354  // The detached relay must stop depending on JavaScript before cancellation
1355  // crosses the fallible TSFN boundary. A queue rejection or callback throw can
1356  // then affect diagnostics only, never relay retirement or runtime shutdown.
1357  pending.signal_native_cancellation();
1358  if pending.schedule_state == RelayScheduleState::CallbackComplete {
1359    let relay_health = std::sync::Arc::clone(&pending.relay_health);
1360    run_host_timer_cleanup_safely(|| state.cancel_relay(pending.relay_id, relay_health));
1361  }
1362  wake_host_timer_safely(pending.waker);
1363}
1364
1365fn retire_pending_relays<T: PendingRelayState>(
1366  state: &std::sync::Arc<T>,
1367  pending: rustc_hash::FxHashMap<TimerId, PendingHostTimer>,
1368) {
1369  for (_, pending) in pending {
1370    retire_pending_relay(state, pending);
1371  }
1372}
1373
1374fn complete_relay_schedule_callback<T: PendingRelayState, R>(
1375  state: &std::sync::Arc<T>,
1376  id: TimerId,
1377  relay_id: u32,
1378  relay_health: &std::sync::Arc<HostTimerRelayHealth>,
1379  result: R,
1380  deliver: impl FnOnce(R),
1381) {
1382  // This runs from napi-rs's return-value callback on the JavaScript call
1383  // stack, after the schedule function has returned but before delivery wakes
1384  // the Rust relay future. A terminal runnable drop therefore sees
1385  // CallbackComplete, while cleanup that won earlier is completed here.
1386  run_host_timer_cleanup_safely(|| {
1387    if state.mark_relay_callback_complete(id, relay_id) {
1388      state.cancel_relay(relay_id, std::sync::Arc::clone(relay_health));
1389    }
1390  });
1391  run_host_timer_cleanup_safely(|| deliver(result));
1392}
1393
1394fn normalize_timer_schedule_result(
1395  result: napi::Result<napi::Either<Promise<()>, InvalidReturnValue>>,
1396) -> napi::Result<Promise<()>> {
1397  match result {
1398    Ok(napi::Either::A(promise)) => Ok(promise),
1399    Ok(napi::Either::B(invalid)) => Err(napi::Error::new(
1400      napi::Status::InvalidArg,
1401      format!(
1402        "The function returned `{}`, but expected `object`.",
1403        invalid.value_type.to_string().to_ascii_lowercase()
1404      ),
1405    )),
1406    Err(error) => Err(error),
1407  }
1408}
1409
1410fn wake_host_timer_safely(waker: std::task::Waker) {
1411  // Host eviction runs from a napi env cleanup hook. A custom RawWaker must
1412  // not unwind through that FFI boundary or abort the remaining timer drain.
1413  // Borrowing for the wake keeps a panicking wake and a panicking destructor
1414  // in separate containment boundaries.
1415  run_host_timer_cleanup_safely(|| waker.wake_by_ref());
1416  run_host_timer_cleanup_safely(|| drop(waker));
1417}
1418
1419fn drop_host_timer_waker_safely(waker: std::task::Waker) {
1420  run_host_timer_cleanup_safely(|| drop(waker));
1421}
1422
1423fn recover_host_timer_failure(recover: impl FnOnce(), diagnostic: std::fmt::Arguments<'_>) {
1424  recover();
1425  run_host_timer_cleanup_safely(|| {
1426    use std::io::Write as _;
1427    let _ = writeln!(std::io::stderr().lock(), "{diagnostic}");
1428  });
1429}
1430
1431impl JsTimerHostInner {
1432  fn lock_pending(
1433    &self,
1434  ) -> std::sync::MutexGuard<'_, rustc_hash::FxHashMap<TimerId, PendingHostTimer>> {
1435    self
1436      .pending
1437      .lock()
1438      .unwrap_or_else(std::sync::PoisonError::into_inner)
1439  }
1440
1441  fn register_pending(
1442    &self,
1443    id: TimerId,
1444    waker: std::task::Waker,
1445  ) -> Result<PendingHostTimerRegistration, std::task::Waker> {
1446    // Eviction publishes `dead` before taking this same pending-map mutex to
1447    // drain. Registration therefore either enters the map before that drain,
1448    // or observes the dead/aborted host afterward and returns its waker intact.
1449    register_pending_host_timer_if_live(&self.pending, &self.relay_ids, id, waker, || {
1450      self.is_live()
1451    })
1452  }
1453
1454  /// Can this host still deliver wakes? The `aborted` probe reads the
1455  /// threadsafe function's own closing flag, so a dying env is detected even
1456  /// before any eviction path ran.
1457  fn is_live(&self) -> bool {
1458    !self.dead.load(std::sync::atomic::Ordering::SeqCst)
1459      && !self.callback.aborted()
1460      && !self.cancel_callback.aborted()
1461  }
1462
1463  fn handle_cancellation_failure(
1464    self: &std::sync::Arc<Self>,
1465    relay_id: u32,
1466    relay_health: &HostTimerRelayHealth,
1467    error: napi::Error,
1468    queue_failure: bool,
1469  ) {
1470    let operation = if queue_failure {
1471      "could not be queued"
1472    } else {
1473      "failed"
1474    };
1475    if relay_health.cancellation_accounting() == RelayCancellationAccounting::CleanupOnly {
1476      recover_host_timer_failure(
1477        || {},
1478        format_args!(
1479          "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} during \
1480           cleanup: {error}"
1481        ),
1482      );
1483      return;
1484    }
1485
1486    match record_host_timer_failure(
1487      &self.transient_failures,
1488      relay_health,
1489      error.status,
1490      self.is_live(),
1491    ) {
1492      HostTimerFailureAction::Duplicate => {
1493        recover_host_timer_failure(
1494          || {},
1495          format_args!(
1496            "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} after \
1497             this relay failure was already accounted: {error}"
1498          ),
1499        );
1500      }
1501      HostTimerFailureAction::EvictHost => {
1502        recover_host_timer_failure(
1503          || self.evict(),
1504          format_args!(
1505            "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} (host \
1506             gone, evicting): {error}"
1507          ),
1508        );
1509      }
1510      HostTimerFailureAction::EvictHostAfterStrikes(strikes) => {
1511        recover_host_timer_failure(
1512          || self.evict(),
1513          format_args!(
1514            "napi-async-runtime: host timer cancellation callback {operation} {strikes} times in a row, \
1515             evicting this timer host (relay {relay_id}): {error}"
1516          ),
1517        );
1518      }
1519      HostTimerFailureAction::Retry(strikes) => {
1520        recover_host_timer_failure(
1521          || {},
1522          format_args!(
1523            "napi-async-runtime: host timer cancellation callback {operation} for relay {relay_id} \
1524             ({strikes}/{HOST_TIMER_MAX_TRANSIENT_FAILURES} before eviction): {error}"
1525          ),
1526        );
1527      }
1528    }
1529  }
1530
1531  fn cancel_relay(
1532    self: &std::sync::Arc<Self>,
1533    relay_id: u32,
1534    relay_health: std::sync::Arc<HostTimerRelayHealth>,
1535  ) {
1536    let callback_inner = std::sync::Arc::clone(self);
1537    let callback_health = std::sync::Arc::clone(&relay_health);
1538    let status = self.cancel_callback.call_with_return_value(
1539      FnArgs { data: (relay_id,) },
1540      ThreadsafeFunctionCallMode::NonBlocking,
1541      move |result, _| {
1542        let error = match result {
1543          Ok(napi::Either::A(())) => {
1544            if callback_health.cancellation_accounting() == RelayCancellationAccounting::HostHealth
1545            {
1546              reset_host_timer_failures_after_success(
1547                &callback_inner.transient_failures,
1548                &callback_health,
1549              );
1550            }
1551            return Ok(());
1552          }
1553          Ok(napi::Either::B(invalid)) => napi::Error::new(
1554            napi::Status::InvalidArg,
1555            format!(
1556              "The timer cancellation callback returned `{}`, but expected `undefined`.",
1557              invalid.value_type.to_string().to_ascii_lowercase()
1558            ),
1559          ),
1560          Err(error) => error,
1561        };
1562        callback_inner.handle_cancellation_failure(relay_id, &callback_health, error, false);
1563        Ok(())
1564      },
1565    );
1566    if status != napi::Status::Ok {
1567      let error = napi::Error::new(status, "Threadsafe timer cancellation callback call failed");
1568      self.handle_cancellation_failure(relay_id, &relay_health, error, true);
1569    }
1570  }
1571
1572  fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool {
1573    let mut pending = self.lock_pending();
1574    match pending.get_mut(&id) {
1575      Some(slot) if slot.relay_id == relay_id => {
1576        slot.schedule_state = RelayScheduleState::CallbackComplete;
1577        false
1578      }
1579      _ => true,
1580    }
1581  }
1582
1583  fn take_pending_relay(
1584    &self,
1585    id: TimerId,
1586    relay_id: u32,
1587    accounting: RelayCancellationAccounting,
1588  ) -> Option<PendingHostTimer> {
1589    take_pending_host_timer(&self.pending, id, relay_id, accounting)
1590  }
1591
1592  async fn invoke_schedule_callback(
1593    self: &std::sync::Arc<Self>,
1594    id: TimerId,
1595    relay_id: u32,
1596    ms: f64,
1597    relay_health: std::sync::Arc<HostTimerRelayHealth>,
1598  ) -> napi::Result<Promise<()>> {
1599    let (sender, receiver) = futures::channel::oneshot::channel();
1600    let callback_inner = std::sync::Arc::clone(self);
1601    let status = self.callback.call_with_return_value(
1602      FnArgs {
1603        data: (relay_id, ms),
1604      },
1605      ThreadsafeFunctionCallMode::NonBlocking,
1606      move |result, _| {
1607        complete_relay_schedule_callback(
1608          &callback_inner,
1609          id,
1610          relay_id,
1611          &relay_health,
1612          result,
1613          move |result| {
1614            let _ = sender.send(normalize_timer_schedule_result(result));
1615          },
1616        );
1617        Ok(())
1618      },
1619    );
1620    if status != napi::Status::Ok {
1621      return Err(napi::Error::new(
1622        status,
1623        "Threadsafe function call_async_catch failed",
1624      ));
1625    }
1626    receiver.await.map_err(|_| {
1627      napi::Error::new(
1628        napi::Status::GenericFailure,
1629        "Receive value from threadsafe function sender failed",
1630      )
1631    })?
1632  }
1633
1634  /// Remove this host from timer duty: latch `dead`, drop the registry
1635  /// entry, and wake every sleep armed here so each re-polls onto the next
1636  /// live registrant (absolute deadlines preserve the remaining time; with no
1637  /// live registrant left the re-poll fails LOUD in the scheduler).
1638  /// Idempotent -- the cleanup hook, the `is_live` race path, and the
1639  /// relay-failure backstop may all reach it.
1640  fn evict(self: &std::sync::Arc<Self>) {
1641    self.dead.store(true, std::sync::atomic::Ordering::SeqCst);
1642    let registration = self
1643      .registration
1644      .lock()
1645      .unwrap_or_else(std::sync::PoisonError::into_inner)
1646      .take();
1647    JS_TIMER_HOSTS
1648      .lock()
1649      .unwrap_or_else(std::sync::PoisonError::into_inner)
1650      .remove(&self.host_registration);
1651    if let Some(id) = registration {
1652      unregister_timer_driver(id);
1653    }
1654    let pending = take_pending_host_timers(&self.pending, RelayCancellationAccounting::CleanupOnly);
1655    retire_pending_relays(self, pending);
1656  }
1657}
1658
1659impl PendingRelayState for JsTimerHostInner {
1660  fn take_pending_relay(
1661    &self,
1662    id: TimerId,
1663    relay_id: u32,
1664    accounting: RelayCancellationAccounting,
1665  ) -> Option<PendingHostTimer> {
1666    JsTimerHostInner::take_pending_relay(self, id, relay_id, accounting)
1667  }
1668
1669  fn mark_relay_callback_complete(&self, id: TimerId, relay_id: u32) -> bool {
1670    JsTimerHostInner::mark_relay_callback_complete(self, id, relay_id)
1671  }
1672
1673  fn cancel_relay(
1674    self: &std::sync::Arc<Self>,
1675    relay_id: u32,
1676    relay_health: std::sync::Arc<HostTimerRelayHealth>,
1677  ) {
1678    JsTimerHostInner::cancel_relay(self, relay_id, relay_health);
1679  }
1680}
1681
1682impl TimerDriver for JsTimerHost {
1683  fn register(&self, id: TimerId, deadline: std::time::Instant, waker: std::task::Waker) {
1684    if !self.inner.is_live() {
1685      // The selecting poll raced this host's death: make sure the eviction
1686      // bookkeeping ran, then wake immediately so the sleep re-selects from
1687      // the registry (which no longer offers this host).
1688      self.inner.evict();
1689      wake_host_timer_safely(waker);
1690      return;
1691    }
1692    let (relay_id, cancellation, relay_health) = match self.inner.register_pending(id, waker) {
1693      Err(waker) => {
1694        self.inner.evict();
1695        wake_host_timer_safely(waker);
1696        return;
1697      }
1698      Ok(PendingHostTimerRegistration::Refreshed(replaced_waker)) => {
1699        // A custom RawWaker destructor may re-enter timer code. `register_pending`
1700        // returns the old waker so its destructor runs after `pending` unlocks.
1701        drop_host_timer_waker_safely(replaced_waker);
1702        return;
1703      }
1704      Ok(PendingHostTimerRegistration::Armed {
1705        relay_id,
1706        cancellation,
1707        relay_health,
1708      }) => (relay_id, cancellation, relay_health),
1709      Ok(PendingHostTimerRegistration::Exhausted(waker)) => {
1710        recover_host_timer_failure(
1711          || {
1712            self.inner.evict();
1713            wake_host_timer_safely(waker);
1714          },
1715          format_args!(
1716            "napi-async-runtime: host timer relay id space exhausted, evicting this timer host"
1717          ),
1718        );
1719        return;
1720      }
1721    };
1722    let ms = deadline
1723      .saturating_duration_since(std::time::Instant::now())
1724      .as_secs_f64()
1725      * 1000.0;
1726    let inner = std::sync::Arc::clone(&self.inner);
1727    // Build the guard outside the async body so destroying a never-polled
1728    // relay runnable still retires its pending timer.
1729    let relay_drop_guard = PendingRelayDropGuard::new(std::sync::Arc::clone(&inner), id, relay_id);
1730    let submission = try_spawn_detached(async move {
1731      let mut relay_drop_guard = relay_drop_guard;
1732      let schedule_started = std::sync::atomic::AtomicBool::new(false);
1733      let schedule = async {
1734        schedule_started.store(true, std::sync::atomic::Ordering::Release);
1735        match inner
1736          .invoke_schedule_callback(id, relay_id, ms, std::sync::Arc::clone(&relay_health))
1737          .await
1738        {
1739          Ok(promise) => promise.await,
1740          Err(error) => Err(error),
1741        }
1742      };
1743      futures::pin_mut!(cancellation);
1744      futures::pin_mut!(schedule);
1745      let result = match futures::future::select(cancellation, schedule).await {
1746        futures::future::Either::Left((_cancelled, schedule)) => {
1747          // Native cancellation won the race. The pending entry is already
1748          // gone (`cancel` removes it before signalling this oneshot), so the
1749          // drop guard has nothing left to clean up either way.
1750          relay_drop_guard.disarm();
1751          if !schedule_started.load(std::sync::atomic::Ordering::Acquire) {
1752            // The schedule callback was never invoked (the cancellation was
1753            // already pending at this task's first poll), so there is no host
1754            // result to observe -- and invoking the callback now would arm a
1755            // JS timer that nothing will ever cancel.
1756            return;
1757          }
1758          // The schedule callback is in flight. Conforming hosts settle the
1759          // schedule promise on cancellation (see `cancelTimer` in
1760          // workerd-timer-host.ts and the node host in timer-host.ts), and
1761          // that settlement shares this relay's health record with the
1762          // cancellation callback: whichever failure reaches Rust first
1763          // consumes the relay's one strike and the other is reported as
1764          // diagnostic-only (`Duplicate`, "already accounted"). Dropping the
1765          // schedule future here instead would leave the schedule-side
1766          // result unobserved and the shared accounting blind.
1767          schedule.await
1768        }
1769        futures::future::Either::Right((result, _cancellation)) => result,
1770      };
1771      match result {
1772        Ok(()) => {
1773          reset_host_timer_failures_after_success(&inner.transient_failures, &relay_health);
1774          if let Some(pending) =
1775            inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1776          {
1777            wake_host_timer_safely(pending.waker);
1778          }
1779          relay_drop_guard.disarm();
1780        }
1781        Err(error) => {
1782          let action = record_host_timer_failure(
1783            &inner.transient_failures,
1784            &relay_health,
1785            error.status,
1786            inner.is_live(),
1787          );
1788          match action {
1789            HostTimerFailureAction::EvictHost => {
1790              recover_host_timer_failure(
1791                || inner.evict(),
1792                format_args!(
1793                  "napi-async-runtime: host timer callback failed (host gone, evicting): {error}"
1794                ),
1795              );
1796            }
1797            HostTimerFailureAction::EvictHostAfterStrikes(strikes) => {
1798              recover_host_timer_failure(
1799                || inner.evict(),
1800                format_args!(
1801                  "napi-async-runtime: host timer callback failed {strikes} times in a row, evicting this \
1802                   timer host: {error}"
1803                ),
1804              );
1805            }
1806            HostTimerFailureAction::Retry(strikes) => {
1807              recover_host_timer_failure(
1808                || {
1809                  if let Some(pending) =
1810                    inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1811                  {
1812                    // The callback may have synchronously armed a timeout before
1813                    // throwing, returning the wrong type, or producing a
1814                    // rejected Promise. Cleanup cancellation must not add a
1815                    // second strike for this same relay failure.
1816                    retire_pending_relay(&inner, pending);
1817                  }
1818                },
1819                format_args!(
1820                  "napi-async-runtime: host timer callback failed \
1821                   ({strikes}/{HOST_TIMER_MAX_TRANSIENT_FAILURES} before eviction): {error}"
1822                ),
1823              );
1824            }
1825            HostTimerFailureAction::Duplicate => {
1826              recover_host_timer_failure(
1827                || {
1828                  if let Some(pending) =
1829                    inner.take_pending_relay(id, relay_id, RelayCancellationAccounting::CleanupOnly)
1830                  {
1831                    retire_pending_relay(&inner, pending);
1832                  }
1833                },
1834                format_args!(
1835                  "napi-async-runtime: host timer callback failed after this relay failure was already \
1836                   accounted: {error}"
1837                ),
1838              );
1839            }
1840          }
1841          relay_drop_guard.disarm();
1842        }
1843      }
1844    });
1845    if let Err(rejected_relay) = submission {
1846      // The rejected future owns the exact-match guard. Dropping it removes
1847      // and wakes the pending timer before returning to the caller.
1848      drop(rejected_relay);
1849    }
1850  }
1851
1852  fn cancel(&self, id: TimerId) {
1853    let pending = {
1854      let mut pending = self.inner.lock_pending();
1855      if let Some(timer) = pending.get(&id) {
1856        timer
1857          .relay_health
1858          .set_cancellation_accounting(RelayCancellationAccounting::HostHealth);
1859      }
1860      pending.remove(&id)
1861    };
1862    if let Some(mut pending) = pending {
1863      pending.signal_native_cancellation();
1864      if pending.schedule_state == RelayScheduleState::CallbackComplete {
1865        let relay_health = std::sync::Arc::clone(&pending.relay_health);
1866        self.inner.cancel_relay(pending.relay_id, relay_health);
1867      }
1868      drop_host_timer_waker_safely(pending.waker);
1869    }
1870  }
1871
1872  fn is_live(&self) -> bool {
1873    self.inner.is_live()
1874  }
1875
1876  fn on_swept(&self) {
1877    // The registry's selection sweep noticed this host's death (the
1878    // `aborted` probe can fire before the env-cleanup hook runs): run the
1879    // full eviction so every sleep pending here is woken into re-selection
1880    // instead of stranded. Idempotent with the hook and the relay backstop.
1881    self.inner.evict();
1882  }
1883}
1884
1885#[napi(
1886  ts_args_type = "registrationHigh: number, registrationLow: number, schedule: (id: number, ms: number) => Promise<void>, cancel: (id: number) => void"
1887)]
1888/// Install the host timer callback backing the shared async runtime's
1889/// CurrentThread timers. Called at import by every
1890/// binding-loading JS entry with paired setTimeout/clearTimeout callbacks; each
1891/// importing env (main thread and workers alike) registers its own host, and
1892/// every live host receives each timer.
1893pub fn register_timer_host(
1894  env: &napi::Env,
1895  registration_high: u32,
1896  registration_low: u32,
1897  schedule: JsCallback<FnArgs<(u32, f64)>, Promise<()>>,
1898  cancel: JsCallback<FnArgs<(u32,)>, ()>,
1899) -> napi::Result<()> {
1900  let host_registration = claim_host_registration_id(registration_high, registration_low)?;
1901  let inner = std::sync::Arc::new(JsTimerHostInner {
1902    callback: schedule,
1903    cancel_callback: cancel,
1904    pending: std::sync::Mutex::default(),
1905    relay_ids: RelayIdAllocator::default(),
1906    dead: std::sync::atomic::AtomicBool::new(false),
1907    host_registration,
1908    registration: std::sync::Mutex::default(),
1909    transient_failures: std::sync::atomic::AtomicU32::new(0),
1910  });
1911  if !install_host_driver_registration(
1912    &inner.dead,
1913    &inner.registration,
1914    || {
1915      register_timer_driver(std::sync::Arc::new(JsTimerHost {
1916        inner: std::sync::Arc::clone(&inner),
1917      }))
1918    },
1919    || {
1920      JS_TIMER_HOSTS
1921        .lock()
1922        .unwrap_or_else(std::sync::PoisonError::into_inner)
1923        .insert(host_registration, std::sync::Arc::downgrade(&inner));
1924    },
1925    unregister_timer_driver,
1926  ) {
1927    return Err(napi::Error::new(
1928      napi::Status::GenericFailure,
1929      "The CurrentThread timer host was evicted during registration",
1930    ));
1931  }
1932  // Proactive eviction at env teardown (worker exit): the primary lifetime
1933  // mechanism; the `aborted` probe and the relay-failure path in the driver
1934  // are the backstops for anything the hook cannot reach in time.
1935  let hook_inner = std::sync::Arc::clone(&inner);
1936  install_cleanup_hook_or_rollback(
1937    || {
1938      env.add_env_cleanup_hook(hook_inner, |inner| {
1939        run_host_timer_cleanup_safely(|| inner.evict());
1940      })
1941    },
1942    || run_host_timer_cleanup_safely(|| inner.evict()),
1943  )?;
1944  Ok(())
1945}
1946
1947#[napi]
1948/// Evict exactly one callback installed by `registerTimerHost`.
1949/// Pending sleeps are woken so they can reselect another live environment.
1950pub fn unregister_timer_host(registration_high: u32, registration_low: u32) {
1951  let id = BindingHostRegistration::id(registration_high, registration_low);
1952  release_host_registration_id(id);
1953  if let Some(inner) = registered_timer_host(id) {
1954    inner.evict();
1955  }
1956}
1957
1958/// Configure the shared scheduler and register it as this addon's napi
1959/// [`AsyncRuntime`] backend.
1960///
1961/// Call this from the host's own `#[napi_derive::module_init]` hook, after
1962/// resolving the process configuration (environment variables, defaults) into
1963/// [`RuntimeOptions`]. The scheduler never reads the environment itself; the
1964/// host owns that resolution. `configure` validates and normalizes the
1965/// options, and the runtime controller's options remain the reporting
1966/// authority for `getAsyncRuntimeConfig`.
1967pub fn install(options: RuntimeOptions) -> napi::Result<()> {
1968  configure(options).map_err(|error| napi::Error::from_reason(error.to_string()))?;
1969  register_async_runtime(SharedAsyncRuntime);
1970  Ok(())
1971}
1972
1973/// Resolve an env-derived thread count: parse the raw value, treat a missing,
1974/// non-numeric, OR zero value as "unset" and fall back to `default`, then
1975/// clamp the result to `maximum`. A `0` must never survive here because the
1976/// host's module-init path feeds this count into a constructor that rejects
1977/// `0`: [`RuntimeOptions`] validation rejects a `0` thread count, which would
1978/// panic an `expect()` during addon load over what may be a simple typo in an
1979/// environment variable.
1980pub fn resolve_thread_count(raw: Option<String>, default: usize, maximum: usize) -> usize {
1981  assert!(default > 0, "the default thread count must be positive");
1982  assert!(maximum > 0, "the maximum thread count must be positive");
1983  raw
1984    .and_then(|value| value.parse::<usize>().ok())
1985    .filter(|&value| value != 0)
1986    .unwrap_or(default)
1987    .min(maximum)
1988}
1989
1990#[cfg(test)]
1991mod tests {
1992  use super::{
1993    BindingHostRegistration, BindingRuntimeFlavor, BindingRuntimeOptions,
1994    HOST_TIMER_MAX_TRANSIENT_FAILURES, HostTimerFailureAction, HostTimerRelayHealth,
1995    MAX_SAFE_JS_INTEGER, NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK,
1996    NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK, NativeCurrentThreadTaskHostInner,
1997    NativeCurrentThreadTaskHostPayload, PendingHostTimer, PendingHostTimerRegistration,
1998    PendingRelayDropGuard, PendingRelayState, RelayCancellationAccounting, RelayIdAllocator,
1999    RelayScheduleState, SharedAsyncRuntime, call_native_current_thread_task_host,
2000    claim_host_registration_id, complete_relay_schedule_callback,
2001    get_current_thread_task_host_contract_version, install_cleanup_hook_or_rollback,
2002    install_host_driver_registration, record_host_timer_failure, recover_host_timer_failure,
2003    register_pending_host_timer, register_pending_host_timer_if_live,
2004    reserve_current_thread_host_registration, reset_host_timer_failures_after_success,
2005    resolve_thread_count, retire_pending_relay, retire_pending_relays, safe_js_number,
2006    take_pending_host_timers, unregister_current_thread_task_host, wake_host_timer_safely,
2007  };
2008
2009  #[derive(Default)]
2010  struct TestPendingRelayState {
2011    pending:
2012      std::sync::Mutex<rustc_hash::FxHashMap<crate::async_runtime::TimerId, PendingHostTimer>>,
2013    relay_ids: RelayIdAllocator,
2014    cancelled_relays: std::sync::Mutex<Vec<u32>>,
2015    panic_on_cancel: std::sync::atomic::AtomicBool,
2016  }
2017
2018  impl PendingRelayState for TestPendingRelayState {
2019    fn take_pending_relay(
2020      &self,
2021      id: crate::async_runtime::TimerId,
2022      relay_id: u32,
2023      accounting: RelayCancellationAccounting,
2024    ) -> Option<PendingHostTimer> {
2025      let mut pending = self
2026        .pending
2027        .lock()
2028        .unwrap_or_else(std::sync::PoisonError::into_inner);
2029      if pending
2030        .get(&id)
2031        .is_some_and(|slot| slot.relay_id == relay_id)
2032      {
2033        pending
2034          .get(&id)
2035          .expect("the matching test relay must remain present")
2036          .relay_health
2037          .set_cancellation_accounting(accounting);
2038        pending.remove(&id)
2039      } else {
2040        None
2041      }
2042    }
2043
2044    fn mark_relay_callback_complete(
2045      &self,
2046      id: crate::async_runtime::TimerId,
2047      relay_id: u32,
2048    ) -> bool {
2049      let mut pending = self
2050        .pending
2051        .lock()
2052        .unwrap_or_else(std::sync::PoisonError::into_inner);
2053      match pending.get_mut(&id) {
2054        Some(slot) if slot.relay_id == relay_id => {
2055          slot.schedule_state = RelayScheduleState::CallbackComplete;
2056          false
2057        }
2058        _ => true,
2059      }
2060    }
2061
2062    fn cancel_relay(
2063      self: &std::sync::Arc<Self>,
2064      relay_id: u32,
2065      _relay_health: std::sync::Arc<HostTimerRelayHealth>,
2066    ) {
2067      self
2068        .cancelled_relays
2069        .lock()
2070        .unwrap_or_else(std::sync::PoisonError::into_inner)
2071        .push(relay_id);
2072      assert!(
2073        !self
2074          .panic_on_cancel
2075          .load(std::sync::atomic::Ordering::SeqCst),
2076        "intentional relay cancellation panic"
2077      );
2078    }
2079  }
2080
2081  fn test_pending_relay_guard(
2082    state: &std::sync::Arc<TestPendingRelayState>,
2083    id: crate::async_runtime::TimerId,
2084    waker: std::task::Waker,
2085    schedule_state: RelayScheduleState,
2086  ) -> (
2087    u32,
2088    std::sync::Arc<HostTimerRelayHealth>,
2089    PendingRelayDropGuard<TestPendingRelayState>,
2090  ) {
2091    let relay_id = state
2092      .relay_ids
2093      .reserve()
2094      .expect("the test relay id must be available");
2095    let relay_health = std::sync::Arc::new(HostTimerRelayHealth::default());
2096    let replaced = state
2097      .pending
2098      .lock()
2099      .unwrap_or_else(std::sync::PoisonError::into_inner)
2100      .insert(
2101        id,
2102        PendingHostTimer {
2103          cancellation: None,
2104          relay_health: std::sync::Arc::clone(&relay_health),
2105          relay_id,
2106          waker,
2107          schedule_state,
2108        },
2109      );
2110    assert!(replaced.is_none(), "the test timer id must be vacant");
2111    (
2112      relay_id,
2113      relay_health,
2114      PendingRelayDropGuard::new(std::sync::Arc::clone(state), id, relay_id),
2115    )
2116  }
2117
2118  struct BudgetBoundaryTimerWait {
2119    state: std::sync::Arc<TestPendingRelayState>,
2120    timer_id: crate::async_runtime::TimerId,
2121    schedule_state: RelayScheduleState,
2122    registered: bool,
2123  }
2124
2125  impl std::future::Future for BudgetBoundaryTimerWait {
2126    type Output = ();
2127
2128    fn poll(
2129      mut self: std::pin::Pin<&mut Self>,
2130      cx: &mut std::task::Context<'_>,
2131    ) -> std::task::Poll<Self::Output> {
2132      assert!(
2133        !self.registered,
2134        "the timer wait must be cancelled before a second poll"
2135      );
2136      self.registered = true;
2137      let state = std::sync::Arc::clone(&self.state);
2138      let timer_id = self.timer_id;
2139      let (_, _, relay_drop_guard) =
2140        test_pending_relay_guard(&state, timer_id, cx.waker().clone(), self.schedule_state);
2141      crate::async_runtime::spawn_detached(async move {
2142        let mut relay_drop_guard = relay_drop_guard;
2143        std::future::pending::<()>().await;
2144        relay_drop_guard.disarm();
2145      });
2146      std::task::Poll::Pending
2147    }
2148  }
2149
2150  #[test]
2151  fn current_thread_task_host_contract_version_is_stable() {
2152    assert_eq!(get_current_thread_task_host_contract_version(), 4);
2153  }
2154
2155  #[test]
2156  fn host_registration_reservations_are_exact_and_single_use() {
2157    let claimed = reserve_current_thread_host_registration().unwrap();
2158    let claimed_id = BindingHostRegistration::id(claimed.high, claimed.low);
2159    assert_eq!(
2160      claim_host_registration_id(claimed.high, claimed.low).unwrap(),
2161      claimed_id,
2162      "the exact reserved capability must be claimable once"
2163    );
2164    assert!(
2165      claim_host_registration_id(claimed.high, claimed.low).is_err(),
2166      "a consumed registration capability must not be reusable"
2167    );
2168
2169    let released = reserve_current_thread_host_registration().unwrap();
2170    unregister_current_thread_task_host(released.high, released.low);
2171    assert!(
2172      claim_host_registration_id(released.high, released.low).is_err(),
2173      "unregister must release a reservation before installation"
2174    );
2175  }
2176
2177  fn native_task_host_with_raw_owner(raw: usize) -> NativeCurrentThreadTaskHostInner {
2178    NativeCurrentThreadTaskHostInner {
2179      threadsafe_function: std::sync::Mutex::new(Some(raw)),
2180      dead: std::sync::atomic::AtomicBool::new(false),
2181      environment_closing: std::sync::atomic::AtomicBool::new(false),
2182      host_registration: u64::MAX,
2183      registration: std::sync::Mutex::default(),
2184    }
2185  }
2186
2187  #[test]
2188  fn native_task_host_competing_release_paths_retire_the_raw_owner_once() {
2189    let inner = std::sync::Arc::new(native_task_host_with_raw_owner(1));
2190    let releases = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2191    let barrier = std::sync::Arc::new(std::sync::Barrier::new(3));
2192    let mut threads = Vec::new();
2193
2194    for mode in [
2195      napi::sys::ThreadsafeFunctionReleaseMode::release,
2196      napi::sys::ThreadsafeFunctionReleaseMode::abort,
2197    ] {
2198      let inner = std::sync::Arc::clone(&inner);
2199      let releases = std::sync::Arc::clone(&releases);
2200      let barrier = std::sync::Arc::clone(&barrier);
2201      threads.push(std::thread::spawn(move || {
2202        barrier.wait();
2203        inner.release_threadsafe_function_with(mode, |threadsafe_function, _| {
2204          assert_eq!(threadsafe_function as usize, 1);
2205          releases.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2206          napi::sys::Status::napi_ok
2207        });
2208      }));
2209    }
2210
2211    barrier.wait();
2212    for thread in threads {
2213      thread.join().unwrap();
2214    }
2215    assert_eq!(releases.load(std::sync::atomic::Ordering::SeqCst), 1);
2216    assert!(
2217      inner
2218        .threadsafe_function
2219        .lock()
2220        .unwrap_or_else(std::sync::PoisonError::into_inner)
2221        .is_none()
2222    );
2223  }
2224
2225  #[test]
2226  fn native_task_host_closing_call_retires_owner_without_a_second_release() {
2227    let inner = native_task_host_with_raw_owner(1);
2228    let status = inner
2229      .call_threadsafe_function_with(std::ptr::null_mut(), |threadsafe_function, _| {
2230        assert_eq!(threadsafe_function as usize, 1);
2231        napi::sys::Status::napi_closing
2232      })
2233      .expect("the fake TSFN owner must be callable");
2234
2235    assert_eq!(status, napi::sys::Status::napi_closing);
2236    assert!(
2237      inner
2238        .environment_closing
2239        .load(std::sync::atomic::Ordering::SeqCst)
2240    );
2241    let releases = std::sync::atomic::AtomicUsize::new(0);
2242    inner.release_threadsafe_function_with(
2243      napi::sys::ThreadsafeFunctionReleaseMode::release,
2244      |_, _| {
2245        releases.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2246        napi::sys::Status::napi_ok
2247      },
2248    );
2249    assert_eq!(
2250      releases.load(std::sync::atomic::Ordering::SeqCst),
2251      0,
2252      "napi_closing already retired the initial acquisition and invalidated the pointer"
2253    );
2254  }
2255
2256  #[test]
2257  fn host_registration_capability_round_trips_without_precision_loss() {
2258    for id in [1, u64::from(u32::MAX), u64::from(u32::MAX) + 1, u64::MAX] {
2259      let registration = BindingHostRegistration::from_id(id);
2260      assert_eq!(
2261        BindingHostRegistration::id(registration.high, registration.low),
2262        id
2263      );
2264    }
2265  }
2266
2267  #[test]
2268  fn binding_runtime_options_convert_to_a_partial_core_patch() {
2269    use crate::async_runtime::{RuntimeFlavor, RuntimeOptionsPatch};
2270
2271    let patch = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2272      flavor: Some(BindingRuntimeFlavor::CurrentThread),
2273      worker_threads: None,
2274      max_blocking_tasks: Some(7.0),
2275    })
2276    .expect("valid binding options must convert");
2277
2278    assert!(matches!(patch.flavor, Some(RuntimeFlavor::CurrentThread)));
2279    assert_eq!(patch.worker_threads, None);
2280    assert_eq!(patch.max_blocking_tasks, Some(7));
2281  }
2282
2283  #[test]
2284  fn binding_runtime_options_reject_unsafe_thread_counts() {
2285    use crate::async_runtime::RuntimeOptionsPatch;
2286
2287    for value in [
2288      -1.0,
2289      0.0,
2290      1.5,
2291      f64::NAN,
2292      f64::INFINITY,
2293      257.0,
2294      f64::from(u32::MAX) + 1.0,
2295    ] {
2296      let error = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2297        flavor: None,
2298        worker_threads: Some(value),
2299        max_blocking_tasks: None,
2300      })
2301      .expect_err("invalid worker thread counts must be rejected");
2302      assert!(
2303        error
2304          .reason
2305          .contains("`workerThreads` must be a positive integer"),
2306        "unexpected validation error: {}",
2307        error.reason
2308      );
2309    }
2310
2311    for value in [-1.0, 257.0] {
2312      let error = RuntimeOptionsPatch::try_from(BindingRuntimeOptions {
2313        flavor: None,
2314        worker_threads: None,
2315        max_blocking_tasks: Some(value),
2316      })
2317      .expect_err("invalid blocking task counts must be rejected");
2318      assert!(
2319        error
2320          .reason
2321          .contains("`maxBlockingTasks` must be a positive integer"),
2322        "unexpected validation error: {}",
2323        error.reason
2324      );
2325    }
2326  }
2327
2328  #[test]
2329  fn runtime_metrics_preserve_safe_javascript_integer_range() {
2330    const BEYOND_U32_JS_NUMBER: f64 = 4_294_967_296.0;
2331    const MAX_SAFE_JS_NUMBER: f64 = 9_007_199_254_740_991.0;
2332
2333    let beyond_u32 = u64::from(u32::MAX) + 1;
2334    assert_eq!(
2335      safe_js_number(beyond_u32).to_bits(),
2336      BEYOND_U32_JS_NUMBER.to_bits()
2337    );
2338    assert_eq!(
2339      safe_js_number(MAX_SAFE_JS_INTEGER).to_bits(),
2340      MAX_SAFE_JS_NUMBER.to_bits()
2341    );
2342    assert_eq!(
2343      safe_js_number(MAX_SAFE_JS_INTEGER + 1).to_bits(),
2344      MAX_SAFE_JS_NUMBER.to_bits(),
2345      "metrics must remain exact JavaScript numbers instead of silently losing integer precision"
2346    );
2347  }
2348
2349  #[test]
2350  fn relay_ids_fail_closed_without_reuse_at_u32_exhaustion() {
2351    let allocator = RelayIdAllocator::default();
2352    allocator
2353      .next
2354      .store(u64::from(u32::MAX), std::sync::atomic::Ordering::Relaxed);
2355
2356    assert_eq!(allocator.reserve(), Ok(u32::MAX));
2357    assert!(
2358      allocator.reserve().is_err(),
2359      "the allocator must fail instead of wrapping to a reusable id"
2360    );
2361    assert!(
2362      allocator.reserve().is_err(),
2363      "exhaustion must remain stable instead of spinning or advancing toward wraparound"
2364    );
2365
2366    let pending = std::sync::Mutex::new(rustc_hash::FxHashMap::default());
2367    let waker = futures::task::noop_waker();
2368    let exhausted_waker = match register_pending_host_timer(&pending, &allocator, 1, waker.clone())
2369    {
2370      PendingHostTimerRegistration::Exhausted(waker) => waker,
2371      PendingHostTimerRegistration::Refreshed(_) | PendingHostTimerRegistration::Armed { .. } => {
2372        panic!("an exhausted allocator must reject a new pending timer")
2373      }
2374    };
2375    assert!(
2376      exhausted_waker.will_wake(&waker),
2377      "allocator failure must return the attempted timer's waker intact"
2378    );
2379    assert!(
2380      pending
2381        .lock()
2382        .unwrap_or_else(std::sync::PoisonError::into_inner)
2383        .is_empty(),
2384      "an exhausted allocator must not publish a pending timer without a relay capability"
2385    );
2386  }
2387
2388  #[test]
2389  fn host_timer_wake_and_drop_panics_are_contained_separately() {
2390    struct PanickingWakeAndDrop;
2391
2392    impl std::task::Wake for PanickingWakeAndDrop {
2393      fn wake(self: std::sync::Arc<Self>) {
2394        panic!("intentional host timer waker panic");
2395      }
2396
2397      fn wake_by_ref(self: &std::sync::Arc<Self>) {
2398        panic!("intentional host timer borrowed waker panic");
2399      }
2400    }
2401
2402    impl Drop for PanickingWakeAndDrop {
2403      fn drop(&mut self) {
2404        panic!("intentional host timer waker destructor panic");
2405      }
2406    }
2407
2408    let waker = std::task::Waker::from(std::sync::Arc::new(PanickingWakeAndDrop));
2409    wake_host_timer_safely(waker);
2410  }
2411
2412  #[test]
2413  fn relay_drop_guard_cleans_only_its_matching_pending_relay() {
2414    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2415
2416    impl std::task::Wake for CountingWake {
2417      fn wake(self: std::sync::Arc<Self>) {
2418        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2419      }
2420    }
2421
2422    let state = std::sync::Arc::new(TestPendingRelayState::default());
2423    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2424    let (relay_id, _, guard) = test_pending_relay_guard(
2425      &state,
2426      1,
2427      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2428        &wakes,
2429      )))),
2430      RelayScheduleState::CallbackComplete,
2431    );
2432    drop(guard);
2433
2434    assert!(
2435      state
2436        .pending
2437        .lock()
2438        .unwrap_or_else(std::sync::PoisonError::into_inner)
2439        .is_empty()
2440    );
2441    assert_eq!(
2442      *state
2443        .cancelled_relays
2444        .lock()
2445        .unwrap_or_else(std::sync::PoisonError::into_inner),
2446      [relay_id],
2447      "an armed relay must be cancelled before its waiter is woken"
2448    );
2449    assert_eq!(
2450      wakes.load(std::sync::atomic::Ordering::SeqCst),
2451      1,
2452      "terminal relay cleanup must wake the pending timer"
2453    );
2454
2455    let stale_wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2456    let (stale_relay_id, _, stale_guard) = test_pending_relay_guard(
2457      &state,
2458      2,
2459      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2460        &stale_wakes,
2461      )))),
2462      RelayScheduleState::AwaitingCallback,
2463    );
2464    let replacement_relay_id = state
2465      .relay_ids
2466      .reserve()
2467      .expect("the replacement relay id must be available");
2468    assert_ne!(
2469      replacement_relay_id, stale_relay_id,
2470      "relay ids must never be reused within one host"
2471    );
2472    let replaced = state
2473      .pending
2474      .lock()
2475      .unwrap_or_else(std::sync::PoisonError::into_inner)
2476      .insert(
2477        2,
2478        PendingHostTimer {
2479          cancellation: None,
2480          relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2481          relay_id: replacement_relay_id,
2482          waker: futures::task::noop_waker(),
2483          schedule_state: RelayScheduleState::CallbackComplete,
2484        },
2485      )
2486      .expect("the original relay must still be pending");
2487    drop(replaced);
2488    drop(stale_guard);
2489
2490    let replacement = state
2491      .pending
2492      .lock()
2493      .unwrap_or_else(std::sync::PoisonError::into_inner)
2494      .remove(&2)
2495      .expect("a stale guard must preserve the replacement relay");
2496    assert_eq!(replacement.relay_id, replacement_relay_id);
2497    assert_eq!(stale_wakes.load(std::sync::atomic::Ordering::SeqCst), 0);
2498    assert_eq!(
2499      *state
2500        .cancelled_relays
2501        .lock()
2502        .unwrap_or_else(std::sync::PoisonError::into_inner),
2503      [relay_id],
2504      "a stale guard must not cancel a replacement relay"
2505    );
2506    drop(replacement);
2507
2508    let disarmed_wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2509    let (disarmed_relay_id, _, mut disarmed_guard) = test_pending_relay_guard(
2510      &state,
2511      3,
2512      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2513        &disarmed_wakes,
2514      )))),
2515      RelayScheduleState::CallbackComplete,
2516    );
2517    disarmed_guard.disarm();
2518    drop(disarmed_guard);
2519    let disarmed_pending = state
2520      .pending
2521      .lock()
2522      .unwrap_or_else(std::sync::PoisonError::into_inner)
2523      .remove(&3)
2524      .expect("normal relay completion must retain ownership of pending cleanup");
2525    assert_eq!(disarmed_pending.relay_id, disarmed_relay_id);
2526    assert_eq!(disarmed_wakes.load(std::sync::atomic::Ordering::SeqCst), 0);
2527    assert_eq!(
2528      *state
2529        .cancelled_relays
2530        .lock()
2531        .unwrap_or_else(std::sync::PoisonError::into_inner),
2532      [relay_id],
2533      "a disarmed guard must not change normal relay cleanup semantics"
2534    );
2535    drop(disarmed_pending);
2536  }
2537
2538  #[test]
2539  fn relay_callback_completion_precedes_delivery_and_closes_terminal_drop_window() {
2540    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2541
2542    impl std::task::Wake for CountingWake {
2543      fn wake(self: std::sync::Arc<Self>) {
2544        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2545      }
2546    }
2547
2548    let state = std::sync::Arc::new(TestPendingRelayState::default());
2549    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2550    let (relay_id, relay_health, guard) = test_pending_relay_guard(
2551      &state,
2552      1,
2553      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2554        &wakes,
2555      )))),
2556      RelayScheduleState::AwaitingCallback,
2557    );
2558
2559    // Model napi-rs after the JavaScript schedule function returned and armed
2560    // its timeout, but before result delivery can re-poll the Rust relay.
2561    complete_relay_schedule_callback(&state, 1, relay_id, &relay_health, guard, drop);
2562
2563    assert!(
2564      state
2565        .pending
2566        .lock()
2567        .unwrap_or_else(std::sync::PoisonError::into_inner)
2568        .is_empty()
2569    );
2570    assert_eq!(
2571      *state
2572        .cancelled_relays
2573        .lock()
2574        .unwrap_or_else(std::sync::PoisonError::into_inner),
2575      [relay_id],
2576      "terminal drop during return-value delivery must cancel the synchronously armed timeout"
2577    );
2578    assert_eq!(wakes.load(std::sync::atomic::Ordering::SeqCst), 1);
2579  }
2580
2581  #[test]
2582  fn relay_callback_completion_finishes_cleanup_that_won_before_js_returned() {
2583    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2584
2585    impl std::task::Wake for CountingWake {
2586      fn wake(self: std::sync::Arc<Self>) {
2587        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2588      }
2589    }
2590
2591    let state = std::sync::Arc::new(TestPendingRelayState::default());
2592    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2593    let (relay_id, relay_health, guard) = test_pending_relay_guard(
2594      &state,
2595      1,
2596      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2597        &wakes,
2598      )))),
2599      RelayScheduleState::AwaitingCallback,
2600    );
2601
2602    drop(guard);
2603    assert!(
2604      state
2605        .cancelled_relays
2606        .lock()
2607        .unwrap_or_else(std::sync::PoisonError::into_inner)
2608        .is_empty(),
2609      "cancellation must not overtake a schedule callback that has not returned"
2610    );
2611
2612    complete_relay_schedule_callback(&state, 1, relay_id, &relay_health, (), drop);
2613    assert_eq!(
2614      *state
2615        .cancelled_relays
2616        .lock()
2617        .unwrap_or_else(std::sync::PoisonError::into_inner),
2618      [relay_id],
2619      "the callback return handshake must finish exact cancellation after timer creation"
2620    );
2621    assert_eq!(wakes.load(std::sync::atomic::Ordering::SeqCst), 1);
2622  }
2623
2624  #[test]
2625  fn relay_terminal_cleanup_contains_cancel_panics_and_still_wakes() {
2626    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2627
2628    impl std::task::Wake for CountingWake {
2629      fn wake(self: std::sync::Arc<Self>) {
2630        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2631      }
2632    }
2633
2634    let state = std::sync::Arc::new(TestPendingRelayState::default());
2635    state
2636      .panic_on_cancel
2637      .store(true, std::sync::atomic::Ordering::SeqCst);
2638    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2639    let (relay_id, _, guard) = test_pending_relay_guard(
2640      &state,
2641      1,
2642      std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2643        &wakes,
2644      )))),
2645      RelayScheduleState::CallbackComplete,
2646    );
2647
2648    drop(guard);
2649
2650    assert_eq!(
2651      *state
2652        .cancelled_relays
2653        .lock()
2654        .unwrap_or_else(std::sync::PoisonError::into_inner),
2655      [relay_id]
2656    );
2657    assert_eq!(
2658      wakes.load(std::sync::atomic::Ordering::SeqCst),
2659      1,
2660      "a panicking cancellation path must not suppress the pending timer wake"
2661    );
2662  }
2663
2664  #[test]
2665  fn relay_native_cancellation_settles_before_fallible_host_cancellation() {
2666    use futures::FutureExt as _;
2667
2668    let state = std::sync::Arc::new(TestPendingRelayState::default());
2669    state
2670      .panic_on_cancel
2671      .store(true, std::sync::atomic::Ordering::SeqCst);
2672    let registration = register_pending_host_timer(
2673      &state.pending,
2674      &state.relay_ids,
2675      1,
2676      futures::task::noop_waker(),
2677    );
2678    let cancellation = match registration {
2679      PendingHostTimerRegistration::Armed { cancellation, .. } => cancellation,
2680      PendingHostTimerRegistration::Refreshed(_) | PendingHostTimerRegistration::Exhausted(_) => {
2681        panic!("the first timer registration must reserve a native cancellation relay")
2682      }
2683    };
2684    let mut pending = state
2685      .pending
2686      .lock()
2687      .unwrap_or_else(std::sync::PoisonError::into_inner)
2688      .remove(&1)
2689      .expect("the registered timer must remain pending");
2690    pending.schedule_state = RelayScheduleState::CallbackComplete;
2691
2692    retire_pending_relay(&state, pending);
2693
2694    assert!(
2695      matches!(cancellation.now_or_never(), Some(Ok(()))),
2696      "native relay retirement must publish before the contained host cancellation panic"
2697    );
2698  }
2699
2700  #[test]
2701  fn relay_bulk_eviction_contains_each_cancel_panic_and_wakes_every_timer() {
2702    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2703
2704    impl std::task::Wake for CountingWake {
2705      fn wake(self: std::sync::Arc<Self>) {
2706        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2707      }
2708    }
2709
2710    let state = std::sync::Arc::new(TestPendingRelayState::default());
2711    state
2712      .panic_on_cancel
2713      .store(true, std::sync::atomic::Ordering::SeqCst);
2714    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2715    let pending = rustc_hash::FxHashMap::from_iter([7, 8].map(|relay_id| {
2716      (
2717        u64::from(relay_id),
2718        PendingHostTimer {
2719          cancellation: None,
2720          relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2721          relay_id,
2722          waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2723            &wakes,
2724          )))),
2725          schedule_state: RelayScheduleState::CallbackComplete,
2726        },
2727      )
2728    }));
2729
2730    retire_pending_relays(&state, pending);
2731
2732    let mut cancelled = state
2733      .cancelled_relays
2734      .lock()
2735      .unwrap_or_else(std::sync::PoisonError::into_inner)
2736      .clone();
2737    cancelled.sort_unstable();
2738    assert_eq!(cancelled, [7, 8]);
2739    assert_eq!(
2740      wakes.load(std::sync::atomic::Ordering::SeqCst),
2741      2,
2742      "one panicking cancellation must not suppress any later pending timer wake"
2743    );
2744  }
2745
2746  #[test]
2747  fn failed_relay_retirement_cancels_only_after_the_schedule_callback_returns() {
2748    struct CountingWake(std::sync::Arc<std::sync::atomic::AtomicUsize>);
2749
2750    impl std::task::Wake for CountingWake {
2751      fn wake(self: std::sync::Arc<Self>) {
2752        self.0.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2753      }
2754    }
2755
2756    let state = std::sync::Arc::new(TestPendingRelayState::default());
2757    let wakes = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
2758    retire_pending_relay(
2759      &state,
2760      PendingHostTimer {
2761        cancellation: None,
2762        relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2763        relay_id: 7,
2764        waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2765          &wakes,
2766        )))),
2767        schedule_state: RelayScheduleState::CallbackComplete,
2768      },
2769    );
2770    retire_pending_relay(
2771      &state,
2772      PendingHostTimer {
2773        cancellation: None,
2774        relay_health: std::sync::Arc::new(HostTimerRelayHealth::default()),
2775        relay_id: 8,
2776        waker: std::task::Waker::from(std::sync::Arc::new(CountingWake(std::sync::Arc::clone(
2777          &wakes,
2778        )))),
2779        schedule_state: RelayScheduleState::AwaitingCallback,
2780      },
2781    );
2782
2783    assert_eq!(
2784      *state
2785        .cancelled_relays
2786        .lock()
2787        .unwrap_or_else(std::sync::PoisonError::into_inner),
2788      [7],
2789      "a callback-complete failure may have armed a timeout, while a failed TSFN submission did not"
2790    );
2791    assert_eq!(
2792      wakes.load(std::sync::atomic::Ordering::SeqCst),
2793      2,
2794      "both failed relays must wake their timer operations"
2795    );
2796  }
2797
2798  #[test]
2799  fn host_timer_cleanup_contains_panicking_payload_drop_across_ffi() {
2800    const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_HOST_TIMER_PANIC_PAYLOAD_CHILD";
2801
2802    if std::env::var_os(CHILD_ENV).is_some() {
2803      struct PanicOnDrop;
2804
2805      impl Drop for PanicOnDrop {
2806        fn drop(&mut self) {
2807          panic!("intentional host timer panic payload destructor panic");
2808        }
2809      }
2810
2811      struct PanickingWake;
2812
2813      impl std::task::Wake for PanickingWake {
2814        fn wake(self: std::sync::Arc<Self>) {
2815          std::panic::panic_any(PanicOnDrop);
2816        }
2817      }
2818
2819      extern "C" fn cleanup_shim() {
2820        let waker = std::task::Waker::from(std::sync::Arc::new(PanickingWake));
2821        wake_host_timer_safely(waker);
2822      }
2823
2824      cleanup_shim();
2825      return;
2826    }
2827
2828    let output = std::process::Command::new(std::env::current_exe().unwrap())
2829      .arg("--exact")
2830      .arg("adapter::tests::host_timer_cleanup_contains_panicking_payload_drop_across_ffi")
2831      .arg("--nocapture")
2832      .env(CHILD_ENV, "1")
2833      .output()
2834      .expect("the host timer cleanup subprocess must start");
2835    assert!(
2836      output.status.success(),
2837      "host timer cleanup must contain panic-payload destruction across the C ABI; status={:?}\nstdout={}\nstderr={}",
2838      output.status.code(),
2839      String::from_utf8_lossy(&output.stdout),
2840      String::from_utf8_lossy(&output.stderr)
2841    );
2842  }
2843
2844  #[test]
2845  fn host_timer_failure_recovery_precedes_and_survives_diagnostic_panics() {
2846    struct PanickingDiagnostic;
2847
2848    impl std::fmt::Display for PanickingDiagnostic {
2849      fn fmt(&self, _formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2850        panic!("intentional host timer diagnostic panic");
2851      }
2852    }
2853
2854    let recovered = std::sync::atomic::AtomicBool::new(false);
2855    let diagnostic = PanickingDiagnostic;
2856    recover_host_timer_failure(
2857      || recovered.store(true, std::sync::atomic::Ordering::SeqCst),
2858      format_args!("{diagnostic}"),
2859    );
2860    assert!(
2861      recovered.load(std::sync::atomic::Ordering::SeqCst),
2862      "timer recovery must complete before fallible diagnostics run"
2863    );
2864  }
2865
2866  #[test]
2867  fn host_timer_registry_drops_wakers_after_unlock() {
2868    struct LockCheckingDrop {
2869      pending: std::sync::Weak<
2870        std::sync::Mutex<rustc_hash::FxHashMap<crate::async_runtime::TimerId, PendingHostTimer>>,
2871      >,
2872      dropped: std::sync::Arc<std::sync::atomic::AtomicBool>,
2873    }
2874
2875    #[expect(
2876      clippy::manual_noop_waker,
2877      reason = "the Arc-backed no-op waker exists to exercise its re-entrant destructor"
2878    )]
2879    impl std::task::Wake for LockCheckingDrop {
2880      fn wake(self: std::sync::Arc<Self>) {}
2881    }
2882
2883    impl Drop for LockCheckingDrop {
2884      fn drop(&mut self) {
2885        let pending = self
2886          .pending
2887          .upgrade()
2888          .expect("the pending registry must outlive its waker");
2889        let _guard = pending.try_lock().unwrap_or_else(|_| {
2890          panic!("a host-timer waker was dropped while the pending registry was locked")
2891        });
2892        self
2893          .dropped
2894          .store(true, std::sync::atomic::Ordering::SeqCst);
2895      }
2896    }
2897
2898    let pending = std::sync::Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
2899    let relay_ids = RelayIdAllocator::default();
2900    let dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2901    let old_waker = std::task::Waker::from(std::sync::Arc::new(LockCheckingDrop {
2902      pending: std::sync::Arc::downgrade(&pending),
2903      dropped: std::sync::Arc::clone(&dropped),
2904    }));
2905    assert!(matches!(
2906      register_pending_host_timer(&pending, &relay_ids, 1, old_waker),
2907      PendingHostTimerRegistration::Armed { .. }
2908    ));
2909
2910    let replaced_waker =
2911      match register_pending_host_timer(&pending, &relay_ids, 1, futures::task::noop_waker()) {
2912        PendingHostTimerRegistration::Refreshed(waker) => waker,
2913        PendingHostTimerRegistration::Armed { .. } => {
2914          panic!("a re-poll must refresh the existing timer")
2915        }
2916        PendingHostTimerRegistration::Exhausted(_) => {
2917          panic!("the test allocator must not be exhausted")
2918        }
2919      };
2920    assert!(
2921      !dropped.load(std::sync::atomic::Ordering::SeqCst),
2922      "the helper must return ownership instead of dropping under its lock"
2923    );
2924    drop(replaced_waker);
2925    assert!(dropped.load(std::sync::atomic::Ordering::SeqCst));
2926
2927    let evicted_waker_dropped = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2928    let evicted_waker = std::task::Waker::from(std::sync::Arc::new(LockCheckingDrop {
2929      pending: std::sync::Arc::downgrade(&pending),
2930      dropped: std::sync::Arc::clone(&evicted_waker_dropped),
2931    }));
2932    assert!(matches!(
2933      register_pending_host_timer(&pending, &relay_ids, 2, evicted_waker),
2934      PendingHostTimerRegistration::Armed { .. }
2935    ));
2936    let evicted = take_pending_host_timers(&pending, RelayCancellationAccounting::CleanupOnly);
2937    assert!(
2938      !evicted_waker_dropped.load(std::sync::atomic::Ordering::SeqCst),
2939      "bulk eviction must move wakers out instead of dropping under its lock"
2940    );
2941    drop(evicted);
2942    assert!(evicted_waker_dropped.load(std::sync::atomic::Ordering::SeqCst));
2943  }
2944
2945  #[test]
2946  fn host_timer_eviction_prevents_pending_registration_after_its_drain() {
2947    let pending = std::sync::Arc::new(std::sync::Mutex::new(rustc_hash::FxHashMap::default()));
2948    let relay_ids = std::sync::Arc::new(RelayIdAllocator::default());
2949    let dead = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
2950    let mut pending_guard = pending
2951      .lock()
2952      .unwrap_or_else(std::sync::PoisonError::into_inner);
2953    let (started_tx, started_rx) = std::sync::mpsc::sync_channel(1);
2954    let registration_pending = std::sync::Arc::clone(&pending);
2955    let registration_relay_ids = std::sync::Arc::clone(&relay_ids);
2956    let registration_dead = std::sync::Arc::clone(&dead);
2957    let registration = std::thread::spawn(move || {
2958      started_tx.send(()).unwrap();
2959      register_pending_host_timer_if_live(
2960        &registration_pending,
2961        &registration_relay_ids,
2962        1,
2963        futures::task::noop_waker(),
2964        || !registration_dead.load(std::sync::atomic::Ordering::SeqCst),
2965      )
2966    });
2967
2968    started_rx.recv().unwrap();
2969    dead.store(true, std::sync::atomic::Ordering::SeqCst);
2970    let drained = std::mem::take(&mut *pending_guard);
2971    drop(pending_guard);
2972    drop(drained);
2973
2974    assert!(
2975      registration.join().unwrap().is_err(),
2976      "registration arriving after death publication and the pending drain must be rejected"
2977    );
2978    assert!(
2979      pending
2980        .lock()
2981        .unwrap_or_else(std::sync::PoisonError::into_inner)
2982        .is_empty(),
2983      "a stale selected driver must not publish a new pending arm after eviction drained the map"
2984    );
2985  }
2986
2987  #[test]
2988  fn cleanup_hook_registration_failure_rolls_back_host_registration() {
2989    let rollback_calls = std::sync::atomic::AtomicUsize::new(0);
2990    let result = install_cleanup_hook_or_rollback(
2991      || -> napi::Result<()> {
2992        Err(napi::Error::new(
2993          napi::Status::GenericFailure,
2994          "intentional cleanup-hook registration failure",
2995        ))
2996      },
2997      || {
2998        rollback_calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
2999      },
3000    );
3001
3002    assert!(result.is_err());
3003    assert_eq!(rollback_calls.load(std::sync::atomic::Ordering::SeqCst), 1);
3004  }
3005
3006  #[test]
3007  fn host_driver_installation_allows_reentrant_eviction_before_publication() {
3008    let dead = std::sync::atomic::AtomicBool::new(false);
3009    let registration = std::sync::Mutex::new(None);
3010    let published = std::sync::atomic::AtomicBool::new(false);
3011    let rolled_back = std::sync::atomic::AtomicUsize::new(0);
3012
3013    let installed = install_host_driver_registration(
3014      &dead,
3015      &registration,
3016      || {
3017        dead.store(true, std::sync::atomic::Ordering::SeqCst);
3018        let removed = registration
3019          .try_lock()
3020          .expect("the arbitrary installation callback must run outside the registration mutex")
3021          .take();
3022        assert!(
3023          removed.is_none(),
3024          "reentrant eviction must see no half-published registration"
3025        );
3026        17_u64
3027      },
3028      || {
3029        published.store(true, std::sync::atomic::Ordering::SeqCst);
3030      },
3031      |id| {
3032        assert_eq!(id, 17);
3033        let guard = registration
3034          .try_lock()
3035          .expect("rollback must run outside the registration mutex");
3036        drop(guard);
3037        rolled_back.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
3038      },
3039    );
3040
3041    assert!(
3042      !installed,
3043      "reentrant eviction must reject the exported registration"
3044    );
3045    assert!(!published.load(std::sync::atomic::Ordering::SeqCst));
3046    assert_eq!(rolled_back.load(std::sync::atomic::Ordering::SeqCst), 1);
3047    assert!(registration.lock().unwrap().is_none());
3048  }
3049
3050  #[test]
3051  fn shared_runtime_rejects_after_shutdown_and_accepts_after_restart() {
3052    use std::{
3053      sync::{
3054        Arc,
3055        atomic::{AtomicBool, Ordering},
3056        mpsc,
3057      },
3058      time::Duration,
3059    };
3060
3061    napi::bindgen_prelude::AsyncRuntime::start(&SharedAsyncRuntime)
3062      .expect("the adapter runtime must start");
3063    crate::async_runtime::reset_metrics();
3064    let (first_tx, first_rx) = mpsc::channel();
3065    napi::bindgen_prelude::AsyncRuntime::spawn_blocking(
3066      &SharedAsyncRuntime,
3067      Box::new(move || {
3068        first_tx
3069          .send(())
3070          .expect("test receiver must still be listening");
3071      }),
3072    )
3073    .unwrap_or_else(|_| panic!("the shared blocking lane must accept napi work"));
3074    first_rx
3075      .recv_timeout(Duration::from_secs(5))
3076      .expect("the shared blocking lane must execute accepted napi work");
3077
3078    napi::bindgen_prelude::AsyncRuntime::shutdown(&SharedAsyncRuntime)
3079      .expect("the adapter runtime must shut down");
3080    let block_on_ran = Arc::new(AtomicBool::new(false));
3081    let block_on_ran_future = Arc::clone(&block_on_ran);
3082    let mut block_on_future = std::pin::pin!(async move {
3083      block_on_ran_future.store(true, Ordering::SeqCst);
3084    });
3085    let block_on_error =
3086      napi::bindgen_prelude::AsyncRuntime::block_on(&SharedAsyncRuntime, block_on_future.as_mut())
3087        .expect_err("the adapter must reject block_on while stopped");
3088    assert_eq!(
3089      block_on_error.reason,
3090      "the async runtime is stopped; call start before submitting work"
3091    );
3092    assert!(
3093      !block_on_ran.load(Ordering::SeqCst),
3094      "rejected block_on must not poll its future"
3095    );
3096
3097    let rejected_ran = Arc::new(AtomicBool::new(false));
3098    let rejected_ran_work = Arc::clone(&rejected_ran);
3099    let (rejected_retry_tx, rejected_retry_rx) = mpsc::channel();
3100    let rejected = napi::bindgen_prelude::AsyncRuntime::spawn_blocking(
3101      &SharedAsyncRuntime,
3102      Box::new(move || {
3103        rejected_ran_work.store(true, Ordering::SeqCst);
3104        rejected_retry_tx
3105          .send(())
3106          .expect("test receiver must still be listening");
3107      }),
3108    )
3109    .expect_err("the adapter must reject work while stopped");
3110    assert!(!rejected_ran.load(Ordering::SeqCst));
3111    let (rejected, error) = rejected.into_parts();
3112    assert_eq!(
3113      error.reason,
3114      "the async runtime is stopped; call start before submitting work"
3115    );
3116
3117    napi::bindgen_prelude::AsyncRuntime::start(&SharedAsyncRuntime)
3118      .expect("the adapter runtime must restart");
3119    napi::bindgen_prelude::AsyncRuntime::block_on(&SharedAsyncRuntime, block_on_future.as_mut())
3120      .expect("the restarted adapter runtime must accept the retained future");
3121    assert!(block_on_ran.load(Ordering::SeqCst));
3122
3123    napi::bindgen_prelude::AsyncRuntime::spawn_blocking(&SharedAsyncRuntime, rejected)
3124      .unwrap_or_else(|_| panic!("the restarted runtime must accept the retained napi work"));
3125    rejected_retry_rx
3126      .recv_timeout(Duration::from_secs(5))
3127      .expect("the restarted runtime must execute the retained napi work");
3128    assert!(
3129      rejected_ran.load(Ordering::SeqCst),
3130      "rejected work must be returned intact"
3131    );
3132    assert!(
3133      crate::async_runtime::metrics().blocking_tasks_started >= 2,
3134      "both accepted adapter submissions should reach the shared blocking scheduler"
3135    );
3136  }
3137
3138  #[cfg(not(target_family = "wasm"))]
3139  #[test]
3140  fn native_task_host_null_env_retires_payload_and_recovers_exact_delivery() {
3141    const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_NATIVE_TASK_HOST_NULL_ENV_CHILD";
3142
3143    if std::env::var_os(CHILD_ENV).is_some() {
3144      use std::sync::{Arc, mpsc};
3145      use std::time::Duration;
3146
3147      use crate::async_runtime::{
3148        CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3149        configure, register_current_thread_task_driver, shutdown, spawn_detached, start,
3150        unregister_current_thread_task_driver,
3151      };
3152
3153      struct RecordingTaskDriver {
3154        dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3155      }
3156
3157      impl CurrentThreadTaskDriver for RecordingTaskDriver {
3158        fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3159          self.dispatches.send(delivery).is_ok()
3160        }
3161      }
3162
3163      configure(RuntimeOptions {
3164        flavor: RuntimeFlavor::CurrentThread,
3165        worker_threads: 1,
3166        max_blocking_tasks: 1,
3167        ..RuntimeOptions::default()
3168      })
3169      .expect("the isolated runtime must accept CurrentThread configuration");
3170      start().expect("the isolated CurrentThread runtime must start");
3171
3172      let (dispatch_tx, dispatch_rx) = mpsc::channel();
3173      let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3174        dispatches: dispatch_tx,
3175      }));
3176      let (completed_tx, completed_rx) = mpsc::channel();
3177      spawn_detached(async move {
3178        completed_tx
3179          .send(())
3180          .expect("the completion observer must remain live");
3181      });
3182
3183      let payload_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3184      let first_delivery = dispatch_rx
3185        .recv_timeout(Duration::from_secs(2))
3186        .expect("the queued task must publish its first host delivery");
3187      let first_payload = Box::into_raw(Box::new(
3188        NativeCurrentThreadTaskHostPayload::with_drop_observer(
3189          first_delivery,
3190          Arc::clone(&payload_drops),
3191        ),
3192      ))
3193      .cast();
3194      unsafe {
3195        call_native_current_thread_task_host(
3196          std::ptr::null_mut(),
3197          std::ptr::null_mut(),
3198          std::ptr::null_mut(),
3199          first_payload,
3200        );
3201      }
3202      assert_eq!(
3203        payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3204        1,
3205        "the null-env callback must destroy its queued payload exactly once"
3206      );
3207
3208      let replacement_delivery = dispatch_rx
3209        .recv_timeout(Duration::from_secs(2))
3210        .expect("the exact failed delivery must publish one replacement");
3211      assert_ne!(
3212        replacement_delivery, first_delivery,
3213        "recovery must use a fresh registration-scoped delivery capability"
3214      );
3215      let replacement_payload = Box::into_raw(Box::new(
3216        NativeCurrentThreadTaskHostPayload::with_drop_observer(
3217          replacement_delivery,
3218          Arc::clone(&payload_drops),
3219        ),
3220      ))
3221      .cast();
3222      let fake_env = std::ptr::NonNull::<std::ffi::c_void>::dangling()
3223        .as_ptr()
3224        .cast();
3225      unsafe {
3226        call_native_current_thread_task_host(
3227          fake_env,
3228          std::ptr::null_mut(),
3229          std::ptr::null_mut(),
3230          replacement_payload,
3231        );
3232      }
3233
3234      completed_rx
3235        .recv_timeout(Duration::from_secs(2))
3236        .expect("the replacement host delivery must run the queued task");
3237      assert_eq!(
3238        payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3239        2,
3240        "successful recovery must also destroy its queued payload exactly once"
3241      );
3242      assert!(
3243        matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3244        "successful replacement acknowledgement must not publish another delivery"
3245      );
3246
3247      unregister_current_thread_task_driver(driver_id);
3248      shutdown().expect("the isolated CurrentThread runtime must shut down cleanly");
3249      return;
3250    }
3251
3252    let output = std::process::Command::new(std::env::current_exe().unwrap())
3253      .arg("--exact")
3254      .arg("adapter::tests::native_task_host_null_env_retires_payload_and_recovers_exact_delivery")
3255      .arg("--nocapture")
3256      .env(CHILD_ENV, "1")
3257      .output()
3258      .expect("the null-env task-host subprocess must start");
3259    assert!(
3260      output.status.success(),
3261      "the null-env task-host regression failed; status={:?}\nstdout={}\nstderr={}",
3262      output.status.code(),
3263      String::from_utf8_lossy(&output.stdout),
3264      String::from_utf8_lossy(&output.stderr)
3265    );
3266  }
3267
3268  #[cfg(not(target_family = "wasm"))]
3269  #[test]
3270  fn native_task_host_callback_lease_covers_ack_payload_and_restart() {
3271    const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_NATIVE_TASK_HOST_CALLBACK_LEASE_CHILD";
3272
3273    if std::env::var_os(CHILD_ENV).is_some() {
3274      use std::sync::{Arc, mpsc};
3275      use std::time::{Duration, Instant};
3276
3277      use crate::async_runtime::{
3278        CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3279        configure, register_current_thread_task_driver, shutdown, spawn_detached, start,
3280        try_spawn_detached, unregister_current_thread_task_driver,
3281      };
3282
3283      struct RecordingTaskDriver {
3284        dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3285      }
3286
3287      impl CurrentThreadTaskDriver for RecordingTaskDriver {
3288        fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3289          self.dispatches.send(delivery).is_ok()
3290        }
3291      }
3292
3293      configure(RuntimeOptions {
3294        flavor: RuntimeFlavor::CurrentThread,
3295        worker_threads: 1,
3296        max_blocking_tasks: 1,
3297        ..RuntimeOptions::default()
3298      })
3299      .expect("the isolated runtime must accept CurrentThread configuration");
3300      start().expect("the isolated CurrentThread runtime must start");
3301
3302      let (dispatch_tx, dispatch_rx) = mpsc::channel();
3303      let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3304        dispatches: dispatch_tx,
3305      }));
3306      let (task_completed_tx, task_completed_rx) = mpsc::channel();
3307      spawn_detached(async move {
3308        task_completed_tx
3309          .send(())
3310          .expect("the task completion observer must remain live");
3311      });
3312      let delivery = dispatch_rx
3313        .recv_timeout(Duration::from_secs(2))
3314        .expect("the queued task must publish one native host delivery");
3315
3316      let payload_drops = Arc::new(std::sync::atomic::AtomicUsize::new(0));
3317      let payload = Box::into_raw(Box::new(
3318        NativeCurrentThreadTaskHostPayload::with_drop_observer(
3319          delivery,
3320          Arc::clone(&payload_drops),
3321        ),
3322      ))
3323      .cast::<std::ffi::c_void>() as usize;
3324      let (after_drive_tx, after_drive_rx) = mpsc::channel();
3325      let (release_after_drive_tx, release_after_drive_rx) = mpsc::channel();
3326      let (after_payload_tx, after_payload_rx) = mpsc::channel();
3327      let (release_after_payload_tx, release_after_payload_rx) = mpsc::channel();
3328      let callback = std::thread::spawn(move || {
3329        NATIVE_TASK_HOST_AFTER_DRIVE_TEST_HOOK.with(|slot| {
3330          *slot.borrow_mut() = Some(Box::new(move || {
3331            after_drive_tx.send(()).unwrap();
3332            release_after_drive_rx.recv().unwrap();
3333          }));
3334        });
3335        NATIVE_TASK_HOST_AFTER_PAYLOAD_DROP_TEST_HOOK.with(|slot| {
3336          *slot.borrow_mut() = Some(Box::new(move || {
3337            after_payload_tx.send(()).unwrap();
3338            release_after_payload_rx.recv().unwrap();
3339          }));
3340        });
3341        let fake_env = std::ptr::NonNull::<std::ffi::c_void>::dangling()
3342          .as_ptr()
3343          .cast();
3344        unsafe {
3345          call_native_current_thread_task_host(
3346            fake_env,
3347            std::ptr::null_mut(),
3348            std::ptr::null_mut(),
3349            payload as *mut std::ffi::c_void,
3350          );
3351        }
3352      });
3353
3354      after_drive_rx
3355        .recv_timeout(Duration::from_secs(2))
3356        .expect("the native callback must pause after driving the host turn");
3357      task_completed_rx
3358        .recv_timeout(Duration::from_secs(2))
3359        .expect("the host turn must run the queued task before acknowledgement");
3360
3361      let (shutdown_tx, shutdown_rx) = mpsc::channel();
3362      let shutdown_thread = std::thread::spawn(move || {
3363        shutdown_tx.send(shutdown()).unwrap();
3364      });
3365      // Hang detector: the freshly spawned shutdown thread must get CPU and
3366      // publish the stopping lifecycle -- generous for starved CI runners.
3367      let stopping_deadline = Instant::now() + Duration::from_secs(10);
3368      loop {
3369        match try_spawn_detached(async {}) {
3370          Ok(()) => {
3371            assert!(
3372              Instant::now() < stopping_deadline,
3373              "shutdown did not publish the stopping lifecycle"
3374            );
3375            std::thread::yield_now();
3376          }
3377          Err(future) => {
3378            drop(future);
3379            break;
3380          }
3381        }
3382      }
3383      assert!(
3384        shutdown_rx
3385          .recv_timeout(Duration::from_millis(200))
3386          .is_err(),
3387        "shutdown must wait after drive until delivery acknowledgement and payload destruction"
3388      );
3389
3390      let (restart_tx, restart_rx) = mpsc::channel();
3391      let restart_thread = std::thread::spawn(move || {
3392        start().expect("the runtime must restart after the old callback retires");
3393        restart_tx.send(()).unwrap();
3394      });
3395      assert!(
3396        restart_rx.recv_timeout(Duration::from_millis(200)).is_err(),
3397        "restart must not overlap the old callback before acknowledgement"
3398      );
3399
3400      release_after_drive_tx.send(()).unwrap();
3401      after_payload_rx
3402        .recv_timeout(Duration::from_secs(2))
3403        .expect("the callback must pause after acknowledging and destroying its payload");
3404      assert_eq!(
3405        payload_drops.load(std::sync::atomic::Ordering::SeqCst),
3406        1,
3407        "the acknowledged callback must destroy its payload before releasing the lease"
3408      );
3409      assert!(
3410        shutdown_rx
3411          .recv_timeout(Duration::from_millis(200))
3412          .is_err(),
3413        "shutdown must remain blocked after acknowledgement and payload destruction"
3414      );
3415      assert!(
3416        restart_rx.recv_timeout(Duration::from_millis(200)).is_err(),
3417        "restart must remain blocked until the callback lease retires"
3418      );
3419
3420      release_after_payload_tx.send(()).unwrap();
3421      callback.join().unwrap();
3422      shutdown_rx
3423        .recv_timeout(Duration::from_secs(2))
3424        .expect("shutdown must finish after the callback lease retires")
3425        .unwrap();
3426      restart_rx
3427        .recv_timeout(Duration::from_secs(2))
3428        .expect("restart must finish after old-generation shutdown");
3429      shutdown_thread.join().unwrap();
3430      restart_thread.join().unwrap();
3431
3432      unregister_current_thread_task_driver(driver_id);
3433      shutdown().expect("the replacement CurrentThread runtime must shut down cleanly");
3434      return;
3435    }
3436
3437    let output = std::process::Command::new(std::env::current_exe().unwrap())
3438      .arg("--exact")
3439      .arg("adapter::tests::native_task_host_callback_lease_covers_ack_payload_and_restart")
3440      .arg("--nocapture")
3441      .env(CHILD_ENV, "1")
3442      .output()
3443      .expect("the native task-host callback lease subprocess must start");
3444    assert!(
3445      output.status.success(),
3446      "the native task-host callback lease regression failed; status={:?}\nstdout={}\nstderr={}",
3447      output.status.code(),
3448      String::from_utf8_lossy(&output.stdout),
3449      String::from_utf8_lossy(&output.stderr)
3450    );
3451  }
3452
3453  #[cfg(not(target_family = "wasm"))]
3454  #[test]
3455  fn timer_relay_terminal_drop_at_host_turn_budget_settles_without_later_work() {
3456    const CHILD_ENV: &str = "NAPI_RUNTIME_TEST_TIMER_RELAY_TERMINAL_DROP_CHILD";
3457    const HOST_TURN_RUNNABLE_BUDGET: usize = 64;
3458
3459    if let Some(relay_state) = std::env::var_os(CHILD_ENV) {
3460      use std::sync::{Arc, mpsc};
3461      use std::time::Duration;
3462
3463      use crate::async_runtime::{
3464        CurrentThreadTaskDelivery, CurrentThreadTaskDriver, RuntimeFlavor, RuntimeOptions,
3465        acknowledge_current_thread_task_delivery, cancel_current_thread_task_dispatch, configure,
3466        drive_current_thread_tasks, metrics, register_current_thread_task_driver, reset_metrics,
3467        shutdown, spawn, spawn_detached, start, unregister_current_thread_task_driver,
3468      };
3469      use futures::FutureExt as _;
3470
3471      struct RecordingTaskDriver {
3472        dispatches: mpsc::Sender<CurrentThreadTaskDelivery>,
3473      }
3474
3475      impl CurrentThreadTaskDriver for RecordingTaskDriver {
3476        fn dispatch(&self, delivery: CurrentThreadTaskDelivery) -> bool {
3477          self.dispatches.send(delivery).is_ok()
3478        }
3479      }
3480
3481      let schedule_state = if relay_state == "complete" {
3482        RelayScheduleState::CallbackComplete
3483      } else {
3484        RelayScheduleState::AwaitingCallback
3485      };
3486
3487      let options = RuntimeOptions {
3488        flavor: RuntimeFlavor::CurrentThread,
3489        worker_threads: 1,
3490        max_blocking_tasks: 1,
3491        ..RuntimeOptions::default()
3492      };
3493      configure(options).expect("the isolated runtime must accept CurrentThread configuration");
3494      start().expect("the isolated CurrentThread runtime must start");
3495      reset_metrics();
3496
3497      let (dispatch_tx, dispatch_rx) = mpsc::channel();
3498      let driver_id = register_current_thread_task_driver(Arc::new(RecordingTaskDriver {
3499        dispatches: dispatch_tx,
3500      }));
3501
3502      for _ in 0..HOST_TURN_RUNNABLE_BUDGET - 1 {
3503        spawn_detached(async {});
3504      }
3505      let state = Arc::new(TestPendingRelayState::default());
3506      let timer_task = spawn(BudgetBoundaryTimerWait {
3507        state: Arc::clone(&state),
3508        timer_id: 1,
3509        schedule_state,
3510        registered: false,
3511      });
3512      let first_dispatch = dispatch_rx
3513        .recv_timeout(Duration::from_secs(2))
3514        .expect("the initial host turn must be published");
3515      assert!(
3516        matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3517        "all 64 initial runnables must coalesce behind one host turn"
3518      );
3519
3520      let callback_lease = drive_current_thread_tasks(first_dispatch.capability())
3521        .expect("the exact host delivery must be admitted");
3522      acknowledge_current_thread_task_delivery(first_dispatch);
3523      drop(callback_lease);
3524      assert_eq!(
3525        metrics().runnable_polls,
3526        HOST_TURN_RUNNABLE_BUDGET as u64,
3527        "the timer operation must occupy the last poll in the host-turn budget"
3528      );
3529      assert_eq!(
3530        state
3531          .pending
3532          .lock()
3533          .unwrap_or_else(std::sync::PoisonError::into_inner)
3534          .len(),
3535        1,
3536        "the timer poll must register its pending relay"
3537      );
3538      assert_eq!(
3539        metrics().queued_runnables,
3540        1,
3541        "the unpolled relay must be the sole runnable beyond the budget boundary"
3542      );
3543
3544      let failed_dispatch = dispatch_rx
3545        .recv_timeout(Duration::from_secs(2))
3546        .expect("budget exhaustion must publish a fresh relay turn");
3547      cancel_current_thread_task_dispatch(failed_dispatch.capability());
3548      let replacement_dispatch = dispatch_rx
3549        .recv_timeout(Duration::from_secs(2))
3550        .expect("the first scheduler failure must publish one replacement");
3551      assert_ne!(replacement_dispatch, failed_dispatch);
3552      cancel_current_thread_task_dispatch(replacement_dispatch.capability());
3553
3554      assert!(
3555        state
3556          .pending
3557          .lock()
3558          .unwrap_or_else(std::sync::PoisonError::into_inner)
3559          .is_empty(),
3560        "terminal queue destruction must retire the matching pending relay"
3561      );
3562      let cancelled_relays = state
3563        .cancelled_relays
3564        .lock()
3565        .unwrap_or_else(std::sync::PoisonError::into_inner);
3566      if schedule_state == RelayScheduleState::CallbackComplete {
3567        assert_eq!(
3568          cancelled_relays.len(),
3569          1,
3570          "a callback-complete relay must queue exactly one host cancellation"
3571        );
3572      } else {
3573        assert!(
3574          cancelled_relays.is_empty(),
3575          "cleanup must not overtake a schedule callback that has not returned"
3576        );
3577      }
3578      drop(cancelled_relays);
3579      assert_eq!(metrics().queued_runnables, 0);
3580      assert!(
3581        matches!(dispatch_rx.try_recv(), Err(mpsc::TryRecvError::Empty)),
3582        "terminal recovery must stay bounded after the replacement failure"
3583      );
3584      let task_result = timer_task
3585        .now_or_never()
3586        .expect("the original timer operation must settle immediately");
3587      assert_eq!(
3588        task_result
3589          .expect_err("terminal recovery must cancel the pending timer operation")
3590          .to_string(),
3591        "async runtime stopped before the task completed"
3592      );
3593
3594      unregister_current_thread_task_driver(driver_id);
3595      shutdown().expect("the isolated CurrentThread runtime must shut down cleanly");
3596      return;
3597    }
3598
3599    for relay_state in ["awaiting", "complete"] {
3600      let output = std::process::Command::new(std::env::current_exe().unwrap())
3601        .arg("--exact")
3602        .arg(
3603          "adapter::tests::timer_relay_terminal_drop_at_host_turn_budget_settles_without_later_work",
3604        )
3605        .arg("--nocapture")
3606        .env(CHILD_ENV, relay_state)
3607        .output()
3608        .expect("the timer relay regression subprocess must start");
3609      assert!(
3610        output.status.success(),
3611        "{relay_state} timer relay terminal-drop regression failed; status={:?}\nstdout={}\nstderr={}",
3612        output.status.code(),
3613        String::from_utf8_lossy(&output.stdout),
3614        String::from_utf8_lossy(&output.stderr)
3615      );
3616    }
3617  }
3618
3619  /// Relay-eviction invariant: the relay must evict ONLY on host death, and
3620  /// the decision must be STRING-FREE. A rejected JS promise coerces to
3621  /// `GenericFailure` carrying the JS-controlled rejection string (pinned
3622  /// napi 3.10 error.rs), so message matching is forgeable by a live
3623  /// callback. The two authorities: the unforgeable `Closing` status, and
3624  /// the liveness probe.
3625  #[test]
3626  fn relay_eviction_is_decided_by_status_and_probe_never_by_message() {
3627    use napi::Status;
3628
3629    use super::should_evict_for_relay_error;
3630
3631    // `Closing` is authoritative host death: it originates only from the
3632    // TSFN layer (aborted pre-check, raw napi_closing) and JS coercion can
3633    // never produce it -- evict even if the probe still reads live (the
3634    // finalize flag can lag the raw status).
3635    assert!(
3636      should_evict_for_relay_error(Status::Closing, true),
3637      "Closing overrides a live probe"
3638    );
3639    assert!(should_evict_for_relay_error(Status::Closing, false));
3640
3641    // A DEAD probe evicts regardless of status. Queue drain during env
3642    // teardown and env death before a JS promise settles both coincide with
3643    // the env being torn down, which the probe observes directly.
3644    assert!(should_evict_for_relay_error(Status::GenericFailure, false));
3645    assert!(should_evict_for_relay_error(
3646      Status::PendingException,
3647      false
3648    ));
3649
3650    // A LIVE host's failure takes the strike path no matter what the error
3651    // says. A live callback can reject with
3652    // `Promise.reject(new Error('oneshot canceled'))`, which coerces to
3653    // GenericFailure + "Error: oneshot canceled" and must not be mistaken for
3654    // environment teardown.
3655    assert!(
3656      !should_evict_for_relay_error(Status::GenericFailure, true),
3657      "a live host's GenericFailure -- e.g. a forged 'oneshot canceled' rejection -- must strike"
3658    );
3659    assert!(
3660      !should_evict_for_relay_error(Status::PendingException, true),
3661      "a JS throw on a live host must strike"
3662    );
3663    assert!(
3664      !should_evict_for_relay_error(Status::InvalidArg, true),
3665      "a wrong return type on a live host must strike"
3666    );
3667    assert!(
3668      !should_evict_for_relay_error(Status::QueueFull, true),
3669      "queue pressure on a live host must strike"
3670    );
3671  }
3672
3673  #[test]
3674  fn cancellation_failures_share_the_three_strike_budget_without_cleanup_double_counting() {
3675    use std::sync::atomic::{AtomicU32, Ordering};
3676
3677    use napi::Status;
3678
3679    let failures = AtomicU32::new(0);
3680    for expected_strikes in 1..HOST_TIMER_MAX_TRANSIENT_FAILURES {
3681      let relay_health = HostTimerRelayHealth::default();
3682      assert_eq!(
3683        record_host_timer_failure(&failures, &relay_health, Status::GenericFailure, true),
3684        HostTimerFailureAction::Retry(expected_strikes),
3685      );
3686      assert_eq!(
3687        record_host_timer_failure(&failures, &relay_health, Status::GenericFailure, true),
3688        HostTimerFailureAction::Duplicate,
3689        "one relay failure must consume at most one strike"
3690      );
3691    }
3692
3693    let cleanup_health = HostTimerRelayHealth::default();
3694    cleanup_health.set_cancellation_accounting(RelayCancellationAccounting::CleanupOnly);
3695    assert_eq!(
3696      cleanup_health.cancellation_accounting(),
3697      RelayCancellationAccounting::CleanupOnly
3698    );
3699    assert_eq!(
3700      failures.load(Ordering::SeqCst),
3701      HOST_TIMER_MAX_TRANSIENT_FAILURES - 1,
3702      "schedule-failure and eviction cleanup must not mutate host health"
3703    );
3704
3705    let final_health = HostTimerRelayHealth::default();
3706    assert_eq!(
3707      record_host_timer_failure(&failures, &final_health, Status::GenericFailure, true),
3708      HostTimerFailureAction::EvictHostAfterStrikes(HOST_TIMER_MAX_TRANSIENT_FAILURES),
3709    );
3710  }
3711
3712  #[test]
3713  fn a_same_relay_success_cannot_erase_its_recorded_failure() {
3714    use std::sync::atomic::{AtomicU32, Ordering};
3715
3716    use napi::Status;
3717
3718    let failures = AtomicU32::new(1);
3719    let healthy_relay = HostTimerRelayHealth::default();
3720    reset_host_timer_failures_after_success(&failures, &healthy_relay);
3721    assert_eq!(failures.load(Ordering::SeqCst), 0);
3722
3723    failures.store(1, Ordering::SeqCst);
3724    let failed_relay = HostTimerRelayHealth::default();
3725    assert_eq!(
3726      record_host_timer_failure(&failures, &failed_relay, Status::GenericFailure, true),
3727      HostTimerFailureAction::Retry(2),
3728    );
3729    reset_host_timer_failures_after_success(&failures, &failed_relay);
3730    assert_eq!(
3731      failures.load(Ordering::SeqCst),
3732      2,
3733      "a later success from the same relay must not hide its cancellation failure"
3734    );
3735  }
3736
3737  #[test]
3738  fn resolve_thread_count_handles_env_inputs() {
3739    const DEFAULT: usize = 8;
3740    const MAXIMUM: usize = 16;
3741    // A `0` typo must be treated as unset, never fed to a constructor that
3742    // rejects 0 (`RuntimeOptions` validation), which would panic the host's
3743    // module_init `expect`.
3744    assert_eq!(
3745      resolve_thread_count(Some("0".to_string()), DEFAULT, MAXIMUM),
3746      DEFAULT
3747    );
3748    // Valid positive numbers pass through unchanged.
3749    assert_eq!(
3750      resolve_thread_count(Some("4".to_string()), DEFAULT, MAXIMUM),
3751      4
3752    );
3753    // Valid but unsafe values clamp to the construction limit.
3754    assert_eq!(
3755      resolve_thread_count(Some("1000".to_string()), DEFAULT, MAXIMUM),
3756      MAXIMUM
3757    );
3758    assert_eq!(
3759      resolve_thread_count(Some(usize::MAX.to_string()), DEFAULT, MAXIMUM),
3760      MAXIMUM
3761    );
3762    // Parse overflow is invalid input and falls back instead of wrapping.
3763    assert_eq!(
3764      resolve_thread_count(Some("18446744073709551616".to_string()), DEFAULT, MAXIMUM,),
3765      DEFAULT
3766    );
3767    // Non-numeric values fall back gracefully (unchanged behaviour).
3768    assert_eq!(
3769      resolve_thread_count(Some("abc".to_string()), DEFAULT, MAXIMUM),
3770      DEFAULT
3771    );
3772    // Unset falls back to the default.
3773    assert_eq!(resolve_thread_count(None, DEFAULT, MAXIMUM), DEFAULT);
3774    // Host-derived defaults are subject to the same production ceiling.
3775    assert_eq!(resolve_thread_count(None, 32, MAXIMUM), MAXIMUM);
3776  }
3777
3778  #[test]
3779  #[should_panic(expected = "the default thread count must be positive")]
3780  fn resolve_thread_count_rejects_an_invalid_default() {
3781    let _ = resolve_thread_count(None, 0, 1);
3782  }
3783}