napi 3.12.0-alpha.0

N-API bindings
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
use std::{
  any::Any,
  cell::{Cell, RefCell},
  collections::{hash_map::Entry, HashMap, HashSet},
  marker::PhantomData,
  sync::{
    atomic::{AtomicUsize, Ordering},
    Arc, LazyLock, Mutex, Weak,
  },
  thread::{self, ThreadId},
};

use crate::{Error, Result, Status};
use napi_sys as sys;

type ResolverId = usize;
type EnvId = usize;
type ResolverIdentity = Weak<ResolverHandle>;

#[derive(Clone)]
struct ResolverKey {
  id: ResolverId,
  identity: ResolverIdentity,
}

struct ResolverEntry {
  #[cfg_attr(
    not(any(feature = "tokio_rt", feature = "async-runtime")),
    allow(dead_code)
  )]
  env: Option<EnvId>,
  identity: ResolverIdentity,
  resolver: Option<Box<dyn Any>>,
}

impl Drop for ResolverEntry {
  fn drop(&mut self) {
    if let Some(resolver) = self.resolver.take() {
      crate::bindgen_runtime::catch_unwind_safely(|| drop(resolver));
    }
  }
}

thread_local! {
  static RESOLVERS: RefCell<HashMap<ResolverId, ResolverEntry>> = RefCell::new(HashMap::new());
  static CLOSING_ENVS: RefCell<HashSet<EnvId>> = RefCell::new(HashSet::new());
  static RETIRED_RESOLVER_ENVS: RefCell<HashSet<EnvId>> = RefCell::new(HashSet::new());
  static RESOLVER_ENV_REQUIRES_EXACT: Cell<bool> = const { Cell::new(false) };
  static RESOLVER_CLEANUP_ENVS: RefCell<HashSet<EnvId>> = RefCell::new(HashSet::new());
  static RESOLVER_THREAD_GUARD: ResolverThreadGuard = const { ResolverThreadGuard };
}

static NEXT_RESOLVER_ID: AtomicUsize = AtomicUsize::new(1);
static RESOLVER_CLEANUP: LazyLock<Mutex<ResolverCleanup>> =
  LazyLock::new(|| Mutex::new(ResolverCleanup::default()));

#[derive(Default)]
struct ResolverCleanup {
  active_owners: HashSet<ThreadId>,
  pending: HashMap<ThreadId, Vec<ResolverKey>>,
}

struct ResolverThreadGuard;

impl Drop for ResolverThreadGuard {
  fn drop(&mut self) {
    let owner = thread::current().id();
    let mut cleanup = RESOLVER_CLEANUP
      .lock()
      .unwrap_or_else(std::sync::PoisonError::into_inner);
    cleanup.active_owners.remove(&owner);
    cleanup.pending.remove(&owner);
  }
}

struct ResolverHandle {
  key: ResolverKey,
  owner: ThreadId,
  unavailable: Option<ResolverUnavailable>,
}

impl ResolverHandle {
  fn new(id: ResolverId, owner: ThreadId) -> Arc<Self> {
    Arc::new_cyclic(|identity| Self {
      key: ResolverKey {
        id,
        identity: identity.clone(),
      },
      owner,
      unavailable: None,
    })
  }

  fn unavailable(owner: ThreadId, unavailable: ResolverUnavailable) -> Arc<Self> {
    Arc::new(Self {
      key: ResolverKey {
        id: 0,
        identity: Weak::new(),
      },
      owner,
      unavailable: Some(unavailable),
    })
  }
}

#[derive(Clone, Copy)]
enum ResolverUnavailable {
  EnvironmentClosed,
  ExactEnvironmentRequired,
}

impl ResolverUnavailable {
  fn error(self) -> Error {
    match self {
      Self::EnvironmentClosed => Error::new(
        Status::Cancelled,
        "Async resolver is no longer available because its Node environment was closed",
      ),
      Self::ExactEnvironmentRequired => Error::new(
        Status::InvalidArg,
        "SendableResolver::new requires an exact Node environment after multiple environments or environment teardown were observed on this thread; use SendableResolver::new_with_env",
      ),
    }
  }
}

