diff --git a/package-lock.json b/package-lock.json index 3d3ff0e6b28ed1c2df2d0170423c556ff1392550..db6b3447ef3420d528ca3246732c68f2713105ed 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 d820587936f0d023c71ef98648dda62ad7401491..3b0975013e5815096adb5a70b33f59519b168ef4 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 d2ba396f315a7a5960c105fbb5383a59a7a45a11..88a4ccedf659dc3412738f83e78e2f940dd7c928 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 92ea09cde20da32cb897392cb5a1456be3707c35..ca57989d4093a06a4262e7fc3d3ca06c1231e9c1 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 0000000000000000000000000000000000000000..59b5c6e7f0cbf60a3ff1743cff82e314e31dfd4a --- /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 60f1b437bafe2c08bdb6d4b364c578e77ca7d05e..63f13377424e58ef3ba12bee37c012fac2069cb8 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 5d4ae399a95965221b893abfc5b52b7ba8e1eb72..ab2a5f1d863799b3e79663e0b49e4f2d111157d7 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); };