فهرست منبع

save room to firebase on unload or portal close (#2207)

* save on unload or portal close

* align naming
David Luzar 4 سال پیش
والد
کامیت
d18a72c879
2فایلهای تغییر یافته به همراه76 افزوده شده و 24 حذف شده
  1. 37 16
      src/components/App.tsx
  2. 39 8
      src/data/firebase.ts

+ 37 - 16
src/components/App.tsx

@@ -176,7 +176,11 @@ import {
 import { MaybeTransformHandleType } from "../element/transformHandles";
 import { renderSpreadsheet } from "../charts";
 import { isValidLibrary } from "../data/json";
-import { loadFromFirebase, saveToFirebase } from "../data/firebase";
+import {
+  loadFromFirebase,
+  saveToFirebase,
+  isSavedToFirebase,
+} from "../data/firebase";
 
 /**
  * @param func handler taking at most single parameter (event).
@@ -469,7 +473,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       return false;
     }
 
-    const roomId = roomMatch[1];
+    const roomID = roomMatch[1];
 
     let collabForceLoadFlag;
     try {
@@ -488,7 +492,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         );
         // if loading same room as the one previously unloaded within 15sec
         //  force reload without prompting
-        if (previousRoom === roomId && Date.now() - timestamp < 15000) {
+        if (previousRoom === roomID && Date.now() - timestamp < 15000) {
           return true;
         }
       } catch {}
@@ -784,7 +788,17 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         );
       } catch {}
     }
-    if (this.state.isCollaborating && this.scene.getElements().length > 0) {
+    const syncableElements = getSyncableElements(
+      this.scene.getElementsIncludingDeleted(),
+    );
+    if (
+      this.state.isCollaborating &&
+      !isSavedToFirebase(this.portal, syncableElements)
+    ) {
+      // this won't run in time if user decides to leave the site, but
+      //  the purpose is to run in immediately after user decides to stay
+      this.saveCollabRoomToFirebase(syncableElements);
+
       event.preventDefault();
       // NOTE: modern browsers no longer allow showing a custom message here
       event.returnValue = "";
@@ -1182,6 +1196,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
   };
 
   closePortal = () => {
+    this.saveCollabRoomToFirebase();
     window.history.pushState({}, "Excalidraw", window.location.origin);
     this.destroySocketClient();
   };
@@ -1227,8 +1242,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     }
     const roomMatch = getCollaborationLinkData(window.location.href);
     if (roomMatch) {
-      const roomId = roomMatch[1];
-      const roomSecret = roomMatch[2];
+      const roomID = roomMatch[1];
+      const roomKey = roomMatch[2];
 
       const initialize = () => {
         this.portal.socketInitialized = true;
@@ -1358,7 +1373,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
         /* webpackChunkName: "socketIoClient" */ "socket.io-client"
       );
 
-      this.portal.open(socketIOClient(SOCKET_SERVER), roomId, roomSecret);
+      this.portal.open(socketIOClient(SOCKET_SERVER), roomID, roomKey);
 
       // All socket listeners are moving to Portal
       this.portal.socket!.on(
@@ -1430,7 +1445,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       });
 
       try {
-        const elements = await loadFromFirebase(roomId, roomSecret);
+        const elements = await loadFromFirebase(roomID, roomKey);
         if (elements) {
           updateScene(
             { type: "SCENE_UPDATE", payload: { elements } },
@@ -1484,6 +1499,18 @@ class App extends React.Component<ExcalidrawProps, AppState> {
     }
   };
 
+  saveCollabRoomToFirebase = async (
+    syncableElements: ExcalidrawElement[] = getSyncableElements(
+      this.scene.getElementsIncludingDeleted(),
+    ),
+  ) => {
+    try {
+      await saveToFirebase(this.portal, syncableElements);
+    } catch (error) {
+      console.error(error);
+    }
+  };
+
   // maybe should move to Portal
   broadcastScene = async (
     sceneType: SCENE.INIT | SCENE.UPDATE,
@@ -1530,16 +1557,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
       data as SocketUpdateData,
     );
 
-    if (syncAll && this.portal.roomID && this.portal.roomKey) {
+    if (syncAll && this.state.isCollaborating) {
       await Promise.all([
         broadcastPromise,
-        saveToFirebase(
-          this.portal.roomID,
-          this.portal.roomKey,
-          syncableElements,
-        ).catch((e) => {
-          console.error(e);
-        }),
+        this.saveCollabRoomToFirebase(syncableElements),
       ]);
     } else {
       await broadcastPromise;

+ 39 - 8
src/data/firebase.ts

@@ -1,6 +1,7 @@
 import { createIV, getImportedKey } from "./index";
 import { ExcalidrawElement } from "../element/types";
 import { getSceneVersion } from "../element";
+import Portal from "../components/Portal";
 
 let firebasePromise: Promise<typeof import("firebase/app")> | null = null;
 
@@ -69,14 +70,40 @@ async function decryptElements(
   return JSON.parse(decodedData);
 }
 
+const firebaseSceneVersionCache = new WeakMap<SocketIOClient.Socket, number>();
+
+export const isSavedToFirebase = (
+  portal: Portal,
+  elements: readonly ExcalidrawElement[],
+): boolean => {
+  if (portal.socket && portal.roomID && portal.roomKey) {
+    const sceneVersion = getSceneVersion(elements);
+    return firebaseSceneVersionCache.get(portal.socket) === sceneVersion;
+  }
+  // if no room exists, consider the room saved so that we don't unnecessarily
+  //  prevent unload (there's nothing we could do at that point anyway)
+  return true;
+};
+
 export async function saveToFirebase(
-  roomId: string,
-  roomSecret: string,
+  portal: Portal,
   elements: readonly ExcalidrawElement[],
 ) {
+  const { roomID, roomKey, socket } = portal;
+  if (
+    // if no room exists, consider the room saved because there's nothing we can
+    //  do at this point
+    !roomID ||
+    !roomKey ||
+    !socket ||
+    isSavedToFirebase(portal, elements)
+  ) {
+    return true;
+  }
+
   const firebase = await getFirebase();
   const sceneVersion = getSceneVersion(elements);
-  const { ciphertext, iv } = await encryptElements(roomSecret, elements);
+  const { ciphertext, iv } = await encryptElements(roomKey, elements);
 
   const nextDocData = {
     sceneVersion,
@@ -87,7 +114,7 @@ export async function saveToFirebase(
   } as FirebaseStoredScene;
 
   const db = firebase.firestore();
-  const docRef = db.collection("scenes").doc(roomId);
+  const docRef = db.collection("scenes").doc(roomID);
   const didUpdate = await db.runTransaction(async (transaction) => {
     const doc = await transaction.get(docRef);
     if (!doc.exists) {
@@ -104,17 +131,21 @@ export async function saveToFirebase(
     return true;
   });
 
+  if (didUpdate) {
+    firebaseSceneVersionCache.set(socket, sceneVersion);
+  }
+
   return didUpdate;
 }
 
 export async function loadFromFirebase(
-  roomId: string,
-  roomSecret: string,
+  roomID: string,
+  roomKey: string,
 ): Promise<readonly ExcalidrawElement[] | null> {
   const firebase = await getFirebase();
   const db = firebase.firestore();
 
-  const docRef = db.collection("scenes").doc(roomId);
+  const docRef = db.collection("scenes").doc(roomID);
   const doc = await docRef.get();
   if (!doc.exists) {
     return null;
@@ -122,6 +153,6 @@ export async function loadFromFirebase(
   const storedScene = doc.data() as FirebaseStoredScene;
   const ciphertext = storedScene.ciphertext.toUint8Array();
   const iv = storedScene.iv.toUint8Array();
-  const plaintext = await decryptElements(roomSecret, iv, ciphertext);
+  const plaintext = await decryptElements(roomKey, iv, ciphertext);
   return plaintext;
 }