Pārlūkot izejas kodu

Implement state syncro on connection

tripeur 4 gadi atpakaļ
vecāks
revīzija
454acc8d5c

+ 18 - 5
package-lock.json

@@ -1,6 +1,6 @@
 {
   "name": "bdlg-scheduler",
-  "version": "1.2.0",
+  "version": "1.2.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
@@ -394,6 +394,12 @@
       "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==",
       "dev": true
     },
+    "@types/pako": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@types/pako/-/pako-1.0.2.tgz",
+      "integrity": "sha512-8UJl2MjkqqS6ncpLZqRZ5LmGiFBkbYxocD4e4jmBqGvfRG1RS23gKsBQbdtV9O9GvRyjFTiRHRByjSlKCLlmZw==",
+      "dev": true
+    },
     "@types/parse-json": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -2440,6 +2446,14 @@
       "dev": true,
       "requires": {
         "pako": "~1.0.5"
+      },
+      "dependencies": {
+        "pako": {
+          "version": "1.0.11",
+          "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+          "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+          "dev": true
+        }
       }
     },
     "browserslist": {
@@ -8450,10 +8464,9 @@
       "dev": true
     },
     "pako": {
-      "version": "1.0.11",
-      "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
-      "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
-      "dev": true
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
+      "integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
     },
     "parallel-transform": {
       "version": "1.2.0",

+ 2 - 0
package.json

@@ -15,6 +15,7 @@
     "lodash": "^4.17.21",
     "lodash-es": "^4.17.21",
     "marked": "^2.1.3",
+    "pako": "^2.0.4",
     "sockjs": "^0.3.21",
     "sockjs-client": "^1.5.1",
     "stomp": "^0.1.1",
@@ -30,6 +31,7 @@
     "@types/lodash-es": "^4.17.4",
     "@types/marked": "^2.0.3",
     "@types/materialize-css": "^1.0.9",
+    "@types/pako": "^1.0.2",
     "@types/sockjs-client": "^1.5.1",
     "@types/stompjs": "^2.3.5",
     "@types/uuid": "^8.3.0",

+ 36 - 13
src/PlannerApp.vue

@@ -33,12 +33,15 @@
 <script lang="ts">
 import SockJS from "sockjs-client";
 import Stomp from "stompjs";
+import dayjs from "dayjs";
 import { defineComponent } from "vue";
-import toast, { Toast } from "@/utils/Toast";
 import ConstraintTranslation from "@/assets/ConstraintTranslation";
+import toast, { Toast } from "@/utils/Toast";
+import { zipEncode, decodeUnzip } from "@/utils/Compression";
 import Evenement from "@/models/Evenement";
 import Creneau from "@/models/Creneau";
-import PlanningUpdateMessage from "@/models/PlanningUpdateMessage";
+import { PlanningUpdateMessage } from "@/models/messages/PlanningUpdateMessage";
+import { StompQueryMessage } from "@/models/messages/StompQueryMessage";
 import {
   AssignementPair,
   SolverInput,
@@ -49,11 +52,10 @@ import {
 } from "@/models/SolverInput";
 import updatePlanningVersions from "@/mixins/updatePlanningVersions";
 import importJsonState from "@/mixins/ImportJsonState";
-import { MutationTypes } from "@/store/Mutations";
-import dayjs from "dayjs";
-import { StateJSON } from "@/store/State";
-import Header, { HeaderLink } from "./components/Utils/Header.vue";
-import cFooter from "./components/Utils/Footer.vue";
+import { MutationTypes } from "@/views/Mutations";
+import { EvenementStateJSON } from "@/store/State";
+import Header, { HeaderLink } from "@/components/Utils/Header.vue";
+import cFooter from "@/components/Utils/Footer.vue";
 
 const API_URL = process.env.VUE_APP_API_URL;
 
@@ -104,9 +106,11 @@ export default defineComponent({
           var content = JSON.parse(msg.body) as { text: string; type: string };
           toast({ html: content.text, classes: content.type });
         });
+        this.stompClient.subscribe("/user/queue/query", this.handleStompQuery);
         this.subscribe(this.uuid);
       },
-      () => {
+      (error) => {
+        console.error(error);
         toast({
           html:
             "Impossible d'établir une connection avec le serveur.<br>Veuillez recharger la page.",
@@ -175,14 +179,33 @@ export default defineComponent({
         }
       }
     },
+    handleStompQuery(msg: Stomp.Message) {
+      var content = JSON.parse(msg.body) as StompQueryMessage;
+      if (content.type == "planningRequest" && content.uuid == this.uuid) {
+        const response: StompQueryMessage = {
+          type: "planningData",
+          uuid: this.uuid,
+          destination: content.destination,
+          payload: zipEncode(JSON.stringify(this.$store.getters.getJSONEvenementState)),
+        };
+        this.stompClient.send("/app/response", {}, JSON.stringify(response));
+      }
+      if (content.type == "planningData" && content.uuid == this.uuid) {
+        const payload = JSON.parse(decodeUnzip(content.payload)) as EvenementStateJSON;
+        this.importState(payload);
+      }
+    },
     localSave() {
-      window.localStorage.setItem("activeState", JSON.stringify(this.$store.getters.getJSONState));
+      window.localStorage.setItem(
+        "activeState",
+        JSON.stringify(this.$store.getters.getJSONEvenementState)
+      );
     },
     save() {
       const body = {
         uuid: this.$store.state.evenement.uuid,
         name: this.$store.state.evenement.name,
-        content: JSON.stringify(this.$store.getters.getJSONState),
+        content: JSON.stringify(this.$store.getters.getJSONEvenementState),
       };
       // local save
       window.localStorage.setItem("activeState", body.content);
@@ -232,7 +255,7 @@ export default defineComponent({
       }
     },
     newEvenement() {
-      const newState: StateJSON = {
+      const newState: EvenementStateJSON = {
         evenement: new Evenement().toJSON(),
         competences: [],
         benevoles: [],
@@ -243,7 +266,7 @@ export default defineComponent({
       this.save();
       this.importState(newState);
     },
-    importState(newState: StateJSON) {
+    importState(newState: EvenementStateJSON) {
       const prevUuid = this.$store.state.evenement.uuid;
       this.importJsonState(newState);
       if (this.$store.state.evenement.uuid != prevUuid) {
@@ -253,7 +276,7 @@ export default defineComponent({
       this.clearToast();
     },
     exportStateToJson(): void {
-      const obj: StateJSON = this.$store.getters.getJSONState;
+      const obj: EvenementStateJSON = this.$store.getters.getJSONEvenementState;
       const mimeType = "data:text/json;charset=utf-8";
       const dummy = document.createElement("a");
       dummy.href = mimeType + ", " + encodeURI(JSON.stringify(obj));

+ 2 - 2
src/models/PlanningUpdateMessage.ts → src/models/messages/PlanningUpdateMessage.ts

@@ -1,6 +1,6 @@
-import { Mutations } from "@/store/Mutations";
+import { Mutations } from "@/views/Mutations";
 
-type PlanningUpdateMessage = {
+export type PlanningUpdateMessage = {
   uuid: string;
   user: string;
   method: keyof Mutations | "toast";

+ 10 - 0
src/models/messages/StompQueryMessage.ts

@@ -0,0 +1,10 @@
+export type StompQuery = "planningRequest" | "planningData";
+
+export type StompQueryMessage = {
+  type: StompQuery;
+  uuid: string;
+  destination: string;
+  payload: string;
+};
+
+export default StompQueryMessage;

+ 109 - 0
src/utils/Compression.ts

@@ -0,0 +1,109 @@
+import { gzip, ungzip } from "pako";
+/*
+MIT License
+Copyright (c) 2020 Egor Nepomnyaschih
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+const base64abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");
+
+const l = 256;
+const base64codes = new Uint8Array(l);
+for (let i = 0; i < l; ++i) {
+  base64codes[i] = 255; // invalid character
+}
+base64abc.forEach((char, index) => {
+  base64codes[char.charCodeAt(0)] = index;
+});
+base64codes["=".charCodeAt(0)] = 0; // ignored anyway, so we just need to prevent an error
+
+function getBase64Code(charCode: number): number {
+  if (charCode >= base64codes.length) {
+    throw new Error("Unable to parse base64 string.");
+  }
+  const code = base64codes[charCode];
+  if (code === 255) {
+    throw new Error("Unable to parse base64 string.");
+  }
+  return code;
+}
+
+export function bytesToBase64(bytes: Uint8Array) {
+  let result = "",
+    i: number;
+  const l = bytes.length;
+  for (i = 2; i < l; i += 3) {
+    result += base64abc[bytes[i - 2] >> 2];
+    result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+    result += base64abc[((bytes[i - 1] & 0x0f) << 2) | (bytes[i] >> 6)];
+    result += base64abc[bytes[i] & 0x3f];
+  }
+  if (i === l + 1) {
+    // 1 octet yet to write
+    result += base64abc[bytes[i - 2] >> 2];
+    result += base64abc[(bytes[i - 2] & 0x03) << 4];
+    result += "==";
+  }
+  if (i === l) {
+    // 2 octets yet to write
+    result += base64abc[bytes[i - 2] >> 2];
+    result += base64abc[((bytes[i - 2] & 0x03) << 4) | (bytes[i - 1] >> 4)];
+    result += base64abc[(bytes[i - 1] & 0x0f) << 2];
+    result += "=";
+  }
+  return result;
+}
+
+export function base64ToBytes(str: string) {
+  if (str.length % 4 !== 0) {
+    throw new Error("Unable to parse base64 string.");
+  }
+  const index = str.indexOf("=");
+  if (index !== -1 && index < str.length - 2) {
+    throw new Error("Unable to parse base64 string.");
+  }
+  const missingOctets = str.endsWith("==") ? 2 : str.endsWith("=") ? 1 : 0;
+  const n = str.length;
+  const result = new Uint8Array(3 * (n / 4));
+  let buffer: number;
+  for (let i = 0, j = 0; i < n; i += 4, j += 3) {
+    buffer =
+      (getBase64Code(str.charCodeAt(i)) << 18) |
+      (getBase64Code(str.charCodeAt(i + 1)) << 12) |
+      (getBase64Code(str.charCodeAt(i + 2)) << 6) |
+      getBase64Code(str.charCodeAt(i + 3));
+    result[j] = buffer >> 16;
+    result[j + 1] = (buffer >> 8) & 0xff;
+    result[j + 2] = buffer & 0xff;
+  }
+  return result.subarray(0, result.length - missingOctets);
+}
+
+export function base64encode(str: string, encoder = new TextEncoder()): string {
+  return bytesToBase64(encoder.encode(str));
+}
+
+export function base64decode(str: string, decoder = new TextDecoder()): string {
+  return decoder.decode(base64ToBytes(str));
+}
+
+export function zipEncode(str: string): string {
+  return bytesToBase64(gzip(str));
+}
+export function decodeUnzip(str: string): string {
+  return ungzip(base64ToBytes(str), { to: "string" });
+}