WebSocket Integration
This section will focus on integrating the WebSocket plugin into our application, allowing real-time communication between the server and clients. We shall use this to power our chat functionality and to broadcast how many users are connected to the web page.
Monitoring WS Connections.
At the start we already created our main.ts
file that has the WebSocket plugin registered, and we created our API controller, well let's handle extend the api.ts
controller to deal with our WebSocket connections.
import { Body, Controller, Delete, Get, Params, Patch, Post, Put } from '@sodacore/http';
import { PrismaClient } from '../../prisma/prisma/client';
import { Inject } from '@sodacore/di';
import { WsConnections } from '@sodacore/ws';
import { Hook } from '@sodacore/core';
@Controller('/api')
export default class TodoApiController {
@Inject('prisma') private readonly prisma!: PrismaClient;
@Inject() private readonly connections!: WsConnections;
@Hook('wsOpen')
public async onWsOpen() {
this.connections.broadcast('userCount', {
count: this.connections.getConnectionCount(),
});
}
@Hook('wsClose')
public async onWsClose() {
this.connections.broadcast('userCount', {
count: this.connections.getConnectionCount(),
});
}
@Get('/messages')
public async getMessages() {
return this.prisma.chatMessages.findMany({
orderBy: {
createdAt: 'asc',
},
});
}
@Get('/tasks')
public async list() {
return this.prisma.todos.findMany();
}
@Post('/tasks')
public async create(@Body() body: { title: string, description?: string }) {
const createTodo = await this.prisma.todos.create({
data: {
title: body.title,
description: body.description,
},
});
this.connections.broadcast('refresh');
return createTodo;
}
@Get('/tasks/:id')
public async get(@Params('id') id: number) {
return await this.prisma.todos.findUnique({
where: { id },
});
}
@Put('/tasks/:id')
public async replace(@Params('id') id: number, @Body() body: { title: string, description?: string }) {
const todo = await this.prisma.todos.update({
where: { id },
data: {
title: body.title,
description: body.description,
},
});
if (!todo) {
throw new Error('Todo not found');
}
this.connections.broadcast('refresh');
return todo;
}
@Patch('/tasks/:id')
public async update(@Params('id') id: number, @Body() body: { title?: string, description?: string, completed?: boolean }) {
const todo = await this.prisma.todos.update({
where: { id },
data: {
title: body.title,
description: body.description,
completed: body.completed,
},
});
this.connections.broadcast('refresh');
return todo;
}
@Delete('/tasks/:id')
public async delete(@Params('id') id: number) {
const todo = await this.prisma.todos.delete({
where: { id },
});
this.connections.broadcast('refresh');
return todo;
}
}
This code will use the WebSocket connection to tell the web application when to refresh it's data, for example when a new todo is created, updated or deleted, it will broadcast a refresh
event to all connected clients.
We then want to deal with connections, so we use the Hook/Events functionality within the platform, to hook into a specific event, in this case, we want to listen to wsOpen
and wsClose
events, which will be triggered when a WebSocket connection is opened or closed. We then broadcast the current user count to all connected clients.
Lastly, we also add a new endpoint to get the chat messages, this will be used by the web application to get a list of all chat messages, in the future you may want to only get the last 25 messages or so, especially if you have a lot of messages, but for this guide we will just return all messages.
Add chat functionality.
Next step, let's actually add the chat functionality, in this, we don't really need to use HTTP to deal with sending messages, because HTTP carries an overhead, whereas WebSockets are already connected and can send messages instantly.
Let's create a new controller for the chat functionality, we will call it chat.ts
and place it in the src/controllers
folder.
import type { PrismaClient } from '../../prisma/prisma';
import { Controller, Expose, WsConnections, WsContext } from '@sodacore/ws';
import { Inject } from '@sodacore/di';
@Controller('chat')
export class ChatController {
@Inject('prisma') private readonly prisma!: PrismaClient
@Inject() private readonly connections!: WsConnections;
@Expose()
public async send(context: WsContext) {
const content = context.getData<{ username: string; message: string }>();
const message = await this.prisma.chatMessages.create({
data: content,
});
this.connections.broadcast('message', message);
}
}
If you haven't noticed, we are defining a controller again, but this time the decorator comes from the @sodacore/ws
package, this is because we are defining a WebSocket controller, which will handle WebSocket messages, the string that goes in as the first parameter is the "namespace", in the @sodacore/ws
package, we use a packet system consisting of a command
and context
where the command is the namespace and method formatted like {namespace}:{method}
and the context is the essentially "data" that is related to that command.
Therefore when sending a message to the WebSocket, you would do something like:
const ws = new WebSocket('ws://localhost:3000/ws');
ws.onopen = () => {
ws.send(JSON.stringify({
command: 'chat:send',
data: {
username: 'JohnDoe',
message: 'Hello World!',
},
}));
};
The @Expose()
decorator works as an opt-in ideaology, where you can define many methods within your controller, but you use the @Expose()
decorator to define which methods are exposed to the WebSocket connection, this is useful for keeping your code clean and only exposing the methods you want to be accessible via WebSocket. Going forward in the future, the expose method may offer the ability to define permissions and other features, but for now, it is simply an opt-in decorator.
Summary.
Hurrah! We have now integrated WebSockets into our application, allowing real-time communication between the server and clients. We can now send and receive messages in real-time, and we can also broadcast events to all connected clients.
If you run your application using bun dev
, you should now be able to access your application at http://localhost:3000
, and you should be able to see the chat functionality working, as well as the user count updating in real-time, and you can make todos and see them update in real-time as well. In short, your application is ready to use, the next step is to add translations to our frontend.