Redux websockets implemented

This commit is contained in:
Claudio Maggioni (maggicl) 2020-04-12 17:46:16 +02:00
parent a4b59dc922
commit 0805d9d4d0
5 changed files with 61 additions and 82 deletions

View file

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@giantmachines/redux-websocket": "^1.1.7",
"@material-ui/core": "^4.9.4", "@material-ui/core": "^4.9.4",
"@material-ui/icons": "^4.9.1", "@material-ui/icons": "^4.9.1",
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",

20
smart-hut/src/endpoint.js Normal file
View file

@ -0,0 +1,20 @@
/**
* Returns the endpoint URL (SmartHut backend URL)
* @returns {String} endpoint URL
*/
export function endpointURL() {
return window.BACKEND_URL !== "__BACKEND_URL__"
? window.BACKEND_URL
: "http://localhost:8080";
}
export function socketURL(token) {
const httpURL = new URL(endpointURL());
const isSecure = httpURL.protocol === "https:";
const protocol = isSecure ? "wss:" : "ws:";
const port = httpURL.port || (isSecure ? 443 : 80);
const url = `${protocol}//${httpURL.hostname}:${port}/sensor-socket?token=${token}`;
console.log('socket url: ', url);
return url;
}

View file

@ -1,6 +1,9 @@
import smartHutStore from "./store"; import smartHutStore from "./store";
import actions from "./storeActions"; import actions from "./storeActions";
import axios from "axios"; import axios from "axios";
import { endpointURL, socketURL } from "./endpoint";
import { connect, disconnect } from "@giantmachines/redux-websocket";
/** /**
* An object returned by promise rejections in remoteservice * An object returned by promise rejections in remoteservice
@ -16,16 +19,6 @@ class RemoteError extends Error {
} }
} }
/**
* Returns the endpoint URL (SmartHut backend URL)
* @returns {String} endpoint URL
*/
function endpointURL() {
return window.BACKEND_URL !== "__BACKEND_URL__"
? window.BACKEND_URL
: "http://localhost:8080";
}
const Endpoint = { const Endpoint = {
axiosInstance: axios.create({ axiosInstance: axios.create({
baseURL: endpointURL(), baseURL: endpointURL(),
@ -176,7 +169,10 @@ export const RemoteService = {
login: (usernameOrEmail, password) => { login: (usernameOrEmail, password) => {
return (dispatch) => { return (dispatch) => {
return Endpoint.login(usernameOrEmail, password) return Endpoint.login(usernameOrEmail, password)
.then((token) => dispatch(actions.loginSuccess(token))) .then((token) => {
dispatch(actions.loginSuccess(token))
dispatch(connect(socketURL(token)))
})
.catch((err) => { .catch((err) => {
console.warn("login error", err); console.warn("login error", err);
throw new RemoteError([ throw new RemoteError([
@ -193,7 +189,10 @@ export const RemoteService = {
*/ */
logout: () => { logout: () => {
return (dispatch) => return (dispatch) =>
Endpoint.logout().then(void dispatch(actions.logout())); Endpoint.logout().then(() => {
dispatch(disconnect())
dispatch(actions.logout())
});
}, },
/** /**
@ -476,71 +475,6 @@ for (const key in RemoteService) {
RemoteService[key] = RemoteService[key].bind(RemoteService); RemoteService[key] = RemoteService[key].bind(RemoteService);
} }
/** Class to handle connection with the sensor socket */
class ServiceSocket {
token;
authenticated = false;
retries = 0;
connection;
static get URL() {
const httpURL = new URL(endpointURL());
const isSecure = httpURL.protocol === "https:";
const protocol = isSecure ? "wss:" : "ws:";
const port = httpURL.port || (isSecure ? 443 : 80);
return `${protocol}//${httpURL.host}:${port}/sensor-socket`;
}
/**
* Create a new sensor socket connection
* @param {string} token - The JWT token (needed for authentication)
*/
constructor(token) {
this.token = token;
this.authenticated = false;
}
_init() {
this.connection = new WebSocket(ServiceSocket.URL);
this.connection.onopen = (_) => {
this.connection.send(JSON.stringify({ token: this.token }));
};
this.connection.onmessage = (evt) => {
let data = JSON.parse(evt.data);
if (!this.authenticated) {
if (data.authenticated) {
this.authenticated = true;
this.retries = 0;
} else {
console.error("socket authentication failed", data);
}
} else {
this.invokeCallbacks(data);
}
};
this.connection.onerror = (evt) => {
console.warn("socket error", evt);
if (this.retries >= 5) {
console.error("too many socket connection retries");
return;
}
this.retries++;
this._init();
};
}
/**
* Closes the underlying websocket connection
*/
close() {
this.connection.close();
}
}
export class Forms { export class Forms {
/** /**
* Attempts to create a new user from the given data. * Attempts to create a new user from the given data.

View file

@ -1,6 +1,8 @@
import { createStore, applyMiddleware } from "redux"; import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk"; import thunk from "redux-thunk";
import update from "immutability-helper"; import update from "immutability-helper";
import reduxWebSocket, { connect } from "@giantmachines/redux-websocket";
import { socketURL } from "./endpoint";
function reducer(previousState, action) { function reducer(previousState, action) {
let newState, change; let newState, change;
@ -229,12 +231,20 @@ function reducer(previousState, action) {
}, },
}); });
break; break;
case "REDUX_WEBSOCKET::MESSAGE":
const devices = JSON.parse(action.payload.message);
console.log(devices);
newState = reducer(previousState, {
type: "DEVICES_UPDATE",
partial: true,
devices
});
break;
default: default:
console.warn(`Action type ${action.type} unknown`, action); console.warn(`Action type ${action.type} unknown`, action);
return previousState; return previousState;
} }
console.log("new state: ", newState);
return newState; return newState;
} }
@ -274,7 +284,14 @@ function createSmartHutStore() {
initialState.login.token = null; initialState.login.token = null;
} }
return createStore(reducer, initialState, applyMiddleware(thunk)); const store = createStore(reducer, initialState,
compose(
applyMiddleware(thunk),
applyMiddleware(reduxWebSocket())));
if (initialState.login.loggedIn) {
store.dispatch(connect(socketURL(token)));
}
return store;
} }
const smartHutStore = createSmartHutStore(); const smartHutStore = createSmartHutStore();

View file

@ -973,6 +973,13 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed" resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@giantmachines/redux-websocket@^1.1.7":
version "1.1.7"
resolved "https://registry.yarnpkg.com/@giantmachines/redux-websocket/-/redux-websocket-1.1.7.tgz#8c045a359cd3f9a73ef141ce722fd14ae754cd1b"
integrity sha512-t90k+NcVInXvppMVpU3c7ZC6i58S/jBPqltckAlKfrtc92YmGZ/He3qYT9OiemlvS+/d+R6P/Ed4yEqKVevYdg==
dependencies:
redux "~4"
"@hapi/address@2.x.x": "@hapi/address@2.x.x":
version "2.1.4" version "2.1.4"
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
@ -9008,7 +9015,7 @@ redux-thunk@^2.3.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw== integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
redux@^4.0.5: redux@^4.0.5, redux@~4:
version "4.0.5" version "4.0.5"
resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.5.tgz#4db5de5816e17891de8a80c424232d06f051d93f"
integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w== integrity sha512-VSz1uMAH24DM6MF72vcojpYPtrTUu3ByVWfPL1nPfVRb5mZVTve5GnNCUV53QM/BZ66xfWrm0CTWoM+Xlz8V1w==