git.fiddlerwoaroof.com
Browse code

Move files around and add a restart button

Ed Langley authored on 21/06/2019 17:24:42
Showing 9 changed files
... ...
@@ -1,12 +1,19 @@
1
+import * as R from "ramda";
1 2
 import React from "react";
2 3
 import ReactDOM from "react-dom";
3 4
 import { Provider, connect } from "react-redux";
4 5
 import { createStore, applyMiddleware, compose } from "redux";
5 6
 import createSagaMiddleware from "redux-saga";
6 7
 
7
-import { Root } from "./react";
8
-import { rootReducer, updateName } from "./redux";
9
-import { rootSaga, getIp } from "./saga";
8
+import { withDispatch } from "./redux-utils";
9
+import { restart, recordError } from "./saga-utils";
10
+import { take, spawn, delay } from "redux-saga/effects";
11
+
12
+import { Root } from "./toplevel-react";
13
+import { rootReducer, updateName } from "./reducer";
14
+import { rootSaga, getIp, fail } from "./root-saga";
15
+
16
+import { put } from "redux-saga/effects";
10 17
 
11 18
 // Store Setup ==============================================================================
12 19
 
... ...
@@ -20,29 +27,56 @@ export const store = createStore(
20 27
   composeEnhancers(applyMiddleware(sagaMiddleware))
21 28
 );
22 29
 
23
-// Run the root saga
24
-sagaMiddleware.run(rootSaga);
25
-
26
-// Connect Redux to Toplevel Component ======================================================
27 30
 const ConnectedRoot = connect(
28 31
   // Map the store's state to the toplevel component's props
29
-  ({ name, ip }) => ({ name, ip }),
32
+  R.pickAll(["name", "ip", "error"]),
30 33
   // Map redux's dispatch function to props that will call it with a specific action
31
-  dispatch => ({
32
-    getIp() {
33
-      dispatch(getIp());
34
-    },
35
-    updateName(v) {
36
-      dispatch(updateName(v));
37
-    }
38
-  })
34
+  withDispatch({ getIp, updateName, fail, restart })
39 35
 )(Root);
40 36
 
