A stateful agent (#1247)

* first sketch of a stateful agent

* share the data with listeners only immutably

* rename the stateful agent to Store

* moved stuff around, fixed missing parts

* removed a useless directory

* fmt fixes

* added missing comments
This commit is contained in:
Michal Kawalec 2020-05-21 07:21:41 -07:00 committed by GitHub
parent 6583b38c17
commit 9130f814af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 363 additions and 1 deletions

View File

@ -49,6 +49,7 @@ members = [
"examples/node_refs",
"examples/npm_and_rest",
"examples/pub_sub",
"examples/store",
"examples/textarea",
"examples/timer",
"examples/todomvc",

22
examples/store/Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "store"
version = "0.1.0"
authors = ["Michał Kawalec <michal@monad.cat>"]
edition = "2018"
[dependencies]
wasm-bindgen = { version = "^0.2", features = ["serde-serialize"] }
wasm-bindgen-futures = "0.4"
yew = { path = "../../yew" }
yewtil = { path = "../../yewtil" }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["raw_value"] }
[dependencies.web-sys]
version = "0.3"
features = [
"Window",
"Navigator",
"MediaDevices",
"MediaStreamConstraints"
]

View File

@ -0,0 +1,109 @@
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{spawn_local, JsFuture};
use web_sys::{console, window, MediaDevices, MediaStreamConstraints};
use yew::agent::AgentLink;
use yewtil::store::{Store, StoreWrapper};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum Request {
GetStream,
GetDevices,
}
#[derive(Debug)]
pub enum Action {
SetStream(JsValue),
SetStreamError(JsValue),
SetDevices(Vec<InputDeviceInfo>),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DeviceId(pub String);
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InputDeviceInfo {
device_id: DeviceId,
group_id: String,
kind: String,
label: String,
}
pub struct MediaManager {
pub known_devices: Vec<InputDeviceInfo>,
pub media_stream: Option<JsValue>,
pub media_devices: MediaDevices,
pub set_stream_error: Option<JsValue>,
}
impl Store for MediaManager {
type Action = Action;
type Input = Request;
fn new() -> Self {
let window = window().unwrap();
let navigator = window.navigator();
let media_devices = navigator.media_devices().unwrap();
MediaManager {
known_devices: Vec::new(),
media_stream: None,
media_devices,
set_stream_error: None,
}
}
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input) {
match msg {
Request::GetStream => {
console::log_1(&"Continuing handling getstream".into());
let mut media_constraints = MediaStreamConstraints::new();
media_constraints
.audio(&JsValue::TRUE)
.video(&JsValue::TRUE);
let media_promise = MediaDevices::get_user_media_with_constraints(
&self.media_devices,
&media_constraints,
)
.unwrap();
spawn_local(async move {
match JsFuture::from(media_promise).await {
Ok(media) => link.send_message(Action::SetStream(media)),
Err(e) => link.send_message(Action::SetStreamError(e)),
}
});
}
Request::GetDevices => {
let devices_promise = MediaDevices::enumerate_devices(&self.media_devices).unwrap();
spawn_local(async move {
let devices = JsFuture::from(devices_promise)
.await
.unwrap()
.into_serde::<Vec<InputDeviceInfo>>()
.unwrap();
link.send_message(Action::SetDevices(devices));
});
}
}
}
fn reduce(&mut self, msg: Self::Action) {
match msg {
Action::SetStream(stream) => {
self.media_stream = Some(stream);
}
Action::SetStreamError(error) => {
self.set_stream_error = Some(error);
}
Action::SetDevices(devices) => {
self.known_devices = devices;
}
}
}
}

View File

@ -0,0 +1 @@
pub mod media_manager;

69
examples/store/src/lib.rs Normal file
View File

@ -0,0 +1,69 @@
mod agents;
use yew::prelude::*;
//use yew::agent::{Bridgeable, Dispatcher, Dispatched, ReadOnly, StoreWrapper};
use web_sys::console;
use yewtil::store::{Bridgeable, ReadOnly, StoreWrapper};
use crate::agents::media_manager::{MediaManager, Request};
pub struct App {
link: ComponentLink<Self>,
media_manager: Box<dyn Bridge<StoreWrapper<MediaManager>>>,
}
pub enum Msg {
GetStream,
GetDevices,
MediaManagerMsg(ReadOnly<MediaManager>),
}
impl Component for App {
type Message = Msg;
type Properties = ();
fn create(_: Self::Properties, link: ComponentLink<Self>) -> Self {
let callback = link.callback(Msg::MediaManagerMsg);
let media_manager = MediaManager::bridge(callback);
Self {
link,
media_manager,
}
}
fn update(&mut self, msg: Self::Message) -> ShouldRender {
match msg {
Msg::GetStream => {
self.media_manager.send(Request::GetStream);
console::log_1(&"after send".into());
}
Msg::GetDevices => self.media_manager.send(Request::GetDevices),
Msg::MediaManagerMsg(state) => {
if let Some(stream) = &state.borrow().media_stream {
console::log_2(&"We have a stream".into(), &stream);
}
console::log_1(&"Received update".into());
}
}
false
}
fn change(&mut self, _props: Self::Properties) -> ShouldRender {
false
}
fn view(&self) -> Html {
html! {
<div>
<button onclick=self.link.callback(|_| Msg::GetStream)>
{ "get stream" }
</button>
<button onclick=self.link.callback(|_| Msg::GetDevices)>
{ "get devices" }
</button>
</div>
}
}
}

View File

@ -0,0 +1,3 @@
fn main() {
yew::start_app::<store::App>();
}

View File

@ -14,7 +14,7 @@ all = ["stable", "experimental"]
# Broad features
## All features MUST be stable or experimental
stable = ["neq", "pure", "history", "mrc_irc", "effect", "future"]
stable = ["neq", "pure", "history", "mrc_irc", "effect", "future", "store"]
experimental = ["dsl", "lrc", "with_callback", "fetch"]
# Some pointers are stable, some experimental.
@ -30,6 +30,7 @@ dsl = []
effect = []
fetch = ["serde", "serde_json", "neq", "future", "web-sys"]
future = ["wasm-bindgen-futures", "wasm-bindgen", "futures"]
store = []
# Ptr features
lrc = []

View File

@ -9,6 +9,7 @@
//! * "mrc_irc" - Ergonomic Rc pointers.
//! * "lrc" - Linked-list Rc pointer.
//! * "history" - History tracker
//! * "store" - Global state with easy binding
// //! * "dsl" - Use functions instead of Yew's `html!` macro.
//#[cfg(feature = "dsl")]
@ -48,3 +49,6 @@ pub use effect::{effect, Effect};
#[cfg(feature = "future")]
pub mod future;
#[cfg(feature = "store")]
pub mod store;

152
yewtil/src/store.rs Normal file
View File

@ -0,0 +1,152 @@
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::rc::Rc;
use yew::agent::{Agent, AgentLink, Context, Discoverer, Dispatcher, HandlerId};
use yew::prelude::*;
/// A functional state wrapper, enforcing a unidirectional
/// data flow and consistent state to the observers.
///
/// `handle_input` receives incoming messages from components,
/// `reduce` applies changes to the state
///
/// Once created with a first bridge, a Store will never be destroyed
/// for the lifetime of the application.
pub trait Store: Sized + 'static {
/// Messages instructing the store to do somethin
type Input;
/// State updates to be consumed by `reduce`
type Action;
/// Create a new Store
fn new() -> Self;
/// Receives messages from components and other agents. Use the `link`
/// to send actions to itself in order to notify `reduce` once your
/// operation completes. This is the place to do side effects, like
/// talking to the server, or asking the user for input.
///
/// Note that you can look at the state of your Store, but you
/// cannot modify it here. If you want to modify it, send a Message
/// to the reducer
fn handle_input(&self, link: AgentLink<StoreWrapper<Self>>, msg: Self::Input);
/// A pure function, with no side effects. Receives a message,
/// and applies it to the state as it sees fit.
fn reduce(&mut self, msg: Self::Action);
}
/// Hides the full context Agent from a Store and does
/// the boring data wrangling logic
#[derive(Debug)]
pub struct StoreWrapper<S: Store> {
/// Currently subscibed components and agents
pub handlers: HashSet<HandlerId>,
/// Link to itself so Store::handle_input can send actions to reducer
pub link: AgentLink<Self>,
/// The actual Store
pub state: Shared<S>,
/// A circular dispatcher to itself so the store is not removed
pub self_dispatcher: Dispatcher<Self>,
}
type Shared<T> = Rc<RefCell<T>>;
/// A wrapper ensuring state observers can only
/// borrow the state immutably
#[derive(Debug)]
pub struct ReadOnly<S> {
state: Shared<S>,
}
impl<S> ReadOnly<S> {
/// Allow only immutable borrows to the underlying data
pub fn borrow<'a>(&'a self) -> impl Deref<Target = S> + 'a {
self.state.borrow()
}
}
/// This is a wrapper, intended to be used as an opaque
/// machinery allowing the Store to do it's things.
impl<S: Store> Agent for StoreWrapper<S> {
type Reach = Context<Self>;
type Message = S::Action;
type Input = S::Input;
type Output = ReadOnly<S>;
fn create(link: AgentLink<Self>) -> Self {
let state = Rc::new(RefCell::new(S::new()));
let handlers = HashSet::new();
// Link to self to never go out of scope
let self_dispatcher = Self::dispatcher();
StoreWrapper {
handlers,
state,
link,
self_dispatcher,
}
}
fn update(&mut self, msg: Self::Message) {
{
self.state.borrow_mut().reduce(msg);
}
for handler in self.handlers.iter() {
self.link.respond(
*handler,
ReadOnly {
state: self.state.clone(),
},
);
}
}
fn handle_input(&mut self, msg: Self::Input, _id: HandlerId) {
self.state.borrow().handle_input(self.link.clone(), msg);
}
fn connected(&mut self, id: HandlerId) {
self.handlers.insert(id);
}
fn disconnected(&mut self, id: HandlerId) {
self.handlers.remove(&id);
}
}
// This instance is quite unfortunate, as the Rust compiler
// does not support mutually exclusive trait bounds (https://github.com/rust-lang/rust/issues/51774),
// we have to create a new trait with the same function as in the original one.
/// Allows us to communicate with a store
pub trait Bridgeable: Sized + 'static {
/// A wrapper for the store we want to bridge to,
/// which serves as a communication intermediary
type Wrapper: Agent;
/// Creates a messaging bridge between a worker and the component.
fn bridge(
callback: Callback<<Self::Wrapper as Agent>::Output>,
) -> Box<dyn Bridge<Self::Wrapper>>;
}
/// Implementation of bridge creation
impl<T> Bridgeable for T
where
T: Store,
{
/// The hiding wrapper
type Wrapper = StoreWrapper<T>;
fn bridge(
callback: Callback<<Self::Wrapper as Agent>::Output>,
) -> Box<dyn Bridge<Self::Wrapper>> {
<Self::Wrapper as Agent>::Reach::spawn_or_join(Some(callback))
}
}