Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5,690 changes: 2,840 additions & 2,850 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"license": "GPL-3.0",
"devDependencies": {
"@mate-academy/eslint-config": "latest",
"@mate-academy/scripts": "^1.8.6",
"@mate-academy/scripts": "^2.1.3",
"eslint": "^8.57.0",
"eslint-plugin-jest": "^28.6.0",
"eslint-plugin-node": "^11.1.0",
Expand All @@ -26,5 +26,10 @@
},
"mateAcademy": {
"projectType": "javascript"
},
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1",
"ws": "^8.20.0"
}
}
78 changes: 78 additions & 0 deletions src/handlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
const { rooms } = require('./store');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project currently lacks the client-side implementation required by the task: UI for typing a username, saving it to localStorage, sending it to the server, and room UI (create/rename/join/delete). These are mandatory according to the description and must be added.

const { broadcastRooms, broadcastToRoom } = require('./utils');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In src/index.js you require './src/websocket' from a file that is already in src; this will resolve to src/src/websocket.js and fail. Change the path to './websocket' (or correct relative path) so Node can load the websocket module.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect require path: index.js is inside src, but this imports './src/websocket' which resolves to src/src/websocket and will likely throw MODULE_NOT_FOUND. Change to require('./websocket') or the correct relative path to the websocket module.

function handleMessage(ws, wss, messageAsString) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This repository contains server-side code only. The task requires both client and server and specifically that the username is typed by the user and saved in localStorage (checklist items #1, #3, #4 and #15). Add the client implementation that sends the username to the server and stores it in localStorage.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server-side implements room operations and history broadcasting, which is good, but the task requires that the username be saved in localStorage (client-side). Make sure the client saves the username in localStorage and re-sends it when reconnecting/joining a room so the server can set ws.username correctly.

const data = JSON.parse(messageAsString);

switch (data.type) {
case 'JOIN_ROOM':
ws.roomId = data.roomId;
ws.username = data.username;

if (!rooms[data.roomId]) {
rooms[data.roomId] = {
id: data.roomId,
name: data.roomId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When creating a missing room inside the JOIN_ROOM handler you set the room name to the roomId (line 15). For consistency with CREATE_ROOM (which uses data.roomName), consider using a human-friendly name if available or a consistent naming strategy.

messages: [],
};
}

ws.send(
JSON.stringify({
type: 'ROOM_HISTORY',
roomId: data.roomId,
messages: rooms[data.roomId].messages,
}),
);
break;

case 'NEW_MESSAGE':
if (ws.roomId && rooms[ws.roomId]) {
const newMessage = {
id: Date.now().toString(),
author: ws.username,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message author is set to ws.username here, but ws.username may be undefined if the client hasn't properly joined/identified. Validate that ws.username exists (or reject the message) before creating/persisting a new message to ensure every message has an author as required.

time: new Date().toISOString(),
text: data.text,
};

rooms[ws.roomId].messages.push(newMessage);

broadcastToRoom(wss, ws.roomId, {
type: 'MESSAGE',
roomId: ws.roomId,
message: newMessage,
});
}
break;

case 'CREATE_ROOM':
const newRoomId = 'room_' + Date.now();

rooms[newRoomId] = { id: newRoomId, name: data.roomName, messages: [] };
broadcastRooms(wss);
break;

case 'RENAME_ROOM':
if (rooms[data.roomId] && data.roomId !== 'general') {
rooms[data.roomId].name = data.newName;
broadcastRooms(wss);
}
break;

case 'DELETE_ROOM':
if (rooms[data.roomId] && data.roomId !== 'general') {
delete rooms[data.roomId];

wss.clients.forEach((client) => {
if (client.roomId === data.roomId) {
client.roomId = 'general';
client.send(JSON.stringify({ type: 'ROOM_DELETED' }));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a room is deleted you set moving clients' roomId to 'general' and send a generic ROOM_DELETED message. Consider including the new room id and/or immediately sending the ROOM_HISTORY for 'general' so clients have enough info to update UI and show previous messages as required.

}
});
Comment on lines +83 to +96
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When deleting a room you set affected clients' roomId to 'general' and send a ROOM_DELETED event, but you do not send the ROOM_HISTORY for the 'general' room to those clients. The requirement states new users (and users moved to a room) should see previous messages in the room (checklist item #12). Consider sending the 'ROOM_HISTORY' with rooms['general'].messages to moved clients.

broadcastRooms(wss);
}
break;
}
}

module.exports = { handleMessage };
14 changes: 13 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,13 @@
'use strict';
/* eslint-disable no-console */
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall repo is missing client-side implementation files (UI and client JS). The task explicitly requires both client and server; without client code you can't meet requirements like saving username to localStorage, letting users create/rename/join/delete rooms from the UI, and showing previous messages to new users.

const http = require('http');
const { setupWebSocket } = require('./src/websocket');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect require path: this file is already in src, so requiring './src/websocket' will try to load src/src/websocket.js and fail. Change to require('./websocket') or adjust the path to the actual websocket module location.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From src/index.js this require uses './src/websocket' while the file itself is already in src/. That resolves to src/src/websocket which likely doesn't exist. Use a correct relative path such as require('./websocket') (or adjust the path to where the websocket module actually lives).


Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The repository contains only server-side code (handlers, websocket, utils, store). The task requires a client as well (UI to type username, send it to server, and save username in localStorage). Without client-side code, checklist items #1, #2, #3 and #15 are not fulfilled — add a client that sends JOIN_ROOM/CREATE_ROOM/etc. and saves the username in localStorage.

