-
Notifications
You must be signed in to change notification settings - Fork 0
[OA][Infrastructure] - Init WS Server #267
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,82 @@ | ||
| import { WebSocketServer } from 'ws'; | ||
| import { jwtVerify } from 'jose'; | ||
|
|
||
| const ws = new WebSocketServer({ port: 8080 }); | ||
| const secret = new TextEncoder().encode(process.env.JWT_SECRET); | ||
|
|
||
| ws.on('connection', (ws) => { | ||
| ws.on('message', (data) => { | ||
| ws.send(data.toString('utf-8')); | ||
| console.log('Sarge WS server listening on 8080'); | ||
|
|
||
| // <userId, socket> | ||
| const clients = new Map(); | ||
|
|
||
| const HEARTBEAT_INTERVAL = 5000; | ||
|
|
||
| function getTokenFromRequest(request) { | ||
| return new URL(request.url ?? '/', 'ws://x').searchParams.get('token'); | ||
| } | ||
|
|
||
| async function verifyToken(token) { | ||
| const { payload } = await jwtVerify(token, secret); | ||
| return payload; | ||
| } | ||
|
|
||
| function startHeartbeat(socket) { | ||
| return setInterval(() => { | ||
| if (!socket.isAlive) { | ||
| // TODO: add an offline timer before the socket gets terminated unique to each user | ||
| // Once done, send an API request back to server to end the OA | ||
|
|
||
| // terminate() jumps to socket.on('close') | ||
| socket.terminate(); | ||
| return; | ||
| } | ||
| socket.isAlive = false; | ||
| socket.ping(); | ||
| }, HEARTBEAT_INTERVAL); | ||
| } | ||
|
|
||
| ws.on('connection', async (socket, request) => { | ||
| const token = getTokenFromRequest(request); | ||
| try { | ||
| const payload = await verifyToken(token ?? ''); | ||
| socket.userId = payload.userId; | ||
| socket.isAlive = true; | ||
| clients.set(payload.userId, socket); | ||
| } catch { | ||
| socket.close(1008, 'Unauthorized'); | ||
| return; | ||
| } | ||
|
|
||
| socket.heartbeat = startHeartbeat(socket); | ||
|
|
||
| socket.on('message', (data) => { | ||
| socket.send(data.toString('utf-8')); | ||
| }); | ||
|
|
||
| socket.on('pong', () => { | ||
| socket.isAlive = true; | ||
| }); | ||
|
|
||
| socket.on('close', (code, reason) => { | ||
| if (!socket.userId) return; | ||
| clearInterval(socket.heartbeat); | ||
| clients.delete(socket.userId); | ||
| console.log( | ||
| `Client ${socket.userId} disconnected | code: ${code}, reason: ${reason.toString()}` | ||
| ); | ||
| }); | ||
|
|
||
| socket.on('error', (err) => { | ||
| console.error(`Socket error for ${socket.userId}:`, err.message); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to send an error message to our main server if it's broken? Lmk if this is even feasible.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. Let's add this when developing the client side logic. |
||
| }); | ||
| }); | ||
|
|
||
| ws.on('error', (err) => { | ||
| console.error('Socket error: ', err); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
| }); | ||
|
|
||
| ws.on('close', () => { | ||
| for (const socket of clients.values()) { | ||
| socket.close(1001, 'Shutting down server'); | ||
| } | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Where does the token get initially added to the connection request? I'm assuming this is something we will set up when configuring the websocket on the client (aka in the react code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the token will be generated using our client and API. As long as the secrets are the same, this will be a successful operation. If not, then the catch will drop the connection.