Skip to content
Snippets Groups Projects
Commit feb613ef authored by xaralis's avatar xaralis
Browse files

feat: avoid losing any intermediate state when WS gets disconnected

parent dcb458b8
No related branches found
No related tags found
No related merge requests found
......@@ -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",
......
......@@ -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",
......
......@@ -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>
......
......@@ -23,3 +23,4 @@ export const loadGroupMappings = createAsyncAction(
},
}
);
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());
}
});
......@@ -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.
......
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);
};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment