Yew: Consider handling async service effects through futures

Created on 16 Aug 2018  Â·  11Comments  Â·  Source: yewstack/yew

Description

I'm submitting a feature request.

As it currently stands, asynchronous tasks are done through functions exposed by services, taking user-supplied callbacks which handle effects upon completion.

This approach is not very idiomatic. Besides, callback-structured code tends to be brittle and compose poorly.

The underlying Web APIs make use of promises. With the new wasm-bindgen-futures, it might be plausible to switch to using futures instead, without much implementation hassle. This would integrate well with the rest of the Rust async ecosystem.

feature

Most helpful comment

Hi! Since yew now supports wasm-bindgen, it's possible to use async await alongside yew.

Here's a little snippet that I've created to let rust handle the async task via futures.

use wasm_timer::Delay;
use std::sync::Arc;
use futures::lock::Mutex;
use wasm_bindgen_futures::futures_0_3::{future_to_promise, spawn_local, JsFuture};

//
// typical yew setup, I created a minimal model that act as an counter.
//

#[wasm_bindgen]
pub fn run_yew_fut() -> Result<(), JsValue> {
    spawn_local(async {
        let scope = make_scope();
        let guarded_scope = Arc::new(Mutex::new(scope));
        let fut1 = call_scope_later(guarded_scope.clone());
        let fut2 = log_later();

        let chained = futures::future::join(fut1, fut2);
        let resolved = chained.await;
    });
    Ok(())
}

fn make_scope() -> yew::html::Scope<Model> {
    yew::initialize();
    let app: App<Model> = App::new();
    app.mount_to_body()
}

async fn log_later() {
    console_log::init_with_level(Level::Debug).unwrap();
    let mut counts: usize = 0;
    loop {
        Delay::new(Duration::from_secs(1)).await.unwrap();
        counts += 1;
        info!("logged: {} times", counts);
    }
}

async fn call_scope_later(scope: Arc<Mutex<yew::html::Scope<Model>>>) {
    let mut inner = scope.lock().await;
    let mut counter = 0;
    while counter < 10 {
        Delay::new(Duration::from_secs(1)).await.unwrap();
        inner.send_message(Msg::DoIt);
        counter += 1;
    }
}

Although I'm not sure who is handling the UI loop since I didn't execute yew::run_loop(), but here's what I've got.
Maybe some work flow might benefit more from using futures instead of js promises.

out

You can see that the two async funtion are not blocking each other, either prevents the user from interacting with the component.

Noted that wasm didn't have mature threading support right now, so I have to use local executor. Maybe after we have threading in wasm we can make this into a threaded UI library.

All 11 comments

Hi @DenisKolodin!

I'd like to work on this one, but I'm a bit unfamiliar with the code.
Perhaps you could give me some directions on gitter? If possible, just ping me and I'll reply.

Thanks.

I believe this would be difficult to implement without rewriting Yew to use wasm-bindgen instead of stdweb. Maybe it would be worth it to make that switch though? @DenisKolodin thoughts?

Hi! Since yew now supports wasm-bindgen, it's possible to use async await alongside yew.

Here's a little snippet that I've created to let rust handle the async task via futures.

use wasm_timer::Delay;
use std::sync::Arc;
use futures::lock::Mutex;
use wasm_bindgen_futures::futures_0_3::{future_to_promise, spawn_local, JsFuture};

//
// typical yew setup, I created a minimal model that act as an counter.
//

#[wasm_bindgen]
pub fn run_yew_fut() -> Result<(), JsValue> {
    spawn_local(async {
        let scope = make_scope();
        let guarded_scope = Arc::new(Mutex::new(scope));
        let fut1 = call_scope_later(guarded_scope.clone());
        let fut2 = log_later();

        let chained = futures::future::join(fut1, fut2);
        let resolved = chained.await;
    });
    Ok(())
}

fn make_scope() -> yew::html::Scope<Model> {
    yew::initialize();
    let app: App<Model> = App::new();
    app.mount_to_body()
}

async fn log_later() {
    console_log::init_with_level(Level::Debug).unwrap();
    let mut counts: usize = 0;
    loop {
        Delay::new(Duration::from_secs(1)).await.unwrap();
        counts += 1;
        info!("logged: {} times", counts);
    }
}

