Skip to content

Commit

Permalink
feat: read command and response
Browse files Browse the repository at this point in the history
- use repository in app
- test with simple html
  • Loading branch information
hhow09 committed Jan 22, 2025
1 parent 38c4c6e commit 1cb3f49
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 39 deletions.
19 changes: 18 additions & 1 deletion backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import pino from "pino";
import { ChatServer } from "./chat-server";
import CommandService from "./command-service";
import ChatRepo, { ChatSession } from "./repositories/chat-repo";
import { MongoClient, Collection } from "mongodb";

// config, consider to use env variables in the future
const uri = process.env.MONGODB_URI || "mongodb://localhost:27017";
const dbName = "math-chat";
const collectionName = "chat-session";

const logger = pino();
const chatServer = new ChatServer(logger, 3000);
const collection = getMongoCollection(uri, dbName, collectionName);
const commandService = new CommandService(logger, new ChatRepo(collection));
const chatServer = new ChatServer(logger, commandService, 3000);
chatServer.listen();

function getMongoCollection(uri: string, dbName: string, collectionName: string): Collection<ChatSession> {
const mongoClient = new MongoClient(uri);
const db = mongoClient.db(dbName);
const collection = db.collection<ChatSession>(collectionName);
return collection;
}
29 changes: 28 additions & 1 deletion backend/src/chat-server.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import express, { Request, Response } from 'express';
import { Logger } from "pino";
import { Server, Socket } from "socket.io";
import { ICommandService } from './command-service';

export class ChatServer {
port: number;
app: express.Application;
logger: Logger;
commandService: ICommandService;

constructor(logger: Logger, port: number = 3000) {
constructor(logger: Logger, commandService: ICommandService, port: number = 3000) {
this.port = port;
this.logger = logger;
this.app = express();
this.setupAppRoutes();
this.commandService = commandService;
}
private setupAppRoutes() {
// health check
Expand All @@ -25,12 +28,36 @@ export class ChatServer {
io.on('connection', (socket: Socket) => {
const sessionLogger = this.logger.child({ session: socket.id });
sessionLogger.info(`Connected client session ${socket.id} on port ${this.port}`);
socket.on('operation', async (operation: string) => {
sessionLogger.info(`Received operation: ${operation}`);
try {
const result = await this.commandService.evaluateAndSave(socket.id, operation);
sessionLogger.info(`Returning result: ${result}`);
socket.emit('result', result);
} catch (error) {
this.handleSocketError(socket, sessionLogger, "operation", error as Error);
}
});
socket.on('history', async () => {
try {
const history = await this.commandService.getHistory(socket.id);
sessionLogger.info(`Returning history: ${JSON.stringify(history)}`);
socket.emit('history', history);
} catch (error) {
this.handleSocketError(socket, sessionLogger, "history", error as Error);
}
});
socket.on('disconnect', () => {
sessionLogger.info('a client disconnected');
});
});
}

private handleSocketError(socket: Socket, logger: Logger, operation: string, error: Error) {
logger.error(`Error during operation: ${operation}`, error);
socket.emit('error', error instanceof Error ? error.message : String(error));
}

public listen() {
const httpServer = this.app.listen(this.port,
() => { console.log(`Server listening on port ${this.port}`) }
Expand Down
7 changes: 6 additions & 1 deletion backend/src/command-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { evaluate as evaluateMathjs } from 'mathjs';
import { CommandAndResult } from "./entities/command-result.entity";
import { IRepository } from "./repositories";

class CommandService {
export interface ICommandService {
evaluateAndSave(clientId: string, expression: string): Promise<string>;
getHistory(clientId: string): Promise<CommandAndResult[]>;
}

class CommandService implements ICommandService {
private operators = new Set(['+', '-', '*', '/']);
private repository: IRepository;
private logger: Logger;
Expand Down
5 changes: 3 additions & 2 deletions backend/src/repositories/chat-repo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ describe("ChatRepo", () => {
expect(history2).toEqual([expected[1]]);
});

it("should throw an error if the chat session is not found", async () => {
await expect(repo.getLatest(clientId)).rejects.toThrow("Chat session not found");
it("should return an empty array if the chat session is not found", async () => {
const history = await repo.getLatest(clientId);
expect(history).toEqual([]);
});


Expand Down
2 changes: 1 addition & 1 deletion backend/src/repositories/chat-repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class ChatRepo implements IRepository {
public async getLatest(clientId: string): Promise<CommandAndResult[]> {
const chatSession = await this.db_client.findOne({ clientId });
if (!chatSession) {
throw new Error("Chat session not found");
return [];
}
return chatSession.history;
}
Expand Down
86 changes: 53 additions & 33 deletions frontend/test_index.html
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
<!-- https://github.com/socketio/socket.io-minimal-example/blob/main/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Socket.IO Fiddle</title>
</head>

<body>
<h2>Status: <span id="status">Disconnected</span></h2>
<h2>Messages:</h2>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Chat</title>
</head>
<body>
<ul id="messages"></ul>

<form id="chat-form">
<input id="chat-input" type="text" autocomplete="off">
<button>Send</button>
</form>
<button id="history-button">History</button>
<ul id="history"></ul>

<script src="http://localhost:3000/socket.io/socket.io.js"></script>
<script>
const status = document.getElementById("status");
const messages = document.getElementById("messages");

const appendMessage = (content) => {
const item = document.createElement("li");
item.textContent = content;
messages.appendChild(item);
};
<script >
const socket = io('http://localhost:3000');
const form = document.getElementById('chat-form');
const input = document.getElementById('chat-input');
const messages = document.getElementById('messages');

const socket = io("http://localhost:3000");

socket.on("connect", () => {
status.innerText = "Connected";
appendMessage(`event: connect | session id: ${socket.id}`);
form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value) {
socket.emit('operation', input.value);
appendMessage(`You: ${input.value}`, 'blue');
input.value = '';
}
});

socket.on("connect_error", (err) => {
appendMessage(`event: connect_error | reason: ${err.message}`);
socket.on('result', (msg) => {
appendMessage(`Bot: ${msg}`, 'green');
});

socket.on("disconnect", (reason) => {
status.innerText = "Disconnected";
appendMessage(`event: disconnect | reason: ${reason}`);
socket.on('error', (error) => {
appendMessage(`Bot: [Error] ${error}`, 'red');
});

socket.onAny((event, ...args) => {
appendMessage(`event: ${event} | arguments: ${args}`);
function appendMessage(message, color) {
const item = document.createElement('li');
item.textContent = message;
item.style.color = color;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
}

const historyButton = document.getElementById('history-button');

historyButton.addEventListener('click', () => {
socket.emit('history');
socket.on('history', (history) => {
const historyList = document.getElementById('history');
historyList.innerHTML = '';
history.forEach(item => {
const li = document.createElement('li');
li.textContent = `${item.expression} = ${item.result}`;
historyList.appendChild(li);
});
});
});

</script>
</body>
</html>
</body>
</html>

0 comments on commit 1cb3f49

Please sign in to comment.