Adding in async example
This commit is contained in:
parent
5ed4c0ae05
commit
24920fd7af
|
@ -0,0 +1,5 @@
|
||||||
|
node_modules
|
||||||
|
/dist/js
|
||||||
|
/target
|
||||||
|
/wasm-pack.log
|
||||||
|
/yarn-error.log
|
|
@ -0,0 +1,41 @@
|
||||||
|
[package]
|
||||||
|
name = "async"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Async example using dominator"
|
||||||
|
authors = ["Pauan <pauanyu+github@pm.me>"]
|
||||||
|
categories = ["wasm"]
|
||||||
|
readme = "README.md"
|
||||||
|
license = "MIT"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = true
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
console_error_panic_hook = "0.1.5"
|
||||||
|
dominator = "0.5.0"
|
||||||
|
futures = "0.3.4"
|
||||||
|
futures-signals = "0.3.0"
|
||||||
|
wasm-bindgen-futures = "0.4.9"
|
||||||
|
wasm-bindgen = "0.2.48"
|
||||||
|
gloo-timers = { version = "0.2.1", features = ["futures"] }
|
||||||
|
js-sys = "0.3.36"
|
||||||
|
serde_json = "1.0.10"
|
||||||
|
serde_derive = "1.0.27"
|
||||||
|
serde = "1.0.27"
|
||||||
|
lazy_static = "1.0.0"
|
||||||
|
|
||||||
|
[dependencies.web-sys]
|
||||||
|
version = "0.3.22"
|
||||||
|
features = [
|
||||||
|
"console",
|
||||||
|
"AbortController",
|
||||||
|
"AbortSignal",
|
||||||
|
"Headers",
|
||||||
|
"Response",
|
||||||
|
"RequestInit",
|
||||||
|
"Window",
|
||||||
|
]
|
|
@ -0,0 +1,12 @@
|
||||||
|
## How to install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Builds the project and places it into the `dist` folder.
|
||||||
|
yarn run build
|
||||||
|
```
|
|
@ -0,0 +1,10 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>rust-dominator • Async</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="js/index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"author": "Pauan <pauanyu+github@pm.me>",
|
||||||
|
"name": "async",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf dist/js && rollup --config"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@wasm-tool/rollup-plugin-rust": "^1.0.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"rollup": "^1.31.0"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import rust from "@wasm-tool/rollup-plugin-rust";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
input: {
|
||||||
|
index: "./Cargo.toml",
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
dir: "dist/js",
|
||||||
|
format: "iife",
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
rust({
|
||||||
|
serverPath: "js/",
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,151 @@
|
||||||
|
use std::rc::Rc;
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use gloo_timers::future::TimeoutFuture;
|
||||||
|
use serde_derive::{Serialize, Deserialize};
|
||||||
|
use futures_signals::signal::{Mutable, not};
|
||||||
|
use dominator::{html, class, events, clone, with_node, Dom};
|
||||||
|
use web_sys::HtmlInputElement;
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use util::*;
|
||||||
|
|
||||||
|
mod util;
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
struct User {
|
||||||
|
login: String,
|
||||||
|
id: u32,
|
||||||
|
node_id: String,
|
||||||
|
avatar_url: String,
|
||||||
|
gravatar_id: String,
|
||||||
|
url: String,
|
||||||
|
html_url: String,
|
||||||
|
followers_url: String,
|
||||||
|
following_url: String,
|
||||||
|
gists_url: String,
|
||||||
|
starred_url: String,
|
||||||
|
subscriptions_url: String,
|
||||||
|
repos_url: String,
|
||||||
|
events_url: String,
|
||||||
|
received_events_url: String,
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
type_: String,
|
||||||
|
site_admin: bool,
|
||||||
|
name: Option<String>,
|
||||||
|
company: Option<String>,
|
||||||
|
blog: String,
|
||||||
|
location: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
hireable: Option<bool>,
|
||||||
|
bio: Option<String>,
|
||||||
|
public_repos: u32,
|
||||||
|
public_gists: u32,
|
||||||
|
followers: u32,
|
||||||
|
following: u32,
|
||||||
|
created_at: String,
|
||||||
|
updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
async fn fetch(user: &str) -> Result<Self, JsValue> {
|
||||||
|
let user = fetch_github(&format!("https://api.github.com/users/{}", user)).await?;
|
||||||
|
Ok(serde_json::from_str::<Self>(&user).unwrap_throw())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
user: Mutable<Option<User>>,
|
||||||
|
input: Mutable<String>,
|
||||||
|
loader: AsyncLoader,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new(name: &str, user: Option<User>) -> Rc<Self> {
|
||||||
|
Rc::new(Self {
|
||||||
|
user: Mutable::new(user),
|
||||||
|
input: Mutable::new(name.to_string()),
|
||||||
|
loader: AsyncLoader::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(app: Rc<Self>) -> Dom {
|
||||||
|
lazy_static! {
|
||||||
|
static ref APP: String = class! {
|
||||||
|
.style("white-space", "pre")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
html!("div", {
|
||||||
|
.class(&*APP)
|
||||||
|
|
||||||
|
.children(&mut [
|
||||||
|
html!("input" => HtmlInputElement, {
|
||||||
|
.property_signal("value", app.input.signal_cloned())
|
||||||
|
|
||||||
|
.with_node!(element => {
|
||||||
|
.event(clone!(app => move |_: events::Input| {
|
||||||
|
app.input.set(element.value());
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
|
||||||
|
html!("button", {
|
||||||
|
.text("Lookup user")
|
||||||
|
|
||||||
|
.event(clone!(app => move |_: events::Click| {
|
||||||
|
let input = app.input.lock_ref();
|
||||||
|
|
||||||
|
if *input == "" {
|
||||||
|
app.user.set(None);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
let input = input.to_string();
|
||||||
|
|
||||||
|
app.loader.load(clone!(app => async move {
|
||||||
|
// Simulate a slow network
|
||||||
|
TimeoutFuture::new(5_000).await;
|
||||||
|
|
||||||
|
let user = User::fetch(&input).await.ok();
|
||||||
|
app.user.set(user);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
|
||||||
|
html!("button", {
|
||||||
|
.text("Cancel")
|
||||||
|
|
||||||
|
.event(clone!(app => move |_: events::Click| {
|
||||||
|
app.loader.cancel();
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
|
||||||
|
html!("div", {
|
||||||
|
.visible_signal(app.loader.is_loading())
|
||||||
|
.text("LOADING")
|
||||||
|
}),
|
||||||
|
|
||||||
|
html!("div", {
|
||||||
|
.visible_signal(not(app.loader.is_loading()))
|
||||||
|
|
||||||
|
.text_signal(app.user.signal_ref(|user| format!("{:#?}", user)))
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[wasm_bindgen(start)]
|
||||||
|
pub async fn main_js() -> Result<(), JsValue> {
|
||||||
|
console_error_panic_hook::set_once();
|
||||||
|
|
||||||
|
let user = User::fetch("Pauan").await.ok();
|
||||||
|
|
||||||
|
let app = App::new("Pauan", user);
|
||||||
|
|
||||||
|
dominator::append_dom(&dominator::body(), App::render(app));
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
use std::future::Future;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use futures_signals::signal::{Signal, Mutable};
|
||||||
|
use wasm_bindgen_futures::{JsFuture, spawn_local};
|
||||||
|
use futures::future::{abortable, AbortHandle};
|
||||||
|
use js_sys::Error;
|
||||||
|
use web_sys::{window, Response, RequestInit, Headers, AbortController, AbortSignal};
|
||||||
|
|
||||||
|
|
||||||
|
struct AsyncState {
|
||||||
|
id: usize,
|
||||||
|
handle: AbortHandle,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncState {
|
||||||
|
fn new(handle: AbortHandle) -> Self {
|
||||||
|
static ID: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
|
||||||
|
let id = ID.fetch_add(1, Ordering::SeqCst);
|
||||||
|
|
||||||
|
Self { id, handle }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AsyncLoader {
|
||||||
|
loading: Mutable<Option<AsyncState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncLoader {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
loading: Mutable::new(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cancel(&self) {
|
||||||
|
self.replace(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replace(&self, value: Option<AsyncState>) {
|
||||||
|
let mut loading = self.loading.lock_mut();
|
||||||
|
|
||||||
|
if let Some(state) = loading.as_mut() {
|
||||||
|
state.handle.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
*loading = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load<F>(&self, fut: F) where F: Future<Output = ()> + 'static {
|
||||||
|
let (fut, handle) = abortable(fut);
|
||||||
|
|
||||||
|
let state = AsyncState::new(handle);
|
||||||
|
let id = state.id;
|
||||||
|
|
||||||
|
self.replace(Some(state));
|
||||||
|
|
||||||
|
let loading = self.loading.clone();
|
||||||
|
|
||||||
|
spawn_local(async move {
|
||||||
|
match fut.await {
|
||||||
|
Ok(()) => {
|
||||||
|
let mut loading = loading.lock_mut();
|
||||||
|
|
||||||
|
if let Some(current_id) = loading.as_ref().map(|x| x.id) {
|
||||||
|
// If it hasn't been overwritten with a new state...
|
||||||
|
if current_id == id {
|
||||||
|
*loading = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// It was already cancelled
|
||||||
|
Err(_) => {},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_loading(&self) -> impl Signal<Item = bool> {
|
||||||
|
self.loading.signal_ref(|x| x.is_some())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct Abort {
|
||||||
|
controller: AbortController,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Abort {
|
||||||
|
fn new() -> Result<Self, JsValue> {
|
||||||
|
Ok(Self {
|
||||||
|
controller: AbortController::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signal(&self) -> AbortSignal {
|
||||||
|
self.controller.signal()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Abort {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.controller.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_github(url: &str) -> Result<String, JsValue> {
|
||||||
|
let abort = Abort::new()?;
|
||||||
|
|
||||||
|
let headers = Headers::new()?;
|
||||||
|
headers.set("Accept", "application/vnd.github.v3+json")?;
|
||||||
|
|
||||||
|
let future = window()
|
||||||
|
.unwrap_throw()
|
||||||
|
.fetch_with_str_and_init(
|
||||||
|
url,
|
||||||
|
RequestInit::new()
|
||||||
|
.headers(&headers)
|
||||||
|
.signal(Some(&abort.signal())),
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = JsFuture::from(future)
|
||||||
|
.await?
|
||||||
|
.unchecked_into::<Response>();
|
||||||
|
|
||||||
|
if !response.ok() {
|
||||||
|
return Err(Error::new("Fetch failed").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = JsFuture::from(response.text()?)
|
||||||
|
.await?
|
||||||
|
.as_string()
|
||||||
|
.unwrap_throw();
|
||||||
|
|
||||||
|
Ok(value)
|
||||||
|
}
|
Loading…
Reference in New Issue