diff --git a/bun.lockb b/bun.lockb index 7014f5a..c812f73 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 09230de..9df50d3 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "bcrypt": "^5.1.1", "elysia": "latest", "elysia-helmet": "^1.0.2", - "mongoose": "^8.1.0" + "mongoose": "^8.1.0", + "uuid": "^9.0.1" }, "devDependencies": { "bun-types": "latest" diff --git a/src/index.js b/src/index.js index e3815c6..3541b10 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,8 @@ import { cors } from '@elysiajs/cors'; import { helmet } from 'elysia-helmet'; import { jwt } from '@elysiajs/jwt'; import { logger } from '@grotto/logysia'; -import './config/mongo'; +import { v4 } from 'uuid'; +// import './config/mongo'; import { isAuthenticatedHttp, isAuthenticatedWS } from './middleware/jwt'; // Routers @@ -12,7 +13,10 @@ import authRouter from './modules/auth'; import userRouter from './modules/user'; // Ws -import { getDevices } from './ws/device'; +import ClientHandle from "./ws/clientHandle"; +import ClientConnectionManager from "./ws/clientConnectionManager"; + +const clientConnectionManager = new ClientConnectionManager(); const app = new Elysia() .use(swagger({ @@ -47,17 +51,25 @@ const app = new Elysia() // ws dùng hệ thống auth riêng nên không phải đặt trước onBeforeHandle auth của http .ws('/ws', { body: t.Object({ - type: t.String(), - d: t.String(), + type: t.Optional(t.String()), + content: t.Optional(t.Any()), }), - message(ws, message) { - switch(message.type) { - case 'get-device': - getDevices(ws, message.d); - } + open: (ws) => { + const idSocket = v4(); + const clientHandle = new ClientHandle(idSocket, clientConnectionManager); + clientHandle.initialize(ws); + clientConnectionManager.addClient(idSocket, clientHandle); + ws.data.store.clientHandle = clientHandle; }, + close: (ws) => { + ws.data.store.clientHandle.handleClientDisconnection(); + }, + message(ws, message) { + ws.data.store.clientHandle.handleClientMessage(message); + }, + response: t.String(), maxPayloadLength: 1024 * 1024, // 1 MB - beforeHandle: ({ headers, jwt, set, store }) => isAuthenticatedWS({ headers, jwt, set, store }), + // beforeHandle: ({ headers, jwt, set, store }) => isAuthenticatedWS({ headers, jwt, set, store }), }) // không auth .use(authRouter) diff --git a/src/ws/clientConnectionManager.js b/src/ws/clientConnectionManager.js new file mode 100644 index 0000000..ee790cc --- /dev/null +++ b/src/ws/clientConnectionManager.js @@ -0,0 +1,41 @@ +export default class ClientConnectionManager { + instance; + clients = new Map() + + getClient(id) { + return this.clients.get(id) + } + + addClient(id, client) { + if (client.connectionState !== 'OPEN') { + console.warn(`WebSocket is not open for client ${id}. Retrying...`) + setTimeout(() => this.addClient(id, client), 100) // Retry after 100ms + return + } + this.clients.set(id, client) + } + + removeClient(id) { + const client = this.clients.get(id) + if (!client) { + return + } + + // Close the WebSocket, regardless of its state + client.close() + + // Remove from all maps + this.clients.delete(id) + } + + isClientActive(id) { + const client = this.clients.get(id) + return client !== undefined && client.connectionState === 'OPEN' + } + + getAllClients() { + return Array.from(this.clients.values()).filter( + (clientHandler) => clientHandler.ws.raw.readyState === WebSocket.OPEN, + ) + } +} \ No newline at end of file diff --git a/src/ws/clientHandle.js b/src/ws/clientHandle.js new file mode 100644 index 0000000..b2448d6 --- /dev/null +++ b/src/ws/clientHandle.js @@ -0,0 +1,190 @@ +class ClientHandle { + id; + ws; + clientConnectionManager; + connectionState = "CLOSE"; + + constructor(id, clientConnectionManager) { + this.id = id; + this.clientConnectionManager = clientConnectionManager; + } + + initialize(ws) { + if (!ws) { + return; + } + this.ws = ws; + this.connectionState = "OPEN"; // Set the state to OPEN here + } + + isValidJSON(json) { + json = JSON.stringify(json); + return /^[\],:{}\s]*$/.test( + json + .replace(/\\["\\\/bfnrtu]/g, "@") + .replace( + /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + "]" + ) + .replace(/(?:^|:|,)(?:\s*\[)+/g, "") + ); + } + + handleClientMessage(message) { + if (!this.isValidJSON(message)) { + return this.sendToClient({ ok: 0, e: "Không đúng định dạng JSON" }); + } + + const { type, content } = message; + + switch (type) { + case "info": { + this.sendToClient({ + type: "info", + content: { ok: 1, d: this.id }, + }); + break; + } + case "devices-lists": { + const allClient = this.clientConnectionManager.getAllClients(); + const idList = []; + allClient.forEach((client) => { + if (client.id !== this.id) { + idList.push(client.id); + } + }); + this.sendToClient({ + type: "devices-lists", + content: { ok: 1, d: idList }, + }); + break; + } + case "send-to-web-offer": { + const { toDevice, offer } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "send-to-web-offer", + content: { + offer, + fromDevice: this.id, + }, + }); + break; + } + case "send-to-web-answer": { + const { toDevice, answer } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "send-to-web-answer", + content: { + answer, + fromDevice: this.id, + }, + }); + break; + } + case "ice": { + const { toDevice, ice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "ice", + content: { + ice, + }, + }); + break; + } + case "start": { + const { toDevice, fromDevice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "start", + content: { + fromDevice, + }, + }); + break; + } + case "stop": { + const { toDevice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "stop", + content: {}, + }); + break; + } + case "actionmouse": { + const { toDevice, type, x, y } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "actionmouse", + content: { type, x, y }, + }); + break; + } + case "action": { + const { toDevice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "action", + content: { + type: content.type, + }, + }); + } + case "start_stream_socket": { + const { toDevice, fromDevice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "start_stream_socket", + content: { fromDevice }, + }); + break; + } + case "frame_stream_socket": { + const { toDevice, image } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "frame_stream_socket", + content: { image }, + }); + break; + } + case "stop_stream_socket": { + const { toDevice } = content; + this.clientConnectionManager.getClient(toDevice)?.sendToClient({ + type: "stop_stream_socket", + content: {}, + }); + break; + } + default: + break; + } + } + + handleClientDisconnection() { + this.clientConnectionManager.removeClient(this.id.toString()); + this.connectionState = "CLOSED"; + } + + sendToClient(data) { + try { + if (this.ws && this.connectionState === "OPEN") { + this.ws.send(JSON.stringify(data)); + } else { + this.clientConnectionManager.removeClient(this.id.toString()); + } + } catch (error) { + // Handle error + } + } + + close() { + try { + if (this.ws && this.ws.readyState === this.ws.OPEN) { + this.sendToClient({ + type: "a_device_disconnect", + content: { + device: this.id, + }, + }); + this.ws.close(); + } + } catch (error) {} + } +} + +export default ClientHandle; diff --git a/src/ws/device.js b/src/ws/device.js deleted file mode 100644 index 3ef0049..0000000 --- a/src/ws/device.js +++ /dev/null @@ -1,7 +0,0 @@ -async function getDevices(ws, d) { - const { data: { store } } = ws; - console.log(store); - ws.send({ d }); -} - -export { getDevices }; \ No newline at end of file