From feb613ef617cd8282f0b2e9f3deeea5b972e229b Mon Sep 17 00:00:00 2001 From: xaralis <filip.varecha@fragaria.cz> Date: Sat, 19 Dec 2020 21:22:25 +0100 Subject: [PATCH] feat: avoid losing any intermediate state when WS gets disconnected --- package-lock.json | 5 ++++ package.json | 3 +- src/App.jsx | 8 ++--- src/actions/misc.js | 1 + src/actions/ws.js | 30 +++++++++++++++++++ src/index.js | 18 +++++------ src/ws/connection.js | 71 +++++++++++++++++++++++++++++++------------- 7 files changed, 97 insertions(+), 39 deletions(-) create mode 100644 src/actions/ws.js diff --git a/package-lock.json b/package-lock.json index 3d3ff0e..db6b344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14084,6 +14084,11 @@ "xml-name-validator": "^3.0.0" } }, + "wait-queue": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/wait-queue/-/wait-queue-1.1.4.tgz", + "integrity": "sha512-/VdMghiBDG/Ch43ZRp3d8OSd8A0dx8hfkBO7AfWCDzMn2blHquMf+3gqHHhYcggSBpKf7VTzA939bb0DevYKBA==" + }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", diff --git a/package.json b/package.json index d820587..3b09750 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "react-modal": "^3.12.1", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", - "unfetch": "^4.2.0" + "unfetch": "^4.2.0", + "wait-queue": "^1.1.4" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.jsx b/src/App.jsx index d2ba396..88a4cce 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,9 +3,7 @@ import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; import { KeycloakProvider } from "@react-keycloak/web"; import * as Sentry from "@sentry/react"; -import { loadGroupMappings } from "actions/misc"; -import { loadProgram } from "actions/program"; -import { loadPosts } from "actions/posts"; +import { initializeWSChannel } from "actions/ws"; import Footer from "components/Footer"; import Navbar from "components/Navbar"; import Home from "pages/Home"; @@ -57,9 +55,7 @@ const LoadingComponent = ( ); const BaseApp = () => { - loadGroupMappings.read(); - loadProgram.read(); - loadPosts.read(); + initializeWSChannel.read(); return ( <Router> diff --git a/src/actions/misc.js b/src/actions/misc.js index 92ea09c..ca57989 100644 --- a/src/actions/misc.js +++ b/src/actions/misc.js @@ -23,3 +23,4 @@ export const loadGroupMappings = createAsyncAction( }, } ); + diff --git a/src/actions/ws.js b/src/actions/ws.js new file mode 100644 index 0000000..59b5c6e --- /dev/null +++ b/src/actions/ws.js @@ -0,0 +1,30 @@ +import { createAsyncAction, errorResult, successResult } from "pullstate"; + +import { connect } from "ws/connection"; + +import { loadPosts } from "./posts"; +import { loadProgram } from "./program"; + +export const initializeWSChannel = createAsyncAction(async () => { + try { + const wsChannel = await connect({ + onConnect: async ({ worker }) => { + // Re-load initial data once connected, this will ensure we won't lose + // any intermediate state. + await Promise.all([ + loadProgram.run({}, { respectCache: false }), + loadPosts.run({}, { respectCache: false }), + ]); + + // Once loaded, start processing the messages. + worker.start(); + + return true; + }, + }); + + return successResult(wsChannel); + } catch (err) { + return errorResult([], err.toString()); + } +}); diff --git a/src/index.js b/src/index.js index 60f1b43..63f1337 100644 --- a/src/index.js +++ b/src/index.js @@ -2,23 +2,19 @@ import React from "react"; import ReactDOM from "react-dom"; import ReactModal from "react-modal"; -import { connect } from "./ws/connection"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; const root = document.getElementById("root"); -const render = () => { - ReactDOM.render( - <React.StrictMode> - <App /> - </React.StrictMode>, - root - ); - ReactModal.setAppElement(root); -}; +ReactDOM.render( + <React.StrictMode> + <App /> + </React.StrictMode>, + root +); -connect().then(render); +ReactModal.setAppElement(root); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/src/ws/connection.js b/src/ws/connection.js index 5d4ae39..ab2a5f1 100644 --- a/src/ws/connection.js +++ b/src/ws/connection.js @@ -1,48 +1,77 @@ +import WaitQueue from "wait-queue"; + import { handleRanking } from "./handlers"; const handlerMap = { ranked: handleRanking, }; -const messageRouter = (event) => { - console.debug("[ws] New message", event.data); +function Worker() { + const queue = new WaitQueue(); - try { - const data = JSON.parse(event.data); + const doLoop = async () => { + const event = await queue.shift(); + messageRouter(event); + setTimeout(doLoop); + }; - if (!data.event) { - return console.error("[ws] Missing `event` field"); - } + const messageRouter = (event) => { + console.debug("[ws][worker] New message", event.data); + + try { + const data = JSON.parse(event.data); + + if (!data.event) { + return console.error("[ws][worker] Missing `event` field"); + } + + const handlerFn = handlerMap[data.event]; - const handlerFn = handlerMap[data.event]; + if (!handlerFn) { + return console.warn(`[ws][worker] Can't handle event '${data.event}'`); + } - if (!handlerFn) { - console.warn(`[ws] Can't handle event '${data.event}'`); + handlerFn(data.payload || {}); + } catch (err) { + console.error("[ws][worker] Could not parse message as JSON."); } + }; - handlerFn(data.payload); - } catch (err) { - console.error("[ws] Could not parse message as JSON."); - } -}; + return { + queue, + start: () => { + console.debug("[ws][worker] Start processing messages."); + doLoop(); + }, + }; +} -export const connect = () => { +export const connect = ({ onConnect }) => { return new Promise((resolve, reject) => { + const worker = Worker(); + const ws = new WebSocket(process.env.REACT_APP_WS_BASE_URL); - let keepAlive; + let keepAliveInterval; console.log("[ws] Connecting ..."); ws.onopen = () => { console.log("[ws] Connected."); - resolve(ws); - keepAlive = setInterval(() => { + keepAliveInterval = setInterval(() => { ws.send("KEEPALIVE"); console.debug("[ws] Sending keepalive."); }, 30 * 1000); + + const self = { ws, worker }; + + if (onConnect) { + return onConnect(self).then(() => resolve(self)); + } + + return resolve(self); }; - ws.onmessage = messageRouter; + ws.onmessage = worker.queue.push.bind(worker.queue); ws.onclose = (event) => { console.log( @@ -50,7 +79,7 @@ export const connect = () => { event.reason ); - clearInterval(keepAlive); + clearInterval(keepAliveInterval); setTimeout(connect, 1000); }; -- GitLab