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/node_refs",
|
||||||
"examples/npm_and_rest",
|
"examples/npm_and_rest",
|
||||||
"examples/pub_sub",
|
"examples/pub_sub",
|
||||||
|
"examples/store",
|
||||||
"examples/textarea",
|
"examples/textarea",
|
||||||
"examples/timer",
|
"examples/timer",
|
||||||
"examples/todomvc",
|
"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
|
# Broad features
|
||||||
## All features MUST be stable or experimental
|
## 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"]
|
experimental = ["dsl", "lrc", "with_callback", "fetch"]
|
||||||
|
|
||||||
# Some pointers are stable, some experimental.
|
# Some pointers are stable, some experimental.
|
||||||
@ -30,6 +30,7 @@ dsl = []
|
|||||||
effect = []
|
effect = []
|
||||||
fetch = ["serde", "serde_json", "neq", "future", "web-sys"]
|
fetch = ["serde", "serde_json", "neq", "future", "web-sys"]
|
||||||
future = ["wasm-bindgen-futures", "wasm-bindgen", "futures"]
|
future = ["wasm-bindgen-futures", "wasm-bindgen", "futures"]
|
||||||
|
store = []
|
||||||
|
|
||||||
# Ptr features
|
# Ptr features
|
||||||
lrc = []
|
lrc = []
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
//! * "mrc_irc" - Ergonomic Rc pointers.
|
//! * "mrc_irc" - Ergonomic Rc pointers.
|
||||||
//! * "lrc" - Linked-list Rc pointer.
|
//! * "lrc" - Linked-list Rc pointer.
|
||||||
//! * "history" - History tracker
|
//! * "history" - History tracker
|
||||||
|
//! * "store" - Global state with easy binding
|
||||||
// //! * "dsl" - Use functions instead of Yew's `html!` macro.
|
// //! * "dsl" - Use functions instead of Yew's `html!` macro.
|
||||||
|
|
||||||
//#[cfg(feature = "dsl")]
|
//#[cfg(feature = "dsl")]
|
||||||
@ -48,3 +49,6 @@ pub use effect::{effect, Effect};
|
|||||||
|
|
||||||
#[cfg(feature = "future")]
|
#[cfg(feature = "future")]
|
||||||
pub mod 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