Skip to content

Commit

Permalink
chore: add websocket connection to client
Browse files Browse the repository at this point in the history
  • Loading branch information
bacali95 committed Nov 1, 2024
1 parent 99a3b83 commit 5c37068
Show file tree
Hide file tree
Showing 17 changed files with 107 additions and 57 deletions.
6 changes: 1 addition & 5 deletions apps/client/src/core/contexts/active-contest/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@ export type ActiveContest = {
setCurrentContest: (contest?: Contest) => void;
};

export const ActiveContestContext = createContext<ActiveContest>({
activeContests: [],
currentContest: undefined,
setCurrentContest: () => undefined,
});
export const ActiveContestContext = createContext<ActiveContest | undefined>(undefined);

export const ActiveContestProvider: FC<PropsWithChildren> = ({ children }) => {
const [now, setNow] = useState(new Date());
Expand Down
10 changes: 8 additions & 2 deletions apps/client/src/core/contexts/active-contest/hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { useContext } from 'react';

import { ActiveContest, ActiveContestContext } from './context';
import { ActiveContestContext } from './context';

export function useActiveContest() {
return useContext(ActiveContestContext) as ActiveContest;
const context = useContext(ActiveContestContext);

if (!context) {
throw new Error('useActiveContest must be used within a ActiveContestProvider');
}

return context;
}
8 changes: 1 addition & 7 deletions apps/client/src/core/contexts/auth/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ import { request } from '../../utils';

export type User = Prisma.UserGetPayload<{ include: { role: true; team: true } }>;

export const AuthContext = createContext<AuthContext<User>>({
connected: false,
isUserAdmin: false,
isUserJury: false,
setLastConnectedTime: () => undefined,
checkUserRole: () => false,
});
export const AuthContext = createContext<AuthContext<User> | undefined>(undefined);

export type AuthContext<T extends User> = {
connected: boolean;
Expand Down
8 changes: 7 additions & 1 deletion apps/client/src/core/contexts/auth/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@ import { useContext } from 'react';
import { AuthContext, User } from './context';

export function useAuthContext<T extends User>() {
return useContext(AuthContext) as AuthContext<T>;
const context = useContext(AuthContext);

if (!context) {
throw new Error('useAuthContext must be used within a AuthContextProvider');
}

return context as AuthContext<T>;
}
1 change: 1 addition & 0 deletions apps/client/src/core/contexts/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './active-contest';
export * from './auth';
export * from './toast';
export * from './websocket';
2 changes: 1 addition & 1 deletion apps/client/src/core/contexts/toast/hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function useToastContext(): ToastContext {
const context = useContext(ToastContext);

if (!context) {
throw new Error();
throw new Error('useToastContext must be used within a ToastContextProvider');
}

return context;
Expand Down
14 changes: 14 additions & 0 deletions apps/client/src/core/contexts/websocket/context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FC, PropsWithChildren, createContext } from 'react';
import { Socket, io } from 'socket.io-client';

export type WebSocketContext = {
socket: Socket;
};

const socket = io(`/ws`, { transports: ['websocket'] });

export const WebSocketContext = createContext<WebSocketContext | undefined>(undefined);

export const WebSocketContextProvider: FC<PropsWithChildren> = ({ children }) => {
return <WebSocketContext.Provider value={{ socket }}>{children}</WebSocketContext.Provider>;
};
13 changes: 13 additions & 0 deletions apps/client/src/core/contexts/websocket/hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useContext } from 'react';

import { WebSocketContext } from './context';

export function useWebSocketContext(): WebSocketContext {
const context = useContext(WebSocketContext);

if (!context) {
throw new Error('useWebSocketContext must be used within a WebSocketContextProvider');
}

return context;
}
2 changes: 2 additions & 0 deletions apps/client/src/core/contexts/websocket/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './context';
export * from './hook';
31 changes: 19 additions & 12 deletions apps/client/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import { createRoot } from 'react-dom/client';
import { LayoutContextProvider, SidebarContextProvider, Spinner } from 'tw-react-components';
import 'tw-react-components/css';

import { ActiveContestProvider, AuthContextProvider, ToastContextProvider } from '@core/contexts';
import {
ActiveContestProvider,
AuthContextProvider,
ToastContextProvider,
WebSocketContextProvider,
} from '@core/contexts';
import { Provider as ZenStackHooksProvider } from '@core/queries';

import Root from './Root';
Expand All @@ -29,17 +34,19 @@ root.render(
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools buttonPosition="bottom-right" />
<ZenStackHooksProvider value={{ endpoint: '/api/rpc', fetch: myFetch }}>
<LayoutContextProvider>
<SidebarContextProvider>
<AuthContextProvider>
<ToastContextProvider>
<ActiveContestProvider>
<Root />
</ActiveContestProvider>
</ToastContextProvider>
</AuthContextProvider>
</SidebarContextProvider>
</LayoutContextProvider>
<WebSocketContextProvider>
<LayoutContextProvider>
<SidebarContextProvider>
<AuthContextProvider>
<ToastContextProvider>
<ActiveContestProvider>
<Root />
</ActiveContestProvider>
</ToastContextProvider>
</AuthContextProvider>
</SidebarContextProvider>
</LayoutContextProvider>
</WebSocketContextProvider>
</ZenStackHooksProvider>
</QueryClientProvider>
</Suspense>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,33 @@ import Ansi from 'ansi-to-react';
import { FC, useEffect, useState } from 'react';
import { Sheet } from 'tw-react-components';

import { useWebSocketContext } from '@core/contexts';

type JudgeHostLogsViewerProps = {
hostname?: string;
onClose: () => void;
};

export const JudgeHostLogsViewer: FC<JudgeHostLogsViewerProps> = ({ hostname, onClose }) => {
// const { socket } = useStore<RootStore>('rootStore');
const { socket } = useWebSocketContext();

const [logs, setLogs] = useState<string[]>([]);

// useEffect(() => {
// if (hostname) {
// const event = `judgeHost-${hostname}-logs`;
// socket.off(event);
// socket.on(event, (logLine: string) => {
// setLogs((logs) => [...logs, logLine]);
// const terminalSegment = document.getElementById('terminal-logs');
// terminalSegment && (terminalSegment.scrollTop = terminalSegment.scrollHeight);
// });
// return () => {
// socket.off(event);
// };
// }
// }, [hostname, socket]);
useEffect(() => {
if (!hostname) return;

const event = `judgeHost-${hostname}-logs`;

socket.on(event, (logLine: string) => {
setLogs((logs) => [...logs, logLine]);
const terminalSegment = document.getElementById('terminal-logs');
terminalSegment && (terminalSegment.scrollTop = terminalSegment.scrollHeight);
});

return () => {
socket.off(event);
};
}, [hostname, socket]);

return (
<Sheet open={!!hostname} onOpenChange={(value) => !value && onClose()}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ClipboardPlusIcon, ClipboardXIcon } from 'lucide-react';
import { FC } from 'react';
import { DataTable, DataTableColumn, cn } from 'tw-react-components';
import { DataTable, DataTableColumn, cn, useLayoutContext } from 'tw-react-components';

import { JUDGING_RESULT_LABELS } from '@core/constants';
import { useUpdateJudging } from '@core/queries';
Expand All @@ -13,9 +13,18 @@ export const SubmissionViewDetails: FC<{
highlightedJudging?: Judging;
setHighlightedJudging: (judging: Judging) => void;
}> = ({ submission, highlightedJudging, setHighlightedJudging }) => {
const { showIds } = useLayoutContext();

const { mutateAsync: updateJudging } = useUpdateJudging();

const columns: DataTableColumn<Judging>[] = [
{
header: '#',
field: 'id',
className: 'w-px',
align: 'center',
hide: !showIds,
},
{
header: 'Team',
field: 'id',
Expand Down Expand Up @@ -98,7 +107,7 @@ export const SubmissionViewDetails: FC<{

return (
<DataTable
className="sticky top-0 z-10 flex-shrink-0"
className="flex-shrink-0"
rows={submission.judgings}
columns={columns}
noDataMessage="Not judged yet"
Expand Down
2 changes: 1 addition & 1 deletion apps/client/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default defineConfig({
secure: false,
},
'^/socket.io': {
target: `ws://localhost:3001/`,
target: `ws://localhost:3000`,
ws: true,
},
},
Expand Down
2 changes: 1 addition & 1 deletion apps/judge/src/helpers/download-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const pipelineAsync = promisify(pipeline);

export async function downloadFile(fileName: string, filePath: string) {
const stream = await http.stream(`files/${encodeURIComponent(fileName)}`);
const writer = createWriteStream(filePath);
const writer = createWriteStream(filePath, { flags: 'w' });

return pipelineAsync(stream, writer);
}
11 changes: 3 additions & 8 deletions apps/server/src/files/files.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,10 @@ export class FilesService {
)({ where: { name } }).then((count) => count > 0);
}

getFileByName(@LogParam('name') name: string): Promise<File> {
async getFileByName(@LogParam('name') name: string): Promise<File> {
return (
logPrismaOperation(
this.prisma,
'file',
'findFirst',
)({
where: { name },
}) ?? throwError(new NotFoundException())
(await logPrismaOperation(this.prisma, 'file', 'findFirst')({ where: { name } })) ||
throwError(new NotFoundException())
);
}

Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/scoreboard/scoreboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class ScoreboardService {
contestId: contest.id,
valid: true,
},
include: { judgings: { where: { valid: true } } },
include: { judgings: { where: { valid: true }, orderBy: { startTime: 'asc' } } },
});

const submissions = allProblemSubmissions.filter(
Expand Down Expand Up @@ -128,7 +128,10 @@ export class ScoreboardService {
}

export function getLatestJudging(submission: Submission): Judging | undefined {
return submission.judgings.sort((a, b) => a.startTime.getTime() - b.startTime.getTime()).at(-1);
return (
submission.judgings.find((judging) => judging.result === 'ACCEPTED') ??
submission.judgings.at(-1)
);
}

export function submissionHasResult(
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/websocket/websocket.gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class WebsocketGateway implements OnGatewayConnection {
async handleConnection(client: Socket) {
const user: User = ((client.request as Request).session as unknown as Session)?.passport?.user;
const roleName = user?.role.name;

if (['admin', 'jury'].includes(roleName)) {
client.join('juries');
} else if (roleName === 'judge-host') {
Expand Down

0 comments on commit 5c37068

Please sign in to comment.