This assignment builds on the work you have done for Assignment 3.
You will have noticed in the previous assignment that when you restart the server, the chatroom list is reset and the chat messages are gone. In this assignment, you will be implementing mechanisms to persist data in the server. Here is a high-level overview of what you will be doing:
- Set up a MongoDB database for the application
- Write a "driver" for interacting with the MongoDB service.
- Create REST endpoints for reading objects from the database.
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
/Database.js (ADDED in this assignment)
/server.js
/package.json
All your server-side code for this assignment goes in server.js
.
In addition, you will be implementing Database.js
- we provide the initial implementation here.
We use the following object schemas to describe the structure of objects:
Room = {
"_id": String | ObjectId(String),
"name": String,
"image" : String
}
Message = {
"username": String,
"text": String
}
Conversation = {
"_id": ObjectId(String),
"room_id": String,
"timestamp": Number,
"messages": [ Message, Message, Message, ... ]
}
For example, when we refer to a "Room
object" in the instructions below, we mean an object with fields "_id"
, "name"
, and "image"
as shown in the schema above.
-
[Dependency] As the first step, you will need to install MongoDB. You can download the latest MongoDB Community Server, provided as free and open source software at mongodb.com. Once you have installed MongoDB, also install the Mongo Shell and the MongoDB JavaScript driver from NPM:
npm install mongodb
. You can refer to the official "Quick Start" guide.- We provide a MongoDB script to initialize your database with some initial data. You are not required to use this script, but you will be responsible for managing the structure of all the database objects, and configuring the test script accordingly. To run the MongoDB script, start the MongoDB Shell by entering
mongosh
in the command line (make sure the MongoDB service is running). Once in the MongoDB Shell, load the script by typing:load("initdb.mongo")
. This script will set up 2 Collections -chatrooms
andconversations
- with some initial data. At any point during development, you can reset the database to the initial state by re-running this script.
- We provide a MongoDB script to initialize your database with some initial data. You are not required to use this script, but you will be responsible for managing the structure of all the database objects, and configuring the test script accordingly. To run the MongoDB script, start the MongoDB Shell by entering
-
(1 Points) [JS] We can use the
mongodb
library to interact with the database. However, the API provided by the module are quite low-level and we want to abstract out these details so we don't have to call the low-level methods all the time. In this assignment you will write aDatabase
object that will carry out the low-level queries. We have provided an initial implementation ofDatabase
, whose methods you will complete. Inserver.js
, declare a variabledb
, assigning aDatabase
instance initialized with the appropriate arguments (mongoUrl
is the address of the MongoDB service you want to connect to -- e.g.,mongodb://localhost:27017
by default. Look ininitdb.mongo
to find out thedbName
argument). If theDatabase
instance successfully connects to the MongoDB service, you should see in the NodeJS console:[MongoClient] Connected to ${mongoUrl}/${dbName}
. -
(5 Points) [JS] In this task, we want to make the application read the list of
Room
s from the database.- A) In
Database.js
, complete the implementation ofDatabase.prototype.getRooms
.- It does not accept any arguments.
- It should find documents in the MongoDB
chatrooms
collection, and return a Promise that resolves to an array ofRoom
objects.
- B) In
server.js
, initialize themessages
object as you have done in the previous assignment, but this time based on the results fromdb.getRooms
(initialize an empty array for eachRoom
, using theRoom
ID as the key). Note that MongoDB API will use the key_id
for the database objects and notid
. You will have to update your client side app accordingly to use_id
instead ofid
-- if you have followed the instructions closely up to this point, you will only have to modifyrefreshLobby
and the"click"
handler in theLobbyView
. - C) In
server.js
, update the/chat
endpoint'sGET
request handler to get the list of chat rooms usingdb.getRooms
method you just implemented. You can now remove thechatrooms
variable you have defined in Assignment 3. It should still return an array built from combining themessages
object with the result ofdb.getRooms
(consistent with Assignment 3). - D) In
Database.js
, complete the implementation ofDatabase.prototype.getRoom
.- It should accept a single argument
room_id
, which can either be aString
or amongodb.ObjectID
. In case the given argument is ambiguous,mongodb.ObjectID
type should get priority -- i.e., if there are 2 documents: A with ID ="11111111"
and B with ID =ObjectID("11111111")
, B should be returned. - It should find a single document in the MongoDB
chatrooms
collection that has the_id
equal to the givenroom_id
, and return a Promise that resolves to the document found. - If the document does not exist, the Promise should resolve to
null
.
- It should accept a single argument
- E) In
server.js
, create a newGET
endpoint/chat/:room_id
whereroom_id
is a dynamic route parameter (see Express.js documentation). In the request handler, search for theRoom
in the database using thedb.getRoom
method you just implemented in Task 1.D. If theRoom
was found, send it back to the client. If theRoom
was not found, return HTTP 404 with an error message (e.g.,"Room X was not found"
)
- A) In
-
(3 Points) [JS] In this task, we want to make the application write a
Room
to the database.- A) In
Database.js
, complete the implementation ofDatabase.prototype.addRoom
.- It should accept a single argument
room
, which is aRoom
object with or without the_id
field. - It should insert a
Room
document in the MongoDBchatrooms
collection. - It should return a
Promise
that resolves to the newly insertedRoom
object, including the_id
as assigned by the MongoDB service. - If the
name
field in theroom
object is not provided, thePromise
should reject with anError
.
- It should accept a single argument
- B) In
server.js
, update the/chat
endpoint'sPOST
request handler to use thedb.addRoom
method you have just defined in Task 3. The error handling behaviour (e.g., sendingHTTP 400 Bad Request
upon malformed payload) should remain unchanged and themessages
object should still be updated as it were in the previous assignment. By the end of this task, the list of chatrooms should be persistent even after you restart the server.
- A) In
-
(7 Points) [JS] The list of chat rooms are now persistent, but all the chat message are ephemeral. We need to be clever about saving the chat messages, because they are created very frequently - writing to the database for each message will create a lot of overhead, potentially increasing the risk of a server crash. To handle this more efficiently, we will deal with chat messages in terms of "conversation" blocks - you could think of this as a buffering mechanism. A "conversation" will contain a fixed-length array of messages, and whenever the server accumulates the predefined number of text messages, it will save the block in the database. If the server crashes, it would lose messages in the current block, but any messages before that should be persistent.
- A) In
Database.js
, complete the implementation ofDatabase.prototype.addConversation
.- It accepts a single argument
conversation
, which is aConversation
object. - It should insert a
Conversation
document in the MongoDBconversations
collection. - It should return a
Promise
that resolves to the newly inserted Mongo Document. - If any of the fields (
room_id
,timestamp
,messages
) is not provided, thePromise
should reject with anError
.
- It accepts a single argument
- B) In
Database.js
, complete the implementation ofDatabase.prototype.getLastConversation
.- It accepts 2 arguments:
room_id
and optionalbefore
. - If
before
is not given, use the current UNIX time in milliseconds given byDate.now
. - It should find the
Conversation
object withroom_id
field equal to the givenroom_id
argument, andtimestamp
less than the givenbefore
argument. If multipleConversation
objects are found, it should select the one whosetimestamp
is closest tobefore
. - It should return a
Promise
that resolves to theConversation
object found. - The
Promise
should resolve tonull
if noConversation
was found.
- It accepts 2 arguments:
- C) In
server.js
declare a global constantmessageBlockSize
and assign a number (set a small number like10
during development, so you can test it easily). This number will indicate how many messages to include in a conversation. - D) In
server.js
, update themessage
handler for thebroker
client WebSocket, performing the following in addition to what it was doing in Assignment 3:- If the length of the corresponding
messages[message.roomId]
array is equal tomessageBlockSize
after pushing the new message, create a newConversation
object and add to the database by callingdb.addConversation
. Thetimestamp
of the newConversation
should be UNIX time in milliseconds. After successfully saving theConversation
into the database, empty the corresponding messages array to collect new messages.
- If the length of the corresponding
- E) In
server.js
, define a newGET
endpoint/chat/:room_id/messages
, whereroom_id
is the room ID. This endpoint should return aConversation
based on theroom_id
parameter and thebefore
parameter in the query string. For example, if aGET
request is made to/chat/room-1/messages?before=1604188800000
, the server should return aConversation
withroom_id
equal to"room-1"
andtimestamp
less than1604188800000
. - F) In the
Service
object inapp.js
(client-side), define a new functiongetLastConversation
, which accepts 2 arguments:roomId
andbefore
. It should simply make an AJAXGET
request to the/chat/:room_id/messages
endpoint, encoding thebefore
parameter as a query string in the URL. It should return aPromise
that resolves to theConversation
object returned by the server.
- A) In
-
(8 Points) [JS] By now, messages should persist at least up to the last
Conversation
block. In this task, we will create a mechanism to continuously read theConversation
blocks as we scroll up in the chat view - a UI mechanism commonly refered to as "infinite scrolling". To achieve this, we will make some changes to theRoom
andChatView
classes, and use a generator function to load the messages from the server.- A) In the
Room
class inapp.js
, define a new method namedaddConversation(conversation)
. This method is similar toaddRoom
, except that here you want to insert the given messages at the beginning of theRoom.messages
array. Make sure the order of messages is chronological. After inserting the messages to theRoom
object, call theonFetchConversation(conversation)
event listener callback, which will be assigned later by theChatView
. This callback is used to notify theChatView
that new data is available (similar to howonNewMessage
works). - B) In
app.js
, define a generator function namedmakeConversationLoader(room)
. The generator function will "remember" the last conversation fetched, and incrementally fetch the conversations as the user scrolls to the top of the chat view, until there is no more conversation blocks to be read.- i. In this function, you should use a local variable to keep track of the last conversation block fetched (or just the timestamp of the last conversation).
- ii. Then, using a conditional loop, use the
Service.getLastConversation
method you implemented in Task 4.F to fetch the conversation block from the server. The last conversation read has information needed for determining the next query parameters. - iii. Just before you make a request to the server, set the
room.canLoadConversation
property tofalse
. This property has not been defined yet (you will do that next), but you will use this boolean flag to indicate whether more data can be loaded and if the request is in progress. - iv. Upon fetching a result, update the local variable (the one you're using to maintain the generator state), and set the
canLoadConversation
flag totrue
. If aConversation
object was returned, call theroom.addConversation
method. If there is no result, leave thecanLoadConversation
flag as-is (false
), and the function should terminate. - v. This generator function should
yield
aPromise
object that resolves to the conversation data if exists, and resolves tonull
otherwise.
- C) In the
Room
object's constructor, create a Generator instance by callingmakeConversationLoader
, passing in theRoom
instance itself as the argument. Assign this Generator to theRoom
instance'sgetLastConversation
property. Also in the constructor, define a boolean variable namedcanLoadConversation
and initialize it totrue
. You will use inChatView
to determine whether to allow making further requests or not. - D) In
ChatView
, you will need to trigger the entire fetch-update-render cycle when the mouse is scrolled up in the chat view (we assume thatthis.chatElem
has a minimum and maximum height, and it has a scrollbar in they
direction). Attach awheel
event listener in thethis.chatElem
element. Now that each room has a generator for loading conversations, this event listener should be incredibly simple. Invoke the generator'snext
function, only if the following conditions are met:- The scroll is at the top of the view
- Mouse scroll direction is "up"
this.room.canLoadConversation
istrue
- E) In the
setRoom
method ofChatView
, create an anonymous function and attach it to thethis.room
instance'sonFetchConversation
property -- this is the callback you use to receive new conversation blocks. The behaviour is almost the same asonNewMessage
, except that you will be attaching multiple messages at once, at the beginning of the DOM. After you render the messages, you will have to adjust the scroll position, so you may want to keep track of the scroll height before you render the messages. More specifically, if the scroll height ishb
pixels before rendering, the scroll height will increase toha
pixels after adding the messages. The scroll's vertical position should be atha - hb
after rendering.
- A) In the
The testing infrastructure is identical to the one used in the previous assignment, except for the URL of the scripts used. Therefore, we will not elaborate on how to use the test script's API; you can refer to the "Testing" section in the previous assignment, and update the URLs of the test scripts accordingly.
-
Client-side:
- URL:
http://52.43.220.29/cpen322/test-a4.js
(this goes inindex.html
) - Default Values (can be customized with
cpen322.setDefault(key, val)
):testRoomId
:'room-1'
,image
:'assets/everyone-icon.png'
,webSocketServer
:'ws://localhost:8000'
- Exported closure variables:
lobby
,chatView
- URL:
-
Server-side:
- URL:
http://52.43.220.29/cpen322/test-a4-server.js
(this goes in thecpen322.connect
function insideserver.js
. You still need to use the samecpen322-tester.js
module from Assignment 3) - Exported global variables:
app
,db
,messages
,messageBlockSize
- URL:
-
NOTE: Unlike the test scripts in the previous assignments, this test script will not clean up the test objects it creates during the test. Previously, we could safely delete the test objects because we know that the objects were all in-memory and definitely belong to the application. In this assignment, the application connects to a database, which we consider as "external" to the application. Since we cannot make assumptions about the database service you're connecting to, we refrain from performing destructive operations. (Most of you would probably be running your own fresh database service, but we don't neglect the chance that some of you might be using an existing/shared database or connecting to a cloud-hosted service).
There are 5 tasks for this assignment (Total 24 Points):
- Task 1: 1 Points
- Task 2: 5 Points
- Task 3: 3 Points
- Task 4: 7 Points
- Task 5: 8 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 20, 2022 23:59:59 PST