impl Drop for ResolverHandle {
  fn drop(&mut self) {
    if self.key.id == 0 {
      return;
    }
    if self.owner == thread::current().id() {
      if let Ok(Some(entry)) =
        RESOLVERS.try_with(|resolvers| remove_resolver(&mut resolvers.borrow_mut(), &self.key))
      {
        drop(entry);
      }
    } else {
      let mut cleanup = RESOLVER_CLEANUP
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
      if cleanup.active_owners.contains(&self.owner) {
        cleanup
          .pending
          .entry(self.owner)
          .or_default()
          .push(self.key.clone());
      }
    }
  }
}

fn drain_pending_resolver_cleanup() {
  let owner = thread::current().id();
  let keys = RESOLVER_CLEANUP
    .lock()
    .unwrap_or_else(std::sync::PoisonError::into_inner)
    .pending
    .remove(&owner)
    .unwrap_or_default();
  let entries = RESOLVERS.with(|resolvers| {
    let mut resolvers = resolvers.borrow_mut();
    keys
      .into_iter()
      .filter_map(|key| remove_resolver(&mut resolvers, &key))
      .collect::<Vec<_>>()
  });
  drop(entries);
}

fn remove_resolver(
  resolvers: &mut HashMap<ResolverId, ResolverEntry>,
  key: &ResolverKey,
) -> Option<ResolverEntry> {
  match resolvers.entry(key.id) {
    Entry::Occupied(entry) if Weak::ptr_eq(&entry.get().identity, &key.identity) => {
      Some(entry.remove())
    }
    _ => None,
  }
}

#[cfg(feature = "napi3")]
fn claim_resolver_env_cleanup(env: EnvId) -> bool {
  RETIRED_RESOLVER_ENVS.with(|retired| {
    retired.borrow_mut().remove(&env);
  });
  RESOLVER_CLEANUP_ENVS.with(|envs| envs.borrow_mut().insert(env))
}

#[cfg(feature = "napi3")]
fn mark_resolver_env_registered() {
  if RESOLVER_CLEANUP_ENVS.with(|envs| envs.borrow().len() > 1) {
    RESOLVER_ENV_REQUIRES_EXACT.with(|requires_exact| requires_exact.set(true));
  }
}

#[cfg(feature = "napi3")]
fn release_resolver_env_cleanup(env: EnvId) -> bool {
  RESOLVER_CLEANUP_ENVS
    .try_with(|envs| envs.borrow_mut().remove(&env))
    .unwrap_or(false)
}

#[cfg(feature = "napi3")]
fn retire_resolver_env(env: EnvId) {
  let _ = RESOLVER_ENV_REQUIRES_EXACT.try_with(|requires_exact| requires_exact.set(true));
  let _ = RETIRED_RESOLVER_ENVS.try_with(|retired| {
    retired.borrow_mut().insert(env);
  });
}

enum ResolverEnvAssociation {
  Exact(EnvId),
  Unbound,
  Unavailable(ResolverUnavailable),
}

fn current_resolver_env() -> ResolverEnvAssociation {
  if let Some(env) = crate::bindgen_runtime::current_callback_env() {
    return ResolverEnvAssociation::Exact(env as EnvId);
  }
  if let Some(env) = CLOSING_ENVS.with(|envs| {
    let envs = envs.borrow();
    let mut closing = envs.iter().copied();
    let env = closing.next()?;
    closing.next().is_none().then_some(env)
  }) {
    return ResolverEnvAssociation::Exact(env);
  }
  #[cfg(feature = "napi3")]
  {
    if RESOLVER_ENV_REQUIRES_EXACT.with(Cell::get)
      || RESOLVER_CLEANUP_ENVS.with(|envs| envs.borrow().len() > 1)
    {
      return ResolverEnvAssociation::Unavailable(ResolverUnavailable::ExactEnvironmentRequired);
    }
    if let Some(env) = RESOLVER_CLEANUP_ENVS.with(|envs| envs.borrow().iter().next().copied()) {
      return ResolverEnvAssociation::Exact(env);
    }
  }
  ResolverEnvAssociation::Unbound
}

