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 @@