Search code examples
rustpromisefuturewebassemblywasm-bindgen

Why does js_sys Promise::new require FnMut?


js_sys exposes JavaScript Promise via a function pub fn new(cb: &mut dyn FnMut(Function, Function)) -> Promise;. Per my reading of the MDN documentation, there's nothing suggesting that the executor function will be called more than once, yet js_sys exposes it as an FnMut instead of an FnOnce.

Because of this, I can't wrap my head around a simple task, such as sending a message to myself in the future via a channel. Ideally, the sender half of the channel would be moved into the closure and Promise::new would accept an FnOnce.

async func send_to_future_self() {
  use std::sync::mpsc::{Sender, Receiver, channel};
  let (sender, receiver) = channel().split();

  // Schedule sending a message in the future

  // Option A: Move `sender` --> The closure becomes `FnOnce` because it consumes `sender`
  // Option B: Reference `sender` --> Then the borrow outlives `sender`'s lifetime

  let mut sleep_100 = move?? |accept: Function, _reject: Function| {
    // This Rust closure is exported to Javascript...
    let callback = Closure::__??__(move?? || {
      // Send a message via the channel
      sender.send(()).unwrap();
      let result = js_sys::Array::new();
      // Doesn't matter what value is passed here, it's just for synchronization
      result.push(&JsValue::from(true)); 
      accept.apply(&JsValue::undefined(), &result).unwrap();
    });

    web_sys::window().unwrap()
      .set_timeout_with_callback_and_timeout_and_arguments_0(
         callback.as_ref().unchecked_ref(), 
        100
      ).unwrap();
    // ... so intentionally forget it, otherwise it will be dropped at the end of this block 
    // despite JS still referring to it.
    callback.forget();
  };

  // Convert JS Promise into a Rust future
  // This wants an FnMut(Function, Function)
  let future: JsFuture = js_sys::Promise::new(&mut sleep_100).into();
  let _ = future.await;
  receiver.try_recv().unwrap();
}

Solution

  • I suspect that it gets wrapped as a JavaScript function object internally, which can be called multiple times (even though it won't be).

    The simplest way you can get around this is by having your function be callable multiple times. An easy way to do this is to wrap consumable values in Option and use Option::take() to get the value -- if you get back None then the function was called multiple times.

    Here is some sample code illustrating this technique:

    struct Consumable;
    
    impl Consumable {
        fn consume(self) {}
    }
    
    fn requires_fnmut(_: impl FnMut()) {}
    
    fn main() {
        let mut x = Some(Consumable);
        
        requires_fnmut(move || {
            let x = match x.take() {
                Some(v) => v,
                
                // If None we were called twice, so just bail.
                None => return,
            };
            
            x.consume();
        });
    }
    

    Here, Consumable is similar to Sender in your case: it exposes a method that consumes the receiver. Calling this on a captured variable in a closure would make the closure implement FnOnce and not FnMut, as you have found yourself.

    However, notice in main() that we wrap the Consumable in an Option and mark it mut:

    let mut x = Some(Consumable);
    

    Now we can "unwrap" this using match x.take() in the closure, returning from the closure if take() returns None, which would indicate that the function was called a second time.

    After taking the value from the Option you are free to consume it.


    It looks like in your question you have two nested closures, both of which need to be FnMut, and the inner one is what needs access to the Sender. You can still use this approach, but you have to do the same thing in both closures to transfer the consumable value from the outer closure to the inner closure. Here is an example:

    fn main() {
        let mut x = Some(Consumable);
        //      ^ The outer closure takes ownership of this x.
        
        requires_fnmut(move || {
            let mut x = x.take();
            //      ^ The inner closure takes ownership of this x.
            
            if x.is_none() {
                // We were already called once.
                return;
            }
            
            requires_fnmut(move || {
                let x = match x.take() {
                    Some(v) => v,
                    
                    // We were already called once.
                    None => return,
                };
                
                x.consume();
            });
        });
    }