From a590a4ed30b0d2b53e787b3eb6e0c0c0f541aae9 Mon Sep 17 00:00:00 2001 From: Julian Krauser Date: Fri, 28 Feb 2025 14:02:06 +0100 Subject: [PATCH] sync and keep by change timestamp --- package.json | 2 +- src/stores/admin/operation/missionDetail.ts | 136 +++++++++++++----- .../admin/operation/mission/MissionDetail.vue | 22 ++- .../operation/mission/MissionOverview.vue | 13 +- 4 files changed, 128 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index b6ba446..2b855a3 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Feuerwehr/Verein Einsatzverwaltung UI", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "run-p type-check \"build-only {@}\" --", "preview": "vite preview", "build-only": "vite build", diff --git a/src/stores/admin/operation/missionDetail.ts b/src/stores/admin/operation/missionDetail.ts index c814b4a..2bf1d7a 100644 --- a/src/stores/admin/operation/missionDetail.ts +++ b/src/stores/admin/operation/missionDetail.ts @@ -4,50 +4,116 @@ import * as Y from "yjs"; import { Awareness } from "y-protocols/awareness.js"; import { computed, ref } from "vue"; -export const useMissionDetailStore = defineStore("missionDetail", () => { - const connectionStore = useConnectionStore(); - const initialized = ref(false); +export const useMissionDetailStore = defineStore("missionDetail", { + state: () => { + return { + yDoc: new Y.Doc(), + docId: null as null | string, + lastUpdateTimestamp: 0 as number, + connectionStatus: "disconnected", // 'disconnected', 'connecting', 'connected', 'syncing', 'synced' + }; + }, + actions: { + initialize(docId: string) { + this.docId = docId; - const ydoc = ref(new Y.Doc()); - const awareness = ref(new Awareness(ydoc.value)); + this.lastUpdateTimestamp = this.loadLastUpdateFromLocalStorage(); - const title = computed({ - get() { - return ydoc.value.getMap("form").get("title") ?? ""; + this.setupSocketHandlers(); + this.setupYjsObservers(); }, - set(val) { - ydoc.value.getMap("form").set("title", val); + + setupSocketHandlers() { + const connectionStore = useConnectionStore(); + if (!connectionStore.connection) return; + + connectionStore.connection.on("package-sync", (data) => { + try { + this.connectionStatus = "syncing"; + + Y.applyUpdate(this.yDoc, new Uint8Array(data.update)); + + if (data.timestamp > this.lastUpdateTimestamp) { + this.lastUpdateTimestamp = data.timestamp; + this.saveLastUpdateToLocalStorage(); + } + + this.connectionStatus = "synced"; + } catch (error) { + console.error("Error applying update:", error); + this.requestFullSync(); + } + }); + connectionStore.connection.on("sync-get-missing-updates", (data) => { + const clientUpdates = Y.encodeStateAsUpdate(this.yDoc, new Uint8Array(data.stateVector)); + + connectionStore.connection?.emit("mission:sync-client-updates", { + update: clientUpdates, + timestamp: Date.now(), + }); + }); + + this.joinDocument(); }, - }); - const editor = ref(ydoc.value.getText("editor")); - function init(missionId: string) { - if (!connectionStore.connection) return; + setupYjsObservers() { + if (!this.yDoc) return; - ydoc.value = new Y.Doc(); - awareness.value = new Awareness(ydoc.value); - editor.value = ydoc.value.getText("editor"); - ydoc.value.on("update", (update) => { - connectionStore.connection?.emit("mission:sync", Array.from(update)); - }); + this.yDoc.on("update", (update) => { + const connectionStore = useConnectionStore(); + if (connectionStore.connected) { + connectionStore.connection?.emit("mission:sync-client-updates", { + update: Array.from(update), + timestamp: Date.now(), + }); + } - connectionStore.connection.on("package-sync", (update) => { - Y.applyUpdate(ydoc.value, new Uint8Array(update)); - }); + this.lastUpdateTimestamp = Date.now(); + this.saveLastUpdateToLocalStorage(); + }); + }, - connectionStore.connection.on("package-mission", (initial) => { - Y.applyUpdate(ydoc.value, new Uint8Array(initial)); - }); + joinDocument() { + const connectionStore = useConnectionStore(); + if (!connectionStore.connection || !this.docId) return; - connectionStore.connection.emit("mission:join", missionId, initialized.value); - initialized.value = true; - } - function destroy() { - connectionStore.connection?.emit("mission:leave"); + connectionStore.connection.emit("mission:join", this.docId, { + timestamp: this.lastUpdateTimestamp, + }); + }, - connectionStore.connection?.off("package-mission"); - connectionStore.connection?.off("package-sync"); - } + requestFullSync() { + const connectionStore = useConnectionStore(); + if (!connectionStore.connection || !this.docId) return; - return { ydoc, awareness, title, editor, init, destroy }; + connectionStore.connection.emit("mission:join", this.docId, null); + }, + + loadLastUpdateFromLocalStorage() { + if (!this.docId) return 0; + + const stored = localStorage.getItem(`yjsDoc_timestamp`); + return stored ? parseInt(stored, 10) : 0; + }, + + saveLastUpdateToLocalStorage() { + if (!this.docId) return; + + localStorage.setItem(`yjsDoc_timestamp`, this.lastUpdateTimestamp.toString()); + }, + + cleanup() { + if (this.yDoc) { + this.yDoc.destroy(); + this.yDoc = new Y.Doc(); + } + this.lastUpdateTimestamp = 0; + localStorage.removeItem("yjsDoc_timestamp"); + + const connectionStore = useConnectionStore(); + connectionStore.connection?.emit("mission:leave"); + + this.connectionStatus = "disconnected"; + }, + }, }); diff --git a/src/views/admin/operation/mission/MissionDetail.vue b/src/views/admin/operation/mission/MissionDetail.vue index 3a3fa6f..15eb959 100644 --- a/src/views/admin/operation/mission/MissionDetail.vue +++ b/src/views/admin/operation/mission/MissionDetail.vue @@ -75,7 +75,7 @@