From 39d00791bc7b47cad454937345c562908a639cff Mon Sep 17 00:00:00 2001 From: Henry Zimmerman Date: Wed, 25 Sep 2019 21:42:36 -0400 Subject: [PATCH] Add Dispatchers (#639) * Add Dispatchers, which act like bridges, but don't require a callback to create; updated router example * cargo fmt * improve comment * Another approach * add newtype around dispatcher bridges * added debug impl, run cargo fmt * fix example * make button on routing example start on loading variant * revert singleton_id changes * actually revert singleton_id changes * slabs own option * cargo fmt * remove dead lines * address bad doc comment * fix router example * fix handler id initialization in local agent * add appropriate error message when id is not associated with callback * remove misleading comment * use a type alias to the shared output slab --- examples/routing/src/b_component.rs | 2 +- examples/routing/src/lib.rs | 49 +++---- examples/routing/src/router_button.rs | 81 ++++++++++++ src/agent.rs | 184 ++++++++++++++++++-------- 4 files changed, 225 insertions(+), 91 deletions(-) create mode 100644 examples/routing/src/router_button.rs diff --git a/examples/routing/src/b_component.rs b/examples/routing/src/b_component.rs index 7a429f2f1fb..a2d3eee6334 100644 --- a/examples/routing/src/b_component.rs +++ b/examples/routing/src/b_component.rs @@ -6,7 +6,7 @@ use yew::{html, Bridge, Component, ComponentLink, Html, Renderable, ShouldRender pub struct BModel { number: Option, sub_path: Option, - router: Box>>, + router: Box>>, } pub enum Msg { diff --git a/examples/routing/src/lib.rs b/examples/routing/src/lib.rs index 67a13e38d35..e1b75199e25 100644 --- a/examples/routing/src/lib.rs +++ b/examples/routing/src/lib.rs @@ -2,9 +2,11 @@ mod b_component; mod router; +mod router_button; mod routing; use b_component::BModel; +use crate::router_button::RouterButton; use log::info; use router::Route; use yew::agent::Bridged; @@ -14,15 +16,15 @@ pub enum Child { A, B, PathNotFound(String), + Loading, } pub struct Model { child: Child, - router: Box>>, + router: Box>>, } pub enum Msg { - NavigateTo(Child), HandleRoute(Route<()>), } @@ -32,40 +34,20 @@ impl Component for Model { fn create(_: Self::Properties, mut link: ComponentLink) -> Self { let callback = link.send_back(|route: Route<()>| Msg::HandleRoute(route)); - let mut router = router::Router::bridge(callback); - - // TODO Not sure if this is technically correct. This should be sent _after_ the component has been created. - // I think the `Component` trait should have a hook called `on_mount()` - // that is called after the component has been attached to the vdom. - // It seems like this only works because the JS engine decides to activate the - // router worker logic after the mounting has finished. - router.send(router::Request::GetCurrentRoute); - + let router = router::Router::bridge(callback); Model { - child: Child::A, // This should be quickly overwritten by the actual route. + child: Child::Loading, // This should be quickly overwritten by the actual route. router, } } + fn mounted(&mut self) -> ShouldRender { + self.router.send(router::Request::GetCurrentRoute); + false + } + fn update(&mut self, msg: Self::Message) -> ShouldRender { match msg { - Msg::NavigateTo(child) => { - let path_segments = match child { - Child::A => vec!["a".into()], - Child::B => vec!["b".into()], - Child::PathNotFound(_) => vec!["path_not_found".into()], - }; - - let route = router::Route { - path_segments, - query: None, - fragment: None, - state: (), - }; - - self.router.send(router::Request::ChangeRoute(route)); - false - } Msg::HandleRoute(route) => { info!("Routing: {}", route.to_route_string()); // Instead of each component selecting which parts of the path are important to it, @@ -92,8 +74,8 @@ impl Renderable for Model { html! {
{self.child.view()} @@ -105,7 +87,7 @@ impl Renderable for Model { impl Renderable for Child { fn view(&self) -> Html { - match *self { + match self { Child::A => html! { <> {"This corresponds to route 'a'"} @@ -122,6 +104,9 @@ impl Renderable for Child { {format!("Invalid path: '{}'", path)} }, + Child::Loading => html! { + {"Loading"} + }, } } } diff --git a/examples/routing/src/router_button.rs b/examples/routing/src/router_button.rs new file mode 100644 index 00000000000..efb42807c4f --- /dev/null +++ b/examples/routing/src/router_button.rs @@ -0,0 +1,81 @@ +//! A component wrapping a + } + } +} diff --git a/src/agent.rs b/src/agent.rs index 583d0ee55a9..487448072ed 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -10,6 +10,7 @@ use slab::Slab; use std::cell::RefCell; use std::fmt; use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; use stdweb::Value; #[allow(unused_imports)] @@ -56,28 +57,75 @@ impl Packed for T { } } +/// Type alias to a sharable Slab that owns optional callbacks that emit messages of the type of the specified Agent. +type SharedOutputSlab = Shared::Output>>>>; + /// Id of responses handler. #[derive(Serialize, Deserialize, Eq, PartialEq, Hash, Clone, Copy)] -pub struct HandlerId(usize); - -impl From for HandlerId { - fn from(id: usize) -> Self { - HandlerId(id) - } -} +pub struct HandlerId(usize, bool); impl HandlerId { + fn new(id: usize, respondable: bool) -> Self { + HandlerId(id, respondable) + } fn raw_id(self) -> usize { self.0 } + /// Indicates if a handler id corresponds to callback in the Agent runtime. + pub fn is_respondable(&self) -> bool { + self.1 + } } -/// This traits allow to get addres or register worker. +/// This trait allows registering or getting the address of a worker. pub trait Bridged: Agent + Sized + 'static { /// Creates a messaging bridge between a worker and the component. fn bridge(callback: Callback) -> Box>; } +/// This trait allows the creation of a dispatcher to an existing agent that will not send replies when messages are sent. +pub trait Dispatched: Agent + Sized + 'static { + /// Creates a dispatcher to the agent that will not send messages back. + /// + /// # Note + /// Dispatchers don't have `HandlerId`s and therefore `Agent::handle` will be supplied `None` + /// for the `id` parameter, and `connected` and `disconnected` will not be called. + /// + /// # Important + /// Because the Agents using Context or Public reaches use the number of existing bridges to + /// keep track of if the agent itself should exist, creating dispatchers will not guarantee that + /// an Agent will exist to service requests sent from Dispatchers. You **must** keep at least one + /// bridge around if you wish to use a dispatcher. If you are using agents in a write-only manner, + /// then it is suggested that you create a bridge that handles no-op responses as high up in the + /// component hierarchy as possible - oftentimes the root component for simplicity's sake. + fn dispatcher() -> Dispatcher; +} + +/// A newtype around a bridge to indicate that it is distinct from a normal bridge +pub struct Dispatcher(Box>); + +impl fmt::Debug for Dispatcher { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Dispatcher<_>") + } +} + +impl Deref for Dispatcher { + type Target = dyn Bridge; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} +impl DerefMut for Dispatcher { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +/// Marker trait to indicate which Discoverers are able to be used with dispatchers. +pub trait Dispatchable: Discoverer {} + /// Implements rules to register a worker in a separate thread. pub trait Threaded { /// Executes an agent in the current environment. @@ -137,7 +185,17 @@ where T: Agent, { fn bridge(callback: Callback) -> Box> { - Self::Reach::spawn_or_join(callback) + Self::Reach::spawn_or_join(Some(callback)) + } +} + +impl Dispatched for T +where + T: Agent, + ::Reach: Dispatchable, +{ + fn dispatcher() -> Dispatcher { + Dispatcher(Self::Reach::spawn_or_join::(None)) } } @@ -145,7 +203,7 @@ where #[doc(hidden)] pub trait Discoverer { /// Spawns an agent and returns `Bridge` implementation. - fn spawn_or_join(_callback: Callback) -> Box> { + fn spawn_or_join(_callback: Option>) -> Box> { unimplemented!(); } } @@ -160,7 +218,7 @@ pub trait Bridge { struct LocalAgent { scope: AgentScope, - slab: Shared>>, + slab: SharedOutputSlab, } type Last = bool; @@ -174,15 +232,17 @@ impl LocalAgent { } } - fn slab(&self) -> Shared>> { + fn slab(&self) -> SharedOutputSlab { self.slab.clone() } - fn create_bridge(&mut self, callback: Callback) -> ContextBridge { - let id = self.slab.borrow_mut().insert(callback); + fn create_bridge(&mut self, callback: Option>) -> ContextBridge { + let respondable = callback.is_some(); + let id: usize = self.slab.borrow_mut().insert(callback); + let id = HandlerId::new(id, respondable); ContextBridge { scope: self.scope.clone(), - id: id.into(), + id, } } @@ -201,7 +261,7 @@ thread_local! { pub struct Context; impl Discoverer for Context { - fn spawn_or_join(callback: Callback) -> Box> { + fn spawn_or_join(callback: Option>) -> Box> { let mut scope_to_init = None; let bridge = LOCAL_AGENTS_POOL.with(|pool| { match pool.borrow_mut().entry::>() { @@ -231,18 +291,29 @@ impl Discoverer for Context { } } +impl Dispatchable for Context {} + struct SlabResponder { - slab: Shared>>, + slab: Shared>>>, } impl Responder for SlabResponder { fn response(&self, id: HandlerId, output: AGN::Output) { - let callback = self.slab.borrow().get(id.raw_id()).cloned(); - if let Some(callback) = callback { - callback.emit(output); - } else { - warn!("Id of handler not exists : {}", id.raw_id()); - } + locate_callback_and_respond::(&self.slab, id, output); + } +} + +/// The slab contains the callback, the id is used to look up the callback, +/// and the output is the message that will be sent via the callback. +fn locate_callback_and_respond( + slab: &SharedOutputSlab, + id: HandlerId, + output: AGN::Output, +) { + match slab.borrow().get(id.raw_id()).cloned() { + Some(Some(callback)) => callback.emit(output), + Some(None) => warn!("The Id of the handler: {}, while present in the slab, is not associated with a callback.", id.raw_id()), + None => warn!("Id of handler does not exist in the slab: {}.", id.raw_id()), } } @@ -268,8 +339,10 @@ impl Drop for ContextBridge { false } }; + let upd = AgentUpdate::Disconnected(self.id); self.scope.send(upd); + if terminate_worker { let upd = AgentUpdate::Destroy; self.scope.send(upd); @@ -283,7 +356,8 @@ impl Drop for ContextBridge { pub struct Job; impl Discoverer for Job { - fn spawn_or_join(callback: Callback) -> Box> { + fn spawn_or_join(callback: Option>) -> Box> { + let callback = callback.expect("Callback required for Job"); let scope = AgentScope::::new(); let responder = CallbackResponder { callback }; let agent_link = AgentLink::connect(&scope, responder); @@ -296,7 +370,7 @@ impl Discoverer for Job { } } -const SINGLETON_ID: HandlerId = HandlerId(0); +const SINGLETON_ID: HandlerId = HandlerId(0, true); struct CallbackResponder { callback: Callback, @@ -335,7 +409,8 @@ impl Drop for JobBridge { pub struct Private; impl Discoverer for Private { - fn spawn_or_join(callback: Callback) -> Box> { + fn spawn_or_join(callback: Option>) -> Box> { + let callback = callback.expect("Callback required for Private agents"); let handler = move |data: Vec| { let msg = FromWorker::::unpack(&data); match msg { @@ -395,22 +470,24 @@ impl Drop for PrivateBridge { struct RemoteAgent { worker: Value, - slab: Shared>>, + slab: SharedOutputSlab, } impl RemoteAgent { - pub fn new(worker: &Value, slab: Shared>>) -> Self { + pub fn new(worker: &Value, slab: SharedOutputSlab) -> Self { RemoteAgent { worker: worker.clone(), slab, } } - fn create_bridge(&mut self, callback: Callback) -> PublicBridge { - let id = self.slab.borrow_mut().insert(callback); + fn create_bridge(&mut self, callback: Option>) -> PublicBridge { + let respondable = callback.is_some(); + let id: usize = self.slab.borrow_mut().insert(callback); + let id = HandlerId::new(id, respondable); PublicBridge { worker: self.worker.clone(), - id: id.into(), + id, _agent: PhantomData, } } @@ -430,7 +507,7 @@ thread_local! { pub struct Public; impl Discoverer for Public { - fn spawn_or_join(callback: Callback) -> Box> { + fn spawn_or_join(callback: Option>) -> Box> { let bridge = REMOTE_AGENTS_POOL.with(|pool| { match pool.borrow_mut().entry::>() { Entry::Occupied(mut entry) => { @@ -438,7 +515,7 @@ impl Discoverer for Public { entry.get_mut().create_bridge(callback) } Entry::Vacant(entry) => { - let slab_base: Shared>> = + let slab_base: Shared>>> = Rc::new(RefCell::new(Slab::new())); let slab = slab_base.clone(); let handler = move |data: Vec| { @@ -449,15 +526,7 @@ impl Discoverer for Public { // TODO Send `Connected` message } FromWorker::ProcessOutput(id, output) => { - let callback = slab.borrow().get(id.raw_id()).cloned(); - if let Some(callback) = callback { - callback.emit(output); - } else { - warn!( - "Id of handler for remote worker not exists : {}", - id.raw_id() - ); - } + locate_callback_and_respond::(&slab, id, output); } } }; @@ -479,6 +548,8 @@ impl Discoverer for Public { } } +impl Dispatchable for Public {} + /// A connection manager for components interaction with workers. pub struct PublicBridge { worker: Value, @@ -486,25 +557,22 @@ pub struct PublicBridge { _agent: PhantomData, } -impl PublicBridge { - fn send_to_remote(&self, msg: ToWorker) { - // TODO Important! Implement. - // Use a queue to collect a messages if an instance is not ready - // and send them to an agent when it will reported readiness. - let msg = msg.pack(); - let worker = &self.worker; - js! { - var worker = @{worker}; - var bytes = @{msg}; - worker.postMessage(bytes); - }; - } +fn send_to_remote(worker: &Value, msg: ToWorker) { + // TODO Important! Implement. + // Use a queue to collect a messages if an instance is not ready + // and send them to an agent when it will reported readiness. + let msg = msg.pack(); + js! { + var worker = @{worker}; + var bytes = @{msg}; + worker.postMessage(bytes); + }; } impl Bridge for PublicBridge { fn send(&mut self, msg: AGN::Input) { let msg = ToWorker::ProcessInput(self.id, msg); - self.send_to_remote(msg); + send_to_remote::(&self.worker, msg); } } @@ -519,10 +587,10 @@ impl Drop for PublicBridge { } }; let upd = ToWorker::Disconnected(self.id); - self.send_to_remote(upd); + send_to_remote::(&self.worker, upd); if terminate_worker { let upd = ToWorker::Destroy; - self.send_to_remote(upd); + send_to_remote::(&self.worker, upd); pool.borrow_mut().remove::>(); } });