Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
1d38a28
backend boilerplate
RaihanSharif Sep 20, 2026
919a6d9
frontend boilerplate
RaihanSharif Sep 20, 2026
d5af26b
define message shape
RaihanSharif Sep 20, 2026
5200c6f
post new message
RaihanSharif Sep 20, 2026
ef6d1e5
Finish non-interactive backend
RaihanSharif Sep 21, 2026
507640d
Merge branch 'main' into frontend
RaihanSharif Sep 21, 2026
d7df7b5
complete frontend layout and from
RaihanSharif Sep 21, 2026
e42720b
non-live chat app done
RaihanSharif Sep 21, 2026
49ceee5
dockerfile for deployment
RaihanSharif Sep 21, 2026
8f48f94
environment variables
RaihanSharif Sep 21, 2026
fe110b9
install dotenv
RaihanSharif Sep 21, 2026
40dbadd
fetch from production backend
RaihanSharif Sep 21, 2026
b5aec51
short polling
RaihanSharif Sep 21, 2026
d31c582
optional long polling
RaihanSharif Sep 21, 2026
afd1068
no live update likes
RaihanSharif Sep 21, 2026
80805e0
sort of working live update reactions
RaihanSharif Sep 21, 2026
be3e08f
Define message class with autoincrement id
RaihanSharif Sep 22, 2026
4306653
rebuild using integer id and Message class
RaihanSharif Sep 22, 2026
590918b
store the id of the last fetched message in state.
RaihanSharif Sep 22, 2026
0f9a390
add validation for getMessages
RaihanSharif Sep 22, 2026
630a3ae
no sleep before next fetch
RaihanSharif Sep 22, 2026
0f632b0
Errors and validatoin
RaihanSharif Sep 22, 2026
034791b
Use REST style API endpoint
RaihanSharif Sep 22, 2026
8b2c69d
frontend error handling
RaihanSharif Sep 22, 2026
09d08ab
production backend url
RaihanSharif Sep 22, 2026
4f625eb
Clean up model
RaihanSharif Sep 29, 2026
065aa34
Use event streaming and REST for messages
RaihanSharif Sep 29, 2026
06b6430
use Map instead of list to store messages.
RaihanSharif Sep 29, 2026
ef22de1
addMessage takes in username and msg_body as separate parameters
RaihanSharif Sep 29, 2026
ef2383b
when message sent, immediately add to DOM
RaihanSharif Sep 29, 2026
b46622d
regular polling of the event stream
RaihanSharif Sep 29, 2026
67ff4c3
event driven likes/dislikes
RaihanSharif Sep 29, 2026
90417f9
update model and app to better handle message ID errors
RaihanSharif Sep 29, 2026
69f05c2
code clean up and comments
RaihanSharif Sep 29, 2026
2e7dc03
Merge pull request #1 from RaihanSharif/event-driven
RaihanSharif Sep 29, 2026
8fa296a
production backend URL
RaihanSharif Sep 29, 2026
1756f54
return after catching failed message send
RaihanSharif Sep 30, 2026
ee4caca
basic version working
RaihanSharif Oct 1, 2026
8717b46
send messages from client cursor so that there are no missing messages.
RaihanSharif Oct 2, 2026
b551c70
update subscribe logic and add restarting websocket from client side
RaihanSharif Oct 2, 2026
e801458
remove unnecessary check
RaihanSharif Oct 2, 2026
425817f
remove comment
RaihanSharif Oct 2, 2026
5f28f18
Refactor: websocket version of backend.
RaihanSharif Oct 5, 2026
2180255
extract common code out of web socket frontend script
RaihanSharif Oct 5, 2026
806633e
extract out express application
RaihanSharif Oct 5, 2026
3361d8e
polling backend done
RaihanSharif Oct 5, 2026
f98b3e0
separate polling & websocket frontend and backend
RaihanSharif Oct 5, 2026
6ffe037
Merge pull request #2 from RaihanSharif/refactor
RaihanSharif Oct 5, 2026
2dc2778
production URL
RaihanSharif Oct 5, 2026
a548c4d
update dockerfile to run the polling app
RaihanSharif Oct 5, 2026
d484bf9
stop tracking vs code settings
RaihanSharif Oct 9, 2026
59383a6
Comments to explain waiters and subscribers.
RaihanSharif Oct 9, 2026
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
148 changes: 148 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.*
!.env.example

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist
.output

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp directory
.temp

# Sveltekit cache directory
.svelte-kit/

# vitepress build output
**/.vitepress/dist

