diff --git a/package-lock.json b/package-lock.json index bd591f5..0297ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "sqlite3": "^5.1.7", "typeorm": "^0.3.20", "uuid": "^10.0.0", + "y-protocols": "^1.0.6", "yjs": "^13.6.23" }, "devDependencies": { @@ -4831,6 +4832,26 @@ "node": ">=0.4" } }, + "node_modules/y-protocols": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz", + "integrity": "sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==", + "license": "MIT", + "dependencies": { + "lib0": "^0.2.85" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + }, + "funding": { + "type": "GitHub Sponsors ❤", + "url": "https://github.com/sponsors/dmonad" + }, + "peerDependencies": { + "yjs": "^13.0.0" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index edf5a42..5e482ae 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "sqlite3": "^5.1.7", "typeorm": "^0.3.20", "uuid": "^10.0.0", + "y-protocols": "^1.0.6", "yjs": "^13.6.23" }, "devDependencies": { diff --git a/src/helpers/missionDocHelper.ts b/src/helpers/missionDocHelper.ts new file mode 100644 index 0000000..e7cc31f --- /dev/null +++ b/src/helpers/missionDocHelper.ts @@ -0,0 +1,29 @@ +import * as Y from "yjs"; +import { MissionMap } from "../storage/missionMap"; + +export default abstract class MissionDocHelper { + public static async populateDoc(missionId: string) { + // get Data from database + 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'); + + MissionMap.write(missionId, { missionId, doc, timestamp: 0 }, true); + } + + public static async saveDoc(missionId: string) { + // store Data to database + const mission = MissionMap.read(missionId); + } +} diff --git a/src/storage/missionMap.ts b/src/storage/missionMap.ts index c3a27a8..7784a96 100644 --- a/src/storage/missionMap.ts +++ b/src/storage/missionMap.ts @@ -3,6 +3,7 @@ import * as Y from "yjs"; export interface missionStoreModel { missionId: string; doc: Y.Doc; + timestamp: number; } /** @@ -15,6 +16,18 @@ export abstract class MissionMap { if (!this.exists(identifier) || overwrite) this.store.set(identifier, data); } + public static updateState(identifier: string, data: Uint8Array): void { + let mission = this.read(identifier); + Y.applyUpdate(mission.doc, data); + this.write(identifier, mission, true); + } + + public static updateTimestamp(identifier: string, data: number): void { + let mission = this.read(identifier); + mission.timestamp = data; + this.write(identifier, mission, true); + } + public static read(identifier: string): missionStoreModel { return this.store.get(identifier); } diff --git a/src/websocket/endpoints/missionManagement.ts b/src/websocket/endpoints/missionManagement.ts index f844e74..cde2cfe 100644 --- a/src/websocket/endpoints/missionManagement.ts +++ b/src/websocket/endpoints/missionManagement.ts @@ -1,14 +1,15 @@ import { Server, Socket } from "socket.io"; import { handleEvent } from "../handleEvent"; -import { MissionMap } from "../../storage/missionMap"; +import { MissionMap, missionStoreModel } from "../../storage/missionMap"; import * as Y from "yjs"; +import MissionDocHelper from "../../helpers/missionDocHelper"; export default (io: Server, socket: Socket) => { socket.on( "mission:join", handleEvent( { type: "read", section: "operation", module: "mission" }, - async (data: string, initialized: boolean) => { + async (missionId: string, clientLastUpdate: missionStoreModel) => { socket.rooms.forEach((room) => { if (room !== socket.id && room != "home") { socket.leave(room); @@ -16,33 +17,35 @@ export default (io: Server, socket: Socket) => { }); try { - const doc = new Y.Doc(); - doc.getMap("form"); - doc.getText("editor"); + socket.join(missionId); - // const ymap = ydoc.getMap('myMap'); - // ymap.set('titel', 'Mein Dokument'); - // ymap.set('inhalt', 'Hier ist der initiale Inhalt'); - // ymap.set('erstelltAm', new Date().toISOString()); + await MissionDocHelper.populateDoc(missionId); + const mission = MissionMap.read(missionId); - // 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); - - if (!initialized) { - socket.emit("package-mission", Y.encodeStateAsUpdate(mission.doc)); + if (clientLastUpdate && clientLastUpdate.timestamp) { + // Prüfe, ob der Client aktuell ist oder nicht + if (mission.timestamp > clientLastUpdate.timestamp) { + // Server hat neuere Daten, sende vollständigen State + socket.emit("package-sync", { + update: Y.encodeStateAsUpdate(mission.doc), + timestamp: mission.timestamp, + source: "server", + }); + } else { + // Client hat aktuellere oder gleich aktuelle Daten + socket.emit("sync-get-missing-updates", { + stateVector: Y.encodeStateVector(mission.doc), + timestamp: mission.timestamp, + source: "server", + }); + } + } else { + // Client hat keine Zeitstempel-Info, sende vollständigen State + socket.emit("package-sync", { + update: Y.encodeStateAsUpdate(mission.doc), + timestamp: mission.timestamp, + source: "server", + }); } return { @@ -58,23 +61,58 @@ export default (io: Server, socket: Socket) => { ); socket.on( - "mission:sync", + "mission:sync-client-updates", handleEvent( - { type: "read", section: "operation", module: "mission" }, + { type: "create", section: "operation", module: "mission" }, + async (data: { update: Array; timestamp: number }) => { + const socketRooms = Array.from(socket.rooms).filter((room) => room !== socket.id && room !== "home"); + try { + MissionMap.updateState(socketRooms[0], new Uint8Array(data.update)); + + return { + type: "package-sync", + answer: { + update: data.update, + timestamp: data.timestamp, + source: "client", + }, + room: socketRooms[0], + }; + } catch (error) { + const mission = MissionMap.read(socketRooms[0]); + socket.emit("package-sync", { + update: Y.encodeStateAsUpdate(mission.doc), + timestamp: mission.timestamp, + source: "server", + error: true, + }); + return { type: "status-mission:sync-client-updates", answer: { status: "failed", msg: error.message } }; + } + }, + socket + ) + ); + + socket.on( + "mission:sync-get-missing-updates", + handleEvent( + { type: "create", 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)); + const missingUpdates = Y.encodeStateAsUpdate(mission.doc, data.stateVector); - socket.emit("status-mission:sync", { status: "success" }); return { type: "package-sync", - answer: data, - room: socketRooms[0], + answer: { + update: missingUpdates, + timestamp: mission.timestamp, + source: "server", + }, }; } catch (error) { - return { type: "status-mission:sync", answer: { status: "failed", msg: error.message } }; + return { type: "status-mission:sync-get-missing-updates", answer: { status: "failed", msg: error.message } }; } }, socket