41
-// Render Connected Component to the dom at #root ===========================================
42
-ReactDOM.render(
43
-  // The Toplevel component is wrapped in a provider, to make the store available to connect
44
-  <Provider store={store}>
45
-    <ConnectedRoot />
46
-  </Provider>,
47
-  document.getElementById("root")
48
-);
37
+function* toplevel() {
38
+  // Run the root saga
39
+  let rootTask = yield spawn(rootSaga);
40
+
41
+  // Connect Redux to Toplevel Component ======================================================
42
+  // Render Connected Component to the dom at #root ===========================================
43
+  yield take("co/fwoar/APP_INIT");
44
+
45
+  const reactRender = new Promise(resolve => {
46
+    ReactDOM.render(
47
+      // The Toplevel component is wrapped in a provider, to make the store available to connect
48
+      <Provider store={store}>
49
+        <ConnectedRoot />
50
+      </Provider>,
51
+      document.getElementById("root"),
52
+      resolve
53
+    );
54
+  });
55
+
56
+  yield reactRender;
57
+
58
+  let quit = false;
59
+  while (!quit) {
60
+    try {
61
+      yield rootTask.toPromise();
62
+    } catch (err) {
63
+      console.log("Error!");
64
+      yield put(recordError(err));
65
+
66
+      const { type } = yield take([
67
+        "co/fwoar/APP_RESTART",
68
+        "co/fwoar/APP_QUIT"
69
+      ]);
70
+      if (type === "co/fwoar/APP_QUIT") {
71
+        console.log("Quitting!");
72
+        quit = true;
73
+      } else if (type === "co/fwoar/APP_RESTART") {
74
+        console.log("restarting");
75
+        rootTask = yield spawn(rootSaga);
76
+      }
77
+    }
78
+    yield delay(10);
79
+  }
80
+}
81
+
82
+sagaMiddleware.run(toplevel);
49 83
new file mode 100644
... ...
@@ -0,0 +1,22 @@
1
+import * as R from "ramda";
2
+import { typeEquals, action, applyAction } from "./redux-utils";
3
+import { errorReducer } from "./saga-utils";
4
+
5
+const UPDATE_NAME = "UPDATE_NAME";
6
+const UPDATE_IP = "UPDATE_IP";
7
+
8
+export const updateName = action(UPDATE_NAME);
9
+export const updateIp = action(UPDATE_IP);
10
+
11
+const initialState = {
12
+  name: "",
13
+  ip: "",
14
+  error: null
15
+};
16
+
17
+export const rootReducer = (state = initialState, action) =>
18
+  R.cond([
19
+    [typeEquals(UPDATE_NAME), applyAction("name", state)],
20
+    [typeEquals(UPDATE_IP), applyAction("ip", state)],
21
+    [R.T, action => ({ ...state, error: errorReducer(state.error, action) })]
22
+  ])(action);
0 23
new file mode 100644
... ...
@@ -0,0 +1,12 @@
1
+import * as R from "ramda";
2
+
3
+export const withDispatch = o => dispatch => R.map(f => v => dispatch(f(v)), o);
4
+export const typeEquals = R.propEq("type");
5
+
6
+export const updateStateFromAction = R.curry(
7
+  (actionKey, stateKey, state, action) =>
8
+    R.assoc(stateKey, R.prop(actionKey, action), state)
9
+);
10
+
11
+export const action = type => data => ({ type, data });
12
+export const applyAction = updateStateFromAction("data");
0 13
deleted file mode 100644
... ...
@@ -1,22 +0,0 @@
1
-const initialState = {
2
-  name: "",
3
-  ip: ""
4
-};
5
-
6
-export const updateName = newName => {
7
-  return { type: "UPDATE_NAME", data: newName };
8
-};
9
-
10
-export const updateIp = newIp => {
11
-  return { type: "UPDATE_IP", data: newIp };
12
-};
13
-
14
-export const rootReducer = (state = initialState, action) => {
15
-  if (action.type === "UPDATE_NAME") {
16
-    return { ...state, name: action.data };
17
-  } else if (action.type === "UPDATE_IP") {
18
-    return { ...state, ip: action.data };
19
-  } else {
20
-    return state;
21
-  }
22
-};
23 0
new file mode 100644
... ...
@@ -0,0 +1,25 @@
1
+import { takeLatest, take, put } from "redux-saga/effects";
2
+import { updateIp } from "./reducer";
3
+
4
+export function* rootSaga() {
5
+  yield put({ type: "co/fwoar/APP_INIT" });
6
+  yield takeLatest("GET_IP", ipWorker);
7
+  while (true) {
8
+    yield take("FAIL");
9
+    throw new Error("FAILURE!");
10
+  }
11
+}
12
+
13
+export function getIp() {
14
+  return { type: "GET_IP" };
15
+}
16
+
17
+export function fail() {
18
+  return { type: "FAIL" };
19
+}
20
+
21
+function* ipWorker() {
22
+  const ipR = yield fetch("https://api.ipify.org");
23
+  const ip = yield ipR.text();
24
+  yield put(updateIp(ip));
25
+}
0 26
new file mode 100644
... ...
@@ -0,0 +1,55 @@
1
+import * as R from "ramda";
2
+import { take, spawn, delay } from "redux-saga/effects";
3
+import { action } from "./redux-utils";
4
+
5
+export const restart = () => ({
6
+  type: "co/fwoar/APP_RESTART"
7
+});
8
+const RECORD_ERROR = "RECORD_ERROR";
9
+export const recordError = action(RECORD_ERROR);
10
+
11
+export const errorReducer = (state = null, { type, data }) => {
12
+  if (type === "co/fwoar/APP_RESTART") {
13
+    return null;
14
+  } else if (type === "co/fwoar/RECORD_ERROR") {
15
+    return data;
16
+  } else {
17
+    return state;
18
+  }
19
+};
20
+
21
+export const makeToplevel = (rootSaga, main, onError) =>
22
+  function* toplevel() {
23
+    // Run the root saga
24
+    let rootTask = yield spawn(rootSaga);
25
+
26
+    // Connect Redux to Toplevel Component ======================================================
27
+    // Render Connected Component to the dom at #root ===========================================
28
+    yield take("co/fwoar/APP_INIT");
29
+
30
+    yield main();
31
+
32
+    let quit = false;
33
+    while (!quit) {
34
+      try {
35
+        yield rootTask.toPromise();
36
+      } catch (err) {
37
+        console.log("Error!");
38
+        if (onError) {
39
+          yield* onError(err);
40
+        }
41
+        const { type } = yield take([
42
+          "co/fwoar/APP_RESTART",
43
+          "co/fwoar/APP_QUIT"
44
+        ]);
45
+        if (type === "co/fwoar/APP_QUIT") {
46
+          console.log("Quitting!");
47
+          quit = true;
48
+        } else if (type === "co/fwoar/APP_RESTART") {
49
+          console.log("restarting");
50
+          rootTask = yield spawn(rootSaga);
51
+        }
52
+      }
53
+      yield delay(10);
54
+    }
55
+  };
0 56
deleted file mode 100644
... ...
@@ -1,16 +0,0 @@
1
-import { takeLatest, put } from "redux-saga/effects";
2
-import { updateIp } from "./redux";
3
-
4
-export function* rootSaga() {
5
-  yield takeLatest("GET_IP", ipWorker);
6
-}
7
-
8
-export function getIp() {
9
-  return { type: "GET_IP" };
10
-}
11
-
12
-function* ipWorker() {
13
-  const ipR = yield fetch("https://api.ipify.org");
14
-  const ip = yield ipR.text();
15
-  yield put(updateIp(ip));
16
-}
17 0
\ No newline at end of file
18 1
similarity index 51%
19 2
rename from src/react.js
20 3
rename to src/toplevel-react.js
... ...
@@ -3,15 +3,26 @@ import PropTypes from "prop-types";
3 3
 import { NameControl } from "./NameControl";
4 4
 import { IpControl } from "./IpControl";
5 5
 
6
-export const Root = ({ name, updateName, ip, getIp }) => (
6
+export const Root = ({ name, updateName, ip, getIp, fail, error, restart }) => (
7 7
   <div>
8
+    {error ? (
9
+      <div>
10
+        {error}
11
+        <button onClick={restart}>Restart</button>
12
+      </div>
13
+    ) : null}
8 14
     <NameControl name={name} updateName={updateName} />
9 15
     <IpControl ip={ip} getIp={getIp} />
16
+    <button onClick={fail}>Fail</button>
17
+    <button onClick={restart}>Restart</button>
10 18
   </div>
11 19
 );
12 20
 Root.propTypes = {
21
+  error: PropTypes.any,
22
+  fail: PropTypes.func,
23
+  getIp: PropTypes.func,
13 24
   ip: PropTypes.string,
14 25
   name: PropTypes.string,
15
-  getIp: PropTypes.func,
26
+  restart: PropTypes.func,
16 27
   updateName: PropTypes.func
17 28
 };