Skip to content

Commit

Permalink
Initial frontend SSE implementation (WIP)
Browse files Browse the repository at this point in the history
- Current assignment state will be send on connect
- Changes will not yet be send to event source
  • Loading branch information
S-Schickentanz committed Jul 8, 2024
1 parent 310329f commit d9ab32c
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 5 deletions.
23 changes: 18 additions & 5 deletions backend/src/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
SubmissionAdminOverviewEntry,
TerminalStateType,
} from "../Environment";
import ActiveEnvironmentTracker from "../trackers/ActiveEnvironmentTracker";
import TrackerSSEHandler from "../sse/TrackerSSEHandler";

export default (persister: Persister): Router => {
const router = Router();
Expand Down Expand Up @@ -391,10 +391,23 @@ export default (persister: Persister): Router => {
"/environments",
authenticationMiddleware,
adminRoleMiddleware,
(_, res) => {
res
.status(200)
.json(Object.fromEntries(ActiveEnvironmentTracker.getActivityMap()));
(req, res) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
});

const reqWithUser = req as RequestWithUser;

TrackerSSEHandler.addClient(res, reqWithUser.user.username);

req.on("close", () => {
TrackerSSEHandler.removeClient(reqWithUser.user.username);
res.end();
});

TrackerSSEHandler.sendInitialActivityData(res);
},
);

Expand Down
34 changes: 34 additions & 0 deletions backend/src/sse/TrackerSSEHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ServerResponse } from "http";
import ActiveEnvironmentTracker from "../trackers/ActiveEnvironmentTracker";

export default class TrackerSSEHandler {
private static clients: Map<string, ServerResponse> = new Map();

public static clientAlreadyConnected(username: string): boolean {
return this.clients.has(username);
}

public static addClient(client: ServerResponse, username: string): void {
this.forceCloseStaleConnection(username);
this.clients.set(username, client);
}

public static removeClient(username: string): void {
this.clients.delete(username);
}

public static sendInitialActivityData(client: ServerResponse): void {
const data = JSON.stringify(
Object.fromEntries(ActiveEnvironmentTracker.getActivityMap()),
);

client.write(`data: ${data}\n\n`);
}

public static forceCloseStaleConnection(username: string): void {
if (this.clientAlreadyConnected(username)) {
const client = this.clients.get(username);
client?.end();
}
}
}
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.5",
"@fontsource/roboto": "^5.0.13",
"@microsoft/fetch-event-source": "^2.0.1",
"@monaco-editor/react": "^4.4.5",
"@mui/icons-material": "^5.15.20",
"@mui/lab": "^5.0.0-alpha.170",
Expand Down
71 changes: 71 additions & 0 deletions frontend/src/components/ActiveEnvironmentTracker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { useEffect, useRef } from "react";
import { fetchEventSource } from "@microsoft/fetch-event-source";
import { APIBasePath } from "../api/Request";
import { useAuthStore } from "../stores/authStore";

const ActiveEnvironmentTracker = (): JSX.Element => {
const abortControllerRef = useRef(new AbortController());

useEffect(() => {
const abortController = new AbortController();
abortControllerRef.current = abortController;

const initSSE = async () => {
try {
console.log("initSSE");
await fetchEventSource(`${APIBasePath}/admin/environments`, {
method: "GET",
headers: {
"Content-Type": "text/event-stream",
Authorization: useAuthStore.getState().token,
},
onmessage(ev) {
console.log("SSE message");
console.log(ev.data);
},
onclose() {
console.log("SSE closed");
stopResponseSSE();
},
async onopen(response) {
console.log(response);
if (response.ok) {
console.log("SSE open");
} else {
console.log("SSE failed to open");
}
return Promise.resolve();
},
onerror(ev) {
console.error(ev);
throw new Error("SSE error");
},
signal: abortController.signal,
});
} catch (error) {
console.log("Error initializing SSE:", error);
}
};

initSSE().catch((error) => {
console.log(error);
});

return () => {
console.log("Cleanup");
stopResponseSSE();
};
}, []);

function stopResponseSSE() {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
console.log("SSE connection aborted");
}
abortControllerRef.current = new AbortController();
}

return <p>Hello</p>;
};

export default ActiveEnvironmentTracker;
3 changes: 3 additions & 0 deletions frontend/src/views/Administration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import AddEntryDialog from "../components/AddEntryDialog";
import SubmissionOverview from "../components/SubmissionOverview";

import { APIRequest } from "../api/Request";
import ActiveEnvironmentTracker from "../components/ActiveEnvironmentTracker";

const assignmentValidator = z.object({
_id: z.string(),
Expand Down Expand Up @@ -357,6 +358,7 @@ function Administration(): JSX.Element {
"Assign Users",
"Course Assignments",
"Submission Overview",
"Active Environments",
]}
>
<UserAssignment
Expand All @@ -375,6 +377,7 @@ function Administration(): JSX.Element {
key="submissionOverview"
assignments={assignments}
></SubmissionOverview>
<ActiveEnvironmentTracker key="activeEnvironments"></ActiveEnvironmentTracker>
</AdminTabs>
</Grid>
) : null}
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit d9ab32c

Please sign in to comment.