# vitepress cache directory
**/.vitepress/cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# Firebase cache directory
.firebase/

# TernJS port file
.tern-port

# Stores Visual Studio Code versions used for testing Visual Studio Code extensions
.vscode-test

# pnpm
.pnpm-store

# yarn v3
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions

# Vite files
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
.vite/

CORS_ORIGINS

#VS Code
.vscode
5 changes: 5 additions & 0 deletions chat-app/backend/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.env
.env.*
.git
node_modules
npm-debug.log
7 changes: 7 additions & 0 deletions chat-app/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
FROM node:26.7.0-alpine
ENV NODE_ENV=production
WORKDIR /app
COPY . .
RUN npm ci --omit=dev
USER node
ENTRYPOINT ["node", "pollingApp.js"]
125 changes: 125 additions & 0 deletions chat-app/backend/EventStream.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
class EventStream {
constructor() {
this.events = [];
this.sequence = 0;

// waiters are HTTP polling requests
this.waiters = new Set();

// subscribers are websocket connections.
this.subscribers = new Set();
}

/**
* Adds an event to the event stream. {sequnce, type, timestamp, data}.
Comment thread
RaihanSharif marked this conversation as resolved.
*
* @param {"message.liked" | "message.disliked" | "message.created"} type - The type of event (e.g. "like").
* @param {*} data - The message that was added or changed, and the changed fields.
* @returns The event object, with a sequence number
*/
append(type, data) {
const event = {
sequence: ++this.sequence,
type,
timestamp: Date.now(),
data,
};

this.events.push(event);
this.notifyWaiters();
this.notifySubscribers();
return event;
}

/**
*
* @param {number} sequence - returns events with sequence number > given sequence number.
* @returns list of events.
*/
getAfter(sequence) {
return this.events.filter((event) => event.sequence > sequence);
}

// websocket subscriber
subscribe(callback) {
const subscriber = {
cursor: null,
callback: callback,
};
this.subscribers.add(subscriber);
return subscriber;
}

unsubscribe(subscriber) {
this.subscribers.delete(subscriber);
}

// WebSocket subscribers
notifySubscribers() {
for (const subscriber of this.subscribers) {
if (subscriber.cursor === null) {
continue;
}

const events = this.getAfter(subscriber.cursor);

if (events.length === 0) {
continue;
}

try {
subscriber.callback(events);
} catch (err) {
console.error("subscriber failed", err);
}
}
}

/**
* Polling (HTTP) connections waiting for new events
* If no new events are available, keeps connection open for specified time.
* @param {number} after - returns events after this sequence number.
* @param {*} timeout - How long to wait for a new event before closing the connection.
* @returns A promise which resolves with a list of new events or empty list
* if timer expires.
*/
waitForEvents(after, timeout = 30_000) {
const existing = this.getAfter(after);

if (existing.length > 0) {
return Promise.resolve(existing);
}

return new Promise((resolve) => {
const waiter = {
after,
resolve,
timer: setTimeout(() => {
this.waiters.delete(waiter);
resolve([]);
}, timeout),
};

this.waiters.add(waiter);
});
}

/**
* Sends new events for waiting HTTP requests, if there are any events to send.
*/
notifyWaiters() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the difference between a "waiter" and a "subscriber"?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

In this code waiters are the polling requests, and subscribers are WebSocket connections.

I was going to create a more general "subscriber" interface so that both WebSocket and HTTP, but I didn't have the time to get it done.

I've added some comments to make this clearer.

for (const waiter of this.waiters) {
const events = this.getAfter(waiter.after);

if (events.length === 0) {
continue;
}

clearTimeout(waiter.timer);
this.waiters.delete(waiter);
waiter.resolve(events);
}
}
}

export { EventStream };
17 changes: 17 additions & 0 deletions chat-app/backend/Message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export class Message {
static #nextId = 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain what the static keyword here means?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is a class field, not an instance field. It belongs to the class itself rather than any one individual instance of the message class.


/**
*
* @param {string} username the message sender
* @param {*} msg_body text content of the message
*/
constructor(username, msg_body, likes = 0, dislikes = 0) {
this.id = Message.#nextId++;
this.username = username;
this.msg_body = msg_body;
this.createdAt = new Date();
this.likes = likes;
this.dislikes = dislikes;
}
}
15 changes: 15 additions & 0 deletions chat-app/backend/errorClasses.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}

class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = "NotFoundError";
}
}

export { ValidationError, NotFoundError };
Loading
Loading