pub(crate) fn register_resolver_env(env: sys::napi_env) -> Result<bool> {
  #[cfg(feature = "napi3")]
  {
    let env_id = env as EnvId;
    if !claim_resolver_env_cleanup(env_id) {
      return Ok(false);
    }
    #[cfg(not(target_family = "wasm"))]
    let status =
      unsafe { sys::napi_add_env_cleanup_hook(env, Some(resolver_env_cleanup), env.cast()) };
    #[cfg(target_family = "wasm")]
    let status =
      unsafe { crate::napi_add_env_cleanup_hook(env, Some(resolver_env_cleanup), env.cast()) };
    if status != sys::Status::napi_ok {
      release_resolver_env_cleanup(env_id);
      retire_resolver_env(env_id);
      return Err(Error::new(
        Status::GenericFailure,
        "Failed to add SendableResolver environment cleanup hook",
      ));
    }
    mark_resolver_env_registered();
    Ok(true)
  }
  #[cfg(not(feature = "napi3"))]
  {
    let _ = env;
    Ok(false)
  }
}

#[cfg(feature = "napi4")]
pub(crate) fn unregister_resolver_env(env: sys::napi_env, owns_cleanup_hook: bool) -> Result<()> {
  if !owns_cleanup_hook {
    return Ok(());
  }
  clear_resolvers_for_env(env);
  if RESOLVER_CLEANUP_ENVS.with(|envs| envs.borrow().contains(&(env as EnvId))) {
    #[cfg(not(target_family = "wasm"))]
    let status =
      unsafe { sys::napi_remove_env_cleanup_hook(env, Some(resolver_env_cleanup), env.cast()) };
    #[cfg(target_family = "wasm")]
    let status =
      unsafe { crate::napi_remove_env_cleanup_hook(env, Some(resolver_env_cleanup), env.cast()) };
    if status != sys::Status::napi_ok {
      return Err(Error::new(
        Status::from(status),
        "Failed to remove SendableResolver environment cleanup hook",
      ));
    }
    release_resolver_env_cleanup(env as EnvId);
    retire_resolver_env(env as EnvId);
  }
  Ok(())
}

#[cfg(all(
  feature = "napi4",
  any(feature = "tokio_rt", feature = "async-runtime")
))]
pub(crate) fn cleanup_resolver_env(env: sys::napi_env, owns_cleanup_hook: bool) {
  if owns_cleanup_hook && release_resolver_env_cleanup(env as EnvId) {
    retire_resolver_env(env as EnvId);
    clear_resolvers_for_env(env);
    crate::bindgen_runtime::cleanup_registered_classes_for_env(env);
  }
}

#[cfg(feature = "napi3")]
unsafe extern "C" fn resolver_env_cleanup(data: *mut core::ffi::c_void) {
  let env = data.cast();
  if release_resolver_env_cleanup(env as EnvId) {
    retire_resolver_env(env as EnvId);
    crate::bindgen_runtime::with_runtime_teardown_guard(|| {
      crate::bindgen_runtime::catch_unwind_safely(|| {
        clear_resolvers_for_env(env);
        crate::bindgen_runtime::cleanup_registered_classes_for_env(env);
      });
    });
  }
}

/// A resolver handle that can cross worker threads while its closure remains owned by the
/// JavaScript thread where the handle was created.
///
/// With N-API 3 or newer, environment cleanup invalidates outstanding environment-associated
/// handles and drops their closures on the JavaScript owner thread. N-API 1/2 do not provide
/// environment cleanup hooks; on those API levels callers must consume or drop every handle on
/// its JavaScript owner thread before the Node environment closes. Dropping the last handle from
/// another thread only queues owner-thread cleanup and cannot guarantee reclamation before
/// teardown.
pub struct SendableResolver<
  Data: 'static + Send,
  R: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>,
> {
  handle: Arc<ResolverHandle>,
  _data: PhantomData<Data>,
  // The resolver itself remains in the owner JavaScript thread's registry. A function-pointer
  // marker preserves the generic type for downcasting without making auto-traits depend on R.
  _resolver: PhantomData<fn() -> R>,
}

