This assignment builds on the work you have done for Assignment 2.
In this part, you will be implementing mechanisms to exchange data with the server. Here is a high-level overview of what you will be doing:
- Implement the client-side functionalities to read and update the list of chat rooms from the server via AJAX requests.
- Implement a WebSocket client to send and receive messages from the WebSocket server.
- Implement a WebSocket server to act as a message broker between the client applications.
We continue to prohibit the use of third-party JavaScript frameworks, except those specifically mentioned, such as Express.js on the server side.
Same as previous assignments. If you still have client/chat.html
and client/profile.html
, you can delete them.
/client/
/assets/
/index.html
/style.css
/app.js
/server.js
/package.json
All your server-side code for this assignment goes in server.js
.
During development, you will need to restart the server after you make changes to your server-side code. This can be distracting when you're making frequent changes. To make your development experience pleasant, you can use an NPM module named nodemon
, which watches your files and restarts the server automatically. We allow the use of this module from this assignment onwards.
When you install this module, make sure you install it with the --save-dev
flag to indicate that this module is only used during development, or --no-save
flag to exclude it entirely from the application dependencies list.
You can install this package like below:
npm install --save-dev nodemon
and then use it to serve your application:
nodemon server.js
Depending on your NodeJS installation, you may have to add the node_modules/.bin
directory to your PATH
environment variable.
-
(5 Points) [JS (
app.js
)] In this task, you will fetch the list of rooms (as a JSON object) from the server by making an AJAX request. All the subtasks (1.A ~ 1.E) are to be done inapp.js
.- A) Define an object (associative array) in the global scope named
Service
. This object will store functions you can call to make different requests to the server. - B) In the
Service
object, add anorigin
property to store the URL of your server as a string. The format should be the same aswindow.location.origin
. In case you do not host your server locally (i.e., localhost), the test script checks your value againstwindow.location.origin
- C) In the
Service
object, define a function namedgetAllRooms
that does not take any arguments.- i. This function should make an AJAX request to
Service.origin + "/chat"
URL and return aPromise
that resolves to the JSON response data. Note the lack of "#" - this URL is a server-side endpoint - ii. In case of client-side error, the
Promise
should reject with the error that was caught. - iii. In case of server-side error (i.e., the server returns a response that does not have HTTP status 200), the
Promise
should reject with a newly createdError
containing any message from the server. - You can either use the
XMLHttpRequest
API, or thefetch
API that most modern browsers support.
- i. This function should make an AJAX request to
- D) Now that you can read the list of rooms from the server, you will use the
getAllRooms
function to fetch the list of rooms, then render it in your view dynamically. Note that thisgetAllRooms
function is asynchronous, and you will not obtain the list of rooms in the return value. Rather, the returned list will be available as the first argument in the callback you pass toPromise.prototype.then(callback)
method.- i. Update the
Lobby
constructor so thatrooms
property is set to an empty object. - ii. Define a function named
refreshLobby
inside themain
function, which takes zero arguments. - iii. In
refreshLobby
, call thegetAllRooms
function you just created to make an AJAX request to the server. When the returnedPromise
resolves, updatelobby.rooms
object by iterating through the array of rooms just received from the server. Note that the server returns anArray
whilelobby.rooms
is anObject
(associative array). Make sure you do not replace thelobby.rooms
object itself, but rather update each individualRoom
instances inside it. If aRoom
already exists, update the name and image. If aRoom
does not exist, calllobby.addRoom
method to add the new room. - iv. Call
refreshLobby
once inside themain
function.
- i. Update the
- E) In the
main
function, create a timer usingsetInterval
, and periodically refresh the list of chat rooms by callingrefreshLobby
.
- A) Define an object (associative array) in the global scope named
-
(4 Points) [JS (
server.js
)] Now that you can request the server for data, you will create the server-sideGET
endpoint to handle the requests.- A) In
server.js
, create a variable namedchatrooms
and then assign an Array of objects (Associative Arrays) with the following properties:id
,name
,image
. You can assign arbitrary values to each of the properties, given thatid
is unique. These objects are similar to theRoom
objects, but without any methods. Note the lack ofmessages
property - we will use another object to store the messages separately. - B) In
server.js
, create a variable namedmessages
and then assign an Associative Array. This object will store the messages for each of the rooms, using the roomid
as the key. For each of the rooms in thechatrooms
array, create a corresponding empty array in themessages
object, indexed by the roomid
. - C) Define an Express.js
GET
endpoint at the path/chat
using the Express.js API methodroute
. You can refer to the Express.js API documentation (v.4) to learn how to use it. TheGET
endpoint should return an array of objects with the propertiesid
,name
,image
,messages
, built from thechatrooms
array and themessages
object. Make sure you do not modify the original objects insidechatrooms
array.
- A) In
-
(4 Points) [JS] Previously, when you refreshed the page on your client-side application, the array of chatrooms would be reset because any updates to the array is local to the browser. In this task, you will make an AJAX
POST
request to update the data on the server.- A) In the
Service
object in the client-side app (app.js
), define a function namedaddRoom
that takes a single argumentdata
.data
will be an object (associative array) containing 2 fields:name
, andimage
. In theService.addRoom
function, make aPOST
request to theService.origin + "/chat"
endpoint, with the givendata
in the request payload. Set theContent-Type
header toapplication/json
, and make sure you serialize the object into a JSON string. - B) In
server.js
, define aPOST
endpoint in the sameroute
as theGET
endpoint from Task 2. ThePOST
request handler should inspect thedata
object sent by the client, validate that it has at least thename
field, then proceed with the following:- i. If the data does not have a
name
field, then respond with HTTP status 400 and provide an error message. - ii. If the data does have a
name
field, then create an associative array - with fieldsid
,name
, andimage
- to represent a room. Generate a new unique ID for this object and assign it to theid
field. For thename
andimage
fields, assign the data received from the client. After you create this object, add it to thechatrooms
array. In addition, add an empty array in themessages
object, using the newly generated room ID as the key. Finally, respond with HTTP status 200, sending the newly created room object as a JSON string.
- i. If the data does not have a
- C) As a last step, we need to call the
Service.addRoom
function from the client-side app when the "Create Room" button is clicked. Locate theclick
event handler you created from assignment 2 (in theLobbyView
class), in which you calledthis.lobby.addRoom
. Update the handler to callService.addRoom
instead, passing in the appropriate arguments. Only after the server returns a response without errors, call thethis.lobby.addRoom
method with the appropriate arguments.
- A) In the
-
(3 Points) [JS (
app.js
)] By now, the lobby view should be fully functional. In this task, you will implement the chat functionality, usingWebSocket
to immediately relay information between client applications. For AJAX requests, you would have noticed that the server cannot send messages to the client unless the client sends a request first. This is a typical client-server request-response protocol. In modern web applications, we want a more flexible bi-directional communication where the server is also able to push data to the client.WebSocket
s allow you to do just that. Using aWebSocket
object on the client side, the client application can send messages to the server as usual, but can also "listen" to messages from the server. On the server side, there would be a WebSocket server, that maintains connections to multipleWebSocket
clients. In this task, you will first create the WebSocket client.- A) In the
main
function, create a variable namedsocket
and assign a newWebSocket
instance, connecting to the appropriate WebSocket server endpoint (ws://localhost:8000
if you used port 8000). Since you don't have the server side implemented yet, you may encounter connection errors. This is normal. If you want to just test your connection, you can connect to the WebSocket test server at99.79.42.146:8000
- B) Attach a
message
event handler on theWebSocket
instance. Inside the event handler, you will need to do the following:- i. Parse the given message passed in the argument. The message is a serialized JSON string and will have 3 fields:
roomId
,username
, andtext
. - ii. Based on the
roomId
, get the appropriateRoom
object usingLobby.getRoom
method you implemented from assignment 2. Then add the message in theRoom
object usingRoom.addMessage
method.
- i. Parse the given message passed in the argument. The message is a serialized JSON string and will have 3 fields:
- C) Modify the contructor of
ChatView
class to accept a single argumentsocket
. It should store a reference to the givensocket
object in its property with the same namesocket
. Then, modify the existing instantiation ofchatView
to use thesocket
object created in Task 4A. - D) Modify the
sendMessage
method ofChatView
. In addition to all the code you have from assignment 2 (you won't need to remove any code, provided you have implemented it correctly in the previous assignment), send the message to the server using thethis.socket
object. The message should be an object with 3 fields:roomId
,username
,text
. Make sure you serialize the object into a JSON string.
- A) In the
-
(4 Points) [JS (
server.js
)] In this task, you will make the "chat view" fully functional by implementing aWebSocket
server to act as a message broker between the clients.- A) Install the
ws
module from NPM (this is one of the 3rd-party modules we require you to use.ws
is the de-facto WebSocket module providing a high-level API for essential WebSocket operations. While you can technically implement a WebSocket using the Node.js built-innet
module, conforming to the WebSocket protocol specifications requires a lot of effort, and we do not want to reinvent the wheel here). - B) Inside
server.js
, require thews
library. Then, create a variable namedbroker
and assign aws.Server
instance. Use a different port (e.g., 8000) than theexpress
server. You can refer to thews
API documentation on Github. The client-side test script will use the addressws://localhost:8000
by default. If you wish to use a different address, configure the test script by callingcpen322.setDefault('webSocketServer', URL)
inapp.js
- C) The WebSocket server's role is to simply act as a message broker between the clients. For example, assuming 3 clients - A, B, and C - are connected, if client A sends a message, it will relay the message to clients B and C. Whenever a new client connects to the WebSocket server, a
connection
event is raised. Theconnection
event handler for thews.Server
object receives the newly connected client socket as the argument. Inside theconnection
event handler, you define what to do with each client. When any client sends a message (message
event), iterate through thebroker.clients
set and forward the message to each client, except to the client that sent the message. Additionally, parse the message and then push it into the corresponding array in themessages
object you created in Task 2. Remember that themessages
object stores arrays of messages for each room, indexed by theroomId
. - You can verify that the chat functionality now works by opening multiple tabs in the browser and entering some text in the chat view. Alternatively, if your smartphone and laptop/desktop are in the same home network, you can test it using your smartphone by opening the application at your laptop/desktop's local IP
- A) Install the
Now that you're working on a "full-stack" implementation (client & server), there are 2 different test scripts, one for the server and another for the client. The 2 scripts are meant to work together, so you will need to set up both in order for the tests to work properly.
Insert the following script tag within the head
tag of your page, BEFORE your app.js
. It is important that the test script is loaded synchronously before your application script for it to work correctly.
<script src="http://99.79.42.146/cpen322/test-a3.js" type="text/javascript"></script>
In addition to adding the script tag above, you will need to inject some tester code to expose some of the objects accessed during the test. The testing interface is designed this way because there is no way for a third-party script (i.e. the test script) to observe closure variables unless you expose them explicitly.
For example, in Task 1 you are asked to declare a local function refreshLobby
inside the main
function. If you do not expose the name refreshLobby
from within the main
function, there is simply no way for the tester to observe refreshLobby
since it is a closed variable. To make such variables available, you must invoke the cpen322.export
function as shown below.
// assuming we want to expose `refreshLobby` and `lobby` from inside `main`
// traditional syntax
function main(){
/* other code */
cpen322.export(arguments.callee, {
refreshLobby: refreshLobby,
lobby: lobby
});
}
// concise ES6 property shorthand syntax
function main(){
/* other code */
cpen322.export(arguments.callee, { refreshLobby, lobby });
}
You will see a red button on the top-right corner of your web page. Click it to test your code. Watch out for any alert messages or console output, which tell you any missing components/functionalities. You are responsible for ensuring that all the functionalities above are implemented correctly - the tests are only there to help you. We reserve the right to test your code with other test cases than the above.
The test script uses certain default values during the test.
- WebSocket server URL - the test script expects that your WebSocket server is at
"ws://localhost:8000"
. - Default room image URL - the test script expects the default image URL to be
"assets/everyone-icon.png"
.
To set a different default value for your application, you can use cpen322.setDefault
as shown below:
cpen322.setDefault("webSocketServer", YOUR_SERVER_URL);
cpen322.setDefault("image", YOUR_IMAGE_URL);
Download this cpen322-tester.js
script and place it in your project directory. Then in server.js
, import the tester module as shown below:
// assuming cpen322-tester.js is in the same directory as server.js
const cpen322 = require('./cpen322-tester.js');
/* your code */
// at the very end of server.js
cpen322.connect('http://99.79.42.146/cpen322/test-a3-server.js');
cpen322.export(__filename, { app });
Similar to the client-side script, you will need to call some tester code to expose some of the objects accessed during the test. Note that the first argument to cpen322.export
is __filename
and not arguments.callee
.
There are 5 tasks for this assignment (Total 20 Points):
- Task 1: 5 Points
- Task 2: 4 Points
- Task 3: 4 Points
- Task 4: 3 Points
- Task 5: 4 Points
Copy the commit hash from Github and enter it in Canvas.
For step-by-step instructions, refer to the tutorial.
These deadlines will be strictly enforced by the assignment submission system (Canvas).
- Sunday, Nov 7, 2021 23:59:59 PST