-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
copy redux state mgmt from softw-eng
- Loading branch information
Showing
5 changed files
with
181 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
use models::TemperatureMeasurement; | ||
|
||
use super::State; | ||
|
||
#[derive(Debug)] | ||
pub enum Action { | ||
Overwrite(State), | ||
Insert(TemperatureMeasurement), | ||
Clear, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
//! In the redux pattern, reducers must be pure functions. This means that they | ||
//! cannot perform side effects. This has several benefits, but it means that we | ||
//! need to do some extra work to perform side effects. This is where middleware | ||
//! comes in. Instead of dispatching an action directly, we process it by a | ||
//! middleware first. The middleware can then perform side effects and dispatch | ||
//! a new action. This new action is then passed to the reducer. Pre- and | ||
//! post-middleware actions have different types, so there is no risk of | ||
//! forgetting to run a middleware outside of the store module. | ||
use gloo::net::http::Request; | ||
use models::TemperatureMeasurement; | ||
|
||
use super::{action::Action, State}; | ||
|
||
pub enum PreMiddlewareAction { | ||
Reload, | ||
InsertRandom, | ||
DeleteAll, | ||
} | ||
|
||
pub async fn process(action: PreMiddlewareAction) -> Action { | ||
match action { | ||
PreMiddlewareAction::Reload => { | ||
let state = Request::get("/api/measurements") | ||
.send() | ||
.await | ||
.unwrap() | ||
.json::<State>() | ||
.await | ||
.unwrap(); | ||
Action::Overwrite(state) | ||
} | ||
PreMiddlewareAction::InsertRandom => { | ||
let measurement = Request::post("/api/measurements/random") | ||
.send() | ||
.await | ||
.unwrap() | ||
.json::<TemperatureMeasurement>() | ||
.await | ||
.unwrap(); | ||
Action::Insert(measurement) | ||
} | ||
PreMiddlewareAction::DeleteAll => { | ||
Request::delete("/api/measurements").send().await.unwrap(); | ||
Action::Clear | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
use models::MeasurementList; | ||
|
||
mod action; | ||
mod middleware; | ||
mod reducer; | ||
mod store; | ||
|
||
use self::middleware::PreMiddlewareAction; | ||
pub use self::store::{provide_store, use_store}; | ||
|
||
/// The type of data managed by the store. | ||
type State = MeasurementList; | ||
|
||
// Users of the store only need to know about the pre-middleware actions. | ||
// The fact that middleware is run is an implementation detail. | ||
pub type Action = PreMiddlewareAction; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
use super::{action::Action, State}; | ||
|
||
pub fn reduce(state: &State, action: Action) -> State { | ||
match action { | ||
Action::Overwrite(state) => state, | ||
Action::Insert(measurement) => { | ||
let mut new_state = state.clone(); | ||
new_state.insert(measurement); | ||
new_state | ||
} | ||
Action::Clear => State::default(), | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
use leptos::*; | ||
use leptos_use::{use_websocket, UseWebsocketReturn}; | ||
use models::Notification; | ||
|
||
use super::{ | ||
action::Action, | ||
middleware::{self, PreMiddlewareAction}, | ||
reducer::reduce, | ||
State, | ||
}; | ||
|
||
#[derive(Debug, Clone, Copy)] | ||
pub struct Store { | ||
state: RwSignal<State>, | ||
} | ||
|
||
/// Basically a wrapper around [leptos::RwSignal]. The only difference is that | ||
/// updates have to go through the `dispatch` method. | ||
impl Store { | ||
fn new() -> Self { | ||
let store = Self { | ||
state: RwSignal::new(Default::default()), | ||
}; | ||
// Load initial state | ||
store.dispatch(PreMiddlewareAction::Reload); | ||
|
||
let UseWebsocketReturn { message, .. } = use_websocket("/api/notifications"); | ||
create_effect(move |_| { | ||
if let Some(message) = message.get() { | ||
let notification = serde_json::from_str::<Notification>(&message).unwrap(); | ||
let action = match notification { | ||
Notification::New(measurements) => Action::Insert(measurements), | ||
Notification::Cleared => Action::Clear, | ||
}; | ||
store.dispatch_without_middleware(action); | ||
} | ||
}); | ||
|
||
store | ||
} | ||
|
||
fn dispatch_without_middleware(&self, action: Action) { | ||
let new_state = reduce(&self.state.get_untracked(), action); | ||
if new_state != self.state.get_untracked() { | ||
self.state.set(new_state); | ||
} | ||
} | ||
|
||
pub fn dispatch(self, action: PreMiddlewareAction) { | ||
spawn_local(async move { | ||
let action = middleware::process(action).await; | ||
self.dispatch_without_middleware(action); | ||
}); | ||
} | ||
} | ||
|
||
pub fn provide_store() { | ||
provide_context(Store::new()); | ||
} | ||
|
||
pub fn use_store() -> Store { | ||
use_context().expect("should find store context") | ||
} | ||
|
||
/// These impls are a little gnarly looking. | ||
/// They are not necessary, but they make the store callable. | ||
/// Essentially, it turns the [Store] into a custom closure type. | ||
/// This is what leptos does for its own signal types, so I did it here too. | ||
/// This is a nightly feature, activated by `#![feature(fn_traits)]`. | ||
mod impl_fn_for_store { | ||
use leptos::SignalGet; | ||
|
||
use super::{State, Store}; | ||
|
||
impl FnOnce<()> for Store { | ||
type Output = State; | ||
|
||
extern "rust-call" fn call_once(self, _: ()) -> Self::Output { | ||
self.state.get() | ||
} | ||
} | ||
|
||
impl FnMut<()> for Store { | ||
extern "rust-call" fn call_mut(&mut self, _: ()) -> Self::Output { | ||
self.state.get() | ||
} | ||
} | ||
|
||
impl Fn<()> for Store { | ||
extern "rust-call" fn call(&self, _: ()) -> Self::Output { | ||
self.state.get() | ||
} | ||
} | ||
} |