impl<Data: 'static + Send, R: 'static + FnOnce(sys::napi_env, Data) -> Result<sys::napi_value>>
  SendableResolver<Data, R>
{
  /// Create a resolver owned by the current thread.
  ///
  /// The resolver is associated with the active callback environment. Without an active callback
  /// it uses the sole registered environment. After multiple environments or environment teardown
  /// are observed on the thread, unguarded construction fails closed for the remainder of that
  /// thread. Use [`Self::new_with_env`] whenever an [`Env`](crate::Env) is available explicitly.
  pub fn new(resolver: R) -> Self {
    match current_resolver_env() {
      ResolverEnvAssociation::Exact(env) => Self::insert(Some(env), resolver),
      ResolverEnvAssociation::Unbound => Self::insert(None, resolver),
      ResolverEnvAssociation::Unavailable(unavailable) => Self::unavailable(resolver, unavailable),
    }
  }

  /// Create a resolver owned by the current thread and an exact Node environment.
  pub fn new_with_env(env: &crate::Env, resolver: R) -> Self {
    Self::new_for_env(env.raw(), resolver)
  }

  #[cfg_attr(
    not(any(feature = "tokio_rt", feature = "async-runtime")),
    allow(dead_code)
  )]
  pub(crate) fn new_for_env(env: sys::napi_env, resolver: R) -> Self {
    Self::insert(Some(env as EnvId), resolver)
  }

  #[cfg_attr(
    not(any(feature = "tokio_rt", feature = "async-runtime")),
    allow(dead_code)
  )]
  pub(crate) fn clone_handle(&self) -> Self {
    Self {
      handle: Arc::clone(&self.handle),
      _data: PhantomData,
      _resolver: PhantomData,
    }
  }

  fn insert(env: Option<EnvId>, resolver: R) -> Self {
    let owner = thread::current().id();
    if env.is_some_and(|env| {
      CLOSING_ENVS.with(|envs| envs.borrow().contains(&env))
        || RETIRED_RESOLVER_ENVS.with(|envs| envs.borrow().contains(&env))
    }) {
      crate::bindgen_runtime::catch_unwind_safely(|| drop(resolver));
      return Self {
        handle: ResolverHandle::unavailable(owner, ResolverUnavailable::EnvironmentClosed),
        _data: PhantomData,
        _resolver: PhantomData,
      };
    }
    drain_pending_resolver_cleanup();
    let mut resolver: Option<Box<dyn Any>> = Some(Box::new(resolver));
    let handle = RESOLVERS.with(|resolvers| loop {
      let id = NEXT_RESOLVER_ID.fetch_add(1, Ordering::Relaxed);
      if id == 0 {
        continue;
      }
      if let Entry::Vacant(entry) = resolvers.borrow_mut().entry(id) {
        let handle = ResolverHandle::new(id, owner);
        entry.insert(ResolverEntry {
          env,
          identity: handle.key.identity.clone(),
          resolver: Some(resolver.take().expect("resolver is inserted exactly once")),
        });
        break handle;
      }
    });
    RESOLVER_THREAD_GUARD.with(|_| {});
    RESOLVER_CLEANUP
      .lock()
      .unwrap_or_else(std::sync::PoisonError::into_inner)
      .active_owners
      .insert(owner);
    Self {
      handle,
      _data: PhantomData,
      _resolver: PhantomData,
    }
  }

  fn unavailable(resolver: R, unavailable: ResolverUnavailable) -> Self {
    let owner = thread::current().id();
    crate::bindgen_runtime::catch_unwind_safely(|| drop(resolver));
    Self {
      handle: ResolverHandle::unavailable(owner, unavailable),
      _data: PhantomData,
      _resolver: PhantomData,
    }
  }

  /// Resolve on the owner thread where this handle was created.
  ///
  /// The handle may cross worker threads, but consuming it there returns a cancellation error
  /// and queues the owner-thread closure for cleanup by the next resolver operation on that
  /// thread or, on N-API 3+, by environment teardown, instead of invoking it on the wrong thread.
  pub fn resolve(self, env: sys::napi_env, data: Data) -> Result<sys::napi_value> {
    let (owner_env, resolver) = self.take()?;
    if owner_env.is_some_and(|owner_env| owner_env != env as EnvId) {
      crate::bindgen_runtime::catch_unwind_safely(|| drop(resolver));
      return Err(Error::new(
        Status::InvalidArg,
        "Async resolver belongs to a different Node environment",
      ));
    }
    let _callback_env = crate::bindgen_runtime::enter_callback_env(env);
    resolver(env, data)
  }

  #[cfg_attr(
    not(any(feature = "tokio_rt", feature = "async-runtime")),
    allow(dead_code)
  )]
  pub(crate) fn discard(self) -> Result<()> {
    self.take().map(drop)
  }

  fn take(self) -> Result<(Option<EnvId>, R)> {
    if self.handle.owner != thread::current().id() {
      return Err(Error::new(
        Status::Cancelled,
        "Async resolver must be consumed on the thread where it was created",
      ));
    }
    if let Some(unavailable) = self.handle.unavailable {
      return Err(unavailable.error());
    }
    drain_pending_resolver_cleanup();
    let mut entry = RESOLVERS
      .with(|resolvers| remove_resolver(&mut resolvers.borrow_mut(), &self.handle.key))
      .ok_or_else(|| {
        Error::new(
          Status::Cancelled,
          "Async resolver is no longer available because its Node environment was closed",
        )
      })?;
    let resolver = entry
      .resolver
      .take()
      .expect("registered resolver is taken exactly once");
    match resolver.downcast::<R>() {
      Ok(resolver) => Ok((entry.env, *resolver)),
      Err(resolver) => {
        crate::bindgen_runtime::catch_unwind_safely(|| drop(resolver));
        Err(Error::new(
          Status::GenericFailure,
          "Async resolver type mismatch",
        ))
      }
    }
  }
}

