From 3bc56bf91ef1bcf5dddc6b8f95c7422e66b0f103 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Wed, 26 Feb 2025 16:26:56 +0100 Subject: [PATCH] broadcast synced --- package-lock.json | 51 +++++++++- package.json | 3 +- src/storage/missionMap.ts | 4 +- src/websocket/endpoints/base.ts | 8 ++ src/websocket/endpoints/missionManagement.ts | 101 +++++++++++++++++++ src/websocket/endpoints/roomManagement.ts | 36 ------- src/websocket/handleEvent.ts | 5 +- src/websocket/index.ts | 4 +- 8 files changed, 170 insertions(+), 42 deletions(-) create mode 100644 src/websocket/endpoints/missionManagement.ts delete mode 100644 src/websocket/endpoints/roomManagement.ts diff --git a/package-lock.json b/package-lock.json index 7d91d4c..e8d5c3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "speakeasy": "^2.0.0", "sqlite3": "^5.1.7", "typeorm": "^0.3.20", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "yjs": "^13.6.23" }, "devDependencies": { "@types/cors": "^2.8.14", @@ -2163,6 +2164,16 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, + "node_modules/isomorphic.js": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", + "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", + "license": "MIT", + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -2220,6 +2231,27 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lib0": { + "version": "0.2.99", + "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.99.tgz", + "integrity": "sha512-vwztYuUf1uf/1zQxfzRfO5yzfNKhTtgOByCruuiQQxWQXnPb8Itaube5ylofcV0oM0aKal9Mv+S1s1Ky0UYP1w==", + "license": "MIT", + "dependencies": { + "isomorphic.js": "^0.2.4" + }, + "bin": { + "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js", + "0gentesthtml": "bin/gentesthtml.js", + "0serve": "bin/0serve.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -4799,6 +4831,23 @@ "node": ">=6" } }, + "node_modules/yjs": { + "version": "13.6.23", + "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.23.tgz", + "integrity": "sha512-ExtnT5WIOVpkL56bhLeisG/N5c4fmzKn4k0ROVfJa5TY2QHbH7F0Wu2T5ZhR7ErsFWQEFafyrnSI8TPKVF9Few==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.99" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index 2fc96b3..1ddad04 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,8 @@ "speakeasy": "^2.0.0", "sqlite3": "^5.1.7", "typeorm": "^0.3.20", - "uuid": "^10.0.0" + "uuid": "^10.0.0", + "yjs": "^13.6.23" }, "devDependencies": { "@types/cors": "^2.8.14", diff --git a/src/storage/missionMap.ts b/src/storage/missionMap.ts index 67d2793..c3a27a8 100644 --- a/src/storage/missionMap.ts +++ b/src/storage/missionMap.ts @@ -1,6 +1,8 @@ +import * as Y from "yjs"; + export interface missionStoreModel { missionId: string; - doc: any; + doc: Y.Doc; } /** diff --git a/src/websocket/endpoints/base.ts b/src/websocket/endpoints/base.ts index f53921d..8f9c403 100644 --- a/src/websocket/endpoints/base.ts +++ b/src/websocket/endpoints/base.ts @@ -1,6 +1,7 @@ import { Server, Socket } from "socket.io"; import { SocketMap } from "../../storage/socketMap"; import CustomRequestException from "../../exceptions/customRequestException"; +import { MissionMap } from "../../storage/missionMap"; export default (io: Server, socket: Socket) => { socket.on("ping", (callback: Function) => { @@ -28,6 +29,13 @@ export default (io: Server, socket: Socket) => { socket.on("disconnecting", () => { console.log("socket disconnection: ", socket.id); + const socketRooms = Array.from(socket.rooms).filter((room) => room !== socket.id && room !== "home"); + socket.leave(socketRooms[0]); + + const clients = io.sockets.adapter.rooms.get(socketRooms[0]); + if (!clients || clients.size == 0) { + MissionMap.delete(socketRooms[0]); + } SocketMap.delete(socket.id); }); diff --git a/src/websocket/endpoints/missionManagement.ts b/src/websocket/endpoints/missionManagement.ts new file mode 100644 index 0000000..5accd3c --- /dev/null +++ b/src/websocket/endpoints/missionManagement.ts @@ -0,0 +1,101 @@ +import { Server, Socket } from "socket.io"; +import { handleEvent } from "../handleEvent"; +import { MissionMap } from "../../storage/missionMap"; +import * as Y from "yjs"; + +export default (io: Server, socket: Socket) => { + socket.on( + "mission:join", + handleEvent( + { type: "read", section: "operation", module: "mission" }, + async (data: any) => { + socket.rooms.forEach((room) => { + if (room !== socket.id && room != "home") { + socket.leave(room); + } + }); + + try { + const doc = new Y.Doc(); + doc.getMap("form"); + doc.getText("editor"); + + // const ymap = ydoc.getMap('myMap'); + // ymap.set('titel', 'Mein Dokument'); + // ymap.set('inhalt', 'Hier ist der initiale Inhalt'); + // ymap.set('erstelltAm', new Date().toISOString()); + + // const yarray = ydoc.getArray('meineArray'); + // yarray.push(['Element 1', 'Element 2', 'Element 3']); + + // const ytext = ydoc.getText('meinText'); + // ytext.insert(0, 'Hier ist ein initialer Text'); + + // get mission data + MissionMap.write(data, { + missionId: data, + doc, + }); + + const mission = MissionMap.read(data); + + socket.join(data); + socket.emit("status-mission:join", { status: "success" }); + return { + type: "package-mission", + answer: Y.encodeStateAsUpdate(mission.doc), + }; + } catch (error) { + return { type: "status-join:join", answer: { status: "failed", msg: error.message } }; + } + }, + socket + ) + ); + + socket.on( + "mission:sync", + handleEvent( + { type: "read", section: "operation", module: "mission" }, + async (data: any) => { + const socketRooms = Array.from(socket.rooms).filter((room) => room !== socket.id && room !== "home"); + try { + const mission = MissionMap.read(socketRooms[0]); + Y.applyUpdate(mission.doc, new Uint8Array(data)); + + socket.emit("status-mission:sync", { status: "success" }); + return { + type: "package-sync", + answer: data, + room: socketRooms[0], + }; + } catch (error) { + return { type: "status-mission:sync", answer: { status: "failed", msg: error.message } }; + } + }, + socket + ) + ); + + socket.on( + "mission:leave", + handleEvent( + { type: "read", section: "operation", module: "mission" }, + async (data: any) => { + const socketRooms = Array.from(socket.rooms).filter((room) => room !== socket.id && room !== "home"); + socket.leave(socketRooms[0]); + + const clients = io.sockets.adapter.rooms.get(socketRooms[0]); + if (!clients || clients.size == 0) { + MissionMap.delete(socketRooms[0]); + } + + return { + type: "deleted", + answer: "leave instance", + }; + }, + socket + ) + ); +}; diff --git a/src/websocket/endpoints/roomManagement.ts b/src/websocket/endpoints/roomManagement.ts deleted file mode 100644 index c3890ac..0000000 --- a/src/websocket/endpoints/roomManagement.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Server, Socket } from "socket.io"; -import { SocketMap } from "../../storage/socketMap"; -import CustomRequestException from "../../exceptions/customRequestException"; -import { handleEvent } from "../handleEvent"; - -export default (io: Server, socket: Socket) => { - socket.on( - "join:mission", - - handleEvent( - { type: "read", section: "operation", module: "mission" }, - async (data: any) => { - socket.rooms.forEach((room) => { - if (room !== socket.id && room != "home") { - socket.leave(room); - } - }); - - try { - socket.join(data); - socket.emit("status-mission:room", { status: "success" }); - // get mission data - // create yjs sync doc - // provide yjs sync doc to client - return { - type: "package-mission", - answer: "mission-data by yjs", - }; - } catch (error) { - return { type: "status-join:room", answer: { status: "failed", msg: error.message } }; - } - }, - socket - ) - ); -}; diff --git a/src/websocket/handleEvent.ts b/src/websocket/handleEvent.ts index ab9f209..9f35efa 100644 --- a/src/websocket/handleEvent.ts +++ b/src/websocket/handleEvent.ts @@ -38,7 +38,10 @@ export let handleEvent = ( throw new ForbiddenRequestException(`missing admin permission`); } } else { - if (!PermissionHelper.can(socketData.permissions, permissions.type, permissions.section, permissions.module)) { + if ( + !socketData.isOwner && + !PermissionHelper.can(socketData.permissions, permissions.type, permissions.section, permissions.module) + ) { throw new ForbiddenRequestException(`missing required permission`); } } diff --git a/src/websocket/index.ts b/src/websocket/index.ts index 629067b..af1adc8 100644 --- a/src/websocket/index.ts +++ b/src/websocket/index.ts @@ -4,7 +4,7 @@ import { Server } from "socket.io"; import authenticateSocket from "./middleware/authenticateSocket"; import base from "./endpoints/base"; import perRequestCheck from "./middleware/perRequestCheck"; -import roomManagement from "./endpoints/roomManagement"; +import missionManagement from "./endpoints/missionManagement"; export default abstract class SocketServer { private static io: Server; @@ -28,7 +28,7 @@ export default abstract class SocketServer { socket.use((packet, next) => perRequestCheck(socket, packet, next)); base(this.io, socket); - roomManagement(this.io, socket); + missionManagement(this.io, socket); }); }