mirror of
https://github.com/yewstack/yew.git
synced 2025-12-08 21:26:25 +00:00
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:
parent
6583b38c17
commit
9130f814af
@ -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
22
examples/store/Cargo.toml
Normal 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"
|
||||
]
|
||||
109
examples/store/src/agents/media_manager.rs
Normal file
109
examples/store/src/agents/media_manager.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
examples/store/src/agents/mod.rs
Normal file
1
examples/store/src/agents/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod media_manager;
|
||||
69
examples/store/src/lib.rs
Normal file
69
examples/store/src/lib.rs
Normal 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>
|
||||
}
|
||||
}
|
||||
}
|
||||
3
examples/store/src/main.rs
Normal file
3
examples/store/src/main.rs
Normal file
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
yew::start_app::<store::App>();
|
||||
}
|
||||
@ -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 = []
|
||||
|
||||
@ -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
152
yewtil/src/store.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user