#[cfg_attr(
  not(any(feature = "tokio_rt", feature = "async-runtime")),
  allow(dead_code)
)]
pub(crate) fn clear_resolvers_for_env(env: sys::napi_env) {
  drain_pending_resolver_cleanup();
  CLOSING_ENVS.with(|envs| {
    envs.borrow_mut().insert(env as EnvId);
  });
  let entries = RESOLVERS.with(|resolvers| {
    let mut resolvers = resolvers.borrow_mut();
    let ids = resolvers
      .iter()
      .filter_map(|(id, entry)| (entry.env == Some(env as EnvId)).then_some(*id))
      .collect::<Vec<_>>();
    ids
      .into_iter()
      .filter_map(|id| resolvers.remove(&id))
      .collect::<Vec<_>>()
  });
  for entry in entries {
    drop(entry);
  }
  CLOSING_ENVS.with(|envs| {
    envs.borrow_mut().remove(&(env as EnvId));
  });
}

#[cfg(test)]
mod tests {
  use std::{
    cell::Cell,
    ptr,
    rc::Rc,
    sync::mpsc,
    thread::{self, ThreadId},
  };

  use super::*;

  type TestResolver = fn(sys::napi_env, u32) -> Result<sys::napi_value>;
  type TestSendableResolver = SendableResolver<u32, TestResolver>;

  struct DropThread {
    dropped_on: Rc<Cell<Option<ThreadId>>>,
  }

  impl Drop for DropThread {
    fn drop(&mut self) {
      self.dropped_on.set(Some(thread::current().id()));
    }
  }

  struct ReentrantDrop {
    dropped: Rc<Cell<bool>>,
    nested_dropped_on: Rc<Cell<Option<ThreadId>>>,
  }

  impl Drop for ReentrantDrop {
    fn drop(&mut self) {
      self.dropped.set(true);
      let captured = DropThread {
        dropped_on: Rc::clone(&self.nested_dropped_on),
      };
      let nested = SendableResolver::new(move |_, _: u32| {
        drop(captured);
        Ok(ptr::null_mut())
      });
      std::mem::forget(nested);
    }
  }

  struct PanickingDrop;

  impl Drop for PanickingDrop {
    fn drop(&mut self) {
      panic!("resolver destructor panic");
    }
  }

  fn return_resolved_value(_env: sys::napi_env, value: u32) -> Result<sys::napi_value> {
    Ok(value as usize as sys::napi_value)
  }

  fn test_sendable_resolver(id: ResolverId) -> TestSendableResolver {
    TestSendableResolver {
      handle: ResolverHandle::new(id, thread::current().id()),
      _data: PhantomData,
      _resolver: PhantomData,
    }
  }

  fn insert_test_resolver(key: &ResolverKey, env: sys::napi_env) {
    let previous = RESOLVERS.with(|resolvers| {
      resolvers.borrow_mut().insert(
        key.id,
        ResolverEntry {
          env: Some(env as EnvId),
          identity: key.identity.clone(),
          resolver: Some(Box::new(return_resolved_value as TestResolver)),
        },
      )
    });
    assert!(previous.is_none());
  }