const server = http.createServer();

setupWebSocket(server);

const PORT = 8080;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server stores ws.username from the incoming JOIN_ROOM message here, which is fine server-side. The task requires the username to be saved in localStorage on the client — ensure you implement client-side code that saves the username to localStorage and sends it with the JOIN_ROOM message.

server.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
5 changes: 5 additions & 0 deletions src/store.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
const rooms = {
general: { id: 'general', name: 'General', messages: [] },
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This require is incorrect given this file is already in src. Requiring './src/websocket' will try to load src/src/websocket and fail with MODULE_NOT_FOUND. Change to require('./websocket') (or adjust path relative to this file).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This require is incorrect given this file is already in src. Requiring './src/websocket' will try to load src/src/websocket and fail with MODULE_NOT_FOUND. Change to require('./websocket') (or adjust path relative to this file).


module.exports = { rooms };
28 changes: 28 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const WebSocket = require('ws');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task requires both client and server parts and that the username is saved in localStorage and sent to the server. I don't see any client-side files here implementing the UI, localStorage usage, or sending username/join/create room actions. You must add the client implementation (HTML/JS) so users can type a username, save it to localStorage, and perform room actions.

const { rooms } = require('./store');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This require path is incorrect given file location. src/index.js is requiring ./src/websocket, which resolves to src/src/websocket.js and will fail. Change to require the local module: ./websocket (or the correct relative path) so the server can start.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This require path is incorrect for a file located at src/index.js: requiring './src/websocket' will try to load src/src/websocket.js and likely fail. Use the correct relative path (e.g. require('./websocket')) or adjust according to your project layout so Node can find the websocket module.

function broadcastRooms(wss) {
const roomList = Object.values(rooms).map((room) => ({
id: room.id,
name: room.name,
}));
const message = JSON.stringify({ type: 'ROOMS_LIST', rooms: roomList });

Comment on lines +8 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server expects the client to send username in the JOIN_ROOM message (ws.username = data.username). The task requires the user to be able to type a username and save it to localStorage (checklist items #3 and #15). There is no client implementation here — add a client UI that reads/saves the username to localStorage and sends it to the server on join.

wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}

function broadcastToRoom(wss, roomId, messageObj) {
Comment on lines +12 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When JOIN_ROOM creates a new room (if it doesn't exist) the code doesn't broadcast the updated rooms list to other clients. That means other connected users won't see rooms that are implicitly created by JOIN_ROOM. Consider calling broadcastRooms(wss) after creating the room (or ensure rooms are only created via CREATE_ROOM which already broadcasts).

const message = JSON.stringify(messageObj);

wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN && client.roomId === roomId) {
client.send(message);
}
Comment on lines +18 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When JOIN_ROOM auto-creates a room (inside this block) you also need to notify other connected clients about the new room. Call broadcastRooms(wss) after creating rooms[data.roomId] so the new room appears in other clients' room lists (checklist requires broadcasting rooms on auto-create).

});
}

module.exports = { broadcastRooms, broadcastToRoom };
47 changes: 47 additions & 0 deletions src/websocket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
const WebSocket = require('ws');
const { rooms } = require('./store');
const { handleMessage } = require('./handlers');

function setupWebSocket(server) {
const wss = new WebSocket.Server({ server });

const interval = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) {
return ws.terminate();
}

ws.isAlive = false;
ws.ping();
});
}, 30000);

wss.on('close', function close() {
clearInterval(interval);
});

wss.on('connection', (ws) => {
ws.isAlive = true;

ws.on('pong', () => {
ws.isAlive = true;
});

ws.send(
JSON.stringify({
type: 'ROOMS_LIST',
rooms: Object.values(rooms).map((r) => ({ id: r.id, name: r.name })),
}),
);
Comment on lines +20 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On new connections you send the rooms list but do not assign a default room or send its history. Consider setting a default ws.roomId = 'general' and sending a ROOM_HISTORY for that room so a newly connected user sees previous messages without requiring an explicit JOIN_ROOM.

Comment on lines +20 to +32
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On new connections you send the rooms list but do not assign a default room or send its history. Consider setting a default ws.roomId = 'general' and sending a ROOM_HISTORY for that room so a newly connected user sees previous messages without requiring an explicit JOIN_ROOM.


ws.on('message', (message) => handleMessage(ws, wss, message));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling handleMessage directly can allow exceptions from JSON.parse (in the handlers) to bubble up and crash the server. Wrap this call in a try/catch (or validate input) and handle parse errors gracefully so a malformed client message doesn't bring down the server.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling handleMessage directly can allow exceptions from JSON.parse (in the handlers) to bubble up and crash the server. Wrap this call in a try/catch (or validate input) and handle parse errors gracefully so a malformed client message doesn't bring down the server.


ws.on('close', () => {
ws.roomId = null;
});
});

return wss;
}

module.exports = { setupWebSocket };
Loading