-
Notifications
You must be signed in to change notification settings - Fork 179
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
refactor(app): Implement modules API-client with redux-observable #3395
Conversation
Codecov Report
@@ Coverage Diff @@
## edge #3395 +/- ##
==========================================
+ Coverage 52.56% 53.74% +1.18%
==========================================
Files 771 782 +11
Lines 22821 24241 +1420
==========================================
+ Hits 11996 13029 +1033
- Misses 10825 11212 +387
Continue to review full report at Codecov.
|
|
||
return ( | ||
<IntervalWrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This IntervalWrapper
called GET /modules/:serial/data
can be removed because:
- Its parent is already calling
GET /modules
on an interval - Module data by serial is now properly merged into the state in the same place as the response from
GET /modules
goes
@@ -31,18 +31,21 @@ | |||
"lodash": "^4.17.4", | |||
"mixpanel-browser": "^2.22.1", | |||
"moment": "^2.19.1", | |||
"path-to-regexp": "^3.0.0", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the same module react-router
uses to turn route paths like /modules/:serial
into regular expressions. Figured we could use it for the same thing for the same reasons
const getRobotModules = makeGetRobotModules() | ||
function mapStateToProps(state: State, ownProps: OP): SP { | ||
const sessionModules = robotSelectors.getModules(state) | ||
const actualModules = getModulesState(state, ownProps.robot.name) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
getModulesState
is a non-memoized selector. It's simply pulling an object out of state by the robot name, so any sort of memoization:
- Probably adds some overhead
- Makes everything harder to read and more prone to errors
tempdeck, | ||
tempdeckData, | ||
} | ||
function mapStateToProps(state: State): SP { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff on this one mapStateToProps
I think is the best encapsulation on the problems with our previous state management strategy
app/src/robot-api/fetch.js
Outdated
} | ||
|
||
return fromFetch(url, options).pipe( | ||
timeout(timeoutMs), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default timeout of something less than 5 minutes!
// after POST /modules/:serial completes, call GET /modules/:serial/data | ||
const _setTargetTempEpic = createBaseRequestEpic('POST', MODULE_BY_SERIAL_PATH) | ||
|
||
const setTargetTempEpic: Epic = action$ => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because an epic is an action stream (action$: Observable<Action>) => Observable<Action>
, we can pipe more stuff onto the base stream to add functionality. In this case, if the POST comes back successfully, we tack on a GET .../data request for the same serial
Because I'm new to RxJS, I don't actually know if a switchMap
is the best way to do this, so any guidance is appreciated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This article does a pretty good job of comparing map
vs. mergeMap
vs. switchMap
(https://netbasal.com/understanding-mergemap-and-switchmap-in-rxjs-13cf9c57c885). The TL;DR of it is that in this case, For each emission of the outer Observable (every time the epic is hit with it's request action), map
would mint a new inner Observable that needs to be subscribed to and cleaned up etc., mergeMap
manages the subscription of the inner observable and will emit once even if a past request is still in flight, without cancelling the past request, switchMap
is very similar to mergeMap, except it will cancel all older inner observables as soon as a newer one emits. This makes switchMap
a good enough fit for this case as we only care about the most recent call to the robots
setTargetTempEpic | ||
) | ||
|
||
export function modulesReducer( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
By having per-resource-type reducers, we avoid the problems caused by trying to be too generic with API state. For /modules
, our resource state is best expressed as an array of attached modules. This get rid of a bunch of annoying modulesCall && modulesCall.response && ...
chains.
In the future we should look into normalization (i.e. flattening to by-ID objects), but I didn't want to lump that in here, too
@@ -0,0 +1,107 @@ | |||
// @flow |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Trying to keep all API types in this file for now. Maybe won't scale
app/src/robot-api/utils.js
Outdated
payload, | ||
}) | ||
|
||
export const createBaseRequestEpic = (method: Method, path: string): Epic => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Base epic for any given endpoint. I'm hopeful this will be broadly applicable, with additional logic able to be pipe
'd in
@@ -885,7 +885,7 @@ | |||
"aspirate": [ | |||
[1.774444444, -0.1917910448, 1.2026], | |||
[2.151481481, -0.0706286837, 1.0125], | |||
[2.898518519, -0.04343083788, 0.9540], | |||
[2.898518519, -0.04343083788, 0.954], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just here because I ran make format
and apparently someone had forgotten to before me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After looking at the code it seems like this is the right path to go down.
I added a few discussion point comments, but overall, I think the diff elucidates the positive effect of introducing this layer of async state management.
💡
@@ -3,11 +3,14 @@ | |||
import { combineReducers } from 'redux' | |||
import { routerReducer } from 'react-router-redux' | |||
|
|||
// robot state | |||
// oldest robot api state |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😭
Co-Authored-By: mcous <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 🚀
README was super helpful
New State makes sense
Will do further reading up on rxjs before asking any in person questions
overview
After going through the 3.8.2 release testing process and running into #3378, I started an exploration last week of finally revamping the app's API state tracking with these goals:
Based on @b-cooper's previous experience using it, I decided to try out redux-observable and rxjs. Observables are neat, but there's a learning curve we'll have to deal with. Luckily HTTP API calls don't go too deep into that curve.
I'm really curious to see if we think this is a viable path forward and if it looks like it solves our existing problems. Did I miss anything in the above bullets, and is anything in this PR confusing?
changelog
The
/modules
endpoints hit the bullet points above really nicely:GET /modules
,GET /modules/:serial/data
, andPOST /modules/:serial
all retrieve and change the same resourcesPOST /modules/:serial
needs an immediateGET /modules/:serial/data
afterwards to update the model in state, which involves layering some logic on top of a default request behaviorreview requests
The new new HTTP API state lives in
app/src/robot-api
. I'm planning on getting everything moved over fromhttp-api-client
as quickly as possible to minimize confusion, but for a minute it's gonna be weird. Sorry.checklist
OT_APP_DEV_INTERNAL__TEMPDECK_CONTROLS=1
tour of
robot-api
resources/
index.js
- Exportshealth.js
- Simple module for the/health
endpoint/health
isn't actually used explicitly by the app; it's wrapped up in the discovery clientmodules.js
- Action creators, a reducer, a selector, and epics for the three modules endpointsfetch.js
rxjs::fromFetch
; callswindow.fetch
but wraps it in an Observable with cancellation, which will come in handy later for networking endpointsindex.js
- ExportsREADME.md
- Instructions; give this one a read and let me know what you thinkreducer.js
resources
andnetworking
per robot by robot namecombineReducers
that handles thenetworking
state and the "per robot name" part of thingsutils.js
createBaseRequestEpic
- Defines an epic for a given endpointgetRobotApiState
- Gets a robot's API stateapiCall
,apiResponse
, andapiError