1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
use crate::{Agent, AgentLink, Bridge, Context, Discoverer, Dispatched, Dispatcher, HandlerId};
use std::cell::RefCell;
use std::collections::HashSet;
use std::ops::Deref;
use std::rc::Rc;
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
///
/// The state is sent once whenever a bridge is opened and then once
/// for each `Action` sent by the `handle_input` function. This means
/// the initial state of the store must be valid for the consumers.
///
/// 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 subscribed 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(&self) -> impl Deref<Target = S> + '_ {
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,
link,
state,
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 connected(&mut self, id: HandlerId) {
self.handlers.insert(id);
self.link.respond(
id,
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 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))
}
}
