Repository: parcel-bundler/parcel
File: crates/node-bindings/src/helpers/js_callable/js_callable.rs (Rust)

Code before:
use std::collections::HashMap;
use std::panic;
use std::rc::Rc;
use std::sync::mpsc::channel;
use std::sync::mpsc::Sender;
use std::thread::ThreadId;

use super::JsValue;
use erased_serde::Serialize as ErasedSerialize;
use napi::bindgen_prelude::Array;
use napi::bindgen_prelude::FromNapiValue;
use napi::threadsafe_function::ErrorStrategy;
use napi::threadsafe_function::ThreadSafeCallContext;
use napi::threadsafe_function::ThreadsafeFunction;
use napi::threadsafe_function::ThreadsafeFunctionCallMode;
use napi::Env;
use napi::JsFunction;
use napi::JsObject;
use napi::JsUnknown;
use napi::NapiRaw;
use serde::de::DeserializeOwned;
use serde::Serialize;

thread_local! {
  /// Storage for napi JavaScript functions on the local thread
  static LOCAL_FUNCTIONS: (RefCell<usize>, RefCell<HashMap<usize, Rc<JsValue>>>) = Default::default();
}

/// JsCallable provides a Send + Sync wrapper around callable JavaScript functions.
/// Functions can be called from threads or the main thread.
/// Parameters and return types will automatically be converted using serde.
pub struct JsCallable {
  initial_thread: ThreadId,
  tsfn: ThreadsafeFunction<Box<dyn ErasedSerialize>, ErrorStrategy::Fatal>,
  callback: usize,
}

impl JsCallable {
  pub fn new(callback: JsFunction) -> napi::Result<Self> {
    let initial_thread = std::thread::current().id();

    // Store the threadsafe function on the struct
    let tsfn: ThreadsafeFunction<Box<dyn ErasedSerialize>, ErrorStrategy::Fatal> = callback
      .create_threadsafe_function(0, |ctx: ThreadSafeCallContext<Box<dyn ErasedSerialize>>| {
        let result = ctx.env.to_js_value(&ctx.value)?;
        if result.is_array()? {
          // SAFETY: type assertion above
          let result = panic::catch_unwind::<_, napi::Result<Array>>(|| unsafe {
            Array::from_napi_value(ctx.env.raw(), result.raw())
          })
          .map_err(|_| napi::Error::from_reason("Unable to cast to array"))??;

Review comment (refactor):
I think everything looks pretty good, except for the unsafe & type-erasure this introduces.

But IMO it's much much simpler to just have 2 forms of delegation:

* One is using multi-threaded function & delegates using the `create_js_thread_safe_method` and `create_async_js_thread_safe_method`, which are combined around 40 lines of code with no unsafe
* The other can use normal delegation - https://github.com/parcel-bundler/parcel/blob/25b7f904fa7511b0a88b7c90252d4d66d51014cf/crates/parcel_filesystem/src/js_delegate_file_system.rs#L44

If we found that there is a need to write an abstraction that works for both cases, we can use it.

Code after:
use std::thread::ThreadId;

use napi::threadsafe_function::ErrorStrategy;
use napi::threadsafe_function::ThreadSafeCallContext;
use napi::threadsafe_function::ThreadsafeFunction;
use napi::threadsafe_function::ThreadsafeFunctionCallMode;
use napi::Env;
use napi::JsFunction;
use napi::JsObject;
use napi::JsUnknown;
use serde::de::DeserializeOwned;
use serde::Serialize;

use super::map_params_serde;
use super::map_return_serde;
use super::JsValue;

pub type MapJsParams = Box<dyn FnOnce(&Env) -> napi::Result<Vec<JsUnknown>> + 'static>;
pub type MapJsReturn<Return> = Box<dyn Fn(&Env, JsUnknown) -> napi::Result<Return> + 'static>;

/// JsCallable provides a Send + Sync wrapper around callable JavaScript functions.
/// Functions can be called from threads or the main thread.
/// Parameters and return types can be mapped by the caller.
pub struct JsCallable {
  #[cfg(debug_assertions)]
  initial_thread: ThreadId,
  tsfn: ThreadsafeFunction<MapJsParams, ErrorStrategy::Fatal>,
}

impl JsCallable {
  pub fn new(callback: JsFunction) -> napi::Result<Self> {
    // Store the threadsafe function on the struct
    let tsfn: ThreadsafeFunction<MapJsParams, ErrorStrategy::Fatal> = callback
      .create_threadsafe_function(0, |ctx: ThreadSafeCallContext<MapJsParams>| {
        (ctx.value)(&ctx.env)
      })?;

    Ok(Self {
      #[cfg(debug_assertions)]
      initial_thread: std::thread::current().id(),
      tsfn,
    })
  }

  /// Construct a JsCallable from an object property
  pub fn new_from_object_prop(method_name: &str, obj: &JsObject) -> napi::Result<Self> {
    Self::new(obj.get_named_property(method_name)?)
  }

  /// Construct a JsCallable from an object property, binding it to the source object
  pub fn new_from_object_prop_bound(method_name: &str, obj: &JsObject) -> napi::Result<Self> {