Explorar o código

Add `SCENE_INIT` broadcast type for new user (#1095)

Sanghyeon Lee %!s(int64=5) %!d(string=hai) anos
pai
achega
763735ac84
Modificáronse 2 ficheiros con 103 adicións e 86 borrados
  1. 97 86
      src/components/App.tsx
  2. 6 0
      src/data/index.ts

+ 97 - 86
src/components/App.tsx

@@ -502,7 +502,7 @@ export class App extends React.Component<any, AppState> {
       getDrawingVersion(globalSceneState.getAllElements()) >
       this.lastBroadcastedOrReceivedSceneVersion
     ) {
-      this.broadcastSceneUpdate();
+      this.broadcastScene("SCENE_UPDATE");
     }
 
     history.record(this.state, globalSceneState.getAllElements());
@@ -708,6 +708,92 @@ export class App extends React.Component<any, AppState> {
       //  initial SCENE_UPDATE message
       const initializationTimer = setTimeout(initialize, 5000);
 
+      const updateScene = (
+        decryptedData: SocketUpdateDataSource["SCENE_INIT" | "SCENE_UPDATE"],
+      ) => {
+        const { elements: remoteElements } = decryptedData.payload;
+        const restoredState = restore(remoteElements || [], null, {
+          scrollToContent: true,
+        });
+        // Perform reconciliation - in collaboration, if we encounter
+        // elements with more staler versions than ours, ignore them
+        // and keep ours.
+        if (
+          globalSceneState.getAllElements() == null ||
+          globalSceneState.getAllElements().length === 0
+        ) {
+          globalSceneState.replaceAllElements(restoredState.elements);
+        } else {
+          // create a map of ids so we don't have to iterate
+          // over the array more than once.
+          const localElementMap = getElementMap(
+            globalSceneState.getAllElements(),
+          );
+
+          // Reconcile
+          globalSceneState.replaceAllElements(
+            restoredState.elements
+              .reduce((elements, element) => {
+                // if the remote element references one that's currently
+                //  edited on local, skip it (it'll be added in the next
+                //  step)
+                if (
+                  element.id === this.state.editingElement?.id ||
+                  element.id === this.state.resizingElement?.id ||
+                  element.id === this.state.draggingElement?.id
+                ) {
+                  return elements;
+                }
+
+                if (
+                  localElementMap.hasOwnProperty(element.id) &&
+                  localElementMap[element.id].version > element.version
+                ) {
+                  elements.push(localElementMap[element.id]);
+                  delete localElementMap[element.id];
+                } else if (
+                  localElementMap.hasOwnProperty(element.id) &&
+                  localElementMap[element.id].version === element.version &&
+                  localElementMap[element.id].versionNonce !==
+                    element.versionNonce
+                ) {
+                  // resolve conflicting edits deterministically by taking the one with the lowest versionNonce
+                  if (
+                    localElementMap[element.id].versionNonce <
+                    element.versionNonce
+                  ) {
+                    elements.push(localElementMap[element.id]);
+                  } else {
+                    // it should be highly unlikely that the two versionNonces are the same. if we are
+                    // really worried about this, we can replace the versionNonce with the socket id.
+                    elements.push(element);
+                  }
+                  delete localElementMap[element.id];
+                } else {
+                  elements.push(element);
+                  delete localElementMap[element.id];
+                }
+
+                return elements;
+              }, [] as Mutable<typeof restoredState.elements>)
+              // add local elements that weren't deleted or on remote
+              .concat(...Object.values(localElementMap)),
+          );
+        }
+        this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
+          globalSceneState.getAllElements(),
+        );
+
+        // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
+        // when we receive any messages from another peer. This UX can be pretty rough -- if you
+        // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
+        // right now we think this is the right tradeoff.
+        history.clear();
+        if (this.socketInitialized === false) {
+          initialize();
+        }
+      };
+
       this.socket = socketIOClient(SOCKET_SERVER);
       this.roomID = roomMatch[1];
       this.roomKey = roomMatch[2];
@@ -729,90 +815,15 @@ export class App extends React.Component<any, AppState> {
           switch (decryptedData.type) {
             case "INVALID_RESPONSE":
               return;
-            case "SCENE_UPDATE": {
-              const { elements: remoteElements } = decryptedData.payload;
-              const restoredState = restore(remoteElements || [], null, {
-                scrollToContent: true,
-              });
-              // Perform reconciliation - in collaboration, if we encounter
-              // elements with more staler versions than ours, ignore them
-              // and keep ours.
-              if (
-                globalSceneState.getAllElements() == null ||
-                globalSceneState.getAllElements().length === 0
-              ) {
-                globalSceneState.replaceAllElements(restoredState.elements);
-              } else {
-                // create a map of ids so we don't have to iterate
-                // over the array more than once.
-                const localElementMap = getElementMap(
-                  globalSceneState.getAllElements(),
-                );
-
-                // Reconcile
-                globalSceneState.replaceAllElements(
-                  restoredState.elements
-                    .reduce((elements, element) => {
-                      // if the remote element references one that's currently
-                      //  edited on local, skip it (it'll be added in the next
-                      //  step)
-                      if (
-                        element.id === this.state.editingElement?.id ||
-                        element.id === this.state.resizingElement?.id ||
-                        element.id === this.state.draggingElement?.id
-                      ) {
-                        return elements;
-                      }
-
-                      if (
-                        localElementMap.hasOwnProperty(element.id) &&
-                        localElementMap[element.id].version > element.version
-                      ) {
-                        elements.push(localElementMap[element.id]);
-                        delete localElementMap[element.id];
-                      } else if (
-                        localElementMap.hasOwnProperty(element.id) &&
-                        localElementMap[element.id].version ===
-                          element.version &&
-                        localElementMap[element.id].versionNonce !==
-                          element.versionNonce
-                      ) {
-                        // resolve conflicting edits deterministically by taking the one with the lowest versionNonce
-                        if (
-                          localElementMap[element.id].versionNonce <
-                          element.versionNonce
-                        ) {
-                          elements.push(localElementMap[element.id]);
-                        } else {
-                          // it should be highly unlikely that the two versionNonces are the same. if we are
-                          // really worried about this, we can replace the versionNonce with the socket id.
-                          elements.push(element);
-                        }
-                        delete localElementMap[element.id];
-                      } else {
-                        elements.push(element);
-                        delete localElementMap[element.id];
-                      }
-
-                      return elements;
-                    }, [] as Mutable<typeof restoredState.elements>)
-                    // add local elements that weren't deleted or on remote
-                    .concat(...Object.values(localElementMap)),
-                );
-              }
-              this.lastBroadcastedOrReceivedSceneVersion = getDrawingVersion(
-                globalSceneState.getAllElements(),
-              );
-              // We haven't yet implemented multiplayer undo functionality, so we clear the undo stack
-              // when we receive any messages from another peer. This UX can be pretty rough -- if you
-              // undo, a user makes a change, and then try to redo, your element(s) will be lost. However,
-              // right now we think this is the right tradeoff.
-              history.clear();
-              if (this.socketInitialized === false) {
-                initialize();
+            case "SCENE_INIT": {
+              if (!this.socketInitialized) {
+                updateScene(decryptedData);
               }
               break;
             }
+            case "SCENE_UPDATE":
+              updateScene(decryptedData);
+              break;
             case "MOUSE_LOCATION": {
               const { socketID, pointerCoords } = decryptedData.payload;
               this.setState((state) => {
@@ -852,7 +863,7 @@ export class App extends React.Component<any, AppState> {
         });
       });
       this.socket.on("new-user", async (socketID: string) => {
-        this.broadcastSceneUpdate();
+        this.broadcastScene("SCENE_INIT");
       });
 
       this.setState({
@@ -879,9 +890,9 @@ export class App extends React.Component<any, AppState> {
     }
   };
 
-  private broadcastSceneUpdate = () => {
-    const data: SocketUpdateDataSource["SCENE_UPDATE"] = {
-      type: "SCENE_UPDATE",
+  private broadcastScene = (sceneType: "SCENE_INIT" | "SCENE_UPDATE") => {
+    const data: SocketUpdateDataSource[typeof sceneType] = {
+      type: sceneType,
       payload: {
         elements: getSyncableElements(globalSceneState.getAllElements()),
       },

+ 6 - 0
src/data/index.ts

@@ -32,6 +32,12 @@ export type EncryptedData = {
 };
 
 export type SocketUpdateDataSource = {
+  SCENE_INIT: {
+    type: "SCENE_INIT";
+    payload: {
+      elements: readonly ExcalidrawElement[];
+    };
+  };
   SCENE_UPDATE: {
     type: "SCENE_UPDATE";
     payload: {