  #[test]
  fn non_send_resolver_is_owned_and_dropped_by_the_javascript_thread() {
    let owner_thread = thread::current().id();
    let dropped_on = Rc::new(Cell::new(None));
    let captured = DropThread {
      dropped_on: Rc::clone(&dropped_on),
    };
    let resolver = SendableResolver::new(move |_, value: u32| {
      drop(captured);
      Ok(value as sys::napi_value)
    });

    let (sender, receiver) = mpsc::channel();
    thread::spawn(move || sender.send(resolver).unwrap())
      .join()
      .unwrap();
    let resolver = receiver.recv().unwrap();

    resolver.resolve(ptr::null_mut(), 1).unwrap();
    assert_eq!(dropped_on.get(), Some(owner_thread));
  }

  #[test]
  fn dropping_an_internal_clone_does_not_invalidate_the_resolver() {
    let resolver = SendableResolver::new_for_env(ptr::null_mut(), |_, value: u32| {
      Ok(value as sys::napi_value)
    });
    let clone = resolver.clone_handle();

    drop(clone);

    assert_eq!(resolver.resolve(ptr::null_mut(), 42).unwrap() as usize, 42);
  }

  #[test]
  fn stale_resolver_handles_cannot_alias_a_reused_id_after_wrap() {
    let env = 5usize as sys::napi_env;
    let stale_drop = test_sendable_resolver(1);
    let stale_key = stale_drop.handle.key.clone();
    insert_test_resolver(&stale_key, env);
    clear_resolvers_for_env(env);

    let replacement = test_sendable_resolver(stale_key.id);
    let replacement_key = replacement.handle.key.clone();
    insert_test_resolver(&replacement_key, env);

    drop(stale_drop);
    assert_eq!(replacement.resolve(env, 41).unwrap() as usize, 41);

    let stale_consume = test_sendable_resolver(1);
    let stale_key = stale_consume.handle.key.clone();
    insert_test_resolver(&stale_key, env);
    clear_resolvers_for_env(env);

    let replacement = test_sendable_resolver(stale_key.id);
    let replacement_key = replacement.handle.key.clone();
    insert_test_resolver(&replacement_key, env);

    let error = stale_consume
      .resolve(env, 1)
      .expect_err("a stale resolver must not consume a reused registry ID");
    assert!(error.reason.contains("environment was closed"));
    assert_eq!(replacement.resolve(env, 42).unwrap() as usize, 42);
  }

  #[test]
  fn stale_off_owner_cleanup_cannot_remove_a_reused_resolver_id() {
    let env = 6usize as sys::napi_env;
    let owner = thread::current().id();
    let stale = test_sendable_resolver(1);
    let stale_key = stale.handle.key.clone();
    insert_test_resolver(&stale_key, env);
    clear_resolvers_for_env(env);

    let replacement = test_sendable_resolver(stale_key.id);
    let replacement_key = replacement.handle.key.clone();
    insert_test_resolver(&replacement_key, env);

    RESOLVER_THREAD_GUARD.with(|_| {});
    RESOLVER_CLEANUP
      .lock()
      .unwrap_or_else(std::sync::PoisonError::into_inner)
      .active_owners
      .insert(owner);
    thread::spawn(move || drop(stale)).join().unwrap();
    assert_eq!(
      RESOLVER_CLEANUP
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner)
        .pending
        .get(&owner)
        .map(Vec::len),
      Some(1)
    );

