Search code examples
rustwebassemblywasm-bindgenrust-wasm

Rust/WebAssembly -- streaming HTTP request: convert JsValue from ReadableStreamDefaultReader.read into vector


I'm new to Rust and trying to implement a web page that shows a graph with many edges. I plan to use WebAssembly to lay out the graph and determine the positions of the nodes (and a WebGL library to draw the graph).

Context

I want Rust/wasm to make a streaming request to a biggish (7mb potentially)binary composed of sixteen bit integers, delimited by the max value, representing target node indices for each index.

Because of the potentially big file, I'd like Rust to stream it and start laying out the graph as soon as it has the first chunk.

Problem

Eventually, I'd like to turn the JsValue chunks of the response body into vectors of 16-bit integers, but just coercing the chunks into some kind of array type would be enough to unblock me.

Attempted solutions

Below is the start of my lib.rs, which as far as I can tell at least works.

use js_sys::Uint8Array;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{ReadableStreamDefaultReader, Response};
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub async fn fetch_and_compute_graph() -> Result<JsValue, JsValue> {
    let window = web_sys::window().unwrap();
    let resp_promise = window.fetch_with_str(&"./edges.bin");
    let resp_value = JsFuture::from(resp_promise).await?;
    let resp: Response = resp_value.dyn_into().unwrap();
    log(&format!("Response status code: {}", resp.status()));
    if resp.status() != 200 {
        return Err(JsValue::FALSE);
    }
    let reader_obj = resp.body().unwrap().get_reader();
    let stream_reader: ReadableStreamDefaultReader = reader_obj.dyn_into().unwrap();

Below are two snippets I've tried putting after the above code:

Update: I've realised that JsFuture::from(stream_reader.read()).await?; results in a JS object with two properties. So maybe I can just cast to Object

1. Type casting solution?

    let chunk_obj = JsFuture::from(stream_reader.read()).await?;
    let chunk_bytes: Uint8Array = chunk_obj.dyn_into().unwrap();

I would have expected this to work, given that the other type casts do. Maybe I've got the type wrong; the API docs aren't clear on what the promise should resolve to. However, manually inspecting one of the chunks resulting from a fetch call in my browser console confirmed that it was a Uint8Array.

Here's as much of a stack trace as I could get:

Uncaught (in promise) RuntimeError: unreachable executed
    __wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
    real http://localhost:8080/pkg/rust_wasm_centrality.js:189
    promise callback*getImports/imports.wbg.__wbg_then_11f7a54d67b4bfad http://localhost:8080/pkg/rust_wasm_centrality.js:326
    __wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
    real http://localhost:8080/pkg/rust_wasm_centrality.js:189
rust_wasm_centrality_bg.wasm:24649:1
Uncaught (in promise) RuntimeError: unreachable executed
    __wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
    real http://localhost:8080/pkg/rust_wasm_centrality.js:189
    promise callback*getImports/imports.wbg.__wbg_then_11f7a54d67b4bfad http://localhost:8080/pkg/rust_wasm_centrality.js:326
    __wbg_adapter_14 http://localhost:8080/pkg/rust_wasm_centrality.js:204
    real http://localhost:8080/pkg/rust_wasm_centrality.js:189
rust_wasm_centrality_bg.wasm:24649:1

2. Deserialisation solution?

    let chunk_obj = JsFuture::from(stream_reader.read()).await?;
    let bytes = serde_wasm_bindgen::from_value(chunk_obj)?;

This seems tantalisingly close to working. The output in the browser console suggests that the chunk has been converted to a JS or JSON object, which seems odd to me.

Loading graph result: Error: invalid type: JsValue(Object({"done":false,"value":{"0":170,"1":4,"2":180,"3":4,"4":194,"5":6,"6":213,"7":18,"8":40,"9":19,"10":38,"11":3,"12":175,"13":2,"14":90,"15":10,"16":204,"17":1,"18":110,"19":0,"20":223,"21":31,"22":1,"23":1,"24":55,"25":2,"26":77,"27":2,"28":75,"29":2,"30":78,"31":3,"32":36,"33":2,"34":3,"35":6,"36":112,"37":27,"38":187,"39":8,"40":37,"41":0,"42":19,"43":0,"44":7,"45":0,"46":32,"47":0,"48":148,"49":0,"50":27,"51":51,"52":39,"53":0,"54":6,"55":0,"56":14,"57":0,"58":8,"59":0,"60":12,"61":0,"62":22,"63":0,"64":236,"65":0,"66":211,"67":13,"68":11,"69":0,"70":2,"71":0,"72":54,"73":1,"74":232,"75":32,"76":196,"77":54,"78":159,"79":20,"80":156,"81":0,"82":120,"83":35,"84":118,"85":2,"86":250,"87":23,"88":217,"89":27,"90":190,"91":6,"92":121,"93":2,"94":211,"95":25,"96":206,"97":9,"98":111,"99":19,"100":22,"101":40,"102":207,"103":9,"104":30,"105":58,"106":34,"107":22,"108":141,"109":40,"110":218,"111":15,"112":144,"113":10,"114":68,"115":…
localhost:8080:93:17

Other code

The contents of my Cargo.tomlmay also be useful.

name = "rust-wasm-centrality"
version = "0.1.0"
authors = ["Simon Crowe <[email protected]>"]
description = "Display a network graph and cenrality-ranked table of nodes"
license = "MIT"
repository = "https://github.com/simoncrowe/rust-wasm-centrality"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[profile.release]
lto = "thin"

[dependencies]
wasm-bindgen = "0.2.63"
wasm-bindgen-futures = "0.4.33"
js-sys = "0.3.60"
serde = { version = "1.0", features = ["derive"] }
serde_bytes = "0.11"
serde-wasm-bindgen = "0.4"


[dependencies.web-sys]
version = "0.3.60"
features = [
  'console',
  'ReadableStream',
  'ReadableStreamDefaultReader',
  'Response',
  'Window',
]


Solution

  • I realised what was wrong soon after I posted this question. The chunks that the promise returned by fetch resolve into are objects like this: {"done": false, "value":[...]}. So I just needed to do some casting and reflection using Rust's JS bindings to get the array object I needed.

    I'm leaving this question and answer up in case someone else has similar issues when getting to grips with Rust and WebAssembly.

    Specifically, I needed to cast the result of the read method on ReadableStreamDefaultReader from JsValue to Object, then access its value property and cast that to Uint8Array. Once I had the array, I could just call to_vec on it. Below is the code needed to make a GET request and convert the first chunk of the response body to Vec<u8>.

        let window = web_sys::window().unwrap();
        let resp_promise = window.fetch_with_str(&"./edges.bin");
        let resp_value = JsFuture::from(resp_promise).await?;
        let resp: Response = resp_value.dyn_into().unwrap();
        log(&format!("Response status code: {}", resp.status()));
        if resp.status() != 200 {
            return Err(JsValue::FALSE);
        }
        let reader_value = resp.body().unwrap().get_reader();
        let reader: ReadableStreamDefaultReader = reader_value.dyn_into().unwrap();
        let result_value = JsFuture::from(reader.read()).await?;
        let result: Object = result_value.dyn_into().unwrap();
        let chunk_value = js_sys::Reflect::get(&result, &JsValue::from_str("value")).unwrap();
        let chunk_array: Uint8Array = chunk_value.dyn_into().unwrap();
        let chunk = chunk_array.to_vec();