async fn call_scope_later(scope: Arc<Mutex<yew::html::Scope<Model>>>) {
    let mut inner = scope.lock().await;
    let mut counter = 0;
    while counter < 10 {
        Delay::new(Duration::from_secs(1)).await.unwrap();
        inner.send_message(Msg::DoIt);
        counter += 1;
    }
}

Although I'm not sure who is handling the UI loop since I didn't execute yew::run_loop(), but here's what I've got.
Maybe some work flow might benefit more from using futures instead of js promises.

out

You can see that the two async funtion are not blocking each other, either prevents the user from interacting with the component.

Noted that wasm didn't have mature threading support right now, so I have to use local executor. Maybe after we have threading in wasm we can make this into a threaded UI library.

It seems that wasm-bindgen >= 0.2.48 is required to support async-await.
Currently, yew 0.8 set the dependency of wasm-bindgen=0.2.42, so if you want to try async/await, you might need to stick to 0.7 for now.

Thanks for calling this out @extraymond, later versions of wasm-bindgen were breaking stdweb so I fixed the version for now. Created an issue here: https://github.com/yewstack/yew/issues/586

Sure! Looking forward to future releases. It's getting better and better every releases.

rustasync team had released a http request library surf, which can be used under wasm and async. Seems like the easiest way to integrate fetch as future.

The only drawback to surf is it doesn't support aborting/dropping requests.
https://github.com/rustasync/surf/issues/26#issuecomment-524636486

Thx for the update. I believe futures can be aborted by wrapping it in a Abortable.

use futures::future::{AbortHandle, Abortable};

fn create_abort_handle() -> AbortHandle  {
    let (abort_handle, abort_registration) = AbortHandle::new_pair();
    let req_futures = create_request(some_url);
    let wrapped_req = Abortable::new(req_futures, abort_registration);
    spawn_local(wrapped_req);
    abort_handle
}

fn main() {
    let abort_handle = create_abort_handle();
    abort_handle.abort();
}

I tried AbortHandle to cancel tasks, and it worked right away. Even wrapping promise to a JsFuture works.

However I'm not sure if this will stop the browser from requesting, or just ignoring the incoming response into the wasm program. I think the latter case is ideal enough though.

Having async operations available would also help in dealing with heavily asynchronous APIs accessed through web-sys. In the tests I'm currently doing that appears to work well, and I'm using code like

async fn discover() -> Result<web_sys::BluetoothRemoteGattServer, String> {
    [ lots of web-sys calls through JsFuture ]

    // This still has to get better ergonomics, but one thing at a time…
    let connection = gatt.connect();
    let connection = wasm_bindgen_futures::JsFuture::from(connection).await
        .map_err(|e| format!("Connect failed {:?}", e))?;
    let connection = web_sys::BluetoothRemoteGattServer::from(connection);

    Ok(connection)
}

async fn wrap<F: std::future::Future>(f: F, done_cb: yew::Callback<F::Output>) {
    done_cb.emit(f.await);
}

impl Component for BleBridge {
    [...]
    fn update(&mut self, msg: Self::Message) -> ShouldRender {
        match msg {
            Msg::GattConnectClicked => {
                wasm_bindgen_futures::spawn_local(wrap(discover(),
                self.link.callback(|e| Msg::GattConnectDone(e))));
            }
            Msg::GattConnectDone(d) => {
                match d {
                    Ok(d) => { info!("Got {:?}", d); }
                    Err(d) => { info!("Error: {:?}", d); }
                }
            }
        }
        false
    }
}

For some applications, it may suffice to document that this works (provided it's correct w/rt when emit may be called). This shouldn't stop any deeper integration of async patterns into yew, but may save current users the despair of thinking that there's no futures / async support around because none of it is mentioned in the documentation.

I really like this solution (it feels satisfying as I read the short code excerpt) 🙃.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

kellytk picture kellytk  Â·  3Comments

ghost picture ghost  Â·  5Comments

thienpow picture thienpow  Â·  3Comments

ghost picture ghost  Â·  4Comments

DenisKolodin picture DenisKolodin  Â·  5Comments