generated from pure_sagacity/turborepo-starter-project
added realtime stuff (NOT TESTED)
This commit is contained in:
@@ -1 +1,12 @@
|
|||||||
|
import { integer, pgTable, text, serial, boolean } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export * from "./auth";
|
export * from "./auth";
|
||||||
|
|
||||||
|
export const message = pgTable("message", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
roomId: text("room_id").notNull(),
|
||||||
|
username: text("username").notNull(),
|
||||||
|
content: text("content").notNull(),
|
||||||
|
timestamp: integer("timestamp").notNull(),
|
||||||
|
deleted: boolean("deleted").notNull().default(false),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { auth } from "../lib/auth";
|
import { auth } from "../lib/auth";
|
||||||
import { Elysia, t } from "elysia";
|
import { Elysia, t } from "elysia";
|
||||||
|
import { realtime } from "./routes/realtime";
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.mount("/auth", auth.handler)
|
.mount("/auth", auth.handler)
|
||||||
.get("/health", () => "OK", {
|
.get("/health", () => "OK", {
|
||||||
response: t.String(),
|
response: t.String(),
|
||||||
})
|
})
|
||||||
|
.use(realtime)
|
||||||
.listen(3000);
|
.listen(3000);
|
||||||
|
|
||||||
export type App = typeof app;
|
export type App = typeof app;
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { ElysiaWS } from "elysia/ws";
|
||||||
|
import { ClientList, ServerMessage } from "./types";
|
||||||
|
|
||||||
|
export class Clients {
|
||||||
|
private clients: ClientList;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.clients = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
userInRoom(username: string): boolean {
|
||||||
|
this.clients.forEach((room) => {
|
||||||
|
if (room.some((client) => client.username === username)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get clientList(): ClientList {
|
||||||
|
return this.clients;
|
||||||
|
}
|
||||||
|
|
||||||
|
addClient(roomId: string, username: string, ws: ElysiaWS) {
|
||||||
|
if (!this.clients.has(roomId)) {
|
||||||
|
this.clients.set(roomId, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = this.clients.get(roomId);
|
||||||
|
if (room) {
|
||||||
|
room.push({ username, ws });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveRoom(roomId: string, username: string) {
|
||||||
|
const room = this.clients.get(roomId);
|
||||||
|
if (room) {
|
||||||
|
this.clients.set(
|
||||||
|
roomId,
|
||||||
|
room.filter((client) => client.username !== username),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (room.length === 0) {
|
||||||
|
this.clients.delete(roomId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove a client from all rooms based on the WebSocket instance
|
||||||
|
removeClient(ws: ElysiaWS) {
|
||||||
|
this.clients.forEach((room, roomId) => {
|
||||||
|
const updatedRoom = room.filter((client) => client.ws !== ws);
|
||||||
|
if (updatedRoom.length === 0) {
|
||||||
|
this.clients.delete(roomId);
|
||||||
|
} else {
|
||||||
|
this.clients.set(roomId, updatedRoom);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
broadcast = {
|
||||||
|
all: (message: ServerMessage) => {
|
||||||
|
this.clients.forEach((room) => {
|
||||||
|
room.forEach((client) => {
|
||||||
|
client.ws.send(JSON.stringify(message));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
room: (roomId: string, message: ServerMessage) => {
|
||||||
|
const room = this.clients.get(roomId);
|
||||||
|
if (room) {
|
||||||
|
room.forEach((client) => {
|
||||||
|
client.ws.send(JSON.stringify(message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,82 @@
|
|||||||
import Elysia from "elysia";
|
import Elysia from "elysia";
|
||||||
|
import type { ElysiaWS } from "elysia/ws";
|
||||||
|
import { ClientMessage, ClientMessageSchema } from "./types";
|
||||||
|
import { Clients } from "./classes";
|
||||||
|
import { db, message as messageTable } from "@/lib/db";
|
||||||
|
|
||||||
|
let clients = new Clients();
|
||||||
|
|
||||||
|
async function handleMessage(ws: ElysiaWS, message: ClientMessage) {
|
||||||
|
switch (message.type) {
|
||||||
|
case "join": {
|
||||||
|
clients.addClient(message.roomId, message.username, ws);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "leave": {
|
||||||
|
clients.leaveRoom(message.roomId, message.username);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "message": {
|
||||||
|
clients.broadcast.room(message.roomId, {
|
||||||
|
type: "message",
|
||||||
|
username: message.username,
|
||||||
|
m: message.m,
|
||||||
|
});
|
||||||
|
|
||||||
|
// No await because it don't want clients to wait on a stupid db
|
||||||
|
db.insert(messageTable)
|
||||||
|
.values({
|
||||||
|
id: message.m.id,
|
||||||
|
roomId: message.m.roomId,
|
||||||
|
username: message.m.username,
|
||||||
|
content: message.m.content,
|
||||||
|
timestamp: message.m.timestamp,
|
||||||
|
deleted: false,
|
||||||
|
})
|
||||||
|
.onConflictDoNothing()
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "sync": {
|
||||||
|
// This time we have to await :(
|
||||||
|
const rows = await db.select().from(messageTable);
|
||||||
|
|
||||||
|
ws.send({
|
||||||
|
type: "sync",
|
||||||
|
roomId: message.roomId,
|
||||||
|
messages: rows.map((row) => ({
|
||||||
|
id: row.id,
|
||||||
|
roomId: row.roomId,
|
||||||
|
username: row.username,
|
||||||
|
content: row.content,
|
||||||
|
timestamp: row.timestamp,
|
||||||
|
deleted: row.deleted,
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const realtime = new Elysia().ws("/realtime", {
|
export const realtime = new Elysia().ws("/realtime", {
|
||||||
open: (ws) => {
|
open: (ws: ElysiaWS) => {
|
||||||
console.log("WebSocket connection opened");
|
console.log("[+] Client connected:", ws.id);
|
||||||
},
|
},
|
||||||
message: (ws, message) => {
|
|
||||||
console.log("Received message:", message);
|
body: ClientMessageSchema,
|
||||||
|
|
||||||
|
message: async (ws: ElysiaWS, message: ClientMessage) => {
|
||||||
|
console.log("[*] Received message:", message);
|
||||||
|
|
||||||
|
await handleMessage(ws, message);
|
||||||
|
},
|
||||||
|
|
||||||
|
close: (ws: ElysiaWS) => {
|
||||||
|
// Remove the client from all rooms they are in
|
||||||
|
clients.removeClient(ws);
|
||||||
|
|
||||||
|
console.log("[-] Client disconnected:", ws.id);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,22 +1,73 @@
|
|||||||
export type Message = {
|
import { t } from "elysia";
|
||||||
id: number;
|
import { ElysiaWS } from "elysia/ws";
|
||||||
roomId: string;
|
|
||||||
username: string;
|
const MessageSchema = t.Object({
|
||||||
content: string;
|
id: t.Number(),
|
||||||
timestamp: number;
|
roomId: t.String(),
|
||||||
deleted: boolean;
|
username: t.String(),
|
||||||
};
|
content: t.String(),
|
||||||
|
timestamp: t.Number(),
|
||||||
|
deleted: t.Boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
// Client -> Server
|
// Client -> Server
|
||||||
export type ClientMessage =
|
const ClientMessageSchema = t.Union([
|
||||||
| { type: "join"; roomId: string }
|
t.Object({
|
||||||
| { type: "leave"; roomId: string }
|
type: t.Literal("join"),
|
||||||
| { type: "message"; roomId: string; m: Message }
|
username: t.String(),
|
||||||
| { type: "sync"; roomId: string };
|
roomId: t.String(),
|
||||||
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("leave"),
|
||||||
|
username: t.String(),
|
||||||
|
roomId: t.String(),
|
||||||
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("message"),
|
||||||
|
username: t.String(),
|
||||||
|
roomId: t.String(),
|
||||||
|
m: MessageSchema,
|
||||||
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("sync"),
|
||||||
|
roomId: t.String(),
|
||||||
|
// No need for username as we can just directly send the sync through the WS
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
// Server -> Client(s)
|
// Server -> Client(s)
|
||||||
export type ServerMessage =
|
const ServerMessageSchema = t.Union([
|
||||||
| { type: "joined"; username: string }
|
t.Object({
|
||||||
| { type: "left"; username: string }
|
type: t.Literal("joined"),
|
||||||
| { type: "message"; username: string; m: Message }
|
username: t.String(),
|
||||||
| { type: "sync"; roomId: string; messages: Message[] };
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("left"),
|
||||||
|
username: t.String(),
|
||||||
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("message"),
|
||||||
|
username: t.String(),
|
||||||
|
m: MessageSchema,
|
||||||
|
}),
|
||||||
|
t.Object({
|
||||||
|
type: t.Literal("sync"),
|
||||||
|
roomId: t.String(),
|
||||||
|
messages: t.Array(MessageSchema),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
type ClientList = Map<
|
||||||
|
string, // Room ID
|
||||||
|
Array<{
|
||||||
|
username: string; // Username of the client
|
||||||
|
ws: ElysiaWS; // WebSocket connection of the client
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
||||||
|
type ServerMessage = typeof ServerMessageSchema.static;
|
||||||
|
type Message = typeof MessageSchema.static;
|
||||||
|
type ClientMessage = typeof ClientMessageSchema.static;
|
||||||
|
|
||||||
|
export { ServerMessageSchema, MessageSchema, ClientMessageSchema };
|
||||||
|
export type { ServerMessage, Message, ClientMessage, ClientList };
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||||
/* Modules */
|
/* Modules */
|
||||||
"module": "ES2022", /* Specify what module code is generated. */
|
"module": "ES2022", /* Specify what module code is generated. */
|
||||||
|
"moduleResolution": "bundler", /* Use package.json exports for Bun/ESM. */
|
||||||
"rootDir": "./", /* Specify the root folder within your source files. */
|
"rootDir": "./", /* Specify the root folder within your source files. */
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
info:
|
||||||
|
name: connect
|
||||||
|
type: websocket
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
websocket:
|
||||||
|
url: ws://localhost:3000/realtime
|
||||||
|
message:
|
||||||
|
type: json
|
||||||
|
data: "{}"
|
||||||
|
auth: inherit
|
||||||
Reference in New Issue
Block a user