    drain_pending_resolver_cleanup();
    assert_eq!(replacement.resolve(env, 42).unwrap() as usize, 42);
  }

  #[test]
  #[cfg(all(
    feature = "napi4",
    any(feature = "tokio_rt", feature = "async-runtime")
  ))]
  fn duplicate_resolver_env_cleanup_preserves_the_owner_registry() {
    let env = 7usize as sys::napi_env;
    assert!(claim_resolver_env_cleanup(env as EnvId));
    assert!(!claim_resolver_env_cleanup(env as EnvId));

    let duplicate_probe = SendableResolver::new_for_env(env, return_resolved_value as TestResolver);
    unregister_resolver_env(env, false).unwrap();
    assert_eq!(duplicate_probe.resolve(env, 41).unwrap() as usize, 41);

    let owner_probe = SendableResolver::new_for_env(env, return_resolved_value as TestResolver);
    cleanup_resolver_env(env, true);
    let error = owner_probe
      .resolve(env, 42)
      .expect_err("the owner cleanup must invalidate the resolver registry");
    assert!(error.reason.contains("environment was closed"));

    assert!(claim_resolver_env_cleanup(env as EnvId));
    assert!(release_resolver_env_cleanup(env as EnvId));
  }

  #[test]
  fn off_owner_resolution_defers_cleanup_to_the_owner_thread() {
    let owner_thread = thread::current().id();
    let dropped_on = Rc::new(Cell::new(None));
    let captured = DropThread {
      dropped_on: Rc::clone(&dropped_on),
    };
    let resolver = SendableResolver::new(move |_, _: u32| {
      drop(captured);
      Ok(ptr::null_mut())
    });

    let error = thread::spawn(move || resolver.resolve(ptr::null_mut(), 1).unwrap_err().reason)
      .join()
      .unwrap();
    assert!(error.contains("thread where it was created"));
    assert_eq!(dropped_on.get(), None);

    let trigger = SendableResolver::new(|_, _: u32| Ok(ptr::null_mut()));
    drop(trigger);
    assert_eq!(dropped_on.get(), Some(owner_thread));
  }

  #[test]
  fn environment_cleanup_drops_registered_resolvers_on_the_owner_thread() {
    let owner_thread = thread::current().id();
    let dropped_on = Rc::new(Cell::new(None));
    let captured = DropThread {
      dropped_on: Rc::clone(&dropped_on),
    };
    let env = 1usize as sys::napi_env;
    let resolver =
      SendableResolver::new_for_env(env, move |_, _: u32| -> Result<sys::napi_value> {
        drop(captured);
        Ok(ptr::null_mut())
      });

    thread::spawn(move || {
      let _resolver = resolver;
    })
    .join()
    .unwrap();
    assert_eq!(dropped_on.get(), None);

    clear_resolvers_for_env(env);
    assert_eq!(dropped_on.get(), Some(owner_thread));
  }

  #[test]
  fn public_constructor_is_bound_to_the_current_environment() {
    let owner_thread = thread::current().id();
    let env = 4usize as sys::napi_env;
    let dropped_on = Rc::new(Cell::new(None));
    let captured = DropThread {
      dropped_on: Rc::clone(&dropped_on),
    };
    let resolver = {
      let _callback_env = crate::bindgen_runtime::enter_callback_env(env);
      SendableResolver::new(move |_, _: u32| {
        drop(captured);
        Ok(ptr::null_mut())
      })
    };
    let (ready_tx, ready_rx) = mpsc::channel();
    let (release_tx, release_rx) = mpsc::channel();
    let (resolver_tx, resolver_rx) = mpsc::channel();
    let worker = thread::spawn(move || {
      ready_tx.send(()).unwrap();
      release_rx.recv().unwrap();
      resolver_tx.send(resolver).unwrap();
    });

    ready_rx.recv().unwrap();
    clear_resolvers_for_env(env);
    release_tx.send(()).unwrap();
    let resolver = resolver_rx.recv().unwrap();
    worker.join().unwrap();

    assert_eq!(dropped_on.get(), Some(owner_thread));
    let error = resolver
      .resolve(env, 1)
      .expect_err("environment cleanup must invalidate the public resolver handle");
    assert!(error.reason.contains("environment was closed"));
  }

  #[test]
  #[cfg(feature = "napi3")]
  fn ambiguous_environment_construction_fails_closed_through_teardown() {
    let owner_thread = thread::current().id();
    let first_env = 8usize as sys::napi_env;
    let second_env = 9usize as sys::napi_env;
    assert!(claim_resolver_env_cleanup(first_env as EnvId));
    assert!(claim_resolver_env_cleanup(second_env as EnvId));
    mark_resolver_env_registered();

    let guarded_resolver = {
      let _callback_env = crate::bindgen_runtime::enter_callback_env(second_env);
      SendableResolver::new(return_resolved_value as TestResolver)
    };

    let first_drop = Rc::new(Cell::new(None));
    let first_capture = DropThread {
      dropped_on: Rc::clone(&first_drop),
    };
    let first_resolver = SendableResolver::new(move |_, _: u32| {
      drop(first_capture);
      Ok(ptr::null_mut())
    });
    assert_eq!(first_drop.get(), Some(owner_thread));
    let error = first_resolver
      .resolve(second_env, 41)
      .expect_err("an unguarded resolver must fail closed while environments are ambiguous");
    assert_eq!(error.status, Status::InvalidArg);
    assert!(error.reason.contains("exact Node environment"));

    assert!(release_resolver_env_cleanup(first_env as EnvId));
    retire_resolver_env(first_env as EnvId);
    clear_resolvers_for_env(first_env);

    let survivor_drop = Rc::new(Cell::new(None));
    let survivor_capture = DropThread {
      dropped_on: Rc::clone(&survivor_drop),
    };
    let survivor_resolver = SendableResolver::new(move |_, _: u32| {
      drop(survivor_capture);
      Ok(ptr::null_mut())
    });
    assert_eq!(survivor_drop.get(), Some(owner_thread));
    let error = survivor_resolver
      .resolve(second_env, 42)
      .expect_err("ambiguity must remain fail-closed until every overlapping env is gone");
    assert_eq!(error.status, Status::InvalidArg);
    assert_eq!(
      guarded_resolver.resolve(second_env, 42).unwrap() as usize,
      42
    );

    assert!(release_resolver_env_cleanup(second_env as EnvId));
    retire_resolver_env(second_env as EnvId);
    clear_resolvers_for_env(second_env);

    let retired_drop = Rc::new(Cell::new(None));
    let retired_capture = DropThread {
      dropped_on: Rc::clone(&retired_drop),
    };
    let retired_resolver = SendableResolver::new(move |_, _: u32| {
      drop(retired_capture);
      Ok(ptr::null_mut())
    });
    assert_eq!(retired_drop.get(), Some(owner_thread));
    let error = retired_resolver
      .resolve(second_env, 43)
      .expect_err("construction after the final env retires must fail closed");
    assert_eq!(error.status, Status::InvalidArg);
    assert!(error.reason.contains("exact Node environment"));
  }

  #[test]
  fn nested_callback_environments_bind_public_resolvers_exactly() {
    let outer_env = 10usize as sys::napi_env;
    let inner_env = 11usize as sys::napi_env;
    let (outer_resolver, inner_resolver) = {
      let _outer_callback_env = crate::bindgen_runtime::enter_callback_env(outer_env);
      let outer_resolver = SendableResolver::new(return_resolved_value as TestResolver);
      let inner_resolver = {
        let _inner_callback_env = crate::bindgen_runtime::enter_callback_env(inner_env);
        SendableResolver::new(return_resolved_value as TestResolver)
      };
      (outer_resolver, inner_resolver)
    };

    clear_resolvers_for_env(outer_env);

    let error = outer_resolver
      .resolve(outer_env, 41)
      .expect_err("the outer environment cleanup must invalidate only its resolver");
    assert!(error.reason.contains("environment was closed"));
    assert_eq!(inner_resolver.resolve(inner_env, 42).unwrap() as usize, 42);
  }

  #[test]
  fn environment_bound_resolver_rejects_a_foreign_environment() {
    let owner_env = 12usize as sys::napi_env;
    let foreign_env = 13usize as sys::napi_env;
    let env = crate::Env::from_raw(owner_env);
    let resolver = SendableResolver::new_with_env(&env, return_resolved_value as TestResolver);

    let error = resolver
      .resolve(foreign_env, 42)
      .expect_err("an exact environment association must be enforced at resolution");

    assert_eq!(error.status, Status::InvalidArg);
    assert!(error.reason.contains("different Node environment"));
  }

  #[test]
  fn environment_cleanup_allows_reentrant_resolver_destruction() {
    let env = 2usize as sys::napi_env;
    let dropped = Rc::new(Cell::new(false));
    let nested_dropped_on = Rc::new(Cell::new(None));
    let captured = ReentrantDrop {
      dropped: Rc::clone(&dropped),
      nested_dropped_on: Rc::clone(&nested_dropped_on),
    };
    let _resolver =
      SendableResolver::new_for_env(env, move |_, _: u32| -> Result<sys::napi_value> {
        drop(captured);
        Ok(ptr::null_mut())
      });

    clear_resolvers_for_env(env);
    assert!(dropped.get());
    assert_eq!(nested_dropped_on.get(), Some(thread::current().id()));
  }

  #[test]
  fn environment_cleanup_contains_resolver_destructor_panics() {
    let env = 3usize as sys::napi_env;
    let captured = PanickingDrop;
    let _resolver =
      SendableResolver::new_for_env(env, move |_, _: u32| -> Result<sys::napi_value> {
        drop(captured);
        Ok(ptr::null_mut())
      });

    clear_resolvers_for_env(env);
  }
}